Invoices (finance lens)
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 / reissue / cancel actions.
Posting path: when a group invoice is issued, the handler inserts
the journal via createJournalWithLines with
referenceType = 'group_invoice'. Cancellation posts a reversal with
referenceType = 'group_invoice_cancellation'.
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 | finance.edit |
| Approve / void the posting journal | approvals.approve (+ maker-checker) |
| Create group invoice | group_invoices.create |
| Issue group invoice (posts journal) | group_invoices.issue |
| Cancel group invoice (posts reversal) | group_invoices.cancel |
| View group invoices | group_invoices.view |
Role highlights (see docs/PERMISSIONS.md):
FINANCE_MANAGER— full invoice create / edit / issue / cancel.ACCOUNTANT— create / edit only (no issue / cancel).CASHIER— record payments against invoices viafinance.payments.record; cannot create or void invoices.
6. Void vs cancellation
Voiding an invoice = cancelling the posting journal = posting a reversal. The reversal goes through the journal state machine (approved automatically, since reversals are auto-approved; see journals.md §8).
Voiding after payments have been recorded against the invoice is not automatic — the refund has to be posted as a separate payment voucher first, then the invoice voided. UI should guide this flow.