Skip to main content

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_itemsdevice_inventory.sold_to_*
WhatFormal document: draft → issued → paid, line items, printable, super-admin panelFour bare columns on the asset row
Written byPOST /api/v1/device-invoices + the legacy transmitter auto-draft hookcore/device_claim.py, sets sold_to_user only, unconditionally, on every claim
Linked tomosque_devices.id (legacy transmitter table)n/a — flat columns, no document
CoversTransmitters claimed through the legacy mosque_devices endpoints onlyBoth 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

ItemChangeTicket
Bill-todevice_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 itemsdevice_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 snapshotAZD-111, AZD-113
Commercial snapshotdevice_inventory.sold_to_*/sale_price/payment_status become a denormalized snapshot written by the invoice lifecycle (issue/mark-paid/cancel), not hand-written anywhere elseAZD-114
Auto-draft hookMoves 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 routeRenamed 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 pathAZD-116

Ticket list

TicketItemStatus
AZD-111Migration: bill-to XOR check, FK repoint, quantity CHECKNot confirmed done in sources available to this page
AZD-113Invoice DB layer: unified device lookupsNot confirmed done
AZD-114Snapshot writerNot confirmed done
AZD-116Auto-draft hook on the unified claim pathNot confirmed done
AZD-117Delete legacy auto-draft hookNot confirmed done
AZD-119E2E: claim → invoice → paidNot confirmed done
AZD-120Bill-to toggle (super-admin panel)Not confirmed done
AZD-121Unified 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-122Billed-to displayNot confirmed done
AZD-123Inventory page sale snapshot displayNot confirmed done
AZD-124Auto-draft settings panel repointNot 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.