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 orgPaymentType check ([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_category column was added by migration 0028 on 13th February 2026, but with null=True, db_default=None — meaning all pre-migration rows and any rows created before each write path was deployed still have NULL in 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 as OrganizationTransaction rows
  • 10th March 2026 was chosen as the safe cutoff: by that date, both livehealthapp (add_organization_advance, Organization.advanceCollection) and crelio-app (manual_ledger_entry) were fully deployed and setting transaction_category on 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_CONVERSION to detect the event and clamp startDate automatically, 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 Payments record is created with paymentType = "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 - billAdvance is calculated in mapAccountStatementResponseToRows()
  • 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 AccountStatementRow in 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 — only entryType badge styling needs updating

groupDateKey as a stable grouping key

  • Row grouping by YYYY-MM-DD (in lab timezone) is handled entirely by the hidden groupDateKey field
  • Changing timezone logic or date format requires updating only toGroupKey() in helpers.ts

Tab lazy rendering

  • AccountStatementTab receives null when 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

On this page