Compliance - Signals Documentation
Location: api/nextango/apps/compliance/cannabis/signals.py
Last Updated: 2026-07-06
Overview
All compliance signals are registered in compliance/cannabis/signals.py. They are post-save handlers that keep cannabis profile models in sync with their parent records without requiring callers to invoke compliance services explicitly.
sync_cannabis_profile
Trigger: products.ProductVariant, post_save (any save)
Keeps ProductVariantCannabisProfile synchronized with ProductVariant.cannabis_compliance. Calls sync_cannabis_profile_from_variant(), which is also used by the data migration backfill; both paths use identical logic.
Fields synced from cannabis_compliance (JSONField):
| Source key | Target field | Notes |
|---|---|---|
limitCategory | limit_category | Defaults to 'other' if invalid or missing |
unitWeightMg | unit_weight_mg | None if not present |
menuType | menu_type | Defaults to 'both' if invalid or missing |
vapeTaxApplicable | vape_tax_applicable | Defaults to False |
Creates the profile if it does not exist. Idempotent, safe to call multiple times for the same variant. Does not trigger another ProductVariant save.
create_cannabis_snapshot_for_return_line
Trigger: transactions.ReturnLineItem, post_save (created=True only)
Creates a ReturnLineItemCannabisSnapshot when a ReturnLineItem is created for a cannabis sale line item. Copies the Metrc package tag from the original SaleLineItemCannabisSnapshot so the return has a complete audit trail back to the originating package.
If the original sale line item has no cannabis snapshot (a non-cannabis item), no snapshot is created; this is correct behaviour.
Bulk-create caveat: Django's bulk_create() bypasses post_save signals. Any future bulk-create path for ReturnLineItem must write snapshots explicitly.
sync_metrc_tags_on_awaiting_compliance
Trigger: procurement.PurchaseOrder, post_save (status transition to AWAITING_COMPLIANCE_VERIFICATION only)
Fires when a PurchaseOrder transitions to AWAITING_COMPLIANCE_VERIFICATION. For each package ID in compliance_verification['packageIds'], creates a MetrcTag row (if it does not already exist) and calls get_state_track_client().pull_package().
Client behaviour:
| Client | pull_package() result |
|---|---|
StubStateTrackClient (default) | Logs a PACKAGE_PULL StateTrackSyncRecord with status=STUB |
MetrcClient (production) | Calls the live Metrc API; writes VERIFIED status on success |
Known gap: MetrcTag rows are created with purchase_order set but inventory_level=None. The package-to-InventoryLevel mapping is not available at this transition point. The link is established later when an employee calls the metrc-verify endpoint or performs manual reconciliation. Closing this gap requires adding package→InventoryLevel mapping to the compliance_verification slot (Phase 7 scope).
create_destruction_event_on_destruction_disposition
Trigger: transactions.ReturnLineItem, post_save (created=True and disposition=DESTRUCTION)
Creates a CannabisDestructionEvent when a ReturnLineItem with disposition=DESTRUCTION is created and the original sale line item has a cannabis snapshot.
Signal registration order matters: create_cannabis_snapshot_for_return_line fires before this signal, so instance.cannabis_snapshot is available when this handler runs.
Inventory level resolution: Derived from store + product + variant_sku via a queryset lookup (the same path used by InventoryAdjustmentService). The reference_inventory_levels M2M cannot be used here because post_save(created=True) fires before any M2M assignment.
witnessed_by: Set to None, not available from signal context. Must be updated via admin or a future employee-session context for compliance reports.
If the return line item has no cannabis snapshot (non-cannabis return), no destruction event is created.