refactor
This commit is contained in:
Binary file not shown.
@@ -46,128 +46,80 @@ start the dispatch/receipt workflow on it*.
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| ~~`FK_OrderId`~~ | — | **Removed.** Replaced by `LoadingSheetOrder` linking table. |
|
||||
| **`FK_RideId`** | Guid | **Required. Set at population time** — dispatcher provides the ride when triggering populate. |
|
||||
| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is now tracked per item, not per sheet — a single sheet can be fulfilled from multiple warehouses (see `LoadingSheetItem` below). |
|
||||
| Status | LoadingSheetStatus | See state machine below. |
|
||||
| **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. |
|
||||
| **`PopulatedAt`** | DateTime | **New.** When population was triggered. |
|
||||
| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step — ride is set at populate time. |
|
||||
| ~~`AssignedBy`~~ | — | **Removed.** Same reason. |
|
||||
| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (locked). |
|
||||
| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. |
|
||||
| DispatchConfirmedAt | DateTime? | Path B only — see Open Questions. |
|
||||
| DispatchConfirmedBy | Guid? | Path B only. |
|
||||
| ReceiptConfirmedAt | DateTime? | Path B only. |
|
||||
| ReceiptConfirmedBy | Guid? | Path B only. |
|
||||
| **`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. |
|
||||
|
||||
### New Entity — `LoadingSheetOrder` (linking table)
|
||||
|
||||
One loading sheet aggregates multiple Confirmed POs. This table records which POs were included.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| FK_LoadingSheetId | Guid | FK → LoadingSheet |
|
||||
| FK_OrderId | Guid | FK → Order (must be a Confirmed PO at population time) |
|
||||
|
||||
### Updated Entity — `LoadingSheetItem`
|
||||
|
||||
`LoadingSheetItem` gains versioning, a source reference, and a per-item warehouse:
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_LoadingSheetId | Guid | FK → LoadingSheet |
|
||||
| **`FK_WarehouseId`** | Guid | **New. Required.** The warehouse this specific item is loaded from. Different items on the same sheet can come from different warehouses. Set by dispatcher when editing items. |
|
||||
| **`FK_OrderItemId`** | Guid? | **New.** Traces back to the source `OrderItem` from the Confirmed PO. Null for manually added lines. |
|
||||
| FK_ItemId | Guid | FK → Item (product) |
|
||||
| QuantityDispatched | decimal | Quantity to load from `FK_WarehouseId` |
|
||||
| QuantityReceived | decimal? | Driver's received qty (Path B only) |
|
||||
| QuantityResolved | decimal? | Supervisor's resolved qty after discrepancy (Path B only) |
|
||||
| **`IsActive`** | bool | **Versioning.** Every edit soft-deletes current items (`IsActive = false`) and writes a fresh set. Active items = latest version; soft-deleted = history. |
|
||||
| CreatedAt | DateTime | From Template |
|
||||
`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, // populated by dispatcher (ride set, items editable/versioned)
|
||||
Confirmed, // items locked — stock movement path TBD (see Open Questions)
|
||||
DispatchConfirmed, // Path B only: warehouse confirms dispatch
|
||||
ReceiptConfirmed, // Path B only: driver confirms receipt, stock moves
|
||||
DiscrepancyFlagged, // Path B only: received qty mismatch
|
||||
Resolved // Path B only: supervisor resolves discrepancy, stock moves
|
||||
Planned, // 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 `Planned` and `Confirmed`. `Assigned` is
|
||||
removed — ride is now set at populate time, so there is no separate assign step.
|
||||
`DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and `Resolved` are retained for
|
||||
Path B — see Open Questions.
|
||||
`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 -> Confirmed -> [Path A: stock moves here]
|
||||
\-> [Path B: DispatchConfirmed -> ReceiptConfirmed]
|
||||
\-> DiscrepancyFlagged -> Resolved
|
||||
Planned -> Assigned -> DispatchConfirmed -> ReceiptConfirmed
|
||||
\-> DiscrepancyFlagged -> Resolved
|
||||
```
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| (create) -> Planned | `POST /api/loading-sheets/populate` | Dispatcher provides filters (route/agent/shop) + `rideId`. System finds all matching Confirmed POs, creates one `LoadingSheet` with `FK_RideId` set, populates `LoadingSheetItem` rows from their `OrderItem` lines, records PO links in `LoadingSheetOrder`. `PopulatedBy`/`PopulatedAt` set. Items editable (versioned via `IsActive`). Dispatcher also sets `FK_WarehouseId` per item (which warehouse each item loads from). |
|
||||
| Planned -> Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen (no further edits). Sets `ConfirmedAt`/`ConfirmedBy`. Stock movement path TBD (see Open Questions). |
|
||||
| Confirmed -> DispatchConfirmed | `POST /{id}/confirm-dispatch` | **Path B only.** Warehouse manager confirms dispatch quantities. |
|
||||
| DispatchConfirmed -> ReceiptConfirmed | `POST /{id}/confirm-receipt` | **Path B only.** Driver's received quantities all match. Stock movements created — one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. No ride status transition. |
|
||||
| DispatchConfirmed -> DiscrepancyFlagged | `POST /{id}/confirm-receipt` | **Path B only.** Any line mismatch. No stock movements yet. |
|
||||
| DiscrepancyFlagged -> Resolved | `POST /{id}/resolve` | **Path B only.** Supervisor enters `QuantityResolved` per line. Stock movements created using resolved quantities. |
|
||||
| (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 entirely.
|
||||
Replaces `04_loading_event.md`'s "Item Population by Ride Type" section — there is no more
|
||||
Delivery/Sale split:
|
||||
|
||||
A dispatcher manually triggers population via `POST /api/loading-sheets/populate` providing:
|
||||
|
||||
| Input | Notes |
|
||||
|---|---|
|
||||
| `rideId` | **Required.** The ride this loading sheet is for. Must be an `Open` ride. |
|
||||
| `routeId` | Filter — all Confirmed POs for shops on this route |
|
||||
| `agentId` | Filter — all Confirmed POs created by this agent |
|
||||
| `shopId` | Filter — all Confirmed POs for this specific shop |
|
||||
|
||||
At least one filter must be provided alongside `rideId`. The system finds all matching `Order`
|
||||
records where items are locked (Confirmed PO), aggregates their active `OrderItem` lines into one
|
||||
`LoadingSheet`, and records each PO in `LoadingSheetOrder`.
|
||||
|
||||
`LoadingSheetItem.FK_OrderItemId` traces each line back to its source `OrderItem` for audit.
|
||||
|
||||
After population, the dispatcher sets `FK_WarehouseId` on each item — specifying which warehouse
|
||||
that item is loaded from. Different items can come from different warehouses. Items are editable
|
||||
while `Planned` — every edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`)
|
||||
and writes a fresh set. Latest version = `IsActive = true` rows.
|
||||
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
|
||||
|
||||
One stock movement pair per `LoadingSheetItem` (active rows only). Created exactly once.
|
||||
|
||||
- **Debit**: `LoadingSheetItem.FK_WarehouseId` (the warehouse that item loads from)
|
||||
- **Credit**: the vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`)
|
||||
- **Type**: `Load`
|
||||
|
||||
Since different items can have different `FK_WarehouseId` values, one loading sheet can produce
|
||||
stock movements from multiple warehouses into one vehicle.
|
||||
|
||||
**When** movements are created depends on the path chosen (see Open Questions):
|
||||
- **Path A** — stock moves at `Confirmed`
|
||||
- **Path B** — stock moves at `ReceiptConfirmed` or `Resolved`
|
||||
**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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -177,37 +129,43 @@ Base route: `/api/loading-sheets`
|
||||
|
||||
| Method | Route | Who | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/populate` | Dispatcher | Trigger population: `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Finds all matching Confirmed POs, creates one `LoadingSheet` in `Planned` status with `FK_RideId` set, populates items. |
|
||||
| GET | `/` | Any | List sheets. Filter by `routeId`, `agentId`, `shopId`, `rideId`, `status`. Paginated. |
|
||||
| GET | `/{id}` | Any | Get sheet detail. Active items only by default; `?history=true` includes soft-deleted versions. |
|
||||
| PUT | `/{id}/items` | Dispatcher | Rewrite items while `Planned`. Each item must include `FK_WarehouseId`. Soft-deletes current rows (`IsActive = false`) and writes a fresh set. |
|
||||
| POST | `/{id}/confirm` | Dispatcher | Lock the sheet. `Planned -> Confirmed`. Items frozen. |
|
||||
| POST | `/{id}/confirm-dispatch` | Warehouse mgr | **Path B only.** `Confirmed -> DispatchConfirmed`. |
|
||||
| PUT | `/{id}/receipt-items` | Driver | **Path B only.** Set `QuantityReceived` per line. |
|
||||
| POST | `/{id}/confirm-receipt` | Driver | **Path B only.** `-> ReceiptConfirmed` or `DiscrepancyFlagged`. |
|
||||
| POST | `/{id}/resolve` | Supervisor | **Path B only.** Enter `QuantityResolved` per line `-> Resolved`. |
|
||||
| ~~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 manually by a dispatcher via `POST /api/loading-sheets/populate` —
|
||||
not automatically. `rideId` and at least one PO filter (routeId, agentId, or shopId) are required.
|
||||
2. The ride (`FK_RideId`) is set at populate time — there is no separate assign-ride step.
|
||||
3. Only Confirmed POs (locked items) are included in population — Shop POs are excluded.
|
||||
4. One `LoadingSheet` aggregates all matching Confirmed POs — not one sheet per PO.
|
||||
5. A ride may have more than one `LoadingSheet` — each progresses independently.
|
||||
6. The target ride must be `Open` at populate time.
|
||||
7. Each `LoadingSheetItem` carries its own `FK_WarehouseId` — items on the same sheet can come
|
||||
from different warehouses. There is no sheet-level source warehouse.
|
||||
8. Items are editable (versioned via `IsActive`) while `Planned` — frozen at `Confirmed`.
|
||||
9. Every item edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes
|
||||
a fresh set. Active rows = latest version; soft-deleted = history.
|
||||
10. Stock movements are created exactly once — one debit per item warehouse, one credit to the
|
||||
vehicle. Timing depends on path chosen (see Open Questions).
|
||||
11. No `Ride.Status` transition is triggered at any point in this flow.
|
||||
12. All list endpoints return paginated responses.
|
||||
13. Vehicle capacity check is not enforced in this step.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,10 +194,10 @@ Base route: `/api/loading-sheets`
|
||||
| Code | Who |
|
||||
|---|---|
|
||||
| `loading.view` | All roles |
|
||||
| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items, confirm sheet |
|
||||
| `loading.dispatch` | Warehouse manager — Path B dispatch confirmation |
|
||||
| `loading.receive` | Driver — Path B receipt confirmation |
|
||||
| `loading.resolve` | Supervisor — Path B discrepancy resolution |
|
||||
| **`loading.assign`** | **New.** Dispatcher and above |
|
||||
| `loading.dispatch` | Warehouse manager and above |
|
||||
| `loading.receive` | Driver |
|
||||
| `loading.resolve` | Supervisor and above |
|
||||
|
||||
---
|
||||
|
||||
@@ -256,14 +214,17 @@ a specific ride/vehicle, which didn't exist as a separate step before.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A:
|
||||
warehouse confirms and dispatches in one step) or at `ReceiptConfirmed`/`Resolved` (Path B:
|
||||
driver receipt confirmation). Both paths are retained until this is decided.
|
||||
2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager, or
|
||||
either? Not specified here.
|
||||
3. **What happens to a `Planned` sheet if it is never assigned to a ride?** — no cancellation or
|
||||
timeout path is designed here.
|
||||
4. Vehicle capacity check — still deferred.
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,28 +15,25 @@ the PO/Invoice directly, not from a Route. How that `LoadingSheet` later attache
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the Delivery-vs-Sale ride-type split with a single unified pipeline. The PO and Invoice are
|
||||
two separate but linked concerns — the PO tracks what the shop ordered; the Invoice tracks the
|
||||
billing document that goes with the delivery.
|
||||
Replace the Delivery-vs-Sale ride-type split as the organizing concept for how a store's order gets
|
||||
turned into money. Today `05_delivery_ride.md` (rep takes an order now, delivers days later) and
|
||||
`06_sale_ride.md` (rep+driver visit together and sell on the spot) are two separate flows. Underneath,
|
||||
both are the same shape: a rep collects a **PO** from the shop — whether fulfilled immediately or
|
||||
days later — and once the PO exists, an **Invoice** is generated from it. That invoice always moves
|
||||
through the same three statuses regardless of which scenario produced it:
|
||||
|
||||
### PO lifecycle (on `Order`)
|
||||
1. **Pending** — invoice just created from the PO; the PO's line items are freely editable.
|
||||
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
|
||||
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.
|
||||
|
||||
1. **Shop PO** — PO created by sales agent, line items freely editable (versioned via `IsActive`).
|
||||
2. **Confirmed PO** — office user confirms the PO, locking its items. Invoice creation depends on
|
||||
the system's active mode (see [Invoice Creation Modes](#invoice-creation-modes) below).
|
||||
|
||||
### Invoice lifecycle (on `Invoice`)
|
||||
|
||||
An Invoice is **not** auto-created when a PO is created. It is created by an office user action —
|
||||
either combined with PO confirmation (Mode A) or as a separate step after confirmation (Mode B).
|
||||
Once created, the Invoice always moves through these states:
|
||||
|
||||
1. **Draft** — Invoice created, ready to be printed.
|
||||
2. **Printed** — Invoice physically printed and handed to the agent for the field. The agent makes
|
||||
handwritten edits on the paper copy during the ride.
|
||||
3. **Finalized** — after the ride or day-end, the agent returns the printed invoice. The office
|
||||
worker enters any edits the agent made into the system and submits — locking the invoice as the
|
||||
final billing record.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -59,9 +56,6 @@ Order ("PO")
|
||||
├── FK_StoreId (Guid) — required, unchanged
|
||||
├── FK_SaleId (Guid?) — now OPTIONAL (was required). See Impact section — a PO can now
|
||||
│ exist before any Sale/payment event completes.
|
||||
├── InvoiceId (Guid?) — NOT a FK constraint. Set when the Invoice is created for this PO
|
||||
│ (Mode A or B). Null until then. Stored for lookup convenience —
|
||||
│ no referential enforcement at DB level.
|
||||
├── TotalAmount (decimal) — unchanged, though only authoritative once the linked Invoice is
|
||||
│ Finalized (see below)
|
||||
├── Status (OrderStatus) — unchanged: Pending, PartiallyFulfilled, Fulfilled, Cancelled.
|
||||
@@ -80,101 +74,88 @@ below), not by `Order.Status` directly.
|
||||
```
|
||||
Invoice
|
||||
├── Id (Guid) — PK, from Template
|
||||
├── FK_OrderId (Guid) — required. One PO → one Invoice (1:1).
|
||||
├── Status (InvoiceStatus) — Draft, Printed, Finalized
|
||||
├── CreatedBy (Guid) — UserId of office user who created the invoice
|
||||
├── PrintedAt (DateTime?) — set on Draft → Printed
|
||||
├── PrintedBy (Guid?) — UserId of whoever triggered the print
|
||||
├── FinalizedAt (DateTime?) — set on Printed → Finalized
|
||||
├── FinalizedBy (Guid?) — UserId of office worker who entered agent edits and submitted
|
||||
├── TotalAmount (decimal) — reflects current line items through Draft/Printed; becomes the
|
||||
│ authoritative locked amount once Finalized
|
||||
├── FK_OrderId (Guid) — required. One PO → one Invoice for v1 (see Open Questions).
|
||||
├── Status (InvoiceStatus) — Pending, Draft, Finalized
|
||||
├── 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
|
||||
│ authoritative, locked amount once Finalized
|
||||
├── 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
|
||||
public enum InvoiceStatus { Draft, Printed, Finalized } // Finalized is the terminal state
|
||||
public enum InvoiceStatus { Pending, Draft, Finalized }
|
||||
```
|
||||
|
||||
### Invoice Creation Modes
|
||||
|
||||
In a given system instance, **exactly one** of the following modes is active. The active mode is
|
||||
controlled by a system-level configuration flag (`InvoiceCreationMode: "A" | "B"`). Both modes
|
||||
cannot be active at the same time.
|
||||
|
||||
**Mode A — Confirm PO + Create Invoice (combined action)**
|
||||
- Office user presses "Confirm PO & Create Invoice"
|
||||
- Single endpoint: `POST /api/orders/{id}/confirm-and-create-invoice`
|
||||
- Confirms the PO (locks `OrderItem` lines) and creates the Invoice at `Draft` in one action.
|
||||
|
||||
**Mode B — Create Invoice from Confirmed POs (separate action)**
|
||||
- PO confirmation is a separate step: `POST /api/orders/{id}/confirm`
|
||||
- Office user then navigates to invoice creation, filters confirmed POs by agent ID, route ID, or
|
||||
shop ID to find the right PO, and creates the invoice: `POST /api/invoices { orderId }`
|
||||
- Invoice created at `Draft`.
|
||||
|
||||
### `LoadingSheet` — populated from Confirmed POs, separate flow
|
||||
|
||||
Loading sheets are populated from Confirmed POs as a separate dispatcher-triggered flow — not
|
||||
auto-triggered at PO confirm or invoice create time. One loading sheet aggregates multiple Confirmed
|
||||
POs based on route/agent/shop filters. See [loading_event.md](./loading_event.md) and
|
||||
[dispatch_pipeline.md](./dispatch_pipeline.md) for the full loading sheet lifecycle.
|
||||
An `Invoice` is created automatically the moment a PO (`Order`) is created, starting at `Pending` —
|
||||
there is no separate "create invoice" action.
|
||||
|
||||
---
|
||||
|
||||
## State Machine
|
||||
|
||||
### PO (`Order`) transitions
|
||||
```
|
||||
Pending → Draft → Finalized
|
||||
```
|
||||
|
||||
```
|
||||
(create) → Shop PO → Confirmed PO
|
||||
```
|
||||
No branch/cancel path is designed in this pass (unlike the Ride state machine) — cancellation of a
|
||||
PO/Invoice is not addressed here; flagged as an open question below.
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| (create) → Shop PO | `POST /api/orders` | PO created. `OrderItem` lines editable — every edit soft-deletes current rows (`IsActive = false`) and writes a fresh set. Active rows = latest version. |
|
||||
| Shop PO → Confirmed PO | **Mode A:** `POST /api/orders/{id}/confirm-and-create-invoice` | Confirms PO and creates Invoice at `Draft` in one atomic action. `OrderItem` lines locked. `Order.InvoiceId` set to the new Invoice's Id. |
|
||||
| Shop PO → Confirmed PO | **Mode B:** `POST /api/orders/{id}/confirm` | Confirms PO only. `OrderItem` lines locked. `Order.InvoiceId` remains null until invoice is created separately. |
|
||||
|
||||
### Invoice transitions
|
||||
|
||||
```
|
||||
Draft → Printed → Finalized
|
||||
```
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| (create) → Draft | **Mode A:** combined with PO confirm above | Invoice created at `Draft`. |
|
||||
| (create) → Draft | **Mode B:** `POST /api/invoices` `{ orderId }` | Office user selects a Confirmed PO (filtered by agentId / routeId / shopId) and creates the Invoice at `Draft`. `Order.InvoiceId` set to the new Invoice's Id. |
|
||||
| Draft → Printed | `POST /api/invoices/{id}/print` | Invoice is physically printed. `PrintedAt`/`PrintedBy` set. Agent takes the printed copy to field and makes handwritten edits during the ride. |
|
||||
| Printed → Finalized | `POST /api/invoices/{id}/complete` | After ride/day-end, agent returns printed invoice. Office worker enters any edits the agent made (quantity adjustments, returns, damage, discounts) and submits. `TotalAmount` locked. `FinalizedAt`/`FinalizedBy` set. |
|
||||
| (implicit) → Pending | PO (`Order`) created | Invoice is created automatically, `Status = Pending`. `OrderItem` lines fully editable. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
1. `OrderItem` editing is allowed **only** while the PO is a Shop PO (before confirmation). Every
|
||||
edit soft-deletes all current `OrderItem` rows (`IsActive = false`) and creates a fresh set —
|
||||
active rows are the latest version, soft-deleted rows are history. Once confirmed, items are
|
||||
locked permanently.
|
||||
2. Invoice is **not** auto-created at PO creation — it is always a deliberate office user action.
|
||||
3. Exactly one Invoice Creation Mode (A or B) is active per system instance — controlled by a
|
||||
system-level configuration flag (`InvoiceCreationMode`). Both modes cannot be active at the
|
||||
same time.
|
||||
4. Mode A only: a PO cannot be confirmed without simultaneously creating its invoice — the two
|
||||
actions are atomic.
|
||||
5. Mode B only: a PO can be confirmed without an invoice existing; the invoice is created later as
|
||||
a separate action by the office user.
|
||||
6. An Invoice cannot skip `Printed` — `Draft → Finalized` directly is not a valid transition.
|
||||
7. `Invoice.TotalAmount` is only authoritative once `Finalized`; through `Draft`/`Printed` it
|
||||
reflects the current line items.
|
||||
8. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is a
|
||||
separate axis from `Invoice.Status` — not merged in this pass.
|
||||
9. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded.
|
||||
10. Loading sheet population is a separate dispatcher-triggered flow — not triggered by PO
|
||||
confirmation or invoice creation. See [dispatch_pipeline.md](./dispatch_pipeline.md).
|
||||
1. `OrderItem` add/edit/remove is allowed while the linked `Invoice.Status` is `Pending` **or**
|
||||
`Draft` — both are working-document states.
|
||||
2. An `Invoice` cannot be `Finalized` without first passing through `Draft` — `Pending → Finalized`
|
||||
directly is not a valid transition.
|
||||
3. `Order.TotalAmount`/`Invoice.TotalAmount` is only authoritative once `Finalized`; while
|
||||
Pending/Draft it reflects the current (still-changeable) line items.
|
||||
4. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is
|
||||
untouched and operates on a separate axis from `Invoice.Status` (billing/document lifecycle) —
|
||||
whether these two should eventually merge is an open question, not resolved here.
|
||||
5. `Order.FK_SaleId` is now optional — a PO/Invoice can exist and progress through Pending/Draft
|
||||
before any `Sale` is recorded, covering the take-order-now/deliver-later scenario. See Impact
|
||||
section for what this touches.
|
||||
6. Both ride scenarios (take-order-now-deliver-later vs. on-the-spot sale) create a PO the same way
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -218,19 +199,26 @@ visible and intentional rather than discovered later as a silent regression.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. ~~**Signature/OTP on PO confirmation**~~ — **Resolved:** dropped. Plain action, no OTP.
|
||||
2. ~~**One Invoice per PO or multiple?**~~ — **Resolved:** 1:1 for v1.
|
||||
3. **Should `Order.Status` and `Invoice.Status` eventually merge?** — kept separate in this pass;
|
||||
not decided.
|
||||
4. **How does `Payment` attach to `Invoice`?** — see Impact item 2. Not designed here.
|
||||
5. **Cancellation/void path for a PO or Invoice?** — not addressed here.
|
||||
6. **What edits can the agent make on the printed invoice?** — quantity adjustments, returns,
|
||||
damage, and discounts are assumed but the exact adjustment payload for `POST /complete` is not
|
||||
specified here.
|
||||
7. ~~**How does a LoadingSheet get matched to a Ride?**~~ — **Resolved in
|
||||
[loading_event.md](./loading_event.md):** `POST /{id}/assign-ride`.
|
||||
8. ~~**Does editing PO items after confirm re-sync the LoadingSheet?**~~ — **Resolved:** items lock
|
||||
at confirm; no re-sync needed.
|
||||
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
|
||||
deliberately separate in this pass (fulfillment progress vs. document/billing lifecycle); not
|
||||
decided whether that separation should be permanent.
|
||||
4. **How does `Payment` attach once `Invoice` exists?** — see Impact item 2. Not designed here.
|
||||
5. **Is there a cancellation/void path for a PO or Invoice?** — the state machine in this document
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -238,27 +226,26 @@ 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:
|
||||
|
||||
- Make `Order.FK_SaleId` nullable in `Models/Order.cs`; audit all code paths that assume it's
|
||||
non-null.
|
||||
- Add `Order.InvoiceId` (Guid?, no FK constraint) to `Models/Order.cs` — configure explicitly in
|
||||
Fluent API to ensure no referential constraint is generated. Set it in the same transaction as
|
||||
Invoice creation (both Mode A and Mode B).
|
||||
- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `CreatedBy`, `PrintedAt`, `PrintedBy`,
|
||||
`FinalizedAt`, `FinalizedBy`, `TotalAmount`, plus `Template` base fields) and the `InvoiceStatus`
|
||||
enum (`Draft`, `Printed`, `Finalized`).
|
||||
- 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`, `ConfirmedAt`, `FinalizedAt`, `FinalizedBy`,
|
||||
`TotalAmount`, plus `Template` base fields) and the `InvoiceStatus` enum.
|
||||
- New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`,
|
||||
`InvoiceRepository`, `InvoiceDTOs`) — do not place under `Features/PurchaseOrder/`.
|
||||
- Add a system configuration flag `InvoiceCreationMode` (`"A"` or `"B"`) — read at startup; gate
|
||||
the Mode A and Mode B endpoints accordingly so only the active mode's endpoints are reachable.
|
||||
- **Mode A endpoint:** `POST /api/orders/{id}/confirm-and-create-invoice` — atomic: locks
|
||||
`OrderItem` rows and creates `Invoice` at `Draft` in one transaction.
|
||||
- **Mode B endpoints:** `POST /api/orders/{id}/confirm` (locks items only) + `POST /api/invoices`
|
||||
`{ orderId }` (creates Invoice at `Draft` from a Confirmed PO). Add `GET /api/orders` filter
|
||||
params `agentId`, `routeId`, `shopId` to support the office user's PO selection UI.
|
||||
- `POST /api/invoices/{id}/print` — sets `PrintedAt`/`PrintedBy`, transitions to `Printed`.
|
||||
- `POST /api/invoices/{id}/complete` — accepts adjustment payload (quantities, returns, damage,
|
||||
discounts); locks `TotalAmount`; sets `FinalizedAt`/`FinalizedBy`. Resolve Open Question 6
|
||||
before specifying the exact payload shape.
|
||||
- `LoadingSheet` population is a separate flow — see [dispatch_pipeline.md](./dispatch_pipeline.md).
|
||||
- A new PO-creation path independent of `SaleService.CreateAsync` is needed for the
|
||||
take-order-now/deliver-later scenario (Impact item 1) — not specified here.
|
||||
`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.
|
||||
- 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 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`.
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# [Refactor] Ride Lifecycle — Route-Free, Type-Free Rides
|
||||
|
||||
**Status**: Proposed (design only — no code changes yet)
|
||||
**Refactors**: [../03_ride_lifecycle.md](../03_ride_lifecycle.md) (kept as-is, this is the target design)
|
||||
**Depends On**: 02 — Stock Ledger
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
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 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).
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Updated Entity — Ride (`DeliveryRun`)
|
||||
|
||||
```
|
||||
Ride (DeliveryRun)
|
||||
├── Id (Guid) — PK, from Template
|
||||
├── FK_VehicleId (Guid) — required
|
||||
├── FK_DriverProfileId (Guid) — required
|
||||
├── Meterstart (int)
|
||||
├── Meterend (int)
|
||||
├── 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, 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)).
|
||||
- **`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`,
|
||||
`FulfillmentCenter`, `CrossDocking`, `ColdStorage`, `BondedWarehouse`, `AutomatedWarehouse`) stays
|
||||
exactly as-is; it's still useful for warehouse management/reporting elsewhere in the system. What's
|
||||
different from [03_ride_lifecycle.md](../03_ride_lifecycle.md) is that **rides no longer reference
|
||||
a warehouse at all**, so there's nothing on the ride side left to filter or gate by `Type` — this
|
||||
supersedes the first pass of this document, which kept `FK_SourceWarehouseId` and only dropped the
|
||||
type constraint on it. See [Impact on Dependent Features](#impact-on-dependent-features) for what
|
||||
that means for stock reconciliation, which previously depended on `FK_SourceWarehouseId`.
|
||||
|
||||
---
|
||||
|
||||
## State Machine
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
Open → Closed
|
||||
Open → Cancelled
|
||||
```
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| (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. |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
Carried over from [03_ride_lifecycle.md](../03_ride_lifecycle.md#business-rules), with the
|
||||
route-dependent rules removed:
|
||||
|
||||
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 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`, `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, 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.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Same routes as [03_ride_lifecycle.md](../03_ride_lifecycle.md#api-endpoints), with `routeId`,
|
||||
`rideType`, and `sourceWarehouseId` removed from the request/response shapes:
|
||||
|
||||
| Method | Route | Description |
|
||||
|---|---|---|
|
||||
| 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}/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.
|
||||
|
||||
---
|
||||
|
||||
## Impact on Dependent Features
|
||||
|
||||
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. 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; **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
|
||||
stock balance against at all. This is the biggest open gap in this pass — closing/cancelling a
|
||||
ride previously guaranteed the vehicle ends up empty (stock-wise); without a target warehouse,
|
||||
that guarantee has no mechanism to enforce it. Needs its own design — not attempted here.
|
||||
5. **[05_delivery_ride.md](../05_delivery_ride.md)** describes the whole order-to-ride relationship
|
||||
as `Ride → Route → Stores → Pending Orders`. That document goes stale the moment this refactor
|
||||
ships and needs its own rewrite — tracked as follow-up, not attempted in this pass.
|
||||
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 — **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.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
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?**~~ —
|
||||
**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
|
||||
loading-event or order-linkage mechanism eventually replaces the route-derived flow — even though
|
||||
it's removed from `Ride` itself? Not decided here.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (for when this is implemented)
|
||||
|
||||
Not part of this docs-only pass, but captured for the eventual implementation step:
|
||||
|
||||
- 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` — 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` `<Select>` fields (and the `RideType` picker)
|
||||
from `ride-sheet.tsx` (frontend create form).
|
||||
- `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.
|
||||
- Resolve Open Question 5 (stock-return enforcement without a source warehouse) before touching
|
||||
`Close`/`Cancel` logic in `RideService.cs`.
|
||||
Reference in New Issue
Block a user