Dentolize · Accounting Module Walkthrough
On this pageBusiness viewTechnical view

Cash, Banking & Payables

Business view

This is the module's day-to-day cash-handling layer:

  • Bank Reconciliation — compares what the books say a treasury's cash balance is against what the bank statement says, for a given date, so discrepancies get caught.
  • Reconciliation — a different, automatic, company-wide check: does the general ledger's version of cash/receivables/inventory/payables match what the operational tables (treasuries, invoices, patient balances, stock, expenses) say? This one runs itself; nobody has to start it.
  • Payment Vouchers — the general "pay someone money out of a treasury" document, for paying a supplier, employee, or anyone else that isn't already covered by a more specific flow.
  • Debit Notes — a supplier-side adjustment (a return or a price correction) that reduces what the clinic owes that supplier.
  • Expense Claims — the employee-reimbursement workflow: an employee submits a claim, a manager approves or rejects it, and once approved it gets paid from a treasury.
  • Bulk Insurance Settlement — insurers typically wire one lump payment covering many claims at once, with no per-claim breakdown attached. This screen lets a clinic record that single deposit and allocate it across the open claims it actually covers, so the books end up right without one journal entry per claim.

Technical view

Every mutation and query in this cluster lives in two large files rather than the per-action file pattern used elsewhere in the codebase: packages/server/src/resolvers/mutations/ledgerMutations.js (mutations) and packages/server/src/resolvers/queries/ledgerQueries.js (queries). Posting logic is factored into packages/server/src/accounting/posting/*.js, invoked through a decoupled retry outbox (packages/server/src/accounting/resilientPosting.js — see Journal Entries & Ledgers).

Bank Reconciliation

BankReconciliation/BankStatementLine (schema.prisma:8726-8742/8744-8755). createBankReconciliation (ledgerMutations.js:1522) snapshots the treasury's GL cash-account balance as bookBalance at creation time. addBankStatementLine (:1546) inserts one statement line at a time — there is no CSV/OFX import; lines are typed in by hand. setBankStatementLineMatched (:1560) flips a matched boolean per line. completeBankReconciliation (:1568) compares the frozen statementBalance against the frozen bookBalance and marks the reconciliation RECONCILED or DISCREPANCY based on that difference alone.

Important limitation: the per-line matched toggle never feeds into the completion check — a reconciliation can be marked complete with zero lines matched, or blocked no matter how many lines are matched, because matching and the balance comparison are two unconnected mechanisms. There's no auto-match algorithm and no statement import; treat "matching" here as a manual checklist for the reviewer's own bookkeeping, not something the system enforces. Screen: bankReconciliation/BankReconciliationScreen.js. Permission: POST_JOURNAL and (hasAccessToTreasuryId or DO_ALL).

Reconciliation (company-wide, automatic)

A completely different feature from the above, despite the similar name. reconciliation/ReconciliationScreen.js is read-only and has no user-initiated action at all — it's a live, always-recomputed comparison (glReconciliation query, packages/server/src/accounting/reports/reconciliation.js) between the GL-posted balance and the operational source of truth for six subledgers: cash (Σ Treasury.balance), patient AR, patient wallets, inventory, accounts payable, and doctor payable. It also runs an independent GL-only accounting-equation check (Assets = Liabilities + Equity) so an "all green" result isn't just proving the wiring matches itself. Out-of-tolerance lines log a RECONCILIATION_DRIFT warning and record a metric. Permission: VIEW_REPORTS or VIEW_GL (permissions.js:430).

Payment Vouchers

PaymentVoucher (schema.prisma:8654-8677) — payeeType (SUPPLIER | EMPLOYEE | OTHER), method (CASH | BANK | CHEQUE), a target accountId to debit. addPaymentVoucher (ledgerMutations.js:1240) posts Dr the chosen account / Cr the paying treasury's cash account (or the company default cash account if no treasury is given), validating server-side that payeeId belongs to the company when the payee is a supplier or employee. Both the web (PaymentVouchers.js:130-136) and mobile (NewPaymentVoucherScreen.js:166-190) "new voucher" forms have a required supplier/employee picker that populates payeeId, so the GL line's supplier tag fires correctly from the shipped UI on both platforms. Permission: POST_JOURNAL and (hasAccessToTreasuryId or DO_ALL).

Debit Notes

DebitNote (schema.prisma:8704-8723) — addDebitNote (ledgerMutations.js:1475) posts Dr Accounts Payable / Cr the chosen offset account (reducing both the AP liability and the original expense/inventory booking). The model has an appliedAmount field intended for a future "apply this note against a specific supplier bill" workflow, but nothing writes a non-zero value to it today — it's unused scaffolding. Permission: POST_JOURNAL.

Expense Claims

ExpenseClaim (schema.prisma:8680-8701) — lifecycle confirmed as submit → approve/reject → pay:

  1. addExpenseClaim (ledgerMutations.js:1342) — SUBMITTED, explicitly off-ledger at this point (no GL posting yet).
  2. approveExpenseClaim (:1380) — SUBMITTED → APPROVED.
  3. rejectExpenseClaim (:1390) — from SUBMITTED or APPROVEDREJECTED. The optional reason argument is now persisted to the audit log via recordHistory on rejection.
  4. payExpenseClaim (:1412) — APPROVED → PAID, decrements the chosen treasury, and only this step posts to the ledger: Dr the claim's expense account / Cr the treasury's cash account.

Permission: submit → POST_JOURNAL; approve/reject → APPROVE_JOURNAL; pay → POST_JOURNAL and (hasAccessToTreasuryId or DO_ALL). Unlike manual journal approval, there is no self-approval guard on expense claims — a user with APPROVE_JOURNAL can approve their own submitted claim. The model also has a receiptUrl field with no upload UI anywhere — an unused-scaffolding field, like DebitNote.appliedAmount.

Bulk Insurance Settlement

BulkInsuranceSettlement/BulkSettlementAllocation (schema.prisma:3899-3915/3917-3928). recordBulkInsuranceSettlement (ledgerMutations.js:451) validates that the allocations sum to the deposit amount (within 0.01), then posts Dr the treasury's bank account / Cr the Insurance Settlement Clearing role account (1240), tagged by insurer. The screen exists on both platforms: web (packages/clinic-web/src/components/dashboard/accounting/Healthcare/Healthcare.js, "Bulk Settlement" tab) and mobile (bulkSettlement/BulkSettlementScreen.js) — each has an insurer picker, an open-claims table with per-claim clearing balances, manual or "auto-allocate oldest-first" allocation, and won't let you submit until the allocations balance. Gated POST_JOURNAL on both platforms.

recordBulkInsuranceSettlement now has a permission rule (permissions.js:2999-3006, previously missing, which made the mutation reject for every role including the owner) and is registered in the posting engine's EMITTED_SOURCE_TYPES completeness list with a retry handler (postingHandlers.js:60,410), so a transient posting failure on this mutation can now be automatically retried by the outbox sweep like any other event type. Both of these were real, confirmed gaps earlier in this PR's development and are fixed as of the current code.

Treasury (pre-existing feature, extended by this PR)

Treasuries themselves aren't new, but this PR wires treasury-to-treasury transfers into the new ledger: addNewTreasuryTransaction now posts Dr destination treasury's cash account / Cr source treasury's cash account, plus an inter-branch due-to/due-from pair if the two treasuries belong to different branches. This one is correctly registered in the posting engine and covered by the retry outbox — unlike Bulk Insurance Settlement above.