Skip to content

Tickets

Per-passenger flight ticket issuance tracker — a workflow queue that follows each ticket from "pending clearance" (finance must clear it) through to "issued" with a final PNR and ticket number. This is not an internal support/helpdesk ticket system; those live in the Requests and Internal Chat modules.

Scope

A "ticket" here is a physical/electronic airline ticket for one passenger on one booking. The module enforces the agency's rule that tickets cannot be issued until finance has cleared the booking (or a GM exception has been approved with email proof). See docs/ALHUDA_ERP_PRD.md lines 20, 30, 40, 45 for the business rule.

What's tracked

Each TicketRecord row represents one passenger's ticket and carries:

  • bookingId + bookingPassengerId + passengerRef — links to the booking and the specific passenger (per supabase/migrations/20260406220000_ticket_records_per_passenger.sql, a booking can have one record per passenger).
  • passengerName — editable; the "name update" workflow lets ops finalize the exact name on the ticket before issue.
  • pnr + ticketNumber — the airline references; pnr can be inherited from the booking's group-flight if the user doesn't enter one.
  • nameUpdateDeadline — UI surfaces tickets "due in ≤ 2 days" as a separate queue.
  • financeClearanceStatuspending / cleared / exception. Live-recomputed by the API: if Booking.balanceAmount <= 0, the status flips to cleared on read even if it wasn't stored that way.
  • financeClearedBy / financeClearedAt — audit of who cleared it.
  • exceptionApprovedBy / exceptionEmailProof — used when a ticket is issued before full finance clearance; a GM approval with attached email proof is recorded.
  • issuedAt / issuedBy — audit of issuance.
  • status — UI enum pending_clearance / ready_to_issue / issued / void. Backend writes internal enum values (NOT_READY, ON_HOLD, FINANCE_CLEARED, ISSUED, etc.) exposed to the UI as apiStatus.
  • TicketStatusHistory — a full audit trail of fromStatustoStatus transitions with changedBy and optional note.
  • TicketDocument — e-ticket PDFs / scans attached to the record.

Workflow

┌─────────────────┐   finance clears     ┌──────────────────┐   issue   ┌─────────┐
│ NOT_READY       │ ───────────────────▶ │ FINANCE_CLEARED  │ ────────▶ │ ISSUED  │
│ (pending name / │                       │ (ready to issue) │           └─────────┘
│  pending funds) │   exception approved ▲│                  │
└────────┬────────┘ ──────────────────────┘└──────────────────┘
         │ exception requested
┌─────────────────┐
│ ON_HOLD         │
│ (exception awai-│
│  ting approval) │
└─────────────────┘

Key transitions (src/lib/api.ts lines ~15029–15160):

  1. name-update (PATCH) — ops finalizes the passenger name. Status flips to FINANCE_CLEARED if (a) finance is cleared and (b) a group flight has been assigned; otherwise stays NOT_READY or goes to ON_HOLD if an exception is in play.
  2. finance-clear (POST) — triggered by finance approval of the booking; writes financeClearanceStatus='cleared' + financeClearedAt + pushes status to FINANCE_CLEARED.
  3. request-exception (POST) — ops attaches email proof, status flips to ON_HOLD, financeClearanceStatus='exception'.
  4. approve-exception (POST) — GM approves; same effect as finance-clear.
  5. issue (POST) — writes ticketNumber + pnr + issuedAt + issuedBy, status ISSUED. PNR is required — the handler throws 400 if neither the request body, the stored pnr, nor the group-flight snapshot provides one.

Tickets "due soon" (filter in Tickets.tsx:72) surfaces rows with NAME_UPDATE_PENDING within 2 days of nameUpdateDeadline — these are the operational hot queue.

Pages

  • src/pages/tickets/Tickets.tsx — queue view with tabs (Pending Clearance / Ready to Issue / Issued / Name Update). Route: /tickets.
  • src/pages/tickets/TicketDetail.tsx — detail view with travel segments, flight assignments, documents tab, issue dialog, upload dialog. Route: /tickets/:id.

Relationship to bookings

Booking ──(1..N)──▶ TicketRecord ──(N)──▶ TicketStatusHistory
                         │         └─(N)─▶ TicketDocument
                         └──(optional)──▶ BookingPassenger (per-passenger)
  • Every TicketRecord is tied to a booking and, since the per-passenger migration, usually to a specific BookingPassenger.
  • The "sync tickets" action (POST /tickets with bookingId, groupId, or syncAll: true — see handleTickets in src/lib/api.ts line ~15215) generates TicketRecord rows from the booking's passenger list. New passengers added to a booking create new ticket records on the next sync.
  • PNR is resolved from multiple sources with a defined precedence (see getGroupFlightSnapshot in src/lib/api.ts line ~14527): per-passenger override → per-leg PNR → block PNR → group-flight PNR.
  • Travel segments shown on the detail page are reconstructed on the fly from the booking's group's AirlineQuotaBlock / FITInventory / B2BFlightOffer linkages — they are not denormalized onto the ticket row.

Finance clearance is live-evaluated

financeClearanceStatus is stored but can be superseded at read time: if the booking's balanceAmount is ≤ 0 when the detail endpoint runs, the response flips the status to cleared even if no explicit clearance event has fired. This prevents stale ticket rows blocking issuance after a late payment. See src/lib/api.ts line ~15003.

Not a helpdesk system

If you're looking for customer service / agent correspondence / internal chat, that's in /communications, /requests, and /internal/chat — separate modules with their own tables (CommunicationsLog, CustomerRequest, Chat*). The Tickets module here is strictly about airline-ticket issuance.

Permissions

Route guard: tickets.view (src/App.tsx:169–170).

Action Permission Enforcement
View /tickets queue or /tickets/:id detail tickets.view <ProtectedRoute>
Sync tickets (create/refresh TicketRecord rows) tickets.edit <PermissionGate> + server requirePermission('tickets.edit') on POST /tickets
Update passenger name (per-row or bulk) tickets.edit <PermissionGate> + server check on PATCH /tickets/:id?action=name-update
Upload ticket document tickets.edit <PermissionGate> + server check on POST /tickets/:id?action=documents
Request an exception (below-clearance ticket) tickets.edit <PermissionGate> + server check on POST .../request-exception
Approve exception tickets.approve <PermissionGate> + server check on POST .../approve-exception
Issue ticket tickets.approve <PermissionGate> on "Issue" button.

See docs/PERMISSIONS.md §6.7 for the canonical matrix. The dedicated role TICKET_MANAGER carries tickets.view + tickets.edit + tickets.approve + reports.export.

  • TicketRecord — the core row. Created in 20260113130121_remote_schema.sql; per-passenger split in 20260406220000_ticket_records_per_passenger.sql.
  • TicketStatusHistory — audit trail.
  • TicketDocument — uploaded e-tickets / scans.
  • Related: BookingPassenger.pnr (per-passenger override), GroupFlight.pnr (default for the leg), AirlineQuotaBlock.pnr / .returnPnr (block default).