Revamp dispatch pipeline and PO cancellation docs

Updated documentation to clarify and redesign dispatch pipeline, loading event, and order/invoice lifecycle. Added PO cancellation flow for both Shop and Confirmed POs, introduced new API endpoint for cancellation, and simplified loading sheet model and endpoints. Added new permissions for loading operations, improved auditability, and detailed migration steps. Highlighted open questions on cancellation cascades, permissions, and vehicle capacity checks for future resolution.
This commit is contained in:
Kalana B. Thilakarathna
2026-09-16 11:30:21 +05:30
parent 541f59b15d
commit 6c40421b2d
3 changed files with 86 additions and 83 deletions
@@ -45,6 +45,8 @@ quantities.
a fresh set. `IsActive = true` = latest version; `IsActive = false` = history. a fresh set. `IsActive = true` = latest version; `IsActive = false` = history.
- Editing allowed only while Shop PO (before confirmation). - Editing allowed only while Shop PO (before confirmation).
- No Invoice exists yet at this stage. - 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:** **API:**
| Method | Route | Description | | Method | Route | Description |
@@ -52,6 +54,7 @@ quantities.
| POST | `/api/orders` | Create Shop PO for a store. | | POST | `/api/orders` | Create Shop PO for a store. |
| PUT | `/api/orders/{id}/items` | Rewrite line items (versioned). | | PUT | `/api/orders/{id}/items` | Rewrite line items (versioned). |
| GET | `/api/orders/{id}` | Get PO with current active items. | | 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. - Confirmation requires no signature or OTP.
- `OrderItem` lines are permanently locked after confirmation. - `OrderItem` lines are permanently locked after confirmation.
- `Order.Status` (fulfillment tracking) is untouched — separate axis. - `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? 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. 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. 3. **Cancellation cascade** — both Shop PO and Confirmed PO can be cancelled. If cancelling a
4. **Exact adjustment payload for `POST /complete`** — the fields the office worker can edit Confirmed PO that already has a linked Invoice: should the Invoice be voided automatically?
(returns, damage, discounts) are assumed but not fully specified. If the PO's items were already pulled into a loading sheet: what happens to those loading sheet
5. **Does Stage 4 (loading sheet) depend on Stage 3 (invoice printed), or can they happen in items? Cascade rules needed before implementation.
parallel?** — not decided here. 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.
@@ -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 1. **No more auto-creation.** Loading sheets are no longer generated automatically when a PO is
confirmed. A dispatcher manually triggers population. confirmed. A dispatcher manually triggers population.
2. **No more `Open → Active`.** `Ride.Status` collapses to `Open`/`Cancelled`/`Closed` — no 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 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. 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 — 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. 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 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. 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 ## Background / Context
Same physical mechanics as `04_loading_event.md`: a vehicle is a location with 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) `LocationType.Vehicle` in the stock ledger. Confirming the loading sheet creates stock movements —
creates stock movements — debit source warehouse(s), credit vehicle. What changes is *when and how debit source warehouse(s), credit vehicle. What changes is *when and how a `LoadingSheet` comes
a `LoadingSheet` comes into existence*, *which POs it covers*, and *how warehouse selection works*. 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 | | Id | Guid | PK |
| ~~`FK_OrderId`~~ | — | **Removed.** One sheet now aggregates multiple POs — replaced by `LoadingSheetOrder` linking table. | | ~~`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_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. | | ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is tracked per item, not per sheet. |
| Status | LoadingSheetStatus | See state machine below. | | Status | LoadingSheetStatus | `Planned` or `Confirmed`. |
| **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. | | **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. |
| **`PopulatedAt`** | DateTime | **New.** When population was triggered. | | **`PopulatedAt`** | DateTime | **New.** When population was triggered. |
| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step. | | ConfirmedAt | DateTime? | Set when loading sheet is confirmed (items locked, stock moves). |
| ~~`AssignedBy`~~ | — | **Removed.** Same reason. | | ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. TBD — see Open Questions. |
| 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. |
| CreatedAt | DateTime | From Template. | | CreatedAt | DateTime | From Template. |
### New Entity — `LoadingSheetOrder` (linking table) ### 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_OrderItemId`** | Guid? | **New.** Traces back to the source `OrderItem` from the Confirmed PO. Null for manually added lines. |
| FK_ItemId | Guid | FK → Item (product) | | FK_ItemId | Guid | FK → Item (product) |
| QuantityDispatched | decimal | Quantity to load from `FK_WarehouseId` | | 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. | | **`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 | | CreatedAt | DateTime | From Template |
@@ -86,36 +80,30 @@ One loading sheet aggregates multiple Confirmed POs. This table records which PO
```csharp ```csharp
public enum LoadingSheetStatus public enum LoadingSheetStatus
{ {
Planned, // populated by dispatcher (ride set, items editable/versioned) Planned, // populated by dispatcher — ride set, items editable and versioned
Confirmed, // items locked — stock movement path TBD (see Open Questions) Confirmed // items locked, stock movements created
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 `Pending`, `Assigned`, `DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and
removed — ride is set at populate time, no separate assign step. `Resolved` from `04_loading_event.md` are all removed. Stock moves at `Confirmed` — no driver
receipt confirmation step in this flow.
--- ---
## State Machine ## State Machine
```text ```text
Planned -> Confirmed -> [Path A: stock moves here] Planned → Confirmed (stock moves)
\-> [Path B: DispatchConfirmed -> ReceiptConfirmed]
\-> DiscrepancyFlagged -> Resolved
``` ```
| Transition | Trigger | What it does | | 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. | | (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). | | 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`. |
| 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. | A `Planned` sheet stays as `Planned` indefinitely until the dispatcher confirms it — there is no
| DispatchConfirmed → DiscrepancyFlagged | `POST /{id}/confirm-receipt` | **Path B only.** Any line mismatch. No stock movements yet. | timeout or auto-cancellation.
| DiscrepancyFlagged → Resolved | `POST /{id}/resolve` | **Path B only.** Supervisor enters `QuantityResolved` per line. Stock movements created using resolved quantities. |
--- ---
@@ -145,18 +133,14 @@ and writes a fresh set. Latest version = `IsActive = true` rows.
## Stock Movements ## 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) - **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` - **Type**: `Load`
Since different items can have different `FK_WarehouseId` values, one loading sheet can produce Since different items can have different `FK_WarehouseId` values, one loading sheet can produce
stock movements from multiple warehouses into one vehicle. stock movements from multiple warehouses into one vehicle in a single confirmation action.
**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`
--- ---
@@ -166,15 +150,11 @@ Base route: `/api/loading-sheets`
| Method | Route | Who | Description | | 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 | `/` | 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. | | 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. | | 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` | TBD | Lock the sheet. `Planned → Confirmed`. Items frozen. Stock movements created. |
| 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`. |
--- ---
@@ -192,11 +172,12 @@ Base route: `/api/loading-sheets`
8. Items are editable (versioned via `IsActive`) while `Planned` — frozen at `Confirmed`. 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 9. Every item edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes
a fresh set. Active rows = latest version; soft-deleted = history. 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 10. Stock movements are created at `Confirmed` — one debit per item's warehouse, one credit to the
vehicle. Timing depends on path chosen (see Open Questions). vehicle — all in one transaction.
11. No `Ride.Status` transition is triggered at any point in this flow. 11. A `Planned` sheet stays `Planned` until confirmed — no timeout or auto-cancellation.
12. All list endpoints return paginated responses. 12. No `Ride.Status` transition is triggered at any point in this flow.
13. Vehicle capacity check is not enforced in this step. 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 | | Code | Who |
|---|---| |---|---|
| `loading.view` | All roles | | `loading.view` | All roles |
| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items, confirm sheet | | **`loading.populate`** | **New.** Dispatcher — trigger population, edit items |
| `loading.dispatch` | Warehouse manager — Path B dispatch confirmation | | **`loading.confirm`** | **New.** TBD — who can confirm the sheet (see Open Questions) |
| `loading.receive` | Driver — Path B receipt confirmation |
| `loading.resolve` | Supervisor — Path B discrepancy resolution |
--- ---
## Fraud / Control Considerations ## 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. - `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. - `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 ## Open Questions
1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A) 1. **Who confirms the loading sheet?** — dispatcher, warehouse manager, or either? TBD. Controls
or at `ReceiptConfirmed`/`Resolved` (Path B). Both paths retained until decided. the `loading.confirm` permission assignment.
2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager, 2. **What happens to a `Planned` sheet if its source PO is cancelled?** — if a Confirmed PO is
or either? cancelled after a loading sheet has been populated from it, the sheet's items sourced from that
3. **What happens to a `Planned` sheet if it is never confirmed?** — no cancellation or timeout PO become invalid. No cascade behaviour designed here.
path is designed here. 3. Vehicle capacity check — still deferred.
4. Vehicle capacity check — still deferred.
--- ---
## Migration Notes (for when this is implemented) ## Migration Notes (for when this is implemented)
- Remove `FK_OrderId` and `FK_SourceWarehouseId` from `Models/LoadingSheet.cs`. Make `FK_RideId` - Remove `FK_OrderId`, `FK_SourceWarehouseId`, `AssignedAt`, `AssignedBy`,
required (non-nullable). Remove `AssignedAt`/`AssignedBy`. Add `PopulatedBy`, `PopulatedAt`, `DispatchConfirmedAt`, `DispatchConfirmedBy`, `ReceiptConfirmedAt`, `ReceiptConfirmedBy` from
`Models/LoadingSheet.cs`. Make `FK_RideId` required. Add `PopulatedBy`, `PopulatedAt`,
`ConfirmedAt`, `ConfirmedBy`. `ConfirmedAt`, `ConfirmedBy`.
- Add `Models/LoadingSheetOrder.cs` (`FK_LoadingSheetId`, `FK_OrderId`) with composite PK. - Add `Models/LoadingSheetOrder.cs` (`FK_LoadingSheetId`, `FK_OrderId`) with composite PK.
- Add `FK_WarehouseId` (required), `FK_OrderItemId` (nullable), `IsActive` to - Add `FK_WarehouseId` (required), `FK_OrderItemId` (nullable), `IsActive` to
`Models/LoadingSheetItem.cs`. `Models/LoadingSheetItem.cs`. Remove `QuantityReceived`, `QuantityResolved`.
- Replace `LoadingSheetStatus.Pending` and `Assigned` with `Planned` and `Confirmed`; update all - Replace `LoadingSheetStatus` entirely with `Planned` and `Confirmed`; remove all other values
status checks in `LoadingSheetService`/`LoadingSheetRepository`. and update all status checks in `LoadingSheetService`/`LoadingSheetRepository`.
- Remove `POST /api/loading-sheets` (old manual create) and `POST /{id}/assign-ride`; add - Remove `POST /api/loading-sheets` (old manual create), `POST /{id}/assign-ride`,
`POST /api/loading-sheets/populate` and `POST /{id}/confirm`. `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 - Remove ride-route-based item population (`GetAggregatedOrderItemsByRouteIdAsync`); replace with
filter-based multi-PO aggregation in `LoadingSheetService`. filter-based multi-PO aggregation.
- Remove all calls to ride-activation logic from `confirm-receipt`/`resolve` — no `Ride.Status` - Remove all calls to ride-activation logic — no `Ride.Status` transition exists anymore.
transition exists anymore. - Seed `loading.populate` and `loading.confirm` permissions.
- Seed the new `loading.populate` permission. - Resolve Open Question 1 (who confirms) before assigning `loading.confirm` permission.
- Resolve Open Question 1 (stock movement timing) before implementing `POST /{id}/confirm` and
`POST /{id}/confirm-receipt` stock movement logic.
@@ -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. 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 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). 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. 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. 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. 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 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. `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). - `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. - 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.