diff --git a/Distribution/docs/refactor_docs/ride_lifecycle.md b/Distribution/docs/refactor_docs/ride_lifecycle.md new file mode 100644 index 0000000..5061b6c --- /dev/null +++ b/Distribution/docs/refactor_docs/ride_lifecycle.md @@ -0,0 +1,221 @@ +# [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 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. + +--- + +## 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) +├── StartedAt (DateTime?) — set when Draft → Open +├── ActivatedAt (DateTime?) — set when Open → Active +├── ClosedAt (DateTime?) — set when Active → Closed +├── IsActive, CreatedAt — from Template +``` + +That's the whole record: a vehicle, a driver, a meter start/end reading, and the three workflow +timestamps. Everything else is removed: + +- **`FK_RouteId`** — gone (see the original goal above; unchanged from the first pass of this doc). +- **`RideType`** — gone. Nothing in the Ride record itself needs to distinguish Delivery from Sale + once the ride no longer drives order/task auto-population (that was the only place `RideType` + mattered inside the Ride entity's own logic — see + [Impact on Dependent Features](#impact-on-dependent-features)). +- **`Status` (`RideStatus`)** — gone as a stored column. The workflow stages (Draft, Open, Active, + CancelRequested, Cancelled, Closed) are **kept as a concept** — see + [State Machine](#state-machine) — but this document does not resolve how "current state" is + represented without a stored enum. Left as an open question (see below), not solved here. +- **`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`. + +### 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 stages and endpoints are **kept** from [03_ride_lifecycle.md](../03_ride_lifecycle.md#state-machine) +— none of the transitions reference `Route`, `RideType`, or a warehouse by name. What's removed is +the *storage*: there is no `Status` column to hold "which stage is this ride in." This document does +not resolve what replaces it (e.g., deriving state purely from which timestamp fields are set — +`StartedAt` null = Draft, `StartedAt` set + `ActivatedAt` null = Open, etc. — is one option, but not +decided here). Flagged as [Open Question 4](#open-questions). + +``` +Draft → Open → Active → Closed + ↘ CancelRequested → Cancelled +``` + +| Transition | Trigger | What it does | +|---|---|---| +| Draft → Open | `POST /api/rides/{id}/open` | Validates driver compliance. Checks vehicle has no other Open/Active ride. Sets `StartedAt`. | +| Open → Active | `POST /api/rides/{id}/activate` | Called by loading event confirmation. Sets `ActivatedAt`. | +| Active → Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation. Sets `ClosedAt`. Previously required "all stock returned to source warehouse" — with no `FK_SourceWarehouseId` on the ride, this check has nothing to reconcile against; see [Impact on Dependent Features](#impact-on-dependent-features). | +| Active → CancelRequested | `POST /api/rides/{id}/cancel-request` | Driver submits cancel request with type code and reason. | +| CancelRequested → Cancelled | `POST /api/rides/cancel-requests/{id}/approve` | Supervisor approves. Previously required stock returned to source warehouse first — same gap as Close above. | +| CancelRequested → Active | `POST /api/rides/cancel-requests/{id}/reject` | Supervisor rejects. Ride returns to `Active`. | + +--- + +## 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 transition Draft → Open if `IsActive = false` on the driver profile, or (when the + relevant `DriverComplianceConfig` flags are enabled) if license/medical clearance is expired. +2. A vehicle cannot be on more than one Open or Active ride at a time. +3. A CancelRequested ride cannot close or activate until the request is resolved. +4. All cancel requests must have a `TypeCode` — free text reason alone is not sufficient. +5. All list endpoints return paginated responses. +6. **A ride carries no order, task, route, or warehouse linkage of its own.** Creating a ride never + touches `Order`, `DeliveryTask`, `LoadingSheet`, or `StockBalance`/`StockMovement` records — + those remain entirely separate concerns, attached (if at all) by whatever future mechanism + replaces route-derived linkage and warehouse reconciliation. + +**Removed rules** (no longer apply once `Route`, `RideType`, `Status`, and `FK_SourceWarehouseId` +are gone): +- ~~"A ride uses a single route for now."~~ +- ~~"Orders can only be assigned to a Delivery ride while it is in Draft status."~~ — there is no + order-assignment mechanism on the ride at all anymore, in Draft or otherwise. +- ~~"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). + +--- + +## 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 (Draft): `{ vehicleId, driverProfileId }`. Nothing else required. | +| GET | `/api/rides` | List rides (paginated). Filter by driverProfileId, vehicleId. (Filtering by `status`/`type` depends on how [Open Question 4](#open-questions) resolves.) | +| GET | `/api/rides/{id}` | Get ride detail — response no longer includes `routeId`/`routeName`, `rideType`, or `sourceWarehouseId`/`sourceWarehouseName`. | +| PUT | `/api/rides/{id}/driver` | Assign or change driver profile | +| POST | `/api/rides/{id}/open` | Transition Draft → Open | +| POST | `/api/rides/{id}/activate` | Transition Open → Active | +| POST | `/api/rides/{id}/close` | Transition Active → Closed | +| POST | `/api/rides/{id}/cancel-request` | Submit cancel request (Active rides only) | +| GET | `/api/rides/cancel-requests` | List cancel requests (paginated). Filter by status. | +| POST | `/api/rides/cancel-requests/{id}/approve` | Approve cancel request | +| POST | `/api/rides/cancel-requests/{id}/reject` | Reject cancel request | +| GET | `/api/rides/breakdown-assist/{rideId}` | See [Impact on Dependent Features](#impact-on-dependent-features) — this endpoint currently keys off the stranded ride's route and needs rework. | + +`DriverComplianceConfig` and `DriverProfile` endpoints are unaffected by this refactor. + +--- + +## Impact on Dependent Features + +Removing `FK_RouteId`, `RideType`, `Status`, and `FK_SourceWarehouseId` from `Ride` is not a purely +additive/subtractive field change — several existing behaviors currently derive their entire input +from these fields, and none of them get a replacement in this pass. Each is flagged here as +**known-broken, deferred follow-up work**, not solved: + +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. +2. **Loading sheet auto-population** (`Features/LoadingEvent/LoadingSheetService` + + `LoadingSheetRepository.GetAggregatedOrderItemsByRouteIdAsync`) — auto-fills a Delivery ride's + loading-sheet items by aggregating orders on `Order.Store.FK_RouteId == ride.FK_RouteId`. Same + gap as above; documented in [04_loading_event.md](../04_loading_event.md), which will need its + own refactor pass once this lands. +3. **Breakdown-assist endpoint** (`RideService.GetBreakdownAssistAsync`) — uses + `strandedRide.FK_RouteId` to list the stranded route's open orders for manual reassignment. With + no route, this listing has no scope; the endpoint's shape needs to change (e.g., list all open + orders regardless of route, or drop this listing) — not decided here. +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 — flagged, not designed, here. + +None of the above are implemented or redesigned by this document. They're called out so the gap is +visible and intentional rather than discovered later as a silent regression. + +--- + +## Open Questions + +1. **How does a Delivery ride's orders/tasks/items get attached, if not via Route?** — + Deliberately unresolved. Per the business owner's direction, the Ride entity itself should not + carry that responsibility going forward; whatever mechanism replaces route-derived linkage (if + any) is a separate design effort, not part of slimming down the Ride record. +2. **What happens to `GetBreakdownAssistAsync`'s route-scoped order listing?** — Needs its own + decision once (1) is resolved; not designed here. +3. **`04_loading_event.md` / `05_delivery_ride.md` rewrite** — scheduled as follow-up work once (1) + is resolved; explicitly out of scope for this document. +4. **How is a ride's current workflow stage tracked without a stored `Status` column?** — The + Draft/Open/Active/CancelRequested/Cancelled/Closed stages are kept conceptually (see + [State Machine](#state-machine)), but this document doesn't decide whether state is derived from + the existing timestamp fields (`StartedAt`/`ActivatedAt`/`ClosedAt` plus the presence of an open + `RideCancelRequest`), computed some other way, or whether a status-like field comes back under a + different name. Left for the business owner/implementer to resolve. +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`, `Status`, and `FK_SourceWarehouseId` columns from + `DeliveryRun`/`Ride` and their Fluent/EF configuration. +- Remove `RouteId`, `RideType`, `Status`, and `SourceWarehouseId`/`SourceWarehouseName` from + `CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. +- Remove the `.Include(r => r.Route)`/`.Include(r => r.SourceWarehouse)` calls and + `GetActiveOrdersByRouteIdAsync` usage in `RideRepository.cs`/`RideService.cs` once the + breakdown-assist redesign (Open Question 2) lands. +- Remove the required `Route` and `Source Warehouse` `