Compliance - Models
Location: api/nextango/apps/compliance/models.py, api/nextango/apps/compliance/cannabis/models.py
Last Updated: 2026-07-06
Overview
The compliance app separates models into two layers. Abstract base classes in compliance/models.py define the structural contract for each compliance concept. Cannabis-specific concrete classes in compliance/cannabis/models.py extend these bases with vertical-specific fields and db_table declarations.
Abstract Base Classes
These classes are never instantiated directly. They establish the field contract that concrete subclasses must honour.
PurchaseLimitRecordBase
Records that a customer purchased a regulated quantity within a compliance window.
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
customer | FK → users.Customer | on_delete=PROTECT, db_index=True |
transaction | FK → transactions.SaleTransaction | on_delete=PROTECT |
store | FK → stores.Store | on_delete=PROTECT, db_index=True |
period_type | CharField(25) | db_index=True; subclass defines TextChoices |
period_start | DateTimeField | db_index=True; store-local midnight (adult-use) or first of month (medical) |
is_voided | BooleanField | default=False, db_index=True; set on return |
voided_at | DateTimeField | nullable |
voided_by_return | FK → transactions.Return | on_delete=SET_NULL, nullable |
IdentityVerificationEventBase
Audit record for an identity or credential check at point of sale.
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
customer | FK → users.Customer | db_index=True |
store | FK → stores.Store | |
verified_by | FK → users.Employee | Staff member who performed the check |
verification_type | CharField(30) | Subclass defines choices |
result | CharField(30) | db_index=True; subclass defines choices |
transaction | FK → transactions.SaleTransaction | nullable; links event to a specific sale |
notes | TextField |
BatchTestResultBase
Certificate of Analysis or equivalent batch test record.
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
lab_name | CharField(255) | |
test_date | DateField | |
test_type | CharField(30) | Subclass defines choices |
is_passing | BooleanField | default=True |
is_current | BooleanField | db_index=True; only one current COA per batch |
report_url | URLField | nullable |
confirmed_by | FK → users.Employee | nullable |
confirmed_at | DateTimeField | nullable |
source | CharField(20) | Subclass defines choices |
StateTrackSyncRecordBase
Log entry for every state-tracking regulatory API call.
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
operation | CharField(30) | db_index=True; subclass defines TextChoices |
status | CharField(10) | db_index=True; subclass defines TextChoices |
reference_type | CharField(50) | Identifies the model type being reported |
reference_id | CharField(100) | db_index=True; PK of the referenced record |
request_payload | JSONField | Raw API request body |
response_payload | JSONField | Raw API response |
error_message | TextField | Populated on failure |
performed_by | FK → users.Employee | nullable |
RegulatedBatchProfileBase
Batch provenance fields with completed cross-vertical evidence matrices.
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
inventory_level | OneToOneField → products.InventoryLevel | Accessor name is %(class)s; concrete classes override with a friendly name |
lot_number | CharField(100) | nullable; 21 CFR 211.130(c) (pharma), TTB 27 CFR 5.66 (alcohol) |
is_quarantined | BooleanField | default=False, db_index=True; set by recall service on regulatory hold; distinct from InventoryLevel.is_active |
RegulatedStoreProfileBase
Vertical-agnostic store compliance policy. Holds the store-level compliance fields shared across regulated verticals; a concrete subclass declares the Store OneToOne and adds vertical-specific store fields. These four fields are the Django side of the regulatedCompliance sync seam (store.regulatedCompliance.* in Sanity).
Base class: TimestampedModel | Abstract: yes
| Field | Type | Notes |
|---|---|---|
require_age_gate | BooleanField | default=True; POS requires age verification before checkout |
required_credential_types | JSONField | default=list; credential-type strings a customer must present to complete a purchase at this store. Vertical-agnostic; read by POS, storefront, and the onboarding backend |
secondary_review_credential_types | JSONField | default=list; credential types that always enter pending_employee_review on POS capture instead of going straight to active |
permits | JSONField | default=list; array of permit objects synced from Sanity store.regulatedCompliance.permits |
compliance_manager | FK → users.Employee | on_delete=SET_NULL, nullable; a concrete subclass may override the reverse accessor |
RegulatedSaleLineSnapshotBase
OneToOneField anchor for point-of-sale compliance snapshots.
Base class: TimestampedModel | Abstract: yes
Each concrete subclass provides its own related_name and adds vertical-specific content fields. Snapshots are immutable after creation.
RegulatedReturnLineSnapshotBase
OneToOneField anchor for return-line compliance snapshots.
Base class: TimestampedModel | Abstract: yes
RegulatedTransactionProfileBase
OneToOneField anchor for per-transaction compliance profiles.
Base class: TimestampedModel | Abstract: yes
Cannabis Concrete Models
CannabisPurchaseLimitRecord
db_table: cannabis_purchase_limit_record
Base class: PurchaseLimitRecordBase
| Field | Type | Notes |
|---|---|---|
customer | FK → users.Customer | related_name='cannabis_purchase_limit_records' |
transaction | FK → transactions.SaleTransaction | related_name='cannabis_purchase_limit_records' |
store | FK → stores.Store | related_name='cannabis_purchase_limit_records' |
period_type | CharField(25) | Choices: adult_use_daily, medical_monthly |
product_category | CharField(20) | Choices: flower, concentrate, edible, other |
category_amount | DecimalField(12,4) | Units: flower=oz, concentrate=g, edible=mg THC |
thc_mg_total | DecimalField(12,4) | Total THC milligrams consumed in this transaction |
Index: cplr_limit_lookup_idx on (customer, store, period_type, period_start, product_category, is_voided).
CannabisAgeVerificationEvent
db_table: cannabis_age_verification_event
Base class: IdentityVerificationEventBase
| Field | Type | Notes |
|---|---|---|
customer | FK → users.Customer | related_name='cannabis_age_verification_events' |
store | FK → stores.Store | |
verified_by | FK → users.Employee | Budtender who performed the check |
transaction | FK → transactions.SaleTransaction | nullable |
verification_type | CharField(20) | Choices: government_id, passport, military_id, medical_card |
result | CharField(25) | Choices: approved, rejected, rejected_expired, rejected_invalid |
id_expiry_date | DateField | nullable; expiry on the presented document |
customer_date_of_birth | DateField | nullable; captured for audit on first verification |
CannabisLabResult
db_table: cannabis_lab_result
Base class: BatchTestResultBase
| Field | Type | Notes |
|---|---|---|
inventory_level | FK → products.InventoryLevel | related_name='cannabis_lab_results', db_index=True |
test_type | CharField(30) | Choices: full_panel, potency, residual_solvents, microbials, heavy_metals, pesticides |
source | CharField(20) | Choices: sanity_seed, manual_entry, state_track_pull |
cannabinoid_data | JSONField | Array of compound objects with compound name, valuePct, and valueMgPerG |
terpene_data | JSONField | Array of terpene objects with name and valuePct |
confirmed_by | FK → users.Employee | nullable |
Constraint: cannabis_lab_result_one_current_per_batch, at most one is_current=True per inventory_level.
StoreComplianceProfile
The cannabis vertical's store compliance profile.
db_table: store_compliance_profile
Base class: RegulatedStoreProfileBase
Accessor: store.compliance_profile
require_age_gate, required_credential_types, secondary_review_credential_types, and permits are inherited from RegulatedStoreProfileBase (see the field table above). The permits array mirrors Sanity store.regulatedCompliance.permits; each entry has _key, permitType, permitNumber, status, issuedDate, expiryDate, payload, documentUrl, notes. The concrete class adds the store relation, a cannabis-specific field, and a friendlier reverse accessor for the manager.
| Field | Type | Notes |
|---|---|---|
store | OneToOneField → stores.Store | related_name='compliance_profile' |
compliance_manager | FK → users.Employee | nullable; overrides the base field to pin related_name='managed_compliance_stores' |
state_tracking_license_number | CharField(100) | Cannabis-specific; state-issued license for Metrc or equivalent |
ProductVariantCannabisProfile
Denormalized cannabis compliance flat columns. The authoritative source is ProductVariant.cannabis_compliance (JSONField). This profile is never written directly; it is populated automatically, synchronously, whenever the parent ProductVariant is saved.
db_table: product_variant_cannabis_profile
Base class: TimestampedModel
Accessor: variant.cannabis_profile
| Field | Type | Notes |
|---|---|---|
variant | OneToOneField → products.ProductVariant | related_name='cannabis_profile' |
limit_category | CharField(20) | Choices: flower, concentrate, edible, other; db_index=True |
unit_weight_mg | DecimalField(10,4) | nullable; net weight of one unit in milligrams |
menu_type | CharField(15) | Choices: adult_use, medical, both |
vape_tax_applicable | BooleanField | default=False; triggers Ohio vape excise if True |
Never write to limit_category, unit_weight_mg, menu_type, or vape_tax_applicable directly. Write to ProductVariant.cannabis_compliance and save the variant; the profile updates automatically.
InventoryLevelCannabisProfile
Cannabis-specific batch compliance data for an inventory level. Populated on stock receipt from supplier manifest or Metrc package pull.
db_table: cannabis_inventory_level_profile
Base class: RegulatedBatchProfileBase
Accessor: inventory_level.cannabis_profile
| Field | Type | Notes |
|---|---|---|
inventory_level | OneToOneField → products.InventoryLevel | related_name='cannabis_profile' |
lot_number | CharField(100) | Inherited from base; nullable |
is_quarantined | BooleanField | Inherited from base; set by recall service |
batch_number | CharField(100) | nullable |
packaged_date | DateField | nullable |
expiry_date | DateField | nullable |
coa_url | URLField | nullable |
state_tracking_package_id | CharField(30) | nullable, db_index=True; Metrc package UID |
harvest_date | DateField | nullable |
actual_thc_pct | DecimalField(5,2) | nullable; batch-tested THC %; overrides label claim for limit calculations |
total_thc_mg | DecimalField(10,4) | nullable; unit_weight_mg × (actual_thc_pct / 100); used by PurchaseLimitEngine |
SaleTransactionCannabisProfile
Point-in-time compliance classification for a transaction. Written once at checkout Gate 1; never recomputed after the sale closes.
db_table: cannabis_saletransaction_profile
Base class: TimestampedModel
Accessor: transaction.compliance_profile
| Field | Type | Notes |
|---|---|---|
transaction | OneToOneField → transactions.SaleTransaction | related_name='compliance_profile' |
transaction_type | CharField(20) | Choices: adult_use, medical; db_index=True; derived by TaxClassificationService.classify_transaction_type() |
transaction_type is never accepted from the client payload. The serializer exposes it read-only via SerializerMethodField.
SaleLineItemCannabisSnapshot
Cannabis compliance data captured at point of sale. Immutable after creation.
db_table: cannabis_sale_line_item_snapshot
Base class: RegulatedSaleLineSnapshotBase
Accessor: line_item.cannabis_snapshot
| Field | Type | Notes |
|---|---|---|
sale_line_item | OneToOneField → transactions.SaleLineItem | related_name='cannabis_snapshot' |
state_tracking_package_id_snapshot | CharField(30) | Metrc package UID at time of sale |
thc_mg_snapshot | DecimalField(10,4) | nullable; THC mg per unit at time of sale; used for limit reversal on return |
Only created for cannabis line items. Non-cannabis line items have no snapshot row.
ReturnLineItemCannabisSnapshot
Cannabis return snapshot. Created automatically when a cannabis ReturnLineItem is saved.
db_table: cannabis_return_line_item_snapshot
Base class: RegulatedReturnLineSnapshotBase
Accessor: return_line_item.cannabis_snapshot
| Field | Type | Notes |
|---|---|---|
return_line_item | OneToOneField → transactions.ReturnLineItem | related_name='cannabis_snapshot' |
state_tracking_package_tag | CharField(30) | nullable; copied from the originating SaleLineItemCannabisSnapshot |
MetrcTag
Tracks individual state-tracking package tags pulled into the system. One row per unique package tag.
db_table: cannabis_metrc_tag
Base class: TimestampedModel
| Field | Type | Notes |
|---|---|---|
tag | CharField(30) | unique=True, db_index=True; Metrc format: 24-char tag |
status | CharField(15) | Choices: unverified, verified, active, sold, returned, destroyed, discrepancy; db_index=True |
inventory_level | FK → products.InventoryLevel | nullable; related_name='state_track_tags'; set when linked to a stock batch |
purchase_order | FK → procurement.PurchaseOrder | nullable; related_name='state_track_tags' |
pulled_at | DateTimeField | nullable; when the tag was fetched from the state track API |
last_sync_at | DateTimeField | nullable |
state_track_data | JSONField | Raw package data from the API |
StateTrackSyncRecord
Log of every state-tracking API call (stub or production).
db_table: cannabis_state_track_sync_record
Base class: StateTrackSyncRecordBase
| Field | Type | Notes |
|---|---|---|
tracking_system | CharField(30) | Choices: metrc; db_index=True |
operation | CharField(30) | Choices: package_pull, sale_report, transfer_manifest, adjustment, destruction, return_report |
status | CharField(10) | Choices: stub, success, failed, retry |
performed_by | FK → users.Employee | nullable |
state_tracking_package_id | CharField(30) | Package tag involved in this operation |
CannabisRecallEvent
A DCC or manufacturer-issued cannabis product recall.
db_table: cannabis_recall_event
Base class: TimestampedModel
| Field | Type | Notes |
|---|---|---|
title | CharField(255) | |
recall_type | CharField(25) | Choices: mandatory, voluntary, market_withdrawal |
status | CharField(20) | Choices: pending, active, resolved, cancelled; db_index=True |
issuing_authority | CharField(255) | |
reason | TextField | |
recall_date | DateField | |
resolution_date | DateField | nullable |
affected_lot_numbers | JSONField | Lot numbers matched against InventoryLevelCannabisProfile.lot_number |
affected_package_ids | JSONField | Metrc package IDs matched against InventoryLevelCannabisProfile.state_tracking_package_id |
affected_variants | M2M → products.ProductVariant | Through: CannabisProductRecall |
affected_inventory_levels | M2M → products.InventoryLevel | |
managed_by | FK → users.Employee | nullable |
dcc_notice_url | URLField | nullable |
internal_notes | TextField |
CannabisProductRecall
Through model linking CannabisRecallEvent to ProductVariant.
db_table: cannabis_product_recall
Base class: TimestampedModel
| Field | Type | Notes |
|---|---|---|
recall_event | FK → CannabisRecallEvent | |
product_variant | FK → products.ProductVariant | related_name='cannabis_product_recalls' |
units_affected | PositiveIntegerField | |
units_disposed | PositiveIntegerField | |
disposal_notes | TextField |
CannabisDestructionEvent
Immutable audit record for every cannabis destruction. Immutable after creation; admin has has_delete_permission=False.
db_table: cannabis_destruction_event
Base class: TimestampedModel
| Field | Type | Notes |
|---|---|---|
return_line_item | FK → transactions.ReturnLineItem | nullable; set when destruction was triggered by a return |
recall_event | FK → CannabisRecallEvent | nullable; set when triggered by a recall |
inventory_level | FK → products.InventoryLevel | on_delete=PROTECT |
state_tracking_package_id_snapshot | CharField(30) | Metrc tag at time of destruction; snapshot, not a live reference |
quantity_destroyed | PositiveIntegerField | |
reason | CharField(25) | Choices: customer_return, recall, expired, quality_failure, compliance_order, damaged |
destruction_date | DateField | |
witnessed_by | FK → users.Employee | nullable; signal-created events have no employee context |
state_track_reported_at | DateTimeField | nullable; set when reported to state track |
notes | TextField |
Async Report Jobs
ComplianceReport
Status record for an async compliance report generation job. Created by ComplianceReportService.request_report(); populated by the generate_compliance_report Celery task, which sets output_file and transitions status to ready.
db_table: compliance_report
Base class: TimestampedModel
| Field | Type | Notes |
|---|---|---|
report_type | CharField(30) | db_index=True; choices: daily_sales, purchase_limit_audit, age_verification_audit, recall_destruction_audit, cash_reconciliation |
store | FK → stores.Store | nullable, on_delete=PROTECT, related_name='compliance_reports'; null=True means all stores the requester has access to |
date_from | DateField | Report range start (inclusive) |
date_to | DateField | Report range end (inclusive) |
status | CharField(15) | db_index=True; choices: pending, generating, ready, failed; default pending |
requested_by | FK → users.Employee | nullable, on_delete=PROTECT, related_name='requested_compliance_reports' |
generated_at | DateTimeField | nullable; set when the task writes output_file |
output_file | FileField | nullable; upload_to='compliance_reports/' |
error_message | TextField | Populated when status='failed' |
Ordering: -created_at.
Access control is applied at request time in ComplianceReportService, store-scoped reports verify that the requester's employment record covers the store, chain-wide reports (store=None) are restricted to roles with cross-store visibility.