Numbering and approval limits
Two pieces of finance configuration that are enforced in the database: the document counters that hand out invoice, receipt and booking numbers, and the money limits that decide who may approve what.
1. Document numbering
Every series is issued by one function, fin_next_series_number(series), which takes a row
lock on the single FinanceConfig row and bumps the counter in the same statement. Two
sessions asking at the same moment get consecutive numbers; neither gets a duplicate.
| Series | Used for | Prefix / counter in FinanceConfig |
Shape |
|---|---|---|---|
INV |
customer invoice | invoicePrefix, invoiceNextNumber |
INV-00001 |
GINV |
group invoice | groupInvoicePrefix, groupInvoiceNextNumber |
GINV-00001 |
AINV |
partner-branded invoice | agentInvoicePrefix, agentInvoiceNextNumber |
AINV-00001 |
RCP |
receipt, minted on verification | receiptPrefix, receiptNextNumber |
RCP-00001 |
BKG |
booking number | bookingPrefix, bookingNextNumber |
BK-00001 (prefix blank: BK) |
Any other series name is refused.
The booking prefix (FIN-048).
The number is read from the trailing digits, so a prefix may carry a hyphen (AH-BK gives
AH-BK-00012) or end in digits (AH-26). Finance → Settings → Booking Prefix takes 1 to 12
letters or digits with a hyphen only between them, and stores it in capitals; anything else is
refused with that sentence, by the screen and by the database. Left blank, bookings are
numbered BK-… — staff, partner and transfer bookings alike.
fin_next_series_number is not callable by a browser session at all — it is granted only to
the service role. Callers go through a wrapper that checks the permission for the document
it is about to number (for example next_group_invoice_number needs one of
group_invoices.create / .edit / .issue). None of the wrappers are callable anonymously.
Refund vouchers are the exception. RFV-00001 comes from its own sequence
(refund_voucher_seq) inside approve_refund, not from fin_next_series_number. It is not
reachable from set_document_counter.
Changing a counter
set_document_counter(series, next, reason) needs finance.config.edit, needs a written
reason, and only moves forward — "A document counter can only move forward (current
N)". The change is written to the audit trail. Counters cannot be edited by writing to
FinanceConfig from the browser; fin_finance_config_guard() refuses that.
This matters because a counter that goes backwards produces two documents with the same number, which is a statutory problem, not a cosmetic one (FIN-010).
2. Approval limits
ACC-030 says approvals route by value, and that the
amounts are configuration with a named owner, not code. They live in the ApprovalLimit
table — one row per decision, each with its unit, two tiers, the permission each tier needs,
and the person who owns the number. Reading the table needs staff access; changing it needs
finance.config.edit, and every change is audited.
Decision (area) |
Unit | Tier 1 | Tier 2 | Enforced today |
|---|---|---|---|---|
customer_refund |
INR | 50,000 — finance.refunds.approve |
200,000 — finance.approvals.high_value |
yes |
manual_journal |
INR | 25,000 — finance.journals.approve |
above — finance.approvals.high_value |
yes |
supplier_payment |
INR | 200,000 | 1,000,000 | no |
discount |
percent | 5% | 10% | no |
fare_commitment |
INR | 500,000 | — | no |
seat_release |
count | 10 seats | — | no |
write_off |
INR | any amount — finance.approvals.high_value |
— | no |
fin_check_approval_limit(area, amount, approver) refuses an approval above the tier the
approver holds. It is wired by two triggers:
PaymentRefund_limit_guardon refund approval;JournalEntry_limit_guardon voucher approval — for every voucher a person writes (owner, 01/10/2026;fin_voucher_is_hand_written):- journals —
journal,manual,manual_journal(the phone's journal, payment, receipt and contra),adjustment,journal_entry; - the website's contra —
contra_transfer; - on-account receipts —
customer_on_account,agent_on_account; - a reversal whose lines are not a mirror of the voucher it reverses
(
fin_lines_mirror_originalis false).
- journals —
Not limited: a mirror reversal (it only undoes what was approved), a cancellation credit, and system postings — payments recorded against a booking, refunds, booking revenue, B2B decision vouchers, operational costs — which have their own rules.
approve_journal_entries catches the refusal and reports the voucher as skipped with
reason: 'above_approval_limit' rather than failing the whole batch. The message says it in
plain words, for example "₹30,000 is above your approval limit for journals (₹25,000). It
needs CEO or GM." — the website and the app show it as it is.
A voucher of these kinds inserted already approved is checked too, at the end of its
transaction (JournalEntry_limit_on_insert). No path does that today; the check stops a
future one from going round the limit.
The rows that are not enforced are configuration, not behaviour
A row with enforced = false is seeded so the number has a home and an owner, and
fin_check_approval_limit returns immediately for it. Supplier payments, discounts,
fare commitment and seat release are enforced — or not — by the workstreams that own
those actions; seat release, for instance, is checked against
FinanceConfig.seatReleaseApprovalSeatLimit in the release functions, not here
(AIR §18).
finance.approvals.high_value is held only by CEO, GM and SUPER_ADMIN. It is on the
super-admin-only list, so no functional role can pick it up through a role bundle.
3. Where to look
| Concern | Path |
|---|---|
| Numbering | supabase/migrations/20260919100500_finance_document_numbering.sql |
| Approval limits | supabase/migrations/20260919100700_finance_approval_limits.sql, 20261006120000_finance_guards_in_the_database.sql |
| Config guard | supabase/migrations/20260919100400_finance_supplier_tds_controls.sql |
| Tests | supabase/tests/finance_controls.sql, supabase/tests/finance_guards_in_the_database.sql |