# Frontend Integration Contract

This contract describes the current MVP frontend/API handoff for launch integration. It is factual for the current local MVP and should be rechecked after parallel backend/frontend changes land.

## Runtime Rules

- Frontend files live under `public/`.
- Default API base is `./api/v1`.
- Pages may override the API base with `<meta name="zv-api-base" content="...">`.
- Mutating requests fetch `GET /auth/csrf` and send `X-CSRF-Token`.
- Fetch requests use same-origin credentials.
- Mock fallback is disabled by default and must remain disabled for launch testing.
- Mock fallback can be enabled only for design review with `?mock=1`, a preview meta tag, or `localStorage['zv:allowMockFallbacks']='true'`.

## Response Shape

Preferred JSON shape:

```json
{
  "status": "ok",
  "data": {},
  "meta": {}
}
```

The frontend also tolerates collection responses where `data` is an array or where `data.items` is an array.

Error shape:

```json
{
  "status": "validation_error",
  "message": "Human readable message."
}
```

Use stable `status` strings for UI decisions, and human-readable `message` text for display.

## Auth and Account

| Method | Path | Purpose |
|---|---|---|
| GET | `/auth/csrf` | Return CSRF token as `data.csrf_token`, `csrf_token`, `token`, `csrf`, or `csrfToken`. |
| POST | `/auth/login` | Login with credentials. |
| POST | `/auth/logout` | Logout current session. |
| GET | `/auth/me` | Current user. |
| POST | `/auth/password-reset/request` | Request password reset. |
| POST | `/auth/password-reset/complete` | Complete password reset. |
| GET | `/auth/mfa/status` | MFA status. |
| POST | `/auth/mfa/request` | Request MFA challenge. |
| POST | `/auth/mfa/verify` | Verify MFA challenge. |
| GET | `/account/tickets` | Wallet/tickets/orders data. |
| GET | `/account/tickets/{ticketRef}/pass` | Pass-ready ticket JSON. |
| GET | `/account/tickets/{ticketRef}/print` | Printable ticket HTML. |
| POST | `/account/orders/{orderId}/resend-tickets` | Queue ticket resend. |
| GET | `/account/privacy-export` | Privacy export. |
| POST | `/privacy-requests` | Create privacy request. |
| GET | `/account/phone-verification/status` | Phone verification state. |
| POST | `/account/phone-verification/request` | Request phone OTP. |
| POST | `/account/phone-verification/verify` | Verify phone OTP. |
| POST | `/account/consents` | Persist logged-in consent choice. |

## Public Event Discovery

| Method | Path | Purpose |
|---|---|---|
| GET | `/events` | Event cards for home/listing. |
| GET | `/events/{slug}` | Event detail, ticket types, media, merchandise, donation, and seats. |
| GET | `/site/branding` | Published public branding. |
| GET | `/site/consent-policy` | Published consent policy. |
| POST | `/promo/validate` | Server-side promo validation. |

Event card fields consumed by frontend normalizers include:

- `slug`
- `title`
- `status`
- `summary` or `description`
- `category` or `category_name`
- `organiser` or `organiser_name`
- `venue` or `venue_name`
- `city`, `venue_city`, or `venue_country`
- `date`, `start_at`, `startsAt`, or `starts_at`
- `currency`
- `price_from_minor_amount` or `min_price_minor_amount`
- `poster`, `poster_url`, or `media.poster.url`
- `hero`, `hero_url`, or `media.hero.url`
- `age_policy`
- `featured`

Event detail should include:

- `ticket_types`: each with `id`, `name`, `price_minor_amount`, `currency`, `available`, `min_per_order`, `max_per_order`, `ticket_category`, `requires_adult`, `requires_id_check`, `requires_parental_consent`, `description`, and `terms_text`.
- `seats`: each with `event_seat_id` or `id`, `row_label`, `seat_number`, `label`, `section_name`, `seat_type`, and `status`.
- `products` or `merchandise` where merchandise is enabled.
- `donation_settings` and `donation_enabled` where donation is enabled.
- `media` for gallery/poster/hero assets.

## Seat Holds and Checkout

| Method | Path | Purpose |
|---|---|---|
| POST | `/seat-holds` | Hold selected reserved seats for checkout. |
| POST | `/seat-holds/release` | Release selected held seats. |
| POST | `/seat-holds/expire` | Expire stale holds. |
| POST | `/checkout/session` | Create checkout draft and Stripe/free/mock next action. |
| GET | `/checkout/drafts` | Recent checkout drafts for diagnostics/admin-like views. |
| POST | `/checkout/drafts/{checkoutId}/edit` | Create a recalculated replacement draft after reducing/removing editable checkout items. |
| POST | `/checkout/confirm-free` | Complete zero-total checkout. |
| POST | `/checkout/simulate-success` | Local/dev/test mock payment only. Must be disabled in production. |
| POST | `/webhooks/stripe` | Stripe webhook endpoint. |

Checkout payload expectations:

- `event_slug`
- `ticket_lines` or equivalent normalized ticket selection with ticket type IDs and quantities.
- `selected_event_seat_ids` for reserved seats.
- `customer_token` for seat-hold ownership.
- `customer` with name/email/phone where collected.
- Optional promo, donation, and merchandise selections.

Checkout edit command examples:

- Remove one reserved seat: `{ "commands": { "action": "remove_seat", "event_seat_id": "..." } }`
- Reduce a general-admission ticket type: `{ "commands": { "action": "set_quantity", "ticket_type_id": "...", "quantity": 1 } }`
- Remove a ticket type: `{ "commands": { "action": "remove_ticket_type", "ticket_type_id": "..." } }`
- Remove merchandise: `{ "commands": { "action": "remove_product", "sku": "DEMO-TEE" } }`
- Clear donation: `{ "commands": { "action": "clear_donation" } }`

The edit endpoint returns a new checkout draft ID and cancels the superseded draft. The frontend must replace the URL/session draft reference with the returned draft before payment.

Server rules the frontend must not bypass:

- Child/under-18 tickets requiring an adult must be blocked server-side.
- Reserved-seat count must equal reserved-seat ticket quantity.
- Ticket min/max per order must be enforced server-side.
- Active 15-minute seat holds are required before reserved-seat completion.
- Inventory/capacity is authoritative on the server.
- Checkout totals, fees, taxes, discounts, donation, and merchandise are server-calculated.
- Paid ticket issuance must rely on Stripe webhook completion, not browser-only success.

Frontend controls required for launch:

- Increase, reduce, and remove ticket lines.
- Increase, reduce, and remove checkout items where editable.
- Clear feedback when seat quantity and ticket quantity diverge.
- Visible seat-hold countdown.
- Error messages for expired holds, inventory unavailable, child/adult validation, payment unavailable, and mock disabled.

## Scanner

| Method | Path | Purpose |
|---|---|---|
| GET | `/scanner/devices` | List authorised scanner devices for organiser/admin users. |
| POST | `/scanner/devices` | Create scanner device and return one-time API key. |
| POST | `/scanner/devices/{deviceId}/rotate` | Rotate scanner key. |
| POST | `/scanner/devices/{deviceId}/revoke` | Revoke scanner device. |
| GET/POST | `/scanner/manifest` | Offline manifest for assigned event/device. |
| POST | `/scanner/validate` | Online ticket validation/check-in. |
| POST | `/scanner/offline-sync` | Upload offline scans and receive authoritative conflict statuses. |

Scanner validation statuses used by the UI include:

- `valid`
- `already_scanned`
- `wrong_event`
- `cancelled`
- `invalid`
- `feature_disabled`

Scanner payload should include:

- `token` and/or `qr_token`
- `event_slug` where needed
- `device_id`
- Scanner API key credential according to current scanner device flow.

Offline manifest must not expose raw internal ticket/order IDs. It should use token hashes and public ticket references.

## Organiser Console

| Method | Path | Purpose |
|---|---|---|
| GET | `/organiser/summary` | Dashboard summary. |
| GET | `/organiser/features` | Enabled organiser capabilities. |
| GET/POST | `/organiser/events` | List/create events. |
| POST | `/organiser/events/{slug}/status` | Update event status with readiness blockers. |
| POST | `/organiser/events/{slug}/age-policy` | Save event age policy. |
| GET/POST | `/organiser/venues` | List/create venues. |
| GET/POST | `/organiser/ticket-types` | List/create ticket types. |
| GET/POST | `/organiser/seat-maps` | List/create seat maps. |
| POST | `/organiser/events/{slug}/seat-map` | Attach reusable seat map to event. |
| GET | `/organiser/orders` | Orders. |
| GET | `/organiser/refunds` | Refunds. |
| GET | `/organiser/reports` | Reports. |
| GET/POST | `/organiser/staff` | Staff list/create. |
| GET | `/organiser/commercial` | Products/promos/donations commercial snapshot. |
| POST | `/organiser/products` | Create/update product. |
| POST | `/organiser/promo-codes` | Create/update promo code. |
| POST | `/organiser/donation-settings` | Save donation settings. |
| POST | `/organiser/attendee-imports` | Upload/import attendees. |
| POST | `/organiser/box-office/sale` | Local box-office sale. |
| POST | `/organiser/orders/{orderId}/refunds` | Request refund. |
| POST | `/organiser/refunds/{refundId}/status` | Update refund status when allowed. |
| POST | `/organiser/stripe-account` | Save organiser Stripe readiness/account data. |
| GET | `/organiser/exports/orders.csv` | Orders export. |
| GET | `/organiser/exports/refunds.csv` | Refunds export. |
| GET | `/organiser/exports/attendees.csv` | Attendees export. |

Ticket type creation should support adult and child ticket categories per seat. Current known category vocabulary includes adult, child, infant, student, senior, family, carer, companion, VIP, general, and custom categories.

## Admin Console

| Method | Path | Purpose |
|---|---|---|
| GET | `/admin/summary` | Admin summary snapshot. |
| GET | `/admin/console-data` | Broader console data. |
| GET | `/admin/catalog` | Categories/featured event catalogue. |
| GET/POST | `/admin/themes` and `/admin/themes/active` | Theme list and publish active skin. |
| POST | `/admin/themes/draft` | Save theme draft. |
| POST | `/admin/themes/rollback` | Roll back active theme. |
| GET/POST | `/site/settings` | Site settings/branding persistence. |
| GET | `/admin/system-health` | System health. |
| GET | `/admin/stripe-readiness` | Stripe readiness. |
| GET/POST | `/admin/features` | Platform/organiser feature controls. |
| GET/POST | `/admin/verification-policy` | Phone verification policy. |
| GET/POST | `/admin/consent-policy` | Consent policy. |
| GET | `/admin/compliance` | Compliance dashboard. |
| POST | `/admin/privacy-requests/{id}/status` | Review privacy request. |
| GET | `/admin/finance-rules` | Fee/tax rule snapshot. |
| POST | `/admin/platform-fee-rules` | Create platform fee rule. |
| POST | `/admin/tax-rules` | Create tax rule. |
| GET | `/admin/reconciliation` | Finance reconciliation. |
| GET | `/admin/settlements` | Settlement review. |
| GET | `/admin/exports/ledger.csv` | Ledger export. |
| GET | `/admin/exports/settlements.csv` | Settlement export. |
| GET/POST | `/admin/finance-export-schedule` | Finance export schedule. |
| GET/POST | `/admin/finance-export-runs` | Finance export runs. |
| POST | `/admin/finance-export-deliveries/send` | Send queued finance export deliveries. |
| GET | `/admin/media-library` | Admin media library. |
| GET | `/admin/media-scan-summary` | Media scan summary. |
| POST | `/admin/media/rescan` | Rescan media. |
| GET | `/admin/media-retention-report` | Media retention preview. |
| POST | `/admin/media/retention` | Apply media retention policy. |
| GET | `/admin/system-alerts` | System alerts. |
| POST | `/admin/system-alerts/{id}/acknowledge` | Acknowledge alert. |
| GET/POST | `/admin/alert-notification-policy` | Alert notification policy. |
| POST | `/admin/system-alert-notifications/queue` | Queue alert notifications. |
| POST | `/admin/system-alert-notifications/send` | Send alert notifications. |
| GET | `/admin/ticket-deliveries` | Ticket delivery queue. |
| POST | `/admin/ticket-deliveries/send` | Process ticket deliveries. |

Admin skin auto-rotation is a launch requirement from the product brief. Before launch, confirm whether auto-rotation is implemented as persisted admin policy, operational scheduled change, or manual runbook. Do not assume it is live unless verified in the target branch.

## Media Upload

| Method | Path | Purpose |
|---|---|---|
| POST | `/media/upload/{slot}` | Upload event/site/venue/product media. |
| POST | `/media/assets/{id}` | Update alt text/crop/focal metadata. |
| POST | `/media/assets/{id}/remove` | Soft-remove media asset. |
| GET | `/organiser/media-library` | Organiser-scoped media. |

Upload responses should include:

- `id`
- `url`
- `alt_text`
- `mime_type`
- `width`
- `height`
- `scan_status`
- `processing_status`
- `variants`

Public-facing APIs must expose only active, non-deleted, clean media. Admin review views may show blocked, failed, infected, archived, or removed assets with clear status labels.

## Compatibility Notes

- Do not expose Stripe secret keys, webhook secrets, scanner API key hashes, raw QR tokens, password hashes, or internal payment secrets to the browser.
- All frontend money display may estimate before checkout, but the server total is authoritative.
- All mutation routes should remain CSRF-protected except API-key scanner flows covered by scanner credentials.
- Production launch should fail closed when Stripe, SMS, ticket delivery, alert delivery, or malware scanning is required but not configured.
- The current large MVP router is an integration surface; future Yii controller migration should preserve this contract or version it explicitly.
