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:
Kalana B. Thilakarathna
2026-08-24 21:34:57 +05:30
parent a833586e83
commit 6d8b6b3e59
38 changed files with 3938 additions and 563 deletions
+88 -55
View File
@@ -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.