Skip to content

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 AccountingPeriod throws 400.
  • 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 throws 409no 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 (409 if 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 skipped with reason: 'maker_checker_self_approval'.
  • Skips rows that another approver already flipped — returns in skipped with reason: '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 pendingverified 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 TicketRecord rows for every passenger (generateTicketRecordsForBooking).
  • Auto-create the persistent Invoice row (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.