Finance module — overview
The five controls the module rests on, and where each screen lives.
Every one of these controls is a trigger or a SECURITY DEFINER function in Postgres. The
browser cannot reach around them, which is the point: src/lib/api.ts runs on the client
(ACC-001).
1. Double entry
A posting is a JournalEntry with at least two JournalLine rows, and the debits equal the
credits to the paisa (FIN-031).
Every line is stored rounded to the paisa (JournalLine_paisa). The posting functions round
each line first and then require the two sides to be equal — there is no ₹0.01 of drift, so
Dr ₹100 against Cr ₹33.333 three times (stored as ₹99.99) is refused. The same is checked
when a voucher is approved: fewer than two lines, a non-positive debit total, or any gap
at all — "Voucher X does not balance." A voucher written straight in as approved (a
receipt, a refund, a reversal) is checked at the end of its transaction
(JournalEntry_balanced) and refused if it does not balance. The website's journal form
and createJournalWithLines apply the same rule before they send anything.
Voucher numbers are PREFIX-YYYYMMDD-XXXXXX. Prefixes come from FinanceConfig: RV
(receipt), PV (payment), SV (sales), CN / DN (credit and debit note), CT (contra),
SN (settlement), REV (reversal), OB (opening balance), JV (manual journal).
Document numbers — invoices, receipts, booking numbers — are separate and come from one atomic counter; see Numbering and approval limits.
2. Nothing posts approved from a browser
A voucher written by a signed-in browser session is stored pending, always. Setting its
status to approved by editing the row is refused: "Vouchers are approved through
approve_journal_entries, not by editing the row (FIN-032)."
Four functions legitimately post an approved voucher, because each one is the same transaction as the business event it records and each carries its own maker-checker:
| Function | Posts |
|---|---|
verify_payment |
the receipt voucher, when a second person verifies the money |
approve_refund |
the refund voucher and the refund payment |
post_journal_reversal |
a reversal, approved only for a caller who may approve and did not make the original |
post_decision_journal |
the B2B cancellation vouchers, only for the person who just decided that cancellation, once per reference |
3. Posted vouchers never change
FIN-031 is a legal requirement, not a preference (Companies (Accounts) Rules r.3(1)). Four triggers enforce it:
JournalEntry_guard— an approved or rejected voucher cannot be edited or deleted: "Reverse it and post a new one."JournalLine_guard— lines cannot be added to, changed in, or removed from a posted voucher. A companion check refuses reversals that would exceed the voucher they reverse, under a lock, so partial reversals work and cancellation reversals cannot stack.LedgerEntry_guard— a posted ledger row cannot be edited or deleted (only markedreversed).SupplierTransaction_guard— while its voucher is posted, a supplier transaction is frozen down to the amount and the date. Once the voucher is reversed it is corrected throughcorrect_supplier_transaction, which needsfinance.supplier_transactions.correctand a written reason.
Break-glass exists but is not a role: it needs the service role and a session setting.
Ledger reset and GL force-delete are gone
"Clear All Ledger Data" hard-deleted every journal, ledger entry and quota-block
supplier transaction. DELETE /finance/ledger now answers 410. DELETE
/finance/accounts/:id?force=true also answers 410 — deactivate the account
instead. A plain account delete still refuses any account with children or a single
journal line. POST /finance/ledger (rebuild) survives, gated on
finance.ledger.rebuild, which only the super admin holds; re-posting booking revenue
was removed from it because it duplicated revenue.
4. Maker ≠ checker
Nobody approves their own work — receipts, refunds, journals, reversals, B2B cancellation decisions. See Maker-checker.
5. The period lock is on entryDate
AccountingPeriod rows are open, locked or closed. fin_period_block_reason(date)
resolves the period containing the entry date — the day the transaction belongs to, not
the day somebody keyed it in — and the voucher guard applies it on insert and on update.
record_payment applies the same check to receivedDate.
Locking, unlocking and closing each need their own permission, and each stamps who did it
and when. A closed period cannot be changed at all. A financial year closed through
close_financial_year cannot be reopened.
This used to be createdAt
Before Wave 1B the lock resolved the period from the row's creation timestamp, so a
back-dated entry into a closed period went through and a correctly dated entry keyed in
later did not. Reports had the same bug; they now aggregate by entryDate in IST.
The Finance page
All tabs live under /finance (src/pages/finance/Finance.tsx), gated on finance.view.
| Tab | Purpose |
|---|---|
| Intelligence | Insight cards and the finance co-pilot. Deterministic numbers, language only from the model (INT-002). |
| Accounts | Chart of accounts and ledger drill-down. |
| Vouchers | Receipt, payment and contra vouchers. Only the voucher types the user may post are offered (Vouchers). |
| Transactions | Payments and invoices. Record Payment needs finance.payments.record, so a cashier can use it (Receipts and refunds). |
| Journal | Manual entries. |
| Reports | The report catalogue. |
| Reconcile | Bank statement import and matching. |
| Approvals | The finance review queue. The number on the tab counts everything waiting there: bookings for the finance check, cancellations awaiting a refund, vouchers to approve and refund requests. |
| Settings | Prefixes and GL defaults, accounting periods, financial years, PIN. |
Above the tabs, four tiles read the balance sheet
(FIN-033):
Liquid Funds is every cash and bank account added up (1000 Cash and 1010 Bank),
Total Receivables every receivable (1200 Accounts Receivable and 1210 Agent
Receivables), Total Payables every payable, and Net Position assets less liabilities.
An account group that matches counts once, with its children inside it. A real ₹0 shows as
₹0. Until the balance sheet has loaded, Liquid Funds and Net Position show a dash — never
lifetime receipts in their place — and Total Receivables shows what the bookings say is
outstanding. Matching is by account name (src/pages/finance/financeKpis.ts).
The finance PIN (FinancePinGate) is a second factor on top of a permission, not a
permission of its own. Each PIN box has an eye that shows the digits typed (UX-021). A user without the permission is refused whether or not they know the
PIN.
Roles
SUPER_ADMIN is the only role that holds every permission
(ACC-012, decided 2026-09-17). In finance:
| Role | Holds |
|---|---|
CASHIER |
finance.view, finance.payments.record. Nothing else. |
ACCOUNTANT |
Prepares and verifies other people's receipts, reconciles. No approvals, no year close, no configuration, no numbering. |
FINANCE_MANAGER |
Every finance.* except break-glass, ledger rebuild, year and period close, and supplier-transaction correction. |
CHARTERED_ACCOUNTANT |
Reads all finance, reports and the audit trail; prepares journals, adjusting entries and supplier-transaction corrections that someone else approves. Handles no cash. |
CEO, GM |
Every view and export, the business approvals, finance.approvals.high_value, supplier-transaction correction. Not full rights. |
IT_ADMIN, ADMIN_HR |
No finance write rights at all. |
The full grid is PERMISSIONS.md §5.
Supporting documents on a voucher. The voucher dialog's Attachments card uploads a bill or
receipt (a PDF or a photo, 10 MB) to the company Shared Drive under Finance / Vouchers / the
voucher (drive-upload, kind voucher_attachment, finance.edit) and records its Drive
reference; Open shows it in a viewer on the page (drive-file, finance.view). A receipt
a customer uploads with a portal payment goes to Finance / Receipts / the customer and opens
the same way (ACC-074).
Code map
| Concern | Path |
|---|---|
| Finance page | src/pages/finance/Finance.tsx |
| Chart of accounts UI | src/pages/finance/ChartOfAccounts.tsx |
| Voucher detail | src/components/finance/VoucherDetailDialog.tsx |
| Posting helpers | src/lib/financePosting.ts, src/lib/financeOperations.ts |
| Bank statement parser | src/lib/bankStatementParser.ts |
| GSTIN, GSTR, TDS helpers | src/lib/gstin.ts, src/lib/gstReports.ts, src/lib/tds.ts |
| Wave 1B migration | supabase/migrations/20260918110000_money_integrity.sql |
| F1 migrations | supabase/migrations/20260919100000 … 20260919100900 |
| Tests | supabase/tests/money_integrity.sql, supabase/tests/finance_controls.sql |