Skip to content

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 marked reversed).
  • 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 through correct_supplier_transaction, which needs finance.supplier_transactions.correct and 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