Skip to content

05 · Airline group ticketing & block inventory

Source: business requirements provided by Alhuda management, 2026-09-17. Status: DECIDED as the target specification (see decisions-log.md). Section numbers (§1–§36) are the rule references for this area, e.g. AIR §16. Related rules: capacity and holds in 04-inventory-and-groups.md, infant seats in 02-passengers-and-children.md (PAX-010, PAX-011), approvals in 08-access-control.md (ACC-030).

Known conflicts with today's system (2026-09-17 gap analysis): blocks start life already purchased (no request / quotation / negotiation / approval stage); seat availability is computed three different ways; no UTR/receipt fields; no deadline alerts; deleting a block hard-deletes its ledger rows (breaks §26 and AUD-002).

"FOC" has two meanings in the system today. (1) Complimentary seats, as in the spec: focSeats on a block or FIT is the number of seats the airline gives free, entered with the purchase and left out of the payable (§15; 20260924110000_foc_seats_are_an_input.sql). (2) A release with no penalty: a filed seat release whose penalty is zero is marked FOC — the cancellation is filed with isFoc = true and the release's type becomes free (§37; 20260930220000_a_release_without_penalty_is_foc.sql). The 2026-09-17 gap analysis found only the second meaning in code; the first has existed since 2026-09-24.

1. Objective

The Airline Ticketing Module manages the complete lifecycle of airline group blocks — from initial requirement and airline quotation through negotiation, confirmation, payment, inventory creation, passenger allocation, seat release, cancellation, ticketing and final reconciliation.

The system maintains a complete audit trail of: who created the request; who negotiated the fare; who approved the fare; PNR details; payments made; seats received; seats sold/allocated; seats released; FOC received; cancellation charges; airline penalties; refunds; outstanding balance; final profitability.

2. Main flow

GROUP REQUIREMENT → CREATE GROUP ID → AIRLINE GROUP REQUEST → AIRLINE QUOTATION / INITIAL FARE
→ FARE NEGOTIATION → NEGOTIATED FARE RECEIVED → ACCEPT / REJECT
   IF REJECTED → return to negotiation / close request
   IF ACCEPTED ↓
GROUP PNR CREATION → INITIAL PAYMENT / DEPOSIT → AIRLINE BLOCK CONFIRMATION → AIR INVENTORY CREATED
→ PASSENGER ALLOCATION / SALES → SEAT RELEASE MONITORING → FOC / PAID RELEASE / CANCELLATION
→ FINAL PASSENGER LIST → FINAL PAYMENT → TICKETING → POST-TICKETING RECONCILIATION → GROUP CLOSED

3. Group ID

Every airline group request receives a unique, permanent, system-generated Group ID.

Format: <AIRLINE>-<DEPDDMON>-<RETDDMON>-<N>D-<SERIAL> — e.g. AI-15AUG-29AUG-15D-001 (AI = airline, 15AUG = departure, 29AUG = return, 15D = duration, 001 = serial).

4. Group request — mandatory fields

Field Details
Group ID Auto-generated
Travel type Hajj / Umrah / Ziyarat / General
Airline Required
Sector e.g. SXR-DEL-JED
Departure date Required
Return date Required
Total passengers Required
Adults / Children / Infants Numbers
Baggage requirement Required
Meal requirement Required
Cabin Economy / Business
Preferred flight Required
Alternative flight Optional
Requested fare Optional
Target fare Optional
Ticketing deadline If known
Deposit requirement If known
Remarks Free text

5. Airline group request

Status: DRAFT → SUBMITTED TO AIRLINE → UNDER AIRLINE REVIEW → FARE RECEIVED

Record: request date/time; airline; airline contact; sales representative; email/communication reference; requested seats; requested flights; requested dates; requested fare; airline response; response date; quotation validity.

6. Initial airline fare

Quotations are stored as versions and never overwritten. Example: Requested seats 100; initial fare ₹28,500 + taxes ₹6,500 = ₹35,000/pax.

History example: Quotation V1 ₹35,000 → Negotiation V1 ₹33,500 → Negotiation V2 ₹32,750 → Final accepted ₹32,500.

7. Fare negotiation

Action: Request negotiation. Record: existing fare; requested fare; negotiation amount; reason; expected passenger volume; airline response; negotiated fare; negotiation date; person responsible.

8. Accept / reject

  • Reject → status NEGOTIATION REJECTED; options: renegotiate, request revised quotation, close group request, cancel requirement.
  • Accept → status FARE ACCEPTED; requires approval per company authority limits (e.g. Ticketing Manager → Operations Manager → GM/Director). Approval hierarchy is configurable.

9. PNR creation

Only after fare acceptance and approval. Capture: PNR; airline; flight number; sector; departure/return dates; seats; class; fare basis; baggage; meal; ticketing deadline; release deadline; cancellation policy; no-show policy; name submission deadline; payment deadline; airline contact; airline booking reference. PNR is permanently linked to the Group ID.

10. Initial payment

After PNR confirmation, create the airline payable. Support deposits of 10/20/30/50/75/100% or custom amount (e.g. 100 × ₹32,500 = ₹32,50,000; 30% = ₹9,75,000). Record: amount payable; amount paid; payment date; bank account; payment reference; UTR; receipt; outstanding; next payment deadline.

11. Inventory creation

Once the block is confirmed, seats move automatically into Airline Block Inventory, tracking Purchased, Sold/Allocated, Available, Released, Cancelled, FOC.

Available = Purchased − Allocated − Released − Cancelled, updated automatically after every transaction.

Enforced by (the purchase behind the inventory, FIN-030): create_quota_block (20260922120000) writes the block and the browser-built purchase voucher in one transaction on inventory.create; create_quota_block_and_post / post_quota_block_purchase_from_block (20260929010000) do the same for a caller that is not the browser — the native app — deriving the voucher (paid seats × fare at the contract rate to Stock-in-Hand, input GST to GST Input Credit, the whole to the supplier's ledger, §15) from the block row itself, so a block cannot exist with seats sellable and nothing owed. Test: supabase/tests/an_airline_block_posts_its_own_purchase.sql. The deposit (§10) is recorded with it through record_airline_block_payment (initial_payment, 20260929200000). Since §36 the block is created by submitting a draft; create_quota_block_and_post saves a draft and submits it in one transaction.

12. Inventory status (RAG)

  • GREEN — sufficient inventory.
  • AMBER — release deadline approaching / utilisation needs attention.
  • RED — release deadline passed / payment due / critical action.

Shown automatically on the dashboard.

13. Passenger allocation

Allocate against the block by channel (e.g. B2B Partner A 20, Partner B 15, B2C 30, Internal 5 → allocated 70, available 30). Allocation beyond available inventory is blocked unless an authorised override is given.

14. Holds

AVAILABLE → HOLD → CONFIRMED → TICKETED or HOLD → RELEASED. Record: customer/partner; seats; hold date; hold expiry; staff member; package/group; reason. On expiry, alert the responsible employee and offer release.

Enforced by: 20260920110000_inventory_holds_deadlines.sql + 20260920110100_inventory_hold_release_functions.sql (InventoryHold, create_inventory_hold, extend_inventory_hold, convert_inventory_hold, release_inventory_hold, expire_inventory_holds). Held seats stop counting as available; converting a hold to its booking drops the hold in the same step the booking takes the seats, so nothing is counted twice. Covers airline blocks, FIT, hotel rooms and ground capacity (INV-011). UI: /inventory/holds.

15. FOC

Track paid seats and FOC seats separately (e.g. block 100 = 98 paid + 2 FOC). Compute effective cost per passenger using the company's configured FOC treatment. FOC eligibility comes from the actual airline contract, not a fixed assumption.

FOC is an input to the purchase. A block is bought the way the airline sells it — "20 seats, 2 F.O.C" — so focSeats is entered on POST /inventory/quota-blocks and POST /inventory/fit alongside totalSeats, and may be corrected on the PATCH. All totalSeats are sellable; only totalSeats − focSeats are payable. A complimentary seat:

  • is not in the payable. The purchase voucher debits Stock-in-Hand with paid seats × fare. A posting that bills a complimentary seat is refused by create_quota_block() / create_fit_inventory(), not by the browser.
  • lowers the cost per seat, by focTreatment: spread (the default — the block cost over every sellable seat) or margin (each paid seat carries its own fare and the free ones are margin). GET /inventory/quota-blocks/:id returns this as focPosition, and the group cost report charges a departure this figure rather than the list fare.
  • cannot be written off as an unsold cost, because it cost nothing. Of the seats left over when the aircraft goes, the complimentary ones are the last to carry a cost: block_unsold_seat_cost() charges unsold − complimentary seats and the UnsoldSeatWriteOff_foc_guard trigger refuses any other amount (FIN-037).

Enforced by: 20260920110100_inventory_hold_release_functions.sql (block_foc_summary, block_pnl) and 20260924110000_foc_seats_are_an_input.sql (fin_block_payable, block_unsold_seat_cost, create_quota_block, create_fit_inventory). focSeats existed since 20260330100000 and until 20260924110000 nothing could put a number in it: the two blocks on the 29 Aug 2026 departure were sent focSeats: 2 and focSeats: 1 and both stored 0, so ₹1,55,030 of complimentary seats were billed as bought, and then written off a second time as unsold. The release preview warns when a release could take the block below the airline's FOC threshold.

16. Seat release

Dedicated Release seats function. Before release, show: total block; sold; available; proposed release; remaining; release deadline; airline release policy; release charges; FOC impact; financial impact. Creates a release transaction and updates inventory.

Enforced by: preview_seat_release() — that list is the function's return value, and the UI at /inventory/releases renders it unchanged. The preview is recomputed by the database at request time, so the numbers the requester saw are the numbers the approver sees, and the whole preview is stored on the SeatRelease row.

17. Release types (configurable)

A. FOC / free release · B. Paid release · C. Partial release · D. Full release · E. Auto release (by contractual deadline, subject to approval rules).

18. Release approval

Release never deletes inventory: Release request → Approval → Airline release → Inventory update. Approval chain (Ticketing Manager → Operations Manager → GM → Director) configurable by value / seat count.

Enforced by: request_seat_release → approve_seat_release / reject_seat_release → complete_seat_release. The approver is never the requester (a table CHECK as well as the function), and the seat count decides the authority: at or below FinanceConfig.seatReleaseApprovalSeatLimit (working default 10, ACC-030) a Ticketing or Operations Manager signs; above it the approver needs inventory.release.approve_large (CEO/GM). Nothing is deleted — the seats stop being sellable from the moment the release is requested, and the release row keeps the whole history. The ledger consequence is posted by file_airline_cancellation() from the cancellation chain, so there is still exactly one airline posting path (CXL-020); that step needs finance.create, like every other filing.

19. Cancellation

Distinguish customer cancellation, airline block cancellation, passenger cancellation and seat release — never the same transaction. Each cancellation records: passenger; PNR; Group ID; ticket number; date; reason; airline cancellation fee; supplier penalty; company cancellation fee; refund due; refund received; net financial impact; approving employee.

20. Airline cancellation policy

Enter the airline's actual contract. Example rules: before deadline → free release; after deadline → charge; within X days → higher penalty; after ticketing → ticket cancellation rules; no-show → airline no-show rules. Configurable by airline, route, season and contract — no hard-coded "industry policy".

Enforced by: AirlineReleasePolicyBand + lookup_release_penalty(). A band is matched most-specific-first — block, then airline + route + season, then airline + route, then airline + season, then airline — against the days between the release date and departure. Penalty types: free, per_seat_amount, pct_of_fare, full_fare. With no band on file the lookup returns "not found", never zero, and the UI says so: the spec forbids a hard-coded industry policy standing in for the airline's contract. What the airline actually charged is recorded on the release (actualPenalty, actualRefund) and the variance against the expected refund is shown, so the contract can be checked against what was paid.

21. Deadlines

Deposit; balance payment; name submission; ticketing; free-release; paid-release; passport/document; final passenger list; check-in. Dashboard buckets: TODAY / TOMORROW / 3 DAYS / 7 DAYS.

Enforced by: 20260920110000_inventory_holds_deadlines.sql — AirlineQuotaBlock gains depositDueDate, nameSubmissionDeadline, ticketingDeadline, freeReleaseDeadline, paidReleaseDeadline and finalPaxListDeadline (FIT gains the three that apply to it). Before this, one column — expiryDate — stood in for five contractually different dates. expiryDate is kept as a legacy alias and a trigger holds it equal to freeReleaseDeadline in both directions, so the existing block form keeps working; existing rows were migrated on that reading. Passport/document and check-in deadlines are not block columns and are not covered here. The §9 contract detail the gap analysis found missing is also stored now: fare/tax split, fareBasis, baggage, meal spec, cabin and airlineBookingRef.

22. Alerts

  • 7 days before release deadline: "Group X has 35 unused seats. Free-release deadline is approaching."
  • 3 days: "Action required: 35 seats remain unused."
  • 1 day: "URGENT: Release deadline tomorrow."
  • Deadline day: "FINAL ALERT: Airline block release deadline is today."

Sent to the responsible Ticketing Manager and the escalation authority.

Enforced by: inventory_deadline_rows() + fire_inventory_deadline_alerts() + escalate_inventory_deadline_alerts() (InventoryDeadlineAlert). Each rung fires once per deadline occurrence — a unique index makes re-running the sweep a no-op — and carries the seats at risk, the money at risk, the responsible role (Ticketing for release/ticketing/names, Finance for the deposit, Operations for the final pax list; a hold names the person who owns it) and the escalation authority. An urgent rung nobody acknowledges within a day is escalated. Internal only — INT-003 allows purely internal reminders and escalations to fire by themselves; nothing reaches a customer or an airline without a person (UX-001).

Nothing here runs on a schedule. pg_cron is installed by 20260929110100 and runs only the communications dispatcher (LC-007), the WhatsApp template sync (AUD-024) and the leave accruals (LV-062); no inventory job and no scheduled edge function exists. Until one does:

  • the radar derives on read — /inventory/deadlines and the inv.deadline-radar widget compute every rung from the records each time they load, so nothing is missed;
  • the durable alert rows are written when someone runs the ladder from that page (inventory.edit);
  • the hooks are ready for whichever scheduler is chosen. Both migrations carry the exact commands: SELECT cron.schedule('inventory-deadline-alerts', '30 2 * * *', $$SELECT public.fire_inventory_deadline_alerts()$$); (02:30 UTC = 08:00 IST) and SELECT cron.schedule('inventory-hold-expiry', '*/15 * * * *', $$SELECT public.expire_inventory_holds(now(), true)$$);, or a Supabase scheduled edge function calling the same RPCs with the service key.
  • An expired hold needs no scheduler to free its seats — inventory_held_units() ignores a hold past its expiry. The sweep only writes the state change and the audit record.

23. Final passenger list

Fields: serial; name; passport number; DOB; gender; nationality; ticket status; PNR; seat; baggage; meal; visa status; payment status; partner/customer. Locked after final approval; authorised amendments only.

24. Ticketing

PAX CONFIRMED → TICKET ISSUED. Capture: ticket number; PNR; passenger; fare; taxes; commission; supplier cost; selling price; payment status; ticketing staff; issue date/time.

Decided 2026-09-22 (owner): a ticket number may be entered once finance has confirmed the booking (APPROVED, LC-001); an exception, asked with a reason and approved by a different person (LC-010), lets a pilgrim be ticketed earlier. Confirmation does not check payment while PRC-010 is open.

Enforced by: 20260925100000_ticketing_without_sync.sql. A passenger's ticket is created by the database when they are seated (BookingPassengerFlight trigger → ensure_passenger_ticket()), never for a cancelled passenger, and a browser session cannot create one. Readiness is ticketing_confirmed(), worked out live, never stored. issue_tickets() saves a whole departure's numbers in one call — tickets.approve, a reason (UX-001), the number format from TicketingPolicy, no duplicates, all or nothing — with the issuer taken from the session. The ticket's name follows the passenger until issue; a change after issue is recorded for an airline reissue. Screen: one sheet per departure with paste-in numbers (Tickets). Not held: fare, taxes, commission and selling price on the ticket. Tests: supabase/tests/ticketing_without_sync.sql, src/lib/api.tickets.test.ts, src/lib/ticketPaste.test.ts.

25. Financial reconciliation

Compare airline cost vs customer sales vs payments to airline vs refunds/cancellations vs profit.

Final airline cost = Block cost − released-seat adjustment ± cancellation charges − refunds − FOC adjustment Gross airline margin = Total customer sales − Final airline cost

26. Audit trail

Every action creates a permanent log entry (date/time, user, action, old value, new value). No transaction is permanently deleted; corrections are made through reversal/amendment transactions.

Enforced by: 20260917120000_audit_trail_hardening.sql (append-only AuditLog, row triggers on every business table) and 20260920100100_inventory_archive_not_delete.sql for blocks and FIT inventory. A BEFORE DELETE trigger refuses to destroy a block or a FIT that still has group flight links, passenger seats, issued tickets, released-but-unfiled seats, airline cancellations, B2B offers, vouchers, ledger entries, supplier transactions or recorded human edits, and the message names what is linked. archive_airline_block() / archive_fit_inventory() (and their restore twins) are the intended end of life: the row and everything hanging off it stay, the inventory disappears from pickers and refuses new allocations. Archiving is idempotent and refuses while seats are still allocated. A block keyed in by mistake, never used and never edited, is still deletable. AirlineCancellationSeat was missing from the audit-trigger list and is now included. Test: supabase/tests/inventory_integrity.sql.

27. Air block dashboard (summary)

Active blocks; purchased seats; allocated; available; released; FOC; ticketed; outstanding airline payment.

28. Block-wise dashboard

Columns: Group ID · Airline · Sector · Travel date · Block · Sold · Available · Release due · Status.

29. Required modules

1 Airline master (name, code, contact, sales manager, payment terms, cancellation policy) · 2 Route master (sector, flight, dep/arr time, aircraft, operating days) · 3 Group request · 4 Fare quotation · 5 Negotiation · 6 Approval · 7 PNR management · 8 Payment management · 9 Block inventory · 10 Passenger allocation · 11 Hold management · 12 FOC management · 13 Seat release · 14 Cancellation · 15 Ticketing · 16 Refund · 17 Reconciliation · 18 Profitability · 19 Alerts & deadlines · 20 Audit trail.

30. User access

  • Ticketing Executive — create request, enter passenger info, allocate seats, update PNR; edit blocks and sell seats on to another agency; record the airline's payment (the owner, 2026-09-25).
  • Ticketing Manager — create group, negotiate fare, manage blocks, request release, approve operational changes; record the airline's payment.
  • Finance — verify/record payments, approve the payments ticketing records (FIN-044), reconcile supplier account, verify refunds.
  • Operations Manager — approve operational transactions, monitor group inventory.
  • GM — approve major commercial/operational transactions.
  • Director — final authority per company policy and financial limits.

Enforced by (the two ticketing roles and the airline's payment, 20260929145900 / 20260929150000): TICKET_MANAGER owns the blocks (inventory.view, inventory.edit, holds, release request and approval, tickets.approve); TICKET_EXEC is the executive — exactly tickets.view, tickets.edit, bookings.view, bookings.edit, groups.view, inventory.view, inventory.edit, inventory.block_payments.record, dashboard.view and the work-inbox rights, and no approval of any kind (docs/PERMISSIONS.md §5.2). Both record the payment to the airline for a block or a FIT (§10) through record_airline_block_payment, which posts the desktop's voucher pending a second person in finance — FIN-044. Both sell seats on to another agency from the phone through sell_block_seats_to_third_party (INV-014). Test: supabase/tests/ticketing_pays_for_its_blocks.sql.

Owner, 26 Sep 2026 — ticketing owns its blocks. The ticket manager and the ticket executive create blocks (inventory.create) and, when a deposit was paid at purchase, pick the bank or cash account it left from (the list comes from GET /inventory/paid-from-accounts, not the chart of accounts); the deposit is recorded pending finance approval (FIN-044). The ticket manager may approve or reject its own seat release (inventory.release.approve_own): an exception to maker-checker (ACC-020) the owner chose for speed; the release is marked selfDecided and the audit trail keeps an entry, and a release above the seat limit still needs CEO/GM (ACC-030). Enforced by 20260930000000_ticketing_owns_its_blocks.sql (functions and the table check SeatRelease_maker_not_checker); test supabase/tests/ticketing_owns_its_blocks.sql. Block pickers show the code with the PNR. The ticketing roles also record the airline filing of an approved release (inventory.release.file: airline reference, actual penalty and refund); this opens the airline cancellation filing in pending_refund and finance settles the refund (CXL-030) — 20260930030000_ticketing_files_its_releases.sql, test supabase/tests/ticketing_files_its_releases.sql.

31. Status flow

DRAFT → REQUEST SENT → QUOTE RECEIVED → NEGOTIATION → NEGOTIATED FARE RECEIVED → PENDING APPROVAL → APPROVED → PNR CONFIRMED → PAYMENT PARTIAL → BLOCK CONFIRMED → IN INVENTORY → PARTIALLY ALLOCATED → FULLY ALLOCATED → PARTIAL RELEASE → FINAL PAX LIST → TICKETED → RECONCILED → CLOSED

32. System rules

Enforced by (rules 2 and 3, partial): 20260920100000_inventory_counter_integrity.sql — every seat on a block or FIT is accounted for by the derived identity total = allocated + available + cancelled/lost, and an allocation past it is refused (INV-012/INV-013). Rule 4 (released seats do not return to available unless the airline confirms) is handled by the cancellation chain: a released seat stays sellable only until it is filed with the airline, after which it leaves the block (CXL-022). Rule 5 (FOC seats separately identified) is built for the money — focSeats is an input, is out of the payable and out of the unsold-seat write-off (§15, 20260924110000) — but is still not part of the derived seat identity: a complimentary seat is counted as available like any other, so total = allocated + available + cancelled/lost does not distinguish them. Rules 1, 8 and 9 (approved fare before PNR, deadline alerts, financial transactions linked to Group ID and PNR) are not enforced.

  1. A PNR cannot be created without an approved fare.
  2. Inventory cannot be created until the airline block is confirmed.
  3. Every inventory seat must have a status.
  4. Released seats cannot return to available inventory unless the airline confirms reinstatement.
  5. FOC seats must be separately identified.
  6. Cancellation charges are recorded separately from the original fare.
  7. No transaction is deleted from the audit trail.
  8. Every deadline generates an alert.
  9. Financial transactions are linked to the Group ID and PNR.
  10. One Group ID can contain multiple PNRs.
  11. Quotation and negotiation versions are maintained.
  12. Airline-specific contract terms control release and cancellation calculations.

33. Data relationships

AIRLINE MASTER → GROUP REQUEST → QUOTATION → NEGOTIATION HISTORY → APPROVAL → PNR → AIR BLOCK → INVENTORY → PAX ALLOCATION → RELEASE / CANCELLATION → TICKETING → PAYMENT → REFUND → RECONCILIATION → PROFITABILITY

34. Two inventory channels

  • A. Group block inventory — pre-purchased seats, group PNR, negotiated fare, deposit, FOC, release deadlines, paid/free release, passenger allocation.
  • B. Online FIT inventory — live/online booking, individual PNR, dynamic fare, individual cancellation, ticket issue, refund/reissue.

Both feed one Airline Inventory Dashboard showing total seat position, cost, sales, utilisation, released seats and profitability in real time.

35. Open questions for management (holds, deadlines and release)

Raised by the Wave 3 implementation of §14–§22. Each has a working default in the database so the system is usable today; none of them is a decision engineering should be making.

Question Working default Where it lives
How long is a hold, when nobody says? 48 hours FinanceConfig.defaultHoldHours
How many times may a hold be extended, and by whom? Twice, by anyone with inventory.holds.manage FinanceConfig.maxHoldExtensions; consider a separate inventory.holds.extend if only managers may extend
Who may hold seats at all? Ticketing, Operations, Sales and B2B staff inventory.holds.manage grants in 20260920110200
Seat-release approval limit (ACC-030 says 10 seats — Ticketing Manager, above that CEO/GM) 10 FinanceConfig.seatReleaseApprovalSeatLimit
Should the release limit be by penalty value as well as seat count? A 9-seat release inside 14 days can cost more than a 25-seat release three months out. Seat count only Needs a rule before it is built
FOC treatment — does a complimentary seat lower the cost per passenger (spread) or become margin (margin)? spread AirlineQuotaBlock.focTreatment, per block
Penalty bands per airline: who owns entering the contract, and is it per airline, per route, or per season in practice? Whoever has inventory.edit; the model supports all three AirlineReleasePolicyBand
Does releasing seats below the FOC threshold cost the complimentary seats? The preview warns but cannot compute it — the airline contracts differ. Warn only Needs the contract terms
Which scheduler runs the nightly ladder and the hold sweep? None — derive on read AIR §22 note above
Escalation window for an unacknowledged urgent alert 24 hours escalate_inventory_deadline_alerts(p_hours)

36. A block starts as a draft

Status: DECIDED 2026-09-26 (owner). "While creating an airline block, there should be some final submit button to actually create a block, otherwise it should not affect anything, neither ledgers, nor anything else, neither should it list anywhere else, nor should we be able to assign that to the group … once it goes active then it works as intended, and as long as it is in draft, ticket manager should be able to edit/correct to reduce errors."

  1. A new block is first saved as a draft. A draft posts nothing: no journal, no ledger line, no supplier transaction, no deposit.
  2. A draft is not a block. It is in no block list, picker, dashboard, report, deadline radar or partner offer. It cannot be linked to a group, held, released, sold on, allocated, cancelled, written off or paid for.
  3. While it is a draft, every field can be corrected, the cost, seats, complimentary seats, deposit and legs included. inventory.create or inventory.edit may correct or discard one; the drafts list is visible to the same people.
  4. Submit is the final step and needs inventory.create. It runs every check the create path runs (§3 trip type and legs, a unique PNR, §15 complimentary seats, FIN-034 a rate on a foreign contract, a supplier to owe), then creates the block with the draft's id, posts the purchase voucher (FIN-030, pending — FIN-032) and records the deposit (FIN-044), all in one transaction. A refusal leaves a draft, with the reason on it. Submitting twice creates one block.
  5. A submitted draft is closed. The block is edited, re-costed or archived (§26) like any other; it is not discarded.
  6. Discarding a draft deletes it. The audit trail keeps what it was, who discarded it and why.

Enforced by: 20260930010000_a_block_starts_as_a_draft.sql (applied to production on 26 Sep 2026) and 20260930040000_drafts_stay_out_of_sight.sql. A draft is a row in AirlineQuotaBlockDraft, not in AirlineQuotaBlock, so every reader of blocks leaves it out and every link to a block (all foreign keys to AirlineQuotaBlock) refuses it. Functions: create_quota_block_draft, update_quota_block_draft, discard_quota_block_draft, quota_block_draft_preview (what the submit would post, and what is still missing), submit_quota_block. create_quota_block_and_post saves a draft and submits it in one transaction, so both paths share one door; a refusal there still leaves nothing behind. Guard triggers refuse a JournalEntry, LedgerEntry or SupplierTransaction naming an open draft, and a block row written under an open draft's id. Row security: drafts are read by inventory.create or inventory.edit; no one writes the table except through the functions. Audit: quota_block_draft_created, _edited, _submitted, _submit_refused, _discarded. Test: supabase/tests/a_block_starts_as_a_draft.sql. Screens: Airline blocks (desktop and app).

Not built: the desktop's one-shot create_quota_block (browser-built voucher) is still callable for old callers; the desktop screen no longer uses it. A draft cannot be started from a group request or quotation (§4–§8 are not built).

37. A filed release with no penalty is FOC

Status: DECIDED 2026-09-26 (owner). "If seat is released and filed for refund, it shows two options, penalty and Refund / credit expected. If the penalty is zero, it should be categorised as FOC, and if penalty is some amount, it should adjust the refund amount accordingly. Also when it creates approvals, those approvals should mention PNR also and other details as well."

  1. The fare for the released seats is what the request priced them at (§16): expected refund + penalty.
  2. At filing (§18) the refund follows the penalty: refund = fare − penalty, never below zero. A refund the filer enters is kept, because the airline may refund less; the difference against the expected refund is the §20 variance.
  3. A filing with a penalty of zero is FOC. The airline cancellation is filed as FOC, so the block counts the seats as FOC-cancelled, not charged, and the release's type becomes free. A filing with a penalty is charged, and a free release becomes paid. The type asked for at request stays in the stored preview.
  4. An airline refund waiting for approval names the release number, the PNR, the block code, the route and departure date, the seat count, FOC or the penalty, the refund, the airline reference and who filed it — in the filing's note and in the approvals inbox.

Enforced by: 20260930220000_a_release_without_penalty_is_foc.sql (complete_seat_release, dash_approvals_inbox). Test: supabase/tests/a_release_without_penalty_is_foc.sql. Screens: Inventory holds and seat releases (desktop and app); the finance approval shows the PNR next to the block code.

Not built: a seat-release request waiting for approval is not in the approvals inbox; it is decided on the seat releases screen, which now shows the PNR, route and date.