Refactor order invoice lifecycle and ride lifecycle documentation

This commit is contained in:
2026-09-14 11:53:04 +05:30
parent 2000ba67f4
commit fb62448e2c
4 changed files with 418 additions and 94 deletions
@@ -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.
@@ -4,8 +4,12 @@
**Refactors**: [../05_delivery_ride.md](../05_delivery_ride.md) and **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) [../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 **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 `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. 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: 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. 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 2. **Draft** — a PO-confirm action moves it out of Pending (no customer signature/OTP — see
line items **remain editable** in this state too — Draft is a working document, not a lock. [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, 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 short-delivery, discounts) get applied here, producing the final, locked invoice amount — this is
the only state where free-form editing stops. 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 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. ride-type-specific branching is needed in the PO/Invoice model itself.
@@ -69,7 +76,7 @@ Invoice
├── Id (Guid) — PK, from Template ├── Id (Guid) — PK, from Template
├── FK_OrderId (Guid) — required. One PO → one Invoice for v1 (see Open Questions). ├── FK_OrderId (Guid) — required. One PO → one Invoice for v1 (see Open Questions).
├── Status (InvoiceStatus) — Pending, Draft, Finalized ├── 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 ├── FinalizedAt (DateTime?) — set on Draft → Finalized
├── FinalizedBy (Guid?) — UserId of whoever finalized it ├── FinalizedBy (Guid?) — UserId of whoever finalized it
├── TotalAmount (decimal) — mirrors the PO total while Pending/Draft; becomes the ├── TotalAmount (decimal) — mirrors the PO total while Pending/Draft; becomes the
@@ -77,6 +84,27 @@ Invoice
├── IsActive, CreatedAt — from Template ├── 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` ### New Enum — `InvoiceStatus`
```csharp ```csharp
@@ -100,7 +128,7 @@ PO/Invoice is not addressed here; flagged as an open question below.
| Transition | Trigger | What it does | | Transition | Trigger | What it does |
|---|---|---| |---|---|---|
| (implicit) → Pending | PO (`Order`) created | Invoice is created automatically, `Status = Pending`. `OrderItem` lines fully editable. | | (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. | | 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 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 through Pending → Draft → Finalized. No `RideType`-specific branching applies to the PO/Invoice
model. 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. 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 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. 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 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. 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 ## Open Questions
1. **Signature/OTP mechanism for the Pending → Draft transition** — same deferral as 1. ~~**Signature/OTP mechanism for the Pending → Draft transition**~~ — **Resolved:** dropped
`05_delivery_ride.md`'s original "signature (image) or OTP (SMS code) — one, or both, in v1?" entirely. Pending → Draft is driven by a plain PO-confirm action (`POST /api/orders/{id}/confirm`),
question. Not resolved here. 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 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. 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 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 has no cancel branch (unlike the Ride state machine's `CancelRequested`/`Cancelled`). Not
addressed here. addressed here.
6. **Does `DeliveryTask.CustomerConfirmed` merge with the Draft transition?** — see Impact item 4. 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 - 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. 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. `TotalAmount`, plus `Template` base fields) and the `InvoiceStatus` enum.
- New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`, - New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`,
`InvoiceRepository`, `InvoiceDTOs`) — do not place this under `Features/PurchaseOrder/` or reuse `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. 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 - Add `POST /api/orders/{id}/confirm` on the `Order`/PO feature (not on `Invoice`) — it sets
endpoint's adjustment/deduction payload should reuse the `CreditNote` shape from `Invoice.ConfirmedAt`/`Status = Draft` and creates the `LoadingSheet` in one action; no
[18_returns_claims.md](../18_returns_claims.md) where applicable rather than inventing a parallel `POST /api/invoices/{id}/draft` endpoint and no signature/OTP capture are needed.
mechanism. - `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 - 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 `Order`/`Invoice` before any `Sale` exists (Impact item 1) — needs its own design, not specified
here. here.
- Resolve Open Questions 1–2 before implementing the Draft-transition endpoint and the - Resolve Open Question 2 (one Invoice per PO) before finalizing the `FK_OrderId` uniqueness
one-Invoice-per-PO assumption respectively. 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`.
@@ -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. 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 ├── FK_DriverProfileId (Guid) — required
├── Meterstart (int) ├── Meterstart (int)
├── Meterend (int) ├── Meterend (int)
├── StartedAt (DateTime?) — set when Draft → Open ├── Status (RideStatus) — Open | Cancelled | Closed
├── ActivatedAt (DateTime?) — set when Open → Active ├── StartedAt (DateTime?) — set on create, ride starts Open immediately
├── ClosedAt (DateTime?) — set when Active → Closed ├── 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 ├── IsActive, CreatedAt — from Template
``` ```
That's the whole record: a vehicle, a driver, a meter start/end reading, and the three workflow That's the whole record: a vehicle, a driver, a meter start/end reading, a status, and the
timestamps. Everything else is removed: workflow/cancellation timestamps. Everything else is removed:
- **`FK_RouteId`** — gone (see the original goal above; unchanged from the first pass of this doc). - **`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 - **`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` 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 mattered inside the Ride entity's own logic — see
[Impact on Dependent Features](#impact-on-dependent-features)). [Impact on Dependent Features](#impact-on-dependent-features)).
- **`Status` (`RideStatus`)** — gone as a stored column. The workflow stages (Draft, Open, Active, - **`ActivatedAt`** — gone. There is no separate Draft or Active stage anymore (see
CancelRequested, Cancelled, Closed) are **kept as a concept** — see [State Machine](#state-machine)), so there's no "activation" event to timestamp.
[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.
- **`FK_SourceWarehouseId`** — gone. The ride record no longer pins down a warehouse to load from - **`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 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`. 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 ### Warehouse
No changes to the `Warehouse` entity — `Warehouse.Type` (`WareHouseType`: `DistributionCenter`, 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 ## State Machine
The workflow stages and endpoints are **kept** from [03_ride_lifecycle.md](../03_ride_lifecycle.md#state-machine) The workflow is simplified from [03_ride_lifecycle.md](../03_ride_lifecycle.md#state-machine) to
— none of the transitions reference `Route`, `RideType`, or a warehouse by name. What's removed is three stages, per direction from the business owner: no Draft or Active setup stages, and
the *storage*: there is no `Status` column to hold "which stage is this ride in." This document does cancellation needs no supervisor approval — a driver/dispatcher cancels directly.
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).
``` ```
Draft → Open → Active → Closed Open → Closed
↘ CancelRequested → Cancelled Open → Cancelled
``` ```
| Transition | Trigger | What it does | | 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`. | | (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 → Active | `POST /api/rides/{id}/activate` | Called by loading event confirmation. Sets `ActivatedAt`. | | 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). |
| 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). | | 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. |
| 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`. |
--- ---
@@ -90,25 +92,26 @@ Draft → Open → Active → Closed
Carried over from [03_ride_lifecycle.md](../03_ride_lifecycle.md#business-rules), with the Carried over from [03_ride_lifecycle.md](../03_ride_lifecycle.md#business-rules), with the
route-dependent rules removed: 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. 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. 2. A vehicle cannot be on more than one Open ride at a time.
3. A CancelRequested ride cannot close or activate until the request is resolved. 3. A cancel action must include a `TypeCode` — free text reason alone is not sufficient.
4. All cancel requests must have a `TypeCode` — free text reason alone is not sufficient. 4. All list endpoints return paginated responses.
5. All list endpoints return paginated responses. 5. **A ride carries no order, task, route, or warehouse linkage of its own.** Creating a ride never
6. **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 — touches `Order`, `DeliveryTask`, `LoadingSheet`, or `StockBalance`/`StockMovement` records —
those remain entirely separate concerns, attached (if at all) by whatever future mechanism those remain entirely separate concerns, attached (if at all) by whatever future mechanism
replaces route-derived linkage and warehouse reconciliation. replaces route-derived linkage and warehouse reconciliation.
**Removed rules** (no longer apply once `Route`, `RideType`, `Status`, and `FK_SourceWarehouseId` **Removed rules** (no longer apply once `Route`, `RideType`, `FK_SourceWarehouseId`, Draft/Active
are gone): stages, and the CancelRequested approval flow are gone):
- ~~"A ride uses a single route for now."~~ - ~~"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 - ~~"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 - ~~"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 warehouse on the ride record to compare "loading" against "unloading" anymore. See
[Impact on Dependent Features](#impact-on-dependent-features). [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 | | Method | Route | Description |
|---|---|---| |---|---|---|
| POST | `/api/rides` | Create ride (Draft): `{ vehicleId, driverProfileId }`. Nothing else required. | | 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. (Filtering by `status`/`type` depends on how [Open Question 4](#open-questions) resolves.) | | 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`. | | 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 | | PUT | `/api/rides/{id}/driver` | Assign or change driver profile |
| POST | `/api/rides/{id}/open` | Transition Draft → Open | | POST | `/api/rides/{id}/close` | Transition Open → Closed |
| POST | `/api/rides/{id}/activate` | Transition Open → Active | | POST | `/api/rides/{id}/cancel` | Cancel an Open ride directly: `{ typeCode, reason }`. Transitions Open → Cancelled immediately, no approval step. |
| POST | `/api/rides/{id}/close` | Transition Active → Closed | | 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). |
| 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. |
`DriverComplianceConfig` and `DriverProfile` endpoints are unaffected by this refactor. `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 ## Impact on Dependent Features
Removing `FK_RouteId`, `RideType`, `Status`, and `FK_SourceWarehouseId` from `Ride` is not a purely Removing `FK_RouteId`, `RideType`, and `FK_SourceWarehouseId` from `Ride` (and collapsing `Status`
additive/subtractive field change — several existing behaviors currently derive their entire input to three values with direct cancellation) is not a purely additive/subtractive field change —
from these fields, and none of them get a replacement in this pass. Each is flagged here as several existing behaviors currently derive their entire input from the removed fields, and none of
**known-broken, deferred follow-up work**, not solved: 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`) — 1. **Delivery task generation** (`Features/DeliveryRide/DeliveryTaskService.GenerateAsync`) —
currently calls `orderRepo.GetPendingByRouteIdAsync(ride.FK_RouteId)` to auto-create delivery 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 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" 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` + 2. **Loading sheet auto-population** (`Features/LoadingEvent/LoadingSheetService` +
`LoadingSheetRepository.GetAggregatedOrderItemsByRouteIdAsync`) — auto-fills a Delivery ride's `LoadingSheetRepository.GetAggregatedOrderItemsByRouteIdAsync`) — auto-fills a Delivery ride's
loading-sheet items by aggregating orders on `Order.Store.FK_RouteId == ride.FK_RouteId`. Same 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 gap as above; **resolved in [loading_event.md](./loading_event.md)**, which generates a
own refactor pass once this lands. `LoadingSheet` per PO instead (see [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)).
3. **Breakdown-assist endpoint** (`RideService.GetBreakdownAssistAsync`) — uses 3. **Breakdown-assist endpoint** (`RideService.GetBreakdownAssistAsync`) — previously used
`strandedRide.FK_RouteId` to list the stranded route's open orders for manual reassignment. With `strandedRide.FK_RouteId` to list the stranded route's open orders for manual reassignment. Per
no route, this listing has no scope; the endpoint's shape needs to change (e.g., list all open direction from the business owner, this listing is **removed outright**, not redesigned to scope
orders regardless of route, or drop this listing) — not decided here. 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 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 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 `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 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 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 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 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. 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 ## Open Questions
1. **How does a Delivery ride's orders/tasks/items get attached, if not via Route?** — 1. **How does a Delivery ride's orders/tasks/items get attached, if not via Route?** — Still
Deliberately unresolved. Per the business owner's direction, the Ride entity itself should not unresolved, but partially constrained: `Order` already has a direct `FK_StoreId`
carry that responsibility going forward; whatever mechanism replaces route-derived linkage (if (`Distribution/Models/Order.cs`) independent of Route, so a future attachment mechanism can key
any) is a separate design effort, not part of slimming down the Ride record. off Store directly without reintroducing Route anywhere. The exact mechanism (manual dispatcher
2. **What happens to `GetBreakdownAssistAsync`'s route-scoped order listing?** — Needs its own assignment, an implicit link at task-creation time, or something else) is still a separate design
decision once (1) is resolved; not designed here. 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) 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. 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 4. ~~**How is a ride's current workflow stage tracked without a stored `Status` column?**~~ —
Draft/Open/Active/CancelRequested/Cancelled/Closed stages are kept conceptually (see **Resolved:** the workflow collapses to three stages (Open, Cancelled, Closed — see
[State Machine](#state-machine)), but this document doesn't decide whether state is derived from [State Machine](#state-machine)), simple enough that `Status` is kept as a stored `RideStatus`
the existing timestamp fields (`StartedAt`/`ActivatedAt`/`ClosedAt` plus the presence of an open column again.
`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.
5. **What replaces stock-return enforcement at Close/Cancel without `FK_SourceWarehouseId`?** — See 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. [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 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: 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 - Drop the `FK_RouteId`, `RideType`, `ActivatedAt`, and `FK_SourceWarehouseId` columns from
`DeliveryRun`/`Ride` and their Fluent/EF configuration. `DeliveryRun`/`Ride` and their Fluent/EF configuration. `Status` stays, re-typed to the narrowed
- Remove `RouteId`, `RideType`, `Status`, and `SourceWarehouseId`/`SourceWarehouseName` from `RideStatus` (`Open`/`Cancelled`/`Closed`). Add `CancelTypeCode`, `CancelReason`, `CancelledAt`
`CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. 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 - Remove the `.Include(r => r.Route)`/`.Include(r => r.SourceWarehouse)` calls and
`GetActiveOrdersByRouteIdAsync` usage in `RideRepository.cs`/`RideService.cs` once the `GetActiveOrdersByRouteIdAsync` usage in `RideRepository.cs`/`RideService.cs` — the breakdown-assist
breakdown-assist redesign (Open Question 2) lands. 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` `<Select>` fields (and the `RideType` picker) - Remove the required `Route` and `Source Warehouse` `<Select>` fields (and the `RideType` picker)
from `ride-sheet.tsx` (frontend create form). from `ride-sheet.tsx` (frontend create form).
- `DeliveryTaskService.GenerateAsync` and `LoadingSheetService`'s route-based aggregation need a - `DeliveryTaskService.GenerateAsync` and `LoadingSheetService`'s route-based aggregation need a
redesign or removal before this ships — this is the load-bearing blocker, not a cleanup item. redesign or removal before this ships — this is the load-bearing blocker, not a cleanup item.
- Resolve Open Question 4 (how workflow stage is tracked without `Status`) before touching the
`RideController`/`RideService` transition methods — they currently branch on the stored enum.
- Resolve Open Question 5 (stock-return enforcement without a source warehouse) before touching - Resolve Open Question 5 (stock-return enforcement without a source warehouse) before touching
`Close`/`CancelRequest` approval logic in `RideService.cs`. `Close`/`Cancel` logic in `RideService.cs`.