# Implementation Status

Last updated: 2026-06-09

Handoff note: the current MVP architect/runbook document is `docs/mvp-handoff.md`. It consolidates local XAMPP setup, demo seed credentials, public checkout, Stripe readiness, box-office policy, featured placement purchase, refund/correction policy, and manual launch evidence.

## Current State

Zavvion Events is a working local MVP running through the current PHP router with Yii 3-oriented structure, Composer dependencies, MySQL persistence, and clickable public, customer, organiser, scanner, and admin screens.

Implemented MVP workflows:

- DB-backed public event list and event detail APIs
- Session login/logout, hardened session cookies, inactive-user session invalidation, current-user endpoint, local password reset, email verification-ready flows, MFA challenge APIs with hashed OTP storage, admin-configurable phone verification policy, customer phone OTP request/verify workflow with hashed codes, fail-closed production SMS delivery policy, CSRF coverage for state-changing web workflows, and rate limits for auth, account verification, MFA, checkout session creation, seat holds, scanner APIs, and media uploads
- Role and permission defaults, scope-aware permission checks, active organiser-staff enforcement, admin RBAC controls, organiser staff assignment, and permission-gated admin/organiser mutation routes
- Organiser staff assignment now runs through `App\Organiser\OrganiserStaffAssignmentService`, including existing-user validation, allowed organiser role enforcement, staff reactivation, and reusable role-assignment callbacks
- Admin organiser feature controls with global defaults and per-organiser overrides; refunds are disabled for organisers by default as a platform policy gate
- Organiser feature-gate rules are partly extracted into `App\Organiser\OrganiserFeatureGateService`; unknown feature keys now deny by default and denied feature access is audited
- Site branding, approved 16-skin catalog, admin-controlled skin auto-rotation cadence by hours/days/weeks, footer, public notices, disclaimers, media upload metadata, event/site media libraries, soft-remove lifecycle, alt-text editing, and crop/focal metadata storage
- Media upload hardening with MIME checks, image dimension checks, SHA-256 metadata, local heuristic scan status, optional WebP display/thumb variant generation, upload execution blocking under `public/uploads`, persisted scan run history, admin media scan summary, and CLI/admin rescan paths for existing media assets
- Venue floor-plan uploads now use the same hardened media pipeline as event/site images, persist through `venue_media`, appear in organiser venue lists, and can be replaced from the organiser console before seat-map building
- Organiser venue UX now exposes the launch workflow explicitly: create organiser-owned venues, upload an optional floor-plan reference image, generate a selectable row/seat grid, and attach that reusable seat map to an event; event creation now uses an organiser venue dropdown instead of free-text venue entry
- Public event, merchandise, and site-branding media now only expose active, non-deleted uploaded assets with a clean scan status; failed, blocked, infected, archived, or removed local uploads stay visible in admin review views but are withheld from customer-facing APIs
- Server-side media variant generation now applies organiser/admin crop, focal-point zoom, and 90-degree rotation metadata, and regenerates variants when media crop settings are updated
- Media retention operations now detect orphaned uploads, unsafe scan statuses, and old archived assets; admin and CLI paths support dry-run reports, and explicit soft archive hides candidates without deleting files
- Admin operational alerts now persist a durable `system_alerts` feed from system-health signals, expose an admin API for alert snapshots and acknowledgement, and render open/resolved alerts in the admin dashboard and Operations area
- Admin alert notifications now have platform policy controls, a durable `system_alert_notifications` outbox, duplicate cooldown protection, safe local log delivery, production fail-closed behavior, and a future signed HTTP provider path
- Organiser events, venues, ticket types, seat-map generation, event media uploads, and event status controls
- Organiser event creation now runs through `App\Event\EventCreationService`, including organiser-scoped venue validation, default category/venue selection, slug collision handling, event age-policy fields, draft-first publication review, and transactional status persistence
- Organiser event status changes now run through `App\Event\EventStatusUpdateService`, including scoped event lookup, publish-readiness review, explicit publish-blocker responses, and audited status persistence
- Organiser venue creation now runs through `App\Venue\VenueCreationService`, including required venue details, exact ISO country-code validation, deterministic creation timestamps, and audit-ready response data
- Organiser rectangular seat-map generation now runs through `App\SeatMap\SeatMapCreationService`, including organiser-scoped venue resolution, bounded row/seat counts, active seat creation, row labels beyond Z, and transactional map/section/seat persistence
- Reusable seat maps are attached to events through `App\SeatMap\SeatMapAttachService`, including organiser-scoped event/map lookup, venue mismatch protection, event venue backfill, idempotent event-seat creation, and duplicate-safe inserts
- Organiser-created reserved-seat demand blocks now run through `App\SeatMap\EventSeatBlockService`, randomly selecting available event seats, storing synthetic blocks as `event_seats.status = 'blocked'`, exposing immediate organiser UI counts, and releasing only synthetic blocks through the Unblock action
- Merchandise products can now be assigned to all organiser events or selected organiser events through `product_event_assignments`; public event product lookup and checkout stock validation both honour all-event and multi-event product allocations
- Organiser ticket-type creation now runs through `App\Ticket\TicketTypeCreationService`, including age-category validation, free-ticket pricing, min/max order normalisation, atomic inventory snapshot creation, and an initial `ticket_inventory` row for capacity reporting
- Event age policies and age-based ticket setup for adult, child, infant, student, senior, family, carer, companion, VIP, general, and custom ticket categories
- Server-side checkout validation for child/under-18 tickets that require an adult, reserved-seat ticket quantity matching, min/max per-order rules, and family ticket MVP bundles
- Ticket-type inventory snapshots now show configured capacity, issued tickets, active 15-minute pending checkout demand, available capacity, gross ticket revenue, and inventory health in the organiser console; checkout creation and completion now block ticket quantities that exceed server-side capacity
- Ticket inventory persistence now has a scheduler-safe `bin/sync-ticket-inventory` command that refreshes `ticket_inventory` from issued tickets and active checkout drafts for reporting and future admin/API consumers
- Reserved seat holds with a 15-minute customer countdown, expiry release, checkout drafts, fee/tax calculation, promo discount handling, donations, merchandise lines, stock-checked merchandise reservation during checkout completion, mock success flow, order creation, tickets, ledger rows, QR tokens, and delivery logs
- Admin checkout-capture reporting now includes completed, abandoned, cancelled, expired, and failed basket lifecycle states, contact capture basis, ticket/product quantities from the current checkout payload shape, and CSV export for Excel review
- Checkout drafts can now be edited from the checkout page: reserved seats, ticket lines, merchandise, and donations are reduced or removed through a server-side edit endpoint that recalculates totals, cancels the superseded draft, and releases removed seat holds; checkout also shows the server hold countdown and disables payment when the hold expires
- Checkout money calculation now runs through the tested `App\Checkout\CheckoutCalculator` for ticket, merchandise, donation, platform fee, organiser gross, and customer total amounts instead of leaving component math split across the router
- Checkout draft and pending payment persistence now run through the tested `App\Checkout\CheckoutDraftStore`, giving future Yii controllers and Stripe webhook retry handling a reusable storage boundary
- Checkout completion eligibility now runs through `App\Checkout\CheckoutCompletionEligibility`, blocking cancelled, failed, expired, or unknown checkout drafts from later issuing tickets through local mock, free checkout, or late Stripe webhook paths
- Zero-total complimentary/free checkouts now use a separate `checkout/confirm-free` path guarded by `App\Checkout\FreeCheckoutPolicy`, so free tickets can be issued without Stripe and without enabling local mock payment
- The legacy `public/checkout-demo.php` fake checkout endpoint is disabled with HTTP 410 so public checkout traffic must use the DB-backed, CSRF-protected `/api/v1/checkout/session` flow
- Frontend mock fallback data is now opt-in only (`?mock=1`, preview meta tag, or localStorage flag), so broken live API routes are visible instead of silently showing fake customer, organiser, admin, or theme data
- Scanner camera flow uses the native `BarcodeDetector` QR API when available, confirms QR format support, preserves manual entry fallback, and stops camera tracks on page hide
- Checkout draft terminal updates for expired/failed Stripe sessions now also run through `CheckoutDraftStore`, preserving already-paid drafts and recording payment terminal status through one tested path
- Stripe Checkout webhook interpretation now uses the tested `App\Stripe\StripeCheckoutWebhookPolicy` for event identity, checkout ID extraction, terminal event mapping, and local fallback IDs
- Stripe webhook checkout completion now records the payment provider as `stripe_checkout` on the first successful completion write, with Stripe session details and a Stripe-specific audit action instead of writing a local mock record first
- Webhook event persistence and duplicate detection now use the tested `App\Payment\WebhookEventStore`, so received/processed webhook bookkeeping is reusable outside the route handler
- Scheduler-safe seat-hold expiry command with JSON output, non-zero failure exit codes, and `DB_USERNAME`/`DB_USER` environment compatibility for production task runners
- Stripe Checkout/Connect adapter path with fail-closed payment mode selection: local mock payment must be explicitly enabled with `PAYMENT_MOCK_ENABLED=true` in a local/dev/test environment, otherwise missing or failing Stripe credentials keep checkout drafts pending without issuing tickets
- Customer ticket wallet, QR display, pass-ready JSON, printable ticket HTML, order history, delivery resend, and privacy request submission
- Ticket delivery emails now attach each ticket QR code as a SenMail-compatible inline PNG image payload while keeping the plain-text QR token fallback
- New QR ticket tokens are opaque and do not expose ticket IDs; scanner validation resolves tokens through a shared `TicketTokenResolver` by stored token hash first, with a legacy signed-ticket fallback for already-issued tokens
- Offline scanner manifests now use QR token hashes and public ticket references instead of raw ticket/order IDs, and the scanner UI can make local offline valid/invalid/duplicate decisions before syncing
- Scanner device registration, one-time API key display, key rotation/revocation, API-key protected online validation, offline manifest, and offline sync conflict handling
- Live online scanner validation now delegates ticket state mutation and scan logging to the tested `App\Scanner\TicketScanService`, keeping route code responsible only for event/device authorisation and response shaping
- Online scanner check-in now uses an atomic status-constrained update so duplicate scans resolve as already scanned instead of double-validating a ticket
- Admin organiser applications, finance rules, compliance centre, privacy request review, customer privacy export, audit logs, roles, permissions, and user-role assignment
- Admin organiser application review now runs through `App\Organiser\OrganiserApplicationReviewService`, keeping approval, rejection, suspension, unique organiser slugging, review stamps, and organiser status updates transactional and tested
- Privacy request lifecycle rules are now extracted into `App\Compliance\PrivacyRequestWorkflow`, preventing unsupported request types, invalid status jumps, terminal request reopening, and unaudited rejection decisions
- Delete/anonymisation request completion can now call `App\Compliance\PrivacySubjectAnonymizer` to replace account profile identifiers, revoke reset/verification/MFA material, withdraw active consents, and preserve accounting records with an audit trail
- Admin consent policy settings are now persisted through `privacy.consent_policy`, including cookie banner visibility, marketing opt-in, analytics opt-in, organiser attendee email permission, labels, notices, and policy version
- Public/customer pages now load the published consent policy and show a local cookie/analytics choice banner when enabled; admin, organiser, and scanner consoles are excluded from the banner
- Logged-in customer consent choices can now be persisted to `user_consents` through a tested `App\Compliance\UserConsentRecorder`; anonymous banner choices remain browser-local
- Public/customer pages now require a customer to accept the platform Terms and Conditions before continuing, with logged-in acceptance recorded through the existing consent endpoint and anonymous acceptance stored locally by policy version
- Checkout and public legal pages now state that, when organisers collect payment through their connected payment account, organisers are responsible for refund decisions and approved refund processing under their published event policy
- Audit logs now have an optional hash-chain integrity table and compliance summary so new audit writes can be checked for tampering or broken continuity
- Admin Settings now includes phone verification policy modes: disabled, optional, required for privileged users, or required for all users; customer Account Profile can request and verify a local SMS OTP code against that policy
- Product owner feature map and frontend sitemap board is available at `public/product-map.html` for a Figma-style overview of public, customer, organiser, scanner, and admin experiences
- No-login MVP feature showcase is available at `public/mvp-showcase.html`, bringing customer, organiser, admin, scanner, payment, compliance, theme, and handoff features into one front-end review page without requiring seeded login accounts
- Admin console users, events, orders, payments, and finance panels now have a live DB-backed snapshot endpoint instead of static placeholder content
- Admin event oversight now consumes synced ticket inventory totals for available, held, issued, capacity, and inventory health across each event
- Admin fee and tax pages now load existing DB-backed rules and can create new platform fee/tax rules through the secured admin API
- Admin tax rule creation now runs through `App\Tax\TaxRuleCreationService`, including exact country-code validation, date-window validation, applies-to flags, and audit-ready response data
- Admin platform fee rule creation now runs through `App\Fee\PlatformFeeRuleCreationService`, including exact currency validation, date-window validation, min/max bounds, fee-type validation, and audit-ready response data
- Admin category and featured-event curation now use secured DB-backed catalogue endpoints and persisted site settings
- Admin system health view for runtime, DB schema, local uploads, delivery queues, webhooks, checkout drafts, security access denials/rate limits, and Stripe readiness
- Runtime schema repair policy is disabled by default for production environments; controlled schema maintenance should use `docs/schema.sql` or the Yii migration path
- Deployment readiness checks for debug mode, production secrets, upload execution guard, and security headers; baseline security headers are enforced in PHP and Apache configuration, and public backup/source artifacts such as `*.php.backup-before-*` are denied by `.htaccess`
- Deployment readiness now also has a scheduler/CI-friendly `bin/check-deployment-readiness` JSON preflight command with critical/warning exit behavior
- Local MVP live-run smoke checks now have `bin/check-mvp-smoke`, validating core DB tables, demo users, demo event data, public/admin/organiser/scanner files, upload execution blocking, and optional XAMPP URL responses before a walkthrough
- Product handoff now includes `docs/platform-feature-map.md` and `public/platform-map.html`, giving a Figma-style clickable sitemap across customer, organiser, admin, and operations workflows
- Deployment readiness now also warns when PHP GD is unavailable, because server-side poster/banner crop and WebP variant rendering depends on it
- Media uploads currently use local image and PDF validation only; malware scanning can be reintroduced later as a separate production hardening step
- Media storage readiness now checks `MEDIA_STORAGE_DRIVER` and `MEDIA_PUBLIC_BASE_URL`, allowing local public uploads for XAMPP while warning before production launch without managed CDN/object storage and blocking invalid managed storage URLs
- Deployment readiness now flags real Stripe keys without webhook signing, production runtime schema repair, local-only SMS OTP providers, local-only operational alert providers, and local-only ticket delivery providers before launch
- Deployment readiness now also treats `PAYMENT_MOCK_ENABLED=true` as critical in production and warning-only in local/dev environments
- Canonical schema coverage now includes runtime operational tables for checkout payment state and scanner validation telemetry, with regression coverage to catch future request-time schema drift before deployment
- Stripe readiness now explicitly checks destination-charge mode and requires a transfer-ready connected account with onboarding complete and charges enabled before Connect transfer testing is marked ready
- Organiser feature gates now protect generic organiser collection endpoints, commercial module reads omit disabled or permission-restricted sections, and organiser media collection is scoped to the organiser's own event, venue, and product media
- Admin organiser feature switching now writes defaults and per-organiser overrides through the tested `App\Organiser\OrganiserFeatureAdminService`, keeping platform capability policy changes out of the MVP router while preserving audit actions
- Organiser commercial operations for products, richer promo rules, event donation settings, donation reporting, header-aware attendee import validation, and product image metadata
- Organiser console now exposes edit flows for venues and ticket types: venue name/address/country edits, floor-plan replacement, ticket category/age/price/quantity/rules edits, and an inventory guard that blocks reducing capacity below already held or sold tickets
- Organiser ticketing now follows the recommended ticket model: a reusable organiser ticket catalogue stores Adult, Child, Student, Senior, VIP, Family, Companion, and custom categories without prices, while each event has separately editable ticket allocations for price, stock, order limits, and rules. Unsold allocations may move between organiser events; sold allocations must be archived and recreated to preserve accounting history.
- Organiser staff roles are additive: the same member can hold both cashier and ticket scanner roles, and organiser admins/event managers include cashier and scanner operational access where policy permits. Staff-sensitive actions continue to write audit logs.
- Organiser finance sales-by-event reporting now includes ticket-type breakdowns, reconciliation checks, and CSV/Excel/PDF exports. Historical/local rows that have order ticket gross without ticket-type order-item detail are surfaced as an explicit unallocated ticket-sales reconciliation line rather than disappearing from totals.
- Organiser product create/update now runs through `App\Shop\ProductUpsertService`, including SKU sanitisation, exact currency validation, stock/price bounds, organiser-scoped SKU updates, and audit-ready product responses
- Organiser merchandise UI now makes product updates explicit: editing locks the SKU to avoid accidental duplicate products, supports selected-event/all-event allocation, active/inactive/archived status, price, and stock updates through the existing DB-backed upsert path
- Organiser promo-code create/update now runs through `App\Promo\PromoCodeUpsertService`, including code sanitisation, usage-limit normalisation, percentage cap protection, active-window validation, and organiser-scoped code updates
- Organiser promo UI now supports editing existing codes, discount type/value, usage limits, per-customer limits, minimum order, active/inactive status, event allocation, and start/end date windows through the existing DB-backed promo upsert path
- Organiser donation settings now run through `App\Donation\DonationSettingsService`, including suggested amount normalisation, event donation flag updates, duplicate-safe settings persistence, and transactional save/update behavior
- Event publishing now has a server-side publication policy: incomplete events stay draft, paid events require a Stripe connected account, unsafe media blocks publication, and readiness blockers are returned to organiser UI/event listings
- Local box-office ticket issuing for cash, external card, and complimentary sales, plus organiser refund request/status workflow when the platform feature gate allows it
- Box-office sale responses now return the persisted payment id from `PaymentRecordWriter` instead of referencing an undefined local variable after successful order/ticket creation
- Refunds now persist reviewable `refund_items` for whole-order and selected order-line refunds
- Attendee import row storage/preview and organiser reports for sales by event, ticket status, and refund totals
- Attendee import persistence now runs through `App\Organiser\AttendeeImportService`, reusing the header-aware CSV parser while transactionally storing import summaries and row-level validation results
- CSV export endpoints for organiser orders, refunds, attendees, and platform admin ledger review
- Admin financial reconciliation endpoint and finance-panel UI compare paid order gross, platform fee, organiser gross, payment records, ledger debits/credits, ticket quantities, and order-level exceptions
- Finance/accounting reporting now states the strict double-entry invariant: customer gross must equal platform-fee gross plus organiser gross, and any mismatch is a review exception rather than acceptable reconciliation noise
- Admin settlement summary and CSV export now aggregate paid/refunded orders by organiser, event, and currency, including platform fees, organiser gross, known Stripe fees, refunds, net settlement, and review flags
- Admin settlement review flags now explain payout risks per organiser/event/currency, including payment total mismatch, ledger imbalance, platform/organiser ledger mismatch, ticket quantity mismatch, missing connected Stripe account readiness, and negative settlement rows
- Admin finance UI now surfaces settlement review signals and notes directly in the settlement table, including succeeded-payment comparison next to customer gross
- Admin finance export schedule settings now persist export frequency, timing, recipients, export types, retention, and delivery mode for the future production worker/mail pipeline
- Scheduled finance export execution now has a CLI/admin run path, duplicate period protection, local CSV file generation under `runtime/finance-exports`, and a persisted run history table
- Finance export run files now have an admin-only guarded download endpoint that verifies files remain inside the configured runtime export directory
- Scheduled finance export email delivery now queues per-recipient delivery records linked to generated export runs, ready for a production SMTP/provider worker
- Queued finance export deliveries now have a CLI/admin sender path with a safe local log adapter and future HTTP mail-provider adapter configuration
- Queued ticket email/SMS delivery logs now have `bin/send-ticket-deliveries` with a safe local log adapter, production fail-closed behavior, and future HTTP provider configuration
- HTTP delivery provider configuration for customer tickets and finance exports now uses a shared production policy: local HTTP endpoints remain usable in development, while production requires HTTPS endpoints and bearer tokens before any outbound delivery attempt
- Admin email/SMS operations now use live ticket-delivery queue data and can manually process queued customer email/SMS ticket deliveries through the configured provider
- Ticket issuing is now partly extracted into `App\Ticket\TicketIssuer`, so checkout and box-office share one tested path for ticket rows, locked seat sale updates, hold conversion, and QR token persistence
- Checkout completion recording is now partly extracted into `App\Checkout\CheckoutCompletionRecorder`, so local success flows and future Stripe webhook retries can reuse an existing paid order instead of duplicating ticket issuance
- Stripe checkout completion orchestration is now extracted into `App\Stripe\StripeCheckoutCompletionService`, keeping idempotent paid-draft checks and Stripe provider-reference handling out of the MVP router
- RBAC reasoning is now partly extracted into `App\Auth\AccessControlService` and `App\Auth\AccessDecision`, so the MVP router keeps HTTP/audit handling while permission decisions are tested outside route code
- Database-backed rate limiting is now partly extracted into `App\Security\DatabaseRateLimiter`, so login, checkout, scanner, media, and account throttling use a tested persistence path
- CSRF route matching is now extracted into `App\Security\CsrfRoutePolicy`, so browser mutation coverage is tested independently while API-key scanner flows stay unblocked
- Session CSRF endpoint and header enforcement for authenticated account, organiser, admin, site settings, media upload, and privacy mutation routes
- Login credential checks are now extracted into the tested `App\Auth\AuthenticationService`, keeping credential validation and active-account rules reusable for the future Yii auth controller while the MVP router owns session regeneration, rate limiting, and audit
- Admin role creation now runs through `App\Auth\RoleCreationService`, keeping configurable role scope validation, key normalisation, duplicate detection, and audit-ready response data outside the router
- Admin permission creation now runs through `App\Auth\PermissionCreationService`, keeping configurable permission keys, module defaults, duplicate detection, and audit-ready response data outside the router
- Admin role-permission attachment now runs through `App\Auth\RolePermissionAttachmentService`, keeping existing role/permission validation and duplicate-safe attachment outside the router
- Admin user-role assignment now runs through `App\Auth\UserRoleAssignmentService`, keeping existing-user lookup, role validation, scoped duplicate detection, and audit-ready assignment data outside the router
- Password reset token request and completion are now extracted into the tested `App\Auth\PasswordResetService`, keeping hashed reset tokens, one-time use, expiry checks, pending-account activation, and password replacement reusable for the future Yii auth controller
- Email verification token request and completion are now extracted into the tested `App\Auth\EmailVerificationService`, keeping hashed verification tokens, one-time use, expiry checks, signed-in account matching, and email_verified_at updates reusable for the future Yii auth controller
- Public event list/detail API behavior is now extracted into `App\Api\EventApiController`, reducing router SQL and creating a clearer Yii 3 controller migration path for customer discovery endpoints
- Checkout request input normalisation and checkout draft list presentation are now extracted into `App\Checkout` services, reducing customer-money route coupling while preserving the public checkout API shape
- Stripe webhook orchestration is now extracted into `App\Payment\PaymentWebhookController`, keeping signature verification, idempotent webhook recording, terminal draft handling, seat-hold release, and checkout completion behavior on the same trusted path with controller-level tests
- Online scanner validation orchestration is now extracted into `App\Api\ScannerApiController`, keeping rate limits, feature gates, device authorisation, ticket scan delegation, and response presentation on the same tested path
- Offline scanner sync conflict resolution, ticket check-in mutation, scan-log persistence, and result presentation are now extracted into `App\Scanner\OfflineScannerSyncService`
- Promo code evaluation is now extracted into `App\Promo\PromoCodeEvaluator`, keeping event-specific selection, active-window checks, minimum spend, global and per-customer usage limits, and server-side discount calculation out of the public router
- Checkout tax and platform-fee rule resolution is now extracted into `App\Checkout\CheckoutRuleResolver`, preserving event > organiser > category > global fee precedence and safe no-tax/no-fee fallbacks
- Checkout merchandise selection is now extracted into `App\Shop\CheckoutProductSelector`, preserving merchandise-enabled checks, product availability, stock validation, and server-side line subtotals
- Checkout donation settings resolution is now extracted into `App\Donation\DonationSettingsResolver`, preserving event-level overrides and safe default suggested amounts
- Stripe Checkout Session parameter creation is now extracted into `App\Stripe\StripeCheckoutSessionFactory`, preserving hosted Checkout URLs, metadata, Connect destination-charge organiser-gross transfers, and explicit legacy direct-charge configuration for future real Stripe testing
- Stripe connected-account selection is now extracted into `App\Stripe\StripeConnectedAccountSelector`, preferring charge-enabled completed onboarding accounts for organiser destination charges
- Production launch gating is now available through `App\Admin\ProductionLaunchGate` and `bin/check-production-launch`, giving a stricter go/no-go report that blocks live launch on unresolved readiness warnings unless explicitly relaxed
- Checkout seat-hold release is now extracted into `App\SeatMap\CheckoutSeatHoldReleaser`, so abandoned/expired payment paths reuse a tested payload parser before releasing selected seats
- Checkout accounting writes are now extracted into `App\Checkout\CheckoutAccountingWriter`, so order item, tax breakdown, and ledger persistence share one tested path across customer checkout, box office, and refund records
- Ticket-line tax allocation is now extracted into `App\Checkout\CheckoutTicketLineTaxAllocator`, so multi-ticket checkouts distribute tax exactly across order lines without penny drift from independent rounding
- Ticket delivery log writes are now extracted into `App\Ticket\TicketDeliveryLogWriter`, normalising email, SMS, wallet, and PDF delivery rows for ticket issue, resend, account verification, and local adapter queues
- Paid order persistence is now extracted into `App\Checkout\CheckoutOrderWriter` and `PaidOrderWrite`, keeping customer contact validation and order money columns consistent between online checkout completion and box-office sales
- Payment persistence is now extracted into `App\Payment\PaymentRecordWriter`, so box-office payments write both succeeded payment rows and transaction log rows through one tested path
- Merchandise fulfilment order creation is now extracted into `App\Shop\ProductOrderWriter`, reserving stock and writing product order items through one tested checkout-completion path
- Donation order recording is now extracted into `App\Donation\DonationOrderWriter`, keeping donation ledger inputs and tax-rule references on a reusable tested persistence path
- Promo-code redemption recording is now extracted into `App\Promo\PromoCodeRedemptionWriter`, ensuring checkout discounts persist through a focused, reusable order-side effect
- Checkout seat selection resolution is now extracted into `App\SeatMap\CheckoutSeatSelectionResolver`, keeping event-seat-id and legacy seat-label lookup behaviour tested before ticket issue
- Checkout ticket-seat allocation is now extracted into `App\Checkout\CheckoutTicketSeatAllocator`, assigning selected reserved seats to ticket lines deterministically before QR ticket issue
- Ticket delivery planning is now extracted into `App\Ticket\TicketDeliveryPlanner`, so online checkout and box-office ticket issue share one tested wallet/email/SMS delivery logging path
- Checkout ticket-line resolution is now extracted into `App\Checkout\CheckoutTicketLineResolver`, preserving explicit multi-ticket payloads and legacy seat-selection fallback lines through one tested path
- Local product-owner live-run readiness is now available through `App\Admin\LocalLiveRunChecklist` and `bin/check-local-live-run`, checking pages, APIs, demo data, checkout persistence, scanner readiness, theme/media controls, and local payment completion mode before hands-on MVP testing

## Validation Baseline

Current expected local checks:

```powershell
C:\xampp\php\php.exe -l public\index.php
C:\xampp\php\php.exe -l public\mvp.php
node --check public\assets\js\zavvion-ui.js
C:\xampp\php\php.exe bin\rescan-media-assets --force --limit=100
C:\xampp\php\php.exe bin\apply-media-retention --limit=100
C:\xampp\php\php.exe bin\check-deployment-readiness
C:\xampp\php\php.exe bin\check-production-launch --allow-warnings
C:\xampp\php\php.exe bin\check-stripe-readiness
C:\xampp\php\php.exe bin\check-mvp-smoke --base-url=http://localhost/zavvion-events/public
C:\xampp\php\php.exe vendor\bin\phpunit
C:\xampp\php\php.exe composer.phar validate --strict
C:\xampp\php\php.exe composer.phar audit
```

## Still Not Full Commercial Production

Remaining large-scale work:

- Convert the large MVP router into Yii 3 controllers, middleware, services, repositories, request DTOs, and console commands
- Resolve strict launch-gate environment warnings before any internet-facing production run: set `APP_ENV=prod`, `APP_DEBUG=false`, replace local `COOKIE_SECRET`/`QR_SIGNING_SECRET`, enable PHP GD, and connect Stripe test/live credentials with a signed webhook
- Configure and validate a real SMS HTTP provider endpoint for phone OTP delivery; the local `log` adapter now fails closed in production and the generic signed HTTP provider path is present
- Complete the remaining media production pipeline around the new scan/rescan, retention, crop-rendering, alert-feed, alert-notification, and floor-plan foundations: CDN/private storage, managed malware-scanner deployment, and floor-plan processing
- Complete real Stripe test validation with your Stripe test account, connected accounts, webhook signing secret, organiser-gross transfers, platform service fees, and reconciliation checks. Stripe-originated disputes/refunds remain intentionally out of scope for the current launch policy.
- Add season passes, connect a production SMTP/HTTP mail provider for queued finance export deliveries, and deeper payout reconciliation once Stripe test credentials are connected
- Expand API tests and browser/device QA across mobile, tablet, scanner devices, and accessibility tooling
- Replace MVP family-ticket descriptions with structured bundle rules, and add venue/organiser-level default age policies once product policy is final

## Recommended Next Step

Continue with the next commercial-production slice: deeper payout reconciliation, media retention/CDN planning, or moving high-risk workflow code from `public/mvp.php` and `public/index.php` into focused Yii/application services.
