diff --git a/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf b/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf new file mode 100644 index 0000000..c038954 Binary files /dev/null and b/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf differ diff --git a/Distribution/docs/refactor_docs/dispatch_pipeline.md b/Distribution/docs/refactor_docs/dispatch_pipeline.md new file mode 100644 index 0000000..c8c81df --- /dev/null +++ b/Distribution/docs/refactor_docs/dispatch_pipeline.md @@ -0,0 +1,195 @@ +# [Refactor] Dispatch Pipeline — Shop PO to Invoice Finalization + +**Status**: Proposed (design only — no code changes yet) +**Depends On**: [order_invoice_lifecycle.md](./order_invoice_lifecycle.md), [loading_event.md](./loading_event.md), [ride_lifecycle.md](./ride_lifecycle.md) + +--- + +## Goal + +Define the end-to-end order processing pipeline from a shop's initial PO through to invoice +completion. This replaces the separate Delivery ride and Sale ride flows +([05_delivery_ride.md](../05_delivery_ride.md), [06_sale_ride.md](../06_sale_ride.md)) with a +single unified pipeline. + +--- + +## Pipeline Overview + +``` +[Stage 1] Shop PO created (editable) + ↓ +[Stage 2] PO Confirmed + Invoice Created → Draft (Mode A: combined / Mode B: separate) + ↓ +[Stage 3] Invoice Printed → agent takes to field + ↓ +[Stage 4] Loading Sheet Population (dispatcher-triggered, separate flow) + ↓ +[Stage 5] Delivery (TBD) + ↓ +[Stage 6] Agent returns printed invoice → office worker enters edits → Invoice Finalized +``` + +--- + +## Stage 1 — Shop PO + +**Business term**: Shop PO +**System state**: `Order` (no Invoice yet) + +The entry point to the pipeline. A sales agent creates a PO for a shop capturing line items and +quantities. + +**Key rules:** +- Items editable — every edit soft-deletes current `OrderItem` rows (`IsActive = false`) and writes + a fresh set. `IsActive = true` = latest version; `IsActive = false` = history. +- Editing allowed only while Shop PO (before confirmation). +- No Invoice exists yet at this stage. + +**API:** +| Method | Route | Description | +|---|---|---| +| POST | `/api/orders` | Create Shop PO for a store. | +| PUT | `/api/orders/{id}/items` | Rewrite line items (versioned). | +| GET | `/api/orders/{id}` | Get PO with current active items. | + +--- + +## Stage 2 — Confirmed PO + Invoice Created + +**Business term**: Confirmed PO +**System state**: `Order` (items locked) + `Invoice` at `Draft` + +PO items are locked and an Invoice is created at `Draft`. How this happens depends on the active +mode — controlled by the system-level `InvoiceCreationMode` config flag. + +**Mode A — combined action (one button):** +- `POST /api/orders/{id}/confirm-and-create-invoice` +- Locks PO items and creates Invoice at `Draft` atomically. + +**Mode B — separate actions:** +- `POST /api/orders/{id}/confirm` — locks PO items only. +- Office user then browses Confirmed POs (filtered by `agentId`, `routeId`, `shopId`) and creates + the invoice: `POST /api/invoices { orderId }`. + +**Key rules:** +- Confirmation requires no signature or OTP. +- `OrderItem` lines are permanently locked after confirmation. +- `Order.Status` (fulfillment tracking) is untouched — separate axis. + +--- + +## Stage 3 — Invoice Printed + +**Trigger**: `POST /api/invoices/{id}/print` + +Office user prints the invoice. The agent takes the physical copy to the field. + +**Key rules:** +- Agent may make handwritten edits on the printed copy during the ride (quantity adjustments, + returns, damage, discounts). +- Invoice is locked from system edits between `Printed` and `Finalized`. + +--- + +## Stage 4 — Loading Sheet Population + +A dispatcher manually triggers population via `POST /api/loading-sheets/populate` with one or more +filters. The system finds all matching Confirmed POs and aggregates them into **one loading sheet**. + +**Filters (at least one required):** +- `routeId` — all Confirmed POs for shops on this route +- `agentId` — all Confirmed POs created by this agent +- `shopId` — all Confirmed POs for this specific shop + +**What happens:** +1. System finds all `Order` records matching the filter where items are locked (Confirmed PO) +2. One `LoadingSheet` created at `Planned` status +3. `LoadingSheetItem` rows populated from the active `OrderItem` lines of those POs +4. Each source PO recorded in `LoadingSheetOrder` linking table +5. Each item carries `FK_OrderItemId` pointing back to its source `OrderItem` + +**After population — editing:** +- Dispatcher can edit item quantities while the sheet is `Planned` or `Assigned` +- Every edit soft-deletes current `LoadingSheetItem` rows (`IsActive = false`) and writes a fresh + set — same versioning as Shop PO items + +**Confirmation and stock movement:** +- Ride is provided at populate time — no separate assign step +- Each item has its own `FK_WarehouseId` — one sheet can load from multiple warehouses +- Dispatcher confirms the sheet (`Planned → Confirmed`) — items frozen +- Stock movements: one debit per item's warehouse, one credit to the ride's vehicle +- Timing: open question (Path A at `Confirmed` vs Path B at `ReceiptConfirmed`) + +See [loading_event.md](./loading_event.md) for the full state machine. + +--- + +## Stage 5 — Delivery + +**Status**: TBD — to be designed in a subsequent pass. + +--- + +## Stage 6 — Invoice Finalized + +**Trigger**: `POST /api/invoices/{id}/complete` + +After the ride or day-end, the agent returns the printed invoice to the office. The office worker +enters any edits the agent made into the system and submits. + +**What happens:** +- Office worker enters adjustments (quantity changes, returns, damage, discounts) from the paper copy +- `Invoice.TotalAmount` is recomputed and locked +- `Invoice.Status` → `Finalized` +- `FinalizedAt` / `FinalizedBy` set + +--- + +## Stage 3 — Loading Sheet Population + +A dispatcher manually triggers population via `POST /api/loading-sheets/populate` with one or more +filters. The system finds all matching Confirmed POs and aggregates them into **one loading sheet**. + +**Filters (at least one required):** +- `routeId` — all Confirmed POs for shops on this route +- `agentId` — all Confirmed POs created by this agent +- `shopId` — all Confirmed POs for this specific shop + +**What happens:** +1. System finds all `Order` records matching the filter where `Invoice.Status = Draft` +2. One `LoadingSheet` created at `Planned` status +3. `LoadingSheetItem` rows populated from the active `OrderItem` lines of those POs +4. Each source PO recorded in `LoadingSheetOrder` linking table +5. Each item carries `FK_OrderItemId` pointing back to its source `OrderItem` + +**After population — editing:** +- Dispatcher can edit item quantities while the sheet is `Planned` or `Assigned` +- Every edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes a + fresh set — same versioning mechanism as Shop PO items +- Active rows (`IsActive = true`) = latest version; soft-deleted = history + +**Confirmation and stock movement:** +- Dispatcher assigns a ride + source warehouse (`Planned → Assigned`) +- Dispatcher confirms the sheet (`Assigned → Confirmed`) — items frozen +- Stock movement timing is an open question — see [loading_event.md](./loading_event.md) Open Question 1 + +See [loading_event.md](./loading_event.md) for the full loading sheet state machine and dispatch/receipt flow. + +--- + +## Stage 4+ — Delivery and Invoice Finalization + +**Status**: TBD — to be designed in a subsequent pass. + +--- + +## Open Questions + +1. **Who can confirm a Shop PO?** — the creating sales agent, a supervisor, or either? +2. **Can a Confirmed PO be reverted to Shop PO?** — no un-confirm path designed here. +3. **Cancellation path for a Shop PO or Confirmed PO?** — not addressed here. +4. **Exact adjustment payload for `POST /complete`** — the fields the office worker can edit + (returns, damage, discounts) are assumed but not fully specified. +5. **Does Stage 4 (loading sheet) depend on Stage 3 (invoice printed), or can they happen in + parallel?** — not decided here. diff --git a/Distribution/docs/refactor_docs/loading_event.md b/Distribution/docs/refactor_docs/loading_event.md new file mode 100644 index 0000000..afff95f --- /dev/null +++ b/Distribution/docs/refactor_docs/loading_event.md @@ -0,0 +1,289 @@ +# [Refactor] Loading Event — PO-Generated, Route-Free, No Ride Activation + +**Status**: Proposed (design only — no code changes yet) +**Refactors**: [../04_loading_event.md](../04_loading_event.md) (kept as-is, this is the target design) +**Depends On**: [ride_lifecycle.md](./ride_lifecycle.md), [order_invoice_lifecycle.md](./order_invoice_lifecycle.md) + +--- + +## Goal + +Bring `04_loading_event.md` in line with the other two refactors: + +1. **No more `Open -> Active`.** [ride_lifecycle.md](./ride_lifecycle.md) collapses `Ride.Status` to + `Open`/`Cancelled`/`Closed` — there is no `Active` stage left to auto-trigger. Confirming a + loading event no longer changes `Ride.Status` at all; the ride simply stays `Open` through + loading, delivery, and everything else, until it's explicitly `Closed` or `Cancelled`. + `LoadingSheet.Status` tracks loading progress entirely on its own. +2. **No more Route or `RideType`-based item population.** `04_loading_event.md`'s "Delivery rides + auto-populate from the ride's route" no longer applies — `Ride` no longer carries a route + ([ride_lifecycle.md](./ride_lifecycle.md)) and no longer distinguishes Delivery/Sale. Instead, per + [order_invoice_lifecycle.md](./order_invoice_lifecycle.md), a `LoadingSheet` is generated + automatically from a single PO's items the moment that PO is confirmed (`Invoice` Pending → + Draft) — before any ride exists. +3. **A `LoadingSheet` now starts ride-less.** It's created PO-scoped + (`FK_OrderId`), not ride-scoped, and gets attached to an actual `Ride` + source warehouse later, + via a new explicit action, once a vehicle/trip is planned for it. The rest of the + dispatch/receipt/discrepancy workflow is unchanged in shape — it just now runs per PO-generated + sheet instead of per ride-created sheet. + +--- + +## Background / Context + +Same physical mechanics as `04_loading_event.md`: a vehicle is a location with +`LocationType.Vehicle` in the stock ledger, and confirming receipt (or resolving a discrepancy) +creates the same two stock movements per item — debit source warehouse, credit vehicle. None of that +changes here. What changes is *when a `LoadingSheet` comes into existence* and *what it takes to +start the dispatch/receipt workflow on it*. + +--- + +## Data Model Changes + +### Updated Entity — `LoadingSheet` + +| Column | Type | Notes | +|---|---|---| +| Id | Guid | PK | +| ~~`FK_OrderId`~~ | — | **Removed.** Replaced by `LoadingSheetOrder` linking table. | +| **`FK_RideId`** | Guid | **Required. Set at population time** — dispatcher provides the ride when triggering populate. | +| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is now tracked per item, not per sheet — a single sheet can be fulfilled from multiple warehouses (see `LoadingSheetItem` below). | +| Status | LoadingSheetStatus | See state machine below. | +| **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. | +| **`PopulatedAt`** | DateTime | **New.** When population was triggered. | +| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step — ride is set at populate time. | +| ~~`AssignedBy`~~ | — | **Removed.** Same reason. | +| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (locked). | +| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. | +| DispatchConfirmedAt | DateTime? | Path B only — see Open Questions. | +| DispatchConfirmedBy | Guid? | Path B only. | +| ReceiptConfirmedAt | DateTime? | Path B only. | +| ReceiptConfirmedBy | Guid? | Path B only. | +| CreatedAt | DateTime | Unchanged. | + +### New Entity — `LoadingSheetOrder` (linking table) + +One loading sheet aggregates multiple Confirmed POs. This table records which POs were included. + +| Column | Type | Notes | +|---|---|---| +| FK_LoadingSheetId | Guid | FK → LoadingSheet | +| FK_OrderId | Guid | FK → Order (must be a Confirmed PO at population time) | + +### Updated Entity — `LoadingSheetItem` + +`LoadingSheetItem` gains versioning, a source reference, and a per-item warehouse: + +| Column | Type | Notes | +|---|---|---| +| Id | Guid | PK | +| FK_LoadingSheetId | Guid | FK → LoadingSheet | +| **`FK_WarehouseId`** | Guid | **New. Required.** The warehouse this specific item is loaded from. Different items on the same sheet can come from different warehouses. Set by dispatcher when editing items. | +| **`FK_OrderItemId`** | Guid? | **New.** Traces back to the source `OrderItem` from the Confirmed PO. Null for manually added lines. | +| FK_ItemId | Guid | FK → Item (product) | +| QuantityDispatched | decimal | Quantity to load from `FK_WarehouseId` | +| QuantityReceived | decimal? | Driver's received qty (Path B only) | +| QuantityResolved | decimal? | Supervisor's resolved qty after discrepancy (Path B only) | +| **`IsActive`** | bool | **Versioning.** Every edit soft-deletes current items (`IsActive = false`) and writes a fresh set. Active items = latest version; soft-deleted = history. | +| CreatedAt | DateTime | From Template | + +### Updated Enum — `LoadingSheetStatus` + +```csharp +public enum LoadingSheetStatus +{ + Planned, // populated by dispatcher (ride set, items editable/versioned) + Confirmed, // items locked — stock movement path TBD (see Open Questions) + DispatchConfirmed, // Path B only: warehouse confirms dispatch + ReceiptConfirmed, // Path B only: driver confirms receipt, stock moves + DiscrepancyFlagged, // Path B only: received qty mismatch + Resolved // Path B only: supervisor resolves discrepancy, stock moves +} +``` + +`Pending` from `04_loading_event.md` is replaced by `Planned` and `Confirmed`. `Assigned` is +removed — ride is now set at populate time, so there is no separate assign step. +`DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and `Resolved` are retained for +Path B — see Open Questions. + +--- + +## State Machine + +```text +Planned -> Confirmed -> [Path A: stock moves here] + \-> [Path B: DispatchConfirmed -> ReceiptConfirmed] + \-> DiscrepancyFlagged -> Resolved +``` + +| Transition | Trigger | What it does | +|---|---|---| +| (create) -> Planned | `POST /api/loading-sheets/populate` | Dispatcher provides filters (route/agent/shop) + `rideId`. System finds all matching Confirmed POs, creates one `LoadingSheet` with `FK_RideId` set, populates `LoadingSheetItem` rows from their `OrderItem` lines, records PO links in `LoadingSheetOrder`. `PopulatedBy`/`PopulatedAt` set. Items editable (versioned via `IsActive`). Dispatcher also sets `FK_WarehouseId` per item (which warehouse each item loads from). | +| Planned -> Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen (no further edits). Sets `ConfirmedAt`/`ConfirmedBy`. Stock movement path TBD (see Open Questions). | +| Confirmed -> DispatchConfirmed | `POST /{id}/confirm-dispatch` | **Path B only.** Warehouse manager confirms dispatch quantities. | +| DispatchConfirmed -> ReceiptConfirmed | `POST /{id}/confirm-receipt` | **Path B only.** Driver's received quantities all match. Stock movements created — one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. No ride status transition. | +| DispatchConfirmed -> DiscrepancyFlagged | `POST /{id}/confirm-receipt` | **Path B only.** Any line mismatch. No stock movements yet. | +| DiscrepancyFlagged -> Resolved | `POST /{id}/resolve` | **Path B only.** Supervisor enters `QuantityResolved` per line. Stock movements created using resolved quantities. | + +--- + +## Item Population + +Replaces `04_loading_event.md`'s "Item Population by Ride Type" section entirely. + +A dispatcher manually triggers population via `POST /api/loading-sheets/populate` providing: + +| Input | Notes | +|---|---| +| `rideId` | **Required.** The ride this loading sheet is for. Must be an `Open` ride. | +| `routeId` | Filter — all Confirmed POs for shops on this route | +| `agentId` | Filter — all Confirmed POs created by this agent | +| `shopId` | Filter — all Confirmed POs for this specific shop | + +At least one filter must be provided alongside `rideId`. The system finds all matching `Order` +records where items are locked (Confirmed PO), aggregates their active `OrderItem` lines into one +`LoadingSheet`, and records each PO in `LoadingSheetOrder`. + +`LoadingSheetItem.FK_OrderItemId` traces each line back to its source `OrderItem` for audit. + +After population, the dispatcher sets `FK_WarehouseId` on each item — specifying which warehouse +that item is loaded from. Different items can come from different warehouses. Items are editable +while `Planned` — every edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) +and writes a fresh set. Latest version = `IsActive = true` rows. + +--- + +## Stock Movements + +One stock movement pair per `LoadingSheetItem` (active rows only). Created exactly once. + +- **Debit**: `LoadingSheetItem.FK_WarehouseId` (the warehouse that item loads from) +- **Credit**: the vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`) +- **Type**: `Load` + +Since different items can have different `FK_WarehouseId` values, one loading sheet can produce +stock movements from multiple warehouses into one vehicle. + +**When** movements are created depends on the path chosen (see Open Questions): +- **Path A** — stock moves at `Confirmed` +- **Path B** — stock moves at `ReceiptConfirmed` or `Resolved` + +--- + +## API Endpoints + +Base route: `/api/loading-sheets` + +| Method | Route | Who | Description | +|---|---|---|---| +| POST | `/populate` | Dispatcher | Trigger population: `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Finds all matching Confirmed POs, creates one `LoadingSheet` in `Planned` status with `FK_RideId` set, populates items. | +| GET | `/` | Any | List sheets. Filter by `routeId`, `agentId`, `shopId`, `rideId`, `status`. Paginated. | +| GET | `/{id}` | Any | Get sheet detail. Active items only by default; `?history=true` includes soft-deleted versions. | +| PUT | `/{id}/items` | Dispatcher | Rewrite items while `Planned`. Each item must include `FK_WarehouseId`. Soft-deletes current rows (`IsActive = false`) and writes a fresh set. | +| POST | `/{id}/confirm` | Dispatcher | Lock the sheet. `Planned -> Confirmed`. Items frozen. | +| POST | `/{id}/confirm-dispatch` | Warehouse mgr | **Path B only.** `Confirmed -> DispatchConfirmed`. | +| PUT | `/{id}/receipt-items` | Driver | **Path B only.** Set `QuantityReceived` per line. | +| POST | `/{id}/confirm-receipt` | Driver | **Path B only.** `-> ReceiptConfirmed` or `DiscrepancyFlagged`. | +| POST | `/{id}/resolve` | Supervisor | **Path B only.** Enter `QuantityResolved` per line `-> Resolved`. | + +--- + +## Business Rules + +1. A `LoadingSheet` is created manually by a dispatcher via `POST /api/loading-sheets/populate` — + not automatically. `rideId` and at least one PO filter (routeId, agentId, or shopId) are required. +2. The ride (`FK_RideId`) is set at populate time — there is no separate assign-ride step. +3. Only Confirmed POs (locked items) are included in population — Shop POs are excluded. +4. One `LoadingSheet` aggregates all matching Confirmed POs — not one sheet per PO. +5. A ride may have more than one `LoadingSheet` — each progresses independently. +6. The target ride must be `Open` at populate time. +7. Each `LoadingSheetItem` carries its own `FK_WarehouseId` — items on the same sheet can come + from different warehouses. There is no sheet-level source warehouse. +8. Items are editable (versioned via `IsActive`) while `Planned` — frozen at `Confirmed`. +9. Every item edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes + a fresh set. Active rows = latest version; soft-deleted = history. +10. Stock movements are created exactly once — one debit per item warehouse, one credit to the + vehicle. Timing depends on path chosen (see Open Questions). +11. No `Ride.Status` transition is triggered at any point in this flow. +12. All list endpoints return paginated responses. +13. Vehicle capacity check is not enforced in this step. + +--- + +## Impact on Dependent Features + +1. **`ride_lifecycle.md`'s Impact item 2** (loading sheet auto-population without a route) and + **`order_invoice_lifecycle.md`'s Impact item 6** are addressed by this document: population now + comes from the PO directly, and the ride-attachment gap (`order_invoice_lifecycle.md`'s + [Open Question 7](../refactor_docs/order_invoice_lifecycle.md#open-questions)) is resolved by the + new `Planned -> Assigned` step and `POST /{id}/assign-ride` endpoint. +2. **Dispatcher role/permission is new** — `04_loading_event.md`'s permission table has no + "dispatcher" concept; `assign-ride` needs a new permission (see below), and who is authorized to + perform it (a route/trip planner role?) is not fully specified here. +3. **Multiple sheets per ride** changes how a driver/warehouse experiences dispatch — instead of one + confirm-dispatch per ride, there may now be several, one per assigned PO. UI/UX and any + "ride loading summary" view built against the old one-sheet-per-ride assumption needs rework — + not designed here. +4. **`order_invoice_lifecycle.md`'s Open Question 8** (does editing PO items post-confirm re-sync the + `LoadingSheet`?) is still open here too — this document doesn't resolve what happens to an + `Assigned` or later-stage sheet if its source PO's items change afterward. + +--- + +## Permissions to Seed + +| Code | Who | +|---|---| +| `loading.view` | All roles | +| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items, confirm sheet | +| `loading.dispatch` | Warehouse manager — Path B dispatch confirmation | +| `loading.receive` | Driver — Path B receipt confirmation | +| `loading.resolve` | Supervisor — Path B discrepancy resolution | + +--- + +## Fraud / Control Considerations + +Unchanged from `04_loading_event.md`: +- Discrepancies are immediately visible and block further progress. +- `QuantityResolved` is permanently stored per line for audit purposes. + +Additionally: `AssignedAt`/`AssignedBy` gives an audit trail for who committed a planned PO load to +a specific ride/vehicle, which didn't exist as a separate step before. + +--- + +## Open Questions + +1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A: + warehouse confirms and dispatches in one step) or at `ReceiptConfirmed`/`Resolved` (Path B: + driver receipt confirmation). Both paths are retained until this is decided. +2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager, or + either? Not specified here. +3. **What happens to a `Planned` sheet if it is never assigned to a ride?** — no cancellation or + timeout path is designed here. +4. Vehicle capacity check — still deferred. + +--- + +## Migration Notes (for when this is implemented) + +Not part of this docs-only pass, but captured for the eventual implementation step: + +- Add `FK_OrderId` (required), `AssignedAt`, `AssignedBy` to `Models/LoadingSheet.cs`; make + `FK_RideId` and `FK_SourceWarehouseId` nullable. +- Replace `LoadingSheetStatus.Pending` with `Planned` and `Assigned`; update all status checks in + `LoadingSheetService`/`LoadingSheetRepository` accordingly. +- Remove `POST /api/loading-sheets` (manual create); wire automatic `LoadingSheet` creation into the + PO-confirm action from [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)'s migration + notes. +- Add `POST /{id}/assign-ride` endpoint/service method, validating the target ride's `Status == Open` + (per [ride_lifecycle.md](./ride_lifecycle.md)'s narrowed `RideStatus`). +- Remove the `confirm-receipt`/`resolve` code paths' calls into ride-activation logic — there is no + `Ride.Status` transition to trigger anymore. +- Remove the ride-route-based item population path entirely (`Order.Store.FK_RouteId` aggregation); + replace with direct `FK_OrderId`-based population at sheet-creation time. +- Seed the new `loading.assign` permission. +- Resolve Open Questions 1 and 4 before finalizing the `assign-ride` endpoint's authorization rules + and the one-sheet-per-PO assumption, respectively. diff --git a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md new file mode 100644 index 0000000..7192480 --- /dev/null +++ b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md @@ -0,0 +1,264 @@ +# [Refactor] Order/Invoice Lifecycle — Unified PO → Invoice Pipeline + +**Status**: Proposed (design only — no code changes yet) +**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) — that refactor removes +`RideType`/`Route`/`Status` from `Ride`; this document does not attempt to resolve most of its open +questions, and this document's design does not depend on it landing first. One exception: this +document's automatic `LoadingSheet` creation on PO confirm (see below) partially addresses +`ride_lifecycle.md`'s "loading sheet auto-population" gap — the `LoadingSheet` is now generated from +the PO/Invoice directly, not from a Route. How that `LoadingSheet` later attaches to an actual Ride +(vehicle) is resolved in [loading_event.md](./loading_event.md). + +--- + +## Goal + +Replace the Delivery-vs-Sale ride-type split with a single unified pipeline. The PO and Invoice are +two separate but linked concerns — the PO tracks what the shop ordered; the Invoice tracks the +billing document that goes with the delivery. + +### PO lifecycle (on `Order`) + +1. **Shop PO** — PO created by sales agent, line items freely editable (versioned via `IsActive`). +2. **Confirmed PO** — office user confirms the PO, locking its items. Invoice creation depends on + the system's active mode (see [Invoice Creation Modes](#invoice-creation-modes) below). + +### Invoice lifecycle (on `Invoice`) + +An Invoice is **not** auto-created when a PO is created. It is created by an office user action — +either combined with PO confirmation (Mode A) or as a separate step after confirmation (Mode B). +Once created, the Invoice always moves through these states: + +1. **Draft** — Invoice created, ready to be printed. +2. **Printed** — Invoice physically printed and handed to the agent for the field. The agent makes + handwritten edits on the paper copy during the ride. +3. **Finalized** — after the ride or day-end, the agent returns the printed invoice. The office + worker enters any edits the agent made into the system and submits — locking the invoice as the + final billing record. + +--- + +## Data Model Changes + +### Reshaped Entity — `Order` (business term: "PO") + +The existing `Order` entity ([Order.cs](../../Models)) is reshaped into this role, keeping its class +name as `Order` in code. + +> **Naming note**: `Distribution/Models/PurchaseOrder.cs` and `Distribution/Features/PurchaseOrder/` +> already exist in the codebase — that's the *supplier-facing* procurement flow (the distributor +> buying from its own suppliers), completely unrelated to this store-facing PO. To avoid confusion, +> this document deliberately keeps the class name `Order`, using "PO" only as the business-facing +> term for what the store ordered. Do **not** name the reshaped entity/class `PurchaseOrder`. + +``` +Order ("PO") +├── Id (Guid) — PK, from Template +├── FK_StoreId (Guid) — required, unchanged +├── FK_SaleId (Guid?) — now OPTIONAL (was required). See Impact section — a PO can now +│ exist before any Sale/payment event completes. +├── InvoiceId (Guid?) — NOT a FK constraint. Set when the Invoice is created for this PO +│ (Mode A or B). Null until then. Stored for lookup convenience — +│ no referential enforcement at DB level. +├── TotalAmount (decimal) — unchanged, though only authoritative once the linked Invoice is +│ Finalized (see below) +├── Status (OrderStatus) — unchanged: Pending, PartiallyFulfilled, Fulfilled, Cancelled. +│ This tracks fulfillment progress, a separate axis from the new +│ InvoiceStatus below — not merged in this pass. +├── FulfilledAt (DateTime?) — unchanged +├── IsActive, CreatedAt — from Template +``` + +`OrderItem` (the PO's line items) is unchanged in shape (`FK_ItemId`, `Quantity`, +`QuantityFulfilled`, `UnitPrice`) — editability is now gated by the linked `Invoice`'s status (see +below), not by `Order.Status` directly. + +### New Entity — `Invoice` + +``` +Invoice +├── Id (Guid) — PK, from Template +├── FK_OrderId (Guid) — required. One PO → one Invoice (1:1). +├── Status (InvoiceStatus) — Draft, Printed, Finalized +├── CreatedBy (Guid) — UserId of office user who created the invoice +├── PrintedAt (DateTime?) — set on Draft → Printed +├── PrintedBy (Guid?) — UserId of whoever triggered the print +├── FinalizedAt (DateTime?) — set on Printed → Finalized +├── FinalizedBy (Guid?) — UserId of office worker who entered agent edits and submitted +├── TotalAmount (decimal) — reflects current line items through Draft/Printed; becomes the +│ authoritative locked amount once Finalized +├── IsActive, CreatedAt — from Template +``` + +### New Enum — `InvoiceStatus` + +```csharp +public enum InvoiceStatus { Draft, Printed, Finalized } // Finalized is the terminal state +``` + +### Invoice Creation Modes + +In a given system instance, **exactly one** of the following modes is active. The active mode is +controlled by a system-level configuration flag (`InvoiceCreationMode: "A" | "B"`). Both modes +cannot be active at the same time. + +**Mode A — Confirm PO + Create Invoice (combined action)** +- Office user presses "Confirm PO & Create Invoice" +- Single endpoint: `POST /api/orders/{id}/confirm-and-create-invoice` +- Confirms the PO (locks `OrderItem` lines) and creates the Invoice at `Draft` in one action. + +**Mode B — Create Invoice from Confirmed POs (separate action)** +- PO confirmation is a separate step: `POST /api/orders/{id}/confirm` +- Office user then navigates to invoice creation, filters confirmed POs by agent ID, route ID, or + shop ID to find the right PO, and creates the invoice: `POST /api/invoices { orderId }` +- Invoice created at `Draft`. + +### `LoadingSheet` — populated from Confirmed POs, separate flow + +Loading sheets are populated from Confirmed POs as a separate dispatcher-triggered flow — not +auto-triggered at PO confirm or invoice create time. One loading sheet aggregates multiple Confirmed +POs based on route/agent/shop filters. See [loading_event.md](./loading_event.md) and +[dispatch_pipeline.md](./dispatch_pipeline.md) for the full loading sheet lifecycle. + +--- + +## State Machine + +### PO (`Order`) transitions + +``` +(create) → Shop PO → Confirmed PO +``` + +| Transition | Trigger | What it does | +|---|---|---| +| (create) → Shop PO | `POST /api/orders` | PO created. `OrderItem` lines editable — every edit soft-deletes current rows (`IsActive = false`) and writes a fresh set. Active rows = latest version. | +| Shop PO → Confirmed PO | **Mode A:** `POST /api/orders/{id}/confirm-and-create-invoice` | Confirms PO and creates Invoice at `Draft` in one atomic action. `OrderItem` lines locked. `Order.InvoiceId` set to the new Invoice's Id. | +| Shop PO → Confirmed PO | **Mode B:** `POST /api/orders/{id}/confirm` | Confirms PO only. `OrderItem` lines locked. `Order.InvoiceId` remains null until invoice is created separately. | + +### Invoice transitions + +``` +Draft → Printed → Finalized +``` + +| Transition | Trigger | What it does | +|---|---|---| +| (create) → Draft | **Mode A:** combined with PO confirm above | Invoice created at `Draft`. | +| (create) → Draft | **Mode B:** `POST /api/invoices` `{ orderId }` | Office user selects a Confirmed PO (filtered by agentId / routeId / shopId) and creates the Invoice at `Draft`. `Order.InvoiceId` set to the new Invoice's Id. | +| Draft → Printed | `POST /api/invoices/{id}/print` | Invoice is physically printed. `PrintedAt`/`PrintedBy` set. Agent takes the printed copy to field and makes handwritten edits during the ride. | +| Printed → Finalized | `POST /api/invoices/{id}/complete` | After ride/day-end, agent returns printed invoice. Office worker enters any edits the agent made (quantity adjustments, returns, damage, discounts) and submits. `TotalAmount` locked. `FinalizedAt`/`FinalizedBy` set. | + +--- + +## Business Rules + +1. `OrderItem` editing is allowed **only** while the PO is a Shop PO (before confirmation). Every + edit soft-deletes all current `OrderItem` rows (`IsActive = false`) and creates a fresh set — + active rows are the latest version, soft-deleted rows are history. Once confirmed, items are + locked permanently. +2. Invoice is **not** auto-created at PO creation — it is always a deliberate office user action. +3. Exactly one Invoice Creation Mode (A or B) is active per system instance — controlled by a + system-level configuration flag (`InvoiceCreationMode`). Both modes cannot be active at the + same time. +4. Mode A only: a PO cannot be confirmed without simultaneously creating its invoice — the two + actions are atomic. +5. Mode B only: a PO can be confirmed without an invoice existing; the invoice is created later as + a separate action by the office user. +6. An Invoice cannot skip `Printed` — `Draft → Finalized` directly is not a valid transition. +7. `Invoice.TotalAmount` is only authoritative once `Finalized`; through `Draft`/`Printed` it + reflects the current line items. +8. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is a + separate axis from `Invoice.Status` — not merged in this pass. +9. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded. +10. Loading sheet population is a separate dispatcher-triggered flow — not triggered by PO + confirmation or invoice creation. See [dispatch_pipeline.md](./dispatch_pipeline.md). + +--- + +## Impact on Dependent Features + +Reshaping `Order` and introducing `Invoice` touches several existing behaviors. Each is flagged here +as **known-affected, deferred follow-up work**, not solved in this pass: + +1. **`SaleService.CreateAsync`** — currently the only path that creates an `Order`, and it always + sets a non-null `FK_SaleId`. With `FK_SaleId` now optional, a new PO-creation path (not + necessarily originating from a `Sale`) needs to exist for the take-order-now/deliver-later + scenario. Any other code that assumes `Order.Sale` is always non-null also needs auditing — + not enumerated exhaustively here. +2. **`Payment` attachment** — per `06_sale_ride.md`, `Payment` currently attaches to `Sale`. Once + `Invoice` exists as the real billing document, `Payment` more naturally attaches to the `Invoice` + instead. Flagged as a design question for [19_finance_accounting.md](../19_finance_accounting.md) + to reconcile — not decided here. +3. **`19_finance_accounting.md`'s proposed Invoice status** (`Issued, PartiallyPaid, Paid`) needs + reconciling with this document's `Pending/Draft/Finalized`. These are likely two different axes + on the same `Invoice` — document lifecycle status (this doc) vs. payment-collection status (the + finance doc) — but that reconciliation is not attempted here. +4. **`DeliveryTask.CustomerConfirmed`** (boolean, from `05_delivery_ride.md`) overlaps conceptually + with the new Pending → Draft transition (both represent "the customer confirmed this"). Whether + they merge into one confirmation event, or stay deliberately separate (delivery confirmation vs. + invoice sign-off, which could happen at different times), is not decided here. +5. **`05_delivery_ride.md` and `06_sale_ride.md`** both go stale the moment this design ships and + need their own rewrite/retirement — tracked as follow-up, not attempted in this pass. +6. **`LoadingSheet` generation moves from route-scoped to PO-scoped.** `LoadingSheetRepository`'s + existing `GetAggregatedOrderItemsByRouteIdAsync` (aggregating by `Order.Store.FK_RouteId`) is + superseded by generating one `LoadingSheet` per PO at confirm time. This directly answers part of + `ride_lifecycle.md`'s Impact item 2 (loading sheet auto-population without a route) — but raises a + new gap: nothing in this design says how/when a PO-scoped `LoadingSheet` gets matched to an actual + Ride/vehicle for physical loading, since `ride_lifecycle.md` also removed `FK_SourceWarehouseId` + and any route/store linkage from `Ride`. Flagged as [Open Question 7](#open-questions), not solved + here. + +None of the above are implemented or redesigned by this document. They're called out so the gap is +visible and intentional rather than discovered later as a silent regression. + +--- + +## Open Questions + +1. ~~**Signature/OTP on PO confirmation**~~ — **Resolved:** dropped. Plain action, no OTP. +2. ~~**One Invoice per PO or multiple?**~~ — **Resolved:** 1:1 for v1. +3. **Should `Order.Status` and `Invoice.Status` eventually merge?** — kept separate in this pass; + not decided. +4. **How does `Payment` attach to `Invoice`?** — see Impact item 2. Not designed here. +5. **Cancellation/void path for a PO or Invoice?** — not addressed here. +6. **What edits can the agent make on the printed invoice?** — quantity adjustments, returns, + damage, and discounts are assumed but the exact adjustment payload for `POST /complete` is not + specified here. +7. ~~**How does a LoadingSheet get matched to a Ride?**~~ — **Resolved in + [loading_event.md](./loading_event.md):** `POST /{id}/assign-ride`. +8. ~~**Does editing PO items after confirm re-sync the LoadingSheet?**~~ — **Resolved:** items lock + at confirm; no re-sync needed. + +--- + +## Migration Notes (for when this is implemented) + +Not part of this docs-only pass, but captured for the eventual implementation step: + +- Make `Order.FK_SaleId` nullable in `Models/Order.cs`; audit all code paths that assume it's + non-null. +- Add `Order.InvoiceId` (Guid?, no FK constraint) to `Models/Order.cs` — configure explicitly in + Fluent API to ensure no referential constraint is generated. Set it in the same transaction as + Invoice creation (both Mode A and Mode B). +- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `CreatedBy`, `PrintedAt`, `PrintedBy`, + `FinalizedAt`, `FinalizedBy`, `TotalAmount`, plus `Template` base fields) and the `InvoiceStatus` + enum (`Draft`, `Printed`, `Finalized`). +- New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`, + `InvoiceRepository`, `InvoiceDTOs`) — do not place under `Features/PurchaseOrder/`. +- Add a system configuration flag `InvoiceCreationMode` (`"A"` or `"B"`) — read at startup; gate + the Mode A and Mode B endpoints accordingly so only the active mode's endpoints are reachable. +- **Mode A endpoint:** `POST /api/orders/{id}/confirm-and-create-invoice` — atomic: locks + `OrderItem` rows and creates `Invoice` at `Draft` in one transaction. +- **Mode B endpoints:** `POST /api/orders/{id}/confirm` (locks items only) + `POST /api/invoices` + `{ orderId }` (creates Invoice at `Draft` from a Confirmed PO). Add `GET /api/orders` filter + params `agentId`, `routeId`, `shopId` to support the office user's PO selection UI. +- `POST /api/invoices/{id}/print` — sets `PrintedAt`/`PrintedBy`, transitions to `Printed`. +- `POST /api/invoices/{id}/complete` — accepts adjustment payload (quantities, returns, damage, + discounts); locks `TotalAmount`; sets `FinalizedAt`/`FinalizedBy`. Resolve Open Question 6 + before specifying the exact payload shape. +- `LoadingSheet` population is a separate flow — see [dispatch_pipeline.md](./dispatch_pipeline.md). +- A new PO-creation path independent of `SaleService.CreateAsync` is needed for the + take-order-now/deliver-later scenario (Impact item 1) — not specified here.