diff --git a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md index edd44f3..ab8ca51 100644 --- a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md +++ b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md @@ -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. ---