Device Sales Invoices — Unification
:::caution Status
Plan / not implemented, per the explicit header of docs/device-sales-invoices-implementation-plan.md — with one exception (AZD-121) already shipped, cross-referenced from docs/device-registry-unification-jira-breakdown.md.
:::
Why this exists
Two commercial records exist today and don't talk to each other:
device_invoices / device_invoice_items | device_inventory.sold_to_* | |
|---|---|---|
| What | Formal document: draft → issued → paid, line items, printable, super-admin panel | Four bare columns on the asset row |
| Written by | POST /api/v1/device-invoices + the legacy transmitter auto-draft hook | core/device_claim.py, sets sold_to_user only, unconditionally, on every claim |
| Linked to | mosque_devices.id (legacy transmitter table) | n/a — flat columns, no document |
| Covers | Transmitters claimed through the legacy mosque_devices endpoints only | Both kinds, but only one of four commercial columns is ever actually written |
The unified claim path (used by both transmitter and stream player now) has no invoice hook at all — a stream player sold to a member today gets one column set and nothing else: no invoice, no price, no receipt.
Target design
| Item | Change | Ticket |
|---|---|---|
| Bill-to | device_invoices gains owner_user_id; mosque_id becomes optional; a CHECK enforces exactly one of the two set (a stream player is commonly sold before being bound to any mosque) | AZD-111 |
| Line items | device_invoice_items.device_id FK repoints from mosque_devices to the unified devices table (a pure constraint swap — ids already match from the earlier fold-in migration); items gain a device_kind snapshot | AZD-111, AZD-113 |
| Commercial snapshot | device_inventory.sold_to_*/sale_price/payment_status become a denormalized snapshot written by the invoice lifecycle (issue/mark-paid/cancel), not hand-written anywhere else | AZD-114 |
| Auto-draft hook | Moves from the legacy transmitter routes into core/device_claim.py, so both device kinds get an auto-drafted invoice on claim; settings gain per-kind pricing ({"transmitter": ..., "stream_player": ...}) | AZD-116, AZD-117 |
| Settings route | Renamed to /api/v1/device-invoices/auto-draft-settings (from the transmitter-only /api/v1/mosque-devices/... path) — renamed outright, not aliased, since nothing in production depends on the old path | AZD-116 |
Ticket list
| Ticket | Item | Status |
|---|---|---|
| AZD-111 | Migration: bill-to XOR check, FK repoint, quantity CHECK | Not confirmed done in sources available to this page |
| AZD-113 | Invoice DB layer: unified device lookups | Not confirmed done |
| AZD-114 | Snapshot writer | Not confirmed done |
| AZD-116 | Auto-draft hook on the unified claim path | Not confirmed done |
| AZD-117 | Delete legacy auto-draft hook | Not confirmed done |
| AZD-119 | E2E: claim → invoice → paid | Not confirmed done |
| AZD-120 | Bill-to toggle (super-admin panel) | Not confirmed done |
| AZD-121 | Unified device picker (super-admin panel) | Done — landed ahead of the rest of this epic; referenced in the device-registry-unification jira breakdown as already shipped and used by that unification's own fleet-endpoint cutover |
| AZD-122 | Billed-to display | Not confirmed done |
| AZD-123 | Inventory page sale snapshot display | Not confirmed done |
| AZD-124 | Auto-draft settings panel repoint | Not confirmed done |
None of these except AZD-121 carry an explicit completion marker in the
source documents — treat the rest as still planned, not implemented, until
verified against the actual core/device_claim.py, api/v1/endpoints/device_invoices.py,
and super-admin-panel/src/pages/SalesInvoicesPage.jsx.
What does not change
- App installs never get invoices — no commercial facts, no inventory row, free to every approved member; explicitly out of scope.
- Ownership transfer does not re-invoice — a resale/gift is not a new sale from Azan360.
- The printable view and draft→issued→paid→cancelled lifecycle are unchanged — they already work per-invoice regardless of what the line items point to.
Dependency on Device Registry Unification
AZD-116 assumes every claim (transmitter or stream player) goes through
core/device_claim.py:claim(). That assumption only became true for
transmitters once Device Registry Unification
landed — the two were explicitly sequenced together to avoid transmitter
auto-drafting silently doing nothing in the gap between the two shipping.