Finance API
The finance surface is the largest section of src/lib/api.ts — vouchers, journals,
payments, invoices, period management, tax returns, and reports. Every write handler begins
with await requirePermission(...), every journal-posting handler calls
assertAccountingPeriodAllowsPosting(date) before writing, and the approve/reject/reverse
paths enforce maker-checker (you cannot approve your own entry without
finance.journals.approve_own).
See Overview for the shared idempotency pattern (voucher idempotency key + terminal-state short-circuit).
Journals
Handler: handleFinanceJournals at src/lib/api.ts:21591.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/journals |
GET | (read-only) | List journal entries; supports ?limit=, ?referenceType=, ?referenceId= |
/finance/journals |
POST | finance.create |
Create manual journal entry (balanced lines) |
/finance/journals/pending |
GET | (read-only) | All status='pending' entries for the approval queue |
/finance/journals/lines |
GET | (read-only) | Raw JournalLine rows for Dashboard P&L |
/finance/journals/:id |
GET | (read-only) | One voucher + its lines + human-readable reference label |
/finance/journals/:id/approve |
POST | approvals.approve |
Pending → approved |
/finance/journals/:id/reject |
POST | approvals.approve |
Pending → rejected |
/finance/journals/:id/reverse |
POST | finance.edit |
Create inverse posting against an approved entry |
/finance/journals/approve-bulk |
POST | approvals.approve |
Approve multiple pending journals in one call |
POST /finance/journals — create
Input — { referenceType?, referenceId?, memo?, entryDate?, createdBy?, lines: [{ accountId, debit, credit }] }.
Lines must sum to zero (total debit === total credit).
Output — the inserted JournalEntry with its voucherNo minted from
FinanceConfig.journalVoucherPrefix.
Notes:
- New entries default to
status='pending'(the approval-gated default). - Posting into a locked/closed
AccountingPeriodthrows400. - The voucher number is minted server-side — do not pass one.
POST /finance/journals/:id/approve
Cite: src/lib/api.ts:22022.
- Idempotent: already-approved entry returns
{ ok: true, alreadyApplied: true }. - Conflict: a rejected entry returns
409 Conflict. - Maker-checker: the creator cannot approve their own entry unless they have
finance.journals.approve_own. - Concurrency: the UPDATE is conditional on
status='pending'. A lost race re-fetches and either returns the idempotent success or throws409— no duplicate audit row.
POST /finance/journals/:id/reject
Cite: src/lib/api.ts:22071. Same maker-checker + concurrency
pattern as /approve. Cannot reject an already-approved entry (reverse it instead).
POST /finance/journals/:id/reverse
Cite: src/lib/api.ts:21878. Creates a new JournalEntry with
isReversal=true, reversalOfEntryId=<original>, and swapped debit/credit lines.
Only approved journals can be reversed
Pending entries haven't posted to live balances — reversing them would drive balances
negative. The handler throws 400 if the original is pending or rejected.
Rules enforced:
- Cannot reverse a reversal (would double-swap).
- Cannot reverse a journal that already has a reversal (
409if a duplicate reversal now exists after a concurrent race; the losing row is rolled back). - Maker-checker: creator cannot reverse own entry unless they have
finance.journals.reverse_own. - Recomputes booking balance if the original was booking-scoped (
booking,booking_gst,booking_commission, etc.).
POST /finance/journals/approve-bulk
Cite: src/lib/api.ts:21670. Body: { ids: string[] }.
- Skips own-authored entries (maker-checker) — returns them in
skippedwithreason: 'maker_checker_self_approval'. - Skips rows that another approver already flipped — returns in
skippedwithreason: 'concurrent_transition'. - Returns
{ ok, approved, approvedIds, skippedIds, skipped }so the UI can route skipped entries to a different approver.
Payments
Handler: handleFinancePayments at src/lib/api.ts:11955.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/payments |
GET | (read-only) | List all payments; joins Booking for bookingNo |
/finance/payments |
POST | finance.create |
Record a payment (pending or verified) against booking/invoice/group-invoice |
/finance/payments/:id |
PATCH | finance.edit |
Update payment status (verify / reject / annotate) |
/finance/payments/:id/refund |
POST | finance.edit |
Refund a verified payment |
POST /finance/payments
Cite: src/lib/api.ts:12115.
Input — { bookingId? | invoiceId? | groupInvoiceId, amount, currency?, method,
reference?, sourceAccountId?, status?: 'pending'|'verified', notes?, receiptUrl? }.
At least one of bookingId, invoiceId, groupInvoiceId must be supplied.
Work:
1. Insert Payment row.
2. If status='verified', mint receiptNo from FinanceConfig.receiptPrefix +
receiptNextNumber, post balanced journal entries, and recompute booking/invoice totals.
3. Send a payment_receipt email to the customer.
Journal-balance failures roll back
If the journal posting fails (audit #5), the handler re-throws so the Payment insert can be rolled back by the caller — the money does not "arrive" on the books without a matching Dr Cash / Cr Receivable entry.
POST /finance/payments/:id/refund
Cite: src/lib/api.ts:12265.
Input — { amount, reason? }. Creates a new Payment row with a negative amount and
reference='Refund of <id>'. Marks the original as status='refunded'.
Idempotent via reference lookup
A prior refund for the same payment returns 409 Conflict. This prevents double-click
refunds from creating two negative-amount rows.
PATCH /finance/payments/:id
Cite: src/lib/api.ts:12362. Typical use: flip status from
pending → verified after receipt reconciliation. Verification triggers journal posting
and receipt number generation.
Finance vouchers
Handler: handleFinanceVouchers at src/lib/api.ts:11124.
Each voucher posts a balanced journal entry and records a ledger/SupplierTransaction row. All use the voucher idempotency key pattern.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/vouchers/customer-on-account-receipts/:customerId |
GET | (read-only) | List customer on-account receipts + their allocations |
/finance/vouchers/customer-on-account-receipts/:id/allocate |
POST | finance.edit |
Apply an on-account receipt to a specific booking/invoice |
/finance/vouchers/customer-on-account-receipt |
POST | finance.edit |
Record a customer on-account receipt (cash/bank DR, customer receivable CR) |
/finance/vouchers/supplier-refund-receipt |
POST | finance.create |
Record a refund received from a supplier |
/finance/vouchers/agent-on-account-receipt |
POST | finance.create |
Agent on-account receipt |
/finance/vouchers/agent-receipt-allocate |
POST | finance.payments.record |
Allocate an agent receipt to specific bookings |
/finance/vouchers/debit-note |
POST | finance.create |
DR party-ledger / CR offset (customer, agent, or supplier) |
/finance/vouchers/credit-note |
POST | finance.create |
DR offset / CR party-ledger |
POST /finance/vouchers/customer-on-account-receipt
Cite: src/lib/api.ts:11307.
Input — { customerId, amount, method: 'CASH'|'BANK_TRANSFER'|...,
sourceAccountId?, reference?, transactionDate?, currency? }.
Journal — DR Cash/Bank (or sourceAccountId), CR Customer Receivable (1200 sub-ledger).
Idempotency — keyed on (customerId, amount, method, sourceAccountId, reference,
transactionDate). A retry returns the original receipt with deduplicated: true.
POST /finance/vouchers/debit-note + /credit-note
Cite: src/lib/api.ts:11561.
Input — { partyType: 'customer'|'agent'|'supplier', partyId, amount, offsetAccountId,
description?, transactionDate?, reference? }.
- Debit note — DR party-ledger / CR offset (increases what they owe us, or reduces what we owe them).
- Credit note — DR offset / CR party-ledger (reduces what they owe us, or increases what we owe them).
Entries post as status='pending' and wait for finance approval before affecting live
balances.
GL Accounts (chart of accounts)
Handler: handleFinanceAccounts at src/lib/api.ts:20250.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/accounts |
GET | (read-only) | Chart of accounts tree |
/finance/accounts |
POST | finance.create |
Create GL account (respects parent / group rules) |
/finance/accounts/:id |
PATCH | finance.edit |
Update GL account |
/finance/accounts/:id/opening-balance |
PUT | finance.edit |
Set opening balance (posts to Opening Balance Equity) |
/finance/accounts/opening-balances/import |
POST | finance.edit |
Bulk import opening balances |
/finance/accounts/:id/ledger |
GET | (read-only) | Account ledger with running balance |
/finance/accounts/group/:id/ledger |
GET | (read-only) | Consolidated ledger across a group's child accounts |
Legacy /accounts surface
Handler: handleAccounts at src/lib/api.ts:16589.
Historically a parallel "financial account" model (bank / cash / digital wallet abstracted
from the GL chart). Create/edit endpoints now throw deprecation errors — all GL account
operations go through /finance/accounts.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/accounts |
GET | (read-only) | List active GL accounts marked as payment/cash accounts |
/accounts |
POST | deprecated — throws 400 |
Use /finance/accounts |
/accounts/:id |
PATCH | deprecated — throws 400 |
Use /finance/accounts |
/accounts/:id |
DELETE | finance.edit |
Soft-delete (isActive=false) |
/accounts/:id/ledger |
GET | (read-only) | Accounting-style ledger (Dr/Cr/balance) |
/accounts/:id/transactions |
GET | (read-only) | Raw journal-line transactions |
/accounts/:id/transactions |
POST | finance.create |
Manual contra posting |
/accounts/transfers |
POST | finance.create |
Contra transfer — DR toAccount / CR fromAccount |
/finance/settlements/any-to-any |
POST | finance.create |
Cross-ledger settlement (customer credit ↔ supplier payable, write-offs) |
Cross-ledger settlements inherit approval-pending
POST /finance/settlements/any-to-any posts status='pending' and waits in the
Approvals queue. See src/lib/api.ts:16854.
Periods (period lock)
Handler: handleFinancePeriods at src/lib/api.ts:20957.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/periods |
GET | (read-only) | List accounting periods |
/finance/periods |
POST | finance.edit |
Create a period |
/finance/periods/:id |
PATCH | finance.edit |
Edit a period |
/finance/periods/:id/lock |
POST | finance.edit |
Lock — reject new postings to the period |
/finance/periods/:id/unlock |
POST | finance.edit |
Unlock (if not yet closed) |
/finance/periods/:id/close |
POST | finance.edit |
Close — permanent, rolls retained earnings |
Any handler that posts a journal calls assertAccountingPeriodAllowsPosting(date) first
(src/lib/api.ts:164), which throws 400 with { period } context
on violation.
Ledger utilities
Handler: handleFinanceLedger at src/lib/api.ts:21163.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/ledger |
GET | (read-only) | Raw LedgerEntry rows with pagination |
/finance/ledger |
POST | admin.edit |
Rebuild ledger derived rows (e.g. mode: 'rebuild_quota_blocks'); maintenance endpoint |
Booking finance lifecycle
Handler: handleFinanceBookings at src/lib/api.ts:13767.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/bookings/:id/finance-approve |
POST | requireAdminUser() (role-gated) |
Approve / reject a booking after ops approval |
/finance/bookings/:id/mark-paid |
POST | requireAdminUser() (role-gated) |
Force-close a booking balance by recording a CASH payment for the remainder |
Idempotency — if the booking is already in the requested finance state,
finance-approve returns { ok: true, alreadyFinalized: true } without re-posting
journals or audit rows.
Side-effects on approval (non-rejection):
- Auto-generate
TicketRecordrows for every passenger (generateTicketRecordsForBooking). - Auto-create the persistent
Invoicerow (ensureBookingInvoice) — idempotent via bookingId +referenceType='booking'. - Push-notify ops team via
sendPushToUsers.
Tax returns
TDS (Section 26Q)
Handler: handleFinanceTds at src/lib/api.ts:24289.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/tds/summary |
GET | finance.tds.view |
TDS deductions summary |
/finance/tds/deductions |
GET | finance.tds.view |
Deduction-level detail |
/finance/tds/26q |
GET | finance.tds.export |
Generate Section 26Q CSV for filing |
Deductions are triggered inline in supplier-payment flows when the supplier has a TDS rate
set — see handleSuppliers at src/lib/api.ts:17650 which calls
requirePermission('finance.tds.deduct').
GST
| Route | Method | Permission | Handler | Purpose |
|---|---|---|---|---|
/finance/gst-summary |
GET | finance.reports.gst_summary.view |
handleFinanceGstSummary |
Output/input GST totals for a period |
/finance/gstr-1 |
GET | finance.reports.gst_summary.view |
handleFinanceGstr1 |
GSTR-1 return JSON (outward supplies) |
/finance/gstr-3b |
GET | finance.reports.gst_summary.view |
handleFinanceGstr3b |
GSTR-3B return JSON |
Financial reports
| Route | Method | Permission | Handler | Purpose |
|---|---|---|---|---|
/finance/day-book |
GET | (read-only) | handleFinanceDayBook |
Day book by date |
/finance/trial-balance |
GET | (read-only) | handleFinanceTrialBalance |
Balanced trial balance |
/finance/balance-sheet |
GET | (read-only) | handleFinanceBalanceSheet |
Assets / liabilities / equity |
/finance/profit-loss |
GET | (read-only) | handleFinanceProfitLoss |
P&L with expense categorisation |
/finance/cash-flow |
GET | (read-only) | handleFinanceCashFlow |
Cash flow statement |
/finance/account-statement |
GET | (read-only) | handleFinanceAccountStatement |
Per-account running statement |
/finance/pl-summary |
GET | (read-only) | handleFinancePlSummary |
Compact P&L summary for Dashboard |
/finance/group-profitability |
GET | (read-only) | handleFinanceGroupProfitability |
Per-group P&L |
/finance/aging |
GET | (read-only) | handleFinanceAging |
AR aging (customer receivables by bucket) |
/finance/aging-payables |
GET | (read-only) | handleFinanceAgingPayables |
AP aging (supplier payables) |
/finance/aging-report |
GET | (read-only) | handleFinanceAgingReport |
Combined AR+AP aging |
/finance/receivables-payables |
GET | (read-only) | handleReceivablesPayables |
Net receivable vs payable |
/finance/settlement-report |
GET | (read-only) | handleFinanceSettlementReport |
Settlement activity summary |
/finance/settlement-ledgerwise |
GET | finance.reports.settlements.view |
handleFinanceSettlementLedgerwise |
Settlements grouped by ledger |
/finance/cancellation-report |
GET | finance.reports.settlements.view |
handleFinanceCancellationReport |
Airline + B2B cancellations |
/finance/pending-settlements |
GET | (read-only) | handlePendingSettlements |
Unsettled supplier/partner rows |
/finance/supplier-transactions |
GET | finance.view |
handleFinanceSupplierTransactionsAll |
All supplier transactions |
/finance/years |
GET, POST | finance.edit |
handleFinanceYears |
Fiscal years management |
/finance/airline-cancellations |
GET, POST | finance.create |
handleAirlineCancellations |
Airline cancellation postings |
/finance/b2b-cancellations |
GET, POST | finance.create |
handleB2BCancellations |
B2B partner cancellation postings |
/finance/tickets |
GET | (read-only) | handleFinanceTickets |
Ticketing-finance cross-report |
Invoices (persistent document)
Handler: handleFinanceInvoices at src/lib/api.ts:26111.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/invoices |
GET | (read-only) | List invoices |
/finance/invoices |
POST | finance.create |
Create invoice manually |
/finance/invoices/:id |
PATCH | finance.edit |
Edit invoice |
/finance/invoices/schedule |
POST | finance.create |
Schedule recurring invoice |
Auto-issuance on finance-approve
Most invoices are created automatically by handleFinanceBookings when finance
approves a booking. Manual POST /finance/invoices is reserved for back-office
corrections.
Bank imports & reconciliation
Handler: handleBankImports at src/lib/api.ts:27147.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/bank-imports |
GET | finance.view |
List imports |
/finance/bank-imports |
POST | finance.edit |
Upload a bank statement CSV |
/finance/bank-imports/:id |
GET | finance.view |
One import + its lines |
/finance/bank-imports/:id/lines |
GET | finance.view |
All lines of one import |
/finance/bank-imports/:id/auto-match |
POST | finance.edit |
Auto-match lines to existing payments |
/finance/bank-imports/:id/lines/:lineId/match |
POST | finance.edit |
Manually match one line to a payment |
Stock adjustment
/finance/stock-adjustment — handler handleFinanceStockAdjustment
(src/lib/api.ts:22227). Posts inventory write-downs/up. Permission: finance.create.
Finance PIN (sensitive-action gate)
Handler: handleFinancePin at src/lib/api.ts:22416.
Some destructive finance actions are additionally gated behind a per-user PIN, set up once and typed at the moment of the action.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/pin/status |
GET | authenticated | Is a PIN set? |
/finance/pin/set |
POST | authenticated (self) | Set the PIN |
/finance/pin/verify |
POST | authenticated (self) | Verify a typed PIN |
/finance/pin/request-reset |
POST | authenticated (self) | Email a reset link |
/finance/pin/reset |
POST | token-gated | Reset PIN using emailed token |
Finance config
The single-row FinanceConfig (at key id='default') holds prefixes, default currency,
GST rate, and the GL account codes used by every posting helper. Edited via
PUT /admin/finance-config — see Admin. Requires finance.edit.