Design Decisions

Key design constraints, architectural rationale, and extensibility guide for Bill-wise Critical Callout.

👤 Sachin Sharma📅 Updated: Apr 29, 2026🏷️ feature🏷️ architecture🏷️ decisions

Design Decisions & Architecture


Key Design Decisions & Constraints

Order-wise callout as the primary unit

One callout action covers all critical reports in an order simultaneously.

  • Previously: user had to action each critical report separately
  • Now: bill_id is the root key across API, bulk manager, and modal
  • Schema change: bill FK added to CriticalCallout; lab_report made nullable

Single bill-level draft record

Exactly one CriticalCallout row is created per bill when is_draft=True (not one per report).

  • lab_report = None on the draft record
  • Selected test IDs stored in critical_callout_meta.callout_for for UI restore
  • Only one draft can ever exist per order — atomically replaced on every resubmit

CALLOUT_ATTEMPTED as an explicit enum value

CALLOUT_ATTEMPTED = 4 is a first-class status in CriticalValuesEnum, not a flag.

  • Worklist can filter and display attempted orders separately from pending
  • Export includes it as a distinct Callout Status column value
  • Activity log carries is_draft: true in dumped_json
  • Flows through the same badge, tab-filter, and ES-sync paths as PENDING and DONE

Draft deletion on every submit

The existing draft record is always deleted inside transaction.atomic() before new records are created.

  • Ensures: at most one draft per bill at any time
  • Ensures: a completed callout can never coexist with a draft for the same bill

Department-scoped report visibility

FetchCriticalRecordsByBillView applies department filters per session type.

  • Doctor login: filtered by assigned departments or billId__docId
  • Lab user: filtered via LabUserDepartmentRelation
  • Support login: no department filter (elevated access)

Unified modal — action + history in one surface

Callout history is visible in the right panel of the same modal used to perform the callout.

  • Avoids context switching during operations
  • History loaded via a parallel bulk logs API call on modal open
  • Users can see previous attempts before deciding on next action

Communication channels as additive, config-controlled

SMS and WhatsApp were added without touching existing critical_notification_settings keys.

  • is_email, always_notify, mandatory_comments etc. are unchanged
  • New channels use the same notify_to payload structure
  • Future channels can be added the same way — no config schema migration needed

always_notify enforcement at the surface level

always_notify is checked individually in ReportEntryFooter and DoctorFooter, not in shared middleware.

  • Each surface intercepts its own action (Save & Sign, Approve)
  • Condition: completedTests === 1 AND criticalValues ∈ CRITICAL_CALLOUT_VALUES AND always_notify === 1 AND no existing logs
  • Keeps enforcement logic close to the action it guards; easy to modify per surface

Worklist filter state (date range, department, tab, search) is restored when user returns from patient overview.

  • Worklist users process many orders in sequence
  • Losing filters on every row click would break the operational flow

Architectural Rationale

CriticalReport as a proxy model, not a new model

CriticalReport is a Django proxy of LabReportRelation — no extra DB table.

  • Inherits all LabReportRelation fields and relationships
  • Adds behaviour: parameter evaluation, email, activity log, ES sync
  • Avoids data duplication; keeps callout logic co-located with the report

BulkCriticalCalloutManager as a standalone class

Bulk callout logic was extracted into a class rather than embedded in the view.

  • A single process_callout() call spans: parameter batching → email → DB transaction → ES sync → activity log
  • Classmethod get_critical_report_params_mapper is reused by both the manager and FetchCriticalRecordsByBillView
  • Improves testability — no HTTP layer needed to unit test the callout flow

Three-query batch fetch for parameter evaluation

get_critical_report_params_mapper() resolves critical parameters for all reports in an order using 3 DB queries regardless of report count.

  1. ReportValue — all values for the report IDs
  2. ReportFormat — all formats for the format IDs
  3. ValueRanges — age ranges (only if any formats have ageRangeFlag)

Results are grouped in Python; zero N+1 risk. Runs on every modal open and every callout submit.

Two-path ES sync strategy

PathTriggerScope
CriticalReport.after_save()Django save() hookSingle report
BillSplitManager.sync_lab_reports(bill)After bulk callout transactionFull bill

The two-path design exists because bulk_update skips the Django save() hook — bill-level sync after the transaction guarantees ES consistency without re-implementing after_save for bulk operations.

do_save=False for deferred bulk_create

save_critical_callout(do_save=False) returns an unsaved instance instead of calling INSERT immediately.

  • Bulk manager collects all instances, then calls bulk_create inside one atomic transaction
  • Avoids N separate INSERT statements
  • Partial failures roll back cleanly — no orphaned callout records

Reusability & Extensibility

What you can doHow
Add a new communication channelAdd to COMMUNICATION_METHODS (frontend) + METHOD_KEY_VALUE_MAPPER + save_critical_callout kwargs (backend)
Embed the callout button on a new surfaceImport CriticalNotificationButton, pass labReport + onSuccess + visibility condition — no modal changes
Add a new callout statusAdd to CriticalValuesEnumCRITICAL_CALLOUT_VALUESlabReportStatusUtils → worklist tab (if needed)
Extend draft restore fieldsAdd field to CriticalCalloutMeta interface → persist in critical_callout_meta JSON → restore in getRestoredCalloutState()
Add a new recipient typeAdd to RECIPIENT_KEY_VALUE_MAPPER in BulkCriticalCalloutManager + flag in notify_by_email()
Change the email templateEdit critical_notification.jinja — context (report_items, is_bulk, show_patient_name, comment) is parameterised
Add a new export columnAdd field to return object in getCriticalCalloutExportRows() — SheetJS picks up headers dynamically

On this page