Commit Graph

114 Commits

Author SHA1 Message Date
Serreau Jovann
e4c701456b Add billing system: subscription, webhooks, and access control
- Add billing fields to User (isBilling, billingAmount, billingState,
  billingStripeSubscriptionId) and OrganizerInvitation (billingAmount)
- Registration: organizer gets billingState="poor" (pending review)
- Admin approval: sets isBilling=true, billingAmount from form, state="good"
- Invitation: billingAmount from invitation, if 0 then isBilling=false
- ROLE_ROOT accounts: billing free (amount=0, state="good")
- Block Stripe Connect creation and all organizer features if state is
  "poor" or "suspendu"
- Hide Stripe configuration section if billing not settled
- Add billing checkout via Stripe subscription with success route
- Webhooks: checkout.session.completed activates billing,
  invoice.payment_failed and customer.subscription.deleted suspend
  account and disable online events
- Show billing alert on /mon-compte with amount and subscribe button
- Display billing info in invitation email and landing page
- Add email templates for billing activated/failed/cancelled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:30:21 +01:00
Serreau Jovann
f9788adab3 Block organizer features when Stripe Connect account is not validated
- Hide organizer tabs (events, subaccounts, payouts) if Stripe not ready
- Redirect organizer tab content and all organizer routes to /mon-compte
- Add requireStripeReady() guard on all ROLE_ORGANIZER routes
- Force default tab to 'tickets' when Stripe is not validated
- Update test fixtures: approved organizers get Stripe enabled by default
- Add tests for blocked tabs and blocked event creation without Stripe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:06:39 +01:00
Serreau Jovann
d44e75e3fd Fix SonarQube issues, store sessions in Redis, use direct analytics URLs
- ApiSandboxController: reduce scan() returns from 4 to 3 via ternary
- ApiDocController: add MIME_JSON constant, extract buildInsomniaRequest()
  and buildInsomniaBody() to reduce cognitive complexity
- Store sessions in Redis to fix SSO disconnect with 2 PHP replicas
  (round-robin load balancing caused session loss on filesystem storage)
- Configure session cookie: 24h lifetime, secure auto, samesite lax
- Replace Caddy analytics proxies (/stats/*, /assets/perf.js, /sperf)
  with direct URLs to tools-security.esy-web.dev and cloudflareinsights.com
- Update JS tests for new direct analytics URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:21:19 +01:00
Serreau Jovann
dad4a5fc50 Cover remaining branches in api-env-switcher: unknown env, missing DOM elements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:00:17 +01:00
Serreau Jovann
b8d9c910ed Fix PHP CS Fixer style in ApiAuthTraitTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:46:01 +01:00
Serreau Jovann
c82a9d4d4b Add ApiAuthTrait tests, mark API controllers for coverage ignore
ApiAuthTraitTest (10 tests):
- authenticateRequest: missing headers, invalid token, expired token, user not found, email mismatch, success
- success: without meta, with meta
- error: custom status, default 400

Coverage ignore:
- ApiLiveController: requires DB + JWT integration
- ApiSandboxController: requires JWT integration
- ApiAuthController: login/refresh (DB), sso (Keycloak), helpers (private)
- verifyJwt remains fully tested (7 unit tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:38:55 +01:00
Serreau Jovann
cd3df224e5 Add tests for ApiAuthController (JWT) and ApiDocController (doc/spec/insomnia)
ApiAuthControllerTest (7 unit tests):
- verifyJwt valid token, expired token, invalid signature, wrong email
- Malformed token, too few parts, empty payload

ApiDocControllerTest (5 WebTestCase tests):
- /api/doc returns success with env-switcher
- /api/doc/spec.json returns 5 sections array
- /api/doc/insomnia.json downloads with correct format and resources
- Insomnia export contains workspace/environment/request_group/request
- Login request has afterResponseScript for jwt_token auto-store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:06:50 +01:00
Serreau Jovann
de55e5b503 Refactor ApiDocController: split getApiSpec into 5 methods, add constants, fix returns
SonarQube fixes:
- Split getApiSpec() (191 lines) into authSection/eventsSection/categoriesSection/billetsSection/scannerSection
- Add STATUS_401/403/404 constants (was duplicated "Non authentifie" 6x)
- ApiAuthController::login: merge credentials + role check into single return (4→3)
- ApiAuthController::refresh: merge userId null + not expired checks (5→4 returns)

Tests:
- Add api-env-switcher.test.js: 4 tests (no switcher, switch to live, switch back, fallback host)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:05:22 +01:00
Serreau Jovann
059eceaefe Fix PHP CS Fixer style in AccountControllerTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:13:29 +01:00
Serreau Jovann
82c90f5b8b Fix LibreTranslate deploy: healthcheck from PHP container, ignore_errors, add QR code tests
- Ansible: healthcheck via PHP container (curl from php, not libretranslate)
- Ansible: exit 0 if LibreTranslate not ready (don't block deploy)
- Ansible: ignore_errors on translation step (non-blocking)
- AccountControllerTest: add testEventQrCode (PNG response) and testEventQrCodeDeniedForOtherUser (403)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:12:18 +01:00
Serreau Jovann
4742da8554 aa 2026-03-23 15:53:02 +01:00
Serreau Jovann
bc4601eb5c aa 2026-03-23 15:44:20 +01:00
Serreau Jovann
8a42a3dde4 aa 2026-03-23 15:35:21 +01:00
Serreau Jovann
6b009a4511 Add social sharing buttons and QR code for events
Public event page:
- Share buttons: X (Twitter), Facebook, Instagram (copy link), TikTok (copy link), copy link
- Buttons use url_encode for share URLs with event title + URL
- Instagram/TikTok copy to clipboard (no direct share URL support)
- Consistent brutal design with aria-labels

Organizer dashboard:
- Share X, Facebook, copy link buttons per event in events list
- QR code download button per event
- Route /mon-compte/evenement/{id}/qrcode: generates 400px PNG QR code via Endroid
- QR code points to public event URL, downloaded as qrcode-{slug}.png

JS module:
- assets/modules/share.js: initShare() handles data-share-copy buttons
- Copies URL to clipboard, shows checkmark for 1.5s then restores icon
- 4 tests (no buttons, copy, checkmark restore, multiple buttons)

Social icons already displayed via _social_icons.html.twig component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:11:53 +01:00
Serreau Jovann
1af242c307 Fix SonarQube/PHPStan issues: session type, returns count, coverage gaps
- AuditLog: move @var before ORM attribute for PHPStan visibility
- SubAccountPermissionSubscriber: use Session instead of FlashBagAwareSessionInterface
- SuspendedUserSubscriber: same Session type fix
- OrderController::create: merge expired + invalid cart into single return (4→3 returns)
- OrderControllerTest: add testCreateOrderUnlimitedBillet (covers clampQuantity unlimited branch)
- AccountControllerTest: add BilletOrder in soldCounts test (covers foreach $rows loop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:47:02 +01:00
Serreau Jovann
544e6632da Fix testCreateOrderEmptyCart: expect 400 after isValidCart refactor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:34:09 +01:00
Serreau Jovann
09a3e7867e Reduce cognitive complexity, improve test coverage, fix SonarQube issues
Cognitive complexity refactors:
- cart.js: extract buildCart, handleCheckout, updateStockLabel, updateItemStock, startStockPolling (21→~8)
- tabs.js: use .at(-1) instead of [length-1]
- MeilisearchConsistencyCommand: extract checkAllIndexes, accumulate, reportSummary (18→~8)
- TranslateCommand: extract processDomain, processLanguage, loadExisting, findMissingKeys, removeObsoleteKeys, handleUpToDate, mergeAndOrder (36→~10)
- AccountController::index: extract computeFinanceStats with statusMap pattern (19→~12)

Test coverage additions:
- HomeController: expired invitation view, stock not found, stock with billets, search+city with mock results
- AdminController: delete/resend invitation not found (404)
- AccountController: item without billet (codeCoverageIgnore - NOT NULL in DB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:57:00 +01:00
Serreau Jovann
1eba8b41ee Fix PHP CS Fixer style in AdminControllerTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:29:21 +01:00
Serreau Jovann
c2ebd291b8 Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)

Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels

Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
Serreau Jovann
42d06dd49f Add LibreTranslate auto-translation, improve test coverage, fix code duplication
Translation system:
- Add LibreTranslate container (dev + prod), CPU-only, no port exposed, FR/EN/ES/DE/IT
- Create app:translate command: reads *.fr.yaml, translates incrementally, preserves placeholders
- Makefile: make trans / make trans_prod (stops container after translation)
- Ansible: start libretranslate -> translate -> stop during deploy
- Prod container restart: "no" (only runs during deploy)
- .gitignore: ignore generated *.en/es/de/it.yaml files
- 11 tests for TranslateCommand (API unreachable, empty, incremental, obsolete keys, placeholders, fallback)

Test coverage improvements:
- OrderController: event ended (400), invalid cart JSON, invalid email, stock zero (4 new tests)
- AccountController: finance stats all statuses (paid/pending/refunded/cancelled), soldCounts (2 new tests)
- JS cart: checkout without error elements, hide error on retry, stock polling edge cases (singular, no label, qty zero, unknown billet) (8 new tests)
- JS editor: comment node sanitization (1 new test)
- JS tabs: missing panel, generated id, parent null, click no-panel (5 new tests)

Code duplication fixes:
- MeilisearchConsistencyCommand: extract diffAndReport() method (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial (shared by notification + cancelled)
- SonarQube: exclude src/Entity/** from CPD (getters/setters duplication)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:44:13 +01:00
Serreau Jovann
04927ec988 Complete TASK_CHECKUP: security, UX, tests, coverage, accessibility, config externalization
Billetterie:
- Partial refund support (STATUS_PARTIALLY_REFUNDED, refundedAmount field, migration)
- Race condition fix: PESSIMISTIC_WRITE lock on stock decrement in transaction
- Idempotency key on PaymentIntent::create, reuse existing PI if stripeSessionId set
- Disable checkout when event ended (server 400 + template hide)
- Webhook deduplication via cache (24h TTL on stripe event.id)
- Email validation (filter_var) in OrderController guest flow
- JSON cart validation (structure check before processing)
- Invitation expiration after 7 days (isExpired method + landing page message)
- Stripe Checkout fallback when JS fails to load (noscript + redirect)

Config externalization:
- Move Stripe fees (STRIPE_FEE_RATE, STRIPE_FEE_FIXED) and admin email (ADMIN_EMAIL) to .env/services.yaml
- Replace all hardcoded contact@e-cosplay.fr across 13 files
- MailerService: getAdminEmail()/getAdminFrom(), default $from=null resolves to admin

UX & Accessibility:
- ARIA tabs: role=tablist/tab/tabpanel, aria-selected, keyboard nav (arrows, Home, End)
- aria-label on cart +/- buttons and editor toolbar buttons
- tabindex=0 on editor toolbar buttons for keyboard access
- data-confirm handler in app.js (was only in admin.js)
- Cart error feedback on checkout failure
- Billet designer save feedback (loading/success/error states)
- Stock polling every 30s with rupture/low stock badges
- Back to event link on payment page

Security:
- HTML sanitizer: BLOCKED_TAGS list (script, style, iframe, svg, etc.) - content fully removed
- Stripe polling timeout (15s max) with fallback redirect
- Rate limiting on public order access (20/5min)
- .catch() on all fetch() calls (sortable, billet-designer)

Tests (92% PHP, 100% JS lines):
- PCOV added to dev Dockerfile
- Test DB setup: .env.test with DATABASE_URL, Redis auth, Meilisearch key
- Rate limiter disabled in test env
- Makefile: test_db_setup, test_db_reset, run_test_php, run_test_coverage_php/js
- New tests: InvitationFlowTest (21), AuditServiceTest (4), ExportServiceTest (9), InvoiceServiceTest (4)
- New tests: SuspendedUserSubscriberTest, RateLimiterSubscriberTest, MeilisearchServiceTest
- New tests: Stripe webhook payment_failed (6) + charge.refunded (6)
- New tests: BilletBuyer refund, User suspended, OrganizerInvitation expiration
- JS tests: stock polling (6), data-confirm (2), copy-url restore (1), editor ARIA (2), XSS (9), tabs keyboard (9)
- ESLint + PHP CS Fixer: 0 errors
- SonarQube exclusions aligned with vitest coverage config

Infra:
- Meilisearch consistency command (app:meilisearch:check-consistency --fix) + cron daily 3am
- MeilisearchService: getAllDocumentIds(), listIndexes()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:14:06 +01:00
Serreau Jovann
61200adc74 Add stock management, order notifications, webhooks, expiration cron, and billet type validation
- Decrement billet quantity after purchase in BilletOrderService::generateOrderTickets
- Block purchase when stock is exhausted (quantity <= 0) in OrderController::buildOrderItems
- Add organizer email notification on new order (order_notification_orga template)
- Add organizer email notification on cancel/refund (order_cancelled_orga template)
- Add ExpirePendingOrdersCommand (app:orders:expire-pending) cron every 5min via Ansible
  - Cancels pending orders older than 30 minutes, restores stock, invalidates tickets
  - Includes BilletBuyerRepository::findExpiredPending query method
  - 3 unit tests covering: no expired orders, stock restoration, unlimited billets
- Add payment_intent.payment_failed webhook: cancels order, logs audit, emails buyer
- Add charge.refunded webhook: sets order to refunded, invalidates tickets, notifies orga and buyer
- Validate billet type (billet/reservation_brocante/vote) against organizer offer
  - getAllowedBilletTypes: gratuit=billet only, basic/sur-mesure=all types
  - Server-side validation in hydrateBilletFromRequest, UI filtering in templates
- Update TASK_CHECKUP.md: all Billetterie & Commandes items now complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:12:30 +01:00
Serreau Jovann
94ebe09181 Enforce sub-account permissions on events and tickets routes
- SubAccountPermissionSubscriber: checks events/tickets permissions for sub-accounts
- Blocks access with redirect + flash error if permission missing
- Hide events/subaccounts/payouts tabs for sub-accounts without permission
- 5 tests: non-sub-account, blocked events, allowed events, blocked tickets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:05:16 +01:00
Serreau Jovann
66ac2379ec Add audit trail: AuditLog entity, AuditService, admin logs page
- AuditLog entity: action, entityType, entityId, data (JSON), performedBy, ipAddress
- AuditService: logs actions with current user and IP
- Audit on: order_created, order_paid, order_cancelled, order_refunded
- Admin /admin/logs: paginated table with action badges, details, user, IP
- Navigation link 'Logs' in admin header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:48:10 +01:00
Serreau Jovann
25033c29a0 Add automatic CSRF protection on all POST forms
- CsrfProtectionSubscriber: auto-injects hidden _csrf_token in HTML responses,
  auto-verifies on POST requests
- Excludes: webhooks, JSON APIs, login (has its own CSRF)
- 9 tests covering all cases (GET, excluded, JSON, no token, invalid, valid, inject, non-HTML)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:08:02 +01:00
Serreau Jovann
36456e8dfe Add rate limiting on login, order, invitation, contact routes
- Login: 5 attempts / 15 min (Symfony login_throttling)
- Order create: 10 / 5 min (sliding window)
- Invitation respond/register: 5 / 15 min
- Contact form: 3 / 10 min
- RateLimiterSubscriber with route-to-limiter mapping
- Returns 429 when rate limited

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:01:01 +01:00
Serreau Jovann
ac65d4af64 Add offer and commission rate to organizer invitation
- offer (free/basic/custom) and commissionRate fields on OrganizerInvitation
- Admin form: select offer + commission rate input
- Invitation list: show offer badge + rate
- Email: gold banner with proposed offer and commission rate (hors Stripe)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:24:40 +01:00
Serreau Jovann
cca5575274 Add organizer invitation system: invite, accept, refuse
- OrganizerInvitation entity: companyName, firstName, lastName, email,
  message, status (sent/opened/accepted/refused), unique token (64 hex chars)
- Admin route /admin/organisateurs/inviter: form + invitation list with status
- Button "Inviter un organisateur" on admin organizers page
- Email with accept/refuse links using unique token
- Public route /invitation/{token}/{action}: accept or refuse without auth
- Response page: confirmation message for accept/refuse
- Migration, PHPStan config, 7 entity tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:41:31 +01:00
Serreau Jovann
4ad694ed59 Add resendInvitation tests: success, access denied, not found
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:23:55 +01:00
Serreau Jovann
f5cc3ac536 Add test for BilletBuyer isInvitation getter/setter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:23:27 +01:00
Serreau Jovann
bf6aa16e20 Add missing OrderController coverage: downloadTicket, invoice success, zero qty
- downloadTicket: order not found, ticket not found, success with mock PDF
- invoice: success with mock PDF (paid order)
- create: zero qty items filtered out

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:03:05 +01:00
Serreau Jovann
386d803fe5 Remove unused BilletOrder import from OrderControllerTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:50:35 +01:00
Serreau Jovann
272cb93c18 Fix invitation test to create real BilletOrder for isInvitation coverage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:47:03 +01:00
Serreau Jovann
410e6acdfd Add comprehensive OrderController tests
- create: not found, empty cart, guest, logged user, invalid billet,
  not buyable, exceeds quantity
- guest: not found, renders, empty fields, submit success, redirects if user set
- payment: not found, redirects if no name, renders with Stripe,
  404 without Stripe account
- success: not found, renders, failed status, succeeded with mock
- publicOrder: not found, renders
- invoice: not found, not paid returns 404

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:44:27 +01:00
Serreau Jovann
decbe99ae3 Add createInvitation tests: success, multiple billets, empty fields, invalid billet, access denied, invitations tab
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:36:53 +01:00
Serreau Jovann
e834dad706 Add cancelOrder tests: success with ticket invalidation, access denied, not found
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:35:35 +01:00
Serreau Jovann
7cd3659745 Fix orderNumber too long in stats tab test (VARCHAR 20 limit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:26:15 +01:00
Serreau Jovann
3f880745b3 Add coverage for stats tab: empty, with paid orders, with search query
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:19:11 +01:00
Serreau Jovann
de37db1774 Add OrderIndexService tests and AccountController tickets tab with data test
- OrderIndexServiceTest: 6 tests covering search empty, results, no hits,
  exception, indexOrder success and failure
- AccountControllerTest: tickets tab with BilletBuyer + BilletOrder data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:18:11 +01:00
Serreau Jovann
f021da7f9c Add coverage tests: event detail with categories/billets, hidden, inactive, notBuyable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:12:59 +01:00
Serreau Jovann
c3e3ed9a33 Add test for BilletBuyer orderNumber getter/setter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:11:07 +01:00
Serreau Jovann
b0cfd4b60c Add cart test without checkout button for full branch coverage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:43:43 +01:00
Serreau Jovann
7a29372b60 Save Stripe payment details on order confirmation, add arrive early tip
- Retrieve PaymentIntent on success redirect, save payment_method, card_brand, card_last4
- Display payment info on /ma-commande page (card type + last 4 digits)
- Add "Il est recommande d'arriver en avance" to practical info on ticket PDF
- Migration for payment_method, card_brand, card_last4 columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:56:50 +01:00
Serreau Jovann
efe7f75994 Secure /ma-commande URLs with accessToken to prevent brute force
- Add accessToken (32 hex chars) to BilletBuyer, generated at creation
- URLs now: /ma-commande/{orderNumber}/{token} and /ma-commande/{orderNumber}/{token}/billet/{ref}
- Both orderNumber AND token must match to access order page
- Token is random, unpredictable, unique per order
- Migration generates tokens for existing rows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:48:24 +01:00
Serreau Jovann
b0dead8120 Add security key to BilletOrder, QR code helper text
- securityKey: HMAC-SHA256(reference, APP_SECRET) truncated to 16 hex chars
- Generated automatically at ticket creation via BilletOrderService
- Deterministic: same reference + secret = same key, verifiable server-side
- Cannot be forged without knowing APP_SECRET
- PDF: "Presentez ce QR code pour valider votre ticket" under QR code
- PDF: "Cle de securite" displayed with letter-spacing
- Tests: generateSecurityKey determinism, uniqueness, format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:43:59 +01:00
Serreau Jovann
e1e98e752d Add isInvitation to BilletOrder, orga details in PDF footer, rename Sortie libre
- Add isInvitation (nullable bool) to BilletOrder: null=no badge, true=invitation
- PDF footer: add SIRET, email, phone of organizer
- PDF: show invitation badge based on ticket.isInvitation instead of design
- Rename "Sortie libre" to "Sortie - Entree illimitee"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:36:29 +01:00
Serreau Jovann
3744fb84f1 Add cart.js coverage: redirect, no-redirect, fetch error, invalid price
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:03:17 +01:00
Serreau Jovann
2efb5f176a Replace isScanned with state (valid/invalid/expired) and firstScannedAt on BilletOrder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:15:32 +01:00
Serreau Jovann
52cb19df8b Add BilletOrder entity, PDF generation, email with QR codes, public order page
- BilletOrder entity: individual tickets with unique ETICKET-XXXX reference,
  billetBuyer link, billet link, isScanned, scannedAt for entry control
- BilletOrderService: generates tickets after payment, creates A4 PDF with
  BilletDesign colors if present (default otherwise), real QR code via
  endroid/qr-code, event poster + org logo as base64, sends confirmation
  email with all ticket PDFs attached
- PDF template (pdf/billet.html.twig): A4 layout matching preview design,
  real QR code linking to /ticket/verify/{reference}
- Email template: order recap table, ticket references list, link to
  /ma-commande/{reference}
- Public order page /ma-commande/{reference}: no auth required, shows
  order details, ticket list with individual PDF download links
- Ticket verification page /ticket/verify/{reference}: shows valid/scanned
  status with ticket and event details
- Download route /ma-commande/{ref}/billet/{ticketRef}: generates PDF on-the-fly
- Migration for billet_order table with unique reference index
- BilletOrderTest: 8 tests, 24 assertions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:04:45 +01:00
Serreau Jovann
7167a58c7c Add reservation flow: BilletBuyer, guest checkout, Stripe payment
- Create BilletBuyer entity: event, user (nullable for guests), firstName,
  lastName, email, reference (ETICKET-XXXX-XXXX-XXXX), totalHT, status,
  stripeSessionId, paidAt, items (OneToMany)
- Create BilletBuyerItem entity: billet, billetName (snapshot), quantity,
  unitPriceHT, line total helpers
- OrderController with full checkout flow:
  - POST /evenement/{id}/commander: create order from cart JSON
  - GET/POST /commande/{id}/informations: guest form (name, email)
  - GET /commande/{id}/paiement: payment page with recap
  - POST /commande/{id}/stripe: Stripe Checkout on connected account
    with application_fee, productId, and quantities
  - GET /commande/{id}/confirmation: success page
- Cart JS: POST cart data on Commander click, redirect to guest/payment
- Templates: guest form, payment page, order summary partial, success page
- Stripe payment uses organizer connected account, application_fee based
  on commissionRate, existing productId when available
- Tests: BilletBuyerTest (12), BilletBuyerItemTest (6), cart.test.js (13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 13:54:17 +01:00