Compliance - Views Documentation

Location: api/nextango/apps/compliance/views.py, api/nextango/apps/compliance/views_kyc.py, api/nextango/apps/compliance/cannabis/views.py Last Updated: 2026-07-06

Overview

The compliance app exposes endpoints for purchase-limit status, age verification, regulated customer onboarding (POS + storefront), pending-credential review, KYC vendor webhooks, cannabis compliance reports, and the Metrc verification gate on purchase orders. Authentication is required on every endpoint except the storefront self-registration submission and the KYC vendor webhook, both of which have their own authorisation paths.

The purchase-limit and age-verification view classes live in the cannabis container (compliance/cannabis/views.py); their routes stay registered on the agnostic compliance/urls.py, so the public paths do not change. Report and Metrc endpoints route through compliance/cannabis/urls.py. Primary keys are UUIDs across these models, path and body identifiers are UUID strings, not integers.


GET /compliance/purchase-limits/status/

Returns the current purchase limit consumption for a customer at a store. Passes an empty prospective cart, so the response reflects what has already been consumed in the current period, not a hypothetical future check.

Auth: Required
Class: PurchaseLimitStatusView

Query parameters:

ParameterTypeRequiredDescription
customer_idstring (UUID)YesCustomer PK
store_idstring (UUID)YesStore PK
transaction_typestrNo'adult_use' (default) or 'medical'

Response 200 OK (adult-use):

{
  "allowed": true,
  "period_type": "adult_use_daily",
  "categories": {
    "flower":      {"consumed": "0.0000", "limit": "2.5",     "remaining": "2.5",     "allowed": true},
    "concentrate": {"consumed": "0.0000", "limit": "15.0",    "remaining": "15.0",    "allowed": true},
    "edible":      {"consumed": "0.0000", "limit": "15000.0", "remaining": "15000.0", "allowed": true}
  }
}

Response 200 OK (medical):

{
  "allowed": true,
  "period_type": "medical_monthly",
  "categories": {
    "medical_thc_mg": {"consumed": "0.0000", "limit": "90000.0", "remaining": "90000.0", "allowed": true}
  }
}

Error 400:

{"error": "customer_id and store_id are required"}

POST /compliance/age-verification/

Records an age verification attempt at the POS and updates customer verification state. On approval with a date of birth, enqueues a Celery task to upsert a government_id_verification credential.

Auth: Required
Class: AgeVerificationView

Request body:

FieldTypeRequiredDescription
customer_idstring (UUID)YesCustomer PK
store_idstring (UUID)YesStore PK
employee_idstring (UUID)YesEmployee PK (the verifying employee)
verification_typestrYesgovernment_id, passport, military_id, or medical_card
resultstrYesapproved, rejected, rejected_expired, or rejected_invalid
id_expiry_datestrNoYYYY-MM-DD, document expiry
date_of_birthstrNoYYYY-MM-DD, captured on first presentation
notesstrNoFree-text notes

Response 201 Created:

{
  "event_id": "3f2b1e5a-9c74-4a1e-bf03-2d6c8e1a7b40",
  "result": "approved",
  "is_verified_for_session": true,
  "customer_type": "adult_use"
}

For medical customers, two additional fields are included:

{
  "event_id": "3f2b1e5a-9c74-4a1e-bf03-2d6c8e1a7b40",
  "result": "approved",
  "is_verified_for_session": true,
  "customer_type": "medical",
  "medical_card_valid": true,
  "medical_card_status": "active"
}

Error 400:

{"error": "Missing required fields: ['result']"}

GET /compliance/age-verification/status/

Checks whether a customer is currently verified for the session without recording a new event.

Auth: Required
Class: AgeVerificationStatusView

Query parameters:

ParameterTypeRequired
customer_idstring (UUID)Yes
store_idstring (UUID)Yes

Response 200 OK:

{
  "customer_id": "8a1c4d2e-5b39-4e77-9f21-6c0b3a9d1e58",
  "store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
  "is_verified_for_session": true,
  "customer_type": "adult_use"
}

For medical customers, medical_card_valid is included:

{
  "customer_id": "8a1c4d2e-5b39-4e77-9f21-6c0b3a9d1e58",
  "store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
  "is_verified_for_session": true,
  "customer_type": "medical",
  "medical_card_valid": true
}

Error 400:

{"error": "customer_id and store_id are required"}

POST /compliance/onboarding/regulated-customer/

Creates a user and their credentials atomically through RegulatedCustomerOnboardingService with channel='pos'. Credentials in the store's secondary_review_credential_types list enter pending_employee_review; all others become active.

Auth: IsAuthenticated + IsEmployeeOrManager Class: RegulatedCustomerOnboardingView

Request body:

{
  "user_data": {
    "email": "...",
    "name": "...",
    "first_name": "...",
    "last_name": "...",
    "store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"
  },
  "credentials": [
    {"credentialType": "government_id_verification", "payload": {...}}
  ],
  "channel": "pos"
}

Response 201 Created:

{
  "pk": "a4d9c2f1-6e83-4b57-9a02-1c7e5d8b3f60",
  "email": "[email protected]",
  "name": "Jane Doe",
  "credentials": [{"_key": "...", "credentialType": "...", "status": "active", "payload": {...}}]
}

Error 400: Invalid channel, missing store, missing required credential type, unknown credential type, invalid payload.


POST /compliance/onboarding/storefront-customer/

Unauthenticated storefront self-registration endpoint. Delegates to RegulatedCustomerOnboardingService with channel='storefront'. Every submitted credential is written with status='pending_verification' and awaits either employee review or KYC vendor confirmation.

Auth: AllowAny Class: StorefrontCustomerOnboardingView

Request body:

{
  "user_data": {"email": "...", "name": "...", "store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"},
  "credentials": [
    {"credentialType": "government_id_verification", "payload": {...}}
  ]
}

Response 201 Created: Same shape as the POS onboarding endpoint.

Error 400: Invalid input (missing store, unknown credential type, invalid payload). Error 409: Email already in use.


POST /compliance/onboarding/credentials/{credential_key}/approve/

Approves a pending credential. The acting user must pass EmployeeComplianceService.check_can_process_sale (active DCC agent permit + cleared background check). Only stored verificationData keys are verifiedDocumentId, verifiedBiometricId, reviewNotes, anything else in the request is dropped.

Auth: IsAuthenticated + IsEmployeeOrManager Class: ApprovePendingCredentialView

URL parameter: credential_key, the credential's _key.

Request body:

{
  "verificationData": {
    "verifiedDocumentId": "...",
    "verifiedBiometricId": "...",
    "reviewNotes": "..."
  }
}

Response 200 OK: Updated credential dict with status='active', approvedBy, approvedAt, and the filtered verificationData.

Error 400: Employee not authorised, credential not found, credential not in a pending status.


POST /compliance/onboarding/credentials/{credential_key}/reject/

Rejects a pending credential. Same employee and state preconditions as approve.

Auth: IsAuthenticated + IsEmployeeOrManager Class: RejectPendingCredentialView

URL parameter: credential_key, the credential's _key.

Request body:

{"reason": "Document expired and not re-submitted"}

Response 200 OK: Updated credential dict with status='rejected', rejectedBy, rejectedAt, rejectionReason.

Error 400: Employee not authorised, credential not found, credential not in a pending status.


GET /compliance/onboarding/pending-credentials/

Lists every credential in a pending status (pending_verification, pending_employee_review, pending_vendor_response) whose submittedAtStoreId matches the requested store. The requesting user must be an employee or manager with a store assignment covering that store.

Auth: IsAuthenticated + IsEmployeeOrManager Class: PendingCredentialsListView

Query parameters:

ParameterTypeRequired
store_idstring (UUID)Yes

Response 200 OK:

[
  {
    "user_id": "a4d9c2f1-6e83-4b57-9a02-1c7e5d8b3f60",
    "user_name": "Jane Doe",
    "credential_key": "ab12cd34...",
    "credential_type": "government_id_verification",
    "status": "pending_verification",
    "submittedAtStoreId": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"
  }
]

Error 400: store_id missing. Error 403: User has no employee or manager role with a store assignment covering this store.


POST /compliance/kyc/persona/webhook/

Persona inquiry-status webhook. Verifies the Persona-Signature HMAC header over the raw request body, then applies the adapter's VerificationResult to the owning credential. Full signing flow is documented in Webhooks; full adapter behaviour is documented in Adapters.

Auth: AllowAny (HMAC signature is the authorisation) Class: PersonaWebhookView

Response 200 OK: {"status": "active" | "rejected"} on resolved outcomes, or {"status": <current>, "detail": "unresolved"} for in-flight events that do not yet change status. Error 400: Invalid JSON body, missing reference-id. Error 401: Missing/invalid signature, stale timestamp (>300s skew), or webhook secret not configured. Error 404: Credential key from the payload does not match any stored credential.


GET /compliance/cannabis/reports/

Paginated list of compliance reports the caller may read. A report is visible to its requester and to the compliance manager of its target store.

Auth: IsAuthenticated Class: ComplianceReportListCreateView Pagination: 50 per page (ComplianceReportPagination).

Response 200 OK: Standard DRF paginated payload wrapping ComplianceReport records (id, report_type, store, date_from, date_to, status, requested_by, generated_at, error_message).


POST /compliance/cannabis/reports/

Creates a new ComplianceReport (status pending) and enqueues the generate_compliance_report Celery task. Store-scoped requests require the caller to be the compliance manager of that store; chain-wide requests (store=null) resolve to the caller's manager-accessible stores inside the generator.

Auth: IsAuthenticated Class: ComplianceReportListCreateView

Request body:

{
  "report_type": "daily_sales",
  "store": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
  "date_from": "2026-04-01",
  "date_to": "2026-04-20"
}

report_type is one of: daily_sales, purchase_limit_audit, age_verification_audit, recall_destruction_audit, cash_reconciliation.

Response 201 Created: Created ComplianceReport record.


GET /compliance/cannabis/reports/{id}/

Polls a report's status. Subject to the same read-scoping as the list endpoint.

Auth: IsAuthenticated Class: ComplianceReportDetailView

Response 200 OK: ComplianceReport record. Error 404: Report does not exist or caller is not permitted to read it.


GET /compliance/cannabis/reports/{id}/download/

Redirects to the generated output_file.url. Returns 404 unless the report is in ready status and has a file attached.

Auth: IsAuthenticated Class: ComplianceReportDownloadView

Response 302 Found: Redirect to the file URL. Response 404: Report not ready (status != ready) or caller not permitted to read it.


POST /compliance/cannabis/purchase-orders/{pk}/metrc-verify/

Transitions a purchase order from AWAITING_COMPLIANCE_VERIFICATION to RECEIVED after a compliance officer has visually verified the Metrc package tags. Writes verifiedAt and verifiedBy (user PK) to the compliance_verification JSONField.

Auth: Required
Class: PurchaseOrderMetrcVerifyView

URL parameter: pk, PurchaseOrder PK

Request body: Empty ({})

Response 200 OK:

{
  "po_number": "PO-2025-0042",
  "status": "received",
  "verified_at": "2025-03-05T14:32:00.000000+00:00"
}

Error 400 (wrong status):

{"error": "PO is not awaiting compliance verification (current status: received)"}

Was this page helpful?