Skip to content

Tickets

One airline ticket for one passenger. Every seated passenger has one automatically; staff enter the ticket number the airline issues. There is nothing to sync.

Routes: /tickets (the departure list), /tickets?flight=<id> (one departure's sheet), /tickets/:id (one ticket's detail), gated on tickets.view.

Rules: AIR §24 (ticketing), LC-001 (a confirmed booking), LC-010 / ACC-020 (maker ≠ checker), CXL-040 (ticket status follows the seat).


1. How to ticket a departure

  1. Open Ticketing. There is one line per departure flight: seated, issued, ready, held, and any exceptions waiting for a decision.
  2. Open the departure. Every seated passenger is on one sheet.
  3. Type numbers into the Ready rows, or press Paste from airline and paste the list the airline sends back — either one number per line in the order of the Ready rows, or a name and a number on each line (surname first, titles, with or without the dash are all read). Preview shows which number goes to which pilgrim and lists anything it could not place. It never guesses between two pilgrims with the same name.
  4. Press Save, say where the numbers came from, confirm. They are saved together: if any one is refused, none is saved and every problem is listed.

On the phone (staff app, Tickets → a departure, tickets.approve) the same paste is Paste from the airline: paste, Read the list, check the preview, say where the numbers came from, and Save sends them in one issue_tickets call, all or nothing. The phone reads the paste with the same code (src/lib/ticketPaste.ts, copied to apps/mobile/src/lib/ticketPaste.ts and held equal by ticketPaste.drift.test.ts). See The native app.

2. When a ticket may be entered

A passenger's row is Ready once finance has confirmed the booking — its status is APPROVED, which the rulebook calls CONFIRMED (LC-001) — or once an exception for that passenger has been approved by a different person. Otherwise the row is Held, and says why.

Confirmation does not yet check payment

Finance can confirm a booking with nothing paid, because what must be paid before confirmation is still an open question (PRC-010). So a confirmed booking can be ticketed with its balance still owed. Until PRC-010 is decided, payment is chased as a receivable, not by holding the ticket. (Before 25 Sep 2026 the ticket waited for the money itself.)

This is worked out live every time the sheet opens and again when the numbers are saved. It is never stored, so there is nothing that can fall out of step.

3. There is nothing to sync

The database creates a passenger's ticket the moment operations seats them on a flight, and never for a cancelled passenger. The ticket's name is the passenger's name: correct the passenger on their booking and an unissued ticket follows. After issue, a name change is recorded against the ticket and the sheet flags it — the airline must reissue and may charge a name-change fee.

Before 25 Sep 2026 the browser created tickets itself, some failures were swallowed, and a Sync Tickets button existed to repair them; a separate "finance clearance" step and two name editors kept copies in step. All of that is gone. The routes that did it answer 410 Gone with what replaced them.

4. Statuses

NOT_READY · ISSUED · VOID · CANCELLATION_PENDING · REFUND_PENDING · REFUNDED

On the sheet a row shows Ready, Held, Issued or Cancelled. Ready and Held are worked out from the booking, not stored. ISSUED, VOID, CANCELLATION_PENDING, REFUND_PENDING and REFUNDED are final: a browser session cannot move a ticket into or out of them.

5. Exceptions need two people

An exception lets a pilgrim be ticketed before finance confirms the booking — the GM approval of AIR §30.

Step Where Permission
Ask Ask for exception on a held row, with the reason tickets.edit
Decide Approve / Refuse on the row, with a reason tickets.approve

The person who asked sees "You asked — someone else decides" and cannot decide it; the database refuses it too. Editing the ticket row cannot approve an exception.

6. What a browser session cannot change

TicketRecord_guard refuses, from a client session: creating a ticket (the database does, from the seat); typing a name onto a ticket (it comes from the passenger); setting the ticket number, issuer or any exception field by editing the row; and any move into or out of a final status.

7. Cancellation

When a passenger's cancellation is approved, release_passenger_services moves the ticket (CXL-040):

Ticket was Becomes
not yet issued VOID
issued CANCELLATION_PENDING → filed with the airline → REFUND_PENDING → settled → REFUNDED; or VOID if the seat is re-used

The seat itself goes back to the block or FIT it came from, onto the released-seat register — see Holds, deadlines and releases.

8. Permissions

Action Permission
See tickets tickets.view
Ask for an exception tickets.edit
Enter ticket numbers, decide an exception tickets.approve

9. Issued documents

The office issues an e-ticket sheet for a booking as a PDF — Alhuda's own sheet, not the airline's ticket document, and it says so on its face (TRV-013). It carries the booking number and group code, every traveller ticketed by Alhuda (name and the last four of the passport) with their ticket number and PNR, the departure's flights (airline, flight number, route, dates and times, PNR, class and baggage where recorded), the office's contact for emergencies and a terms line. A traveller whose passenger row says they do not need a ticket is listed as "own arrangements" and does not block the sheet.

Where: the native app's booking screen, Documents → Issue e-ticket (tickets.view + bookings.view; see The native app), and the desktop booking page's Documents card on the Overview tab (src/components/bookings/BookingDocumentsSection.tsx): the live files with Open, and Issue e-ticket / Re-issue e-ticket for the same pair of permissions, confirmed first, the function's refusal shown under the buttons. The edge function issue-document checks both permissions, gathers the booking with the service role, renders the sheet, stores it on the company Shared Drive under Issued / the booking (ACC-074) and records it in IssuedDocument with its Drive file id (issue_document_record). Open shows the PDF in a viewer on the page. One live sheet per booking: a re-issue supersedes the previous row (supersededAt); nothing is deleted. A second tap within a minute by the same person returns the sheet just issued.

Refused, with the reason shown: any traveller ticketed by Alhuda who has no issued ticket number yet (the refusal names them — enter the numbers on the departure sheet first); a booking finance has not confirmed, or a cancelled one (LC-004); a departure with no flight recorded.

Who opens it: the booking's traveller in the app (My documents → Tickets, through trv_my_documents() and drive-file), the booking's partner (Booking → Documents), and staff with bookings.view. The e-ticket record on TicketRecord is unchanged: the sheet is prepared from it, never the other way round.

10. Not built

  • Ticket deadlines beyond the block's six — passport/document and check-in deadlines are not modelled (AIR §21).
  • A per-seat selling price on a passenger's flight, so airline gross margin cannot be attributed per passenger (AIR §25).
  • Consistency checks — name on ticket against passport, a ticket issued for a traveller with no visa close to departure (INT-130, Wave 4).
  • The final passenger list locked after approval (AIR §23).
  • Fare, taxes, commission and selling price on the ticket. AIR §24 asks for them; the ticket holds the number, PNR, passenger, issuer and time. Airline cost is on the block or FIT row.
  • A payment rule for ticketing. See the warning in section 2.
  • The airline's own e-ticket PDF. The sheet in section 9 is the company's; the airline's document is not fetched or stored. Nor is a sheet per passenger (one per booking), or a notification to the travellers when a sheet is issued.

11. Where to look

Concern Path
Ticketing without a sync — seat trigger, gate, issue_tickets, the sheet supabase/migrations/20260925100000_ticketing_without_sync.sql
Exceptions, guard, policy (first version) supabase/migrations/20260918130000_tickets_visa_comms.sql
Paste reader src/lib/ticketPaste.ts
Cancellation statuses supabase/migrations/20260917130000_cancellation_chain.sql
Issued e-ticket sheet (TRV-013) supabase/functions/issue-document/index.ts, supabase/migrations/20260929120000_an_eticket_and_a_voucher_are_files.sql, apps/mobile/src/components/IssuedDocuments.tsx
Screens src/pages/tickets/
Tests supabase/tests/ticketing_without_sync.sql, supabase/tests/cancellation_chain.sql, supabase/tests/an_eticket_and_a_voucher_are_files.sql, src/lib/ticketPaste.test.ts, src/lib/api.tickets.test.ts, apps/mobile/src/lib/travellerDocs.test.ts

How the screens load

The ticketing list and a departure's sheet were already one request each (ticketing_departures, ticketing_flight_sheet); ticking "Include departed" now keeps the current lines on screen until the longer list arrives. A single ticket's page loads with one request (PRF-010): ticket_screen, read as you with tickets.view (was 12 requests, all one after another). Routes: Operations screens API.