{"openapi": "3.0.0", "paths": {"/api": {"get": {"operationId": "getHello", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Hello / health check", "tags": ["meta"]}}, "/api/push/fcm/web-config": {"get": {"description": "Returns public Firebase Web SDK settings for browser FCM (apiKey, projectId, VAPID key, etc.). No JWT required. Set FIREBASE_WEB_* in the API `.env`.", "operationId": "webConfig", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FcmWebConfigResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "Firebase Web client config (public)", "tags": ["push"]}}, "/api/push/fcm/token": {"post": {"description": "Requires JWT. Stores the Firebase Cloud Messaging registration token for the signed-in user and subscribes it to the per-user FCM topic (user UUID). If the token was linked to another user, it is reassigned.", "operationId": "register", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RegisterFcmTokenDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RegisterFcmTokenResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Register FCM device token", "tags": ["push"]}, "delete": {"description": "Requires JWT. Removes the registration token for the signed-in user (e.g. on logout).", "operationId": "unregister", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnregisterFcmTokenDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnregisterFcmTokenResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Unregister FCM device token", "tags": ["push"]}}, "/api/push/fcm/topic/subscribe": {"post": {"description": "Server-side `subscribeToTopic` for all FCM tokens registered for the signed-in user. Requires a prior `POST /api/push/fcm/token` for this JWT user. Topic must match mobile `subscribeToTopic` (e.g. `trem_test`).", "operationId": "subscribeTopic", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FcmTopicSubscribeDto"}}}}, "responses": {"200": {"description": "Subscribe result", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "No device tokens for this user, or FCM not configured on server"}}, "security": [{"bearer": []}], "summary": "Subscribe current user device tokens to an FCM topic", "tags": ["push"]}}, "/api/push/fcm/topic/unsubscribe": {"post": {"operationId": "unsubscribeTopic", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FcmTopicSubscribeDto"}}}}, "responses": {"400": {"description": "No device tokens for this user, or FCM not configured"}}, "security": [{"bearer": []}], "summary": "Unsubscribe current user device tokens from an FCM topic", "tags": ["push"]}}, "/api/notification_list": {"get": {"description": "Returns up to 50 most recent rows from `notification_list` for the signed-in user. Includes both read (`read = 1`) and unread (`read = 0`) items, newest first.", "operationId": "list", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NotificationListResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "403": {"description": "CMS admin token not allowed; use app user JWT"}}, "security": [{"bearer": []}], "summary": "List latest in-app notifications (read and unread)", "tags": ["notification_list"]}}, "/api/notification_list/{id}/read": {"patch": {"description": "Sets `read = 1` for a notification belonging to the signed-in user.", "operationId": "markRead", "parameters": [{"name": "id", "required": true, "in": "path", "description": "notification_list.id (UUID)", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NotificationListItemResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "403": {"description": "CMS admin token not allowed; use app user JWT"}, "404": {"description": "Notification not found for this user"}}, "security": [{"bearer": []}], "summary": "Mark notification as read", "tags": ["notification_list"]}}, "/api/auth/register": {"post": {"description": "Creates a user and returns JWT access + opaque refresh tokens. Use POST /auth/oauth for Google or Apple.", "operationId": "register", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RegisterDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthTokensResponseDto"}}}}, "201": {"description": "Registered"}, "409": {"description": "Email already registered"}}, "summary": "Register with email and password", "tags": ["auth"]}}, "/api/auth/login": {"post": {"operationId": "login", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LoginDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthTokensResponseDto"}}}}, "401": {"description": "Invalid credentials"}}, "summary": "Login with email and password", "tags": ["auth"]}}, "/api/auth/oauth": {"post": {"description": "Send provider and the Google ID token or Apple identity token. Apple: set APPLE_CLIENT_ID to the iOS bundle id (comma-separate extra ids if needed). Pass name only on the first Apple sign-in. Later Apple logins can omit email; the API matches the Apple user id.", "operationId": "oauth", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OAuthDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthTokensResponseDto"}}}}, "401": {"description": "Invalid token"}}, "summary": "Sign in or sign up with Google or Apple", "tags": ["auth"]}}, "/api/auth/refresh-token": {"post": {"description": "Returns new access_token and refresh_token; old refresh token is invalidated.", "operationId": "refresh", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RefreshDto"}}}}, "responses": {"401": {"description": "Invalid refresh token"}}, "summary": "Rotate refresh token and get a new access token", "tags": ["auth"]}}, "/api/auth/logout": {"post": {"description": "Pass the refresh_token to revoke that session.", "operationId": "logout", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LogoutDto"}}}}, "responses": {"201": {"description": ""}}, "summary": "Invalidate a refresh token", "tags": ["auth"]}}, "/api/auth/forgot-password": {"post": {"description": "Always returns success. If the email belongs to an account, a one-time reset link is queued and sent. The link uses config `password_reset_base_url` (or env PASSWORD_RESET_BASE_URL) with `token`. Complete the flow with POST /auth/reset-password. Mobile apps should use POST /auth/forgot-password/mobile.", "operationId": "forgotPassword", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ForgotPasswordDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthMessageResponseDto"}}}}}, "summary": "Request a password reset link", "tags": ["auth"]}}, "/api/auth/forgot-password/mobile": {"post": {"description": "Always returns success. If the email belongs to an app user, a 6-digit code is emailed (expires in 120 seconds). Then POST /otp/verify with that code to receive `reset_token`, and POST /auth/reset-password with `{ token: reset_token, password }`. Unknown emails get the same success response. A resend within 30 seconds returns 429.", "operationId": "forgotPasswordMobile", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ForgotPasswordDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthMessageResponseDto"}}}}, "429": {"description": "Reset code was sent too recently"}}, "summary": "Request a mobile password reset code", "tags": ["auth"]}}, "/api/auth/reset-password": {"post": {"description": "Consumes a one-time reset token from the forgot-password email. Returns new access and refresh tokens. Other sessions are signed out.", "operationId": "resetPassword", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ResetPasswordDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthTokensResponseDto"}}}}, "400": {"description": "Invalid or expired token"}}, "summary": "Set a new password using the email token", "tags": ["auth"]}}, "/api/otp/send": {"post": {"description": "Stores a hashed OTP (expires in 120 seconds) and emails it. Use purpose `guest_checkout` for web guest booking. Resend cooldown applies (HTTP 429).", "operationId": "send", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OtpSendDto"}}}}, "responses": {"200": {"description": "OTP email accepted / queued"}, "400": {"description": "Invalid email or send failure"}}, "summary": "Send a 6-digit email OTP", "tags": ["otp"]}}, "/api/otp/verify": {"post": {"description": "Checks the latest unused OTP for the email. For `password_reset`, returns `reset_token`. For `guest_checkout`, returns app `access_token` / `refresh_token` to continue booking.", "operationId": "verify", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OtpVerifyDto"}}}}, "responses": {"200": {"description": "OTP verified. For password reset, `data.reset_token` is returned and must be sent to POST /api/auth/reset-password.", "content": {"application/json": {"schema": {"example": {"success": true, "data": {"verified": true, "request_id": "9f0c4a1b-2b6e-4b80-9d6c-1a2b3c4d5e6f", "email": "user@example.com", "purpose": "password_reset", "message": "Verification successful. Use reset_token with POST /api/auth/reset-password.", "reset_token": "k7nQ2vR9pLwX..."}, "timestamp": "2026-08-27T01:30:00.000Z"}}}}}, "400": {"description": "Invalid / expired / locked OTP or validation error"}}, "summary": "Verify a 6-digit email OTP", "tags": ["otp"]}}, "/api/shows/search": {"get": {"description": "Requires JWT. Performs a full-text search over shows (name/title/description/city/venue). Results are paginated via Elasticsearch. Index is created lazily on first request.", "operationId": "search", "parameters": [{"name": "q", "required": false, "in": "query", "description": "Search query string", "schema": {"example": "hamilton", "type": "string"}}, {"name": "page", "required": false, "in": "query", "description": "1-based page number", "schema": {"default": 1, "type": "number"}}, {"name": "per_page", "required": false, "in": "query", "description": "Page size", "schema": {"maximum": 50, "default": 10, "type": "number"}}, {"name": "city_id", "required": false, "in": "query", "description": "Optional city UUID filter", "schema": {"example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "type": "string"}}, {"name": "sort", "required": false, "in": "query", "description": "Sort order (best: Elasticsearch relevance, newest: created_at desc)", "schema": {"default": "best", "type": "string", "enum": ["best", "newest"]}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowsSearchResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Search shows (Elasticsearch)", "tags": ["shows"]}}, "/api/shows/{showId}/sessions": {"get": {"description": "Returns all sessions for an active show. Response includes venue `seat_map_svg_url` (CMS seat map SVG) plus per-session ticket_tiers, session_label, optional session_name, description (promo badge), and badges (low_seats only). Requires JWT.", "operationId": "getSessions", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionsListResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show not found or inactive"}}, "security": [{"bearer": []}], "summary": "Get show sessions with ticket tiers", "tags": ["show-sessions", "shows"]}}, "/api/shows/{showId}/add-ons": {"get": {"operationId": "list", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "Active add-ons for checkout"}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "List active add-ons for a show", "tags": ["shows"]}}, "/api/shows": {"get": {"description": "Returns active shows with optional filtering by city_id, category_id, and/or tag_id. When tag_id is set, only shows linked to that tag in show_tags are returned. For tag_id `classic`, returns inactive (status=0) shows with the classic tag instead of active only. Page size is fixed at 5.", "operationId": "listShows", "parameters": [{"name": "page", "required": false, "in": "query", "description": "1-based page number", "schema": {"default": 1, "type": "number"}}, {"name": "city_id", "required": false, "in": "query", "description": "City UUID (FK to cities.id)", "schema": {"example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "type": "string"}}, {"name": "tag_id", "required": false, "in": "query", "description": "Tag id (FK to tags.id). Only shows with this tag in show_tags are returned. Use `classic` for inactive archived shows that have the classic tag.", "schema": {"example": "headline", "type": "string"}}, {"name": "category_id", "required": false, "in": "query", "description": "Category id (FK to categories.id)", "schema": {"example": "musical", "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowsListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "List shows (paginated)", "tags": ["shows"]}}, "/api/shows/past": {"get": {"description": "Returns all shows that are inactive (status=0) or completed (end_date is before today). Results use the standard show-card shape and are ordered by most recently ended first.", "operationId": "listPastShows", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PastShowsListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "Get all past shows", "tags": ["shows"]}}, "/api/shows/{showId}": {"get": {"description": "Returns the show when it exists, including inactive (status=0) shows. List endpoints still return active shows only (except classic tag filter).", "operationId": "getShow", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowCardResponseDto"}}}}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "Get one show by id (UUID)", "tags": ["shows"]}}, "/api/show-like/{showId}": {"put": {"description": "Requires JWT. If a `show_reaction` row exists for (user, show), it is removed (unliked). Otherwise a new row is created (liked).", "operationId": "toggle", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowLikeToggleResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "Toggle show like for current user", "tags": ["show-like"]}}, "/api/shows/{showId}/save": {"post": {"description": "Requires JWT. Creates or updates `show_saved` for (user, show): status=1, saved_at=now(), clears unsaved_at.", "operationId": "save", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "Saved row"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "Save show for current user", "tags": ["shows"]}}, "/api/shows/{showId}/unsave": {"post": {"description": "Requires JWT. Sets status=0 and unsaved_at=now() when there is an active saved row (status=1).", "operationId": "unsave", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "Updated row"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "No active saved entry"}}, "security": [{"bearer": []}], "summary": "Unsave show for current user", "tags": ["shows"]}}, "/api/shows/{showId}/notifications": {"post": {"description": "Requires JWT. Upserts `show_notification` for (user, show). Each flag is 0 (off) or 1 (on).", "operationId": "save", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SaveShowNotificationDto"}}}}, "responses": {"200": {"description": "Saved preferences row"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "Save notification preferences for this show", "tags": ["shows"]}}, "/api/shows-saved": {"get": {"description": "Requires JWT. Returns rows from `show_saved` for active saves (`status = 1`), newest first.", "operationId": "list", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSavedListResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "List saved shows for current user", "tags": ["shows"]}}, "/api/show-notifications": {"get": {"description": "Requires JWT. Returns all rows from `show_notification` for the signed-in user, newest first.", "operationId": "list", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowNotificationListResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "List show notification preferences for current user", "tags": ["shows"]}}, "/api/shows/{showId}/sessions/calendar": {"get": {"description": "Returns each date in YYYY-MM with has_show, remaining_seats, session_count, and availability_status. Includes suggested_month (earliest session month) for calendar default navigation.", "operationId": "getCalendar", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}, {"name": "month", "required": true, "in": "query", "schema": {"example": "2026-06", "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionsCalendarResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show not found"}}, "security": [{"bearer": []}], "summary": "Calendar dates with show availability for a month", "tags": ["show-sessions", "shows"]}}, "/api/shows/{showId}/sessions/{sessionId}/price-ranges": {"get": {"description": "Band picker (screen 4). When the show has price bands (CMS show Tickets tab), returns one card per band with its min\u2013max range and grouped sections; bucket id format `band:{bandId}`. If no bands are configured, falls back to one card per venue section tier (id `tier:{url-encoded tierName}`). Buyers pick a band, then a section, then seats. Includes adjacent auto-select availability and membership redeem eligibility.", "operationId": "getPriceRanges", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}, {"name": "sessionId", "required": true, "in": "path", "description": "Show session UUID", "schema": {"type": "string"}}, {"name": "quantity", "required": true, "in": "query", "schema": {"example": 2}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionPriceRangesResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show or session not found"}}, "security": [{"bearer": []}], "summary": "Session price bands with sections and auto-select (Screen 4)", "tags": ["show-sessions", "shows"]}}, "/api/shows/{showId}/sessions/{sessionId}/map": {"get": {"description": "Sections for the chosen band (or tier). Exactly one selectable in-band section has `highlighted: true`. Each section includes `map_region_id` + `level` (ground|first|second) for Figma bind, `selectable`, `price`, and `rows[]` (row UUID for GET \u2026/rows/:rowId). Out-of-band sections are `dimmed: true` but still returned for map paint. Aliases: Premium\u2192stalls_front; Box L\u2192box_1; Box R\u2192box_4. Stalls Back unbound until seeded. Includes `seat_map_svg_url` / `seat_map_svg`. Wheelchair seats count as selectable. Returns legend and optional auto_selected adjacent block.", "operationId": "getMap", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}, {"name": "sessionId", "required": true, "in": "path", "description": "Show session UUID", "schema": {"type": "string"}}, {"name": "price_range_id", "required": true, "in": "query", "description": "Bucket id from GET \u2026/price-ranges (`band:{bandId}` when bands exist, else `tier:{tierName}`)", "schema": {"example": "band:6f1c\u2026", "type": "string"}}, {"name": "quantity", "required": true, "in": "query", "schema": {"example": 2, "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionMapResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show, session, or price range not found"}}, "security": [{"bearer": []}], "summary": "Theatre map filtered by chosen band/tier (Screen 5 \u2014 section + seat pick)", "tags": ["show-sessions", "shows"]}}, "/api/shows/{showId}/sessions/{sessionId}/rows/{rowId}": {"get": {"description": "Per-row seat grid for the seat picker. Statuses: available, wheelchair, sold, held, blocked, orphan_blocked, selected. Pass price_range_id to filter by section tier. Row id = venue row UUID (preferred; see GET \u2026/map `sections[].rows[].id`) or row_label when unique.", "operationId": "getRowDetail", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}, {"name": "sessionId", "required": true, "in": "path", "description": "Show session UUID", "schema": {"type": "string"}}, {"name": "rowId", "required": true, "in": "path", "description": "Venue row UUID or row_label", "schema": {"type": "string"}}, {"name": "quantity", "required": true, "in": "query", "schema": {"example": 2, "type": "string"}}, {"name": "price_range_id", "required": false, "in": "query", "schema": {"type": "string"}}, {"name": "seat_ids", "required": false, "in": "query", "description": "Comma-separated venue seat UUIDs to mark selected", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionRowDetailResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show, session, or row not found"}}, "security": [{"bearer": []}], "summary": "Clickable row seat map (Screen 5)", "tags": ["show-sessions", "shows"]}}, "/api/shows/{showId}/sessions/{sessionId}/availability": {"get": {"description": "**Path:** `GET /api/shows/{showId}/sessions/{sessionId}/availability?quantity=N` (there is no `/api/sessions` route). Returns section-tier buckets in `price_ranges[]` (id = `tier:{tierName}`), adjacent-seat flag, and `can_fit_quantity` per tier.", "operationId": "getAvailability", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}, {"name": "sessionId", "required": true, "in": "path", "description": "Show session UUID", "schema": {"type": "string"}}, {"name": "quantity", "required": true, "in": "query", "schema": {"example": 2, "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowSessionAvailabilityResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show or session not found"}}, "security": [{"bearer": []}], "summary": "Session tier availability for ticket quantity (Screen 3)", "tags": ["show-sessions", "shows"]}}, "/api/users/me": {"get": {"description": "Account fields plus membership, loyalty points (balance, total earned, total redeemed), paid and pending purchase counts, amount spent, and ticket count.", "operationId": "getMe", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMeResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Current user profile", "tags": ["users"]}}, "/api/users/me/membership": {"get": {"description": "Returns complimentary ticket balance for the current calendar year, or null benefits if not a member.", "operationId": "getMembership", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMembershipResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Current user membership benefits", "tags": ["users"]}}, "/api/users/me/purchases": {"get": {"description": "Paged checkout/purchase history for the logged-in user (newest first). Optional `status` filters by checkout order status (`pending`, `paid`, `failed`, `cancelled`).", "operationId": "listMyPurchases", "parameters": [{"name": "page", "required": false, "in": "query", "schema": {"example": 1, "type": "string"}}, {"name": "per_page", "required": false, "in": "query", "schema": {"example": 20, "type": "string"}}, {"name": "status", "required": false, "in": "query", "description": "Checkout order status filter", "schema": {"example": "paid", "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserPurchaseListResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "My purchase history", "tags": ["users"]}}, "/api/users/me/purchases/{checkoutId}": {"get": {"description": "Single checkout/purchase for the logged-in user: order + latest payment, line items, and issued tickets with QR when paid.", "operationId": "getMyPurchase", "parameters": [{"name": "checkoutId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserPurchaseDetailResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Purchase not found or not owned by the user"}}, "security": [{"bearer": []}], "summary": "My purchase detail", "tags": ["users"]}}, "/api/users/me/tickets/{ticketId}": {"get": {"description": "Mobile My Tickets / confirmation detail. Returns show/session/seat summary and `qr_payload` (HTTPS validate URL with `TRM:v1:{ticketNumber}:{qrCode}`) for client-side QR rendering. Only tickets owned by the caller.", "operationId": "getMyTicket", "parameters": [{"name": "ticketId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserTicketDetailResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Ticket not found or not owned by the user"}}, "security": [{"bearer": []}], "summary": "Get one of my tickets (e-ticket + QR payload)", "tags": ["users"]}}, "/api/seats/{seatId}/details": {"get": {"description": "Returns sightline, distance, and session-resolved price (Approach A). Requires show_id and show_session_id query params.", "operationId": "getDetails", "parameters": [{"name": "seatId", "required": true, "in": "path", "description": "Venue seat UUID", "schema": {"type": "string"}}, {"name": "show_id", "required": true, "in": "query", "schema": {"type": "string"}}, {"name": "show_session_id", "required": true, "in": "query", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeatDetailsResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Seat not found"}}, "security": [{"bearer": []}], "summary": "Seat metadata for a session", "tags": ["seats"]}}, "/api/checkout": {"post": {"description": "Seat-map checkout with optional `reservation_id` (8-minute price lock), line-itemised Trem Tix service fee (S$2.50/ticket), and optional `guest_email` for ticket delivery. Creates pending order + payment. When `PAYMENT_GATEWAY=stripe`, returns Stripe Payment Sheet `client_secret` (SGD only). Otherwise returns sandbox `payment_url`. Fulfillment via Stripe webhook + GET /checkout/return/status poll.", "operationId": "createCheckout", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateCheckoutDto"}}}}, "responses": {"201": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateCheckoutResponseDto"}}}}, "400": {"description": "Validation error or insufficient seats"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Show or tier not found"}}, "security": [{"bearer": []}], "summary": "Start ticket checkout", "tags": ["checkout"]}}, "/api/checkout/retry": {"post": {"description": "After payment failure, seats remain held for 3 minutes. Creates a new pending payment on the same checkout order while the hold is active.", "operationId": "retryCheckout", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RetryCheckoutPaymentDto"}}}}, "responses": {"201": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateCheckoutResponseDto"}}}}, "409": {"description": "Retry window expired or order not eligible"}}, "security": [{"bearer": []}], "summary": "Retry failed payment", "tags": ["checkout"]}}, "/api/checkout/payment/{paymentId}/begin-processing": {"post": {"description": "Pauses the price-lock timer while payment is processing (3DS redirect).", "operationId": "beginProcessing", "parameters": [{"name": "paymentId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "Timer paused"}}, "security": [{"bearer": []}], "summary": "Begin 3DS / payment redirect", "tags": ["checkout"]}}, "/api/checkout/return": {"get": {"description": "Called from the sandbox payment WebView after the user taps Success or Failed. Fulfills tickets on success. By default redirects to the checkout `return_url` with `checkout_id`, `payment_id`, and `status` (success|failed). When `Accept: application/json`, returns the completion payload instead (for CMS/API clients).", "operationId": "checkoutReturn", "parameters": [{"name": "payment_id", "required": true, "in": "query", "schema": {"type": "string"}}, {"name": "result", "required": true, "in": "query", "schema": {"enum": ["success", "failed"], "type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CheckoutCompleteResponseDto"}}}}, "400": {"description": "Invalid payment or result"}, "404": {"description": "Payment not found"}, "409": {"description": "Checkout order already paid or payment already submitted for this order id"}}, "summary": "Sandbox payment return (redirect or JSON)", "tags": ["checkout"]}}, "/api/checkout/return/status": {"get": {"description": "Read-only status for a checkout order (by `checkout_id`). For Stripe, polls PaymentIntent state and syncs before returning. Does not submit sandbox payment.", "operationId": "checkoutReturnStatus", "parameters": [{"name": "checkout_id", "required": true, "in": "query", "description": "Checkout order id from POST /api/checkout", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CheckoutPaymentStatusResponseDto"}}}}, "400": {"description": "Missing checkout_id"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Order not found or not owned by the user"}}, "security": [{"bearer": []}], "summary": "Check checkout payment status", "tags": ["checkout"]}}, "/api/checkout/points-redemption": {"get": {"description": "Used by checkout to preview Trem Credit. Rate comes from System \u2192 Points redeem (`loyalty_redemption_rates`).", "operationId": "pointsRedemption", "parameters": [{"name": "currency", "required": true, "in": "query", "schema": {"example": "SGD", "type": "string"}}], "responses": {"200": {"description": "Balance and conversion rate for this currency"}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Points balance and redeem rate for a currency", "tags": ["checkout"]}}, "/api/checkout/{checkoutId}/confirmation": {"get": {"description": "Mobile / CMS confirmation screen after payment succeeds. Returns booking_reference, show/session summary, and tickets with `qr_payload` (HTTPS validate URL containing `TRM:v1:{ticketNumber}:{qrCode}`) for client-side QR rendering. Order must be `paid` and owned by the caller. Poll `GET /api/checkout/return/status` until paid before calling this.", "operationId": "checkoutConfirmation", "parameters": [{"name": "checkoutId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CheckoutConfirmationResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Order not found or not owned by the user"}, "409": {"description": "Order is not paid yet"}}, "security": [{"bearer": []}], "summary": "Get paid checkout confirmation + ticket QR payloads", "tags": ["checkout"]}}, "/api/payments/sandbox/{paymentId}": {"get": {"description": "HTML page with Success / Failed buttons for payment simulation. No JWT required; payment id acts as the session token.", "operationId": "sandboxPage", "parameters": [{"name": "paymentId", "required": true, "in": "path", "description": "Payment UUID from POST /checkout", "schema": {"type": "string"}}], "responses": {"404": {"description": "Payment not found"}}, "summary": "Sandbox payment WebView (HTML)", "tags": ["payments"]}}, "/api/booking/reserve": {"post": {"description": "Creates an 8-minute seat hold with line-itemised pricing preview (including Trem Tix service fee). Timer pauses during 3DS payment redirect.", "operationId": "createReservation", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateBookingReservationDto"}}}}, "responses": {"201": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateBookingReservationResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Hold seats for checkout", "tags": ["booking"]}}, "/api/booking/reserve/{reservationId}": {"get": {"description": "Poll seconds_remaining for the seat hold. Amber at 3 minutes, red at 90 seconds (client UI).", "operationId": "getReservation", "parameters": [{"name": "reservationId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BookingReservationResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Reservation not found"}}, "security": [{"bearer": []}], "summary": "Price lock timer status", "tags": ["booking"]}}, "/api/booking/reserve/{reservationId}/extend": {"patch": {"description": "Restarts the 8-minute price-lock on the same reservation (e.g. after \u201cDo you need more time?\u201d). If the hold already timed out, seats must still be free.", "operationId": "extendReservation", "parameters": [{"name": "reservationId", "required": true, "in": "path", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BookingReservationResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Reservation not found"}}, "security": [{"bearer": []}], "summary": "Extend seat hold timer", "tags": ["booking"]}}, "/api/booking/reserve/{reservationId}/add-ons": {"patch": {"description": "Replaces add-ons on the reservation before checkout. Pass an empty items array to clear add-ons.", "operationId": "updateAddOns", "parameters": [{"name": "reservationId", "required": true, "in": "path", "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateReservationAddOnsDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BookingReservationResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Reservation not found"}}, "security": [{"bearer": []}], "summary": "Set optional add-ons on a seat hold", "tags": ["booking"]}}, "/api/hero-banner/batch": {"get": {"operationId": "getHeroBannerBatch", "parameters": [{"name": "cities", "required": false, "in": "query", "description": "Comma-separated city names (default: Singapore)", "schema": {"example": "Singapore,Tokyo"}}], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Hero banner shows for one or more cities", "tags": ["hero-banner"]}}, "/api/video-feed": {"get": {"operationId": "getVideoFeed", "parameters": [{"name": "city", "required": false, "in": "query", "description": "Filter clips to shows in this city", "schema": {}}, {"name": "limit", "required": false, "in": "query", "description": "Page size (default 10, max 25)", "schema": {}}, {"name": "cursor", "required": false, "in": "query", "description": "Opaque pagination cursor from previous response", "schema": {}}], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Paginated short video feed with show overlay", "tags": ["video-feed"]}}, "/api/trending": {"get": {"operationId": "getTrending", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TrendingListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "List trending entries (active only)", "tags": ["trending"]}}, "/api/recommended": {"get": {"operationId": "getRecommended", "parameters": [{"name": "city", "required": false, "in": "query", "description": "Optional city filter for the category query", "schema": {}}, {"name": "limit", "required": false, "in": "query", "description": "Number of items (default 6, between 2 and 12)", "schema": {}}, {"name": "categories", "required": false, "in": "query", "description": "Comma-separated category filter values (e.g. Musical)", "schema": {}}], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Recommended shows (by category match) or Popular in Asia fallback", "tags": ["recommended"]}}, "/api/categories": {"get": {"operationId": "getCategories", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "List filter categories for discovery UI", "tags": ["categories"]}}, "/api/cities": {"get": {"operationId": "list", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "List cities", "tags": ["cities"]}}, "/api/show-reviews/{showId}/eligibility": {"get": {"description": "Eligible for any signed-in user. Ticket ownership and show date are not required.", "operationId": "getEligibility", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ShowReviewEligibilityResponseDto"}}}}, "401": {"description": "Missing or invalid JWT"}}, "security": [{"bearer": []}], "summary": "Can I write a review for this show?", "tags": ["show-reviews"]}}, "/api/show-reviews/{showId}": {"post": {"description": "Multipart form. Reviews and optional video reactions are **auto-approved** (no moderator queue). Requires a signed-in user (JWT). Ticket ownership and show date are not required. Requires ffprobe/ffmpeg on the server to validate video length. Max seconds: REVIEW_VIDEO_MAX_SECONDS (default 30). Max upload size: REVIEW_VIDEO_MAX_BYTES.", "operationId": "submit", "parameters": [{"name": "showId", "required": true, "in": "path", "description": "Show UUID or legacy id", "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"type": "object", "required": ["star_rating"], "properties": {"star_rating": {"type": "integer", "minimum": 1, "maximum": 5, "description": "Required: 1\u20135 stars"}, "written_review": {"type": "string", "description": "Optional text (max 8000 chars)"}, "recommends": {"type": "string", "enum": ["true", "false", "1", "0", "yes", "no"], "description": "Optional; default false"}, "video": {"type": "string", "format": "binary", "description": "Optional video file"}}}}}}, "responses": {"201": {"description": ""}}, "security": [{"bearer": []}], "summary": "Submit a show review (rating, text, recommends, optional video)", "tags": ["show-reviews"]}}, "/api/upcoming-near-you": {"get": {"description": "Requires JWT. Shows in the chosen city with start_date in the next 30 days, soonest first. Pass ?city= or omit it to use the signed-in user profile city.", "operationId": "getUpcoming", "parameters": [{"name": "limit", "required": false, "in": "query", "description": "Max items (default 40, max 100)", "schema": {}}, {"name": "city", "required": false, "in": "query", "description": "City name (overrides profile city when set)", "schema": {}}], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Section E \u2014 Upcoming near you", "tags": ["upcoming-near-you"]}}, "/api/onboarding/interests/options": {"get": {"description": "Large-tile list. Client enforces min 2 selections or skip; skip copy defaults to trending.", "operationId": "getInterestOptions", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Screen 3 \u2014 interest categories", "tags": ["onboarding"]}}, "/api/onboarding/interests": {"post": {"description": "Send skip: true to clear interests and mark skipped; otherwise at least 2 distinct category ids.", "operationId": "saveInterests", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SaveInterestsDto"}}}}, "responses": {"201": {"description": ""}}, "security": [{"bearer": []}], "summary": "Save interests or skip", "tags": ["onboarding"]}}, "/api/onboarding/cities/options": {"get": {"description": "All cities. Each item id is a cities row UUID for POST /onboarding/city preset.", "operationId": "getCityOptions", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Screen 4 \u2014 city presets", "tags": ["onboarding"]}}, "/api/onboarding/city": {"post": {"description": "preset must be a city id from GET /onboarding/cities/options (UUID). Optional lat/lng if user allows location.", "operationId": "saveCity", "parameters": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SaveCityDto"}}}}, "responses": {"201": {"description": ""}}, "security": [{"bearer": []}], "summary": "Save city and optional device coordinates", "tags": ["onboarding"]}}, "/api/onboarding/status": {"get": {"operationId": "getStatus", "parameters": [], "responses": {"200": {"description": ""}}, "security": [{"bearer": []}], "summary": "Onboarding-related profile fields", "tags": ["onboarding"]}}, "/api/articles": {"get": {"operationId": "list", "parameters": [{"name": "page", "required": false, "in": "query", "schema": {"minimum": 1, "default": 1, "type": "number"}}, {"name": "limit", "required": false, "in": "query", "schema": {"minimum": 1, "maximum": 100, "default": 20, "type": "number"}}, {"name": "q", "required": false, "in": "query", "description": "Case-insensitive search over title/city/highlight", "schema": {"type": "string"}}, {"name": "tag", "required": false, "in": "query", "description": "Filter by article tag id", "schema": {"type": "string"}}], "responses": {"200": {"description": "Returns articles with pagination. Default limit is 20 items.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ArticlesListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "List articles", "tags": ["articles"]}}, "/api/surveys": {"get": {"description": "Returns active surveys (`status = 1`) the current user has not answered yet, with answer totals (`total_yes`, `total_no`, `total_maybe`), `total_responded`, and `percentage_interested` (% of yes answers). Inactive surveys (`status = 0`) are omitted. Surveys with an existing `survey_result` row for this user are also omitted.", "operationId": "list", "parameters": [{"name": "page", "required": false, "in": "query", "schema": {"minimum": 1, "default": 1, "type": "number"}}, {"name": "limit", "required": false, "in": "query", "description": "Use a high limit to fetch all surveys in one page", "schema": {"minimum": 1, "maximum": 100, "default": 20, "type": "number"}}, {"name": "category_id", "required": false, "in": "query", "description": "Filter by category id", "schema": {"type": "string"}}, {"name": "q", "required": false, "in": "query", "description": "Search survey name or category label", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SurveysListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "List surveys", "tags": ["surveys"]}}, "/api/surveys/{surveyId}/results": {"post": {"description": "Requires JWT. Records the signed-in user\u2019s answer (`yes`, `no`, or `maybe`) for a survey. Re-submitting updates the answer; `notify` is unchanged until you call the notify endpoint.", "operationId": "submit", "parameters": [{"name": "surveyId", "required": true, "in": "path", "description": "Survey UUID", "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubmitSurveyResultDto"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SurveyResultResponseDto"}}}}, "400": {"description": "Validation error or survey not open"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Survey not found"}}, "security": [{"bearer": []}], "summary": "Submit survey answer", "tags": ["surveys"]}}, "/api/surveys/{surveyId}/results/notify": {"post": {"description": "Requires JWT. Sets `notify` to `true` on the current user\u2019s existing result for this survey. Call after submitting an answer (e.g. when the user opts in to be notified).", "operationId": "enableNotify", "parameters": [{"name": "surveyId", "required": true, "in": "path", "description": "Survey UUID", "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SurveyResultNotifyResponseDto"}}}}, "400": {"description": "Survey not open"}, "401": {"description": "Missing or invalid JWT"}, "404": {"description": "Survey or result not found"}}, "security": [{"bearer": []}], "summary": "Enable notify for survey result", "tags": ["surveys"]}}, "/api/tags": {"get": {"operationId": "list", "parameters": [], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TagsListResponseDto"}}}}}, "security": [{"bearer": []}], "summary": "List tags", "tags": ["tags"]}}, "/api/tickets/validate": {"get": {"description": "Opens from phone camera QR scans. Query `code` is `TRM:v1:{ticketNumber}:{qrCode}` (URL-encoded). Returns HTML for browsers, JSON when Accept: application/json.", "operationId": "validate", "parameters": [{"name": "code", "required": true, "in": "query", "description": "Ticket QR token, e.g. TRM:v1:00000074:\u2026", "schema": {}}], "responses": {"200": {"description": "Validation result (HTML or JSON)"}}, "summary": "Validate a ticket QR (public)", "tags": ["tickets"]}}}, "info": {"title": "Trem API", "description": "Public app API: shows, hero banner, video feed, trending, recommendations, categories, reviews, onboarding, auth, push (FCM), checkout, and more. **Booking sessions** (availability, budget buckets, seat map) live under `/api/shows/{showId}/sessions/...` \u2014 see tag **show-sessions** (not `/api/sessions`). CMS/admin endpoints are documented separately at `/api/admin-docs`. All endpoints except `/auth/*` and the OpenAPI UI require `Authorization: Bearer <access_token>` (JWT from POST /auth/login, /auth/register, /auth/oauth, or /auth/refresh-token).", "version": "1.0", "contact": {}}, "tags": [{"name": "show-sessions", "description": "Trem Tix booking: list sessions, calendar, quantity availability, budget tiers, theatre map, row seat picker. Base path: /api/shows/{showId}/sessions/{sessionId}/\u2026"}, {"name": "shows", "description": "Show browse, detail, save, like, notifications"}], "servers": [{"url": "https://tremstgapi.axcova.com", "description": "Local"}, {"url": "https://tremdevapi.axcova.com", "description": "Dev"}], "components": {"securitySchemes": {"bearer": {"scheme": "bearer", "bearerFormat": "JWT", "type": "http", "description": "JWT access token from POST /api/auth/login, /api/auth/register, /api/auth/oauth, or /api/auth/refresh-token."}}, "schemas": {"AdminCronJobDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "name": {"type": "string", "example": "scheduler-heartbeat"}, "description": {"type": "object", "example": "Health check for trem-cron-scheduler"}, "cron_expression": {"type": "string", "example": "*/5 * * * *"}, "http_method": {"type": "string", "example": "GET"}, "request_path": {"type": "string", "example": "/api/internal/cron/ping"}, "enabled": {"type": "boolean", "example": true}, "notify_on_failure": {"type": "boolean", "example": false}, "last_run_at": {"type": "object"}, "last_run_status": {"type": "string", "enum": ["success", "failed", "skipped"]}, "last_run_error": {"type": "object"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "name", "cron_expression", "http_method", "request_path", "enabled", "notify_on_failure", "created_at", "updated_at"]}, "AdminCronJobListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminCronJobDto"}}}, "required": ["items"]}, "AdminCronJobListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCronJobListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateCronJobDto": {"type": "object", "properties": {"name": {"type": "string", "example": "heartbeat"}, "description": {"type": "object", "example": "Scheduler health check"}, "cron_expression": {"type": "string", "example": "*/5 * * * *", "description": "Standard cron (5 fields) or with seconds (6 fields)"}, "http_method": {"type": "string", "example": "GET", "default": "POST"}, "request_path": {"type": "string", "example": "/api/internal/cron/ping", "description": "Must start with /api/internal/cron/"}, "enabled": {"type": "boolean", "default": true}, "notify_on_failure": {"type": "boolean", "default": false}}, "required": ["name", "cron_expression", "request_path"]}, "AdminCronJobResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCronJobDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateCronJobDto": {"type": "object", "properties": {"name": {"type": "string", "example": "heartbeat"}, "description": {"type": "object"}, "cron_expression": {"type": "string", "example": "0 * * * *"}, "http_method": {"type": "string", "example": "GET"}, "request_path": {"type": "string", "example": "/api/internal/cron/ping"}, "enabled": {"type": "boolean"}, "notify_on_failure": {"type": "boolean"}}}, "AdminDeleteCronJobDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteCronJobResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteCronJobDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCronJobRunDataDto": {"type": "object", "properties": {"ok": {"type": "boolean", "example": true}, "status": {"type": "number", "example": 200}, "body": {"type": "string", "example": "{\"ok\":true}"}}, "required": ["ok", "status", "body"]}, "AdminCronJobRunResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCronJobRunDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminEmailQueueDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "to_email": {"type": "string", "example": "to@example.com"}, "from_email": {"type": "string", "example": "from@example.com"}, "subject": {"type": "string", "example": "Hello"}, "html_body": {"type": "object", "example": "<p>Hello</p>", "nullable": true}, "text_body": {"type": "object", "example": "Hello", "nullable": true}, "status": {"type": "string", "enum": ["pending", "sent", "received", "failed"], "example": "pending"}, "provider_message_id": {"type": "object", "example": "re_123", "nullable": true}, "attempts": {"type": "number", "example": 0}, "last_error": {"type": "object", "example": null, "nullable": true}, "locked_at": {"type": "object", "example": null, "nullable": true}, "sent_at": {"type": "object", "example": null, "nullable": true}, "received_at": {"type": "object", "example": null, "nullable": true}, "failed_at": {"type": "object", "example": null, "nullable": true}, "created_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "updated_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}}, "required": ["id", "to_email", "from_email", "subject", "html_body", "text_body", "status", "provider_message_id", "attempts", "last_error", "locked_at", "sent_at", "received_at", "failed_at", "created_at", "updated_at"]}, "AdminEmailQueueListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminEmailQueueDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminEmailQueueListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminEmailQueueListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminEmailQueueResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminEmailQueueDto"}}, "required": ["success", "timestamp", "data"]}, "AdminEnqueueEmailDto": {"type": "object", "properties": {"to_email": {"type": "string", "example": "to@example.com"}, "from_email": {"type": "string", "example": "from@example.com"}, "subject": {"type": "string", "example": "Welcome"}, "html_body": {"type": "object", "example": "<p>Hello</p>", "nullable": true}, "text_body": {"type": "object", "example": "Hello", "nullable": true}}}, "AdminEmailQueueEnqueueDataDto": {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}, "AdminEmailQueueEnqueueResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminEmailQueueEnqueueDataDto"}}, "required": ["success", "timestamp", "data"]}, "FcmWebConfigDataDto": {"type": "object", "properties": {"configured": {"type": "boolean"}, "apiKey": {"type": "string"}, "authDomain": {"type": "string"}, "projectId": {"type": "string"}, "storageBucket": {"type": "string"}, "messagingSenderId": {"type": "string"}, "appId": {"type": "string"}, "vapidKey": {"type": "string", "description": "Web Push VAPID key (public)"}, "missing": {"description": "Unset FIREBASE_WEB_* env keys", "type": "array", "items": {"type": "string"}}}, "required": ["configured", "missing"]}, "FcmWebConfigResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/FcmWebConfigDataDto"}}, "required": ["success", "timestamp", "data"]}, "RegisterFcmTokenDto": {"type": "object", "properties": {"token": {"type": "string", "description": "FCM registration token from `FirebaseMessaging.getToken()` (or platform equivalent)", "example": "dK3j9...long_string..."}, "platform": {"type": "string", "enum": ["android", "ios", "web"], "description": "Client platform (optional, for analytics/debugging)"}}, "required": ["token"]}, "RegisterFcmTokenDataDto": {"type": "object", "properties": {"id": {"type": "string", "description": "user_fcm_token.id"}, "user_id": {"type": "string", "description": "FK to users.id"}, "platform": {"type": "string", "nullable": true, "enum": ["android", "ios", "web"]}, "updated_at": {"type": "string", "description": "ISO timestamp"}}, "required": ["id", "user_id", "platform", "updated_at"]}, "RegisterFcmTokenResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/RegisterFcmTokenDataDto"}}, "required": ["success", "timestamp", "data"]}, "UnregisterFcmTokenDto": {"type": "object", "properties": {"token": {"type": "string", "description": "FCM registration token to remove for the current user"}}, "required": ["token"]}, "UnregisterFcmTokenDataDto": {"type": "object", "properties": {"removed": {"type": "boolean", "description": "Whether a matching row was deleted"}}, "required": ["removed"]}, "UnregisterFcmTokenResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UnregisterFcmTokenDataDto"}}, "required": ["success", "timestamp", "data"]}, "FcmTopicSubscribeDto": {"type": "object", "properties": {"topic": {"type": "string", "description": "FCM topic name (same as `FirebaseMessaging.subscribeToTopic` on the device)", "example": "xxx"}}, "required": ["topic"]}, "NotificationListItemDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "user_id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "title": {"type": "string", "example": "New show alert"}, "body": {"type": "string", "example": "Hamilton is back in Jakarta"}, "read": {"type": "number", "enum": [0, 1], "example": 0, "description": "0 = unread, 1 = read"}, "push_queue_id": {"type": "object", "nullable": true, "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}, "fcm_message_id": {"type": "object", "nullable": true, "example": "projects/xxx/messages/yyy"}, "topic": {"type": "object", "nullable": true, "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "description": "FCM topic used for delivery"}, "created_at": {"type": "string", "example": "2026-06-18T12:00:00.000Z"}, "read_at": {"type": "object", "nullable": true, "example": null}}, "required": ["id", "user_id", "title", "body", "read", "push_queue_id", "fcm_message_id", "topic", "created_at", "read_at"]}, "NotificationListDataDto": {"type": "object", "properties": {"items": {"description": "Latest notifications, max 50", "type": "array", "items": {"$ref": "#/components/schemas/NotificationListItemDto"}}, "limit": {"type": "number", "example": 50, "description": "Maximum rows returned (latest first)"}, "total": {"type": "number", "example": 12, "description": "Total notifications stored for this user"}, "unread_count": {"type": "number", "example": 3, "description": "Count of unread notifications for this user"}}, "required": ["items", "limit", "total", "unread_count"]}, "NotificationListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/NotificationListDataDto"}}, "required": ["success", "timestamp", "data"]}, "NotificationListItemResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/NotificationListItemDto"}}, "required": ["success", "timestamp", "data"]}, "AdminNotificationListItemDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "user_id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "title": {"type": "string", "example": "New show alert"}, "body": {"type": "string", "example": "Hamilton is back in Jakarta"}, "read": {"type": "number", "enum": [0, 1], "example": 0, "description": "0 = unread, 1 = read"}, "push_queue_id": {"type": "object", "nullable": true, "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}, "fcm_message_id": {"type": "object", "nullable": true, "example": "projects/xxx/messages/yyy"}, "topic": {"type": "object", "nullable": true, "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "description": "FCM topic used for delivery"}, "created_at": {"type": "string", "example": "2026-06-18T12:00:00.000Z"}, "read_at": {"type": "object", "nullable": true, "example": null}, "user_name": {"type": "object", "nullable": true, "example": "Jane Doe", "description": "Recipient display name from users.name"}, "show_id": {"type": "object", "nullable": true, "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_color": {"type": "object", "nullable": true, "example": "#FF5733"}, "type": {"type": "object", "nullable": true, "example": "ticket_price_change"}, "fcm_payload": {"type": "object", "nullable": true, "example": {"topic": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "notification": {"title": "New show alert", "body": "Hamilton is back in Jakarta"}, "data": {"show_id": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10", "type": "ticket_price_change", "show_color": "#FF5733"}}, "description": "Full FCM message payload sent to the topic"}}, "required": ["id", "user_id", "title", "body", "read", "push_queue_id", "fcm_message_id", "topic", "created_at", "read_at", "user_name", "show_id", "show_color", "type", "fcm_payload"]}, "AdminNotificationListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminNotificationListItemDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminNotificationListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminNotificationListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminPushQueueDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "kind": {"type": "string", "enum": ["user", "topic"], "example": "user"}, "user_id": {"type": "object", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10", "nullable": true}, "user_name": {"type": "object", "example": "Jane Doe", "nullable": true}, "user_email": {"type": "object", "example": "jane@example.com", "nullable": true}, "topic": {"type": "object", "example": "ticket-price-drop", "nullable": true}, "title": {"type": "object", "example": "Price drop", "nullable": true}, "body": {"type": "object", "example": "Tickets are now cheaper", "nullable": true}, "message": {"type": "object", "example": null, "nullable": true}, "type": {"type": "object", "example": null, "nullable": true}, "show_id": {"type": "object", "example": null, "nullable": true, "description": "Related show UUID when applicable"}, "show_color": {"type": "object", "example": null, "nullable": true, "description": "Show brand color (hex) when applicable"}, "fcm_payload": {"type": "object", "nullable": true, "example": {"topic": "show-uuid_ticket-price-drop", "notification": {"title": "Price drop", "body": "Price drop for CATS"}, "data": {"show_id": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10", "type": "ticket_price_change", "show_color": "#FF5733"}}, "description": "Full FCM message payload for this queue row"}, "status": {"type": "string", "enum": ["pending", "sent", "failed"], "example": "pending"}, "attempts": {"type": "number", "example": 0}, "last_error": {"type": "object", "example": null, "nullable": true}, "locked_at": {"type": "object", "example": null, "nullable": true}, "sent_at": {"type": "object", "example": null, "nullable": true}, "failed_at": {"type": "object", "example": null, "nullable": true}, "created_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "updated_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}}, "required": ["id", "kind", "user_id", "user_name", "user_email", "topic", "title", "body", "message", "type", "show_id", "show_color", "fcm_payload", "status", "attempts", "last_error", "locked_at", "sent_at", "failed_at", "created_at", "updated_at"]}, "AdminPushQueueListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminPushQueueDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminPushQueueListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminPushQueueListDataDto"}}, "required": ["success", "timestamp", "data"]}, "RegisterDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Jane Doe"}, "email": {"type": "string", "format": "email", "example": "user@example.com"}, "password": {"type": "string", "format": "password", "minLength": 8, "description": "At least 8 characters", "example": "securePass1"}}, "required": ["name", "email", "password"]}, "AuthUserLocationDto": {"type": "object", "properties": {"latitude": {"type": "number", "example": 3.139}, "longitude": {"type": "number", "example": 101.6869}}, "required": ["latitude", "longitude"]}, "AuthUserDto": {"type": "object", "properties": {"id": {"type": "string", "example": "def36a9f-6600-48d4-9922-be6b1a00c04c"}, "email": {"type": "string", "example": "test@test.com"}, "name": {"type": "string", "example": "Test1"}, "city": {"type": "string", "nullable": true, "example": "Kuala Lumpur"}, "city_id": {"type": "string", "nullable": true, "description": "City UUID (FK to cities.id)", "example": "123b8abc-7c58-4fad-a137-4da4059426e3"}, "interests": {"description": "Interest category ids", "example": ["theatre", "family"], "type": "array", "items": {"type": "string"}}, "location": {"nullable": true, "description": "Device coordinates, if user allowed and they were saved (via onboarding city step).", "allOf": [{"$ref": "#/components/schemas/AuthUserLocationDto"}]}, "auth_provider": {"type": "string", "enum": ["email", "google", "apple"], "example": "email"}, "created_at": {"type": "string", "description": "User account creation timestamp (ISO 8601)", "example": "2026-04-20T13:35:43.727Z"}}, "required": ["id", "email", "name", "interests", "auth_provider", "created_at"]}, "AuthTokensDataDto": {"type": "object", "properties": {"access_token": {"type": "string", "description": "JWT access token", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.***"}, "refresh_token": {"type": "string", "description": "Opaque refresh token", "example": "jp2ih53_F5fEpfGdIkiLkj2dbrCiGLIiQsAmx4_L_8La4Qi-G-tSdtmSkLTEVF-J"}, "user": {"$ref": "#/components/schemas/AuthUserDto"}}, "required": ["access_token", "refresh_token", "user"]}, "AuthTokensResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AuthTokensDataDto"}}, "required": ["success", "timestamp", "data"]}, "LoginDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com"}, "password": {"type": "string", "format": "password", "minLength": 1, "example": "your-password"}}, "required": ["email", "password"]}, "OAuthDto": {"type": "object", "properties": {"provider": {"type": "string", "enum": ["google", "apple"], "example": "google"}, "oauth_token": {"type": "string", "description": "Google ID token or Apple identity token (JWT string)", "minLength": 10}, "name": {"type": "string", "description": "Display name from the Apple authorization credential. Apple sends this only on the first sign-in, and it is not inside the identity token.", "example": "Jane Tan", "maxLength": 100}}, "required": ["provider", "oauth_token"]}, "RefreshDto": {"type": "object", "properties": {"refresh_token": {"type": "string", "description": "Opaque refresh token from login/register/oauth response", "minLength": 10}}, "required": ["refresh_token"]}, "LogoutDto": {"type": "object", "properties": {"refresh_token": {"type": "string", "description": "Refresh token to revoke", "minLength": 10}}, "required": ["refresh_token"]}, "ForgotPasswordDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com"}}, "required": ["email"]}, "AuthMessageDataDto": {"type": "object", "properties": {"message": {"type": "string", "example": "If an account exists for that email, we sent a reset link."}, "expires_in_seconds": {"type": "number", "example": 120, "description": "Present on the mobile reset-code response. Code lifetime in seconds."}}, "required": ["message"]}, "AuthMessageResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AuthMessageDataDto"}}, "required": ["success", "timestamp", "data"]}, "ResetPasswordDto": {"type": "object", "properties": {"token": {"type": "string", "description": "Raw token from the reset email link (query param `token`)", "example": "k7nQ2vR9pLwX\u2026"}, "password": {"type": "string", "format": "password", "minLength": 8, "description": "New password, at least 8 characters", "example": "securePass1"}}, "required": ["token", "password"]}, "OtpSendDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com", "description": "Email address that will receive the 6-digit OTP"}, "purpose": {"type": "string", "enum": ["generic", "guest_checkout"], "default": "generic", "description": "OTP purpose. Use `guest_checkout` for web guest booking (verify returns session tokens)."}}, "required": ["email"]}, "OtpVerifyDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com", "description": "Email address that received the OTP"}, "otp": {"type": "string", "example": "482913", "description": "6-digit one-time password from the email"}}, "required": ["email", "otp"]}, "AdminInfoDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "email": {"type": "string", "example": "demo@trem.com"}, "name": {"type": "string", "example": "Demo Admin"}}, "required": ["id", "email", "name"]}, "AdminLoginDataDto": {"type": "object", "properties": {"access_token": {"type": "string", "description": "JWT access token (role=admin)", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.***"}, "refresh_token": {"type": "string", "description": "Opaque refresh token", "example": "yG6t9tGfQ0gO1YdQe0mVh9h4d4wOQw0HkG9qGmQpKQw"}, "admin": {"$ref": "#/components/schemas/AdminInfoDto"}}, "required": ["access_token", "refresh_token", "admin"]}, "AdminLoginResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminLoginDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminRefreshDataDto": {"type": "object", "properties": {"access_token": {"type": "string", "description": "JWT access token (role=admin)", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.***"}, "refresh_token": {"type": "string", "description": "Rotated opaque refresh token", "example": "m4N9k0d1P2fP6mV7w3jD8oE2Yx2mB1qW0cE9oQ0pQ1I"}}, "required": ["access_token", "refresh_token"]}, "AdminRefreshResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminRefreshDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminLogoutDataDto": {"type": "object", "properties": {"message": {"type": "string", "example": "Logged out"}}, "required": ["message"]}, "AdminLogoutResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminLogoutDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateShowDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Hamilton"}, "city_id": {"type": "string", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "venue_id": {"type": "object", "description": "Venue UUID (FK to venues.id). Required for Show Onboarding / seating prices.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "nullable": true}, "show_image_url": {"type": "string", "description": "Landscape / preview image URL (can be placeholder; poster uses image_poster)", "example": "https://example.com/show.jpg"}, "image_poster": {"type": "object", "description": "Portrait/card poster image URL", "example": "https://example.com/show-poster.jpg", "nullable": true}, "start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)", "example": "2026-04-20"}, "end_date": {"type": "string", "description": "End date (YYYY-MM-DD); show is archived after this date", "example": "2026-05-20"}, "description": {"type": "object"}}, "required": ["name", "city_id", "show_image_url", "start_date", "end_date"]}, "AdminShowRowDto": {"type": "object", "properties": {"id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "name": {"type": "string", "example": "Hamilton"}, "city": {"type": "string", "example": "Singapore"}, "city_id": {"type": "string", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "start_date": {"type": "string", "example": "2026-04-20"}, "end_date": {"type": "string", "example": "2026-05-20", "description": "Show run end date; archive cron uses this"}, "category": {"type": "object", "example": "Musical"}, "categories": {"example": ["Musical", "Family"], "type": "array", "items": {"type": "string"}}, "color_scheme": {"type": "object", "example": "#7c3aed"}, "preview_mode": {"type": "string", "enum": ["video", "image"], "example": "video", "description": "Preview mode for the show card"}, "status": {"type": "number", "description": "Show visibility: 1 = active (visible in app), 0 = inactive", "enum": [0, 1], "example": 1}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}, "ticket_available": {"type": "object", "description": "Combined remaining seats across all sessions and tiers (from show_ticket_balance.seat_balance)", "example": 42, "nullable": true}}, "required": ["id", "name", "city", "city_id", "start_date", "end_date", "categories", "preview_mode", "status", "created_at"]}, "AdminShowRowResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowRowDto"}}, "required": ["success", "timestamp", "data"]}, "AdminShowsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminShowRowDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 123}, "total_pages": {"type": "number", "example": 5}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminShowsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowTagDto": {"type": "object", "properties": {"id": {"type": "string", "example": "headline"}, "label": {"type": "string", "example": "Headline"}}, "required": ["id", "label"]}, "ShowVideoDto": {"type": "object", "properties": {"id": {"type": "string", "example": "78850a4c-a1bb-4b59-9966-3f1b79e08e69"}, "video_url": {"type": "string", "example": "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"}, "title": {"type": "object", "nullable": true, "example": "Opening night highlight", "description": "Display title for this video clip"}, "poster_image_url": {"type": "object", "nullable": true, "description": "Poster for this video only. A frame from the file is captured on upload.", "example": "https://picsum.photos/seed/nusantara-sv-hamilton-1/1280/720"}, "gif_url": {"type": "object", "nullable": true, "description": "Animated GIF from 10 random frames of this video, generated on upload.", "example": "https://cdn.example.com/uploads/show-videos/clip.gif"}, "duration_seconds": {"type": "number", "example": 18}, "kind": {"type": "string", "example": "clip"}, "sort_order": {"type": "number", "example": 10}, "is_main": {"type": "boolean", "example": false, "description": "Indicates the main/hero video for the show."}}, "required": ["id", "video_url", "duration_seconds", "kind", "sort_order", "is_main"]}, "ShowVideoReactionDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "video_url": {"type": "string", "example": "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"}, "duration_secs": {"type": "number", "example": 12}, "moderation_status": {"type": "string", "example": "approved"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}, "approved_at": {"type": "object", "example": "2026-04-11T18:49:41.750Z"}}, "required": ["id", "video_url", "duration_secs", "moderation_status", "created_at"]}, "ShowReviewDto": {"type": "object", "properties": {"id": {"type": "string", "example": "9e1e1dbd-09d2-4900-981c-00eddc5fa468"}, "user_id": {"type": "object", "nullable": true, "description": "Reviewer user id", "example": "a439e524-3a8a-487a-b897-2c3a231a3fda"}, "user_name": {"type": "object", "nullable": true, "description": "Reviewer display name", "example": "Jane Doe"}, "star_rating": {"type": "object", "example": 5}, "written_review": {"type": "object", "example": "Absolutely incredible \u2014 the energy was unreal."}, "recommends": {"type": "boolean", "example": true}, "verified_purchase": {"type": "boolean", "example": false}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}, "moderation_status": {"type": "string", "example": "approved"}, "video_reactions": {"type": "array", "items": {"$ref": "#/components/schemas/ShowVideoReactionDto"}}}, "required": ["id", "recommends", "verified_purchase", "created_at", "moderation_status", "video_reactions"]}, "ShowSessionDto": {"type": "object", "properties": {"id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "session_date": {"type": "string", "example": "2026-06-15", "description": "Performance date (YYYY-MM-DD)"}, "session_time": {"type": "string", "example": "19:30", "description": "Performance time (HH:mm)"}}, "required": ["id", "session_date", "session_time"]}, "ShowNextSessionDto": {"type": "object", "properties": {"id": {"type": "string"}, "session_date": {"type": "string", "example": "2026-06-14"}, "session_time": {"type": "string", "example": "19:30"}, "ticket_price_from": {"type": "object", "example": 78, "nullable": true}}, "required": ["id", "session_date", "session_time"]}, "ShowUrgencyDto": {"type": "object", "properties": {"type": {"type": "string", "enum": ["selling_fast", "low_stock"]}, "message": {"type": "string", "example": "Limited S$138 seats remain for Sat 14 Jun 7:30pm"}, "session_id": {"type": "string"}, "session_date": {"type": "string"}, "session_time": {"type": "string"}, "price_from": {"type": "number", "example": 138}, "remaining_seats": {"type": "number", "example": 12}}, "required": ["type", "message", "session_id", "session_date", "session_time", "price_from", "remaining_seats"]}, "ShowArticleDto": {"type": "object", "properties": {"id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "title": {"type": "string", "example": "Hamilton opens in Singapore"}, "url": {"type": "string", "example": "https://example.com/article/hamilton"}, "image_url": {"type": "object", "example": "https://cdn.example.com/article.jpg", "nullable": true}, "city_venue": {"type": "string", "example": "Singapore \u00b7 Marina Bay Sands"}, "news_date": {"type": "string", "example": "2026-05-12"}, "highlight": {"type": "object", "example": "Must-read", "nullable": true}, "tags": {"type": "array", "items": {"$ref": "#/components/schemas/ShowTagDto"}}}, "required": ["id", "title", "url", "city_venue", "news_date", "tags"]}, "ShowCardDto": {"type": "object", "properties": {"id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "name": {"type": "string", "example": "Hamilton"}, "city": {"type": "string", "example": "Singapore"}, "city_id": {"type": "string", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "is_saved": {"type": "number", "description": "1 if the current user has saved this show (show_saved.status=1), else 0.", "example": 1}, "show_image_url": {"type": "string", "example": "https://picsum.photos/seed/nusantara-show-hamilton/1200/1600"}, "image_poster": {"type": "object", "description": "Portrait/card poster image URL", "example": "https://picsum.photos/seed/nusantara-show-hamilton-poster/800/1200", "nullable": true}, "description": {"type": "object", "example": null}, "venue_id": {"type": "object", "description": "FK to venues.id", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a", "nullable": true}, "venue_name": {"type": "object", "example": "Esplanade Theatre", "nullable": true}, "venue_lat": {"type": "object", "example": 1.28967, "nullable": true}, "venue_lng": {"type": "object", "example": 103.85007, "nullable": true}, "subtitles": {"example": [], "type": "array", "items": {"type": "string"}}, "start_date": {"type": "string", "example": "2026-05-12"}, "end_date": {"type": "string", "example": "2026-06-12", "description": "Show run end date"}, "rating_average": {"type": "number", "example": 4.8}, "rating_count": {"type": "number", "example": 245}, "categories": {"example": ["Musical"], "type": "array", "items": {"type": "string"}}, "tags": {"description": "Tags from the `tags` table (e.g. Headline, Latest)", "type": "array", "items": {"$ref": "#/components/schemas/ShowTagDto"}}, "preview_mode": {"type": "string", "enum": ["video", "image"], "example": "video", "description": "Preview mode for the show card"}, "status": {"type": "number", "description": "Show visibility: 1 = active (visible in app), 0 = inactive", "enum": [0, 1], "example": 1}, "videos": {"type": "array", "items": {"$ref": "#/components/schemas/ShowVideoDto"}}, "reviews": {"type": "array", "items": {"$ref": "#/components/schemas/ShowReviewDto"}}, "show_sessions": {"type": "array", "items": {"$ref": "#/components/schemas/ShowSessionDto"}}, "ticket_price_from": {"type": "object", "description": "Lowest ticket tier price for this show", "example": 88, "nullable": true}, "ticket_price_to": {"type": "object", "description": "Highest ticket tier price for this show", "example": 270, "nullable": true}, "next_session": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/ShowNextSessionDto"}]}, "urgency": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/ShowUrgencyDto"}]}, "articles": {"description": "Articles linked to this show", "type": "array", "items": {"$ref": "#/components/schemas/ShowArticleDto"}}}, "required": ["id", "name", "city", "city_id", "is_saved", "show_image_url", "subtitles", "start_date", "end_date", "rating_average", "rating_count", "categories", "tags", "preview_mode", "status", "videos", "reviews", "show_sessions", "articles"]}, "ShowCardResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowCardDto"}}, "required": ["success", "timestamp", "data"]}, "AdminApplyShowSetupDto": {"type": "object", "properties": {"target_show_ids": {"description": "Target show UUIDs to receive sessions + seating/pricing from the source show", "example": ["11111111-1111-1111-1111-111111111111"], "type": "array", "items": {"type": "string"}}}, "required": ["target_show_ids"]}, "AdminBulkUpdateShowItemDto": {"type": "object", "properties": {"id": {"type": "string", "description": "Show UUID"}, "name": {"type": "string"}, "city_id": {"type": "string"}, "start_date": {"type": "string"}, "end_date": {"type": "string"}, "venue_id": {"type": "object", "nullable": true}, "status": {"type": "number", "enum": [0, 1]}, "preview_mode": {"type": "string", "enum": ["video", "image"]}, "loyalty_points_per_booking": {"type": "number"}}, "required": ["id"]}, "AdminBulkUpdateShowsDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminBulkUpdateShowItemDto"}}}, "required": ["items"]}, "AdminDeleteShowDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteShowResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteShowDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateShowAddOnDto": {"type": "object", "properties": {"type": {"type": "string", "example": "merchandise"}, "title": {"type": "string", "example": "Show programme"}, "description": {"type": "object", "example": "Collect at the foyer before the show."}, "price": {"type": "number", "example": 25}, "currency": {"type": "string", "example": "SGD"}, "sort_order": {"type": "number", "example": 0}, "is_active": {"type": "boolean", "example": true}, "image_url": {"type": "object", "example": null, "description": "Image URL (set via upload endpoint)"}}, "required": ["type", "title", "price"]}, "AdminUpdateShowAddOnDto": {"type": "object", "properties": {"type": {"type": "string", "example": "merchandise"}, "title": {"type": "string", "example": "Show programme"}, "description": {"type": "object"}, "price": {"type": "number", "example": 25}, "currency": {"type": "string", "example": "SGD"}, "sort_order": {"type": "number"}, "is_active": {"type": "boolean"}, "image_url": {"type": "object", "example": null, "description": "Image URL (set via upload endpoint)"}}}, "AdminCreateShowPricingCategoryDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Student"}, "base_price": {"type": "number", "minimum": 0, "example": 100}, "discount_percent": {"type": "number", "minimum": 0, "maximum": 100, "example": 20, "description": "Discount percent 0\u2013100"}, "currency": {"type": "string", "default": "SGD"}, "color": {"type": "string", "example": "#6366F1", "description": "Hex color #RRGGBB for seat map / category display"}, "sort_order": {"type": "number", "minimum": 0}, "parent_id": {"type": "string", "description": "Parent category id when creating a sub-category"}}, "required": ["name", "base_price", "discount_percent"]}, "AdminUpdateShowPricingCategoryDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Student"}, "base_price": {"type": "number", "minimum": 0}, "discount_percent": {"type": "number", "minimum": 0, "maximum": 100}, "currency": {"type": "string"}, "color": {"type": "string", "example": "#6366F1", "description": "Hex color #RRGGBB for seat map / category display"}, "sort_order": {"type": "number", "minimum": 0}, "parent_id": {"type": "object", "description": "Parent category id; null for top-level"}}}, "AdminAdjustLoyaltyPointsDto": {"type": "object", "properties": {"points_delta": {"type": "number", "description": "Points to add (positive) or deduct (negative)", "example": 100}, "note": {"type": "string", "example": "Manual goodwill credit"}}, "required": ["points_delta"]}, "AdminCreateCategoryDto": {"type": "object", "properties": {"id": {"type": "string", "description": "Category id (string key). Used by clients as a stable identifier.", "example": "musical"}, "label": {"type": "string", "example": "Musical"}, "icon_url": {"type": "string", "description": "Public icon URL", "example": "https://example.com/icons/musical.png"}, "filter_type": {"type": "string", "description": "Filter routing type", "enum": ["category", "all"], "example": "category"}, "filter_value": {"type": "object", "description": "Filter value used by discovery UI; required when filter_type=category", "example": "Musical"}}, "required": ["id", "label", "icon_url", "filter_type"]}, "AdminCategoryDto": {"type": "object", "properties": {"id": {"type": "string", "example": "musical"}, "label": {"type": "string", "example": "Musical"}, "icon_url": {"type": "string", "example": "https://example.com/icons/musical.png"}, "filter_type": {"type": "string", "enum": ["category", "all"], "example": "category"}, "filter_value": {"type": "object", "example": "Musical"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}}, "required": ["id", "label", "icon_url", "filter_type", "created_at"]}, "AdminCategoryResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCategoryDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCategoriesListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminCategoryDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminCategoriesListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCategoriesListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateCategoryDto": {"type": "object", "properties": {"label": {"type": "string", "example": "Musical"}, "icon_url": {"type": "string", "description": "Public icon URL", "example": "https://example.com/icons/musical.png"}, "filter_type": {"type": "string", "description": "Filter routing type", "enum": ["category", "all"], "example": "category"}, "filter_value": {"type": "object", "description": "Filter value used by discovery UI; required when filter_type=category", "example": "Musical"}}}, "AdminDeleteCategoryDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteCategoryResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteCategoryDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateCityDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Singapore"}}, "required": ["name"]}, "AdminCityDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "name": {"type": "string", "example": "Singapore"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}}, "required": ["id", "name", "created_at"]}, "AdminCityResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCityDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCitiesListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminCityDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminCitiesListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminCitiesListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateCityDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Kuala Lumpur"}}}, "AdminDeleteCityDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteCityResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteCityDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateVenueDto": {"type": "object", "properties": {"location_name": {"type": "string", "example": "Esplanade Theatre"}, "lat": {"type": "object", "example": 1.28967, "nullable": true}, "lng": {"type": "object", "example": 103.85007, "description": "Longitude", "nullable": true}, "city_id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}}, "required": ["location_name", "city_id"]}, "AdminVenueDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "location_name": {"type": "string", "example": "Esplanade Theatre"}, "lat": {"type": "object", "example": 1.28967, "nullable": true}, "lng": {"type": "object", "example": 103.85007, "description": "Longitude", "nullable": true}, "city_id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "city_name": {"type": "string", "example": "Singapore"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}}, "required": ["id", "location_name", "city_id", "city_name", "created_at"]}, "AdminVenueResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminVenueDto"}}, "required": ["success", "timestamp", "data"]}, "AdminVenuesListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminVenueDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminVenuesListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminVenuesListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateVenueDto": {"type": "object", "properties": {"location_name": {"type": "string", "example": "Esplanade Theatre"}, "lat": {"type": "object", "example": 1.28967, "nullable": true}, "lng": {"type": "object", "example": 103.85007, "description": "Longitude", "nullable": true}, "city_id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}}}, "AdminDeleteVenueDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteVenueResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteVenueDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateUserDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com"}, "name": {"type": "string", "example": "Jane Doe"}, "password": {"type": "string", "format": "password", "minLength": 8, "description": "If set, stores password_hash (bcrypt)."}, "city_id": {"type": "object", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "categories": {"description": "Category ids", "example": ["family", "theatre"], "type": "array", "items": {"type": "string"}}, "auth_provider": {"type": "string", "enum": ["email", "google", "apple"], "example": "email"}, "provider_subject": {"type": "object", "example": null}}, "required": ["email", "name"]}, "AdminUserDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "email": {"type": "string", "example": "user@example.com"}, "password_hash": {"type": "object", "example": null}, "name": {"type": "string", "example": "Jane Doe"}, "city": {"type": "object", "example": "Singapore"}, "city_id": {"type": "object", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "categories": {"description": "Category ids", "example": ["family", "theatre"], "type": "array", "items": {"type": "string"}}, "auth_provider": {"type": "string", "enum": ["email", "google", "apple"], "example": "email"}, "provider_subject": {"type": "object", "example": null}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}, "updated_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}}, "required": ["id", "email", "name", "categories", "auth_provider", "created_at", "updated_at"]}, "AdminUserResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUsersListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUserDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminUsersListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUsersListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUserMembershipDto": {"type": "object", "properties": {"id": {"type": "string"}, "user_id": {"type": "string"}, "tier_id": {"type": "string"}, "tier_code": {"type": "string", "example": "platinum"}, "tier_name": {"type": "string", "example": "BASE PLATINUM MEMBER"}, "year": {"type": "number", "example": 2026}, "tickets_remaining": {"type": "number", "example": 8}, "complimentary_tickets_per_year": {"type": "number", "example": 10}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "user_id", "tier_id", "tier_code", "tier_name", "year", "tickets_remaining", "complimentary_tickets_per_year", "created_at", "updated_at"]}, "AdminUserMembershipResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"nullable": true, "description": "Null when user has no membership for the requested year", "allOf": [{"$ref": "#/components/schemas/AdminUserMembershipDto"}]}}, "required": ["success", "timestamp", "data"]}, "AdminUpsertUserMembershipDto": {"type": "object", "properties": {"tier_id": {"type": "string", "description": "Membership tier UUID"}, "year": {"type": "number", "example": 2026, "description": "Calendar year (defaults to current year)"}, "tickets_remaining": {"type": "number", "example": 8, "description": "Complimentary tickets left this year (defaults to tier annual quota on create)"}}, "required": ["tier_id"]}, "AdminDeleteUserMembershipDataDto": {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}, "AdminDeleteUserMembershipResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteUserMembershipDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateUserDto": {"type": "object", "properties": {"email": {"type": "string", "format": "email", "example": "user@example.com"}, "name": {"type": "string", "example": "Jane Doe"}, "password": {"type": "string", "format": "password", "minLength": 8, "description": "If set, updates password_hash (bcrypt)."}, "city_id": {"type": "object", "description": "City UUID (FK to cities.id)", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "categories": {"description": "Category ids", "example": ["family", "theatre"], "type": "array", "items": {"type": "string"}}, "auth_provider": {"type": "string", "enum": ["email", "google", "apple"], "example": "email"}, "provider_subject": {"type": "object", "example": null}}}, "AdminDeleteUserDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteUserResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteUserDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminReviewDto": {"type": "object", "properties": {"id": {"type": "string", "example": "9e1e1dbd-09d2-4900-981c-00eddc5fa468"}, "show_id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "user_id": {"type": "object", "example": null}, "star_rating": {"type": "object", "example": 5}, "written_review": {"type": "object", "example": "Amazing show."}, "recommends": {"type": "boolean", "example": true}, "verified_purchase": {"type": "boolean", "example": false}, "moderation_status": {"type": "string", "example": "approved"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}, "video_reactions": {"type": "array", "items": {"$ref": "#/components/schemas/ShowVideoReactionDto"}}}, "required": ["id", "show_id", "recommends", "verified_purchase", "moderation_status", "created_at", "video_reactions"]}, "AdminReviewListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminReviewDto"}}}, "required": ["items"]}, "AdminReviewListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminReviewListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateReviewDto": {"type": "object", "properties": {"user_id": {"type": "object", "description": "Optional reviewer user id", "example": null}, "star_rating": {"type": "object", "minimum": 1, "maximum": 5, "example": 5}, "written_review": {"type": "object", "example": "Amazing show."}, "recommends": {"type": "boolean", "example": true, "default": false}, "verified_purchase": {"type": "boolean", "example": false, "default": false}, "moderation_status": {"type": "string", "enum": ["pending", "approved", "rejected"], "example": "approved"}}}, "AdminReviewResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminReviewDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateReviewDto": {"type": "object", "properties": {"user_id": {"type": "object", "description": "Optional reviewer user id", "example": null}, "star_rating": {"type": "object", "minimum": 1, "maximum": 5, "example": 4}, "written_review": {"type": "object", "example": "Updated review."}, "recommends": {"type": "boolean", "example": true}, "verified_purchase": {"type": "boolean", "example": false}, "moderation_status": {"type": "string", "enum": ["pending", "approved", "rejected"], "example": "approved"}}}, "AdminDeleteReviewDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteReviewResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteReviewDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminVideoReactionDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "show_id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "user_id": {"type": "object", "example": null}, "review_id": {"type": "object", "example": null}, "video_url": {"type": "string", "example": "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"}, "duration_secs": {"type": "number", "example": 12}, "moderation_status": {"type": "string", "example": "approved"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}, "approved_at": {"type": "object", "example": "2026-04-11T18:49:41.750Z"}}, "required": ["id", "show_id", "video_url", "duration_secs", "moderation_status", "created_at"]}, "AdminVideoReactionListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminVideoReactionDto"}}}, "required": ["items"]}, "AdminVideoReactionListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminVideoReactionListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminVideoReactionUploadDataDto": {"type": "object", "properties": {"video_url": {"type": "string", "example": "/uploads/review-videos/example.mp4"}, "duration_secs": {"type": "number", "example": 12}}, "required": ["video_url", "duration_secs"]}, "AdminVideoReactionUploadResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminVideoReactionUploadDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateVideoReactionDto": {"type": "object", "properties": {"user_id": {"type": "object", "description": "Optional reviewer user id", "example": null}, "review_id": {"type": "object", "description": "Optional link to a review on this show", "example": null}, "video_url": {"type": "string", "example": "/uploads/review-videos/example.mp4"}, "duration_secs": {"type": "number", "example": 12, "minimum": 1}, "moderation_status": {"type": "string", "enum": ["pending", "approved", "rejected"], "example": "approved"}}, "required": ["video_url", "duration_secs"]}, "AdminVideoReactionResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminVideoReactionDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateVideoReactionDto": {"type": "object", "properties": {"user_id": {"type": "object", "description": "Optional reviewer user id", "example": null}, "review_id": {"type": "object", "description": "Optional link to a review on this show", "example": null}, "video_url": {"type": "string", "example": "/uploads/review-videos/example.mp4"}, "duration_secs": {"type": "number", "example": 12, "minimum": 1}, "moderation_status": {"type": "string", "enum": ["pending", "approved", "rejected"], "example": "approved"}}}, "AdminDeleteVideoReactionDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteVideoReactionResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteVideoReactionDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTicketDto": {"type": "object", "properties": {"id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "show_id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "user_id": {"type": "object", "example": null}, "show_session_id": {"type": "object", "example": null}, "session_id": {"type": "object", "description": "Alias for show_session_id", "example": null}, "category_id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1b"}, "tier_name": {"type": "string", "example": "Premium"}, "price": {"type": "number", "example": 168}, "currency": {"type": "string", "example": "SGD"}, "seat_number": {"type": "object", "example": "A-12"}, "ticket_status": {"type": "string", "example": "reserved"}, "qr_code": {"type": "object", "example": null}, "purchased_at": {"type": "object", "example": null}, "used_at": {"type": "object", "example": null}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}}, "required": ["id", "show_id", "category_id", "tier_name", "price", "currency", "ticket_status", "created_at"]}, "AdminTicketListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminTicketDto"}}}, "required": ["items"]}, "AdminTicketListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTicketListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateTicketDto": {"type": "object", "properties": {"category_id": {"type": "string", "description": "Ticket tier / category for this show", "format": "uuid"}, "user_id": {"type": "object", "description": "Buyer user id", "nullable": true, "format": "uuid"}, "show_session_id": {"type": "object", "description": "Show session id (FK to show_sessions)", "nullable": true, "format": "uuid"}, "session_id": {"type": "object", "description": "Alias for show_session_id", "nullable": true, "format": "uuid"}, "seat_number": {"type": "object", "example": "A-12", "nullable": true}, "ticket_status": {"type": "string", "enum": ["reserved", "purchased", "cancelled", "used"], "example": "reserved"}, "qr_code": {"type": "object", "nullable": true}, "purchased_at": {"type": "object", "example": "2026-04-28T12:00:00.000Z", "nullable": true}, "used_at": {"type": "object", "example": "2026-04-28T12:00:00.000Z", "nullable": true}}, "required": ["category_id"]}, "AdminTicketResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTicketDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTicketCheckInDto": {"type": "object", "properties": {"code": {"type": "string", "description": "Scanned QR payload \u2014 full validate URL or bare token TRM:v1:{ticketNumber}:{qrCode}", "example": "https://tremstgapi.axcova.com/api/tickets/validate?code=TRM%3Av1%3A00000042%3A22222222-2222-2222-2222-222222222222"}}, "required": ["code"]}, "AdminUpdateTicketDto": {"type": "object", "properties": {"category_id": {"type": "string", "description": "Ticket tier / category for this show", "format": "uuid"}, "user_id": {"type": "object", "nullable": true, "format": "uuid"}, "show_session_id": {"type": "object", "description": "Show session id (FK to show_sessions)", "nullable": true, "format": "uuid"}, "session_id": {"type": "object", "description": "Alias for show_session_id", "nullable": true, "format": "uuid"}, "seat_number": {"type": "object", "example": "A-12", "nullable": true}, "ticket_status": {"type": "string", "enum": ["reserved", "purchased", "cancelled", "used"]}, "qr_code": {"type": "object", "nullable": true}, "purchased_at": {"type": "object", "nullable": true}, "used_at": {"type": "object", "nullable": true}}}, "AdminDeleteTicketDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteTicketResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteTicketDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTicketCategoryDto": {"type": "object", "properties": {"id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "show_id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "session_id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "tier_name": {"type": "string", "example": "Premium"}, "price": {"type": "number", "example": 168}, "currency": {"type": "string", "example": "SGD"}, "available_seats": {"type": "number", "example": 150}, "remaining_seats": {"type": "number", "example": 120}, "description": {"type": "object", "example": "Closer to stage"}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.750Z"}}, "required": ["id", "show_id", "session_id", "tier_name", "price", "currency", "available_seats", "remaining_seats", "created_at"]}, "AdminTicketCategoryListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminTicketCategoryDto"}}}, "required": ["items"]}, "AdminTicketCategoryListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTicketCategoryListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCompleteSandboxPaymentDto": {"type": "object", "properties": {"payment_id": {"type": "string", "description": "Payment UUID from POST /api/checkout"}, "result": {"type": "string", "enum": ["success", "failed"]}}, "required": ["payment_id", "result"]}, "CheckoutCompleteDataDto": {"type": "object", "properties": {"checkout_id": {"type": "string"}, "payment_id": {"type": "string"}, "status": {"type": "string", "enum": ["paid", "failed"]}, "purchase_record_id": {"type": "object", "nullable": true}, "redirect_url": {"type": "string", "description": "Client return_url with checkout_id, payment_id, status query params"}, "payment_retry_until": {"type": "string", "nullable": true, "format": "date-time", "description": "After failure, seats held until this time for retry"}, "retry_available": {"type": "boolean", "description": "True when POST /checkout/retry is available"}, "guest_email": {"type": "object", "nullable": true}}, "required": ["checkout_id", "payment_id", "status", "redirect_url", "retry_available"]}, "CheckoutCompleteResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/CheckoutCompleteDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateShowVideoDto": {"type": "object", "properties": {"title": {"type": "string", "example": "Opening night highlight"}}, "required": ["title"]}, "AdminCreateArticleDto": {"type": "object", "properties": {"title": {"type": "string", "example": "Blackpink Adds Extra Show Date"}, "content_html": {"type": "string", "description": "HTML content from WYSIWYG editor", "example": "<p>Hello world</p>"}, "image_url": {"type": "object", "description": "Optional uploaded image URL (e.g. /uploads/article-images/xxx.png)", "example": "/uploads/article-images/abc.png", "nullable": true}, "url": {"type": "string", "description": "Article link (free text)", "example": "https://example.com/news/blackpink-osaka"}, "city_venue": {"type": "string", "example": "Osaka, Japan"}, "news_date": {"type": "string", "description": "News date (YYYY-MM-DD)", "example": "2026-04-28"}, "highlight": {"type": "object", "example": "Just Announced", "nullable": true}, "tag_ids": {"description": "List of tag ids", "example": ["concert", "announcement"], "type": "array", "items": {"type": "string"}}, "main": {"type": "boolean", "description": "Is this a main article for its tags? Only one main article is allowed per tag.", "default": false}, "show_in_homepage": {"type": "boolean", "description": "Show this article on the homepage.", "default": true}}, "required": ["title", "content_html", "url", "city_venue", "news_date"]}, "AdminUpdateArticleDto": {"type": "object", "properties": {"title": {"type": "string", "example": "Blackpink Adds Extra Show Date"}, "content_html": {"type": "string", "description": "HTML content from WYSIWYG editor", "example": "<p>Hello world</p>"}, "image_url": {"type": "object", "description": "Optional uploaded image URL (e.g. /uploads/article-images/xxx.png)", "example": "/uploads/article-images/abc.png", "nullable": true}, "url": {"type": "string", "description": "Article link (free text)", "example": "https://example.com/news/blackpink-osaka"}, "city_venue": {"type": "string", "example": "Osaka, Japan"}, "news_date": {"type": "string", "description": "News date (YYYY-MM-DD)", "example": "2026-04-28"}, "highlight": {"type": "object", "example": "Just Announced", "nullable": true}, "tag_ids": {"description": "Replace tags with this list of tag ids. Omit to keep unchanged.", "example": ["concert", "announcement"], "type": "array", "items": {"type": "string"}}, "main": {"type": "boolean", "description": "Is this a main article for its tags? Only one main article is allowed per tag."}, "show_in_homepage": {"type": "boolean", "description": "Show this article on the homepage."}}}, "AdminCreateArticleTagDto": {"type": "object", "properties": {"id": {"type": "string", "example": "announcement"}, "label": {"type": "string", "example": "Announcement"}}, "required": ["id", "label"]}, "AdminUpdateArticleTagDto": {"type": "object", "properties": {"label": {"type": "string", "example": "Announcement"}}}, "AdminCreateCastDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Hugh Jackman"}, "character_name": {"type": "string", "example": "Jean Valjean"}, "image_url": {"type": "object", "description": "Optional uploaded image URL (e.g. /uploads/cast-images/xxx.png)", "nullable": true}, "description": {"type": "object", "description": "Optional bio or notes", "nullable": true}, "instagram_url": {"type": "object", "example": "https://instagram.com/name", "nullable": true}, "x_url": {"type": "object", "example": "https://x.com/name", "nullable": true}, "tiktok_url": {"type": "object", "example": "https://www.tiktok.com/@name", "nullable": true}}, "required": ["name", "character_name"]}, "AdminUpdateCastDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Hugh Jackman"}, "character_name": {"type": "string", "example": "Jean Valjean"}, "image_url": {"type": "object", "description": "Optional uploaded image URL (e.g. /uploads/cast-images/xxx.png)", "nullable": true}, "description": {"type": "object", "description": "Optional bio or notes", "nullable": true}, "instagram_url": {"type": "object", "example": "https://instagram.com/name", "nullable": true}, "x_url": {"type": "object", "example": "https://x.com/name", "nullable": true}, "tiktok_url": {"type": "object", "example": "https://www.tiktok.com/@name", "nullable": true}}}, "AdminLinkShowCastDto": {"type": "object", "properties": {"cast_id": {"type": "string", "description": "Cast UUID", "example": "b7d1d1a0-1c35-4d2c-8d45-2b0bb6a4bb1a"}}, "required": ["cast_id"]}, "AdminLinkShowArticleDto": {"type": "object", "properties": {"article_id": {"type": "string", "description": "Article UUID", "example": "b7d1d1a0-1c35-4d2c-8d45-2b0bb6a4bb1a"}}, "required": ["article_id"]}, "AdminShowSessionDto": {"type": "object", "properties": {"id": {"type": "string"}, "show_id": {"type": "string"}, "session_date": {"type": "string", "example": "2026-06-15"}, "session_time": {"type": "string", "example": "19:30"}, "session_name": {"type": "string", "nullable": true, "example": "Evening Session", "description": "Custom title; public API uses as session_label when set"}, "description": {"type": "string", "nullable": true, "example": "Best value", "description": "Promo badge text returned on public GET /api/shows/:showId/sessions"}, "available_seats": {"type": "number", "description": "Sum of tier capacities (ticket_categories.available_seats)"}, "remaining_seats": {"type": "number", "description": "Sum of tier remaining seats for this session"}, "created_at": {"type": "string"}}, "required": ["id", "show_id", "session_date", "session_time", "available_seats", "remaining_seats", "created_at"]}, "AdminShowSessionsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminShowSessionDto"}}}, "required": ["items"]}, "AdminShowSessionsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowSessionsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateShowSessionDto": {"type": "object", "properties": {"session_date": {"type": "string", "example": "2026-06-15", "description": "Performance date (YYYY-MM-DD)"}, "session_time": {"type": "string", "example": "19:30", "description": "Performance time (HH:mm or HH:mm:ss)"}, "session_name": {"type": "string", "nullable": true, "example": "Evening", "description": "Optional display name (e.g. Matinee, Evening). Falls back to time-based label if omitted."}, "description": {"type": "string", "nullable": true, "example": "Best value", "description": "Promo badge on session picker (public GET \u2026/sessions returns this as description)"}}, "required": ["session_date", "session_time"]}, "AdminShowSessionResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowSessionDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateShowSessionDto": {"type": "object", "properties": {"session_date": {"type": "string", "example": "2026-06-15"}, "session_time": {"type": "string", "example": "20:00"}, "session_name": {"type": "string", "nullable": true, "example": "Matinee", "description": "Custom session display name (empty clears it)"}, "description": {"type": "string", "nullable": true, "example": "Best value", "description": "Promo badge on session picker (public GET \u2026/sessions)"}}}, "AdminDeleteShowSessionDataDto": {"type": "object", "properties": {"id": {"type": "string", "description": "Deleted show session UUID"}}, "required": ["id"]}, "AdminDeleteShowSessionResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteShowSessionDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateTagDto": {"type": "object", "properties": {"id": {"type": "string", "example": "headline"}, "label": {"type": "string", "example": "Headline"}}, "required": ["id", "label"]}, "AdminUpdateTagDto": {"type": "object", "properties": {"label": {"type": "string", "example": "Headline"}}}, "AdminFcmTokenListItemDto": {"type": "object", "properties": {"id": {"type": "string"}, "user_id": {"type": "string"}, "platform": {"type": "string", "nullable": true, "enum": ["android", "ios", "web"]}, "updated_at": {"type": "string"}, "token_preview": {"type": "string", "description": "Truncated FCM token for display"}, "token": {"type": "string", "description": "Full FCM token (admin list only; for unregister in CMS)"}}, "required": ["id", "user_id", "platform", "updated_at", "token_preview", "token"]}, "AdminFcmTokenListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminFcmTokenListItemDto"}}}, "required": ["items"]}, "AdminFcmTokenListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminFcmTokenListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminRegisterFcmTokenDto": {"type": "object", "properties": {"user_id": {"type": "string", "description": "App user UUID to attach the FCM device token to", "example": "a439e524-3a8a-487a-b897-2c3a231a3fda"}, "token": {"type": "string", "description": "FCM registration token from the client app or Firebase console"}, "platform": {"type": "string", "enum": ["android", "ios", "web"]}}, "required": ["user_id", "token"]}, "AdminUnregisterFcmTokenDto": {"type": "object", "properties": {"user_id": {"type": "string", "description": "App user UUID", "example": "a439e524-3a8a-487a-b897-2c3a231a3fda"}, "token": {"type": "string", "description": "FCM registration token to remove for this user"}}, "required": ["user_id", "token"]}, "AdminSendFcmDto": {"type": "object", "properties": {"user_ids": {"description": "List of user UUIDs to send the push notification to", "example": ["def36a9f-6600-48d4-9922-be6b1a00c04c"], "type": "array", "items": {"type": "string"}}, "title": {"type": "string", "description": "Notification title", "example": "New show added!"}, "body": {"type": "string", "description": "Notification body text", "example": "Check out the latest show near you."}, "data": {"type": "object", "description": "Optional key-value data payload (all values must be strings)", "example": {"screen": "show_detail", "show_id": "abc-123"}}}, "required": ["user_ids", "title", "body"]}, "AdminSendFcmDataDto": {"type": "object", "properties": {"fcm_enabled": {"type": "boolean", "description": "Whether FCM is configured on the server"}, "success_count": {"type": "number", "description": "Number of tokens that received the message"}, "failure_count": {"type": "number", "description": "Number of tokens that failed"}}, "required": ["fcm_enabled", "success_count", "failure_count"]}, "AdminSendFcmResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSendFcmDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSendFcmTopicDto": {"type": "object", "properties": {"topic": {"type": "string", "description": "FCM topic name. Use the same string as the mobile `subscribeToTopic` call (e.g. `xxx` or `event: xxx`).", "example": "xxx"}, "title": {"type": "string", "example": "Event alert"}, "body": {"type": "string", "example": "Something happened for event xxx"}, "data": {"type": "object", "example": {"event": "xxx", "screen": "test"}, "description": "Optional data payload (all values must be strings)"}}, "required": ["topic", "title", "body"]}, "AdminSendFcmTopicDataDto": {"type": "object", "properties": {"fcm_enabled": {"type": "boolean"}, "topic": {"type": "string", "description": "Normalized FCM topic name used for delivery"}, "message_id": {"type": "object", "description": "Firebase message id when send succeeded"}}, "required": ["fcm_enabled", "topic"]}, "AdminSendFcmTopicResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSendFcmTopicDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSubscribeFcmTopicDto": {"type": "object", "properties": {"topic": {"type": "string", "description": "FCM topic (must match mobile `subscribeToTopic`)", "example": "xxx"}, "user_ids": {"description": "Subscribe all tokens for these users (server-side via FCM)", "type": "array", "items": {"type": "string"}}, "tokens": {"description": "Subscribe explicit FCM tokens (e.g. from CMS web receiver)", "type": "array", "items": {"type": "string"}}}, "required": ["topic"]}, "AdminFcmTopicSubscribeDataDto": {"type": "object", "properties": {"fcm_enabled": {"type": "boolean"}, "topic": {"type": "string"}, "success_count": {"type": "number", "description": "Tokens successfully subscribed"}, "failure_count": {"type": "number", "description": "Tokens that failed to subscribe"}}, "required": ["fcm_enabled", "topic", "success_count", "failure_count"]}, "AdminFcmTopicSubscribeResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminFcmTopicSubscribeDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUnsubscribeFcmTopicDto": {"type": "object", "properties": {"topic": {"type": "string", "description": "FCM topic (must match mobile `subscribeToTopic`)", "example": "xxx"}, "user_ids": {"description": "Subscribe all tokens for these users (server-side via FCM)", "type": "array", "items": {"type": "string"}}, "tokens": {"description": "Subscribe explicit FCM tokens (e.g. from CMS web receiver)", "type": "array", "items": {"type": "string"}}}, "required": ["topic"]}, "AdminErrorLogRowDto": {"type": "object", "properties": {"id": {"type": "string"}, "created_at": {"type": "string"}, "level": {"type": "string", "enum": ["error", "warn"]}, "http_method": {"type": "string"}, "path": {"type": "string"}, "status_code": {"type": "number"}, "message": {"type": "string"}, "stack": {"type": "string", "nullable": true}, "actor_user_id": {"type": "string", "nullable": true}, "actor_email": {"type": "string", "nullable": true}, "actor_role": {"type": "string", "nullable": true}}, "required": ["id", "created_at", "level", "http_method", "path", "status_code", "message", "stack", "actor_user_id", "actor_email", "actor_role"]}, "AdminErrorLogsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminErrorLogRowDto"}}, "page": {"type": "number"}, "per_page": {"type": "number"}, "total": {"type": "number"}, "total_pages": {"type": "number"}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminErrorLogsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminErrorLogsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSurveyDto": {"type": "object", "properties": {"id": {"type": "string", "description": "Survey UUID"}, "name": {"type": "string"}, "category_id": {"type": "string"}, "category_label": {"type": "string"}, "launch_date": {"type": "string", "example": "2026-01-01"}, "expiry_date": {"type": "string", "example": "2026-12-31"}, "status": {"type": "number", "description": "Survey visibility: 1 = active (visible in app), 0 = inactive", "enum": [0, 1], "example": 1}, "video_url": {"type": "string", "description": "CDN URL when configured"}, "image_url": {"type": "object", "nullable": true}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "name", "category_id", "category_label", "launch_date", "expiry_date", "status", "video_url", "created_at", "updated_at"]}, "AdminSurveysListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminSurveyDto"}}, "page": {"type": "number"}, "per_page": {"type": "number"}, "total": {"type": "number"}, "total_pages": {"type": "number"}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminSurveysListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSurveysListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateSurveyDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Would you attend this show?"}, "category_id": {"type": "string", "example": "musical", "description": "Category id from categories table"}, "launch_date": {"type": "string", "description": "Launch date (YYYY-MM-DD)", "example": "2026-05-17"}, "expiry_date": {"type": "string", "description": "Expiry date (YYYY-MM-DD)", "example": "2026-12-31"}, "video_url": {"type": "string", "description": "Video URL (optional if uploading a file via POST \u2026/video after create)", "example": "/uploads/survey-videos/abc.mp4"}, "image_url": {"type": "object", "description": "Optional image URL (e.g. /uploads/survey-images/xxx.png)", "nullable": true}, "status": {"type": "number", "description": "Survey visibility: 1 = active, 0 = inactive", "enum": [0, 1], "default": 1}}, "required": ["name", "category_id", "launch_date", "expiry_date"]}, "AdminSurveyResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSurveyDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateSurveyDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Would you attend this show?"}, "category_id": {"type": "string", "example": "musical"}, "launch_date": {"type": "string", "description": "Launch date (YYYY-MM-DD)", "example": "2026-05-17"}, "expiry_date": {"type": "string", "description": "Expiry date (YYYY-MM-DD)", "example": "2026-12-31"}, "video_url": {"type": "object", "description": "Video URL (optional; upload via POST \u2026/video or set null to clear)", "example": "/uploads/survey-videos/abc.mp4", "nullable": true}, "image_url": {"type": "object", "nullable": true}, "status": {"type": "number", "description": "Survey visibility: 1 = active, 0 = inactive", "enum": [0, 1]}}}, "AdminDeleteSurveyDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteSurveyResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteSurveyDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSurveyResultDto": {"type": "object", "properties": {"id": {"type": "string"}, "user_id": {"type": "string"}, "survey_id": {"type": "string"}, "answer": {"type": "string", "enum": ["yes", "no", "maybe"]}, "notify": {"type": "boolean"}, "created_at": {"type": "string"}, "user_email": {"type": "string"}, "user_name": {"type": "string"}}, "required": ["id", "user_id", "survey_id", "answer", "notify", "created_at", "user_email", "user_name"]}, "AdminSurveyResultListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminSurveyResultDto"}}}, "required": ["items"]}, "AdminSurveyResultListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSurveyResultListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateSurveyResultDto": {"type": "object", "properties": {"user_id": {"type": "string", "description": "App user UUID"}, "answer": {"type": "string", "enum": ["yes", "no", "maybe"], "example": "yes"}, "notify": {"type": "boolean", "default": false, "description": "Notify flag for this result"}}, "required": ["user_id", "answer"]}, "AdminSurveyResultResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSurveyResultDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateSurveyResultDto": {"type": "object", "properties": {"user_id": {"type": "string", "description": "App user UUID"}, "answer": {"type": "string", "enum": ["yes", "no", "maybe"], "example": "maybe"}, "notify": {"type": "boolean", "description": "Notify flag for this result"}}}, "AdminDeleteSurveyResultDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteSurveyResultResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteSurveyResultDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminConfigDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "name": {"type": "string", "example": "low_stock_alert_threshold"}, "value": {"type": "string", "example": "10"}}, "required": ["id", "name", "value"]}, "AdminConfigListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminConfigDto"}}, "page": {"type": "number"}, "per_page": {"type": "number"}, "total": {"type": "number"}, "total_pages": {"type": "number"}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminConfigListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminConfigListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminConfigResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminConfigDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateConfigDto": {"type": "object", "properties": {"name": {"type": "string", "example": "low_stock_alert_threshold"}, "value": {"type": "string", "example": "10"}}, "required": ["name", "value"]}, "AdminUpdateConfigDto": {"type": "object", "properties": {"name": {"type": "string", "example": "low_stock_alert_threshold"}, "value": {"type": "string", "example": "10"}}}, "AdminDeleteConfigDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteConfigResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteConfigDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminBlockVenueSeatDto": {"type": "object", "properties": {"session_id": {"type": "string", "description": "Show session UUID"}, "duration_minutes": {"type": "number", "description": "How long the seat remains unavailable (minutes). Required if expires_at is omitted.", "minimum": 1, "maximum": 525600, "example": 60}, "expires_at": {"type": "string", "description": "Absolute expiry (ISO-8601). Future only; max 365 days from now. Required if duration_minutes is omitted.", "example": "2026-09-01T23:59:59.000Z"}, "reason": {"type": "string", "maxLength": 500, "example": "Reserved for production guest"}}, "required": ["session_id"]}, "AdminAssignVenueSeatDto": {"type": "object", "properties": {"session_id": {"type": "string", "description": "Show session UUID"}, "user_id": {"type": "string", "description": "User UUID to assign the seat to"}, "ticket_status": {"type": "string", "enum": ["purchased", "reserved"], "default": "purchased", "description": "Ticket status to create (default purchased)"}}, "required": ["session_id", "user_id"]}, "AdminUpsertVenueLayoutDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Main"}}}, "AdminCreateVenueSectionDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Stalls Centre"}, "code": {"type": "object", "example": "SC"}, "map_region_id": {"type": "object", "example": "stalls_centre", "description": "Figma map region id (stalls_front, dress_circle, box_1, \u2026). Null = resolve from code/name."}, "level": {"type": "object", "example": 1, "description": "1=ground, 2=first, 3=second"}, "sort_order": {"type": "number", "example": 0}, "map_x": {"type": "object", "example": 0.25}, "map_y": {"type": "object", "example": 0.5}, "map_width": {"type": "object", "example": 0.3}, "map_height": {"type": "object", "example": 0.2}}, "required": ["name"]}, "AdminUpdateVenueSectionDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Stalls Centre"}, "code": {"type": "object", "example": "SC"}, "map_region_id": {"type": "object", "example": "stalls_centre", "description": "Figma map region id (stalls_front, dress_circle, box_1, \u2026). Null = resolve from code/name."}, "level": {"type": "object", "example": 1, "description": "1=ground, 2=first, 3=second"}, "sort_order": {"type": "number", "example": 0}, "map_x": {"type": "object", "example": 0.25}, "map_y": {"type": "object", "example": 0.5}, "map_width": {"type": "object", "example": 0.3}, "map_height": {"type": "object", "example": 0.2}}}, "AdminCreateVenueRowDto": {"type": "object", "properties": {"row_label": {"type": "string", "example": "FF"}, "sort_order": {"type": "number", "example": 0}, "distance_from_stage_m": {"type": "object", "example": 12}, "sightline_rating": {"type": "object", "example": 5, "minimum": 1, "maximum": 5}}, "required": ["row_label"]}, "AdminUpdateVenueRowDto": {"type": "object", "properties": {"row_label": {"type": "string", "example": "FF"}, "sort_order": {"type": "number", "example": 0}, "distance_from_stage_m": {"type": "object", "example": 12}, "sightline_rating": {"type": "object", "example": 5, "minimum": 1, "maximum": 5}}}, "AdminCreateVenueSeatsDto": {"type": "object", "properties": {"seat_numbers": {"description": "Seat numbers to create in this row", "example": [19, 20, 21, 22, 23], "type": "array", "items": {"type": "number"}}, "tier_name": {"type": "object", "description": "Ticket tier name (resolved per session at booking \u2014 Approach A). Must match ticket_categories.tier_name.", "example": "Stalls Centre"}, "status": {"type": "string", "example": "active", "enum": ["active", "blocked", "wheelchair"]}}, "required": ["seat_numbers"]}, "AdminUpdateVenueSeatDto": {"type": "object", "properties": {"seat_number": {"type": "number", "example": 21}, "status": {"type": "string", "enum": ["active", "blocked", "wheelchair"]}, "tier_name": {"type": "object", "example": "Stalls Centre", "nullable": true}}}, "AdminUpsertShowVenueTierPriceItemDto": {"type": "object", "properties": {"venue_tier_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional show pricing category id"}}, "required": ["venue_tier_id", "price"]}, "AdminUpsertShowVenueRowPriceItemDto": {"type": "object", "properties": {"row_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional show pricing category id"}}, "required": ["row_id", "price"]}, "AdminUpsertShowVenueSeatPriceItemDto": {"type": "object", "properties": {"seat_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional show pricing category id"}}, "required": ["seat_id", "price"]}, "AdminUpsertShowPriceBandItemDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid", "description": "Existing band id (omit to create)"}, "name": {"type": "string", "description": "Band name shown to buyers, e.g. \"Below RM 100\""}, "min_price": {"type": "number", "minimum": 0, "description": "Displayed range minimum price"}, "max_price": {"type": "number", "minimum": 0, "description": "Displayed range maximum price"}, "currency": {"type": "string", "default": "SGD"}, "sort_order": {"type": "number", "minimum": 0}, "tier_ids": {"description": "Venue tier (section) ids grouped into this band", "type": "array", "items": {"type": "string", "format": "uuid"}}}, "required": ["name", "min_price", "max_price", "tier_ids"]}, "AdminUpsertShowVenueTierPricesDto": {"type": "object", "properties": {"session_id": {"type": "string", "format": "uuid", "description": "When set, prices apply to this session only. Omit (or use apply_to_all_sessions) to update every session."}, "apply_to_all_sessions": {"type": "boolean", "default": false, "description": "When true with session_id, copies the submitted prices to all sessions on the show."}, "use_venue_default_pricing": {"type": "boolean", "default": false, "description": "When true, the show uses the venue default prices (a snapshot is copied on save). Custom items/row/seat prices are ignored."}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertShowVenueTierPriceItemDto"}}, "seat_pricing_mode": {"type": "string", "enum": ["tier", "row", "seat"], "default": "tier"}, "row_prices": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertShowVenueRowPriceItemDto"}}, "seat_prices": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertShowVenueSeatPriceItemDto"}}, "bands": {"description": "Price bands for this show. Each band groups sections and carries a min/max display range.", "type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertShowPriceBandItemDto"}}}}, "AdminUpsertVenueTierPriceItemDto": {"type": "object", "properties": {"venue_tier_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional venue pricing category id"}}, "required": ["venue_tier_id", "price"]}, "AdminUpsertVenueRowPriceItemDto": {"type": "object", "properties": {"row_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional venue pricing category id"}}, "required": ["row_id", "price"]}, "AdminUpsertVenueSeatPriceItemDto": {"type": "object", "properties": {"seat_id": {"type": "string", "format": "uuid"}, "price": {"type": "number", "minimum": 0}, "currency": {"type": "string", "default": "SGD"}, "price_category_id": {"type": "object", "format": "uuid", "nullable": true, "description": "Optional venue pricing category id"}}, "required": ["seat_id", "price"]}, "AdminUpsertVenuePricesDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertVenueTierPriceItemDto"}}, "seat_pricing_mode": {"type": "string", "enum": ["tier", "row", "seat"], "default": "tier"}, "row_prices": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertVenueRowPriceItemDto"}}, "seat_prices": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUpsertVenueSeatPriceItemDto"}}}, "required": ["items"]}, "AdminMembershipTierDto": {"type": "object", "properties": {"id": {"type": "string"}, "code": {"type": "string", "example": "platinum"}, "name": {"type": "string", "example": "BASE PLATINUM MEMBER"}, "complimentary_tickets_per_year": {"type": "number", "example": 10}, "created_at": {"type": "string"}}, "required": ["id", "code", "name", "complimentary_tickets_per_year", "created_at"]}, "AdminMembershipTiersListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminMembershipTierDto"}}}, "required": ["items"]}, "AdminMembershipTiersListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminMembershipTiersListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateMembershipTierDto": {"type": "object", "properties": {"code": {"type": "string", "example": "gold", "description": "Unique tier code (public API tier field)"}, "name": {"type": "string", "example": "GOLD MEMBER"}, "complimentary_tickets_per_year": {"type": "number", "example": 5, "description": "Complimentary tickets granted per calendar year"}}, "required": ["code", "name", "complimentary_tickets_per_year"]}, "AdminMembershipTierResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminMembershipTierDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateMembershipTierDto": {"type": "object", "properties": {"code": {"type": "string", "example": "platinum"}, "name": {"type": "string", "example": "BASE PLATINUM MEMBER"}, "complimentary_tickets_per_year": {"type": "number", "example": 10}}}, "AdminDeleteMembershipTierDataDto": {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}, "AdminDeleteMembershipTierResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteMembershipTierDataDto"}}, "required": ["success", "timestamp", "data"]}, "TrendingShowSummaryDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "name": {"type": "string", "example": "Hamilton"}, "show_image_url": {"type": "string", "example": "https://cdn.example.com/shows/hamilton.jpg"}, "status": {"type": "number", "enum": [0, 1], "example": 1}, "start_date": {"type": "object", "nullable": true, "example": "2026-09-01"}, "end_date": {"type": "object", "nullable": true, "example": "2026-12-31"}}, "required": ["id", "name", "show_image_url", "status"]}, "AdminTrendingDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "video_url": {"type": "string", "example": "/uploads/trending-videos/abc.mp4"}, "image_preview": {"type": "object", "nullable": true, "example": "/uploads/trending-images/abc.jpg", "description": "Preview image URL (jpg/png/svg/gif)"}, "title": {"type": "string", "example": "Trending video title"}, "author": {"type": "string", "example": "John Doe"}, "link_url": {"type": "string", "example": "https://example.com"}, "show_id": {"type": "object", "nullable": true, "format": "uuid", "example": "8f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8", "description": "Show opened from this trending reel"}, "status": {"type": "string", "enum": ["active", "inactive"], "example": "active"}, "show": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/TrendingShowSummaryDto"}]}, "created_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}, "updated_at": {"type": "string", "example": "2026-04-11T18:49:41.665Z"}}, "required": ["id", "video_url", "image_preview", "title", "author", "link_url", "show_id", "status", "created_at", "updated_at"]}, "AdminTrendingListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminTrendingDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminTrendingListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTrendingListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTrendingResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTrendingDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateTrendingDto": {"type": "object", "properties": {"title": {"type": "string", "example": "Trending video title"}, "author": {"type": "string", "example": "John Doe"}, "link_url": {"type": "string", "example": "https://example.com"}, "status": {"type": "string", "enum": ["active", "inactive"], "example": "active"}, "show_id": {"type": "object", "nullable": true, "format": "uuid", "description": "Show to open from this trending reel. Empty string or null clears the link."}}, "required": ["title", "author", "link_url", "status"]}, "AdminUpdateTrendingDto": {"type": "object", "properties": {"title": {"type": "string", "example": "Trending video title"}, "author": {"type": "string", "example": "John Doe"}, "link_url": {"type": "string", "example": "https://example.com"}, "status": {"type": "string", "enum": ["active", "inactive"], "example": "active"}, "show_id": {"type": "object", "nullable": true, "format": "uuid", "description": "Show to open from this trending reel. Pass empty string or null to clear."}}}, "AdminDeleteTrendingDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}}, "required": ["deleted"]}, "AdminDeleteTrendingResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteTrendingDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSendMessagePushDto": {"type": "object", "properties": {"topic": {"type": "string", "description": "Send to a custom FCM topic (alternative to user_ids / user_group_id). Use the same name as mobile `subscribeToTopic` (e.g. `xxx` or `event: xxx`).", "example": "xxx"}, "user_group_id": {"type": "string", "description": "Send to all members of this user group (alternative to user_ids / topic)", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "user_ids": {"description": "Target user UUIDs (ignored when user_group_id or topic is set)", "example": ["b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"], "type": "array", "items": {"type": "string"}}, "title": {"type": "string", "example": "New show alert"}, "body": {"type": "string", "example": "Tickets are on sale now."}, "show_id": {"type": "string", "description": "FCM data.show_id (optional; used with topic sends)", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_color": {"type": "string", "description": "FCM data.show_color (optional; used with topic sends)", "example": "#1E3A5F"}, "type": {"type": "string", "description": "FCM data.type (optional; used with topic sends)", "example": "ticket_extension_log"}}, "required": ["title", "body"]}, "AdminSendMessageEnqueueDataDto": {"type": "object", "properties": {"queued": {"type": "number", "example": 3}, "ids": {"type": "array", "items": {"type": "string"}}}, "required": ["queued", "ids"]}, "AdminSendMessageEnqueueResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminSendMessageEnqueueDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminSendMessageEmailDto": {"type": "object", "properties": {"user_group_id": {"type": "string", "description": "Send to all members of this user group (alternative to user_ids)", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "user_ids": {"description": "Target user UUIDs (email taken from user record; ignored when user_group_id is set)", "example": ["b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"], "type": "array", "items": {"type": "string"}}, "subject": {"type": "string", "example": "Weekly update"}, "html_body": {"type": "string", "example": "<p>Hello from TREM</p>"}}, "required": ["subject", "html_body"]}, "AdminUserGroupDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "name": {"type": "string", "example": "VIP subscribers"}, "topic": {"type": "string", "example": "vipsubscribers", "description": "FCM topic slug (lowercase letters/digits only)"}, "member_count": {"type": "number", "example": 42, "description": "Current member count (max 1000)"}, "created_at": {"type": "string", "example": "2026-06-18T12:00:00.000Z"}, "updated_at": {"type": "string", "example": "2026-06-18T12:00:00.000Z"}}, "required": ["id", "name", "topic", "member_count", "created_at", "updated_at"]}, "AdminUserGroupsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUserGroupDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 5}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminUserGroupsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserGroupsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateUserGroupDto": {"type": "object", "properties": {"name": {"type": "string", "example": "VIP subscribers"}, "topic": {"type": "string", "description": "FCM topic slug (lowercase letters/digits only). Auto-generated from name when omitted.", "example": "vipsubscribers"}}, "required": ["name"]}, "AdminUserGroupResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserGroupDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUpdateUserGroupDto": {"type": "object", "properties": {"name": {"type": "string", "example": "VIP subscribers"}, "topic": {"type": "string", "description": "FCM topic slug (lowercase letters/digits only)", "example": "vipsubscribers"}}}, "AdminDeleteUserGroupDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean"}, "id": {"type": "string"}}, "required": ["deleted", "id"]}, "AdminDeleteUserGroupResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteUserGroupDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUserGroupMemberDto": {"type": "object", "properties": {"user_id": {"type": "string"}, "email": {"type": "string"}, "name": {"type": "string"}, "added_at": {"type": "string"}}, "required": ["user_id", "email", "name", "added_at"]}, "AdminUserGroupMembersListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminUserGroupMemberDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 50}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminUserGroupMembersListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserGroupMembersListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUserGroupMembersDto": {"type": "object", "properties": {"user_ids": {"description": "User UUIDs to add to the group", "example": ["cc800633-5e29-476f-bfc4-da3aa3e13c9a"], "type": "array", "items": {"type": "string"}}}, "required": ["user_ids"]}, "AdminUserGroupMembersAddDataDto": {"type": "object", "properties": {"added": {"type": "number", "example": 3}, "skipped": {"type": "number", "example": 1, "description": "Users already in the group"}, "member_count": {"type": "number", "example": 12}}, "required": ["added", "skipped", "member_count"]}, "AdminUserGroupMembersAddResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserGroupMembersAddDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminUserGroupMemberRemoveDataDto": {"type": "object", "properties": {"removed": {"type": "boolean"}, "member_count": {"type": "number", "example": 11}}, "required": ["removed", "member_count"]}, "AdminUserGroupMemberRemoveResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminUserGroupMemberRemoveDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminFcmTopicTypeDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "topic": {"type": "string", "example": "ticket-price-drop"}, "description": {"type": "string", "example": "Ticket price drop notifications"}}, "required": ["id", "topic", "description"]}, "AdminFcmTopicTypeListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminFcmTopicTypeDto"}}, "page": {"type": "number"}, "per_page": {"type": "number"}, "total": {"type": "number"}, "total_pages": {"type": "number"}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminFcmTopicTypeListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminFcmTopicTypeListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminFcmTopicTypeResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminFcmTopicTypeDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateFcmTopicTypeDto": {"type": "object", "properties": {"topic": {"type": "string", "example": "ticket-price-drop"}, "description": {"type": "string", "example": "Ticket price drop notifications"}}, "required": ["topic"]}, "AdminUpdateFcmTopicTypeDto": {"type": "object", "properties": {"topic": {"type": "string", "example": "ticket-price-drop"}, "description": {"type": "string", "example": "Ticket price drop notifications"}}}, "AdminDeleteFcmTopicTypeDataDto": {"type": "object", "properties": {"deleted": {"type": "boolean", "example": true}, "id": {"type": "string", "format": "uuid"}}, "required": ["deleted", "id"]}, "AdminDeleteFcmTopicTypeResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminDeleteFcmTopicTypeDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTicketPriceChangeDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "ticket_category_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "admin_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "changed_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "price_from": {"type": "string", "example": "250000.00"}, "price_to": {"type": "string", "example": "200000.00"}, "direction": {"type": "string", "enum": ["increase", "decrease"], "example": "decrease"}, "notify": {"type": "boolean", "example": false}, "fcm_sent": {"type": "number", "enum": [0, 1], "example": 0}}, "required": ["id", "ticket_category_id", "show_id", "admin_id", "changed_at", "price_from", "price_to", "direction", "notify", "fcm_sent"]}, "AdminTicketPriceChangeListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminTicketPriceChangeDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminTicketPriceChangeListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTicketPriceChangeListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminTicketExtensionLogDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_name": {"type": "object", "example": "Hamilton", "nullable": true}, "admin_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "changed_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "end_date_from": {"type": "string", "example": "2026-06-15"}, "end_date_to": {"type": "string", "example": "2026-06-30"}, "notify": {"type": "boolean", "example": true}, "fcm_sent": {"type": "number", "enum": [0, 1], "example": 0}}, "required": ["id", "show_id", "show_name", "admin_id", "changed_at", "end_date_from", "end_date_to", "notify", "fcm_sent"]}, "AdminTicketExtensionLogListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminTicketExtensionLogDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminTicketExtensionLogListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminTicketExtensionLogListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminShowLowAvailabilityLogDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_name": {"type": "object", "example": "Hamilton", "nullable": true}, "seat_balance": {"type": "number", "example": 12}, "seat_total": {"type": "number", "example": 100}, "threshold": {"type": "number", "example": 20}, "logged_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "notify": {"type": "boolean", "example": true}, "fcm_sent": {"type": "number", "enum": [0, 1], "example": 0}}, "required": ["id", "show_id", "show_name", "seat_balance", "seat_total", "threshold", "logged_at", "notify", "fcm_sent"]}, "AdminShowLowAvailabilityLogListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminShowLowAvailabilityLogDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminShowLowAvailabilityLogListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowLowAvailabilityLogListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminShowFinalWeekDto": {"type": "object", "properties": {"id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_id": {"type": "string", "example": "b3b0a2c1-7f2b-4aa9-9c88-9d5a4c0e9b10"}, "show_name": {"type": "object", "example": "Hamilton", "nullable": true}, "end_date": {"type": "string", "example": "2026-10-17"}, "days_before_end": {"type": "number", "example": 7, "description": "Days-before-end window used when logged"}, "logged_at": {"type": "string", "example": "2026-06-01T12:00:00.000Z"}, "notify": {"type": "boolean", "example": true}, "fcm_sent": {"type": "number", "enum": [0, 1], "example": 0}}, "required": ["id", "show_id", "show_name", "end_date", "days_before_end", "logged_at", "notify", "fcm_sent"]}, "AdminShowFinalWeekListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/AdminShowFinalWeekDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 30}, "total": {"type": "number", "example": 12}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "AdminShowFinalWeekListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/AdminShowFinalWeekListDataDto"}}, "required": ["success", "timestamp", "data"]}, "AdminCreateAssetDirectoryDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Campaigns"}, "parent_id": {"type": "object", "nullable": true, "description": "Parent directory UUID. Omit or null for root."}}, "required": ["name"]}, "AdminRenameAssetDirectoryDto": {"type": "object", "properties": {"name": {"type": "string", "example": "Summer campaign"}}, "required": ["name"]}, "AdminUpdateAssetDto": {"type": "object", "properties": {"name": {"type": "string", "example": "hero-banner"}, "directory_id": {"type": "object", "nullable": true, "description": "Move into this directory. Null moves to root."}}}, "ShowsSearchDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/ShowCardDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 10}, "total": {"type": "number", "example": 123}, "total_pages": {"type": "number", "example": 13}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "ShowsSearchResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowsSearchDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowTicketCategoryDto": {"type": "object", "properties": {"id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "tier_name": {"type": "string", "example": "Premium"}, "price": {"type": "number", "example": 168}, "currency": {"type": "string", "example": "SGD"}, "available_seats": {"type": "number", "example": 150, "description": "Total seats for this tier in the session"}, "remaining_seats": {"type": "number", "example": 120, "description": "Seats left for this tier"}, "description": {"type": "object", "example": "Closer to stage"}, "session_id": {"type": "object", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a", "nullable": true, "description": "Show session this tier applies to"}}, "required": ["id", "tier_name", "price", "currency", "available_seats", "remaining_seats"]}, "ShowSessionWithTicketTiersDto": {"type": "object", "properties": {"id": {"type": "string", "example": "0395a9e6-f7d7-4e91-9684-6ac79dffeb1a"}, "session_date": {"type": "string", "example": "2026-06-15", "description": "YYYY-MM-DD"}, "session_time": {"type": "string", "example": "19:30", "description": "HH:mm (24h)"}, "available_seats": {"type": "number", "example": 300, "description": "Sum of available_seats across tiers for this session"}, "remaining_seats": {"type": "number", "example": 240, "description": "Sum of remaining_seats across tiers for this session"}, "ticket_tiers": {"type": "array", "items": {"$ref": "#/components/schemas/ShowTicketCategoryDto"}}, "ticket_price_from": {"type": "object", "description": "Lowest tier price for this session", "example": 88, "nullable": true}, "session_name": {"type": "string", "nullable": true, "example": "Evening", "description": "Custom CMS session name; when set, used as session_label"}, "description": {"type": "string", "nullable": true, "example": "Best value", "description": "Promo badge on session picker (CMS Description field)"}, "session_label": {"type": "string", "example": "Matinee", "description": "Display label: session_name if set, else Matinee/Evening from time"}, "badges": {"example": ["low_seats"], "description": "Automatic urgency badges; currently low_seats only. Promo text uses description.", "type": "array", "items": {"type": "string"}}}, "required": ["id", "session_date", "session_time", "available_seats", "remaining_seats", "ticket_tiers"]}, "ShowSessionsListDataDto": {"type": "object", "properties": {"show_id": {"type": "string", "example": "75f842fc-27dd-4ac3-91e5-5e7b5c13a5f0"}, "seat_map_svg_url": {"type": "string", "nullable": true, "example": "https://cdn.example.com/venue-maps/sands-theatre.svg", "description": "Venue seat map SVG URL (CMS Venues \u2192 Seating \u2192 Upload SVG)"}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/ShowSessionWithTicketTiersDto"}}}, "required": ["show_id", "items"]}, "ShowSessionsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/ShowCardDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 5}, "total": {"type": "number", "example": 123}, "total_pages": {"type": "number", "example": 25}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "ShowsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "PastShowsListDataDto": {"type": "object", "properties": {"items": {"description": "Inactive or completed shows", "type": "array", "items": {"$ref": "#/components/schemas/ShowCardDto"}}, "total": {"type": "number", "example": 12}}, "required": ["items", "total"]}, "PastShowsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/PastShowsListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowLikeToggleDataDto": {"type": "object", "properties": {"show_id": {"type": "string", "description": "FK to shows.id"}, "user_id": {"type": "string", "description": "FK to users.id (JWT user)"}, "liked": {"type": "boolean", "description": "true if liked after toggle, false if unliked"}, "id": {"type": "object", "nullable": true, "description": "show_reaction.id when liked; null when unliked"}, "created_at": {"type": "object", "nullable": true, "description": "ISO timestamp when liked; null when unliked"}}, "required": ["show_id", "user_id", "liked", "id", "created_at"]}, "ShowLikeToggleResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowLikeToggleDataDto"}}, "required": ["success", "timestamp", "data"]}, "SaveShowNotificationDto": {"type": "object", "properties": {"ticket_price_drops": {"type": "number", "description": "Notify when ticket prices drop", "enum": [0, 1], "example": 1}, "low_availability": {"type": "number", "description": "Notify on low availability", "enum": [0, 1], "example": 1}, "extension_announced": {"type": "number", "description": "Notify when an extension is announced", "enum": [0, 1], "example": 1}, "final_week_alert": {"type": "number", "description": "Final week alert", "enum": [0, 1], "example": 1}}, "required": ["ticket_price_drops", "low_availability", "extension_announced", "final_week_alert"]}, "ShowSavedRowDto": {"type": "object", "properties": {"id": {"type": "string", "description": "show_saved.id"}, "show_id": {"type": "string", "description": "FK to shows.id"}, "user_id": {"type": "string", "description": "FK to users.id"}, "saved_at": {"type": "string", "description": "ISO timestamp"}, "status": {"type": "number", "description": "1 = saved, 0 = unsaved (list endpoint only returns 1)"}, "unsaved_at": {"type": "object", "description": "When unsaved, if applicable", "nullable": true}}, "required": ["id", "show_id", "user_id", "saved_at", "status", "unsaved_at"]}, "ShowSavedListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/ShowSavedRowDto"}}}, "required": ["items"]}, "ShowSavedListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSavedListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowNotificationRowDto": {"type": "object", "properties": {"id": {"type": "string", "description": "show_notification.id"}, "user_id": {"type": "string", "description": "FK to users.id"}, "show_id": {"type": "string", "description": "FK to shows.id"}, "ticket_price_drops": {"type": "number", "description": "0 or 1"}, "low_availability": {"type": "number", "description": "0 or 1"}, "extension_announced": {"type": "number", "description": "0 or 1"}, "final_week_alert": {"type": "number", "description": "0 or 1"}, "updated_at": {"type": "string", "description": "ISO timestamp"}}, "required": ["id", "user_id", "show_id", "ticket_price_drops", "low_availability", "extension_announced", "final_week_alert", "updated_at"]}, "ShowNotificationListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/ShowNotificationRowDto"}}}, "required": ["items"]}, "ShowNotificationListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowNotificationListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowSessionsCalendarDateDto": {"type": "object", "properties": {"date": {"type": "string", "example": "2026-06-14"}, "has_show": {"type": "boolean", "example": true}, "remaining_seats": {"type": "number", "example": 240}, "session_count": {"type": "number", "example": 2, "description": "Number of sessions on this date"}, "availability_status": {"type": "string", "enum": ["unavailable", "sold_out", "selling_fast", "available"], "example": "available"}}, "required": ["date", "has_show", "remaining_seats", "session_count", "availability_status"]}, "ShowSessionsCalendarDataDto": {"type": "object", "properties": {"show_id": {"type": "string"}, "month": {"type": "string", "example": "2026-06"}, "suggested_month": {"type": "object", "example": "2026-06", "nullable": true, "description": "Earliest month with any session \u2014 use as calendar default"}, "first_bookable_month": {"type": "object", "example": "2026-06", "nullable": true, "description": "Earliest month with bookable seats"}, "dates": {"type": "array", "items": {"$ref": "#/components/schemas/ShowSessionsCalendarDateDto"}}}, "required": ["show_id", "month", "dates"]}, "ShowSessionsCalendarResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionsCalendarDataDto"}}, "required": ["success", "timestamp", "data"]}, "SessionPriceRangeDetailDto": {"type": "object", "properties": {"id": {"type": "string", "example": "tier:Stalls%20Left", "description": "Budget bucket id \u2014 pass as price_range_id on GET \u2026/map"}, "label": {"type": "string", "example": "S$100 \u00b7 Stalls Left"}, "name": {"type": "string", "example": "Premium", "description": "Band or section name from CMS \u2014 use this as the primary card title in the band picker"}, "min_price": {"type": "number", "example": 120}, "max_price": {"type": "number", "example": 170}, "remaining_seats": {"type": "number", "example": 18}, "sections": {"example": ["Stalls Centre", "Stalls Left"], "type": "array", "items": {"type": "string"}}, "can_auto_select": {"type": "boolean", "example": true}}, "required": ["id", "label", "name", "min_price", "max_price", "remaining_seats", "sections", "can_auto_select"]}, "AutoSelectOptionDto": {"type": "object", "properties": {"available": {"type": "boolean", "example": true}, "description": {"type": "object", "example": "We'll pick the best 2 seats for you", "nullable": true}}, "required": ["available"]}, "ShowSessionPriceRangesDataDto": {"type": "object", "properties": {"show_id": {"type": "string"}, "show_session_id": {"type": "string"}, "quantity": {"type": "number", "example": 2}, "ranges": {"type": "array", "items": {"$ref": "#/components/schemas/SessionPriceRangeDetailDto"}}, "auto_select_option": {"$ref": "#/components/schemas/AutoSelectOptionDto"}, "membership_redeemable": {"type": "boolean", "example": true, "description": "True when user has enough complimentary tickets for quantity"}}, "required": ["show_id", "show_session_id", "quantity", "ranges", "auto_select_option", "membership_redeemable"]}, "ShowSessionPriceRangesResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionPriceRangesDataDto"}}, "required": ["success", "timestamp", "data"]}, "SeatPricingCategoryDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "name": {"type": "string", "example": "Adult"}, "base_price": {"type": "number", "example": 160}, "discount_percent": {"type": "number", "example": 0}, "effective_price": {"type": "number", "example": 160}, "currency": {"type": "string", "example": "SGD"}, "color": {"type": "string", "example": "#6366F1", "description": "Hex color #RRGGBB"}}, "required": ["id", "name", "base_price", "discount_percent", "effective_price", "currency", "color"]}, "TheatreMapSectionDto": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string", "example": "Stalls Centre"}, "code": {"type": "object", "nullable": true, "example": "stalls-centre"}, "map_region_id": {"type": "object", "nullable": true, "example": "stalls_centre", "description": "Figma Choose-section region id for deterministic bind (preferred over name heuristics). Values: stalls_front|stalls_left|stalls_centre|stalls_right|stalls_back|dress_circle|grand_circle|box_1\u2026box_8. Aliases: Premium\u2192stalls_front; Box L\u2192box_1; Box R\u2192box_4. Stalls Back unbound until section seeded."}, "level": {"type": "string", "nullable": true, "enum": ["ground", "first", "second"], "example": "ground", "description": "Floor tab for the section map (ground / first / second)."}, "highlighted": {"type": "boolean", "example": true, "description": "Primary section for the current price_range_id. Exactly one selectable in-band section is highlighted."}, "dimmed": {"type": "boolean", "example": false}, "selectable": {"type": "boolean", "example": true, "description": "True when the section can be chosen (!dimmed && available_seats > 0)."}, "available_seats": {"type": "number", "example": 8}, "price": {"type": "object", "example": 160, "nullable": true, "description": "Section (tier) ticket price for seats in this section"}, "pricing_category": {"description": "Show pricing category linked to this price, if configured. Empty object `{}` when none.", "additionalProperties": true, "allOf": [{"$ref": "#/components/schemas/SeatPricingCategoryDto"}]}, "rows": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string", "description": "Venue row UUID for GET \u2026/rows/:rowId"}, "label": {"type": "string", "example": "A"}, "price": {"type": "number", "nullable": true}, "pricing_category": {"type": "object", "description": "Category object, or `{}` when none", "additionalProperties": true}}}, "description": "Rows in this section for the clickable seat map. Use row `id` (UUID) in GET \u2026/rows/:rowId when labels repeat across sections."}, "map_x": {"type": "object", "nullable": true}, "map_y": {"type": "object", "nullable": true}, "map_width": {"type": "object", "nullable": true}, "map_height": {"type": "object", "nullable": true}}, "required": ["id", "name", "highlighted", "dimmed", "selectable", "available_seats", "pricing_category", "rows"]}, "ShowSessionMapDataDto": {"type": "object", "properties": {"show_id": {"type": "string"}, "show_session_id": {"type": "string"}, "price_range_id": {"type": "string", "example": "tier:Stalls%20Left", "description": "Echo of price_range_id query param"}, "price_range_label": {"type": "string", "example": "S$100 \u00b7 Stalls Left"}, "quantity": {"type": "number", "example": 2}, "venue_name": {"type": "object", "example": "Esplanade Theatre", "nullable": true}, "seat_map_svg_url": {"type": "string", "nullable": true, "description": "Venue seat map SVG URL (same as GET \u2026/sessions seat_map_svg_url)"}, "seat_map_svg": {"type": "string", "nullable": true, "description": "Inline venue seat map SVG markup for rendering section boxes without a second fetch"}, "sections": {"type": "array", "items": {"$ref": "#/components/schemas/TheatreMapSectionDto"}}, "legend": {"type": "object"}, "auto_selected": {"type": "object"}}, "required": ["show_id", "show_session_id", "price_range_id", "price_range_label", "quantity", "sections", "legend", "auto_selected"]}, "ShowSessionMapResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionMapDataDto"}}, "required": ["success", "timestamp", "data"]}, "RowSeatDetailDto": {"type": "object", "properties": {"id": {"type": "string"}, "number": {"type": "number", "example": 21, "description": "Configured venue seat number (`venue_seats.seat_number`), not a sequential index in the response array."}, "status": {"type": "string", "enum": ["available", "sold", "held", "blocked", "wheelchair", "orphan_blocked", "selected"]}, "price": {"type": "object", "example": 138, "nullable": true}, "pricing_category": {"description": "Category object, or `{}` when none", "additionalProperties": true, "allOf": [{"$ref": "#/components/schemas/SeatPricingCategoryDto"}]}, "selected": {"type": "boolean", "example": true}}, "required": ["id", "number", "status", "pricing_category", "selected"]}, "ShowSessionRowDetailDataDto": {"type": "object", "properties": {"show_id": {"type": "string"}, "show_session_id": {"type": "string"}, "row_id": {"type": "string"}, "row_label": {"type": "string", "example": "FF"}, "distance_from_stage_m": {"type": "object", "example": 12, "nullable": true}, "sightline_rating": {"type": "object", "example": 5, "nullable": true}, "section_name": {"type": "string", "example": "Stalls Centre"}, "price": {"type": "object", "example": 160, "nullable": true}, "pricing_category": {"description": "Category object, or `{}` when none", "additionalProperties": true, "allOf": [{"$ref": "#/components/schemas/SeatPricingCategoryDto"}]}, "seats": {"type": "array", "items": {"$ref": "#/components/schemas/RowSeatDetailDto"}}, "legend": {"type": "object"}, "pricing_summary": {"type": "object", "nullable": true}}, "required": ["show_id", "show_session_id", "row_id", "row_label", "section_name", "pricing_category", "seats", "legend"]}, "ShowSessionRowDetailResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionRowDetailDataDto"}}, "required": ["success", "timestamp", "data"]}, "SessionPriceRangeAvailabilityDto": {"type": "object", "properties": {"id": {"type": "string"}, "label": {"type": "string", "example": "S$120 - S$170"}, "name": {"type": "string", "example": "Premium", "description": "Band or section name from CMS \u2014 preferred display label"}, "min_price": {"type": "number", "example": 120}, "max_price": {"type": "number", "example": 170}, "remaining_seats": {"type": "number", "example": 18}, "can_fit_quantity": {"type": "boolean", "example": true}}, "required": ["id", "label", "name", "min_price", "max_price", "remaining_seats", "can_fit_quantity"]}, "ShowSessionAvailabilityDataDto": {"type": "object", "properties": {"show_id": {"type": "string"}, "show_session_id": {"type": "string"}, "quantity": {"type": "number", "example": 2}, "max_quantity": {"type": "number", "example": 8}, "price_ranges": {"type": "array", "items": {"$ref": "#/components/schemas/SessionPriceRangeAvailabilityDto"}}, "adjacent_seats_available": {"type": "boolean", "example": true}, "note": {"type": "object", "nullable": true}}, "required": ["show_id", "show_session_id", "quantity", "max_quantity", "price_ranges", "adjacent_seats_available"]}, "ShowSessionAvailabilityResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowSessionAvailabilityDataDto"}}, "required": ["success", "timestamp", "data"]}, "ComplimentaryTicketsBenefitDto": {"type": "object", "properties": {"total_per_year": {"type": "number", "example": 10}, "remaining": {"type": "number", "example": 8}, "price": {"type": "number", "example": 0}, "currency": {"type": "string", "example": "SGD"}}, "required": ["total_per_year", "remaining", "price", "currency"]}, "UserMembershipBenefitsDto": {"type": "object", "properties": {"complimentary_tickets": {"$ref": "#/components/schemas/ComplimentaryTicketsBenefitDto"}}, "required": ["complimentary_tickets"]}, "UserMembershipDataDto": {"type": "object", "properties": {"tier": {"type": "object", "example": "platinum", "nullable": true}, "tier_label": {"type": "object", "example": "BASE PLATINUM MEMBER", "nullable": true}, "benefits": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/UserMembershipBenefitsDto"}]}}}, "UserMeDataDto": {"type": "object", "properties": {"id": {"type": "string", "example": "def36a9f-6600-48d4-9922-be6b1a00c04c"}, "email": {"type": "string", "example": "test@test.com"}, "name": {"type": "string", "example": "Test1"}, "city": {"type": "string", "nullable": true, "example": "Kuala Lumpur"}, "city_id": {"type": "string", "nullable": true, "description": "City UUID (FK to cities.id)", "example": "123b8abc-7c58-4fad-a137-4da4059426e3"}, "interests": {"description": "Interest category ids", "example": ["theatre", "family"], "type": "array", "items": {"type": "string"}}, "location": {"nullable": true, "description": "Device coordinates, if user allowed and they were saved (via onboarding city step).", "allOf": [{"$ref": "#/components/schemas/AuthUserLocationDto"}]}, "auth_provider": {"type": "string", "enum": ["email", "google", "apple"], "example": "email"}, "created_at": {"type": "string", "description": "User account creation timestamp (ISO 8601)", "example": "2026-04-20T13:35:43.727Z"}, "membership": {"$ref": "#/components/schemas/UserMembershipDataDto"}, "points_balance": {"type": "number", "example": 120, "description": "Current loyalty points balance"}, "total_points_earned": {"type": "number", "example": 200, "description": "Sum of loyalty points credited (positive transactions)"}, "total_points_redeemed": {"type": "number", "example": 80, "description": "Sum of loyalty points spent or adjusted down"}, "total_purchases": {"type": "number", "example": 4, "description": "Paid checkout orders"}, "pending_purchases": {"type": "number", "example": 1, "description": "Checkout orders still pending payment"}, "total_spent": {"type": "number", "example": 640.5, "description": "Sum of paid order totals. When paid orders use more than one currency, this is the combined number and currency is null."}, "currency": {"type": "object", "nullable": true, "example": "SGD", "description": "Currency of paid orders when they all use the same one"}, "total_tickets": {"type": "number", "example": 6, "description": "Tickets issued to this user"}}, "required": ["id", "email", "name", "interests", "auth_provider", "created_at", "membership", "points_balance", "total_points_earned", "total_points_redeemed", "total_purchases", "pending_purchases", "total_spent", "total_tickets"]}, "UserMeResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UserMeDataDto"}}, "required": ["success", "timestamp", "data"]}, "UserMembershipResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UserMembershipDataDto"}}, "required": ["success", "timestamp", "data"]}, "UserPurchaseListItemDto": {"type": "object", "properties": {"checkout_id": {"type": "string", "format": "uuid"}, "booking_reference": {"type": "object", "nullable": true, "example": "TRM-2026-A1B2C"}, "order_status": {"type": "string", "example": "paid"}, "payment_id": {"type": "object", "nullable": true, "format": "uuid"}, "payment_status": {"type": "object", "nullable": true, "example": "succeeded"}, "payment_method": {"type": "object", "nullable": true, "example": "stripe"}, "show_id": {"type": "string", "format": "uuid"}, "show_title": {"type": "string", "example": "BLACKPINK World Tour"}, "session_id": {"type": "string", "format": "uuid"}, "session_label": {"type": "string", "example": "First Session \u00b7 2026-07-31 \u00b7 19:00"}, "quantity": {"type": "number", "example": 2}, "total_amount": {"type": "number", "example": 126}, "currency": {"type": "string", "example": "SGD"}, "created_at": {"type": "string", "format": "date-time"}, "paid_at": {"type": "string", "nullable": true, "format": "date-time"}}, "required": ["checkout_id", "order_status", "show_id", "show_title", "session_id", "session_label", "quantity", "total_amount", "currency", "created_at"]}, "UserPurchaseListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/UserPurchaseListItemDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 20}, "total": {"type": "number", "example": 3}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "UserPurchaseListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UserPurchaseListDataDto"}}, "required": ["success", "timestamp", "data"]}, "UserPurchaseLineItemDto": {"type": "object", "properties": {"line_type": {"type": "string", "example": "tickets"}, "label": {"type": "string", "example": "Cat 1"}, "quantity": {"type": "number", "example": 2}, "unit_amount": {"type": "number", "example": 60}, "amount": {"type": "number", "example": 120}, "currency": {"type": "string", "example": "SGD"}}, "required": ["line_type", "label", "quantity", "unit_amount", "amount", "currency"]}, "UserPurchaseDetailDataDto": {"type": "object", "properties": {"checkout_id": {"type": "string", "format": "uuid"}, "booking_reference": {"type": "object", "nullable": true, "example": "TRM-2026-A1B2C"}, "order_status": {"type": "string", "example": "paid"}, "payment_id": {"type": "object", "nullable": true, "format": "uuid"}, "payment_status": {"type": "object", "nullable": true, "example": "succeeded"}, "payment_method": {"type": "object", "nullable": true, "example": "stripe"}, "show_id": {"type": "string", "format": "uuid"}, "show_title": {"type": "string", "example": "BLACKPINK World Tour"}, "session_id": {"type": "string", "format": "uuid"}, "session_label": {"type": "string", "example": "First Session \u00b7 2026-07-31 \u00b7 19:00"}, "quantity": {"type": "number", "example": 2}, "total_amount": {"type": "number", "example": 126}, "currency": {"type": "string", "example": "SGD"}, "created_at": {"type": "string", "format": "date-time"}, "paid_at": {"type": "string", "nullable": true, "format": "date-time"}, "purchase_record_id": {"type": "object", "nullable": true, "format": "uuid"}, "guest_email": {"type": "object", "nullable": true}, "failure_reason": {"type": "object", "nullable": true, "example": "Payment failed"}, "line_items": {"type": "array", "items": {"$ref": "#/components/schemas/UserPurchaseLineItemDto"}}, "tickets": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "ticket_number": {"type": "string", "example": "00000042"}, "seat_number": {"type": "string", "nullable": true}, "section": {"type": "string", "nullable": true}, "row": {"type": "string", "nullable": true}, "ticket_status": {"type": "string", "example": "purchased"}, "qr_payload": {"type": "string", "example": "https://tremstgapi.axcova.com/api/tickets/validate?code=TRM%3Av1%3A00000042%3A22222222-2222-2222-2222-222222222222"}, "barcode_payload": {"type": "string", "example": "TRM:v1:00000042:22222222-2222-2222-2222-222222222222"}}}}, "ticket_ids": {"type": "array", "items": {"type": "string", "format": "uuid"}}, "qr_payload": {"type": "object", "nullable": true, "example": "https://tremstgapi.axcova.com/api/tickets/validate?code=TRM%3Av1%3A00000042%3A22222222-2222-2222-2222-222222222222"}, "barcode_payload": {"type": "object", "nullable": true, "example": "TRM:v1:00000042:22222222-2222-2222-2222-222222222222"}}, "required": ["checkout_id", "order_status", "show_id", "show_title", "session_id", "session_label", "quantity", "total_amount", "currency", "created_at", "line_items", "tickets", "ticket_ids"]}, "UserPurchaseDetailResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UserPurchaseDetailDataDto"}}, "required": ["success", "timestamp", "data"]}, "UserTicketDetailDataDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "ticket_number": {"type": "string", "example": "00000042", "description": "Unique 8-digit running ticket number (shown on ticket / QR)"}, "checkout_id": {"type": "object", "nullable": true, "format": "uuid"}, "purchase_record_id": {"type": "object", "nullable": true, "format": "uuid"}, "booking_reference": {"type": "string", "example": "TRM-2026-A1B2C"}, "show_id": {"type": "string", "format": "uuid"}, "show_title": {"type": "string", "example": "BLACKPINK World Tour"}, "session_id": {"type": "object", "nullable": true, "format": "uuid"}, "session_label": {"type": "object", "nullable": true, "example": "First Session \u00b7 2026-07-31 \u00b7 19:00"}, "section": {"type": "object", "nullable": true}, "row": {"type": "object", "nullable": true}, "seat_number": {"type": "object", "nullable": true}, "tier_label": {"type": "object", "nullable": true, "example": "Cat 1"}, "ticket_status": {"type": "string", "example": "purchased"}, "qr_payload": {"type": "string", "example": "https://tremstgapi.axcova.com/api/tickets/validate?code=TRM%3Av1%3A00000042%3A22222222-2222-2222-2222-222222222222", "description": "Encode this HTTPS URL as a QR code (iPhone Camera opens Safari). Token: TRM:v1:{ticketNumber}:{qrCode}"}, "barcode_payload": {"type": "string", "example": "TRM:v1:00000042:22222222-2222-2222-2222-222222222222", "description": "Code128 barcode value (TRM token, no HTTPS wrapper). Same secret as QR."}, "purchased_at": {"type": "string", "nullable": true, "format": "date-time"}}, "required": ["id", "ticket_number", "booking_reference", "show_id", "show_title", "ticket_status", "qr_payload", "barcode_payload"]}, "UserTicketDetailResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/UserTicketDetailDataDto"}}, "required": ["success", "timestamp", "data"]}, "SeatDetailsDataDto": {"type": "object", "properties": {"id": {"type": "string"}, "row": {"type": "string", "example": "FF"}, "number": {"type": "number", "example": 21}, "section": {"type": "string", "example": "Stalls Centre"}, "price": {"type": "number", "example": 138}, "currency": {"type": "string", "example": "SGD"}, "pricing_category": {"description": "Category object, or `{}` when none", "additionalProperties": true, "allOf": [{"$ref": "#/components/schemas/SeatPricingCategoryDto"}]}, "distance_from_stage_m": {"type": "object", "example": 12, "nullable": true}, "sightline_rating": {"type": "object", "example": 5, "nullable": true}, "tier_name": {"type": "object", "nullable": true}, "status": {"type": "string", "example": "active"}}, "required": ["id", "row", "number", "section", "price", "currency", "pricing_category", "status"]}, "SeatDetailsResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/SeatDetailsDataDto"}}, "required": ["success", "timestamp", "data"]}, "CreateCheckoutDto": {"type": "object", "properties": {"show_id": {"type": "string", "description": "Show UUID (step 1 \u2014 same as CMS simulate)"}, "show_session_id": {"type": "string", "description": "Show session / performance UUID (step 2)"}, "ticket_category_id": {"type": "string", "description": "Ticket tier (category) UUID for the selected session. Trem Tix v2: resolve from selected seats via GET /api/seats/:seatId/details (tier_name \u2192 category)."}, "total_seats_purchases": {"type": "number", "description": "Number of seats to purchase (quantity step). Must match selected seat count in v2 seat-map flow.", "example": 2, "minimum": 1, "maximum": 50}, "return_url": {"type": "string", "description": "Client return URL after sandbox payment (step 4 \u2014 app deep link or web page). Query params will be appended.", "example": "myapp://checkout/complete"}, "reservation_id": {"type": "string", "description": "Active seat hold from POST /api/booking/reserve. Required for seat-map checkout with price lock timer."}, "guest_email": {"type": "string", "description": "Guest checkout \u2014 email only for ticket delivery. No password required; account creation is post-purchase.", "example": "guest@example.com"}, "points_to_redeem": {"type": "number", "description": "Trem Credit points to spend. Converted with the System \u2192 Points redeem rate for the order currency (for example 100 points \u2192 SGD 10). Capped by the balance and the order total. Omit or 0 to pay the full amount.", "example": 100, "minimum": 0}}, "required": ["show_id", "show_session_id", "ticket_category_id", "total_seats_purchases", "return_url"]}, "CheckoutLineItemDto": {"type": "object", "properties": {"line_type": {"type": "string", "enum": ["tickets", "service_fee", "add_on", "points_redemption"]}, "label": {"type": "string", "example": "Trem Tix service fee: S$2.50/ticket"}, "quantity": {"type": "number", "example": 2}, "unit_amount": {"type": "number", "example": 2.5}, "amount": {"type": "number", "example": 5}, "currency": {"type": "string", "example": "SGD"}}, "required": ["line_type", "label", "quantity", "unit_amount", "amount", "currency"]}, "CreateCheckoutDataDto": {"type": "object", "properties": {"checkout_id": {"type": "string"}, "payment_id": {"type": "string"}, "payment_gateway": {"type": "string", "enum": ["sandbox", "stripe"], "example": "stripe"}, "payment_url": {"type": "object", "nullable": true, "description": "Sandbox WebView URL (null when payment_gateway is stripe)", "example": "http://localhost:3001/api/payments/sandbox/uuid"}, "client_secret": {"type": "object", "nullable": true, "description": "Stripe PaymentIntent client_secret for Payment Sheet (null for sandbox)"}, "stripe_publishable_key": {"type": "object", "nullable": true, "description": "Stripe publishable key for Payment Sheet (null for sandbox)"}, "total_amount": {"type": "number", "example": 20000}, "tickets_subtotal": {"type": "number", "example": 195, "description": "Ticket subtotal before service fee"}, "service_fee_total": {"type": "number", "example": 5, "description": "Trem Tix service fee total (S$2.50 \u00d7 ticket count)"}, "currency": {"type": "string", "example": "KRW"}, "total_seats_purchases": {"type": "number", "example": 2, "description": "Seats purchased (same field as CMS simulate)"}, "line_items": {"type": "array", "items": {"$ref": "#/components/schemas/CheckoutLineItemDto"}}, "guest_email": {"type": "object", "nullable": true, "description": "Guest email for ticket delivery"}, "reservation_id": {"type": "object", "nullable": true, "description": "Linked seat hold id"}, "points_redeemed": {"type": "number", "example": 100, "description": "Trem Credit points taken off this order"}, "points_discount": {"type": "number", "example": 10, "description": "Money value of points_redeemed in the order currency"}, "status": {"type": "string", "enum": ["pending", "paid"], "example": "pending", "description": "paid when points cover the full total and tickets are issued immediately"}}, "required": ["checkout_id", "payment_id", "payment_gateway", "total_amount", "tickets_subtotal", "service_fee_total", "currency", "total_seats_purchases", "line_items", "points_redeemed", "points_discount", "status"]}, "CreateCheckoutResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/CreateCheckoutDataDto"}}, "required": ["success", "timestamp", "data"]}, "RetryCheckoutPaymentDto": {"type": "object", "properties": {"checkout_id": {"type": "string", "description": "Checkout order id from POST /api/checkout"}}, "required": ["checkout_id"]}, "CheckoutPaymentStatusDataDto": {"type": "object", "properties": {"checkout_id": {"type": "string", "description": "Checkout order id (same as checkout_id from POST /checkout)"}, "payment_id": {"type": "object", "nullable": true, "description": "Latest payment id for this order"}, "order_status": {"type": "string", "enum": ["pending", "paid", "failed", "cancelled"], "description": "Checkout order status"}, "payment_status": {"type": "string", "nullable": true, "enum": ["pending", "processing", "succeeded", "failed", "cancelled"], "description": "Latest payment status"}, "status": {"type": "string", "enum": ["pending", "paid", "failed", "cancelled"], "description": "Unified status for clients (derived from order)"}, "purchase_record_id": {"type": "object", "nullable": true}, "paid_at": {"type": "string", "nullable": true, "format": "date-time"}, "failed_at": {"type": "string", "nullable": true, "format": "date-time"}, "guest_email": {"type": "object", "nullable": true}, "line_items": {"type": "array", "items": {"$ref": "#/components/schemas/CheckoutLineItemDto"}}, "tickets_subtotal": {"type": "object", "nullable": true}, "service_fee_total": {"type": "number", "example": 5}, "total_amount": {"type": "number", "example": 200}, "payment_retry_until": {"type": "string", "nullable": true, "format": "date-time"}, "retry_available": {"type": "boolean"}, "reservation_id": {"type": "object", "nullable": true}, "price_lock_seconds_remaining": {"type": "object", "nullable": true, "example": 420}, "price_lock_timer_paused": {"type": "boolean"}}, "required": ["checkout_id", "order_status", "status", "line_items", "service_fee_total", "total_amount", "retry_available", "price_lock_timer_paused"]}, "CheckoutPaymentStatusResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/CheckoutPaymentStatusDataDto"}}, "required": ["success", "timestamp", "data"]}, "CheckoutConfirmationTicketDto": {"type": "object", "properties": {"id": {"type": "string", "format": "uuid"}, "ticket_number": {"type": "string", "example": "00000042", "description": "Unique 8-digit running ticket number (shown on ticket / QR)"}, "seat_number": {"type": "object", "nullable": true, "example": "A-12"}, "section": {"type": "object", "nullable": true, "example": "Dress Circle"}, "row": {"type": "object", "nullable": true, "example": "A"}, "ticket_status": {"type": "string", "example": "purchased"}, "qr_payload": {"type": "string", "example": "https://tremstgapi.axcova.com/api/tickets/validate?code=TRM%3Av1%3A00000042%3A22222222-2222-2222-2222-222222222222", "description": "Encode this HTTPS URL as a QR code (iPhone Camera opens Safari). Token: TRM:v1:{ticketNumber}:{qrCode}"}, "barcode_payload": {"type": "string", "example": "TRM:v1:00000042:22222222-2222-2222-2222-222222222222", "description": "Code128 barcode value (TRM token, no HTTPS wrapper)."}}, "required": ["id", "ticket_number", "ticket_status", "qr_payload", "barcode_payload"]}, "CheckoutConfirmationDataDto": {"type": "object", "properties": {"booking_reference": {"type": "string", "example": "TRM-2026-A1B2C"}, "checkout_id": {"type": "string", "format": "uuid"}, "purchase_record_id": {"type": "object", "nullable": true, "format": "uuid"}, "show_id": {"type": "string", "format": "uuid"}, "show_title": {"type": "string", "example": "BLACKPINK World Tour"}, "session_id": {"type": "string", "format": "uuid"}, "session_label": {"type": "string", "example": "First Session \u00b7 2026-07-31 \u00b7 19:00"}, "guest_email": {"type": "object", "nullable": true}, "total_paid": {"type": "number", "example": 126}, "currency": {"type": "string", "example": "SGD"}, "quantity": {"type": "number", "example": 2}, "tickets": {"type": "array", "items": {"$ref": "#/components/schemas/CheckoutConfirmationTicketDto"}}, "ticket_ids": {"type": "array", "items": {"type": "string", "format": "uuid"}}, "qr_payload": {"type": "object", "nullable": true, "description": "Convenience: tickets[0].qr_payload when at least one ticket exists"}, "barcode_payload": {"type": "object", "nullable": true, "description": "Convenience: tickets[0].barcode_payload when at least one ticket exists"}, "email_sent": {"type": "boolean", "description": "True when guest_email was set on the order (delivery target recorded)"}}, "required": ["booking_reference", "checkout_id", "show_id", "show_title", "session_id", "session_label", "total_paid", "currency", "quantity", "tickets", "ticket_ids", "email_sent"]}, "CheckoutConfirmationResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/CheckoutConfirmationDataDto"}}, "required": ["success", "timestamp", "data"]}, "CreateBookingReservationDto": {"type": "object", "properties": {"show_id": {"type": "string", "description": "Show UUID"}, "show_session_id": {"type": "string", "description": "Show session UUID"}, "seat_ids": {"description": "Venue seat UUIDs to hold (must match checkout quantity)", "type": "array", "items": {"type": "string"}}, "guest_email": {"type": "string", "description": "Guest email for ticket delivery (no account required at checkout)"}}, "required": ["show_id", "show_session_id", "seat_ids"]}, "BookingReservationDataDto": {"type": "object", "properties": {"reservation_id": {"type": "string"}, "expires_at": {"type": "string", "format": "date-time"}, "seconds_remaining": {"type": "number", "example": 480, "description": "Seconds until seat hold expires (8 min from create)"}, "timer_paused": {"type": "boolean", "description": "True while payment is in 3DS redirect (timer frozen)"}, "tickets_subtotal": {"type": "number", "example": 150}, "service_fee_total": {"type": "number", "example": 5}, "total_amount": {"type": "number", "example": 155}, "currency": {"type": "string", "example": "SGD"}, "line_items": {"type": "array", "items": {"$ref": "#/components/schemas/CheckoutLineItemDto"}}, "seat_ids": {"type": "array", "items": {"type": "string"}}, "guest_email": {"type": "object", "nullable": true}}, "required": ["reservation_id", "expires_at", "seconds_remaining", "timer_paused", "tickets_subtotal", "service_fee_total", "total_amount", "currency", "line_items", "seat_ids"]}, "CreateBookingReservationResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/BookingReservationDataDto"}}, "required": ["success", "timestamp", "data"]}, "BookingReservationResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/BookingReservationDataDto"}}, "required": ["success", "timestamp", "data"]}, "ReservationAddOnItemDto": {"type": "object", "properties": {"add_on_id": {"type": "string"}, "quantity": {"type": "number", "minimum": 1}}, "required": ["add_on_id", "quantity"]}, "UpdateReservationAddOnsDto": {"type": "object", "properties": {"items": {"description": "Selected add-ons for this reservation", "example": [{"add_on_id": "uuid", "quantity": 1}], "type": "array", "items": {"$ref": "#/components/schemas/ReservationAddOnItemDto"}}}, "required": ["items"]}, "TrendingItemDto": {"type": "object", "properties": {"id": {"type": "string", "example": "cc800633-5e29-476f-bfc4-da3aa3e13c9a"}, "video_url": {"type": "string", "example": "/uploads/trending-videos/abc.mp4"}, "image_preview": {"type": "object", "nullable": true, "example": "/uploads/trending-images/abc.jpg", "description": "Preview image URL (jpg/png/svg/gif)"}, "title": {"type": "string", "example": "Trending video title"}, "author": {"type": "string", "example": "John Doe"}, "link_url": {"type": "string", "example": "https://example.com"}, "show_id": {"type": "object", "nullable": true, "format": "uuid", "example": "8f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8", "description": "Show opened from this trending reel"}, "show": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/TrendingShowSummaryDto"}]}}, "required": ["id", "video_url", "image_preview", "title", "author", "link_url", "show_id"]}, "TrendingListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/TrendingItemDto"}}}, "required": ["items"]}, "TrendingListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/TrendingListDataDto"}}, "required": ["success", "timestamp", "data"]}, "ShowReviewEligibilityDataDto": {"type": "object", "properties": {"eligible": {"type": "boolean", "example": false, "description": "True when the user may write a review for this show"}, "has_ticket": {"type": "boolean", "example": true, "description": "True when the user owns a purchased or used ticket for this show"}, "session_passed": {"type": "boolean", "example": false, "description": "True when at least one of those tickets is for a session that has already started or passed"}, "reason": {"type": "string", "enum": ["ok", "no_ticket", "show_not_ended"], "example": "show_not_ended"}}, "required": ["eligible", "has_ticket", "session_passed", "reason"]}, "ShowReviewEligibilityResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ShowReviewEligibilityDataDto"}}, "required": ["success", "timestamp", "data"]}, "SaveInterestsDto": {"type": "object", "properties": {"skip": {"type": "boolean", "description": "If true, skip interests; do not send selections", "example": false}, "selections": {"description": "At least 2 category ids when skip is not true", "example": ["theatre", "music"], "type": "array", "items": {"type": "string"}}}}, "SaveCityDto": {"type": "object", "properties": {"preset": {"type": "string", "format": "uuid", "description": "City id (UUID) from GET /onboarding/cities/options", "example": "123b8abc-7c58-4fad-a137-4da4059426e3"}, "latitude": {"type": "number", "example": 1.3521}, "longitude": {"type": "number", "example": 103.8198}}, "required": ["preset"]}, "ArticleDto": {"type": "object", "properties": {"id": {"type": "string", "example": "74304744-6286-4cdf-89e3-981c0cf9ebf8"}, "title": {"type": "string", "example": "Moulin Rouge! The Musical makes its Southeast Asia debut"}, "content_html": {"type": "string", "example": "<p>The hit Broadway musical opens in Singapore in February 2027.</p>"}, "url": {"type": "string", "example": "https://example.com/news/moulin-rouge-singapore"}, "image_url": {"type": "object", "example": "https://cdn.example.com/uploads/article-images/abc.png", "nullable": true}, "city_venue": {"type": "string", "example": "Singapore"}, "news_date": {"type": "string", "example": "2026-09-14"}, "highlight": {"type": "object", "example": "Just Announced", "nullable": true}, "created_at": {"type": "string", "example": "2026-09-14T08:12:00.000Z"}, "updated_at": {"type": "string", "example": "2026-09-14T08:12:00.000Z"}, "tags": {"type": "array", "items": {"$ref": "#/components/schemas/ShowTagDto"}}, "main": {"type": "boolean", "description": "Main article for its tag(s). Only one main per tag.", "example": true}, "show_in_homepage": {"type": "boolean", "description": "Whether this article should appear on the homepage.", "example": true}}, "required": ["id", "title", "content_html", "url", "city_venue", "news_date", "created_at", "updated_at", "tags", "main", "show_in_homepage"]}, "ArticlesListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/ArticleDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 20}, "total": {"type": "number", "example": 42}, "total_pages": {"type": "number", "example": 3}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "ArticlesListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/ArticlesListDataDto"}}, "required": ["success", "timestamp", "data"]}, "SurveyListItemDto": {"type": "object", "properties": {"id": {"type": "string", "description": "Survey UUID"}, "name": {"type": "string", "example": "Would you see this show?"}, "category_id": {"type": "string", "example": "musical"}, "category_label": {"type": "string", "example": "Musical"}, "launch_date": {"type": "string", "example": "2026-01-01", "description": "YYYY-MM-DD"}, "expiry_date": {"type": "string", "example": "2026-12-31", "description": "YYYY-MM-DD"}, "video_url": {"type": "string", "description": "CDN URL when configured"}, "image_url": {"type": "object", "nullable": true, "description": "CDN URL when configured"}, "created_at": {"type": "string", "description": "ISO timestamp"}, "updated_at": {"type": "string", "description": "ISO timestamp"}, "total_responded": {"type": "number", "description": "Total survey_result rows for this survey"}, "total_yes": {"type": "number", "description": "Count of yes answers"}, "total_no": {"type": "number", "description": "Count of no answers"}, "total_maybe": {"type": "number", "description": "Count of maybe answers"}, "percentage_interested": {"type": "number", "description": "Percentage of yes answers (0\u2013100, two decimal places)", "example": 59.52}}, "required": ["id", "name", "category_id", "category_label", "launch_date", "expiry_date", "video_url", "created_at", "updated_at", "total_responded", "total_yes", "total_no", "total_maybe", "percentage_interested"]}, "SurveysListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/SurveyListItemDto"}}, "page": {"type": "number", "example": 1}, "per_page": {"type": "number", "example": 20}, "total": {"type": "number", "example": 5}, "total_pages": {"type": "number", "example": 1}}, "required": ["items", "page", "per_page", "total", "total_pages"]}, "SurveysListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/SurveysListDataDto"}}, "required": ["success", "timestamp", "data"]}, "SubmitSurveyResultDto": {"type": "object", "properties": {"answer": {"type": "string", "enum": ["yes", "no", "maybe"], "example": "yes"}}, "required": ["answer"]}, "SurveyResultDataDto": {"type": "object", "properties": {"id": {"type": "string", "description": "survey_result.id"}, "survey_id": {"type": "string", "description": "FK to survey.id"}, "answer": {"type": "string", "enum": ["yes", "no", "maybe"], "example": "yes"}, "notify": {"type": "boolean", "description": "Whether the user opted in to notifications for this survey", "example": false}, "created_at": {"type": "string", "description": "ISO timestamp when the result was first created"}}, "required": ["id", "survey_id", "answer", "notify", "created_at"]}, "SurveyResultResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/SurveyResultDataDto"}}, "required": ["success", "timestamp", "data"]}, "SurveyResultNotifyResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"description": "Updated result with notify=true", "allOf": [{"$ref": "#/components/schemas/SurveyResultDataDto"}]}}, "required": ["success", "timestamp", "data"]}, "TagDto": {"type": "object", "properties": {"id": {"type": "string", "example": "headline"}, "label": {"type": "string", "example": "Headline"}}, "required": ["id", "label"]}, "TagsListDataDto": {"type": "object", "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/TagDto"}}}, "required": ["items"]}, "TagsListResponseDto": {"type": "object", "properties": {"success": {"type": "boolean", "example": true}, "timestamp": {"type": "string", "example": "2026-04-20T13:35:43.727Z"}, "data": {"$ref": "#/components/schemas/TagsListDataDto"}}, "required": ["success", "timestamp", "data"]}}}}