Design Decisions
Key architectural and product decisions behind the Account Statement feature.
👤 Sachin Sharma📅 Updated: May 4, 2026🏷️ feature🏷️ finance🏷️ design
Design Decisions
Key Design Decisions & Constraints
Simplified view over Ledger comprehensiveness
- Account Statement intentionally shows less than the Ledger — only three entry types (Bill, Payment, Advance/Manual)
- The goal is cash-flow visibility, not a complete audit trail
- Ledger remains the source of truth for running balance; Account Statement is a complementary view
No new database tables
- Account Statement reads from three existing tables:
Billing,Payments,OrganizationTransaction - No new model — zero schema surface to maintain
- Trade-off: data quality depends entirely on the correctness of the upstream tables
Prepaid and WalkIn only — PostPaid excluded
- PostPaid organizations use the Invoice + Ledger workflow for settlement tracking
- Showing Account Statement alongside Invoices would create confusion about which view to trust
- The
orgPaymentTypecheck ([ORG.PREPAID, ORG.WALKIN].includes(...)) enforces this at the tab level
Data starts from 10th March 2026 — because transaction_category was nullable before that
- The
transaction_categorycolumn was added by migration0028on 13th February 2026, but withnull=True, db_default=None— meaning all pre-migration rows and any rows created before each write path was deployed still haveNULLin this column - Without
transaction_category, the API cannot distinguish an advance entry from a billing-related ledger entry (e.g., bill settlement credit-back) — both appear asOrganizationTransactionrows - 10th March 2026 was chosen as the safe cutoff: by that date, both
livehealthapp(add_organization_advance,Organization.advanceCollection) andcrelio-app(manual_ledger_entry) were fully deployed and settingtransaction_categoryon every new row ACCOUNT_STATEMENT_MIN_DATE = moment("2026-03-10")is hardcoded in the frontend; the UI date guard prevents API calls for earlier ranges and shows the warning: "Account Statement is valid after 10th March 2026."
Architectural Rationale
Direct source queries, no running balance
- Ledger failures cascade because each entry's balance depends on the previous one
- Account Statement avoids this entirely by computing totals independently per request
- Each of the three queries (
get_bills,get_payments,get_advance_collection) is independent — a failure in one does not affect the others
Conversion-aware window clamping
- When an organization converts payment type (e.g., WalkIn → Prepaid), the financial relationship changes fundamentally
- Mixing pre- and post-conversion data in one statement would show incorrect totals
- The API uses
TransactionCategoryEnum.ORGANIZATION_CONVERSIONto detect the event and clampstartDateautomatically, with no manual intervention required
ADVANCE payment type excluded from payments
- When the lab uses an org's existing advance to settle a bill, a
Paymentsrecord is created withpaymentType = "ADVANCE" - Including this would double-count: the advance was already tracked in
OrganizationTransaction - Exclusion via
.exclude(paymentType="ADVANCE")ensures each rupee appears only once
billDue computed on frontend, not backend
billDue = billTotalAmount - billAdvanceis calculated inmapAccountStatementResponseToRows()- The backend returns raw fields; the frontend owns the presentation logic
- Keeps the API contract simple and allows the frontend to evolve the display (e.g., show 0 instead of negative) without backend changes
statementId resolved by payment type on frontend
- For card payments →
cardNo; cheque →chequeNo; online →transactionId - The backend returns all three fields; the frontend picks the appropriate reference
- Avoids a backend decision about which field is "the" reference for a given payment mode
Reusability & Extensibility
mapAccountStatementResponseToRows() is the single transformation boundary
- All three API lists are normalized into
AccountStatementRowin one function - Adding a new entry type (e.g., credit notes) requires only a new mapping branch here
- The AG Grid component (
AccountStatementTab) requires no changes for new entry types — onlyentryTypebadge styling needs updating
groupDateKey as a stable grouping key
- Row grouping by
YYYY-MM-DD(in lab timezone) is handled entirely by the hiddengroupDateKeyfield - Changing timezone logic or date format requires updating only
toGroupKey()inhelpers.ts
Tab lazy rendering
AccountStatementTabreceivesnullwhen the tab is not active, preventing API calls on unrelated tab switches- This pattern can be reused for any tab that should not fetch data until selected