openapi: 3.0.3 info: title: DNAX Gamezone API version: 0.9.2 description: | DNAX backend API for the staff portal. Auth: Sanctum personal access token (`Authorization: Bearer {token}`). Public: `/status`, `/auth/login`. Gaming setup + Offers: any active user can read; Admin writes. Customers: staff+admin read/create/update; Admin deletes. Phone unique (normalized). Customer passes: sell Offers to Customers (staff+admin). Paid sales post income into Finance cashbook (account required). Finance: simple cashbook — Accounts (Cash/Bank), Categories, Transactions. Admin manages; staff use account-options on sell. Stock: Categories + Brands (global) + Items; bill restock + quick sale; posts to Finance. Staff desk ops; Admin catalog writes. Sessions: thin bag — plays, unpaid snacks, payments; close when due == 0; cancel only empty abandoned bags (no ended plays — including ₹0 pass; voided OK); optional `label` overrides display_name. Plays: end-preview (GET/POST), end, Play Detail (correct/reopen/delete), occupancy claims. Prefer /delete over /void. Bookings: group → windowed bookings → multi-mode lines → hard coverage holds; check-in → session; deposit apply-once as session credit. Desk: dashboard poll + Day Board (read-only); admin KPIs; staff gets kpis null. Money: INR with up to 2 decimal places (paise), e.g. `60.50` or `93953.94`. Percent play discounts round to the nearest whole rupee (half up: 22.2→22, 22.5→23); fixed discounts and payments/transactions keep exact decimals. Session estimates: `estimated_plays_inr` / `estimated_due_inr` / `estimate_basis=planned` / `estimate_incomplete` (live wall = max(planned, ceil elapsed to 0.5h) + FIFO pass; omit live without planned). Human docs: `{APP_URL}/docs`. servers: - url: http://localhost:8000/api description: Local tags: - name: System - name: Auth - name: Users - name: StationTypes - name: Stations - name: PlayModes - name: PricingRules - name: GameFeatures - name: Games - name: Offers - name: Customers - name: CustomerPasses - name: Finance - name: Stock - name: Sessions - name: Plays - name: Bookings - name: Desk - name: Roster components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: Sanctum schemas: User: type: object properties: id: { type: integer } name: { type: string } email: { type: string, format: email } role: { type: string, enum: [admin, staff, helper] } is_active: { type: boolean } created_at: { type: string, format: date-time, nullable: true } updated_at: { type: string, format: date-time, nullable: true } StationType: type: object properties: id: { type: integer } name: { type: string } code: { type: string } sort_order: { type: integer } is_active: { type: boolean } can_be_hub: { type: boolean } can_be_peripheral: { type: boolean } allows_standalone: { type: boolean } notes: { type: string, nullable: true } Station: type: object properties: id: { type: integer } name: { type: string } sort_order: { type: integer } is_active: { type: boolean } notes: { type: string, nullable: true } station_type: { $ref: '#/components/schemas/StationType' } hubs: type: array items: { type: object } peripherals: type: array items: { type: object } PlayMode: type: object properties: id: { type: integer } name: { type: string } code: { type: string } description: { type: string, nullable: true } sort_order: { type: integer } is_active: { type: boolean } requires_hub: { type: boolean } uses_pairing_group: { type: boolean } block_all_active_stations: { type: boolean } blocked_station_types: type: array items: { $ref: '#/components/schemas/StationType' } pricing_rules: type: array items: { $ref: '#/components/schemas/PricingRule' } PricingRule: type: object properties: id: { type: integer } play_mode_id: { type: integer } label: { type: string } hourly_rate: { type: number, description: INR per hour (up to 2 decimals) } player_count: { type: integer, nullable: true } sort_order: { type: integer } is_active: { type: boolean } GameFeature: type: object properties: id: { type: integer } name: { type: string } code: { type: string } sort_order: { type: integer } is_active: { type: boolean } GamePricing: type: object properties: id: { type: integer } game_id: { type: integer } play_mode_id: { type: integer } pricing_rule_id: { type: integer, nullable: true } custom_hourly_rate: { type: number, nullable: true } resolved_hourly_rate: { type: number, nullable: true } Game: type: object properties: id: { type: integer } name: { type: string } cover_path: { type: string, nullable: true } cover_url: { type: string, nullable: true } is_active: { type: boolean } notes: { type: string, nullable: true } features: type: array items: { $ref: '#/components/schemas/GameFeature' } platforms: type: array items: { $ref: '#/components/schemas/StationType' } pricing: type: array items: { $ref: '#/components/schemas/GamePricing' } installed_on_stations: type: array items: { type: object } LoginResponse: type: object properties: token: { type: string } token_type: { type: string, example: Bearer } user: { $ref: '#/components/schemas/User' } ValidationError: type: object properties: message: { type: string } errors: type: object additionalProperties: type: array items: { type: string } security: - bearerAuth: [] paths: /status: get: tags: [System] summary: Health / status check security: [] responses: '200': { description: Healthy } '503': { description: Database unavailable } /auth/login: post: tags: [Auth] summary: Login security: [] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string, format: email } password: { type: string } device_name: { type: string } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/LoginResponse' } '422': description: Invalid credentials / inactive /auth/me: get: tags: [Auth] summary: Current user responses: '200': description: OK content: application/json: schema: type: object properties: data: { $ref: '#/components/schemas/User' } /auth/logout: post: tags: [Auth] summary: Logout (revoke current token) responses: '200': { description: OK } /users: get: tags: [Users] summary: List users (Admin) parameters: - { in: query, name: search, schema: { type: string } } - { in: query, name: role, schema: { type: string, enum: [admin, staff, helper] } } - { in: query, name: is_active, schema: { type: boolean } } - { in: query, name: per_page, schema: { type: integer, default: 20 } } responses: '200': { description: OK } '403': { description: Admin required } post: tags: [Users] summary: Create user (Admin) requestBody: required: true content: application/json: schema: type: object required: [name, email, password, role] properties: name: { type: string } email: { type: string, format: email } password: { type: string } role: { type: string, enum: [admin, staff, helper] } is_active: { type: boolean, default: true } responses: '201': { description: Created } '403': { description: Admin required } /users/{user}: parameters: - { in: path, name: user, required: true, schema: { type: integer } } get: tags: [Users] summary: Get user (Admin) responses: '200': { description: OK } '404': { description: Not found } patch: tags: [Users] summary: Update user (Admin) requestBody: content: application/json: schema: type: object properties: name: { type: string } email: { type: string, format: email } password: { type: string, nullable: true } role: { type: string, enum: [admin, staff, helper] } is_active: { type: boolean } responses: '200': { description: OK } '422': { description: Validation / self-lock prevention } delete: tags: [Users] summary: Delete user (Admin) responses: '200': { description: Deleted } '422': { description: Cannot delete self or last admin } /station-types: get: tags: [StationTypes] summary: List station types parameters: - { in: query, name: is_active, schema: { type: boolean } } responses: '200': description: OK content: application/json: schema: type: object properties: data: type: array items: { $ref: '#/components/schemas/StationType' } post: tags: [StationTypes] summary: Create station type (Admin) requestBody: required: true content: application/json: schema: type: object required: [name, code] properties: name: { type: string } code: { type: string } sort_order: { type: integer } is_active: { type: boolean } can_be_hub: { type: boolean } can_be_peripheral: { type: boolean } allows_standalone: { type: boolean } notes: { type: string, nullable: true } responses: '201': { description: Created } '403': { description: Admin required } /station-types/{stationType}: parameters: - { in: path, name: stationType, required: true, schema: { type: integer } } get: tags: [StationTypes] summary: Get station type responses: '200': { description: OK } patch: tags: [StationTypes] summary: Update station type (Admin) responses: '200': { description: OK } delete: tags: [StationTypes] summary: Delete station type (Admin) responses: '200': { description: Deleted } '422': { description: Still has stations } /stations: get: tags: [Stations] summary: List stations parameters: - { in: query, name: station_type_id, schema: { type: integer } } - { in: query, name: station_type_code, schema: { type: string } } - { in: query, name: is_active, schema: { type: boolean } } - { in: query, name: search, schema: { type: string } } responses: '200': description: OK content: application/json: schema: type: object properties: data: type: array items: { $ref: '#/components/schemas/Station' } post: tags: [Stations] summary: Create station (Admin) requestBody: required: true content: application/json: schema: type: object required: [station_type_id, name] properties: station_type_id: { type: integer } name: { type: string } sort_order: { type: integer } is_active: { type: boolean } notes: { type: string, nullable: true } responses: '201': { description: Created } /stations/{station}: parameters: - { in: path, name: station, required: true, schema: { type: integer } } get: tags: [Stations] summary: Get station (includes installs) responses: '200': { description: OK } patch: tags: [Stations] summary: Update station (Admin) responses: '200': { description: OK } delete: tags: [Stations] summary: Delete station (Admin) responses: '200': { description: Deleted } /stations/{station}/pairings: post: tags: [Stations] summary: Pair peripheral to hub (Admin) parameters: - { in: path, name: station, required: true, schema: { type: integer }, description: Hub station id } requestBody: required: true content: application/json: schema: type: object required: [peripheral_station_id] properties: peripheral_station_id: { type: integer } responses: '201': { description: Created } '422': { description: Pairing rule violation } /stations/{station}/pairings/{peripheral}: delete: tags: [Stations] summary: Unpair peripheral (Admin) parameters: - { in: path, name: station, required: true, schema: { type: integer } } - { in: path, name: peripheral, required: true, schema: { type: integer } } responses: '200': { description: Removed } '422': { description: Pairing not found } /stations/{station}/blocking-preview: get: tags: [Stations] summary: Preview blocked stations for a usage mode parameters: - { in: path, name: station, required: true, schema: { type: integer } } - { in: query, name: mode, schema: { type: string, enum: [standalone, hub_alone, peripheral, combo], default: standalone } } responses: '200': { description: OK } /stations/availability: get: tags: [Stations] summary: Desk occupancy (free | live | blocked | booked) description: > Half-open [from,to). Marks non-free when the window overlaps a live play blocking window (planned / open-ended / overtime) or an upcoming booking hold, including zone/pairing coverage. Ended plays do not reserve availability. blocking_reason includes live_play, covered_by_play, booking, zone_busy, etc. parameters: - { in: query, name: play_mode_id, schema: { type: integer } } - { in: query, name: game_id, schema: { type: integer } } - { in: query, name: from, schema: { type: string, format: date-time } } - { in: query, name: to, schema: { type: string, format: date-time } } responses: '200': { description: OK } /stations/{station}/games: put: tags: [Stations] summary: Sync installed games on station (Admin) parameters: - { in: path, name: station, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [game_ids] properties: game_ids: type: array items: { type: integer } responses: '200': { description: OK } /play-modes: get: tags: [PlayModes] summary: List play modes parameters: - { in: query, name: with_pricing, schema: { type: boolean } } - { in: query, name: is_active, schema: { type: boolean } } responses: '200': { description: OK } post: tags: [PlayModes] summary: Create play mode (Admin) requestBody: required: true content: application/json: schema: type: object required: [name, code] properties: name: { type: string } code: { type: string } description: { type: string, nullable: true } sort_order: { type: integer } is_active: { type: boolean } requires_hub: { type: boolean } uses_pairing_group: { type: boolean } block_all_active_stations: { type: boolean } blocked_station_type_ids: type: array items: { type: integer } responses: '201': { description: Created } /play-modes/{playMode}: parameters: - { in: path, name: playMode, required: true, schema: { type: integer } } get: tags: [PlayModes] summary: Get play mode responses: '200': { description: OK } patch: tags: [PlayModes] summary: Update play mode (Admin) responses: '200': { description: OK } delete: tags: [PlayModes] summary: Delete play mode (Admin) responses: '200': { description: Deleted } '422': { description: Still has pricing rules } /play-modes/{playMode}/pricing-rules: post: tags: [PricingRules] summary: Create pricing rule under play mode (Admin) parameters: - { in: path, name: playMode, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [label, hourly_rate] properties: label: { type: string } hourly_rate: { type: number } player_count: { type: integer, nullable: true } sort_order: { type: integer } is_active: { type: boolean } responses: '201': { description: Created } /pricing-rules: get: tags: [PricingRules] summary: List pricing rules parameters: - { in: query, name: play_mode_id, schema: { type: integer } } - { in: query, name: is_active, schema: { type: boolean } } responses: '200': { description: OK } /pricing-rules/{pricingRule}: parameters: - { in: path, name: pricingRule, required: true, schema: { type: integer } } get: tags: [PricingRules] summary: Get pricing rule responses: '200': { description: OK } patch: tags: [PricingRules] summary: Update pricing rule (Admin) responses: '200': { description: OK } delete: tags: [PricingRules] summary: Delete pricing rule (Admin) responses: '200': { description: Deleted } /game-features: get: tags: [GameFeatures] summary: List game features responses: '200': { description: OK } post: tags: [GameFeatures] summary: Create game feature (Admin) requestBody: required: true content: application/json: schema: type: object required: [name, code] properties: name: { type: string } code: { type: string } sort_order: { type: integer } is_active: { type: boolean } responses: '201': { description: Created } /game-features/{gameFeature}: parameters: - { in: path, name: gameFeature, required: true, schema: { type: integer } } get: tags: [GameFeatures] summary: Get game feature responses: '200': { description: OK } patch: tags: [GameFeatures] summary: Update game feature (Admin) responses: '200': { description: OK } delete: tags: [GameFeatures] summary: Delete game feature (Admin) responses: '200': { description: Deleted } /games: get: tags: [Games] summary: List games parameters: - { in: query, name: search, schema: { type: string } } - { in: query, name: is_active, schema: { type: boolean } } - { in: query, name: station_type_id, schema: { type: integer } } - { in: query, name: station_id, schema: { type: integer } } - { in: query, name: detailed, schema: { type: boolean, default: true } } - { in: query, name: per_page, schema: { type: integer, default: 50 } } responses: '200': { description: OK } post: tags: [Games] summary: Create game (Admin) requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } is_active: { type: boolean } notes: { type: string, nullable: true } feature_ids: type: array items: { type: integer } platform_ids: type: array items: { type: integer } station_ids: type: array items: { type: integer } multipart/form-data: schema: type: object required: [name] properties: name: { type: string } cover: { type: string, format: binary, description: jpeg/png/webp max 400KB } feature_ids: { type: array, items: { type: integer } } platform_ids: { type: array, items: { type: integer } } station_ids: { type: array, items: { type: integer } } responses: '201': { description: Created } /games/{game}: parameters: - { in: path, name: game, required: true, schema: { type: integer } } get: tags: [Games] summary: Get game responses: '200': description: OK content: application/json: schema: type: object properties: data: { $ref: '#/components/schemas/Game' } patch: tags: [Games] summary: Update game (Admin) responses: '200': { description: OK } delete: tags: [Games] summary: Delete game (Admin) responses: '200': { description: Deleted } /games/{game}/cover: parameters: - { in: path, name: game, required: true, schema: { type: integer } } post: tags: [Games] summary: Upload / replace cover (Admin) requestBody: required: true content: multipart/form-data: schema: type: object required: [cover] properties: cover: type: string format: binary description: JPEG/PNG/WebP, max 400 KB responses: '200': { description: OK } delete: tags: [Games] summary: Delete cover (Admin) responses: '200': { description: OK } /games/{game}/pricing: put: tags: [Games] summary: Sync game pricing (Admin). Empty pricing [] clears all links. parameters: - { in: path, name: game, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [pricing] properties: pricing: type: array items: type: object required: [play_mode_id] properties: play_mode_id: { type: integer } pricing_rule_id: { type: integer, nullable: true } custom_hourly_rate: { type: number, nullable: true } responses: '200': { description: OK } '422': { description: Invalid pricing row } /games/{game}/stations: put: tags: [Games] summary: Sync game installs (Admin). Empty station_ids [] clears all installs. parameters: - { in: path, name: game, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [station_ids] properties: station_ids: type: array items: { type: integer } responses: '200': { description: OK } /offers: get: tags: [Offers] summary: List offer templates (v1) parameters: - { in: query, name: type, schema: { type: string, enum: [hour_pack, discount] } } - { in: query, name: is_active, schema: { type: boolean } } - { in: query, name: search, schema: { type: string } } - { in: query, name: play_mode_id, schema: { type: integer }, description: Hour packs with hours > 0 for this play mode } - { in: query, name: per_page, schema: { type: integer, default: 50 } } responses: '200': { description: OK — each item includes allocations[] } post: tags: [Offers] summary: Create offer template (Admin) description: | hour_pack requires allocations with at least one hours > 0. discount requires discount_type, discount_value, applies_to. package / station_type_ids / hours_granted / play_mode_ids are rejected (422). requestBody: required: true content: application/json: schema: type: object required: [name, code, type, price, valid_days] properties: name: { type: string } code: { type: string } type: { type: string, enum: [hour_pack, discount] } price: { type: number } valid_days: { type: integer, minimum: 1 } description: { type: string, nullable: true } notes: { type: string, nullable: true } sort_order: { type: integer } is_active: { type: boolean } discount_type: { type: string, enum: [percent, fixed], nullable: true } discount_value: { type: number, nullable: true, description: Percent 1–100 or fixed INR (up to 2 decimals) } applies_to: { type: string, enum: [plays, session, food, all], nullable: true } allocations: type: array items: type: object required: [play_mode_id, hours] properties: play_mode_id: { type: integer } hours: { type: integer, minimum: 0 } examples: hourPack: value: name: PS5 Starter — 7 hours code: ps5_starter_7h type: hour_pack price: 499 valid_days: 90 allocations: - { play_mode_id: 2, hours: 7 } - { play_mode_id: 1, hours: 0 } discount: value: name: 10% Off Plays code: play_10pct type: discount price: 0 valid_days: 7 discount_type: percent discount_value: 10 applies_to: plays responses: '201': { description: Created } '422': { description: Validation / unsupported v1 fields } /offers/{offer}: parameters: - { in: path, name: offer, required: true, schema: { type: integer } } get: tags: [Offers] summary: Get offer responses: '200': { description: OK } patch: tags: [Offers] summary: Update offer (Admin) description: Sending allocations replaces the full allocation set for hour packs. responses: '200': { description: OK } '422': { description: Validation } delete: tags: [Offers] summary: Delete offer (Admin) responses: '200': { description: Deleted } /offers/{offer}/preview: get: tags: [Offers] summary: Preview offer summary for FE cards parameters: - { in: path, name: offer, required: true, schema: { type: integer } } responses: '200': description: OK — summary reflects allocations (e.g. "PS5 5h · ₹500 · valid 30 days after purchase") /customers: get: tags: [Customers] summary: List customers description: > Each row includes active_passes_count, has_active_pass, and earliest_active_expires_at (soonest non-null active expiry; null when none). parameters: - { in: query, name: search, schema: { type: string } } - { in: query, name: is_active, schema: { type: boolean } } - { in: query, name: tag, schema: { type: string } } - { in: query, name: per_page, schema: { type: integer, default: 30 } } responses: '200': { description: OK } post: tags: [Customers] summary: Create customer (staff + admin) requestBody: required: true content: application/json: schema: type: object required: [name, phone] properties: name: { type: string } phone: { type: string, description: Unique after normalization } email: { type: string, nullable: true } notes: { type: string, nullable: true } tags: type: array items: { type: string } date_of_birth: { type: string, format: date, nullable: true } is_active: { type: boolean } responses: '201': { description: Created — includes stats + empty activity } '422': { description: Validation / duplicate phone } /customers/stats: get: tags: [Customers] summary: Portfolio KPI counters for Customers list responses: '200': description: OK — customers_total, customers_active, visits_total, returning_customers, active_passes_count, passes_sold_count; lifetime_spend_inr admin-only /customers/insights: get: tags: [Customers] summary: Customers Insights page payload (KPIs + actionable lists) parameters: - { in: query, name: inactive_days, schema: { type: integer, minimum: 7, maximum: 365, default: 30 } } - { in: query, name: top_limit, schema: { type: integer, minimum: 1, maximum: 50, default: 10 } } - { in: query, name: recent_limit, schema: { type: integer, minimum: 1, maximum: 20, default: 5 } } - { in: query, name: callback_limit, schema: { type: integer, minimum: 1, maximum: 50, default: 20 } } - { in: query, name: expiring_days, schema: { type: integer, minimum: 1, maximum: 60, default: 7 } } responses: '200': description: OK — data.kpis + data.lists (top_spend, top_visits, recently_here, call_back, expiring_passes) + meta.availability '422': { description: Invalid query params } /customers/lookup: get: tags: [Customers] summary: Exact phone lookup (find or create helper) parameters: - { in: query, name: phone, required: true, schema: { type: string } } responses: '200': description: data is customer detail or null; always returns phone_normalized + whatsapp_url /customers/top: get: tags: [Customers] summary: Top customers by spend or visits parameters: - { in: query, name: by, schema: { type: string, enum: [spend, visits], default: spend } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 50, default: 20 } } responses: '200': { description: OK } /customers/{customer}: parameters: - { in: path, name: customer, required: true, schema: { type: integer } } get: tags: [Customers] summary: Get customer detail (stats + recent passes + activity shell) responses: '200': { description: OK } patch: tags: [Customers] summary: Update customer (staff + admin) responses: '200': { description: OK } delete: tags: [Customers] summary: Delete customer (Admin only) responses: '200': { description: Deleted } '403': { description: Staff forbidden } '422': { description: Customer has passes } /customers/{customer}/passes: parameters: - { in: path, name: customer, required: true, schema: { type: integer } } get: tags: [CustomerPasses] summary: List passes for a customer parameters: - { in: query, name: status, schema: { type: string, enum: [active, expired, used_up, voided] } } - { in: query, name: type, schema: { type: string, enum: [hour_pack, discount] } } - { in: query, name: per_page, schema: { type: integer, default: 50 } } responses: '200': { description: OK } post: tags: [CustomerPasses] summary: Sell an offer to a customer (staff + admin) requestBody: required: true content: application/json: schema: type: object required: [offer_id] properties: offer_id: { type: integer } price_paid: { type: number, minimum: 0, description: Admin override only; staff ignored. Defaults to offer.price. Omitted from staff responses. INR up to 2 decimals. } payment_method: { type: string, enum: [cash, upi, card, other], description: Optional display hint } finance_account_id: { type: integer, description: Required when price > 0 — Cash/Bank account that received payment } notes: { type: string, nullable: true } expires_at: { type: string, format: date-time, nullable: true, description: Override default purchased_at + valid_days } responses: '201': { description: Created pass + refreshed customer detail } '422': { description: Inactive offer/customer or invalid sale } /customer-passes/{customerPass}: get: tags: [CustomerPasses] summary: Get a customer pass parameters: - { in: path, name: customerPass, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [CustomerPasses] summary: Update pass expiry (staff + admin) parameters: - { in: path, name: customerPass, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [expires_at] properties: expires_at: { type: string, format: date-time, nullable: true } responses: '200': { description: Expiry updated + refreshed customer } '422': { description: Voided pass or invalid date } /customer-passes/{customerPass}/void: post: tags: [CustomerPasses] summary: Void a customer pass (staff + admin) parameters: - { in: path, name: customerPass, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: reason: { type: string } responses: '200': { description: Voided } '422': { description: Already voided or used up } /customer-passes/{customerPass}/adjust-hours: post: tags: [CustomerPasses] summary: Manually adjust hours on an active hour-pack balance (staff + admin) parameters: - { in: path, name: customerPass, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [play_mode_id, reason] properties: play_mode_id: { type: integer } hours_remaining: { type: number, description: Absolute remaining hours in 0.5 steps (preferred) } delta_hours: { type: number, description: Relative change in 0.5 steps; use instead of hours_remaining } reason: { type: string } responses: '200': { description: Updated pass + refreshed customer detail } '422': { description: Not active hour pack / invalid mode / not 0.5 step / would go below 0 } /finance/account-options: get: tags: [Finance] summary: Active accounts for sell-pass Paid into (staff + admin) responses: '200': { description: data = [{ id, name }] } /finance/accounts: get: tags: [Finance] summary: List accounts with balances (Admin) responses: '200': { description: OK } '403': { description: Not admin } post: tags: [Finance] summary: Create account e.g. Cash / Bank (Admin) requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } is_active: { type: boolean } responses: '201': { description: Created } /finance/accounts/{financeAccount}: patch: tags: [Finance] summary: Rename or deactivate account (Admin) parameters: - { in: path, name: financeAccount, required: true, schema: { type: integer } } responses: '200': { description: OK } /finance/categories: get: tags: [Finance] summary: List income/expense categories (Admin) parameters: - { in: query, name: type, schema: { type: string, enum: [income, expense] } } responses: '200': { description: OK } post: tags: [Finance] summary: Create category (Admin) requestBody: required: true content: application/json: schema: type: object required: [name, type] properties: name: { type: string } type: { type: string, enum: [income, expense] } is_active: { type: boolean } responses: '201': { description: Created } /finance/categories/{financeCategory}: patch: tags: [Finance] summary: Rename or deactivate category (Admin) parameters: - { in: path, name: financeCategory, required: true, schema: { type: integer } } responses: '200': { description: OK } /finance/summary: get: tags: [Finance] summary: Account balances + day_money for a window (Admin) description: | Same from/to/day_start_hour rules as GET /finance/transactions. Includes balances (cash/bank), day_money (same shape as desk kpis.day_money), and legacy today income/expense/net for the resolved window. parameters: - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: day_start_hour, schema: { type: integer, minimum: 1, maximum: 23 }, description: Only when from=to; e.g. 6 = 6am to next 6am } responses: '200': { description: OK — data.day_money + meta.window } /finance/reports: get: tags: [Finance] summary: Period report — money, plays, stock, passes, bookings, ops (Admin) description: | period=week|month|custom. Week/month use period_start; custom uses from/to (optional day_start_hour when from=to). Money via DayMoneyService. Stock COGS/inventory use last_cost estimate. plays.breakdown attributes ended-play charged_inr + pass_hours_burned (even station split for zone/multi); collected_plays_inr stays cashbook. parameters: - { in: query, name: period, required: true, schema: { type: string, enum: [week, month, custom] } } - { in: query, name: period_start, schema: { type: string, format: date }, description: Required for week/month } - { in: query, name: from, schema: { type: string, format: date }, description: Required for custom } - { in: query, name: to, schema: { type: string, format: date }, description: Required for custom } - { in: query, name: day_start_hour, schema: { type: integer, minimum: 1, maximum: 23 }, description: Only when custom and from=to } responses: '200': { description: OK — data.{ money, plays(+breakdown), stock, passes, bookings, ops } } /finance/transactions: get: tags: [Finance] summary: List cashbook transactions (Admin) description: | Each row always includes session_id, session_display_name, session_payment_id, session_payment_type (null when not session-linked). Session desk links use session_id. parameters: - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: account_id, schema: { type: integer } } - { in: query, name: category_id, schema: { type: integer } } - { in: query, name: type, schema: { type: string, enum: [income, expense, transfer] } } - { in: query, name: day_start_hour, schema: { type: integer, minimum: 1, maximum: 23 }, description: Only when from=to; e.g. 6 = 6am to next 6am } - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK — meta.day_window present when day_start_hour used } post: tags: [Finance] summary: Add income / expense / transfer (Admin) requestBody: required: true content: application/json: schema: type: object required: [type, amount, account_id] properties: type: { type: string, enum: [income, expense, transfer] } amount: { type: number, minimum: 0.01 } account_id: { type: integer } category_id: { type: integer, description: Required for income/expense } to_account_id: { type: integer, description: Required for transfer } note: { type: string } occurred_at: { type: string, format: date-time } responses: '201': { description: Created } '422': { description: Insufficient balance / missing category } /finance/transactions/{financeTransaction}: get: tags: [Finance] summary: Transaction detail (Admin) description: Same session deep-link fields as list (null when not session-linked). parameters: - { in: path, name: financeTransaction, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Finance] summary: Update transaction (Admin) — balances recalculated; linked source rows synced description: > Edits a transaction (amount allowed). Balances are undone/reapplied. Linked denormalized rows stay consistent — session_payment (bag payment/refund + bill), stock_purchase/stock_sale headers, booking_group deposit (+ applied session payment), customer_pass price_paid/purchased_at. parameters: - { in: path, name: financeTransaction, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: type: { type: string, enum: [income, expense, transfer] } amount: { type: number, minimum: 0.01 } account_id: { type: integer } category_id: { type: integer, nullable: true } to_account_id: { type: integer, nullable: true } note: { type: string } occurred_at: { type: string, format: date-time } responses: '200': { description: Updated } '422': { description: Invalid update } delete: tags: [Finance] summary: Delete transaction (Admin) — undoes balance and cascades linked rows description: > Removes the transaction and undoes its balance effect. Also removes linked session payments/refunds (rebills the session), reverses stock purchase/sale qty, clears booking deposits, and voids an unpaid pass sale when its income txn is deleted. parameters: - { in: path, name: financeTransaction, required: true, schema: { type: integer } } responses: '200': { description: Deleted } '422': { description: Cascade blocked (e.g. stock purchase qty) } /finance/cash-locations: get: tags: [Finance] summary: List physical cash locations (Admin) responses: '200': { description: OK — includes last_counted_inr, can_deactivate } post: tags: [Finance] summary: Create cash location (Admin) requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } sort_order: { type: integer } responses: '201': { description: Created } /finance/cash-locations/{cashLocation}: patch: tags: [Finance] summary: Update cash location (Admin) parameters: - { in: path, name: cashLocation, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: name: { type: string } sort_order: { type: integer } is_active: { type: boolean } responses: '200': { description: OK } '422': { description: Cannot deactivate } /finance/cash-checks: get: tags: [Finance] summary: List cash checks (Admin) parameters: - { in: query, name: status, schema: { type: string, enum: [draft, submitted] } } - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: per_page, schema: { type: integer, default: 50 } } responses: '200': { description: OK } post: tags: [Finance] summary: Create draft cash check or return existing open draft (Admin) description: > Tracks multi-admin audit — created_by, updated_by, participants[]. Mutating ops (PATCH/refresh/submit/adjust) append the acting admin to participants. responses: '201': { description: Created } '200': { description: Existing draft returned } '422': { description: Cash account count invalid } /finance/cash-checks/{cashCheck}: get: tags: [Finance] summary: Cash check detail (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Finance] summary: Update draft cash check (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: note: { type: string, nullable: true } locations: type: array items: type: object required: [cash_location_id, amount_inr] properties: cash_location_id: { type: integer } amount_inr: { type: number } denominations: type: array items: type: object properties: denomination_inr: { type: integer, enum: [500, 200, 100, 50, 20, 10, 5, 2, 1] } quantity: { type: integer, minimum: 0 } banks: type: array items: type: object required: [account_id] properties: account_id: { type: integer } stated_inr: { type: number, nullable: true } responses: '200': { description: OK } '422': { description: Draft only / denom mismatch } delete: tags: [Finance] summary: Delete draft cash check (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } responses: '200': { description: Deleted } '422': { description: Submitted immutable } /finance/cash-checks/{cashCheck}/refresh: post: tags: [Finance] summary: Refresh draft system balances (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } responses: '200': { description: OK } /finance/cash-checks/{cashCheck}/submit: post: tags: [Finance] summary: Submit cash check when matched (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } responses: '200': { description: Submitted } '422': { description: Variance not zero } /finance/cash-checks/{cashCheck}/adjustments: post: tags: [Finance] summary: Post shortage/overage adjustment from draft (Admin) parameters: - { in: path, name: cashCheck, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [side, amount_inr, direction] properties: side: { type: string, enum: [cash, bank] } account_id: { type: integer, nullable: true } amount_inr: { type: number, minimum: 0.01 } direction: { type: string, enum: [shortage, overage] } category_id: { type: integer, nullable: true } note: { type: string } responses: '201': { description: Created — returns { check, transaction } } '422': { description: Exceeds variance / draft only } /stock/categories: get: tags: [Stock] summary: List stock categories parameters: - { in: query, name: active, schema: { type: boolean } } responses: '200': { description: OK } post: tags: [Stock] summary: Create category (Admin) requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } sort_order: { type: integer } is_active: { type: boolean } responses: '201': { description: Created } /stock/categories/{stockCategory}: patch: tags: [Stock] summary: Update category (Admin) parameters: - { in: path, name: stockCategory, required: true, schema: { type: integer } } responses: '200': { description: Updated } /stock/brands: get: tags: [Stock] summary: List brands (optional category_id filter) parameters: - { in: query, name: active, schema: { type: boolean } } - { in: query, name: category_id, schema: { type: integer }, description: Brands with active items in category } responses: '200': { description: OK } post: tags: [Stock] summary: Create brand (Admin) requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } sort_order: { type: integer } is_active: { type: boolean } responses: '201': { description: Created } /stock/brands/{stockBrand}: patch: tags: [Stock] summary: Update brand (Admin) parameters: - { in: path, name: stockBrand, required: true, schema: { type: integer } } responses: '200': { description: Updated } /stock/items: get: tags: [Stock] summary: List items (filter category/brand/q/favorite/low_stock) parameters: - { in: query, name: category_id, schema: { type: integer } } - { in: query, name: brand_id, schema: { type: integer } } - { in: query, name: q, schema: { type: string } } - { in: query, name: active, schema: { type: boolean } } - { in: query, name: favorite, schema: { type: boolean } } - { in: query, name: low_stock, schema: { type: boolean } } responses: '200': { description: OK } post: tags: [Stock] summary: Create item (Admin) requestBody: required: true content: application/json: schema: type: object required: [stock_category_id, stock_brand_id, name, sell_price_inr] properties: stock_category_id: { type: integer } stock_brand_id: { type: integer } name: { type: string } sell_price_inr: { type: number } last_cost_inr: { type: number, nullable: true, description: INR (up to 2 decimals) } qty_on_hand: { type: integer } low_stock_at: { type: integer, nullable: true } is_favorite: { type: boolean } is_active: { type: boolean } responses: '201': { description: Created } /stock/items/{stockItem}: get: tags: [Stock] summary: Item detail parameters: - { in: path, name: stockItem, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Stock] summary: Update item (Admin) — not qty_on_hand parameters: - { in: path, name: stockItem, required: true, schema: { type: integer } } responses: '200': { description: Updated } /stock/purchases: get: tags: [Stock] summary: List purchase bills parameters: - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK } post: tags: [Stock] summary: Restock from bill (multi-line) requestBody: required: true content: application/json: schema: type: object required: [lines] properties: supplier_name: { type: string } bill_no: { type: string } purchased_at: { type: string, format: date-time } finance_account_id: { type: integer } note: { type: string } lines: type: array items: type: object required: [stock_item_id, qty, unit_cost_inr] properties: stock_item_id: { type: integer } qty: { type: integer } unit_cost_inr: { type: number, description: INR (up to 2 decimals) } responses: '201': { description: Created — qty up, last_cost updated, Finance expense if total > 0 } '422': { description: Missing account / inactive item } /stock/purchases/{stockPurchase}: get: tags: [Stock] summary: Purchase detail parameters: - { in: path, name: stockPurchase, required: true, schema: { type: integer } } responses: '200': { description: OK } /stock/sales: get: tags: [Stock] summary: List sales parameters: - { in: query, name: per_page, schema: { type: integer } } - { in: query, name: session_id, schema: { type: integer } } responses: '200': { description: OK } post: tags: [Stock] summary: Quick sale requestBody: required: true content: application/json: schema: type: object required: [lines] properties: sold_at: { type: string, format: date-time } finance_account_id: { type: integer } note: { type: string } session_id: { type: integer, nullable: true } lines: type: array items: type: object required: [stock_item_id, qty] properties: stock_item_id: { type: integer } qty: { type: integer } unit_price_inr: { type: number, description: INR (up to 2 decimals); omit to use sell_price } responses: '201': { description: Created — qty down, Finance income if total > 0 } '422': { description: Insufficient stock / missing account } /stock/sales/{stockSale}: get: tags: [Stock] summary: Sale detail parameters: - { in: path, name: stockSale, required: true, schema: { type: integer } } responses: '200': { description: OK } /stock/session-snacks: get: tags: [Stock] summary: Session snack sold history (Stock History → Sold → Session) description: > One row per session_snacks line (including fully returned qty=0). Not stock_sales — money still settles on the session bag. Detail on FE = Session drawer via session_id. parameters: - { in: query, name: from, schema: { type: string, format: date }, description: Filter created_at start of day } - { in: query, name: to, schema: { type: string, format: date }, description: Filter created_at end of day } - { in: query, name: q, schema: { type: string }, description: Item/brand/customer/phone/staff/session id/amount } - { in: query, name: session_id, schema: { type: integer } } - { in: query, name: per_page, schema: { type: integer } } - { in: query, name: page, schema: { type: integer } } responses: '200': { description: OK — { data, meta } } /stock/adjustments: post: tags: [Stock] summary: Qty adjust / wastage (stock only) requestBody: required: true content: application/json: schema: type: object required: [stock_item_id, qty_delta] properties: stock_item_id: { type: integer } qty_delta: { type: integer } note: { type: string } responses: '201': { description: Created } '422': { description: Would go negative } /stock/movements: get: tags: [Stock] summary: Stock movement history parameters: - { in: query, name: stock_item_id, schema: { type: integer } } - { in: query, name: type, schema: { type: string, enum: [purchase, sale, adjust] } } - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK } '200': { description: OK — { data, meta } } /desk/dashboard: get: tags: [Desk] summary: Desk dashboard poll payload description: | Floor OS poll. Top-level JSON (no data wrapper). Floor stations include next_booking (next future hold) and paired_with (peripheral hub label). open_sessions include settled due_inr plus So Far estimate fields (estimated_due_inr, estimated_plays_inr, estimate_basis=planned, estimate_incomplete). Live wall = max(planned, ceil elapsed to 0.5h); same formula as GET /sessions/{id}. bookings_today = upcoming overlapping venue business day OR starting within 7 days. kpis (admin only; null for staff): plays_inr = ended play charges in window; food_inr = counter stock_sales + session snacks; collected_inr = all income; to_settle_inr = open-session due > 0. day_money (admin): collected cash/bank + by_source + by_channel + expenses_inr; accounts use kind cash|bank. parameters: - { in: query, name: day_start_hour, schema: { type: integer, enum: [0, 6] }, description: Default 6 (venue DNAX_TIMEZONE) } responses: '200': description: Floor + open sessions + ending_soon + bookings_today; kpis null for staff '401': { description: Unauthorized } '403': { description: Inactive user } /desk/day-board: get: tags: [Desk] summary: Day Board timeline by station description: | Top-level JSON. Block types live | finished | booked (upcoming holds only). Ended plays use type=finished + play_status=ended. Every block includes coverage (primary|held|zone), anchor_station_id, anchor_station_name, and covered_station_count (tooltip helper). Zone modes (blocked types / full floor / uses_pairing_group+requires_hub) paint every covered station as coverage=zone. Otherwise the anchor row is primary and collateral holds are held. parameters: - { in: query, name: date, required: true, schema: { type: string, format: date } } - { in: query, name: day_start_hour, schema: { type: integer, enum: [0, 6] }, description: Default 6 } responses: '200': description: Rows with live/finished/booked blocks content: application/json: schema: type: object properties: date: { type: string, format: date } day_start_hour: { type: integer } window: type: object properties: from: { type: string, format: date-time } to: { type: string, format: date-time } rows: type: array items: type: object properties: station_id: { type: integer } name: { type: string } blocks: type: array items: type: object required: [type, start, end, coverage, anchor_station_id, anchor_station_name] properties: type: { type: string, enum: [live, finished, booked] } start: { type: string, format: date-time } end: { type: string, format: date-time } label: { type: string } play_id: { type: integer, nullable: true } session_id: { type: integer, nullable: true } booking_id: { type: integer, nullable: true } customer_name: { type: string, nullable: true } game_name: { type: string, nullable: true } play_mode_name: { type: string, nullable: true } play_status: { type: string, nullable: true, enum: [live, ended] } coverage: type: string enum: [primary, held, zone] description: | primary = play/booking anchor on this row; held = collateral coverage from a non-zone mode; zone = intentional multi-station mode on every covered row. anchor_station_id: type: integer description: Station the play started on / booking line station anchor_station_name: type: string description: Display name for anchor_station_id covered_station_count: type: integer description: How many stations this play/booking line covers '422': { description: Missing/invalid date } /roster/settings: get: tags: [Roster] summary: Get roster settings responses: '200': { description: OK — data null if not configured } put: tags: [Roster] summary: Upsert roster settings (Admin) requestBody: required: true content: application/json: schema: type: object required: [anchor_week_start, partner_user_ids] properties: anchor_week_start: { type: string, format: date, description: Must be Monday } partner_user_ids: type: object required: [D, N, A] properties: D: { type: integer } N: { type: integer } A: { type: integer } shifts: type: array items: type: object properties: code: { type: string, enum: [morning, mid, late] } label: { type: string } start_time: { type: string, example: '10:00' } end_time: { type: string, example: '13:00' } responses: '200': { description: OK } '422': { description: Invalid anchor or partners } /roster: get: tags: [Roster] summary: Resolved roster range (max 42 days) parameters: - { in: query, name: from, required: true, schema: { type: string, format: date } } - { in: query, name: to, required: true, schema: { type: string, format: date } } responses: '200': { description: OK } /roster/today: get: tags: [Roster] summary: Today / now snapshot (Asia/Kolkata) responses: '200': { description: OK } /roster/overrides: get: tags: [Roster] summary: List overrides in range parameters: - { in: query, name: from, required: true, schema: { type: string, format: date } } - { in: query, name: to, required: true, schema: { type: string, format: date } } responses: '200': { description: OK } post: tags: [Roster] summary: Create or replace override (Admin) requestBody: required: true content: application/json: schema: type: object required: [date, user_ids] properties: date: { type: string, format: date } slot: { type: string, nullable: true, enum: [morning, mid, late] } user_ids: { type: array, items: { type: integer }, minItems: 1 } reason: { type: string } responses: '201': { description: Created } '403': { description: Admin only } /roster/overrides/{override}: delete: tags: [Roster] summary: Delete override (Admin) parameters: - { in: path, name: override, required: true, schema: { type: integer } } responses: '200': { description: Deleted } /bookings: get: tags: [Bookings] summary: List bookings (overlap filter) parameters: - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: day_start_hour, schema: { type: integer } } - { in: query, name: today, schema: { type: boolean } } - { in: query, name: status, schema: { type: string }, description: Comma list } - { in: query, name: customer_id, schema: { type: integer } } - { in: query, name: booking_group_id, schema: { type: integer } } - { in: query, name: q, schema: { type: string } } - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK } # create is POST /booking-groups /bookings/{booking}: get: tags: [Bookings] summary: Booking detail parameters: - { in: path, name: booking, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Bookings] summary: Patch upcoming booking (window / lines / note) parameters: - { in: path, name: booking, required: true, schema: { type: integer } } responses: '200': { description: OK } '422': { description: Not upcoming / conflict } /bookings/{booking}/cancel: post: tags: [Bookings] summary: Cancel upcoming booking (release holds) parameters: - { in: path, name: booking, required: true, schema: { type: integer } } responses: '200': { description: OK } /bookings/{booking}/no-show: post: tags: [Bookings] summary: Mark no-show (release holds) parameters: - { in: path, name: booking, required: true, schema: { type: integer } } responses: '200': { description: OK } /bookings/{booking}/check-in: post: tags: [Bookings] summary: Check in one booking → session + optional auto_start plays description: > auto_start defaults planned_wall_hours from booking window (ends−starts, 0.5h steps). Optional override via plays.planned_wall_hours or plays.lines[].planned_wall_hours. parameters: - { in: path, name: booking, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [session, station_ids, plays] properties: session: type: object properties: mode: { type: string, enum: [new, reuse] } session_id: { type: integer } station_ids: type: array items: { type: integer } plays: type: object properties: mode: { type: string, enum: [auto_start, manual] } planned_wall_hours: { type: number, description: Optional override; default = booking window length } pricing_rule_id: { type: integer } game_id: { type: integer, nullable: true } lines: type: array items: type: object properties: play_mode_id: { type: integer } station_ids: { type: array, items: { type: integer } } pricing_rule_id: { type: integer } game_id: { type: integer, nullable: true } planned_wall_hours: { type: number } responses: '200': { description: OK — booking + session + plays (auto_start includes planned_wall_hours) } '422': { description: Validation / conflict } /booking-groups: post: tags: [Bookings] summary: Create booking group (+ windows + optional deposit) requestBody: required: true content: application/json: schema: type: object required: [customer_id, bookings] properties: customer_id: { type: integer } note: { type: string } deposit: type: object properties: amount_inr: { type: number } finance_account_id: { type: integer } bookings: type: array items: { type: object } responses: '201': { description: Created } '422': { description: Conflict / validation } /booking-groups/{bookingGroup}: get: tags: [Bookings] summary: Group detail with all bookings parameters: - { in: path, name: bookingGroup, required: true, schema: { type: integer } } responses: '200': { description: OK } /booking-groups/{bookingGroup}/deposit: post: tags: [Bookings] summary: Take or top-up deposit (none→held, or additive while held) parameters: - { in: path, name: bookingGroup, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [amount_inr] properties: amount_inr: { type: number, description: Amount to take now (additive on top-up) } finance_account_id: { type: integer, description: Required on first take; optional on top-up } note: { type: string, nullable: true } responses: '200': { description: OK — group with deposit_status held } '422': { description: Not none/held, no upcoming booking, or applied (use session payments) } /booking-groups/{bookingGroup}/deposit/refund: post: tags: [Bookings] summary: Refund held deposit parameters: - { in: path, name: bookingGroup, required: true, schema: { type: integer } } responses: '200': { description: OK } '422': { description: Not held (e.g. already applied — use session refunds) } /booking-groups/{bookingGroup}/deposit/keep: post: tags: [Bookings] summary: Keep held deposit (status only) parameters: - { in: path, name: bookingGroup, required: true, schema: { type: integer } } responses: '200': { description: OK } '422': { description: Not held } /sessions: get: tags: [Sessions] summary: List sessions parameters: - { in: query, name: status, schema: { type: string, enum: [open, closed, cancelled] } } - { in: query, name: customer_id, schema: { type: integer } } - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: day_start_hour, schema: { type: integer }, description: Only when from=to; e.g. 6 = 6am→next 6am } - { in: query, name: today, schema: { type: boolean }, description: Shorthand today + 6am window } - { in: query, name: q, schema: { type: string } } - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK — default excludes cancelled } post: tags: [Sessions] summary: Open session (customer or walk-in) requestBody: required: true content: application/json: schema: type: object properties: customer_id: { type: integer, nullable: true } walk_in_name: { type: string, nullable: true } notes: { type: string, nullable: true } responses: '201': { description: Created } '422': { description: Missing customer/walk-in } /sessions/{session}: get: tags: [Sessions] summary: Session detail (recalculates bill) description: | Includes settled totals (`plays_total_inr`, `due_inr`) plus planned estimate fields: `estimated_plays_inr`, `estimated_due_inr`, `estimate_basis` (always `planned`), `estimate_incomplete` (true when a live play has no planned_wall_hours). Live wall = max(planned, ceil elapsed to 0.5h) so overtime raises the estimate. parameters: - { in: path, name: session, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Sessions] summary: Patch notes / walk-in name / label (open only) parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: notes: { type: string, nullable: true, description: null clears } walk_in_name: { type: string, description: Walk-in sessions only; non-empty } label: { type: string, nullable: true, description: Desk display override for any open session; null clears back to customer/walk-in name } responses: '200': { description: OK — detailed session (display_name reflects label) } '422': { description: Not open / invalid walk-in rename } /sessions/{session}/plays: post: tags: [Sessions] summary: Start one or more plays on session parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [plays] properties: plays: type: array items: type: object required: [station_id, play_mode_id, pricing_rule_id] properties: station_id: { type: integer } play_mode_id: { type: integer } pricing_rule_id: { type: integer } game_id: { type: integer, nullable: true } customer_pass_id: { type: integer, nullable: true } hourly_rate: { type: number, nullable: true } responses: '201': { description: Created } '422': { description: Station busy / closed session } /sessions/{session}/snacks: post: tags: [Sessions] summary: Add snacks (stock down now, unpaid) parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [lines] properties: lines: type: array items: type: object required: [stock_item_id, qty] properties: stock_item_id: { type: integer } qty: { type: integer } unit_price_inr: { type: number, nullable: true, description: INR up to 2 decimals; omit to use sell_price } responses: '201': { description: Created } /sessions/{session}/discount: patch: tags: [Sessions] summary: Set/clear play discount parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: discount_type: { type: string, enum: [percent, fixed], nullable: true } discount_value: { type: number, nullable: true, description: Percent 1–100 or fixed INR (up to 2 decimals) } responses: '200': { description: OK } /sessions/{session}/payments: post: tags: [Sessions] summary: Add payment (advance allowed) parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [amount_inr, finance_account_id] properties: amount_inr: { type: number } finance_account_id: { type: integer } note: { type: string } responses: '201': { description: Created } /sessions/{session}/refund: post: tags: [Sessions] summary: Refund (cannot exceed net paid) parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [amount_inr, finance_account_id] properties: amount_inr: { type: number } finance_account_id: { type: integer } note: { type: string } responses: '201': { description: Created } /sessions/{session}/close: post: tags: [Sessions] summary: Close when due == 0 and no live plays parameters: - { in: path, name: session, required: true, schema: { type: integer } } responses: '200': { description: OK } '422': { description: Live plays or due != 0 } /sessions/{session}/reopen: post: tags: [Sessions] summary: Reopen a closed session (desk correction) description: | Closed only (not cancelled). Requires reason. Clears closed_at/closed_by, appends reason to notes, undoes visit_count bump from close. parameters: - { in: path, name: session, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: { type: string, maxLength: 500 } responses: '200': { description: OK — status open again } '422': { description: Not closed / missing reason } /sessions/{session}/cancel: post: tags: [Sessions] summary: Cancel empty / abandoned open session description: | Open only. No live plays; no ended plays (including ₹0 pass); no snacks; no payments/refunds; discount cleared. Sets status=cancelled, closed_at/closed_by. Does not bump visit_count. Voided plays OK — Close pass-only visits instead. parameters: - { in: path, name: session, required: true, schema: { type: integer } } responses: '200': { description: OK } '422': { description: Not empty / not open } /plays/start: post: tags: [Plays] summary: Direct start (auto-create session) requestBody: required: true content: application/json: schema: type: object required: [plays] properties: session_id: { type: integer, nullable: true } customer_id: { type: integer, nullable: true } walk_in_name: { type: string, nullable: true } plays: { type: array, items: { type: object } } responses: '201': { description: Created } /plays/past: post: tags: [Plays] summary: Record already-finished play requestBody: required: true content: application/json: schema: type: object required: [station_id, play_mode_id, pricing_rule_id, started_at, ended_at] properties: session_id: { type: integer, nullable: true } customer_id: { type: integer, nullable: true } walk_in_name: { type: string, nullable: true } station_id: { type: integer } play_mode_id: { type: integer } pricing_rule_id: { type: integer } started_at: { type: string, format: date-time } ended_at: { type: string, format: date-time } wall_hours: { type: number } hourly_rate: { type: number } charged_inr: { type: number } customer_pass_id: { type: integer } pass_hours_burn: { type: number, description: Must be ≤ wall_hours × players } responses: '201': { description: Created } '422': { description: Validation / pass over-burn } /plays: get: tags: [Plays] summary: List plays parameters: - { in: query, name: status, schema: { type: string, enum: [live, ended, voided] } } - { in: query, name: session_id, schema: { type: integer } } - { in: query, name: station_id, schema: { type: integer } } - { in: query, name: from, schema: { type: string, format: date } } - { in: query, name: to, schema: { type: string, format: date } } - { in: query, name: day_start_hour, schema: { type: integer } } - { in: query, name: q, schema: { type: string } } - { in: query, name: per_page, schema: { type: integer } } responses: '200': { description: OK } /plays/{play}: get: tags: [Plays] summary: Play Detail drawer (actions, coverage, revisions) parameters: - { in: path, name: play, required: true, schema: { type: integer } } responses: '200': { description: OK } patch: tags: [Plays] summary: Quick edit live or ended play (no revision row) parameters: - { in: path, name: play, required: true, schema: { type: integer } } responses: '200': { description: OK } /plays/{play}/end-preview: get: tags: [Plays] summary: End preview suggestions (query params) parameters: - { in: path, name: play, required: true, schema: { type: integer } } - { in: query, name: customer_pass_id, schema: { type: integer, nullable: true } } - { in: query, name: ended_at, schema: { type: string, format: date-time } } - { in: query, name: wall_hours, schema: { type: number } } - { in: query, name: suggestion_code, schema: { type: string } } responses: '200': { description: OK } post: tags: [Plays] summary: End preview (preferred — body-aware) parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: suggestion_code: { type: string } wall_hours: { type: number } pass_hours_burn: { type: number } charged_inr: { type: number } customer_pass_id: { type: integer, nullable: true } ended_at: { type: string, format: date-time } responses: '200': { description: OK — includes suggestions + selected } /plays/{play}/end: post: tags: [Plays] summary: End live play description: | `ended_at` (default now) drives auto wall hours when `wall_hours` omitted. `suggestion_code` applies with or without `ended_at`; pass chosen here feeds suggestion math. Prefer copying `selected` from POST end-preview. parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: suggestion_code: { type: string } wall_hours: { type: number } pass_hours_burn: { type: number } charged_inr: { type: number } customer_pass_id: { type: integer, nullable: true } ended_at: { type: string, format: date-time } note: { type: string } reason: { type: string } responses: '200': { description: OK } '422': { description: Not live / ended_at before started_at / pass over-burn } /plays/{play}/correction-preview: post: tags: [Plays] summary: Dry-run correction for Play Detail parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: station_id: { type: integer } wall_hours: { type: number } hourly_rate: { type: number } charged_inr: { type: number } customer_pass_id: { type: integer, nullable: true } pass_hours_burn: { type: number } started_at: { type: string, format: date-time } ended_at: { type: string, format: date-time } responses: '200': { description: OK } /plays/{play}/correct: post: tags: [Plays] summary: Correct live or ended play (audited) parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: { type: string } version: { type: integer, description: Optimistic lock } station_id: { type: integer } wall_hours: { type: number } hourly_rate: { type: number } charged_inr: { type: number } customer_pass_id: { type: integer, nullable: true } pass_hours_burn: { type: number } started_at: { type: string, format: date-time } ended_at: { type: string, format: date-time } note: { type: string } responses: '200': { description: OK } '422': { description: Stale version / validation } /plays/{play}/reopen: post: tags: [Plays] summary: Reopen ended play to live parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: { type: string } version: { type: integer } responses: '200': { description: OK } '422': { description: Station busy / not ended } /plays/{play}/delete: post: tags: [Plays] summary: Soft-delete play (preferred desk delete) description: Restores pass hours, releases occupancy, writes revision. Prefer over /void. parameters: - { in: path, name: play, required: true, schema: { type: integer } } requestBody: content: application/json: schema: type: object properties: reason: { type: string } version: { type: integer } responses: '200': { description: OK } /plays/{play}/void: post: tags: [Plays] summary: Void play (legacy soft-delete) description: Same outcome as /delete but no revision / version / custom reason. Prefer /delete. parameters: - { in: path, name: play, required: true, schema: { type: integer } } responses: '200': { description: OK } /session-snacks/{sessionSnack}/return: post: tags: [Sessions] summary: Return snack qty (restore stock; keep row for history) description: > Decrements net qty, increments qty_returned. Full return keeps the row (qty=0). Does not create stock_sales. parameters: - { in: path, name: sessionSnack, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [qty] properties: qty: { type: integer } responses: '200': { description: OK — snack kept with updated qty / qty_returned } /customers/{customer}/pass-options: get: tags: [Plays] summary: Pass options for play mode parameters: - { in: path, name: customer, required: true, schema: { type: integer } } - { in: query, name: play_mode_id, required: true, schema: { type: integer } } responses: '200': { description: OK }