Skip to content

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 by gst_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 a rate (GST %).
  • CGST + SGST when the place-of-supply matches company state; IGST when inter-state. See InvoiceSource.isInterState in src/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 via finance.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).


  • Journals — the posting path (reversal, maker-checker).
  • GST — GSTIN + HSN + GSTR-1/3B.
  • Vouchers — the receipt side (what allocates payments to invoices).
  • src/components/invoices/GroupInvoiceDialog.tsx — group invoice UI.