diff --git a/Distribution/docs/refactor_docs/dispatch_pipeline.md b/Distribution/docs/refactor_docs/dispatch_pipeline.md index af2c31f..7f555a5 100644 --- a/Distribution/docs/refactor_docs/dispatch_pipeline.md +++ b/Distribution/docs/refactor_docs/dispatch_pipeline.md @@ -45,6 +45,8 @@ quantities. 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. +- A Shop PO can be cancelled (`Order.Status → Cancelled`). No Invoice exists at this point so no + cascade required. **API:** | Method | Route | Description | @@ -52,6 +54,7 @@ quantities. | 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. | +| POST | `/api/orders/{id}/cancel` | Cancel a Shop PO. | --- @@ -76,6 +79,9 @@ mode — controlled by the system-level `InvoiceCreationMode` config flag. - Confirmation requires no signature or OTP. - `OrderItem` lines are permanently locked after confirmation. - `Order.Status` (fulfillment tracking) is untouched — separate axis. +- A Confirmed PO can be cancelled even after confirmation (`Order.Status → Cancelled`). If an + Invoice was already created, cascade behaviour (void Invoice, impact on loading sheet items) is + unresolved — see Open Questions. --- @@ -150,8 +156,13 @@ enters any edits the agent made into the system and submits. 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. +3. **Cancellation cascade** — both Shop PO and Confirmed PO can be cancelled. If cancelling a + Confirmed PO that already has a linked Invoice: should the Invoice be voided automatically? + If the PO's items were already pulled into a loading sheet: what happens to those loading sheet + items? Cascade rules needed before implementation. +4. **Exact adjustment payload for `POST /api/invoices/{id}/finalize`** — the fields the office + worker can edit (returns, damage, discounts) are assumed but not fully specified. "Free issues" + handling (items given to the shop at no charge) is a known case needing its own payload field(s). +5. **Stage 3 and Stage 4 ordering** — Invoice printing (Stage 3) and loading sheet population + (Stage 4) are independent flows. They are not strictly sequential — both depend on a Confirmed + PO existing but neither requires the other to complete first. diff --git a/Distribution/docs/refactor_docs/loading_event.md b/Distribution/docs/refactor_docs/loading_event.md index bf07e78..72aaaf0 100644 --- a/Distribution/docs/refactor_docs/loading_event.md +++ b/Distribution/docs/refactor_docs/loading_event.md @@ -13,7 +13,7 @@ Replace the previous loading event design with a dispatcher-driven, multi-PO mod 1. **No more auto-creation.** Loading sheets are no longer generated automatically when a PO is confirmed. A dispatcher manually triggers population. 2. **No more `Open → Active`.** `Ride.Status` collapses to `Open`/`Cancelled`/`Closed` — no - `Active` stage. Loading sheet confirmation no longer changes `Ride.Status` at all. + `Active` stage. Loading sheet confirmation no longer changes `Ride.Status`. 3. **One sheet aggregates multiple POs.** A single loading sheet is created from all Confirmed POs matching a dispatcher's filter (route/agent/shop). No longer one sheet per PO. 4. **Ride set at populate time.** The ride is provided when the dispatcher triggers population — @@ -22,15 +22,17 @@ Replace the previous loading event design with a dispatcher-driven, multi-PO mod loaded from different warehouses. Stock movements are generated per item's warehouse. 6. **Item versioning.** Loading sheet items are versioned the same way as Shop PO items — every edit soft-deletes current rows (`IsActive = false`) and writes a fresh set. +7. **Stock moves at confirmation.** When the dispatcher confirms the sheet, stock moves from each + item's warehouse to the vehicle. No driver receipt confirmation step. --- ## Background / Context Same physical mechanics as `04_loading_event.md`: a vehicle is a location with -`LocationType.Vehicle` in the stock ledger. Confirming receipt (or resolving a discrepancy) -creates stock movements — debit source warehouse(s), credit vehicle. What changes is *when and how -a `LoadingSheet` comes into existence*, *which POs it covers*, and *how warehouse selection works*. +`LocationType.Vehicle` in the stock ledger. Confirming the loading sheet creates stock movements — +debit source warehouse(s), credit vehicle. What changes is *when and how a `LoadingSheet` comes +into existence*, *which POs it covers*, and *how warehouse selection works*. --- @@ -43,18 +45,12 @@ a `LoadingSheet` comes into existence*, *which POs it covers*, and *how warehous | Id | Guid | PK | | ~~`FK_OrderId`~~ | — | **Removed.** One sheet now aggregates multiple POs — 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 tracked per item, not per sheet — a single sheet can be fulfilled from multiple warehouses. | -| Status | LoadingSheetStatus | See state machine below. | +| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is tracked per item, not per sheet. | +| Status | LoadingSheetStatus | `Planned` or `Confirmed`. | | **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. | | **`PopulatedAt`** | DateTime | **New.** When population was triggered. | -| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step. | -| ~~`AssignedBy`~~ | — | **Removed.** Same reason. | -| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (items 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. | +| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (items locked, stock moves). | +| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. TBD — see Open Questions. | | CreatedAt | DateTime | From Template. | ### New Entity — `LoadingSheetOrder` (linking table) @@ -76,8 +72,6 @@ One loading sheet aggregates multiple Confirmed POs. This table records which PO | **`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 | @@ -86,36 +80,30 @@ One loading sheet aggregates multiple Confirmed POs. This table records which PO ```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 + Planned, // populated by dispatcher — ride set, items editable and versioned + Confirmed // items locked, stock movements created } ``` -`Pending` from `04_loading_event.md` is replaced by `Planned` and `Confirmed`. `Assigned` is -removed — ride is set at populate time, no separate assign step. +`Pending`, `Assigned`, `DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and +`Resolved` from `04_loading_event.md` are all removed. Stock moves at `Confirmed` — no driver +receipt confirmation step in this flow. --- ## State Machine ```text -Planned -> Confirmed -> [Path A: stock moves here] - \-> [Path B: DispatchConfirmed -> ReceiptConfirmed] - \-> DiscrepancyFlagged -> Resolved +Planned → Confirmed (stock moves) ``` | Transition | Trigger | What it does | |---|---|---| | (create) → Planned | `POST /api/loading-sheets/populate` | Dispatcher provides `rideId` + filters. System finds all matching Confirmed POs, creates one `LoadingSheet` with `FK_RideId` set, populates `LoadingSheetItem` rows from active `OrderItem` lines, records PO links in `LoadingSheetOrder`. `PopulatedBy`/`PopulatedAt` set. Items editable (versioned via `IsActive`). Dispatcher sets `FK_WarehouseId` per item. | -| Planned → Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen. 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. | +| Planned → Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen. Stock movements created: one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. Sets `ConfirmedAt`/`ConfirmedBy`. | + +A `Planned` sheet stays as `Planned` indefinitely until the dispatcher confirms it — there is no +timeout or auto-cancellation. --- @@ -145,18 +133,14 @@ and writes a fresh set. Latest version = `IsActive = true` rows. ## Stock Movements -One stock movement pair per `LoadingSheetItem` (active rows only). Created exactly once. +Created at `Confirmed`. One pair per active `LoadingSheetItem`: - **Debit**: `LoadingSheetItem.FK_WarehouseId` (the warehouse that item loads from) -- **Credit**: the vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`) +- **Credit**: 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` +stock movements from multiple warehouses into one vehicle in a single confirmation action. --- @@ -166,15 +150,11 @@ 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. Creates one `LoadingSheet` at `Planned` with `FK_RideId` set. | +| POST | `/populate` | Dispatcher | `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Creates one `LoadingSheet` at `Planned` with `FK_RideId` set. | | GET | `/` | Any | List sheets. Filter by `routeId`, `agentId`, `shopId`, `rideId`, `status`. Paginated. `routeId` resolves via `Store.FK_RouteId → Order.FK_StoreId → LoadingSheetOrder.FK_OrderId` server-side. | | 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`. | +| POST | `/{id}/confirm` | TBD | Lock the sheet. `Planned → Confirmed`. Items frozen. Stock movements created. | --- @@ -192,11 +172,12 @@ Base route: `/api/loading-sheets` 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. +10. Stock movements are created at `Confirmed` — one debit per item's warehouse, one credit to the + vehicle — all in one transaction. +11. A `Planned` sheet stays `Planned` until confirmed — no timeout or auto-cancellation. +12. No `Ride.Status` transition is triggered at any point in this flow. +13. All list endpoints return paginated responses. +14. Vehicle capacity check is not enforced in this step. --- @@ -205,50 +186,49 @@ Base route: `/api/loading-sheets` | 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 | +| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items | +| **`loading.confirm`** | **New.** TBD — who can confirm the sheet (see Open Questions) | --- ## Fraud / Control Considerations -- Discrepancies are immediately visible and block further progress. -- `QuantityResolved` is permanently stored per line for audit purposes. - `PopulatedBy`/`PopulatedAt` gives an audit trail for who triggered population and when. +- `ConfirmedBy`/`ConfirmedAt` records who committed stock movements. - `FK_OrderItemId` on each item traces the exact source PO line for full audit. +- `IsActive = false` rows preserve the full edit history of the sheet before confirmation. --- ## Open Questions -1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A) - or at `ReceiptConfirmed`/`Resolved` (Path B). Both paths retained until decided. -2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager, - or either? -3. **What happens to a `Planned` sheet if it is never confirmed?** — no cancellation or timeout - path is designed here. -4. Vehicle capacity check — still deferred. +1. **Who confirms the loading sheet?** — dispatcher, warehouse manager, or either? TBD. Controls + the `loading.confirm` permission assignment. +2. **What happens to a `Planned` sheet if its source PO is cancelled?** — if a Confirmed PO is + cancelled after a loading sheet has been populated from it, the sheet's items sourced from that + PO become invalid. No cascade behaviour designed here. +3. Vehicle capacity check — still deferred. --- ## Migration Notes (for when this is implemented) -- Remove `FK_OrderId` and `FK_SourceWarehouseId` from `Models/LoadingSheet.cs`. Make `FK_RideId` - required (non-nullable). Remove `AssignedAt`/`AssignedBy`. Add `PopulatedBy`, `PopulatedAt`, +- Remove `FK_OrderId`, `FK_SourceWarehouseId`, `AssignedAt`, `AssignedBy`, + `DispatchConfirmedAt`, `DispatchConfirmedBy`, `ReceiptConfirmedAt`, `ReceiptConfirmedBy` from + `Models/LoadingSheet.cs`. Make `FK_RideId` required. Add `PopulatedBy`, `PopulatedAt`, `ConfirmedAt`, `ConfirmedBy`. - Add `Models/LoadingSheetOrder.cs` (`FK_LoadingSheetId`, `FK_OrderId`) with composite PK. - Add `FK_WarehouseId` (required), `FK_OrderItemId` (nullable), `IsActive` to - `Models/LoadingSheetItem.cs`. -- Replace `LoadingSheetStatus.Pending` and `Assigned` with `Planned` and `Confirmed`; update all - status checks in `LoadingSheetService`/`LoadingSheetRepository`. -- Remove `POST /api/loading-sheets` (old manual create) and `POST /{id}/assign-ride`; add - `POST /api/loading-sheets/populate` and `POST /{id}/confirm`. + `Models/LoadingSheetItem.cs`. Remove `QuantityReceived`, `QuantityResolved`. +- Replace `LoadingSheetStatus` entirely with `Planned` and `Confirmed`; remove all other values + and update all status checks in `LoadingSheetService`/`LoadingSheetRepository`. +- Remove `POST /api/loading-sheets` (old manual create), `POST /{id}/assign-ride`, + `POST /{id}/confirm-dispatch`, `PUT /{id}/receipt-items`, `POST /{id}/confirm-receipt`, + `POST /{id}/resolve`; add `POST /api/loading-sheets/populate` and `POST /{id}/confirm`. +- Wire stock movements into `POST /{id}/confirm` — one debit per `LoadingSheetItem.FK_WarehouseId`, + one credit to the ride's vehicle, in a single transaction. - Remove ride-route-based item population (`GetAggregatedOrderItemsByRouteIdAsync`); replace with - filter-based multi-PO aggregation in `LoadingSheetService`. -- Remove all calls to ride-activation logic from `confirm-receipt`/`resolve` — no `Ride.Status` - transition exists anymore. -- Seed the new `loading.populate` permission. -- Resolve Open Question 1 (stock movement timing) before implementing `POST /{id}/confirm` and - `POST /{id}/confirm-receipt` stock movement logic. + filter-based multi-PO aggregation. +- Remove all calls to ride-activation logic — no `Ride.Status` transition exists anymore. +- Seed `loading.populate` and `loading.confirm` permissions. +- Resolve Open Question 1 (who confirms) before assigning `loading.confirm` permission. diff --git a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md index 7af78cc..edd44f3 100644 --- a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md +++ b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md @@ -168,6 +168,11 @@ Draft → Printed → Finalized 10. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded. 11. Loading sheet population is a separate dispatcher-triggered flow — not triggered by PO confirmation or invoice creation. See [dispatch_pipeline.md](./dispatch_pipeline.md). +12. **A Shop PO (pre-confirmation) can be cancelled** — `Order.Status → Cancelled`. No Invoice + exists at this stage so no cascade required. +13. **A Confirmed PO can also be cancelled** even after confirmation — `Order.Status → Cancelled`. + If an Invoice was already created for this PO, cascade behaviour (void/cancel the Invoice) and + behaviour for any loading sheet that included this PO are open questions — see Open Questions. --- @@ -191,9 +196,14 @@ Draft → Printed → Finalized 2. ~~**One Invoice per PO or multiple?**~~ — **Resolved:** 1:1 for v1. 3. **Should `Order.Status` and `Invoice.Status` eventually merge?** — kept separate; not decided. 4. **How does `Payment` attach to `Invoice`?** — see Impact item 2. Not designed here. -5. **Cancellation/void path for a Shop PO, Confirmed PO, or Invoice?** — not addressed here. +5. **Cancellation cascade — Invoice and LoadingSheet** — a Shop PO can be cancelled (no Invoice + exists yet, no cascade). A Confirmed PO can be cancelled even after confirmation. If the PO + 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 here. + 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. --- @@ -219,3 +229,5 @@ Draft → Printed → Finalized `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 — not specified here. +- Add `POST /api/orders/{id}/cancel` — cancels a Shop PO or Confirmed PO. Resolve Open Question 5 + (cascade to Invoice and LoadingSheet) before implementing for the Confirmed PO case.