Document order invoice lifecycle
This commit is contained in:
@@ -1,12 +1,89 @@
|
||||
# [Refactor] Order/Invoice Lifecycle — Unified PO → Invoice Pipeline
|
||||
|
||||
**Status**: Proposed (design only — no code changes yet)
|
||||
**Status**: Partially implemented — schema, API, and DB migration live on branch
|
||||
`feat/order-invoice-lifecycle`. Frontend, `LoadingSheet` wiring, and the cancellation cascade
|
||||
(Open Question 5) are not done. See [Implementation Status](#implementation-status) below for the
|
||||
as-built schema, which differs in a few small ways from the original design further down this
|
||||
document.
|
||||
**Refactors**: [../05_delivery_ride.md](../05_delivery_ride.md) and
|
||||
[../06_sale_ride.md](../06_sale_ride.md) (both kept as-is, this is the target design)
|
||||
**Related but independent**: [ride_lifecycle.md](./ride_lifecycle.md), [dispatch_pipeline.md](./dispatch_pipeline.md)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Everything below reflects what is actually in the database and codebase right now, not just the
|
||||
plan. The rest of this document (Data Model Changes onward) is kept as the original design record —
|
||||
where the two disagree, this section wins.
|
||||
|
||||
### `Orders` (existing table, altered)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `FK_SaleId` | `char(36)`, nullable | Changed from required to nullable, as designed. |
|
||||
| `CreatedByUserId` | `char(36)`, required | **Added — not in the original diagram below.** The
|
||||
original design had no way to know who created a PO or to answer the `agentId` filter Mode B's own
|
||||
endpoints require once a PO can exist without a Sale. Backfilled for every pre-existing row from its
|
||||
linked Sale's `CreatedByUserId`. FK → `Users`. |
|
||||
| `ConfirmedAt` | `datetime`, nullable | **Added — not in the original diagram below.** The design
|
||||
never named a field for "is this PO confirmed" independent of an Invoice existing (Mode B confirms
|
||||
with no Invoice at all), so this was added to actually gate item-editing per Business Rule 1. Null =
|
||||
Shop PO, set = Confirmed PO. |
|
||||
| `ConfirmedByUserId` | `char(36)`, nullable | **Added**, alongside `ConfirmedAt`. FK → `Users`. |
|
||||
| `InvoiceId` | `char(36)`, nullable | As designed — plain column, no DB-level FK constraint. |
|
||||
|
||||
### `OrderItems` (existing table, altered)
|
||||
|
||||
| Change | Notes |
|
||||
|---|---|
|
||||
| Primary key | Changed from the composite `(FK_OrderId, FK_ItemId)` to the single inherited `Id`
|
||||
column. **Required for versioning to work at all** — the original composite key made it impossible
|
||||
for two rows to exist for the same PO+item (one soft-deleted, one active), which item-editing needs.
|
||||
Not called out explicitly in the original design below. |
|
||||
| `IsActive` | Already existed via the `Template` base class; now actually used to mark old versions
|
||||
inactive on every edit. |
|
||||
| New index | `IX_OrderItems_FK_OrderId` (standalone — previously implied by the composite key). |
|
||||
|
||||
### `Invoices` (new table)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Id` | `char(36)` | PK |
|
||||
| `FK_OrderId` | `char(36)`, required | FK → `Orders` |
|
||||
| `Status` | `int` (`InvoiceStatus`) | `Draft` / `Printed` / `Finalized` |
|
||||
| `CreatedByUserId` | `char(36)`, required | FK → `Users`. Named `CreatedByUserId` in code, not
|
||||
`CreatedBy` as in the diagram below — matches this codebase's existing naming convention
|
||||
(`Sale.CreatedByUserId`, etc.). |
|
||||
| `PrintedAt` | `datetime`, nullable | |
|
||||
| `PrintedByUserId` | `char(36)`, nullable | FK → `Users`. Same naming note as above. |
|
||||
| `FinalizedAt` | `datetime`, nullable | |
|
||||
| `FinalizedByUserId` | `char(36)`, nullable | FK → `Users`. Same naming note as above. |
|
||||
| `TotalAmount` | `decimal` | |
|
||||
| `IsActive`, `CreatedAt` | from `Template` | |
|
||||
|
||||
### API endpoints implemented
|
||||
|
||||
`POST /api/orders` (independent PO creation, replacing the old "created from Sale" behaviour),
|
||||
`PUT /api/orders/{id}/items` (versioned rewrite), `POST /api/orders/{id}/confirm` (Mode B),
|
||||
`POST /api/orders/{id}/confirm-and-create-invoice` (Mode A), `GET /api/orders/confirmed`
|
||||
(filtered by `agentId`/`routeId`/`shopId`), `POST /api/orders/{id}/cancel`; `POST /api/invoices`
|
||||
(Mode B), `POST /api/invoices/{id}/print`, `POST /api/invoices/{id}/finalize` (using the resolved
|
||||
payload from Open Question 6 below). Both modes exist side by side, gated at runtime by the
|
||||
`OrderInvoice:InvoiceCreationMode` config flag (`appsettings.json`, defaulted to `"B"`).
|
||||
|
||||
### Not implemented yet
|
||||
|
||||
- Frontend (no UI changes made).
|
||||
- `LoadingSheet` population still needs its own wiring per [loading_event.md](./loading_event.md) —
|
||||
untouched by this pass.
|
||||
- Cancellation cascade to `Invoice`/`LoadingSheet` (Open Question 5) — still unresolved; cancelling a
|
||||
Confirmed PO with a linked Invoice today leaves the Invoice untouched.
|
||||
- `Payment`/`Invoice` wiring from [invoice_collection.md](./invoice_collection.md) — separate doc,
|
||||
not started.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the Delivery-vs-Sale ride-type split with a single unified pipeline. The PO and Invoice are
|
||||
@@ -201,9 +278,38 @@ Draft → Printed → Finalized
|
||||
already has a linked Invoice, what happens to that Invoice (voided? stays Draft?) is unresolved.
|
||||
If the PO's items were pulled into a loading sheet, what happens to those loading sheet items is
|
||||
also unresolved. Both cases need explicit cascade rules before implementation.
|
||||
6. **Exact adjustment payload for `POST /finalize`** — quantity adjustments, returns, damage, and
|
||||
discounts are assumed but the payload shape is not fully specified. "Free issues" handling (items
|
||||
given to the shop at no charge) is a known case that needs its own field(s) in the payload.
|
||||
6. ~~**Exact adjustment payload for `POST /finalize`**~~ — **Resolved:**
|
||||
|
||||
```
|
||||
POST /api/invoices/{id}/finalize
|
||||
{
|
||||
"items": [
|
||||
{ "orderItemId": Guid, "deliveredQuantity": decimal, "freeQuantity": decimal }
|
||||
],
|
||||
"returns": [
|
||||
{ "itemId": Guid, "quantity": decimal, "reasonCode": ReturnReasonCode }
|
||||
],
|
||||
"discount": { "type": "Amount" | "Percentage", "value": decimal, "reason": string? } | null
|
||||
}
|
||||
```
|
||||
|
||||
- `items[].deliveredQuantity` — what the shop actually received per line, replacing the ordered
|
||||
`OrderItem.Quantity` for billing purposes. Captures shortages/quantity edits the agent
|
||||
handwrote on the printed copy.
|
||||
- `items[].freeQuantity` — **the free-issue field this document previously flagged as missing.**
|
||||
Units given to the shop at no charge on top of `deliveredQuantity`. Contributes `0` to
|
||||
`Invoice.TotalAmount` but is still recorded per line for stock/audit — it moves stock the same
|
||||
as a sold unit, it just isn't billed.
|
||||
- `returns` — **reuses the existing `SaleReturn`/`SaleReturnItem`/`CreditNote` shape from
|
||||
[18_returns_claims.md](../18_returns_claims.md) instead of inventing a parallel mechanism.**
|
||||
Finalize creates a `SaleReturn` (`Mode = CreditDeduction`) keyed to the PO's `Order` with one
|
||||
`SaleReturnItem` per entry, which produces a `CreditNote` the normal way. "Damage" is not a
|
||||
separate field — it's `ReasonCode = Damaged` on a return line, since `ReturnReasonCode` already
|
||||
covers it (`Expired, Damaged, OrderError, Other`).
|
||||
- `discount` — a single amount-or-percentage adjustment applied to the invoice subtotal, with an
|
||||
optional free-text reason for audit. Not per-line; one discount per invoice for v1.
|
||||
- `Invoice.TotalAmount` at Finalize = `Σ(items[].deliveredQuantity × OrderItem.UnitPrice)`
|
||||
− discount − `Σ(CreditNote.Amount from returns)`. `freeQuantity` never enters this sum.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user