Skip to main content
Version: 3.0

Resource Category

ResourceCategory is a facility-scoped, self-nesting category tree that classifies resources — product_knowledge, activity_definition, and charge_item_definition — and carries charge-item monetary components that inherit down the tree.

The Django model is just storage. The behaviour you code against lives in the Pydantic resource specs in care/emr/resources/resource_category/: the resource_type enum, the MonetaryComponent shape behind the *_monetary_components JSON fields, slug validation, and the split between read and write schemas. Several model fields are opaque JSONFields whose real structure exists only in the specs — see Resource specs (API schema).

Source:

Models

ModelPurpose
ResourceCategoryA facility-scoped, self-nesting category tree node that classifies resources (product_knowledge, activity_definition, charge_item_definition) and carries inheritable charge-item monetary components

ResourceCategory extends SlugBaseModel with FACILITY_SCOPED = True, the facility-scoped slug variant of the base. It inherits the EMR base fields — external_id, audit fields, soft-delete, history/meta JSON — and adds slug handling on top.

ResourceCategory fields

Core

FieldTypeRequiredNotes
facilityFK → facility.Facility (PROTECT)yesOwning facility; PROTECT blocks deletion while categories reference it. Set server-side and excluded from spec write/read payloads
resource_typeCharField(255)yesTop-level classification. The spec constrains it to the ResourceCategoryResourceTypeOptions enum — see values
resource_sub_typeCharField(255)yesSecondary classification; a free string in the spec (resource_sub_type: str)
titleCharField(255)yesDisplay name (title: str)
slugCharField(255)yesStored encoded as f-<facility_external_id>-<slug_value>. Clients send the unencoded slug_value (validated SlugType); calculate_slug() encodes it
descriptionTextFieldnoNullable, blank-able (description: str | None = None)

Meta declares a composite index on (slug, facility) for slug lookups within a facility.

Tree structure

FieldTypeNotes
parentFK → emr.ResourceCategory (CASCADE)Self-reference to the parent node; nullable. related_name="children". __exclude__d from spec serialization — written via slug lookup, read via get_parent_json()
is_childBooleanFieldDefault False. Exposed on write (ResourceCategoryWriteSpec) and read
has_childrenBooleanFieldDefault False. Flips to True on the parent when a child is created (see save behaviour). Read-only in specs
root_orgFK → emr.ResourceCategory (CASCADE)Self-reference to the root of this node's tree; nullable. related_name="root". Not exposed in specs

Tree caches

set_organization_cache() and get_parent_json() denormalize the ancestor chain into these fields, so reads resolve it without recursive joins. They are platform-maintained; clients never write them.

FieldTypeRebuilt by
parent_cacheArrayField[int]set_organization_cache() — the parent's parent_cache plus the parent's id
level_cacheIntegerFieldset_organization_cache() — parent's level_cache + 1 (default 0 for roots). Exposed read-only (level_cache: int = 0)
cached_parent_jsonJSONFieldget_parent_json() — nested snapshot (shape below) with a cache_expiry timestamp; refreshed after cache_expiry_days (15)

get_parent_json() builds cached_parent_json in this shape, which is also the shape of the parent field in ResourceCategoryReadSpec:

{
"id": str, # parent.external_id (UUID)
"slug": str, # parent.slug (encoded)
"title": str,
"description": str | null,
"parent": { ... }, # recursively the grandparent's cached_parent_json ({} at root)
"cache_expiry": str # ISO datetime; now + 15 days
}

Root nodes (no parent_id) serialize parent as {}.

Charge item monetary components

Both are JSONField(default=list) in the model; the spec types each element as a MonetaryComponent (see shape). They serialize only when resource_type == charge_item_definition.

FieldTypeNotes
configured_monetary_componentsJSONFieldlist[MonetaryComponent] | NoneComponents configured directly on this category
calculated_monetary_componentsJSONFieldlist[MonetaryComponent] | NoneEffective components after merging the parent's calculated components with this node's configured components (see Methods). Server-maintained, read-only

Enums

ResourceCategoryResourceTypeOptions values

care/emr/resources/resource_category/spec.py — bound to resource_type.

ValueMeaning
product_knowledgeCategory classifies Product Knowledge entries
activity_definitionCategory classifies Activity Definitions
charge_item_definitionCategory classifies Charge Item Definitions; enables the monetary-component fields

MonetaryComponentType values

care/emr/resources/common/monetary_component.py — bound to MonetaryComponent.monetary_component_type.

Value
base
surcharge
discount
tax
informational

Resource specs (API schema)

All specs extend EMRResource (serialize/de_serialize; reads run perform_extra_serialization, writes run perform_extra_deserialization). With __model__ = ResourceCategory and __exclude__ = ["parent"], the parent FK is never auto-mapped — every spec resolves it explicitly.

Spec classRoleExposes / behaviour
ResourceCategoryBaseSpecshared baseid (UUID, read), title, description, resource_type (enum), resource_sub_type
ResourceCategoryWriteSpecwrite · createBase + parent: str | None (parent slug), is_child: bool = False, slug_value: SlugType. When parent is set, perform_extra_deserialization resolves obj.parent = ResourceCategory.objects.get(slug=self.parent), and sets obj.slug = self.slug_value
ResourceCategoryUpdateSpecwrite · updateBase + slug_value: SlugType. perform_extra_deserialization sets obj.slug = self.slug_value; update never reassigns parent
ResourceCategoryReadSpecread · list & detailBase + parent: dict, has_children: bool, level_cache: int = 0, is_child: bool, slug: str, slug_config: dict, calculated_monetary_components, configured_monetary_components. See serialization below

Docstrings label these "ChargeItemDefinition Category", but the model is ResourceCategory. There is no separate *ListSpec/*RetrieveSpecResourceCategoryReadSpec serves both list and detail.

ResourceCategoryReadSpec serialization (perform_extra_serialization)

  • id = obj.external_id
  • parent = obj.get_parent_json() — the nested ancestor snapshot (shape above)
  • slug_config = obj.parse_slug(obj.slug) — a facility-scoped slug returns {"facility": <uuid str>, "slug_value": <str>}; an instance slug returns {"slug_value": <str>}
  • calculated_monetary_components / configured_monetary_components — set only when resource_type == "charge_item_definition"; otherwise these default-None fields are omitted

SlugType validation

slug_value is Annotated[str, Field(min_length=5, max_length=50), AfterValidator(slug_validator)]:

  • length 5–50
  • regex ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$ — URL-safe (letters, digits, -, _), starting and ending alphanumeric

The stored slug is the encoded form f-<facility_external_id>-<slug_value> (via SlugBaseModel.calculate_slug()); slug_config round-trips it back to {facility, slug_value}.

MonetaryComponent (nested shape)

Element type of configured_monetary_components / calculated_monetary_components (common/monetary_component.py).

FieldTypeDefaultNotes
monetary_component_typeMonetaryComponentType enumrequired; see values
codeCoding | NoneNoneCoding { system?: str, version?: str, code: str, display?: str } (extra="forbid")
factorDecimal | NoneNonemax_digits=20, decimal_places=6
amountDecimal | NoneNonemax_digits=20, decimal_places=6
tax_included_amountDecimal | NoneNonemax_digits=20, decimal_places=6; allowed only on base
global_componentboolFalse
conditionslist[EvaluatorConditionSpec][]each { metric: str, operation: str, value: dict | str }, validated against the evaluator-metrics registry

MonetaryComponent model validators:

  • tax_included_amount only allowed when type is base
  • base must have no conditions
  • base must have an amount
  • amount and factor are mutually exclusive (not both)
  • either amount or factor must be present (unless global_component and code)

The category stores and validates these per element as MonetaryComponent. The stricter collection validators — MonetaryComponents/MonetaryComponentsWithoutBase (single base, no duplicate codes, tax-sum balance) — live with the charge-item resources, not the category list.

Methods & save behaviour

ResourceCategory overrides save() to populate the tree caches on create. A handful of helper methods plus a Celery task maintain those caches and the inherited monetary components.

save()

  • On create (self.id not yet set): runs super().save(), then set_organization_cache(), then enqueues monetary-component summarisation via summarise_monetary_components(self).
  • On update: a plain super().save(*args, **kwargs), no cache rebuild.

set_organization_cache()

Runs once on creation when a parent is present:

  • Sets parent_cache = [*parent.parent_cache, parent.id] and level_cache = parent.level_cache + 1.
  • Sets root_org to the parent's root_org, or to the parent itself if the parent is a root.
  • If the parent's has_children was False, flips it to True and persists it with save(update_fields=["has_children"]).
  • Persists the node via super().save().

get_parent_json()

Returns cached_parent_json, rebuilding it lazily. A snapshot still within its cache_expiry is returned as-is. Past expiry, the method walks the parent chain, builds a fresh nested snapshot with a new expiry (now + cache_expiry_days), persists it via save(update_fields=["cached_parent_json"]), and returns it. Root nodes (no parent_id) return {}.

summarise_monetary_components(category) — Celery task

A @shared_task that recomputes calculated_monetary_components down the subtree:

  • For a root (no parent), calculated_monetary_components = configured_monetary_components.
  • Otherwise it merges the parent's calculated_monetary_components with this node's configured_monetary_components via merge_monetary_components(), then persists with save(update_fields=["calculated_monetary_components"]).
  • It re-dispatches itself (.delay(component.id)) for every child, propagating changes through the tree.

merge_monetary_components(parent, child) keys components by code.system + code.code; child components override parent components sharing a key, and components without a code are appended unmerged.

API integration notes

  • Categories are facility-scoped. The (slug, facility) index and SlugBaseModel encoding (f-<facility>-<slug>) keep slugs unique per facility. Clients send slug_value (5–50, URL-safe); the server encodes it and slug_config decodes it.
  • Write parent by slug (ResourceCategoryWriteSpec.parentResourceCategory.objects.get(slug=...)), not by id. On read it comes back as the nested get_parent_json() snapshot.
  • parent_cache, level_cache, root_org, has_children, cached_parent_json, and calculated_monetary_components are platform-maintained — don't set them. Write parent and configured_monetary_components; save() and the Celery task derive the rest.
  • Cache rebuilds (set_organization_cache, monetary-component summarisation) run only on create. Changing an existing node's parent or configured components does not rebuild the caches.
  • Monetary-component propagation runs through Celery, so descendants' calculated_monetary_components is eventually consistent after a write.
  • Monetary-component fields serialize only for resource_type == "charge_item_definition"; during merges, component identity rests on the nested code.system + code.code pair.