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:
- Model:
care/emr/models/resource_category.py - Spec:
care/emr/resources/resource_category/spec.py - Shared:
base.py·common/monetary_component.py·common/coding.py
Models
| Model | Purpose |
|---|---|
ResourceCategory | A 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
| Field | Type | Required | Notes |
|---|---|---|---|
facility | FK → facility.Facility (PROTECT) | yes | Owning facility; PROTECT blocks deletion while categories reference it. Set server-side and excluded from spec write/read payloads |
resource_type | CharField(255) | yes | Top-level classification. The spec constrains it to the ResourceCategoryResourceTypeOptions enum — see values |
resource_sub_type | CharField(255) | yes | Secondary classification; a free string in the spec (resource_sub_type: str) |
title | CharField(255) | yes | Display name (title: str) |
slug | CharField(255) | yes | Stored encoded as f-<facility_external_id>-<slug_value>. Clients send the unencoded slug_value (validated SlugType); calculate_slug() encodes it |
description | TextField | no | Nullable, blank-able (description: str | None = None) |
Meta declares a composite index on (slug, facility) for slug lookups within a facility.
Tree structure
| Field | Type | Notes |
|---|---|---|
parent | FK → 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_child | BooleanField | Default False. Exposed on write (ResourceCategoryWriteSpec) and read |
has_children | BooleanField | Default False. Flips to True on the parent when a child is created (see save behaviour). Read-only in specs |
root_org | FK → 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.
| Field | Type | Rebuilt by |
|---|---|---|
parent_cache | ArrayField[int] | set_organization_cache() — the parent's parent_cache plus the parent's id |
level_cache | IntegerField | set_organization_cache() — parent's level_cache + 1 (default 0 for roots). Exposed read-only (level_cache: int = 0) |
cached_parent_json | JSONField | get_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.
| Field | Type | Notes |
|---|---|---|
configured_monetary_components | JSONField → list[MonetaryComponent] | None | Components configured directly on this category |
calculated_monetary_components | JSONField → list[MonetaryComponent] | None | Effective 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.
| Value | Meaning |
|---|---|
product_knowledge | Category classifies Product Knowledge entries |
activity_definition | Category classifies Activity Definitions |
charge_item_definition | Category 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 class | Role | Exposes / behaviour |
|---|---|---|
ResourceCategoryBaseSpec | shared base | id (UUID, read), title, description, resource_type (enum), resource_sub_type |
ResourceCategoryWriteSpec | write · create | Base + 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 |
ResourceCategoryUpdateSpec | write · update | Base + slug_value: SlugType. perform_extra_deserialization sets obj.slug = self.slug_value; update never reassigns parent |
ResourceCategoryReadSpec | read · list & detail | Base + 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/*RetrieveSpec—ResourceCategoryReadSpecserves both list and detail.
ResourceCategoryReadSpec serialization (perform_extra_serialization)
id = obj.external_idparent = 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 whenresource_type == "charge_item_definition"; otherwise these default-Nonefields 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).
| Field | Type | Default | Notes |
|---|---|---|---|
monetary_component_type | MonetaryComponentType enum | — | required; see values |
code | Coding | None | None | Coding { system?: str, version?: str, code: str, display?: str } (extra="forbid") |
factor | Decimal | None | None | max_digits=20, decimal_places=6 |
amount | Decimal | None | None | max_digits=20, decimal_places=6 |
tax_included_amount | Decimal | None | None | max_digits=20, decimal_places=6; allowed only on base |
global_component | bool | False | |
conditions | list[EvaluatorConditionSpec] | [] | each { metric: str, operation: str, value: dict | str }, validated against the evaluator-metrics registry |
MonetaryComponent model validators:
tax_included_amountonly allowed when type isbasebasemust have noconditionsbasemust have anamountamountandfactorare mutually exclusive (not both)- either
amountorfactormust be present (unlessglobal_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.idnot yet set): runssuper().save(), thenset_organization_cache(), then enqueues monetary-component summarisation viasummarise_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]andlevel_cache = parent.level_cache + 1. - Sets
root_orgto the parent'sroot_org, or to the parent itself if the parent is a root. - If the parent's
has_childrenwasFalse, flips it toTrueand persists it withsave(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_componentswith this node'sconfigured_monetary_componentsviamerge_monetary_components(), then persists withsave(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 andSlugBaseModelencoding (f-<facility>-<slug>) keep slugs unique per facility. Clients sendslug_value(5–50, URL-safe); the server encodes it andslug_configdecodes it. - Write
parentby slug (ResourceCategoryWriteSpec.parent→ResourceCategory.objects.get(slug=...)), not by id. On read it comes back as the nestedget_parent_json()snapshot. parent_cache,level_cache,root_org,has_children,cached_parent_json, andcalculated_monetary_componentsare platform-maintained — don't set them. Writeparentandconfigured_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'sparentor configured components does not rebuild the caches. - Monetary-component propagation runs through Celery, so descendants'
calculated_monetary_componentsis eventually consistent after a write. - Monetary-component fields serialize only for
resource_type == "charge_item_definition"; during merges, component identity rests on the nestedcode.system+code.codepair.
Related
- Reference: Charge Item Definition
- Reference: Charge Item
- Reference: Activity Definition
- Reference: Product Knowledge
- Reference: Facility
- Reference: Base Model
- Source: resource_category.py (model) · spec.py · monetary_component.py