Compliance - Domain Overview
Location: api/nextango/apps/compliance/
Last Updated: 2026-07-06
Overview
The compliance app is the platform's industry-agnostic compliance scaffold. It defines abstract base models for purchase limit records, identity verification events, batch test results, state-track sync records, batch and store profiles, and sale/return snapshots. Concrete implementations live in a vertical sub-app (compliance.cannabis). The agnostic app also hosts the pieces that are genuinely vertical-neutral: the RegulatedCustomerOnboardingService orchestrator, the TaxClassificationService rate-filter engine, the KYCVendorAdapter seam, credential and permit read helpers, and the vertical-provider seam that resolves the active vertical at runtime.
Cannabis is the vertical shipping today. It is documented here as one vertical plugged into agnostic slots. The same slots hold a food-safety or pharmaceutical vertical without rewriting the agnostic core.
Architecture
The agnostic app names no industry vocabulary. Everything cannabis-specific (concrete models, tax tiers, purchase-limit ceilings, age-gate rules, recall handling, report generators) lives inside compliance/cannabis/. The two layers connect through three seams.
Abstract base models. compliance/models.py defines the field contract for each compliance concept. The cannabis container subclasses each base and adds vertical-specific fields and a db_table.
The vertical-provider seam. compliance/services/vertical_provider.py exposes get_active_vertical_provider(). Agnostic callers resolve the active vertical's provider through it and never import a cannabis module directly. The cannabis provider (CannabisComplianceProvider) supplies store credential policy, credential-payload validators, employee authorization, the pre-completion checkout gates, purchase-limit recording, the vertical's additional tax excise, and the store-profile write.
Vertical registration. The cannabis app registers itself from ComplianceCannabisConfig.ready() (compliance/cannabis/apps.py): it connects its signals, registers its report generators in the agnostic report registry, and registers its batch profile as a hard-delete blocker on products.InventoryLevel. An install without the cannabis app contributes none of these, and the agnostic models carry no cannabis import.
The compliance domain uses two patterns for storing vertical-specific data:
JSONField compliance slots live on core platform models (ProductVariant.cannabis_compliance, User.credentials, Store.regulatedCompliance.permits) for Sanity-authored content. The Sanity schema holds the editable data; the JSONField is the write path; denormalized flat columns on profile models are populated from it.
One-to-one profile models live in compliance/cannabis/models.py for indexed, queryable compliance state not held in Sanity. Each profile attaches to its core model through a stable reverse accessor.
| Profile model | Core model | Accessor |
|---|---|---|
StoreComplianceProfile | stores.Store | store.compliance_profile |
ProductVariantCannabisProfile | products.ProductVariant | variant.cannabis_profile |
InventoryLevelCannabisProfile | products.InventoryLevel | inventory_level.cannabis_profile |
SaleTransactionCannabisProfile | transactions.SaleTransaction | transaction.compliance_profile |
Sub-apps
| Path | Purpose |
|---|---|
compliance/ | Abstract bases, agnostic services, the vertical-provider seam, KYC adapters, shared endpoints |
compliance/cannabis/ | Cannabis vertical: concrete models, cannabis services, the compliance provider, Metrc integration, report generators |
Endpoints
Customer-facing gate endpoints (purchase limits, age verification) keep their routes on the agnostic compliance/urls.py; their view classes live in the cannabis container. The public paths are unchanged.
| Method | Path | Purpose |
|---|---|---|
| GET | /compliance/purchase-limits/status/ | Current purchase window status for a customer |
| POST | /compliance/age-verification/ | Record an age verification attempt |
| GET | /compliance/age-verification/status/ | Check session verification status |
| POST | /compliance/onboarding/regulated-customer/ | POS employee-assisted onboarding |
| POST | /compliance/onboarding/storefront-customer/ | Storefront self-registration (AllowAny) |
| POST | /compliance/onboarding/credentials/<key>/approve/ | Approve a pending credential |
| POST | /compliance/onboarding/credentials/<key>/reject/ | Reject a pending credential |
| GET | /compliance/onboarding/pending-credentials/ | Store-scoped pending credentials listing |
| POST | /compliance/kyc/persona/webhook/ | Persona KYC vendor webhook (HMAC-signed) |
| GET | /compliance/cannabis/reports/ | List compliance reports |
| POST | /compliance/cannabis/reports/ | Request a compliance report |
| GET | /compliance/cannabis/reports/<id>/ | Poll report status |
| GET | /compliance/cannabis/reports/<id>/download/ | Download a ready report file |
| POST | /compliance/cannabis/purchase-orders/<pk>/metrc-verify/ | Transition a PO to RECEIVED after a compliance check |
Key Services
Location tells you which layer owns the logic. Where an agnostic path still exists for a cannabis-canonical service, that path is a legacy-import compatibility shim holding no logic; the architectural route from agnostic code to vertical behavior is the get_active_vertical_provider() seam, not the shims.
| Service | Location | Purpose |
|---|---|---|
RegulatedCustomerOnboardingService | compliance/services/regulated_customer_onboarding_service.py | Atomic user + credential creation, channel-aware state, approve/reject workflow; delegates vertical rules through the provider seam |
TaxClassificationService | compliance/services/tax_classification.py | Agnostic rate-filter engine that filters a jurisdiction's rate components by compliance tier |
get_active_vertical_provider | compliance/services/vertical_provider.py | Resolves the active vertical's compliance provider for agnostic callers |
AgeVerificationService | compliance/cannabis/services/age_verification.py | Age gate enforcement, session verification, medical-card validation (agnostic path is a compatibility shim) |
CannabisTaxService | compliance/cannabis/services/tax_classification.py | Cannabis transaction-type classification and the Ohio vape excise |
EmployeeComplianceService | compliance/cannabis/services/employee_compliance_service.py | Gate 0 check: DCC agent permit + cleared background check (agnostic path is a compatibility shim) |
CannabisComplianceProvider | compliance/cannabis/services/compliance_provider.py | The cannabis vertical's provider: store policy, validators, checkout gates, excise, profile write |
ComplianceReportService | compliance/services/report_service.py | Report request authorization and Celery task dispatch |
PurchaseLimitEngine | compliance/cannabis/services/purchase_limit_engine.py | Cannabis purchase limit gate and window tracking |
CannabisRecallService | compliance/cannabis/services/recall_service.py | Recall activation, destruction recording, resolution |