Design Decisions
Key architectural choices, trade-offs, and extensibility notes for the Order Update feature.
Design Decisions
Single Atomic Transaction
All DB writes — bill fields, test amounts, payments, org ledger, doctor revenue, appointments, lab reports — are wrapped in a single @transaction.atomic block.
Why: A bill update touches at minimum 8–10 different DB tables. Partial updates would leave the system in an inconsistent state; e.g., payments committed but org ledger not updated. Rolling back the entire operation on any failure is the only safe choice.
Trade-off: The transaction can be long-running if Elasticsearch is slow. The ES update is retried inside the transaction boundary with a 1-second wait. If ES is consistently slow, it can hold a DB connection open.
Mitigation: Elasticsearch update failures do not abort the transaction — ES is treated as an eventually-consistent projection. The transaction is only rolled back for DB errors.
Elasticsearch Outside the Transaction
ES writes run inside the transaction scope but a failure does not roll back the DB changes.
Why: ES is a secondary index. Its state should always be derivable from the primary DB. Allowing an ES timeout to roll back a bill update would be unacceptable from a product standpoint — the billing system cannot be held hostage by a search index.
Accepted risk: There is a brief window after the DB commit where the search index is stale. ES is re-synced via the update_elasticsearch() call with one retry. Persistent ES failures will be caught by Sentry monitoring.
Fusion Webhook Is Fire-and-Forget
The trigger_bill_integration() call runs after the transaction commits and does not await the response.
Why: Fusion is a third-party integration layer. Making the bill update API response dependent on external systems is not acceptable. Integration delivery is handled by Fusion's internal retry mechanism.
Implication: If Fusion is down, integrations will miss the event until the next sync cycle. This is an accepted trade-off for system stability.
Payment Update Is Opt-In (paymentUpdateFlag)
The backend only runs payment logic when the caller explicitly sets paymentUpdateFlag = 1.
Why: The order update form has a dedicated payment section. When the user hasn't touched payments, we should not re-process them (risk of double-counting or inadvertent changes). The flag is a deliberate contract between frontend and backend.
Status 6 — Payment Conflict Guard
The API returns early without writing to the DB when a specific conflict is detected between payment amounts and org payment type.
Why: Some org payment types (prepaid, postpaid) have ledger reconciliation rules that are incompatible with mid-update payment changes. Returning a distinct status code (6) allows the frontend to surface a specific error to the user rather than a generic failure.
Diff-Based Activity Logging
The audit log stores a structured JSON diff of changed fields, not a snapshot of the full record.
Why: Diffs are immediately readable in the activity log UI without needing to compare two records. They also make the log useful for non-technical users (support, product) when reviewing what changed in a bill.
Implication: Only the tracked scalar fields are diffed (see Backend page). Relational changes (e.g., which payments were added) are inferred from separate Activity Log entries (categories 14 and 15).
Frontend State in Redux (Not Local State)
All bill data for the order update form — billing fields, tests, payments, ICD codes, modifiers — is stored in GENERIC.billingData[userId][labBillId] in Redux.
Why:
- Multiple components (
BasicBillDetails,AdditionalDetails,PaymentDetails,BillUpdateFooter) need access to the same bill data simultaneously. Prop-drilling would be unwieldy. - The submit function in
BillUpdateFooterneeds to read the final state of all fields changed anywhere in the form. - Redux allows any component to make incremental updates to nested fields (via
updateBillingData()) without communicating through ancestors.
Trade-off: The Redux tree for billing data can become large for patients with many bills. The shape billingData[userId][labBillId] keeps it scoped per patient+bill combination to reduce cross-contamination.
Virtualised Bill List in the Sidebar
BillDetailsSidebar uses react-virtualized instead of rendering all bill cards.
Why: High-frequency labs can have patients with hundreds of bills. Rendering all bill cards into the DOM simultaneously would cause visible jank when opening the order update modal.
Implementation: CellMeasurerCache with fixedWidth: true allows variable-height bill cards while still virtualising the list. Only ~10 cards are in the DOM at any time.
Price List Reconciliation Is User-Initiated
When the org or referral doctor is changed, the system detects whether a new price list applies and shows a link. The user must explicitly click the link to apply the new prices to existing tests.
Why: Automatic price re-application on every org/referral change would be surprising and potentially destructive if the user is just exploring options. Requiring a deliberate click gives the user control.
AOE and Consent Are Not Part of the Core Update Transaction
AOE form values and patient consent are handled by separate API calls and their own modals — not bundled into the bill update API request.
Why:
- AOE and Consent have their own storage models (
QuestionValue,PatientConsent) managed by the Lab Forms engine. - The bill update API is already highly complex. Bundling form submissions into it would couple two independent systems.
- AOE and Consent can be filled independently of whether the bill update is ultimately saved.
Implication: If a user fills AOE and then the bill update fails, the AOE values are already persisted. This is intentional — AOE data is clinically meaningful and should survive a billing failure.
Missing Field Resolution as a Separate Concern
Missing fields (e.g., Bill ICD Code marked as missing) are resolved via a separate patchLabMissingDetailsResolution call on component mount, not wired into the bill update submit path.
Why: Missing Fields is a cross-cutting concern that applies to multiple flows (registration, billing, order update, accessioning). Embedding its resolution logic inside the bill update API would create tight coupling. Instead, the frontend applies the resolution locally and the bill update carries the ICD data as part of the standard payload.