Add DeliveryTask, Payment, and Sale features
Implemented DeliveryTask, Payment, and Sale entities with full CRUD, business logic, and API endpoints. Updated Order/OrderItem for sale linkage and fulfillment tracking. Registered new DbSets and DI services. Revised documentation for delivery, sale, and payment workflows.
This commit is contained in:
@@ -1,41 +1,59 @@
|
||||
# 03 — Ride Lifecycle
|
||||
# 03 - Ride Lifecycle
|
||||
|
||||
**Status**: Done
|
||||
**Depends On**: 02 — Stock Ledger
|
||||
**Depends On**: 02 - Stock Ledger
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Turn the currently-unused `DeliveryRun` model into a fully working ride — a session that turns a
|
||||
Turn the currently-unused `DeliveryRun` model into a fully working ride - a session that turns a
|
||||
vehicle into a temporary warehouse. Covers driver profile management, ride creation, state
|
||||
transitions, cancellation workflow, and the vehicle breakdown scenario.
|
||||
|
||||
---
|
||||
|
||||
## Current Business Understanding
|
||||
|
||||
- A `Ride` is one operational trip for one vehicle and one driver on one route for now.
|
||||
- `RideType` distinguishes business behavior:
|
||||
- `Delivery` ride: used to fulfill existing orders.
|
||||
- `Sale` ride: used by sales agents in the field to create sales, and every sale creates an
|
||||
order.
|
||||
- `Sale` and `Order` are different:
|
||||
- `Sale` is the commercial transaction created by the sales agent.
|
||||
- `Order` is the fulfillment document created from every sale.
|
||||
- Item lines are maintained once in `OrderItem`; `Sale` does not keep a separate item-line table.
|
||||
- `Sale` statuses use sales language such as `Completed`.
|
||||
- `Order` statuses use logistics language such as `Fulfilled`.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### New Entity — DriverProfile
|
||||
### New Entity - DriverProfile
|
||||
|
||||
Linked one-to-one with `User`. Holds compliance data only — no login or auth fields.
|
||||
Linked one-to-one with `User`. Holds compliance data only - no login or auth fields.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_UserId | Guid | FK → User (unique) |
|
||||
| FK_UserId | Guid | FK -> User (unique) |
|
||||
| LicenseNumber | string | |
|
||||
| LicenseExpiry | DateTime | |
|
||||
| MedicalClearanceExpiry | DateTime | |
|
||||
| IsActive | bool | Can be suspended without deleting |
|
||||
| CreatedAt | DateTime | |
|
||||
|
||||
No license class tied to vehicle type — not required.
|
||||
No license class tied to vehicle type - not required.
|
||||
|
||||
**Compliance check rule:** Driver is compliant if `IsActive = true`, plus any checks that are enabled in `DriverComplianceConfig`. By default all checks are disabled. When enabled: `LicenseExpiry > today` AND/OR `MedicalClearanceExpiry > today`.
|
||||
**Compliance check rule:** Driver is compliant if `IsActive = true`, plus any checks that are
|
||||
enabled in `DriverComplianceConfig`. By default all checks are disabled. When enabled:
|
||||
`LicenseExpiry > today` and/or `MedicalClearanceExpiry > today`.
|
||||
|
||||
---
|
||||
|
||||
### Updated Entity — Vehicle
|
||||
### Updated Entity - Vehicle
|
||||
|
||||
Add one nullable field:
|
||||
|
||||
@@ -45,7 +63,7 @@ Add one nullable field:
|
||||
|
||||
---
|
||||
|
||||
### Updated Entity — DeliveryRun (becomes Ride)
|
||||
### Updated Entity - DeliveryRun (becomes Ride)
|
||||
|
||||
Add the following fields to the existing `DeliveryRun`:
|
||||
|
||||
@@ -53,34 +71,43 @@ Add the following fields to the existing `DeliveryRun`:
|
||||
|---|---|---|
|
||||
| RideType | RideType | Delivery or Sale |
|
||||
| Status | RideStatus | State machine below |
|
||||
| FK_DriverProfileId | Guid | FK → DriverProfile |
|
||||
| FK_DriverProfileId | Guid | FK -> DriverProfile |
|
||||
| FK_SourceWarehouseId | Guid | Where stock is loaded from. Must be the same warehouse stock returns to on cancel/close. |
|
||||
| StartedAt | DateTime? | Set when Draft → Open |
|
||||
| ActivatedAt | DateTime? | Set when Open → Active |
|
||||
| ClosedAt | DateTime? | Set when Active → Closed |
|
||||
| StartedAt | DateTime? | Set when Draft -> Open |
|
||||
| ActivatedAt | DateTime? | Set when Open -> Active |
|
||||
| ClosedAt | DateTime? | Set when Active -> Closed |
|
||||
|
||||
---
|
||||
|
||||
### New Enum — RideType
|
||||
### New Enum - RideType
|
||||
|
||||
```csharp
|
||||
public enum RideType { Delivery, Sale }
|
||||
```
|
||||
|
||||
### New Enum — RideStatus
|
||||
### New Enum - RideStatus
|
||||
|
||||
```csharp
|
||||
public enum RideStatus { Draft, Open, Active, CancelRequested, Cancelled, Closed }
|
||||
```
|
||||
|
||||
Meaning of statuses:
|
||||
|
||||
- `Draft`: ride exists but operational work has not started.
|
||||
- `Open`: ride is prepared and ready for loading/preparation work.
|
||||
- `Active`: ride is live in the field.
|
||||
- `CancelRequested`: active ride is waiting for cancellation approval or rejection.
|
||||
- `Cancelled`: ride ended through approved cancellation.
|
||||
- `Closed`: ride ended normally.
|
||||
|
||||
---
|
||||
|
||||
### New Entity — RideCancelRequest
|
||||
### New Entity - RideCancelRequest
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_RideId | Guid | FK → DeliveryRun |
|
||||
| FK_RideId | Guid | FK -> DeliveryRun |
|
||||
| TypeCode | CancelRequestType | Reason category |
|
||||
| Reason | string | Free text explanation |
|
||||
| RequestedBy | Guid | UserId |
|
||||
@@ -89,7 +116,7 @@ public enum RideStatus { Draft, Open, Active, CancelRequested, Cancelled, Closed
|
||||
| ApprovedBy | Guid? | UserId of approver |
|
||||
| ApprovedAt | DateTime? | |
|
||||
|
||||
### New Enum — CancelRequestType
|
||||
### New Enum - CancelRequestType
|
||||
|
||||
```csharp
|
||||
public enum CancelRequestType
|
||||
@@ -101,7 +128,7 @@ public enum CancelRequestType
|
||||
}
|
||||
```
|
||||
|
||||
### New Entity — DriverComplianceConfig
|
||||
### New Entity - DriverComplianceConfig
|
||||
|
||||
Single-row configuration table. Controls which compliance checks are enforced at ride open time.
|
||||
|
||||
@@ -117,7 +144,7 @@ Managed via a dedicated config endpoint. Only one row ever exists.
|
||||
|
||||
---
|
||||
|
||||
### New Enum — CancelRequestStatus
|
||||
### New Enum - CancelRequestStatus
|
||||
|
||||
```csharp
|
||||
public enum CancelRequestStatus { Pending, Approved, Rejected }
|
||||
@@ -127,21 +154,22 @@ public enum CancelRequestStatus { Pending, Approved, Rejected }
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
Draft → Open → Active → Closed
|
||||
↘ CancelRequested → Cancelled
|
||||
```text
|
||||
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 (Step 04). Sets `ActivatedAt`. |
|
||||
| Active → Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation (Step 07). All stock must be returned to source warehouse. Sets `ClosedAt`. |
|
||||
| Active → CancelRequested | `POST /api/rides/{id}/cancel-request` | Driver submits cancel request with type code and reason. Ride status changes to `CancelRequested`. |
|
||||
| CancelRequested → Cancelled | `POST /api/rides/cancel-requests/{id}/approve` | Supervisor approves. Ride status changes to `Cancelled`. All stock must return to source warehouse before cancel completes. |
|
||||
| CancelRequested → Active | `POST /api/rides/cancel-requests/{id}/reject` | Supervisor rejects. Ride returns to `Active`. |
|
||||
| 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 (Step 04). Sets `ActivatedAt`. |
|
||||
| Active -> Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation (Step 07). All stock must be returned to source warehouse. Sets `ClosedAt`. |
|
||||
| Active -> CancelRequested | `POST /api/rides/{id}/cancel-request` | Driver submits cancel request with type code and reason. Ride status changes to `CancelRequested`. |
|
||||
| CancelRequested -> Cancelled | `POST /api/rides/cancel-requests/{id}/approve` | Supervisor approves. Ride status changes to `Cancelled`. All stock must return to source warehouse before cancel completes. |
|
||||
| CancelRequested -> Active | `POST /api/rides/cancel-requests/{id}/reject` | Supervisor rejects. Ride returns to `Active`. |
|
||||
|
||||
**Order assignment window:** Orders can be assigned to a Delivery ride from Draft until the ride transitions to Open. No new orders after Open.
|
||||
**Order assignment window:** Orders can be assigned to a Delivery ride from Draft until the ride
|
||||
transitions to Open. No new orders after Open.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +178,7 @@ Draft → Open → Active → Closed
|
||||
Per vehicle. A vehicle either has `FK_DefaultWarehouseId` set (always loads from that warehouse)
|
||||
or it is null (warehouse is selected manually at ride creation time).
|
||||
|
||||
Loading and unloading warehouse are always the same — stock must return to its source warehouse
|
||||
Loading and unloading warehouse are always the same - stock must return to its source warehouse
|
||||
before a ride can close or cancel.
|
||||
|
||||
---
|
||||
@@ -160,12 +188,12 @@ before a ride can close or cancel.
|
||||
1. Driver submits cancel request with `TypeCode = VehicleBreakdown`.
|
||||
2. Ride status moves to `CancelRequested`.
|
||||
3. Supervisor views a breakdown assist endpoint that lists all currently `Active` rides with their
|
||||
current vehicle stock levels and the stranded ride's open orders — supervisor decides manually
|
||||
current vehicle stock levels and the stranded ride's open orders. Supervisor decides manually
|
||||
which ride takes over which orders.
|
||||
4. Supervisor approves the cancel request.
|
||||
5. Ride moves to `Cancelled` once all stock is confirmed returned to source warehouse.
|
||||
|
||||
The system surfaces the information — it does not automatically reassign orders.
|
||||
The system surfaces the information - it does not automatically reassign orders.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,14 +207,14 @@ Base route: `/api/rides`
|
||||
| GET | `/` | List rides (paginated). Filter by status, type, driverProfileId, vehicleId. |
|
||||
| GET | `/{id}` | Get ride detail |
|
||||
| PUT | `/{id}/driver` | Assign or change driver profile |
|
||||
| POST | `/{id}/open` | Transition Draft → Open |
|
||||
| POST | `/{id}/activate` | Transition Open → Active (called by loading event) |
|
||||
| POST | `/{id}/close` | Transition Active → Closed (called by reconciliation) |
|
||||
| POST | `/{id}/open` | Transition Draft -> Open |
|
||||
| POST | `/{id}/activate` | Transition Open -> Active (called by loading event) |
|
||||
| POST | `/{id}/close` | Transition Active -> Closed (called by reconciliation) |
|
||||
| POST | `/{id}/cancel-request` | Submit cancel request (Active rides only) |
|
||||
| GET | `/cancel-requests` | List cancel requests (paginated). Filter by status. |
|
||||
| POST | `/cancel-requests/{id}/approve` | Approve cancel request |
|
||||
| POST | `/cancel-requests/{id}/reject` | Reject cancel request |
|
||||
| GET | `/breakdown-assist/{rideId}` | Lists active rides with stock + stranded ride's open orders |
|
||||
| GET | `/breakdown-assist/{rideId}` | Lists active rides with stock plus stranded ride's open orders |
|
||||
|
||||
### DriverComplianceConfig
|
||||
|
||||
@@ -215,37 +243,42 @@ Base route: `/api/driver-profiles`
|
||||
|
||||
## Business Rules
|
||||
|
||||
1. A ride cannot transition Draft → Open if `IsActive = false` on the driver profile. If `EnforceLicenseExpiry = true`, also blocks if license is expired. If `EnforceMedicalClearance = true`, also blocks if medical clearance is expired.
|
||||
1. A ride cannot transition Draft -> Open if `IsActive = false` on the driver profile. If
|
||||
`EnforceLicenseExpiry = true`, also block if license is expired. If
|
||||
`EnforceMedicalClearance = true`, also block if medical clearance is expired.
|
||||
2. A vehicle cannot be on more than one Open or Active ride at a time.
|
||||
3. Orders can only be assigned to a Delivery ride while it is in Draft status.
|
||||
4. Loading and unloading warehouse must be the same — enforced at close/cancel.
|
||||
4. Loading and unloading warehouse must be the same - enforced at close/cancel.
|
||||
5. A CancelRequested ride cannot close or activate until the request is resolved.
|
||||
6. All cancel requests must have a TypeCode — free text reason alone is not sufficient.
|
||||
6. All cancel requests must have a TypeCode - free text reason alone is not sufficient.
|
||||
7. All list endpoints return paginated responses.
|
||||
8. A ride uses a single route for now.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Cancel request type codes** — ✅ Resolved. `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other`.
|
||||
2. **Breakdown assist endpoint** — ✅ Resolved. System shows info only, supervisor decides manually. No auto-reassignment.
|
||||
3. **Driver compliance** — ✅ Resolved. Checks are configurable via `DriverComplianceConfig`. `IsActive` is always enforced. License and medical checks are optional flags, default off.
|
||||
1. Cancel request type codes - resolved. `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`,
|
||||
`Other`.
|
||||
2. Breakdown assist endpoint - resolved. System shows info only, supervisor decides manually. No
|
||||
auto-reassignment.
|
||||
3. Driver compliance - resolved. Checks are configurable via `DriverComplianceConfig`. `IsActive`
|
||||
is always enforced. License and medical checks are optional flags, default off.
|
||||
4. Multi-route ride assignment - deferred for now. Current model stays single-route.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Build order:**
|
||||
1. ✅ Enums — `RideType`, `RideStatus`, `CancelRequestType`, `CancelRequestStatus` → added to `Models/Enums.cs`
|
||||
2. ✅ Entities — `DriverProfile`, `DriverComplianceConfig`, `RideCancelRequest` (new); updated `Vehicle` (+`FK_DefaultWarehouseId`), updated `DeliveryRun` (+`RideType`, `Status`, `FK_DriverProfileId`, `FK_SourceWarehouseId`, `StartedAt`, `ActivatedAt`, `ClosedAt`)
|
||||
3. ✅ `AppDBContext` — new DbSets + Fluent config
|
||||
4. ✅ Migrations — `RideLifecycle` (schema), `RidePermissions` (10 new permissions seeded across all roles)
|
||||
5. ✅ DTOs — `DriverProfileDTOs.cs`, `DriverComplianceConfigDTOs.cs`, `RideDTOs.cs`
|
||||
6. ✅ Repositories — `IDriverProfileRepository`, `IDriverComplianceConfigRepository`, `IRideRepository`, `IRideCancelRequestRepository`
|
||||
7. ✅ Services — `DriverProfileService`, `DriverComplianceConfigService`, `RideService`
|
||||
8. ✅ Controllers — `DriverProfileController`, `DriverComplianceConfigController`, `RideController`
|
||||
9. ✅ DI registration in `Program.cs`
|
||||
1. Enums - `RideType`, `RideStatus`, `CancelRequestType`, `CancelRequestStatus`
|
||||
2. Entities - `DriverProfile`, `DriverComplianceConfig`, `RideCancelRequest`
|
||||
3. `AppDBContext` - new DbSets plus Fluent config
|
||||
4. Migrations - ride lifecycle schema and permissions
|
||||
5. DTOs - driver profile, compliance config, ride
|
||||
6. Repositories - ride and ride cancel request support
|
||||
7. Services - driver profile, compliance config, ride
|
||||
8. Controllers - driver profile, compliance config, ride
|
||||
9. DI registration in `Program.cs`
|
||||
|
||||
**Feature folders:** `Features/Ride/`, `Features/DriverProfile/`, `Features/DriverComplianceConfig/`
|
||||
|
||||
**Pending:** Tests for the three new services.
|
||||
|
||||
Reference in New Issue
Block a user