Skip to content

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_guard on refund approval;
  • JournalEntry_limit_guard on 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_original is false).

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