Invoices (finance lens)
Written April 2026 — read this first
The invoice model below is still right. An issued invoice can no longer be edited — its amount, GST, number, booking and issue date are locked, and a change of price is a credit or debit note (FIN-012, LAW). Invoice, group-invoice and partner-invoice numbers come from one atomic counter (Numbering). A partner can no longer mark their own invoice PAID (PTR-050).
Customer invoices and Group invoices from the Finance perspective — GSTIN + HSN fields, line-level tax calc, and the permissions around create / approve / void.
1. Two invoice flavours
Customer invoice — Invoice
One row per billable line. Polymorphic via referenceType /
referenceId so it can hang off:
- A booking —
referenceType = 'booking'. - A B2B third-party sale —
referenceType = 'b2b_third_party_sale'(no underlying booking). - A group invoice line — rolled up via
GroupInvoice.
Columns relevant to finance (see supabase/migrations/2026*):
number— printed invoice number.installmentNo— when the invoice is one installment of a staggered bill.amountDue,amountPaid,balanceDue,status.dueDate,issuedAt.hsnCode,sacCode— added bygst_primitives.sql.journalEntryId— link to the posting journal once approved.
Group invoice — GroupInvoice
Covers multi-passenger / multi-booking aggregation. Line items are
stored as JSONB (no physical GroupInvoiceLine table) — so HSN/SAC
lives inside the JSON payload, not in a column.
UI: src/components/invoices/GroupInvoiceDialog.tsx. The dialog handles:
- Line item composition (name, HSN/SAC, rate, qty, unit price).
- Line-level GST calculation.
- Customer / payer selection (multi-booking group).
- Issue, post to finance, print and cancel actions.
Three steps: draft → issue → post to finance
A group invoice reaches the books in three separate steps. They are not merged, and the dialog says so in a line above the items.
| Step | Where | What it does | Permission |
|---|---|---|---|
| 1. Create draft invoice | Group → Invoices tab, on the payer's row | Makes a DRAFT priced from the group rate sheet and opens it. No number, nothing posted. Confirm says Create draft. | group_invoices.create |
| 2. Issue | The invoice dialog, on a draft | Mints the invoice number and locks the document (FIN-012). Posts nothing. Asks for a reason (UX-001). | group_invoices.issue |
| 3. Post to Finance | The invoice dialog, on an issued invoice not yet posted | POST /group-invoices/:id/post writes the voucher through createJournalWithLines with referenceType = 'group_invoice': Dr the payer's receivable, Cr revenue, Cr GST payable. The voucher is pending; a finance approver approves it (§6). Asks for a reason. |
group_invoices.issue |
Until step 3 the invoice is a document only: the header badge reads Not posted. When the group's bookings already posted their own revenue, Post posts nothing and the dialog says Nothing to post: the bookings on this group already posted their revenue (FIN-036).
The tab button was labelled Issue invoice until October 2026, although it only ever made a draft. It is now Create draft invoice.
Cancel and Cancel (credit note)
An issued invoice has a red Cancel button in the dialog
(group_invoices.cancel, a reason). Its label depends on whether the
invoice was posted:
- Cancel — the invoice was never posted to finance. It is marked CANCELLED and nothing is posted.
- Cancel (credit note) — the invoice was posted. A reversing credit
note is posted against the invoice's voucher
(
referenceType = 'group_invoice_cancellation', §6), then the invoice is marked CANCELLED. If passenger credit notes already reversed part of the invoice (CXL-002), only what is still billed is reversed.
Either way a new invoice can be made for the payer afterwards. Cancelling twice returns the cancelled invoice and posts nothing more.
Internal invoice
The Invoices tab also has Internal invoice on each payer
(group_invoices.create). It makes a draft with free-form items (name,
quantity, rate) and optional tax lines, for in-house records. It is
issued and printed like any invoice, but it is never posted to
finance: the dialog has no Post to Finance button for it, and the
confirm says so. It carries an Internal badge.
2. GSTIN + HSN fields
GSTIN capture
| Table | Column |
|---|---|
Customer |
gstNumber |
Supplier |
gstNumber |
Agent |
gstNumber |
See supabase/migrations/20260418150000_gst_primitives.sql. Validated
client-side via isValidGstin() (src/lib/gstin.ts:40).
HSN / SAC on lines
| Table | Columns |
|---|---|
QuotationLineItem |
hsnCode, sacCode |
SupplierTransaction |
hsnCode, sacCode |
Invoice |
hsnCode, sacCode |
GroupInvoice (JSON lines) |
hsnCode / sacCode inside the JSONB line object |
Business rule (UI-enforced, not DB CHECK): pick HSN for goods, SAC for services. Rows can legally carry both in legacy data.
3. Line items and tax calc
On a customer invoice:
- Each line has a
taxableValue(subtotal before tax) and arate(GST %). - CGST + SGST when the place-of-supply matches company state;
IGST when inter-state. See
InvoiceSource.isInterStateinsrc/lib/gstReports.ts:79. - Totals are computed on the fly for display and stored on the row on issue.
B2CL threshold
For unregistered buyers, invoices ≥ ₹2,50,000 move from b2cs
(aggregated) to b2cl (per-invoice) in GSTR-1. Threshold constant
at src/lib/gstReports.ts:38.
4. Endpoints (finance lens)
| Method | Path | Permission | Notes |
|---|---|---|---|
GET |
/finance/invoices |
finance.view |
List customer invoices with filter params. |
POST |
/finance/invoices |
finance.create |
Create a customer invoice. |
POST |
/finance/invoices/schedule |
finance.create |
Create a scheduled / installment invoice (src/lib/api.ts:26238). |
GET / POST / PATCH / DELETE |
/finance/invoices/:id* |
mixed — see handler. | Update, approve, void paths. |
Group invoices live under /group-invoices/* — not strictly a
/finance/* path, but their posting goes through the same journal
machinery.
5. Permissions
| Action | Permission |
|---|---|
| Create customer invoice | finance.create |
| Edit customer invoice (not its amount, number or issue date — FIN-012) | finance.edit |
| Create a group invoice draft (Create draft invoice, Internal invoice, Supplementary) | group_invoices.create |
| Edit a group invoice draft | group_invoices.edit |
| Issue a group invoice (numbers and locks it; posts nothing) | group_invoices.issue |
| Post a group invoice to finance (a pending voucher) | group_invoices.issue |
| Approve or reject that voucher | finance.journals.approve (reject: finance.journals.reject or .approve), in approve_journal_entries; never the person who posted it (FIN-032) |
| Cancel a group invoice (Cancel, or Cancel (credit note) when posted) | group_invoices.cancel |
| View group invoices | group_invoices.view |
approvals.approve plays no part in invoice approval or cancellation.
Role highlights (see docs/PERMISSIONS.md):
FINANCE_MANAGER— group invoice create / edit / issue / post / cancel, and approves the voucher (finance.journals.approve).ACCOUNTANT— create / edit only (no issue, post or cancel; no voucher approval).CEO,GM— issue / cancel and voucher approval (the business approvals bundle).CASHIER— record payments against invoices viafinance.payments.record; cannot create or cancel invoices.
6. Approval, cancellation and reversal
Posting. Post to Finance (POST /group-invoices/:id/post) writes the
voucher from the browser, so the database stores it pending
(FIN-032). It is approved in
approve_journal_entries by someone holding finance.journals.approve
who did not post it. The amount limits in
Numbering and limits cover hand-written vouchers
only, so they do not apply to this voucher. Until it is approved the
invoice's revenue is not in the books.
Cancelling. There is no separate void. Cancelling an issued group
invoice is POST /group-invoices/:id/cancel, gated by
group_invoices.cancel, with a reason. When the invoice was posted, the
handler posts a reversing credit note through post_journal_reversal
(journals §3). Reversals are not
auto-approved:
- the credit note follows the invoice's voucher. If that voucher is still pending, the credit note is pending too and cannot be approved before it; rejecting the voucher rejects the credit note (FIN-031).
- If the voucher is approved, the credit note posts approved only when the
person cancelling may approve vouchers (
finance.journals.approve) and did not make the original. Anyone else's credit note posts pending and a finance approver approves it. - The credit note's lines mirror the invoice's voucher (the same accounts,
opposite sides, scaled down when passenger credit notes already
reversed part of it). A reversal whose lines are not a mirror always
posts pending (DECIDED 2026-09-30,
20261004160000).
An invoice that was never posted is cancelled with nothing posted.
Customer invoices (Invoice) have no void or delete route.
/finance/invoices offers GET, POST, PATCH and
POST /finance/invoices/schedule only, and PATCH refuses a change to the
amount, number or issue date (409). A correction after issue is a credit or
debit note (FIN-012). The
booking's revenue is posted by the booking's finance approval, not by the
invoice.
Money already received. Cancelling does not refund anything. A payment against the invoice stays on it; a refund is requested and approved separately (Payments).