diff --git a/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf b/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf new file mode 100644 index 0000000..98766e1 Binary files /dev/null and b/Distribution/docs/refactor_docs/Dispatch-Pipeline.pdf differ diff --git a/Distribution/docs/refactor_docs/loading_event.md b/Distribution/docs/refactor_docs/loading_event.md new file mode 100644 index 0000000..342b193 --- /dev/null +++ b/Distribution/docs/refactor_docs/loading_event.md @@ -0,0 +1,250 @@ +# [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`** | Guid | **New, required.** FK → `Order` (PO). Set at creation; never changes. | +| **`FK_RideId`** | Guid? | **Now nullable.** Null while `Planned`; set by the new assign-ride action. | +| **`FK_SourceWarehouseId`** | Guid? | **Now nullable**, for the same reason — set together with `FK_RideId` at assign time, not at creation. | +| Status | LoadingSheetStatus | State machine below — gains a `Planned`/`Assigned` prefix. | +| **`AssignedAt`** | DateTime? | **New.** Set when a Ride + source warehouse are assigned. | +| **`AssignedBy`** | Guid? | **New.** UserId of whoever assigned the ride. | +| DispatchConfirmedAt | DateTime? | Unchanged — set when warehouse confirms dispatch. | +| DispatchConfirmedBy | Guid? | Unchanged. | +| ReceiptConfirmedAt | DateTime? | Unchanged — set when driver confirms receipt. | +| ReceiptConfirmedBy | Guid? | Unchanged. | +| CreatedAt | DateTime | Unchanged. | + +`LoadingSheetItem` is **unchanged** (`FK_ItemId`, `QuantityDispatched`, `QuantityReceived`, +`QuantityResolved`) — still populated the same way, just from a different source (see +[Item Population](#item-population) below). + +### Updated Enum — `LoadingSheetStatus` + +```csharp +public enum LoadingSheetStatus +{ + Planned, // new: created from a confirmed PO, no ride yet + Assigned, // new: ride + source warehouse attached, ready to dispatch + DispatchConfirmed, + ReceiptConfirmed, + DiscrepancyFlagged, + Resolved +} +``` + +`Pending` from `04_loading_event.md` is replaced by two states (`Planned`, `Assigned`) to make the +new ride-less creation step explicit. `DispatchConfirmed`, `ReceiptConfirmed`, +`DiscrepancyFlagged`, and `Resolved` keep their existing meaning and stock-movement behavior +unchanged. + +--- + +## State Machine + +```text +Planned -> Assigned -> DispatchConfirmed -> ReceiptConfirmed + \-> DiscrepancyFlagged -> Resolved +``` + +| Transition | Trigger | What it does | +|---|---|---| +| (implicit) -> Planned | PO confirmed (`POST /api/orders/{id}/confirm`, per [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)) | `LoadingSheet` is created automatically with `FK_OrderId` set, items populated from the PO's `OrderItem` lines. No ride, no warehouse yet. | +| Planned -> Assigned | `POST /{id}/assign-ride` | Dispatcher attaches `FK_RideId` (must be an `Open` ride) and `FK_SourceWarehouseId`. Sets `AssignedAt`/`AssignedBy`. This is the new step that replaces `04_loading_event.md`'s ride-scoped sheet creation. | +| Assigned -> DispatchConfirmed | `POST /{id}/confirm-dispatch` | Unchanged from `04_loading_event.md` — warehouse manager locks in `QuantityDispatched` per line. **Ride status is untouched** (no more `Open` requirement check beyond what assign-ride already validated). | +| DispatchConfirmed -> ReceiptConfirmed | `POST /{id}/confirm-receipt` | Unchanged — driver's received quantities all match dispatched. Creates stock movements. **No longer triggers any ride transition** (there is no `Active` to trigger). | +| DispatchConfirmed -> DiscrepancyFlagged | `POST /{id}/confirm-receipt` | Unchanged — any line mismatch. No stock movements created. | +| DiscrepancyFlagged -> Resolved | `POST /{id}/resolve` | Unchanged mechanics — supervisor enters `QuantityResolved` per line, creates stock movements using resolved quantities. **No longer triggers any ride transition.** | + +--- + +## Item Population + +Replaces `04_loading_event.md`'s "Item Population by Ride Type" section — there is no more +Delivery/Sale split: + +Every `LoadingSheet` is auto-populated on creation (at `Planned`) from the aggregate of its own +PO's (`Order`) `OrderItem` lines — the same mechanism for every ride scenario, since +[order_invoice_lifecycle.md](./order_invoice_lifecycle.md) generates one `LoadingSheet` per +confirmed PO regardless of how that PO originated. `PUT /{id}/items` remains available to override +quantities before dispatch is confirmed (through `Planned` and `Assigned`). + +--- + +## Stock Movements + +**Unchanged** from `04_loading_event.md`. Still created exactly once, at `ReceiptConfirmed` or +`Resolved`, still one pair per line item (debit `FK_SourceWarehouseId`, credit the assigned ride's +vehicle), still using `QuantityFinal` = `QuantityReceived` or `QuantityResolved`, still +`Type = Load`. + +--- + +## API Endpoints + +Base route: `/api/loading-sheets` + +| Method | Route | Who | Description | +|---|---|---|---| +| ~~POST `/`~~ | — | — | **Removed.** Sheets are no longer manually created — they're generated automatically when a PO is confirmed. | +| GET | `/` | Any | List sheets. Filter by `orderId`, `rideId`, `status`. Paginated. | +| GET | `/{id}` | Any | Get sheet detail with all line items. | +| PUT | `/{id}/items` | Warehouse mgr | Set or override item quantities — allowed through `Planned` and `Assigned`. | +| **POST `/{id}/assign-ride`** | **Dispatcher** | **New.** `{ rideId, sourceWarehouseId }`. Requires the ride to be `Open`. `Planned -> Assigned`. | +| POST | `/{id}/confirm-dispatch` | Warehouse mgr | `Assigned -> DispatchConfirmed`. | +| PUT | `/{id}/receipt-items` | Driver | Set `QuantityReceived` per line. Unchanged. | +| POST | `/{id}/confirm-receipt` | Driver | `-> ReceiptConfirmed` or `DiscrepancyFlagged`. No ride-status side effect. | +| POST | `/{id}/resolve` | Supervisor | Enter `QuantityResolved` per line `-> Resolved`. No ride-status side effect. | + +--- + +## Business Rules + +1. A `LoadingSheet` is created automatically, exactly once per PO, when that PO is confirmed — there + is no manual "create sheet" action and no `Open`-ride precondition at creation time (replaces + `04_loading_event.md` rule 1). +2. **A ride may now have more than one `LoadingSheet` assigned to it** — one per PO whose sheet was + assigned there. `04_loading_event.md`'s "a ride can only have one active loading sheet at a time" + no longer holds, since a single ride/trip can carry stock for several stores' POs at once. Each + assigned sheet still progresses through dispatch/receipt/discrepancy independently. +3. `assign-ride` requires the target ride to be `Open`; a `Cancelled` or `Closed` ride cannot receive + a new assignment. +4. `QuantityDispatched != QuantityReceived` on any line -> `DiscrepancyFlagged`. No partial receipt + confirmation (unchanged from `04_loading_event.md`, minus the "activation" language since there's + no `Active` status anymore). +5. Stock movements are created only once — at `ReceiptConfirmed` or `Resolved`. Never on dispatch or + on assignment. Unchanged. +6. Discrepancies must be resolved per line item with an explicit `QuantityResolved` value. Unchanged. +7. All list endpoints return paginated responses. Unchanged. +8. Vehicle capacity check is still not enforced in this step. Unchanged. +9. Loading confirmation and receipt no longer trigger any `Ride.Status` change — replaces + `04_loading_event.md` rules that referenced `Open -> Active`. + +`04_loading_event.md`'s rule 8 ("Immediate handover from a Sale ride still creates an order") is +dropped along with `RideType` — every PO already creates its own `Invoice`/`LoadingSheet` uniformly, +regardless of how quickly it moves through Pending → Draft → Finalized. + +--- + +## 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.assign`** | **New.** Dispatcher and above | +| `loading.dispatch` | Warehouse manager and above | +| `loading.receive` | Driver | +| `loading.resolve` | Supervisor and above | + +--- + +## 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. **Who is authorized to assign a ride to a Planned sheet?** — a new "dispatcher" role/permission is + introduced (`loading.assign`) but not otherwise defined in this pass. +2. **What happens to a `Planned` sheet if its PO is never assigned to a ride?** — no timeout, retry, + or cancellation path is designed here. +3. **Does editing a PO's items after its sheet is `Planned`/`Assigned` re-sync the sheet?** — carried + over from `order_invoice_lifecycle.md`'s Open Question 8, still unresolved here. +4. **Should multiple `Planned` sheets ever merge into one `Assigned` sheet** (e.g. one dispatch + covering several stores' POs on the same ride), rather than staying as separate per-PO sheets that + each go through their own dispatch/receipt cycle? Not designed here — this document keeps them as + independent per-PO sheets (see Business Rule 2). +5. Vehicle capacity check — still deferred, unchanged from `04_loading_event.md`. + +--- + +## 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 index 79284c3..548c948 100644 --- a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md +++ b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md @@ -4,8 +4,12 @@ **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 its open -questions, and this document's design does not depend on it landing first. +`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). --- @@ -19,11 +23,14 @@ days later — and once the PO exists, an **Invoice** is generated from it. That through the same three statuses regardless of which scenario produced it: 1. **Pending** — invoice just created from the PO; the PO's line items are freely editable. -2. **Draft** — the customer signs once the order is confirmed, moving it out of Pending. The PO's - line items **remain editable** in this state too — Draft is a working document, not a lock. +2. **Draft** — a PO-confirm action moves it out of Pending (no customer signature/OTP — see + [State Machine](#state-machine)). Confirming also automatically generates a `LoadingSheet` from + the PO's current line items. The PO's line items **remain editable** in this state too — Draft is + a working document, not a lock. 3. **Finalize** — the reconciliation step: item add/delete and amount deductions (returns, damage, - short-delivery, discounts) get applied here, producing the final, locked invoice amount. This is - the only state where free-form editing stops. + short-delivery, discounts) get applied here, producing the final, locked invoice amount — this is + what the shop actually received and paid, and is the record used for day-end calculations. This + is the only state where free-form editing stops. The two ride scenarios differ only in *timing* through this one pipeline, not in structure — no ride-type-specific branching is needed in the PO/Invoice model itself. @@ -69,7 +76,7 @@ Invoice ├── Id (Guid) — PK, from Template ├── FK_OrderId (Guid) — required. One PO → one Invoice for v1 (see Open Questions). ├── Status (InvoiceStatus) — Pending, Draft, Finalized -├── SignedAt (DateTime?) — set on Pending → Draft +├── ConfirmedAt (DateTime?) — set on Pending → Draft, when the PO is confirmed ├── FinalizedAt (DateTime?) — set on Draft → Finalized ├── FinalizedBy (Guid?) — UserId of whoever finalized it ├── TotalAmount (decimal) — mirrors the PO total while Pending/Draft; becomes the @@ -77,6 +84,27 @@ Invoice ├── IsActive, CreatedAt — from Template ``` +There is no signature/OTP field on `Invoice` — confirming a PO is a plain action, not a captured +customer signature (see [State Machine](#state-machine); this also resolves +[Open Question 1](#open-questions) from the prior pass of this document). + +### `LoadingSheet` — now generated from the PO, not the Route + +`LoadingSheet` already exists in the codebase (`Features/LoadingEvent/`), previously populated by +aggregating orders on a ride's route (`LoadingSheetRepository.GetAggregatedOrderItemsByRouteIdAsync` +— see `ride_lifecycle.md`'s Impact item 2). Under this design, a `LoadingSheet` is instead created +automatically the moment an `Order`/`Invoice` moves Pending → Draft (PO confirmed), populated from +that PO's `OrderItem` lines directly: + +``` +LoadingSheet +├── FK_OrderId (Guid) — required. Links the sheet to the PO/Invoice it was generated from. +``` + +It is **not** linked to a Ride or vehicle at creation time — it exists purely as "what needs to be +loaded for this PO." How/when it later gets attached to an actual Ride for physical stock loading is +left open — see [Open Questions](#open-questions). + ### New Enum — `InvoiceStatus` ```csharp @@ -100,7 +128,7 @@ PO/Invoice is not addressed here; flagged as an open question below. | Transition | Trigger | What it does | |---|---|---| | (implicit) → Pending | PO (`Order`) created | Invoice is created automatically, `Status = Pending`. `OrderItem` lines fully editable. | -| Pending → Draft | `POST /api/invoices/{id}/draft` | Captures customer sign-off. Sets `SignedAt`. **`OrderItem` lines remain editable** — Draft is not a lock. Signature/OTP mechanism itself is an open question (see below), matching `05_delivery_ride.md`'s own prior deferral on customer confirmation. | +| Pending → Draft | `POST /api/orders/{id}/confirm` | Confirms the PO — a plain action, not a customer signature/OTP. Lives on the `Order` (PO) resource, not the `Invoice`; does **not** add or change any `Order.Status` value — PO confirmation and fulfillment tracking stay separate axes. Sets `Invoice.ConfirmedAt`, `Invoice.Status = Draft`, and automatically creates a `LoadingSheet` from the PO's current `OrderItem` lines. **`OrderItem` lines remain editable** after confirm — Draft is not a lock; whether/how a later edit re-syncs the already-generated `LoadingSheet` is an open question (see below). | | Draft → Finalized | `POST /api/invoices/{id}/finalize` | Accepts an item-adjustment/deduction payload (add/delete lines, amount deductions for returns/damage/short-delivery/discounts), recomputes and locks `TotalAmount`. This is a distinct, audited action — not the same add/edit/remove path used in Pending/Draft. Once Finalized, the Invoice and its PO's items are fully locked. | --- @@ -123,6 +151,11 @@ PO/Invoice is not addressed here; flagged as an open question below. and pass through the same three Invoice statuses — the only difference is how quickly they move through Pending → Draft → Finalized. No `RideType`-specific branching applies to the PO/Invoice model. +7. Confirming a PO (Pending → Draft) requires no customer signature or OTP — it is a plain action + available to whoever is authorized to confirm a PO (e.g. the rep/dispatcher), not a + customer-facing sign-off step. +8. A `LoadingSheet` is generated exactly once per PO, automatically, at PO-confirm time — there is no + separate manual "create loading sheet" action. --- @@ -150,6 +183,14 @@ as **known-affected, deferred follow-up work**, not solved in this pass: 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. @@ -158,9 +199,9 @@ visible and intentional rather than discovered later as a silent regression. ## Open Questions -1. **Signature/OTP mechanism for the Pending → Draft transition** — same deferral as - `05_delivery_ride.md`'s original "signature (image) or OTP (SMS code) — one, or both, in v1?" - question. Not resolved here. +1. ~~**Signature/OTP mechanism for the Pending → Draft transition**~~ — **Resolved:** dropped + entirely. Pending → Draft is driven by a plain PO-confirm action (`POST /api/orders/{id}/confirm`), + not a customer signature or OTP. See [State Machine](#state-machine). 2. **One Invoice per PO, or multiple?** — this document assumes a 1:1 `Order`→`Invoice` relationship for v1. Whether a PO could ever need multiple invoices (e.g. split billing) is not designed here. 3. **Should `Order.Status` and `Invoice.Status` eventually merge into one status axis?** — kept @@ -171,6 +212,13 @@ visible and intentional rather than discovered later as a silent regression. has no cancel branch (unlike the Ride state machine's `CancelRequested`/`Cancelled`). Not addressed here. 6. **Does `DeliveryTask.CustomerConfirmed` merge with the Draft transition?** — see Impact item 4. +7. ~~**How does a PO-scoped `LoadingSheet` get matched to an actual Ride/vehicle?**~~ — **Resolved + in [loading_event.md](./loading_event.md):** the sheet starts `Planned` (ride-less), and a new + `POST /{id}/assign-ride` action attaches a `Ride` + source warehouse, moving it to `Assigned`. +8. **Does editing a PO's items after confirm (while still Draft) re-sync the already-generated + `LoadingSheet`?** — this document allows PO edits through Draft, but doesn't decide whether the + `LoadingSheet` regenerates, gets manually re-synced, or is left to drift until Finalize reconciles + it. Not designed here. --- @@ -180,17 +228,24 @@ Not part of this docs-only pass, but captured for the eventual implementation st - Make `Order.FK_SaleId` nullable in `Models/Order.cs` and its EF/Fluent configuration; audit all code paths that currently assume it's non-null. -- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `SignedAt`, `FinalizedAt`, `FinalizedBy`, +- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `ConfirmedAt`, `FinalizedAt`, `FinalizedBy`, `TotalAmount`, plus `Template` base fields) and the `InvoiceStatus` enum. - New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`, `InvoiceRepository`, `InvoiceDTOs`) — do not place this under `Features/PurchaseOrder/` or reuse any of its DTOs/names, to avoid the supplier-PO naming collision flagged above. -- `POST /api/invoices/{id}/draft` and `POST /api/invoices/{id}/finalize` endpoints; the finalize - endpoint's adjustment/deduction payload should reuse the `CreditNote` shape from - [18_returns_claims.md](../18_returns_claims.md) where applicable rather than inventing a parallel - mechanism. +- Add `POST /api/orders/{id}/confirm` on the `Order`/PO feature (not on `Invoice`) — it sets + `Invoice.ConfirmedAt`/`Status = Draft` and creates the `LoadingSheet` in one action; no + `POST /api/invoices/{id}/draft` endpoint and no signature/OTP capture are needed. +- `POST /api/invoices/{id}/finalize` endpoint; the finalize endpoint's adjustment/deduction payload + should reuse the `CreditNote` shape from [18_returns_claims.md](../18_returns_claims.md) where + applicable rather than inventing a parallel mechanism. +- Add `FK_OrderId` to `Models/LoadingSheet.cs` (or equivalent) and change + `LoadingSheetService`/`LoadingSheetRepository` to generate a sheet from a PO's `OrderItem` lines + directly, replacing `GetAggregatedOrderItemsByRouteIdAsync`'s route-based aggregation — wire this + call into the new PO-confirm action. - A new PO-creation path independent of `SaleService.CreateAsync`, to support creating an `Order`/`Invoice` before any `Sale` exists (Impact item 1) — needs its own design, not specified here. -- Resolve Open Questions 1–2 before implementing the Draft-transition endpoint and the - one-Invoice-per-PO assumption respectively. +- Resolve Open Question 2 (one Invoice per PO) before finalizing the `FK_OrderId` uniqueness + constraint on `Invoice`; resolve Open Questions 7–8 (LoadingSheet-to-Ride matching, and re-sync on + post-confirm PO edits) before wiring the PO-confirm action into `LoadingSheetService`. diff --git a/Distribution/docs/refactor_docs/ride_lifecycle.md b/Distribution/docs/refactor_docs/ride_lifecycle.md index 5061b6c..be99cba 100644 --- a/Distribution/docs/refactor_docs/ride_lifecycle.md +++ b/Distribution/docs/refactor_docs/ride_lifecycle.md @@ -10,7 +10,7 @@ Ride should not require a `Route`, should not carry a `RideType`, should not be gated by a warehouse's `Type`, and should not even pin down a source warehouse on the ride record itself. Per direction from the business owner, the ride record carries only what a real dispatch needs to track — **"a ride only carries vehicle, driver assign, and meter reading from start to end."** Everything route-shaped (which stores get visited, which orders/items travel on this trip) and everything warehouse-shaped (where stock loads from/returns to) moves out of the Ride entity's responsibility entirely; neither is replaced by another matching mechanism here. -This is a genuine pivot from [03_ride_lifecycle.md](../03_ride_lifecycle.md), where `Route` and `FK_SourceWarehouseId` are both **required** on every ride, and `Route` is the *sole* mechanism two other features use to resolve "which orders belong to this ride." See [Impact on Dependent Features](#impact-on-dependent-features) below — those gaps are flagged, not solved, in this pass. The Draft → Open → Active → Closed workflow and the cancel-request flow are kept — only the fields that used to back warehouse reconciliation and ride classification are removed; how those transitions work without them is intentionally left as an open question (see below), not resolved by this document. +This is a genuine pivot from [03_ride_lifecycle.md](../03_ride_lifecycle.md), where `Route` and `FK_SourceWarehouseId` are both **required** on every ride, and `Route` is the *sole* mechanism two other features use to resolve "which orders belong to this ride." See [Impact on Dependent Features](#impact-on-dependent-features) below — those gaps are flagged, not solved, in this pass. The Draft → Open → Active → Closed workflow and the CancelRequested/approval flow from `03_ride_lifecycle.md` are **not** kept as-is: per further direction from the business owner, the workflow collapses to three stages — **Open, Cancelled, Closed** — and cancellation happens directly with no supervisor approval. See [State Machine](#state-machine). --- @@ -25,28 +25,36 @@ Ride (DeliveryRun) ├── FK_DriverProfileId (Guid) — required ├── Meterstart (int) ├── Meterend (int) -├── StartedAt (DateTime?) — set when Draft → Open -├── ActivatedAt (DateTime?) — set when Open → Active -├── ClosedAt (DateTime?) — set when Active → Closed +├── Status (RideStatus) — Open | Cancelled | Closed +├── StartedAt (DateTime?) — set on create, ride starts Open immediately +├── ClosedAt (DateTime?) — set when Open → Closed +├── CancelTypeCode (CancelRequestType?) — set when Open → Cancelled +├── CancelReason (string?) — set when Open → Cancelled +├── CancelledAt (DateTime?) — set when Open → Cancelled ├── IsActive, CreatedAt — from Template ``` -That's the whole record: a vehicle, a driver, a meter start/end reading, and the three workflow -timestamps. Everything else is removed: +That's the whole record: a vehicle, a driver, a meter start/end reading, a status, and the +workflow/cancellation timestamps. Everything else is removed: - **`FK_RouteId`** — gone (see the original goal above; unchanged from the first pass of this doc). - **`RideType`** — gone. Nothing in the Ride record itself needs to distinguish Delivery from Sale once the ride no longer drives order/task auto-population (that was the only place `RideType` mattered inside the Ride entity's own logic — see [Impact on Dependent Features](#impact-on-dependent-features)). -- **`Status` (`RideStatus`)** — gone as a stored column. The workflow stages (Draft, Open, Active, - CancelRequested, Cancelled, Closed) are **kept as a concept** — see - [State Machine](#state-machine) — but this document does not resolve how "current state" is - represented without a stored enum. Left as an open question (see below), not solved here. +- **`ActivatedAt`** — gone. There is no separate Draft or Active stage anymore (see + [State Machine](#state-machine)), so there's no "activation" event to timestamp. - **`FK_SourceWarehouseId`** — gone. The ride record no longer pins down a warehouse to load from or reconcile stock against at all. `Warehouse.Type` accordingly becomes moot for rides (there's no ride-side warehouse selection left to gate); the enum itself is untouched on `Warehouse`. +**`Status` (`RideStatus`) is kept, just narrowed.** The original workflow (Draft, Open, Active, +CancelRequested, Cancelled, Closed) collapses to three stages — **Open, Cancelled, Closed** — per +direction from the business owner: no separate setup/activation stages, and cancellation needs no +supervisor approval. With only three stages, storing `Status` directly is simple again, so this +document no longer needs to resolve "how is current state tracked without a stored enum" (see +[Open Questions](#open-questions), item 4 — now resolved). + ### Warehouse No changes to the `Warehouse` entity — `Warehouse.Type` (`WareHouseType`: `DistributionCenter`, @@ -62,26 +70,20 @@ that means for stock reconciliation, which previously depended on `FK_SourceWare ## State Machine -The workflow stages and endpoints are **kept** from [03_ride_lifecycle.md](../03_ride_lifecycle.md#state-machine) -— none of the transitions reference `Route`, `RideType`, or a warehouse by name. What's removed is -the *storage*: there is no `Status` column to hold "which stage is this ride in." This document does -not resolve what replaces it (e.g., deriving state purely from which timestamp fields are set — -`StartedAt` null = Draft, `StartedAt` set + `ActivatedAt` null = Open, etc. — is one option, but not -decided here). Flagged as [Open Question 4](#open-questions). +The workflow is simplified from [03_ride_lifecycle.md](../03_ride_lifecycle.md#state-machine) to +three stages, per direction from the business owner: no Draft or Active setup stages, and +cancellation needs no supervisor approval — a driver/dispatcher cancels directly. ``` -Draft → Open → Active → Closed - ↘ CancelRequested → Cancelled +Open → Closed +Open → Cancelled ``` | Transition | Trigger | What it does | |---|---|---| -| Draft → Open | `POST /api/rides/{id}/open` | Validates driver compliance. Checks vehicle has no other Open/Active ride. Sets `StartedAt`. | -| Open → Active | `POST /api/rides/{id}/activate` | Called by loading event confirmation. Sets `ActivatedAt`. | -| Active → Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation. Sets `ClosedAt`. Previously required "all stock returned to source warehouse" — with no `FK_SourceWarehouseId` on the ride, this check has nothing to reconcile against; see [Impact on Dependent Features](#impact-on-dependent-features). | -| Active → CancelRequested | `POST /api/rides/{id}/cancel-request` | Driver submits cancel request with type code and reason. | -| CancelRequested → Cancelled | `POST /api/rides/cancel-requests/{id}/approve` | Supervisor approves. Previously required stock returned to source warehouse first — same gap as Close above. | -| CancelRequested → Active | `POST /api/rides/cancel-requests/{id}/reject` | Supervisor rejects. Ride returns to `Active`. | +| (create) → Open | `POST /api/rides` | Validates driver compliance. Checks vehicle has no other Open ride. Ride is created directly in `Open` status with `StartedAt` set — no separate "open" step. | +| Open → Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation. Sets `ClosedAt`, `Status = Closed`. Previously required "all stock returned to source warehouse" — with no `FK_SourceWarehouseId` on the ride, this check has nothing to reconcile against; see [Impact on Dependent Features](#impact-on-dependent-features). | +| Open → Cancelled | `POST /api/rides/{id}/cancel` | Driver/dispatcher cancels directly with a type code and reason. No approval step. Sets `CancelTypeCode`, `CancelReason`, `CancelledAt`, `Status = Cancelled`. Previously required stock returned to source warehouse first — same reconciliation gap as Close above. | --- @@ -90,25 +92,26 @@ Draft → Open → Active → Closed Carried over from [03_ride_lifecycle.md](../03_ride_lifecycle.md#business-rules), with the route-dependent rules removed: -1. A ride cannot transition Draft → Open if `IsActive = false` on the driver profile, or (when the +1. A ride cannot be created (→ Open) if `IsActive = false` on the driver profile, or (when the relevant `DriverComplianceConfig` flags are enabled) if license/medical clearance is expired. -2. A vehicle cannot be on more than one Open or Active ride at a time. -3. A CancelRequested ride cannot close or activate until the request is resolved. -4. All cancel requests must have a `TypeCode` — free text reason alone is not sufficient. -5. All list endpoints return paginated responses. -6. **A ride carries no order, task, route, or warehouse linkage of its own.** Creating a ride never +2. A vehicle cannot be on more than one Open ride at a time. +3. A cancel action must include a `TypeCode` — free text reason alone is not sufficient. +4. All list endpoints return paginated responses. +5. **A ride carries no order, task, route, or warehouse linkage of its own.** Creating a ride never touches `Order`, `DeliveryTask`, `LoadingSheet`, or `StockBalance`/`StockMovement` records — those remain entirely separate concerns, attached (if at all) by whatever future mechanism replaces route-derived linkage and warehouse reconciliation. -**Removed rules** (no longer apply once `Route`, `RideType`, `Status`, and `FK_SourceWarehouseId` -are gone): +**Removed rules** (no longer apply once `Route`, `RideType`, `FK_SourceWarehouseId`, Draft/Active +stages, and the CancelRequested approval flow are gone): - ~~"A ride uses a single route for now."~~ - ~~"Orders can only be assigned to a Delivery ride while it is in Draft status."~~ — there is no - order-assignment mechanism on the ride at all anymore, in Draft or otherwise. + order-assignment mechanism on the ride at all anymore, and there is no Draft stage. - ~~"Loading and unloading warehouse must be the same — enforced at close/cancel."~~ — there is no warehouse on the ride record to compare "loading" against "unloading" anymore. See [Impact on Dependent Features](#impact-on-dependent-features). +- ~~"A CancelRequested ride cannot close or activate until the request is resolved."~~ — there is no + CancelRequested stage or approval step; cancellation is immediate. --- @@ -119,18 +122,13 @@ Same routes as [03_ride_lifecycle.md](../03_ride_lifecycle.md#api-endpoints), wi | Method | Route | Description | |---|---|---| -| POST | `/api/rides` | Create ride (Draft): `{ vehicleId, driverProfileId }`. Nothing else required. | -| GET | `/api/rides` | List rides (paginated). Filter by driverProfileId, vehicleId. (Filtering by `status`/`type` depends on how [Open Question 4](#open-questions) resolves.) | +| POST | `/api/rides` | Create ride: `{ vehicleId, driverProfileId }`. Ride starts `Open` immediately — no separate open step. | +| GET | `/api/rides` | List rides (paginated). Filter by driverProfileId, vehicleId, status (`Open`/`Cancelled`/`Closed`). | | GET | `/api/rides/{id}` | Get ride detail — response no longer includes `routeId`/`routeName`, `rideType`, or `sourceWarehouseId`/`sourceWarehouseName`. | | PUT | `/api/rides/{id}/driver` | Assign or change driver profile | -| POST | `/api/rides/{id}/open` | Transition Draft → Open | -| POST | `/api/rides/{id}/activate` | Transition Open → Active | -| POST | `/api/rides/{id}/close` | Transition Active → Closed | -| POST | `/api/rides/{id}/cancel-request` | Submit cancel request (Active rides only) | -| GET | `/api/rides/cancel-requests` | List cancel requests (paginated). Filter by status. | -| POST | `/api/rides/cancel-requests/{id}/approve` | Approve cancel request | -| POST | `/api/rides/cancel-requests/{id}/reject` | Reject cancel request | -| GET | `/api/rides/breakdown-assist/{rideId}` | See [Impact on Dependent Features](#impact-on-dependent-features) — this endpoint currently keys off the stranded ride's route and needs rework. | +| POST | `/api/rides/{id}/close` | Transition Open → Closed | +| POST | `/api/rides/{id}/cancel` | Cancel an Open ride directly: `{ typeCode, reason }`. Transitions Open → Cancelled immediately, no approval step. | +| GET | `/api/rides/breakdown-assist/{rideId}` | Returns other active rides' vehicle stock levels only. No longer returns an open-orders listing — see [Impact on Dependent Features](#impact-on-dependent-features). | `DriverComplianceConfig` and `DriverProfile` endpoints are unaffected by this refactor. @@ -138,25 +136,32 @@ Same routes as [03_ride_lifecycle.md](../03_ride_lifecycle.md#api-endpoints), wi ## Impact on Dependent Features -Removing `FK_RouteId`, `RideType`, `Status`, and `FK_SourceWarehouseId` from `Ride` is not a purely -additive/subtractive field change — several existing behaviors currently derive their entire input -from these fields, and none of them get a replacement in this pass. Each is flagged here as -**known-broken, deferred follow-up work**, not solved: +Removing `FK_RouteId`, `RideType`, and `FK_SourceWarehouseId` from `Ride` (and collapsing `Status` +to three values with direct cancellation) is not a purely additive/subtractive field change — +several existing behaviors currently derive their entire input from the removed fields, and none of +them get a replacement in this pass. Each is flagged here as **known-broken, deferred follow-up +work**, not solved: 1. **Delivery task generation** (`Features/DeliveryRide/DeliveryTaskService.GenerateAsync`) — currently calls `orderRepo.GetPendingByRouteIdAsync(ride.FK_RouteId)` to auto-create delivery tasks for a Delivery ride. With no route (and no `RideType` to even tell Delivery from Sale) on the ride, this call has nothing to key off. Needs a redesign once "how orders attach to a ride" - is decided — out of scope here. + is decided — out of scope here. Note: `Order` already carries a direct `FK_StoreId` + (`Distribution/Models/Order.cs`), independent of any Route — the current route-based queries only + reach it indirectly via `Store.FK_RouteId`. Whatever mechanism replaces route-derived linkage can + key off `Order.FK_StoreId` directly; Route is confirmed unnecessary here, not a gap that needs + another route-shaped mechanism to fill. 2. **Loading sheet auto-population** (`Features/LoadingEvent/LoadingSheetService` + `LoadingSheetRepository.GetAggregatedOrderItemsByRouteIdAsync`) — auto-fills a Delivery ride's loading-sheet items by aggregating orders on `Order.Store.FK_RouteId == ride.FK_RouteId`. Same - gap as above; documented in [04_loading_event.md](../04_loading_event.md), which will need its - own refactor pass once this lands. -3. **Breakdown-assist endpoint** (`RideService.GetBreakdownAssistAsync`) — uses - `strandedRide.FK_RouteId` to list the stranded route's open orders for manual reassignment. With - no route, this listing has no scope; the endpoint's shape needs to change (e.g., list all open - orders regardless of route, or drop this listing) — not decided here. + gap as above; **resolved in [loading_event.md](./loading_event.md)**, which generates a + `LoadingSheet` per PO instead (see [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)). +3. **Breakdown-assist endpoint** (`RideService.GetBreakdownAssistAsync`) — previously used + `strandedRide.FK_RouteId` to list the stranded route's open orders for manual reassignment. Per + direction from the business owner, this listing is **removed outright**, not redesigned to scope + some other way — the endpoint now only returns other active rides' vehicle stock levels. + Reassigning the stranded ride's orders is left entirely to whatever future order-assignment + mechanism is designed (see item 1 and [Open Question 1](#open-questions)). 4. **Stock reconciliation at Close/Cancel** — [03_ride_lifecycle.md](../03_ride_lifecycle.md) closes or cancels a ride only once "all stock is returned to source warehouse," checked against `FK_SourceWarehouseId`. With no warehouse on the ride, there is nothing to reconcile the vehicle's @@ -169,7 +174,8 @@ from these fields, and none of them get a replacement in this pass. Each is flag 6. **[04_loading_event.md](../04_loading_event.md)**'s loading-event flow currently records which warehouse stock was loaded from via the ride's `FK_SourceWarehouseId`; without that field, each loading event presumably needs to carry its own warehouse reference directly instead of inheriting - it from the ride — flagged, not designed, here. + it from the ride — **resolved in [loading_event.md](./loading_event.md)**: `LoadingSheet` already + carries its own `FK_SourceWarehouseId` independent of `Ride`, set at the new assign-ride step. 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. @@ -178,20 +184,21 @@ visible and intentional rather than discovered later as a silent regression. ## Open Questions -1. **How does a Delivery ride's orders/tasks/items get attached, if not via Route?** — - Deliberately unresolved. Per the business owner's direction, the Ride entity itself should not - carry that responsibility going forward; whatever mechanism replaces route-derived linkage (if - any) is a separate design effort, not part of slimming down the Ride record. -2. **What happens to `GetBreakdownAssistAsync`'s route-scoped order listing?** — Needs its own - decision once (1) is resolved; not designed here. +1. **How does a Delivery ride's orders/tasks/items get attached, if not via Route?** — Still + unresolved, but partially constrained: `Order` already has a direct `FK_StoreId` + (`Distribution/Models/Order.cs`) independent of Route, so a future attachment mechanism can key + off Store directly without reintroducing Route anywhere. The exact mechanism (manual dispatcher + assignment, an implicit link at task-creation time, or something else) is still a separate design + effort, not part of slimming down the Ride record. +2. ~~**What happens to `GetBreakdownAssistAsync`'s route-scoped order listing?**~~ — **Resolved:** + removed outright, not redesigned. See [Impact on Dependent Features](#impact-on-dependent-features) + item 3. 3. **`04_loading_event.md` / `05_delivery_ride.md` rewrite** — scheduled as follow-up work once (1) is resolved; explicitly out of scope for this document. -4. **How is a ride's current workflow stage tracked without a stored `Status` column?** — The - Draft/Open/Active/CancelRequested/Cancelled/Closed stages are kept conceptually (see - [State Machine](#state-machine)), but this document doesn't decide whether state is derived from - the existing timestamp fields (`StartedAt`/`ActivatedAt`/`ClosedAt` plus the presence of an open - `RideCancelRequest`), computed some other way, or whether a status-like field comes back under a - different name. Left for the business owner/implementer to resolve. +4. ~~**How is a ride's current workflow stage tracked without a stored `Status` column?**~~ — + **Resolved:** the workflow collapses to three stages (Open, Cancelled, Closed — see + [State Machine](#state-machine)), simple enough that `Status` is kept as a stored `RideStatus` + column again. 5. **What replaces stock-return enforcement at Close/Cancel without `FK_SourceWarehouseId`?** — See [Impact on Dependent Features](#impact-on-dependent-features) item 4. Not designed here. 6. **Does `RideType` (Delivery vs Sale) need to exist somewhere else** — e.g. on whatever @@ -204,18 +211,30 @@ visible and intentional rather than discovered later as a silent regression. Not part of this docs-only pass, but captured for the eventual implementation step: -- Drop the `FK_RouteId`, `RideType`, `Status`, and `FK_SourceWarehouseId` columns from - `DeliveryRun`/`Ride` and their Fluent/EF configuration. -- Remove `RouteId`, `RideType`, `Status`, and `SourceWarehouseId`/`SourceWarehouseName` from - `CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. +- Drop the `FK_RouteId`, `RideType`, `ActivatedAt`, and `FK_SourceWarehouseId` columns from + `DeliveryRun`/`Ride` and their Fluent/EF configuration. `Status` stays, re-typed to the narrowed + `RideStatus` (`Open`/`Cancelled`/`Closed`). Add `CancelTypeCode`, `CancelReason`, `CancelledAt` + columns to `DeliveryRun`. +- Drop the `RideCancelRequest` table, its Fluent/EF configuration, and + `RideCancelRequestRepository`/`IRideCancelRequestRepository` — the request/approve/reject flow is + gone. +- Remove `RouteId`, `RideType`, and `SourceWarehouseId`/`SourceWarehouseName` from + `CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. Remove the + open-orders list from `BreakdownAssistResponse`. Remove the `RaiseCancelRequest`/cancel-request + response DTOs; add a simple `CancelRideRequest { TypeCode, Reason }`. - Remove the `.Include(r => r.Route)`/`.Include(r => r.SourceWarehouse)` calls and - `GetActiveOrdersByRouteIdAsync` usage in `RideRepository.cs`/`RideService.cs` once the - breakdown-assist redesign (Open Question 2) lands. + `GetActiveOrdersByRouteIdAsync` usage in `RideRepository.cs`/`RideService.cs` — the breakdown-assist + order listing is removed, not replaced. +- Remove the `RaiseCancelRequestAsync`, `GetCancelRequestsAsync`, `ApproveCancelAsync`, + `RejectCancelAsync`, `GetCancelRequestAsync` methods from `RideService.cs` and the corresponding + `/cancel-request`, `/cancel-requests`, `/cancel-requests/{id}/approve`, + `/cancel-requests/{id}/reject`, `/{id}/cancel-request` routes from `RideController.cs`; add a + single `CancelAsync`/`POST /api/rides/{id}/cancel` in their place. +- Remove the `/open` and `/activate` transition endpoints/methods — ride creation sets `Status = Open` + directly. - Remove the required `Route` and `Source Warehouse` `