Skip to main content
Version: 3.0

Account

An Account is the financial bucket that aggregates every transaction made against a patient within a facility. You touch it indirectly: charge items land on a patient's default account, and invoices and reconciliations settle against it.

Several model fields are opaque JSONFields whose real structure lives in the Pydantic resource specs under care/emr/resources/account/. Each field below is paired with the spec-defined enums, JSON shapes, validation, and read/write schemas that govern it.

Source:

Models

ModelPurpose
AccountAggregates charges and balances for one patient in one facility across encounters

Account extends EMRBaseModel, which contributes external_id, the audit fields created_date/modified_date, soft-delete via deleted, created_by/updated_by, and history/meta JSON.

Accounts are perpetual. One active, open account exists per patient per facility (see get_default_account), and charge items land on it. After discharge, the account can be balanced and closed. Invoices are raised against an account or against the items within it.

Account fields

Identity & scope

FieldTypeRequiredNotes
facilityFK → facility.Facility (PROTECT)yesServer-set from the request scope; absent from every write spec
patientFK → emr.Patient (PROTECT)yesThe entity that incurred the expenses. On create, pass a patient external_id (UUID); the server resolves it
nameCharField(255)yesHuman-readable label. The default account auto-names as "{patient.name} {YYYY-MM-DD}"
descriptionTextFieldnoNullable free text for purpose or use
primary_encounterFK → emr.Encounter (SET_NULL, nullable)noPins the account to a single encounter for reports, summaries, and insurance paperwork. Settable only through the update spec, as an encounter external_id

Status

FieldTypeRequiredNotes
statusCharField(255)yesLifecycle status from AccountStatusOptions (see values); the enum rejects out-of-set strings
billing_statusCharField(255)yesBilling lifecycle status from AccountBillingStatusOptions (see values)
service_periodJSONField (default {})yes (on spec)Transaction window holding a PeriodSpec { start, end } — see service_period shape

Balances & totals

These decimal totals (max_digits=20, decimal_places=6, default 0) are platform-maintained aggregates that sync_account_items recomputes. None are client-writable, and none appear on a write spec.

FieldTypeNotes
total_netDecimalFieldNet total before adjustments. Not populated — the sync_account_items logic is commented out, so it holds the default 0
total_grossDecimalFieldSum of total_price over charge items in paid or billed status
total_paidDecimalFieldSum of active+complete payment reconciliations, minus credit-note reconciliations
total_balanceDecimalFieldtotal_gross − total_paid
total_billable_charge_itemsDecimalFieldSum of total_price over charge items in billable status
total_price_componentsJSONField (default {})Intended price-component breakdown (taxes, discounts, etc.) as a list of MonetaryComponent (see below). Not populated — the sync logic is commented out
cached_itemsJSONField (default {})Denormalized snapshot of the account's charge items, serialized as a list on retrieve. Not populated — the sync logic is commented out
calculated_atDateTimeField (nullable)When the balances were last computed; set to care_now() on each sync

Tags & extensions

FieldTypeNotes
tagsArrayField[int] (default [])Tag IDs applied to the account; rendered through SingleFacilityTagManager on read
extensionsJSONField (default {})Open bag for deployment-specific metadata. ExtensionValidator checks it against ExtensionResource.account; surfaced only on the retrieve spec

Enum values

AccountStatusOptions values

Stored as the raw enum value string in status.

ValueMeaning
activeOpen and in use (the default-account state)
inactiveNo longer actively used
entered_in_errorCreated in error
on_holdTemporarily paused

Values use underscores (entered_in_error, on_hold), not the FHIR-style hyphenated forms.

AccountBillingStatusOptions values

Stored as the raw enum value string in billing_status.

ValueMeaning
openBilling open and accruing (the default-account state)
carecomplete_notbilledCare complete, not yet billed
billingBilling in progress
closed_baddebtClosed as bad debt
closed_voidedClosed and voided
closed_completedClosed, billing completed
closed_combinedClosed, combined into another account

JSON field shapes

service_period shape

service_period stores a PeriodSpec (from resources/base.py):

PeriodSpec {
start: datetime | null # ISO 8601, timezone-aware
end: datetime | null # ISO 8601, timezone-aware
}

PeriodSpec.validate_period enforces three rules:

  • start, if present, must be timezone-aware — a naive datetime raises "Start Date must be timezone aware".
  • end, if present, must be timezone-aware — "End Date must be timezone aware".
  • With both present, start <= end, or "Start Date cannot be greater than End Date".

The default account sets only start, formatted as "%Y-%m-%dT%H:%M:%S.%fZ" from care_now().

The FHIR-style Period common type ({ id?, start?, end? }, extra="forbid") is a related but distinct shape — Account binds service_period to PeriodSpec, not Period.

MonetaryComponent shape

total_price_components is meant to hold a list of MonetaryComponent entries, the same shape used across billing:

MonetaryComponent {
monetary_component_type: base | surcharge | discount | tax | informational
code: Coding | null
factor: Decimal(max_digits=20, decimal_places=6) | null
amount: Decimal(max_digits=20, decimal_places=6) | null
tax_included_amount: Decimal(max_digits=20, decimal_places=6) | null
global_component: bool = false
conditions: EvaluatorConditionSpec[] = []
}

MonetaryComponent validation: tax_included_amount is allowed only on a base component; a base component must carry an amount, no factor, and no conditions; amount and factor are mutually exclusive; exactly one of amount/factor is required unless the entry is a global_component with a code.

Resource specs (API schema)

The API layer lives in resources/account/spec.py. Every spec builds on EMRResource (serialize / de_serialize, with the perform_extra_serialization / perform_extra_deserialization hooks). AccountSpec sets __exclude__ = ["patient"] and binds ___extension_resource_type__ = ExtensionResource.account.

Spec classRoleFields exposed (beyond base)
AccountSpecshared baseid, status, billing_status, name, service_period (PeriodSpec), description
AccountCreateSpecwrite · createbase + patient (UUID, required)
AccountUpdateSpecwrite · updatebase + primary_encounter (UUID, optional)
AccountMinimalReadSpecread · minimalbase + total_gross, total_paid, total_balance, total_billable_charge_items (all Decimal 20/6), calculated_at, created_date, modified_date
AccountReadSpecread · listminimal + patient (serialized PatientListSpec), tags (rendered tag dicts)
AccountRetrieveSpecread · detailread + patient (serialized PatientRetrieveSpec), primary_encounter (serialized EncounterRetrieveSpec when set), cached_items (list), total_price_components (dict), extensions (dict)

Validation & server-side behaviour

  • Create (AccountCreateSpec): mixes in ExtensionValidator to validate extensions, and requires patient as a UUID. perform_extra_deserialization resolves it via get_object_or_404(Patient, external_id=...) and assigns obj.patient; patient is otherwise excluded from the base serialize loop.
  • Update (AccountUpdateSpec): mixes in ExtensionValidator; primary_encounter is optional. When supplied, perform_extra_deserialization resolves it via get_object_or_404(Encounter, external_id=...) and assigns obj.primary_encounter.
  • Status / billing_status: the Pydantic enums AccountStatusOptions / AccountBillingStatusOptions reject any value outside their members.
  • Read serialization: perform_extra_serialization sets mapping["id"] = obj.external_id. AccountReadSpec serializes the patient (PatientListSpec) and renders tags via SingleFacilityTagManager; AccountRetrieveSpec serializes the full patient (PatientRetrieveSpec, scoped to obj.facility) and, when present, the primary encounter (EncounterRetrieveSpec).
  • Totals are read-only: balance and aggregate fields appear only on read specs. sync_account_items recomputes them; client payloads never set them.

Default account

get_default_account(patient, facility) (default_account.py) returns the first account for that patient + facility with status = active and billing_status = open. If none exists, it creates one with:

  • status = active, billing_status = open
  • service_period = { "start": care_now() } (start only)
  • name = "{patient.name} {YYYY-MM-DD}"

This runs when the first charge item is added, so the default account materializes on demand. Clients normally never create accounts directly.

Balance sync

sync_account_items(account) (sync_items.py) recomputes the totals under an AccountLock:

  • total_billable_charge_items = Σ total_price of charge items with status billable
  • total_gross = Σ total_price of charge items with status paid or billed
  • total_paid = Σ amount of active + complete non-credit-note payment reconciliations − the same for credit notes
  • total_balance = total_gross − total_paid
  • calculated_at = care_now()

Every sum runs through care_round. total_net, cached_items, and total_price_components are not updated — their logic is commented out — so they hold their defaults. rebalance_account_task(account_id) is the Celery wrapper that runs the sync and saves.

Account is the only class in account.py. Its relationships:

facility → FK facility.Facility (PROTECT)
patient → FK emr.Patient (PROTECT)
primary_encounter → FK emr.Encounter (SET_NULL, nullable)

Charge items reference the account they belong to and drive its totals; invoices and payment reconciliations settle against the account or its items.

API integration notes

  • The REST API aligns to the FHIR Account resource, but spec field names and enum values differ (enums use underscores, not hyphens).
  • get_default_account creates the default account when the first charge item is added for a patient in a facility; clients normally do not create accounts directly.
  • On create, supply patient as the patient's external_id (UUID); facility is server-scoped.
  • On update, primary_encounter accepts an encounter external_id (UUID); only the base fields plus primary_encounter are mutable.
  • Balance and aggregate fields (total_net, total_gross, total_paid, total_balance, total_billable_charge_items, total_price_components, cached_items, calculated_at) are platform-maintained and appear only on read specs — never set them from clients.
  • service_period requires timezone-aware datetimes with start <= end.
  • extensions is the supported place for custom key-value data without schema migrations; ExtensionValidator checks it, and it surfaces only on the retrieve spec.
  • Gate account data behind permissions that restrict access to users with rights to financial information.