Bookings
A booking is one sale for one departure. It ties a customer to a travel group, carries the passengers who travel, and moves through a lifecycle that the database enforces.
Rules: 01 · Customer lifecycle (LC),
02 · Passengers and children (PAX),
04 · Inventory and groups (INV).
1. The lifecycle
stateDiagram-v2
[*] --> DRAFT: create_booking
DRAFT --> PENDING_OPS: submit_booking
PENDING_OPS --> PENDING_FINANCE: ops_decide_booking (approve)
PENDING_OPS --> NEEDS_CORRECTION: ops_decide_booking (send back)
PENDING_FINANCE --> APPROVED: finance_decide_booking (approve)
PENDING_FINANCE --> REJECTED: finance_decide_booking (reject)
PENDING_FINANCE --> NEEDS_CORRECTION: finance_decide_booking (send back)
NEEDS_CORRECTION --> PENDING_OPS: resubmit_booking
NEEDS_CORRECTION --> PENDING_FINANCE: resubmit_booking
APPROVED --> PARTIALLY_CANCELLED: cancellation flow
APPROVED --> CANCELLED: cancellation flow
PARTIALLY_CANCELLED --> CANCELLED: cancellation flow
The nine statuses are DRAFT, PENDING_OPS, PENDING_FINANCE, APPROVED, ON_HOLD,
NEEDS_CORRECTION, REJECTED, PARTIALLY_CANCELLED, CANCELLED.
APPROVED is what the rulebook calls CONFIRMED
LC-001 names the confirmed state CONFIRMED. The
database enum says APPROVED and there is no CONFIRMED label. They are the same
state. IN_TRAVEL, RETURNED and COMPLETED are in the rule and are not built —
Wave 5.
ON_HOLD exists in the enum but nothing moves a booking into it — that is the open
question LC-OPEN-1.
Status is never edited
A browser session cannot change a booking's status at all. The Booking_lifecycle_guard
trigger refuses it:
Booking status cannot be edited. Use submit, approve, send back, resubmit or the cancellation flow (LC-002)
Status moves only through submit_booking, ops_decide_booking, finance_decide_booking,
resubmit_booking and apply_booking_cancellation_status, and only along the transitions
above. A new booking always starts DRAFT — creating one in any other status is refused.
The same trigger also refuses a browser session changing createdBy, either approval
status, either approver, submittedBy, sentBackFrom or deletedAt, and refuses moving a
booking out of its group by editing groupId (use move_booking_to_group).
2. Maker ≠ checker
Booking.createdBy is stamped from the session on insert and can never be changed
afterwards. Both approval functions refuse the creator
(LC-010):
Maker-checker: you cannot approve or send back a booking you created (LC-010)
The approver of record is auth.uid() — never a name supplied by the screen. resubmit,
send back, reject, move group and delete each require a written reason of at least
three characters, which goes into the audit trail
(UX-001).
Which screen does what
-
Sales approvals lists bookings waiting for the first check: the price, the discount and the customer. The approver can Approve (on to finance) or Send back (to the person who entered it, with a reason). They cannot reject: refusing a booking outright is finance's decision, and taking one off the books is the cancellation flow. Neither the money received nor finance's decision holds this approval back — finance comes after it.
The stored status is still
PENDING_OPSand the permission is stillapprovals.approve; only the wording changed, when the owner confirmed on 2026-09-23 that this check is the sales manager's, not operations' (who arrange hotels, seats and visas and do not approve bookings). - Finance → Finance approvals lists only bookings the first approver has already passed. Finance can Approve (the booking is confirmed), Reject (terminal, reason required) or Send back. - If you entered the booking yourself, the Approve button is disabled with the reason on hover: someone else has to decide it (LC-010).
Payment is not checked at approval — deliberately, for now
LC-002 says finance confirms a booking when the "payment policy is met". What that policy is — a deposit, a percentage, full payment by a number of days before departure — is the open question PRC-010. Until the owner decides it, finance can confirm a booking with nothing paid, and the balance stays on the customer's (or the sub-agent's) account as a receivable.
3. What must be true before a booking moves
bl_booking_problems() is the single readiness check, run on submit, on ops approval and on
resubmit. Finance does not re-run it — finance sees what operations already cleared.
Per passenger:
- a name;
- a date of birth — "Date of birth missing for X (PAX-004)";
- a passport number.
Per booking:
- at least one active passenger;
- at least one adult if there is a child or an infant (PAX-020);
- at most one infant per adult (PAX-011).
Then group capacity is re-checked under a row lock (INV-002) — two people racing for the last seat cannot both win; the second one waits and is refused.
GET /sales/bookings/:id/readiness returns the same list so the screen can show the
blockers before anyone presses submit.
4. Passengers, categories and counts
- Category is calculated, never typed. A trigger overwrites
passengerCategoryfrom the date of birth and the service date — the group's departure date, or the booking date when there is no group. A category typed by a screen is replaced (PAX-001, PAX-003). - The cut-offs are configuration, not code.
PassengerCategoryConfigholds them: infant under 2, child under 12, adult from 12. Readable by any signed-in user, editable only withadmin.config.edit. Changing it recategorises passengers whose trip has not started. The row is markedPROPOSED— management has not confirmed the ages (PAX-002). - Counts are derived.
adultCount,childCount,infantCount,passengerCountandpassengersIncompleteare recomputed from active passenger rows. Values typed by a screen are silently replaced (PAX-006). - Infants do not take a group seat. Group capacity counts active non-infant passengers (INV-001, PAX-010).
- The passenger list says what each traveller bought, and whether it has been arranged. A Services column marks the flight, visa, hotel, meals and ground transport that traveller was sold (PAX-034). A solid mark means it is arranged — a seat taken, a visa in hand, a bed, a meal plan, a place on the transfer. A dashed mark means it was sold and still has to be arranged. A service they did not buy is not shown at all. A cancelled row does not count as arranged, and a visa counts only once it is issued or collected (VISA-010); a refused visa is marked in red and never as covered. Hover a mark to read it in words.
Rejected by finance
A rejection is a stronger send-back, not a dead end (LC-005). The booking page shows who refused it and why, and offers the same two ways out: put right what finance objected to and send it back to them, or cancel it. Sending it back runs every readiness check, so an incomplete booking is refused with what is missing.
A refused booking gives its seats back to the departure immediately, and takes them again when it goes back for approval. Before this it held them for ever: a handful of rejected bookings could make a departure report itself full and refuse real travellers, with nothing on screen to explain it.
Going back for approval counts its travellers again before anything is saved: if the departure filled up meanwhile, it is refused ("Group capacity exceeded on … (INV-002)"); if the departure's sales are stopped, it is refused with the reason ("Sales for … are stopped … a rejected booking cannot be sent for approval again"). Reopen sales or raise the capacity first.
Sent back for correction
A booking operations or finance sends back reads Needs correction, and the booking page shows who sent it back, what they asked for, and when. Two things can happen next:
- Send for approval again — correct what was asked for, say what you changed, and the
booking returns to whichever stage sent it back (
sentBackFrom): operations if operations sent it, finance if finance did. The database refuses a booking that is still incomplete and says exactly what is missing, and that is what the screen shows. - Cancel the booking — the ordinary cancellation flow (LC-020).
The same action is on the bookings list and in the corrections queue. All three used to call it "resubmit to finance" even when operations had sent the booking back; they now name the stage that is actually waiting.
Who is family here
Being on one booking does not make people a family (PAX-036). The booking page's Passengers
tab has a Family card when the booking has two or more travellers, or a child or infant.
It shows the families on the booking (a family may take in travellers from other bookings of
the same departure — shown as "on BK-…"), who is marked Not family, and who is Not
recorded. With bookings.edit, while anyone is not recorded, it asks Who is family here?:
| Choice | What happens |
|---|---|
| All one family | Opens the family form pre-filled: the primary adult as head, a name from the head's surname, and each relationship read from what was typed at booking where it is certain ("Wife" → Spouse). The rest are left blank — "family" and "friend" say nothing — and must be chosen before Save |
| Split into families | The same form, empty; save one family at a time until everyone is placed |
| None are family | Marks every traveller not yet recorded as Not family |
| Decide later | Saves nothing; the question comes back next time |
Nothing is required: a booking is submitted and approved without it. It is counted in the departure's readiness instead (Groups §4.2). A booking with one traveller is not asked.
Guardians (PAX-021). Each child and infant shows Guardian: … or No guardian, with Set guardian: pick an adult travelling on the same departure — on this booking or another.
Not built on this card: editing or dissolving a family, removing one member, and clearing a guardian (do these on the group's Passengers tab). The passengers table above refreshes its family details on the next load.
5. Money on a booking
The balance starts when finance approves the booking voucher (FIN-033, #509). The booking's price and GST post on one voucher that waits for a finance approver; until it is approved — or if it is rejected — the balance is 0, and no payment reminder goes out. The customer can still pay, up to the booked amount, and the invoice shows the booked amount throughout.
A 0 balance before approval is not "Fully paid" (owner, 01/10/2026). Each booking also
carries its agreed due — price + GST − receipts − cancellation credit — and whether it is
awaiting approval (the voucher not yet approved). While it is awaiting approval and
something agreed is unpaid, the booking page's Balance card, the Payments tab's Balance due
line and the bookings list's Amount column say Awaiting finance approval — ₹X will be due
in amber, in the booking's currency; never Fully paid in green. A row's expanded Payment
Details says it under Outstanding. The staff phone pages in the browser (/m/bookings and
/m/bookings/:id) say it too, and Receive payment offers the agreed due as its quick amount.
The phone's booking screen, its bookings list and Take payment say the same. Once finance approves the voucher, the
balance takes over. A booking paid in full before approval reads Fully paid. A cancelled,
rejected or transferred booking is never shown as awaiting.
What can be paid now is the agreed due, approved or not: online (razorpay-order, the
customer app, the partner portal and app, the WhatsApp payment link) and by I have paid
(customer_submit_payment, partner_submit_payment, which also subtract the claims still
waiting). Nothing is due only when the agreed due is 0.
Booking.paidAmount is recomputed by the database from verified payments and cannot be
set from the browser. Recording a payment does not move it; verification by a second person
does. See Receipts and refunds.
The Payments tab lists the booking's payments. A verified payment has Receipt
(finance.view or bookings.view): it asks the edge function issue-document for the
payment's receipt — made the first time, the same file after that — and opens the PDF in the
document viewer. The receipt carries the receipt number, the date received, who paid with their
party code, the booking, the amount in figures and in words, the method and reference, who
verified it and the balance after it
(FIN-045). It
is also listed under Documents on the Overview tab. A payment waiting to be verified, a
rejected one and a refund have no receipt; re-issuing a receipt is an API call (reissue), not a
button.
Pricing on a rate-sheet group comes from the group's rate sheet per service line and
passenger category; a typed price is refused without group_pricing.edit
(PAX-030,
PRC-001).
What can be edited when
The price and the travellers are sales' to change only while the booking is with sales (PRC-003):
| Booking is | Price, rates, GST, currency, customer, payer, partner/source | Add or remove a traveller, a traveller's own price, a date of birth that changes adult/child/infant | Contact and passport details, notes, room choices |
|---|---|---|---|
| Draft, Needs correction, Rejected | Yes | Yes | Yes |
| Pending ops, Pending finance, On hold, Approved, Partially cancelled (and later) | No | No | Yes |
Once the booking has left sales, Edit opens the wizard with those fields read-only and the note "This booking is with operations/finance — send it back for correction to change the price or travellers." A save that changes one of them anyway is refused with that message (409), and the database refuses the same write from any other screen. To change the price or the travellers, ask the approver to send the booking back for correction; to take a traveller off, cancel them; to move one, transfer them. Nothing else is locked: a passport correction or a new phone number saves as before. The service flags (ticket, visa, hotel, meals, ground) are not locked.
The browser can never move a traveller to another booking by editing (that is a transfer,
LC-031), un-cancel one, or add or remove one without
bookings.create or bookings.edit.
Saving an edit is all or nothing. The booking's fields, its travellers (changed, added,
removed), the GST and the accounting entry are saved in one database step (update_booking).
If one traveller cannot be saved — say a passport that is not valid for the group — nothing is
saved: the total is not left raised without the traveller, and no entry is posted. The message
says what was refused. A booking whose price is given later (saved at 0) posts its full booking
entry the first time it is priced, not a part adjustment.
Price agreed at finance approval. When finance approves a booking that never posted its booking entry (a partner's booking, or one whose screen was closed mid-save), the entry is posted then, as the approver, and waits for a second finance person. If it cannot be posted — Finance Settings missing, for example — the approval is refused with the reason and the booking stays waiting (FIN-033).
Creating a booking is refused on a group that is closed or has already departed, in a currency the system does not support, or for a business partner or payer that does not exist (LC-036). A customer may not be in a group twice — counting only live bookings: a cancelled, rejected or transferred booking does not stop them booking again (LC-035). Pressing Create booking twice, or again after the screen lost its answer, makes one booking.
Converting a lead to a booking books every traveller the enquirer named (the customer first) at the price after the discount (SAL-002).
Finance approval re-runs the readiness and capacity checks operations ran, so a traveller who lost a passport number or a departure that filled up in between is caught before approval.
GST on a new booking. The wizard's pricing step has Apply GST, a rate and an amount. A typed amount is used as it is. Left blank, the server charges GST on the ground margin when the booking is saved, which the wizard cannot work out — so it shows "GST on the ground margin — set when saved" under the subtotal, the amount in words ends "before GST", and the review's total and balance say "+ GST". The server checks the GST it records: never negative, never more than the rate on the whole price, and always the amount the booking's voucher credits to GST Payable (FIN-030).
The printed invoice (GET /sales/bookings/:id/invoice) lists the booking's share of the
departure's costs — airfare, hotel, food, ground transport — and the rest as Ground Services &
Assistance, so the lines add up to the subtotal. When those costs are more than the booking was
sold for, the cost lines would add up to more than the subtotal; the invoice then prints one
Package line at the price instead (PAX-034).
A passenger may carry their own price (BookingPassenger.rate); blank means the
booking's rate for their category applies. A passenger with their own price is never also
counted at the category rate (PRC-004). A
PATCH that leaves a passenger's rate out keeps it; send rate: null to clear it.
6. Cancelling
Cancelling is a request that somebody else approves — never an edit (LC-020).
| Step | Route | Permission |
|---|---|---|
| Preview the policy charge and refund | GET /sales/bookings/:id/cancel-preview |
bookings.cancel |
| Request | POST /sales/bookings/:id/cancel |
bookings.cancel |
| Approve | POST /sales/bookings/:id/cancel/approve |
bookings.cancel.approve |
| Reject | POST /sales/bookings/:id/cancel/reject |
bookings.cancel.approve |
The same four exist per passenger under /sales/bookings/:b/passengers/:p/…, and there is a
paged POST /sales/bookings/bulk-cancel.
What is charged comes from the group's cancellation policy, or the default policy
(PRC-020). The database works it out
(cancellation_quote) for each traveller from their own price
(PAX-031):
- the band for the days before departure charges a flat amount, a percentage, or both. Days are calendar days in India: the departure date less today's date in IST, whatever the time of day;
- kept items are added on top — for example the visa price from the rate sheet, kept only once the visa has been applied for (PRC-023);
- the charge never exceeds the traveller's price, and the rest is refunded.
What is kept is booked for what it is. The quote splits the retained amount into the company's own charge (the flat amount plus the percentage) and the pass-through costs it recovers, and the approval posts them to their own heads — 4200 Cancellation Fee Income and the cost head each kept item recovers, defaulting by rate-sheet line (visa → 5500, ticket → 5100). Nothing of a cancelled trip is left in Package Revenue. Where the charge is capped, or an approver overrides the refund downwards, the company recovers what it actually spent before it books any fee (FIN-038).
The preview shows the band in words and each traveller's kept items. A group with no policy and no default, a band that does not cover the day count, or a kept rate-sheet item the group has no rate for is refused with a message — the system never assumes a full refund.
The request is written by the database (request_booking_cancellation,
request_passenger_cancellation, 20261007100000). It records who asked and when — the
signed-in user, never a name the screen sends — and needs a reason. The browser cannot write a
request. A request still pending from before this change that does not say who asked cannot
be approved ("This cancellation request does not say who asked for it, so it cannot be
approved. Ask for the cancellation again."); ask for it again and it records you.
The approval is written by the database (approve_passenger_cancellation, #495,
ACC-020). For each traveller it
checks that a cancellation was asked for — theirs, or their booking's — by someone other than
the approver; that the refund is no more than the traveller's price; that it is no more than
the policy refunds unless the approver overrides it and writes why in the approval note;
and that the booking's refunds stay within what it bills. A second approval returns the first.
The browser cannot write an approved cancellation, its refund, charge or approver — not on a
new traveller, and not afterwards.
Policies are edited under Operations → Cancellation policies, which anyone with
groups.view can open. Creating, changing or deleting one needs cancellation_policies.manage,
and a save writes the policy, its bands and its kept items in one transaction. A change applies
to cancellations from then on; approved cancellations keep their amounts. A policy used by a
group cannot be deleted. Three starter policies ship with the system: Umrah — standard
(₹10,000 plus the visa once applied for, rising nearer departure — review its later bands),
Full refund and Non-refundable.
- The requester cannot approve. Checked in the route and again by a trigger on both
BookingandBookingPassenger, which also refuses an approver who is not the signed-in user. - A refund override is set by the approver, not the requester. Sending one on the request is a 400 (PRC-021, ACC-020).
- A draft is deleted, not cancelled — "Draft bookings are deleted, not cancelled (LC-021)". Every other pre-travel status can be cancelled.
- On approval,
release_passenger_servicesruns first, because it carries the permission check: if it refuses, no journal, credit note or status change has happened yet. It releases flight seats to the block or FIT they came from and onto the released-seat register, releases hotel, meal and transport allocations, moves an issued ticket toCANCELLATION_PENDINGand an unissued one toVOID, and closes the visa case (CXL-010, CXL-040, CXL-050). - The booking's own status is then derived:
CANCELLEDwhen no active passenger is left,PARTIALLY_CANCELLEDwhen some remain. - A whole booking is approved in one step (
approve_booking_cancellation, 20261008140000, LC-020). Every traveller still on the booking is approved with their refund, the booking's approval is written and its status derived, in one database transaction. If any traveller's refund is refused — more than their price, more than the policy without an override — nothing is cancelled and the booking stays as it was — and the database tries the approval before any seat is released or credit note issued, so that refusal comes back with nothing touched. While one approval is under way (up to five minutes), a second click is refused: "This cancellation is already being approved (by …). Wait a minute, then open the booking again." If the travellers changed since the refund was worked out, the approval is refused: "The travellers on booking BK-… changed since the refund was worked out. Open the booking again and approve it again." A second click finds it done and changes nothing.
Not yet one transaction end to end
LC-020 asks for the whole chain to be one
all-or-nothing database step. Seat and service release is one step and the approval
(travellers, booking, status) is another; the group-invoice credit notes before the
approval, and the journal reversal and retention vouchers after it, are still run from
src/lib/api.ts. A failure in those is recorded as a posting failure for finance to retry.
7. Deleting
Only a DRAFT booking can be deleted, and the delete is soft — the row and its history stay
(LC-021). It is refused, with the reason, when the draft
has any of:
- a payment;
- money carried into or out of it with a transferred traveller;
- a traveller transferred into it;
- a visa case;
- a ticket with a number.
Cancel such a booking instead. delete_booking records the reason, releases allocations and,
in the same step, rejects every pending voucher of the booking and reverses every approved one
(the reversal waits for a finance checker unless the person deleting may approve journals and
did not make the voucher). Nothing is left on the books for a booking that is gone. A voucher
already partly reversed refuses the delete — ask finance to reverse the rest first. A browser
session cannot set deletedAt or physically delete any booking. The confirmation asks for the
booking number to be typed (UX-001).
8. Moving a booking to another group
move_booking_to_group — needs groups.edit (or booking.transfer / bookings.edit) and
a reason. It checks capacity on the target group, then releases the old group's seats,
rooms, meals and transport back to the register without cancelling passengers, tickets
or visa cases (LC-030). Automatic re-assignment in the
new group and the "needs assignment" list are not built.
Transferring one passenger
Booking detail → a passenger → Transfer (booking.transfer), the group page → Passengers →
a traveller → Transfer to another group or Remove from group… → Moving to another group
(the same dialog, PAX-035),
the staff app's group screen (transfer_traveller_to_group), or
several at once from the bookings list. Pick the new group and, optionally, a booking in it; otherwise the passenger
lands on the booking already made for their booking on that group, or a new one is made
(LC-033). Travellers of one booking
moved to the same group — one by one or several at once — land on one new booking. If the customer already has a
live booking on that group, a new one is not made: the message names the booking to transfer into instead
(LC-035).
Confirmed stays confirmed (LC-034). A traveller moved from a confirmed booking at the price they were sold at lands on a booking that is confirmed at once — it is not sent to operations and finance again. Moved at a new price, the new booking goes for approval like any other, and the person who made the transfer cannot approve it at finance.
What a traveller cannot land on. A cancelled or rejected booking, a transferred one, or a draft that was not made for this transfer. A traveller (or booking) with a cancellation request waiting is not transferred until the request is decided.
The trail. The booking page's Passengers tab has a Transfers list: each traveller moved in or out, from which booking and group to which, the price if it changed, when, and by whom. Each traveller who came in says Transferred from BK-… next to their name — on a booking made for the transfer and on one that already existed.
The new booking keeps the old one's identity (LC-033): the old booking's customer and payer (not the traveller's own customer record), its business partner and channel — direct or through a partner, and so the partner's commission terms — its sales owner, its emergency contact, and a line Transferred from BK-… at the top of the page. A traveller never lands on a booking of another channel: moving a partner's traveller into a direct booking, or into another partner's, is refused with the reason — transfer them to a new booking instead.
The money follows them (LC-032). What was received for the traveller now counts on the new booking: its Paid includes it, and its Payments tab shows Payment carried from BK-…; the old booking shows Carried to BK-…. No payment row is moved, split or deleted — the receipt stays with the booking that received it. The travellers who stay are paid for first; the traveller takes what is left, up to their price; the last traveller out takes everything left — except a refund owed to a cancelled traveller, which stays on the old booking. When the two bookings bill different parties (a relative's direct booking), a pending voucher moves the credit between their ledgers for a second person to approve.
The old booking (LC-031). When some travellers leave, it stays open and shows N transferred out → BK-… on the list and the page. When the last one leaves, it becomes Transferred: the list shows Transferred → BK-… instead of "Fully paid" or "Due", its total, paid and balance are 0, and the page shows "BK-… was transferred to BK-…; it is read-only." with no Edit, Add Passengers, Add Payment, Cancel or Refund. The database refuses every change to it, its travellers and its payments; printing a receipt for money received before the transfer still works. The last traveller cannot be moved out while a payment on the booking is waiting to be verified or a refund on it is waiting for approval — decide those first. There is no undo: a traveller who comes back is transferred again, to a new booking.
Rate Override is the price the passenger moves at (PRC-005):
- Blank — the passenger keeps the price they were sold at: their own price if they had one, otherwise the old booking's rate for their category (PRC-004). The old booking's total drops by that price and the new booking's rises by the same amount. If the new booking charges a different rate, the passenger carries their old price as their own.
- A price — the passenger lands on the new booking at that price. The old booking still
gives up what they were sold at. The difference is posted to the accounts as its own
voucher on the new booking, with the reason typed at the confirm step: a lower price as a
discount (
6700Discounts Allowed), a higher one as package revenue (4000). A reason is required whenever the price changes.
Worked example, a pilgrim sold at ₹1,15,000:
| Rate Override | Old booking | New booking | Vouchers (all pending) |
|---|---|---|---|
| blank | −1,15,000 | +1,15,000 | receivable 1,15,000 old → new |
| 1,00,000 | −1,15,000 | +1,00,000 | receivable 1,15,000 old → new; Dr 6700 Discounts 15,000 / Cr receivable 15,000 |
| 1,30,000 | −1,15,000 | +1,30,000 | receivable 1,15,000 old → new; Dr receivable 15,000 / Cr 4000 revenue 15,000 |
The passenger, both totals and both vouchers move together in one database step — if any part is refused, nothing moves. The receivable voucher only posts when the old booking's price is in the ledger (its revenue voucher, or an earlier transfer into it).
What it does not do:
- Change the service charge (FIN-039). It stays on the old booking, where it was earned; a booking made for the transfer does not charge it again.
- Change the price of a booking that carries GST. The transfer is refused with a message; a plain transfer (no price) still works, but GST does not follow the passenger either. How GST should follow is not decided (FIN-010).
- Adjust agent commission. It stays as posted on the old booking.
- Check discount authority. There is no limit on how far the price can move; the limit is PRC-002, which is open.
- Transfer a cancelled passenger.
- Carry money for transfers made before 2 Oct 2026. Those bookings are marked Transferred, but the money they received stays on them until finance decides where it goes (runbook).
- Let a manager override the channel refusal. Not built; it is an open question in LC-033.
Flight seats on a block or FIT the new group also flies on move with the passenger; any other seat is released to the register (LC-030). The visa case and ticket records follow the passenger, in the same database step as the transfer.
9. Routes
| Route | Method | Permission |
|---|---|---|
/sales/bookings |
GET | bookings.view |
/sales/bookings |
POST | bookings.create |
/sales/bookings/:id |
PATCH | bookings.edit |
/sales/bookings/:id |
DELETE | bookings.delete |
/sales/bookings/:id/submit |
POST | bookings.edit |
/sales/bookings/:id/readiness |
GET | bookings.view |
/sales/bookings/:id/move-group |
POST | groups.edit |
/sales/bookings/:id/passengers/:pid/transfer |
POST | booking.transfer |
/operations/bookings/:id/ops-approve, /ops-send-back |
POST | approvals.approve |
/operations/bookings/:id/resubmit |
POST | bookings.edit |
/operations/bookings/:id/corrections/assign |
POST | approvals.approve (checked by assign_booking_correction) |
/finance/bookings/:id/finance-approve |
POST | approve: approvals.approve or finance.bookings.approve_finance; reject: approvals.approve or finance.bookings.reject_finance — what finance_decide_booking accepts |
/finance/bookings/:id/send-back |
POST | approvals.approve or finance.bookings.reject_finance |
/sales/bookings/bulk-resolve |
POST | bookings.view |
10. The list screen
/sales/bookings. Filtering, searching and counting happen in the database, not over
the rows the browser holds (PLT-051). One SQL
predicate serves the page, its count, its KPI tiles and its bulk target set, so they cannot
disagree. Paging is keyset, not offset — in an approval queue "skipped" means "never
approved". See Lists, paging and search.
The paging bar under the list shows rows per page (25, 50 or 100), which bookings are on screen out of how many ("1–25 of 312") and first, previous, next and last page — even when they all fit on one page (UX-030). The select-all box ticks the bookings on the page being shown.
Search covers the booking number, the customer's name, phone and passport, and each passenger's name, passport, PNR and phone.
The tiles and the Status filter agree (PLT-051).
Each status word means one set of statuses, defined once in the database
(bkg_status_set), and a tile counts exactly what the filter word of the same name lists:
| Filter word | Lists |
|---|---|
| Awaiting approval (any stage) | Pending operations, pending finance and on hold — the Awaiting approval tile |
| Pending operations | Pending operations and on hold (the Approvals queue's own filter) |
| Pending finance · On hold · Draft · Needs correction · Confirmed · Rejected | that status |
| Partially cancelled | bookings with some travellers cancelled; they still travel |
| Cancelled | fully cancelled bookings only |
| Transferred | bookings whose travellers were all transferred out (LC-031) |
Total value (INR, live bookings) adds the price of bookings that are live — not draft,
cancelled, rejected or transferred — in rupees only. Bookings in another currency are not
added in; the tile says how many there are. GET /sales/bookings/stats also returns the
outstanding balance and, apart from it, what is due once finance approves
(FIN-033), on the same rupee-only basis; the list does
not show them as tiles.
Resubmit shows the booking going back to whoever sent it back — operations, or finance if finance did (LC-002) — and then the status the server answered.
The toolbar also narrows to when the booking was made (Booked, Indian days, both ends included) and sorts by booked on, last updated, booking number, amount or status; the Booking #, Amount and Status columns sort on a click. The date range counts in the KPI tiles and in "select all matching" as it does in the list, because the same filter reaches all three (UX-030).
11. Where to look
| Concern | Path |
|---|---|
| Lifecycle functions and guards | supabase/migrations/20260918120000_booking_lifecycle.sql, 20261007100000_booking_db_guards.sql (edit lock, cancellation requests, draft delete) |
| Cancellation chain | supabase/migrations/20260917130000_cancellation_chain.sql |
| Paging and search | supabase/migrations/20260920130000_bookings_paging_search.sql; status words and tiles 20261008140000_booking_screens.sql |
| Booking cancellation approval in one step, correction owner, partner resubmit | supabase/migrations/20261008140000_booking_screens.sql; test supabase/tests/booking_screens.sql |
| List and detail screens | src/pages/sales/Bookings.tsx, src/pages/sales/BookingDetail.tsx |
| Handlers | src/lib/api.ts — handleSalesBookings, handleOperationsBookings, handleFinanceBookings |
| Tests | supabase/tests/booking_lifecycle.sql, booking_db_guards.sql, bookings_paging.sql, cancellation_chain.sql, src/lib/api.bookingLifecycle.test.ts, src/lib/api.bookingDbGuards.test.ts |