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 (persupabase/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;pnrcan 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.financeClearanceStatus—pending/cleared/exception. Live-recomputed by the API: ifBooking.balanceAmount <= 0, the status flips toclearedon 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 enumpending_clearance/ready_to_issue/issued/void. Backend writes internal enum values (NOT_READY,ON_HOLD,FINANCE_CLEARED,ISSUED, etc.) exposed to the UI asapiStatus.TicketStatusHistory— a full audit trail offromStatus→toStatustransitions withchangedByand 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):
name-update(PATCH) — ops finalizes the passenger name. Status flips toFINANCE_CLEAREDif (a) finance is cleared and (b) a group flight has been assigned; otherwise staysNOT_READYor goes toON_HOLDif an exception is in play.finance-clear(POST) — triggered by finance approval of the booking; writesfinanceClearanceStatus='cleared'+financeClearedAt+ pushes status toFINANCE_CLEARED.request-exception(POST) — ops attaches email proof, status flips toON_HOLD,financeClearanceStatus='exception'.approve-exception(POST) — GM approves; same effect as finance-clear.issue(POST) — writesticketNumber+pnr+issuedAt+issuedBy, statusISSUED. PNR is required — the handler throws 400 if neither the request body, the storedpnr, 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
TicketRecordis tied to a booking and, since the per-passenger migration, usually to a specificBookingPassenger. - The "sync tickets" action (POST
/ticketswithbookingId,groupId, orsyncAll: true— seehandleTicketsinsrc/lib/api.tsline ~15215) generatesTicketRecordrows 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
getGroupFlightSnapshotinsrc/lib/api.tsline ~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/B2BFlightOfferlinkages — 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.
Related tables (Supabase)
TicketRecord— the core row. Created in20260113130121_remote_schema.sql; per-passenger split in20260406220000_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).