Approvals
The operations half of the booking approval handshake, and the corrections queue.
Rules: LC-002 (allowed transitions), LC-010 (maker ≠ checker), ACC-030 (approval limits).
1. The two stages
A booking is approved twice, by two different people, neither of whom may be the person who created it:
| Stage | Who | Function | Route |
|---|---|---|---|
| Operations | an approver holding approvals.approve |
ops_decide_booking |
POST /operations/bookings/:id/ops-approve |
| Finance | an approver holding approvals.approve or finance.bookings.approve_finance |
finance_decide_booking |
POST /finance/bookings/:id/finance-approve |
Operations checks that the booking is complete and the inventory exists. Finance then approves, rejects, or sends it back.
/approvals (src/pages/approvals/Approvals.tsx, gated on approvals.view) is the ops
queue. The finance queue is Finance → Approvals.
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).
Not twin-tracked any more
These used to be two independent flags — opsApprovalStatus and
financeApprovalStatus — that a screen could set in any order. They are now stages of
one status. A booking reaches finance only after operations has approved it, and
neither flag can be written from the browser.
2. What each decision does
Approve (ops) — re-runs the readiness check
(Bookings §3) and re-checks group capacity under a lock, then moves the
booking to PENDING_FINANCE. If the booking is not ready it is refused by name:
"Booking cannot be approved: Date of birth missing for …".
Send back — moves the booking to NEEDS_CORRECTION and records which stage sent it
back. A reason of at least three characters is required. resubmit_booking returns it to
exactly that stage, not to the beginning.
Reject (finance) — REJECTED, with a reason. Whether sales may resubmit a rejected
booking or must create a new one is an open question
(LC-OPEN-2); today REJECTED is terminal apart from
cancellation.
Both decisions are idempotent — a second click on an already-decided booking returns
alreadyDecided rather than acting twice.
3. Maker ≠ checker
Both functions refuse the creator, for approve and for send-back:
Maker-checker: you cannot approve or send back a booking you created (LC-010)
The approver recorded is always auth.uid(). Booking.createdBy is stamped from the
session and cannot be edited afterwards.
4. Corrections
/corrections (src/pages/approvals/CorrectionsQueue.tsx) lists bookings in
NEEDS_CORRECTION so sales can fix and resubmit them. Assignment is
POST /operations/bookings/:id/corrections/assign, written by assign_booking_correction,
which checks approvals.approve itself. Before 20261008140000 the browser wrote the owner
on the row, which needs bookings.edit, so an approver without it (finance) saw "Owner
updated" while nothing changed (ACC-020).
Resubmitting needs bookings.edit and a reason, and re-runs the full readiness check. A
partner resubmits its own sent-back booking from the partner portal instead
(PTR-023).
The corrections queue has the same paging bar as the approvals queue. Its owner filter narrows the rows already loaded, so with an owner picked the bar keeps fetching until a page is full and counts "N+" until the end of the queue is reached.
5. Other things that need a second person
The approvals queue is not the only place segregation of duties applies. The full list is in Maker-checker; the ones an operations user meets are:
- Cancellations — requested by one person, approved by a holder of
bookings.cancel.approve(Bookings §6). - Seat releases — requested with
inventory.release.request, approved withinventory.release.approve, and above the seat limitinventory.release.approve_large(Holds, deadlines and releases). - Ticket exceptions — requested with
tickets.edit, decided withtickets.approveby someone else (Tickets). - Refunds and vouchers — Receipts and refunds, Journals. A reversal is approved only by someone who made neither the voucher nor the reversal (ACC-020). Above ₹25,000, every voucher a person writes — journal, contra, on-account receipt, reversal on custom lines — is approved by the CEO or GM; a finance manager sees "₹30,000 is above your approval limit for journals (₹25,000). It needs CEO or GM." (ACC-030, limits).
- A partner over its credit limit — a booking that would take a partner over its limit
is refused, for staff as for the partner. A finance manager, the CEO or the GM
(
partners.credit.override) may save it with a reason, which is recorded; there is no queue for it — the person saving asks one of them (PTR-030, Partners). -
Pending Refunds (Finance → Reports → Pending Refunds) — two kinds of filed cancellation wait here for finance, each tagged in the list:
- Airline — seats on an airline block or FIT cancelled with the airline, with the
airline's refund still to come (AIR §19,
FIN-044). Finance approves it when the money
arrives, or takes it as a supplier credit note: the bank receipt (or the supplier
credit) is posted and the Cancellation Refund Receivable is cleared; a difference from
the expected refund goes to Cancellation Charges.
finance.cancellations.approve_airline. - B2B Buyer — seats we sold to another agency, cancelled by the buyer
(INV-014). Approving posts the reversal of the
sale and credits the buyer's receivable with the refund; when the seats were also
cancelled with the airline, the supplier side is posted too. The person who filed it
cannot decide it.
finance.cancellations.approve_b2b.
Rejecting either one reverses the filing and puts the seats back. The banner on the screen says the same. Money the airline returns outside a filed cancellation is recorded on the block or the FIT instead (Record refund from the airline, Airline blocks). That refund is a pending voucher in the approvals inbox, not a row in Pending Refunds.
- Airline — seats on an airline block or FIT cancelled with the airline, with the
airline's refund still to come (AIR §19,
FIN-044). Finance approves it when the money
arrives, or takes it as a supplier credit note: the bank receipt (or the supplier
credit) is posted and the Cancellation Refund Receivable is cleared; a difference from
the expected refund goes to Cancellation Charges.
6. What is not built
- Routing by value. ACC-030 says a request above a limit should be routed to the next approver. Refund and manual-journal limits are enforced at the moment of approval, but nothing routes a queue item to the right person. The approvals inbox shows everything the viewer may decide.
- One unified approvals inbox across bookings, finance, cancellations, releases and
ticket exceptions — Wave 4. Today each lives on its own screen,
with the role dashboard's
approvals.inboxwidget pulling them together for a count. - "Last materially edited" as a bar to approving — only the creator is tracked (LC-010).
7. Permissions
| Action | Permission |
|---|---|
| See the queue | approvals.view |
| Approve, reject, send back a booking | approvals.approve |
| Finance decision on a booking | approvals.approve or finance.bookings.approve_finance / .reject_finance |
| Approve a cancellation | bookings.cancel.approve |
| Approve / reject an airline cancellation refund (Pending Refunds) | finance.cancellations.approve_airline |
| Approve / reject a B2B buyer cancellation (Pending Refunds) | finance.cancellations.approve_b2b |
| Resubmit after correction | bookings.edit |
| Save a partner booking over the partner's credit limit, with a reason | partners.credit.override |
How the screen loads
The operations queue and its money tiles load with one request
(PRF-010): approvals_screen, which is the bookings list's own page
(booking_list_page, as you) with booking_stats over the same filters beside it (was 6 requests, 5 one after
another). Opening a booking's detail still asks for it when the dialog opens.
Routes: Operations screens API.
Dates and order
The operations approval queue and the corrections queue take the bookings list's search, a Booked date range and a sort (booked on, last updated, booking number, amount), oldest first by default because a queue is worked from the front. Both show the booking's own number (UX-030).