Refactor order invoice lifecycle and ride lifecycle documentation
This commit is contained in:
Binary file not shown.
@@ -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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user