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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 04 — Loading Event
|
||||
# 04 - Loading Event
|
||||
|
||||
**Status**: Planning
|
||||
**Depends On**: 03 — Ride Lifecycle
|
||||
**Depends On**: 03 - Ride Lifecycle
|
||||
|
||||
---
|
||||
|
||||
@@ -9,32 +9,36 @@
|
||||
|
||||
Move stock from a source warehouse onto an opened ride's vehicle, with two-party confirmation.
|
||||
When loading is confirmed, the vehicle's stock ledger is credited and the ride transitions
|
||||
`Open → Active` automatically. This is the first place the stock movement model gets exercised
|
||||
end-to-end.
|
||||
`Open -> Active` automatically.
|
||||
|
||||
This loading flow serves both ride types:
|
||||
- `Delivery` rides load stock to fulfill orders already linked to the ride.
|
||||
- `Sale` rides load stock so the sales agent can sell in the field and create sale-linked orders.
|
||||
|
||||
---
|
||||
|
||||
## Background / Context
|
||||
|
||||
A vehicle is just a location with `LocationType.Vehicle` in the stock ledger — the same math as
|
||||
a warehouse. When loading is confirmed, two stock movements are created per item:
|
||||
- **Debit** source warehouse: `-qty` at `LocationType.Warehouse`
|
||||
- **Credit** vehicle: `+qty` at `LocationType.Vehicle`
|
||||
A vehicle is just a location with `LocationType.Vehicle` in the stock ledger - the same math as a
|
||||
warehouse. When loading is confirmed, two stock movements are created per item:
|
||||
|
||||
From this point the vehicle carries live inventory that decreases with every sale or delivery
|
||||
(Steps 05/06) and must return to zero before the ride can close (Step 07).
|
||||
- Debit source warehouse: `-qty` at `LocationType.Warehouse`
|
||||
- Credit vehicle: `+qty` at `LocationType.Vehicle`
|
||||
|
||||
From this point the vehicle carries live inventory that decreases with every sale or delivery and
|
||||
must return to zero before the ride can close.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### New Entity — LoadingSheet
|
||||
### New Entity - LoadingSheet
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_RideId | Guid | FK → DeliveryRun |
|
||||
| FK_SourceWarehouseId | Guid | FK → Warehouse |
|
||||
| FK_RideId | Guid | FK -> DeliveryRun |
|
||||
| FK_SourceWarehouseId | Guid | FK -> Warehouse |
|
||||
| Status | LoadingSheetStatus | State machine below |
|
||||
| DispatchConfirmedAt | DateTime? | Set when warehouse confirms dispatch |
|
||||
| DispatchConfirmedBy | Guid? | UserId of warehouse manager |
|
||||
@@ -42,18 +46,18 @@ From this point the vehicle carries live inventory that decreases with every sal
|
||||
| ReceiptConfirmedBy | Guid? | UserId of driver |
|
||||
| CreatedAt | DateTime | |
|
||||
|
||||
### New Entity — LoadingSheetItem
|
||||
### New Entity - LoadingSheetItem
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_LoadingSheetId | Guid | FK → LoadingSheet |
|
||||
| FK_ItemId | Guid | FK → Item |
|
||||
| FK_LoadingSheetId | Guid | FK -> LoadingSheet |
|
||||
| FK_ItemId | Guid | FK -> Item |
|
||||
| QuantityDispatched | decimal | Set by warehouse manager at confirm-dispatch |
|
||||
| QuantityReceived | decimal? | Set by driver before confirm-receipt |
|
||||
| QuantityResolved | decimal? | Set by supervisor when resolving a discrepancy |
|
||||
|
||||
### New Enum — LoadingSheetStatus
|
||||
### New Enum - LoadingSheetStatus
|
||||
|
||||
```csharp
|
||||
public enum LoadingSheetStatus
|
||||
@@ -66,23 +70,23 @@ public enum LoadingSheetStatus
|
||||
}
|
||||
```
|
||||
|
||||
No changes to existing entities. Vehicle capacity check is deferred to Step 13 (Fleet Management).
|
||||
No changes to existing entities. Vehicle capacity check is deferred to Step 13.
|
||||
|
||||
---
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
Pending → DispatchConfirmed → ReceiptConfirmed (all lines match)
|
||||
↘ DiscrepancyFlagged → Resolved
|
||||
```text
|
||||
Pending -> DispatchConfirmed -> ReceiptConfirmed
|
||||
\-> DiscrepancyFlagged -> Resolved
|
||||
```
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| Pending → DispatchConfirmed | `POST /{id}/confirm-dispatch` | Warehouse manager locks in `QuantityDispatched` per line. Ride stays `Open`. |
|
||||
| DispatchConfirmed → ReceiptConfirmed | `POST /{id}/confirm-receipt` | Driver's received quantities all match dispatched. Creates stock movements. Auto-triggers ride `Open → Active`. |
|
||||
| DispatchConfirmed → DiscrepancyFlagged | `POST /{id}/confirm-receipt` | Any line mismatch. No stock movements created. Ride stays `Open`. |
|
||||
| DiscrepancyFlagged → Resolved | `POST /{id}/resolve` | Supervisor enters `QuantityResolved` per line. Creates stock movements using resolved quantities. Auto-triggers ride `Open → Active`. |
|
||||
| Pending -> DispatchConfirmed | `POST /{id}/confirm-dispatch` | Warehouse manager locks in `QuantityDispatched` per line. Ride stays `Open`. |
|
||||
| DispatchConfirmed -> ReceiptConfirmed | `POST /{id}/confirm-receipt` | Driver's received quantities all match dispatched. Creates stock movements. Auto-triggers ride `Open -> Active`. |
|
||||
| DispatchConfirmed -> DiscrepancyFlagged | `POST /{id}/confirm-receipt` | Any line mismatch. No stock movements created. Ride stays `Open`. |
|
||||
| DiscrepancyFlagged -> Resolved | `POST /{id}/resolve` | Supervisor enters `QuantityResolved` per line. Creates stock movements using resolved quantities. Auto-triggers ride `Open -> Active`. |
|
||||
|
||||
---
|
||||
|
||||
@@ -90,10 +94,11 @@ Pending → DispatchConfirmed → ReceiptConfirmed (all lines match)
|
||||
|
||||
| Ride Type | How items are added to the sheet |
|
||||
|---|---|
|
||||
| Delivery | Auto-populated on sheet creation from the aggregate of all order lines assigned to the ride |
|
||||
| Sale | Manually entered by warehouse manager via `PUT /{id}/items` (no orders to derive from) |
|
||||
| Delivery | Auto-populated on sheet creation from the aggregate of all order lines assigned to the ride's route |
|
||||
| Sale | Manually entered by warehouse manager via `PUT /{id}/items` based on expected field selling demand |
|
||||
|
||||
For Delivery rides the warehouse manager reviews the pre-populated quantities and may adjust before confirming dispatch. `PUT /{id}/items` is still available to override if needed.
|
||||
For Delivery rides the warehouse manager reviews the pre-populated quantities and may adjust before
|
||||
confirming dispatch. `PUT /{id}/items` is still available to override if needed.
|
||||
|
||||
---
|
||||
|
||||
@@ -101,12 +106,13 @@ For Delivery rides the warehouse manager reviews the pre-populated quantities an
|
||||
|
||||
Created at `ReceiptConfirmed` or `Resolved`. One pair per line item:
|
||||
|
||||
- **Debit** warehouse: `-QuantityFinal` at `LocationType.Warehouse`, `FK_LocationId = SourceWarehouseId`
|
||||
- **Credit** vehicle: `+QuantityFinal` at `LocationType.Vehicle`, `FK_LocationId = VehicleId`
|
||||
- Debit warehouse: `-QuantityFinal` at `LocationType.Warehouse`, `FK_LocationId = SourceWarehouseId`
|
||||
- Credit vehicle: `+QuantityFinal` at `LocationType.Vehicle`, `FK_LocationId = VehicleId`
|
||||
|
||||
`QuantityFinal` = `QuantityReceived` on a clean confirmation, or `QuantityResolved` when a discrepancy was resolved.
|
||||
`QuantityFinal` = `QuantityReceived` on a clean confirmation, or `QuantityResolved` when a
|
||||
discrepancy was resolved.
|
||||
|
||||
Movement `Type` = `Load` (new `StockMovementType` value to add to enums).
|
||||
Movement `Type` = `Load`.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,23 +125,25 @@ Base route: `/api/loading-sheets`
|
||||
| POST | `/` | Warehouse mgr | Create sheet for an Open ride. Delivery rides auto-populate items from orders. |
|
||||
| GET | `/` | Any | List sheets. Filter by `rideId`, `status`. Paginated. |
|
||||
| GET | `/{id}` | Any | Get sheet detail with all line items |
|
||||
| PUT | `/{id}/items` | Warehouse mgr | Set or override item quantities (required for Sale rides, optional for Delivery) |
|
||||
| POST | `/{id}/confirm-dispatch` | Warehouse mgr | Pending → DispatchConfirmed |
|
||||
| PUT | `/{id}/items` | Warehouse mgr | Set or override item quantities |
|
||||
| POST | `/{id}/confirm-dispatch` | Warehouse mgr | Pending -> DispatchConfirmed |
|
||||
| PUT | `/{id}/receipt-items` | Driver | Set `QuantityReceived` per line |
|
||||
| POST | `/{id}/confirm-receipt` | Driver | → ReceiptConfirmed or DiscrepancyFlagged |
|
||||
| POST | `/{id}/resolve` | Supervisor | Enter `QuantityResolved` per line → Resolved + ride activation |
|
||||
| POST | `/{id}/confirm-receipt` | Driver | -> ReceiptConfirmed or DiscrepancyFlagged |
|
||||
| POST | `/{id}/resolve` | Supervisor | Enter `QuantityResolved` per line -> Resolved plus ride activation |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
1. A loading sheet can only be created for a ride in `Open` status.
|
||||
2. A ride can only have one active loading sheet (not `Resolved` or `ReceiptConfirmed`) at a time.
|
||||
3. `QuantityDispatched != QuantityReceived` on any line → `DiscrepancyFlagged`. No partial activation.
|
||||
4. Stock movements are created only once — at `ReceiptConfirmed` or `Resolved`. Never on dispatch.
|
||||
5. Discrepancies must be resolved per line item with an explicit `QuantityResolved` value. No bulk "accept driver" or "accept warehouse" shortcut.
|
||||
2. A ride can only have one active loading sheet at a time.
|
||||
3. `QuantityDispatched != QuantityReceived` on any line -> `DiscrepancyFlagged`. No partial activation.
|
||||
4. Stock movements are created only once - at `ReceiptConfirmed` or `Resolved`. Never on dispatch.
|
||||
5. Discrepancies must be resolved per line item with an explicit `QuantityResolved` value.
|
||||
6. All list endpoints return paginated responses.
|
||||
7. Vehicle capacity check is not enforced in this step — deferred to Step 13.
|
||||
7. Vehicle capacity check is not enforced in this step.
|
||||
8. Immediate handover from a Sale ride does not remove the need for an `Order`; every sale still
|
||||
creates an order, and that order is fulfilled immediately when stock is handed over at sale time.
|
||||
|
||||
---
|
||||
|
||||
@@ -145,54 +153,27 @@ Base route: `/api/loading-sheets`
|
||||
|---|---|
|
||||
| `loading.view` | All roles |
|
||||
| `loading.dispatch` | Warehouse manager and above |
|
||||
| `loading.receive` | Driver (AreaManager and above for testing) |
|
||||
| `loading.receive` | Driver |
|
||||
| `loading.resolve` | Supervisor and above |
|
||||
|
||||
---
|
||||
|
||||
## Fraud / Control Considerations
|
||||
|
||||
- Discrepancies are immediately visible and block activation — they cannot be silently absorbed.
|
||||
- `QuantityResolved` is permanently stored per line for audit purposes, even after the sheet is resolved.
|
||||
- See [09_fraud_controls.md](./09_fraud_controls.md) for the audit trail layer that will sit on top of this.
|
||||
- Discrepancies are immediately visible and block activation.
|
||||
- `QuantityResolved` is permanently stored per line for audit purposes.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- ✅ Vehicle capacity check — deferred to Step 13 (Fleet Management).
|
||||
- ✅ Discrepancy resolution — supervisor enters `QuantityResolved` per line item.
|
||||
- ✅ Ride activation trigger — `confirm-receipt` and `resolve` auto-trigger `Open → Active` internally.
|
||||
- ✅ Item population — Delivery rides auto-populated from orders; Sale rides manually entered.
|
||||
1. Vehicle capacity check - deferred to Step 13.
|
||||
2. Discrepancy resolution - supervisor enters `QuantityResolved` per line item.
|
||||
3. Ride activation trigger - `confirm-receipt` and `resolve` auto-trigger `Open -> Active`.
|
||||
4. Delivery item population - from assigned orders on the ride's route.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Build order:**
|
||||
1. Enum — add `LoadingSheetStatus` to `Models/Enums.cs`; add `Load` to `StockMovementType`
|
||||
2. Entities — `LoadingSheet`, `LoadingSheetItem` (new)
|
||||
3. `AppDBContext` — new DbSets + Fluent config
|
||||
4. Migration — `LoadingEvent` (schema) + `LoadingPermissions` (4 new permissions seeded)
|
||||
5. DTOs — `LoadingSheetDTOs.cs`
|
||||
6. Repository — `ILoadingSheetRepository`, `LoadingSheetRepository`
|
||||
7. Service — `LoadingSheetService`
|
||||
8. Controller — `LoadingSheetController`
|
||||
9. DI registration in `Program.cs`
|
||||
|
||||
**Feature folder:** `Features/LoadingEvent/`
|
||||
|
||||
**What was built:**
|
||||
1. ✅ Enums — added `LoadingSheetStatus` and `Load` to `MovementType` in `Models/Enums.cs`
|
||||
2. ✅ Entities — `LoadingSheet`, `LoadingSheetItem` created in `Models/`
|
||||
3. ✅ `AppDBContext` — `LoadingSheets`, `LoadingSheetItems` DbSets added; Fluent config for FK restrictions and ride/warehouse relationships
|
||||
4. ✅ DTOs — `LoadingSheetDTOs.cs` in `Features/LoadingEvent/`
|
||||
5. ✅ Repository — `ILoadingSheetRepository`, `LoadingSheetRepository` in `Features/LoadingEvent/`
|
||||
6. ✅ Service — `LoadingSheetService` in `Features/LoadingEvent/`
|
||||
7. ✅ Controller — `LoadingSheetController` in `Features/LoadingEvent/`
|
||||
8. ✅ DI registration in `Program.cs`
|
||||
9. ⚠️ Permissions added to seed (`loading.view`, `loading.dispatch`, `loading.receive`, `loading.resolve`) — **authorization layer is being replaced, skip this**
|
||||
|
||||
**Pending (run manually):**
|
||||
- `dotnet ef migrations add LoadingEvent` — blocked by a pre-existing duplicate Permission seed ID in `AppDBContextModelSnapshot.cs` (ID `20000000-0000-0000-0000-000000000040` is assigned to both `categories.delete` and `driver_profiles.create`). This is a snapshot consistency issue from a previous session and needs to be resolved before migration generation will succeed.
|
||||
- `dotnet ef database update` after migration is generated.
|
||||
Build after the ride model and before full delivery/sale execution.
|
||||
|
||||
@@ -1,79 +1,148 @@
|
||||
# 05 — Delivery Ride Execution
|
||||
|
||||
**Status**: Not Started
|
||||
**Status**: Planning
|
||||
**Depends On**: 04 — Loading Event
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Execute pre-confirmed orders against a loaded, active delivery ride. Covers the order-status half
|
||||
of [mainPlan.md §1](./mainPlan.md#1-order-management):
|
||||
`updateOrderStatus`, `convertOrderToInvoice`, `createBackorder`. Requires `Order.Status` to exist
|
||||
first — see gap #7 in [00_existing_gaps.md](./00_existing_gaps.md).
|
||||
Fulfill pending orders against an active Delivery ride. The driver visits each store on the route,
|
||||
delivers the ordered items, gets customer confirmation, and the system creates the stock movement,
|
||||
updates the order, and completes the sale.
|
||||
|
||||
---
|
||||
|
||||
## Background / Context
|
||||
|
||||
Today `Order` has no status and no link to a ride. This step is where the order lifecycle
|
||||
(`pending → approved → picked → dispatched → delivered → invoiced`) actually gets driven, once the
|
||||
order has been picked/loaded ([04_loading_event.md](./04_loading_event.md)).
|
||||
- A Delivery ride goes `Active` after the loading event is confirmed.
|
||||
- Orders are linked to a ride implicitly through route: `Ride → Route → Stores → Pending Orders`.
|
||||
- `QuantityFulfilled` on `OrderItem` tracks what was actually delivered (added in pre-planning).
|
||||
- No separate backorder entity — partially fulfilled orders stay `PartiallyFulfilled` and get picked
|
||||
up by the next delivery ride on the same route.
|
||||
- Customer confirmation in v1 is a boolean flag (`CustomerConfirmed`). Full signature/OTP deferred
|
||||
to Step 09 (Fraud Controls).
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Delivery Task (per order on ride)
|
||||
### New Entity — DeliveryTask
|
||||
|
||||
`RideId`, `OrderId`, `Sequence` (visit order), `Status` (`Pending`, `Delivered`, `PartiallyDelivered`,
|
||||
`Failed`).
|
||||
Extends `Template` (adds `IsActive`, `CreatedAt` — consistent with all other entities).
|
||||
|
||||
### Delivery Outcome
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK (from Template) |
|
||||
| FK_RideId | Guid | FK → DeliveryRun |
|
||||
| FK_OrderId | Guid | FK → Order |
|
||||
| Sequence | int | Visit order on the route |
|
||||
| Status | DeliveryTaskStatus | State machine below |
|
||||
| CustomerConfirmed | bool | Set to true at confirm time |
|
||||
| ConfirmedAt | DateTime? | Timestamp of customer confirmation |
|
||||
| Note | string? | Driver note on failure or partial delivery |
|
||||
| IsActive | bool | From Template (soft delete) |
|
||||
| CreatedAt | DateTime | From Template |
|
||||
|
||||
Per delivery task: quantities actually delivered per line, vs. ordered — feeds `createBackorder`
|
||||
for the shortfall.
|
||||
### New Enum — DeliveryTaskStatus
|
||||
|
||||
### Customer Confirmation
|
||||
```csharp
|
||||
public enum DeliveryTaskStatus
|
||||
{
|
||||
Pending,
|
||||
Delivered,
|
||||
PartiallyDelivered,
|
||||
Failed
|
||||
}
|
||||
```
|
||||
|
||||
Signature or OTP captured against the delivery task before it can be marked complete.
|
||||
### No new columns on OrderItem
|
||||
|
||||
`QuantityFulfilled` already added in pre-planning. No further model changes needed.
|
||||
|
||||
---
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
Pending → Delivered (all lines fully delivered + customer confirmed)
|
||||
Pending → PartiallyDelivered (some lines short + customer confirmed)
|
||||
Pending → Failed (driver could not deliver, no stock movement)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stock Movements
|
||||
|
||||
Created on `Delivered` or `PartiallyDelivered` after customer confirmation. One movement per item:
|
||||
|
||||
- **Debit** vehicle: `-QuantityFulfilled` at `LocationType.Vehicle`, `FK_LocationId = VehicleId`
|
||||
- `VehicleId` is resolved from `DeliveryTask.FK_RideId → DeliveryRun.FK_VehicleId`
|
||||
- `MovementType.Sale`
|
||||
- `ReferenceId = OrderId`
|
||||
|
||||
No movement on `Failed`.
|
||||
|
||||
---
|
||||
|
||||
## Order and Sale Status Transitions (triggered internally)
|
||||
|
||||
| Condition | Order | Sale |
|
||||
|---|---|---|
|
||||
| All OrderItem lines: QuantityFulfilled == Quantity | → Fulfilled | → Completed |
|
||||
| Any OrderItem line: QuantityFulfilled < Quantity | → PartiallyFulfilled | stays Pending |
|
||||
| Task Failed | stays Pending | stays Pending |
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Function reference | Endpoint intent |
|
||||
|---|---|
|
||||
| `updateOrderStatus(orderId, status)` (§1) | `PUT /api/rides/{rideId}/tasks/{orderId}/status` |
|
||||
| `convertOrderToInvoice(orderId)` (§1) | Triggered automatically on `Delivered`/`PartiallyDelivered` — see [19_finance_accounting.md](./19_finance_accounting.md) |
|
||||
| `createBackorder(orderId, unfulfilledItems[])` (§1) | Triggered automatically on `PartiallyDelivered` |
|
||||
| — | `POST /api/rides/{rideId}/tasks/{orderId}/confirm` — captures customer signature/OTP |
|
||||
Base routes: `/api/rides/{rideId}/tasks` and `/api/delivery-tasks`
|
||||
|
||||
| Method | Route | Who | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/rides/{rideId}/tasks/generate` | Driver/System | Auto-create tasks from all Pending/PartiallyFulfilled orders for stores on ride's route |
|
||||
| GET | `/api/rides/{rideId}/tasks` | Driver | List all tasks for this ride with current status |
|
||||
| GET | `/api/delivery-tasks/{id}` | Driver | Task detail with order items and quantities |
|
||||
| PUT | `/api/delivery-tasks/{id}/quantities` | Driver | Set QuantityFulfilled per order item before confirming |
|
||||
| POST | `/api/delivery-tasks/{id}/confirm` | Driver | Customer confirmed — creates stock movements, transitions order/sale |
|
||||
| POST | `/api/delivery-tasks/{id}/fail` | Driver | Mark task as failed with a note |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
- A delivery task cannot be marked `Delivered` without a customer confirmation.
|
||||
- `PartiallyDelivered` always creates a `createBackorder` entry and a credit note for the
|
||||
undelivered quantity (see [18_returns_claims.md](./18_returns_claims.md) for credit note shape).
|
||||
- Each confirmed delivery task creates a stock movement (`Sale`/consumption) against the vehicle
|
||||
location per [02_stock_ledger.md](./02_stock_ledger.md).
|
||||
1. Delivery tasks can only be created for a ride in `Active` status and of type `Delivery`.
|
||||
2. One task per order per ride — cannot create a second task for the same order on the same ride.
|
||||
3. `QuantityFulfilled` must be set on all order items before calling confirm.
|
||||
4. `QuantityFulfilled` cannot exceed `Quantity` on any line.
|
||||
5. `CustomerConfirmed` must be true before the task can move to `Delivered` or `PartiallyDelivered`.
|
||||
6. Stock movement is created only once — at confirm. Never on fail.
|
||||
7. A failed task does not change the order or sale status — the order stays available for the next ride.
|
||||
8. Partial delivery: the unfulfilled quantity (`Quantity - QuantityFulfilled`) remains on the same
|
||||
order. The next delivery ride on the same route will pick it up automatically via `generate`.
|
||||
|
||||
---
|
||||
|
||||
## Fraud / Control Considerations
|
||||
## Implementation Build Order
|
||||
|
||||
- OTP/signature capture must be tamper-evident (timestamped, tied to the delivery task, not
|
||||
editable after submission).
|
||||
1. Enum — add `DeliveryTaskStatus` to `Models/Enums.cs`
|
||||
2. Entity — `DeliveryTask` in `Models/`
|
||||
3. `AppDBContext` — new DbSet + Fluent config (Restrict on Ride, Order FKs)
|
||||
4. Migration
|
||||
5. DTOs — `DeliveryTaskDTOs.cs` in `Features/DeliveryRide/`
|
||||
6. Repository — `IDeliveryTaskRepository`, `DeliveryTaskRepository`
|
||||
7. Service — `DeliveryTaskService`
|
||||
8. Controller — `DeliveryTaskController`
|
||||
9. DI registration in `Program.cs`
|
||||
|
||||
**Feature folder:** `Features/DeliveryRide/`
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
## Open Questions — Resolved
|
||||
|
||||
- Signature capture (image) or OTP (SMS code) — one, or both, in v1?
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
> Fill this section in during planning discussion before writing any code.
|
||||
- ✅ Customer confirmation — boolean flag in v1, full signature/OTP in Step 09
|
||||
- ✅ Backorder — no separate entity; same order stays PartiallyFulfilled for next ride
|
||||
- ✅ Task generation — on-demand via `generate` endpoint, not automatic on ride activation
|
||||
- ✅ Failed task — order stays Pending, available for next ride, no stock movement
|
||||
|
||||
@@ -1,80 +1,132 @@
|
||||
# 06 — Sale Ride Execution
|
||||
|
||||
**Status**: Not Started
|
||||
**Status**: Planning
|
||||
**Depends On**: 04 — Loading Event
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Support on-the-spot van sales with no pre-order: driver picks items/quantities, an invoice is
|
||||
generated immediately, and payment is captured at point of sale. Covers `createInvoice`,
|
||||
`recordPayment` from [mainPlan.md §13](./mainPlan.md#13-finance--accounting),
|
||||
and reuses `applyPricingAndDiscounts` (§1) plus `evaluateSchemeEligibility`/`applySchemeDiscount`
|
||||
(§11) if promotions are live by the time this ships.
|
||||
Allow a sales agent on an active Sale ride to create sales in the field. Every sale creates an
|
||||
order. Immediate handover deducts vehicle stock and closes the order immediately. Non-immediate
|
||||
handover leaves the order pending for a future delivery ride.
|
||||
|
||||
---
|
||||
|
||||
## Background / Context
|
||||
|
||||
Distinct from [05_delivery_ride.md](./05_delivery_ride.md): there is no pre-existing `Order` row —
|
||||
the field invoice *is* the order, created and settled in one step.
|
||||
- `SaleService.CreateAsync` already exists and handles both `FulfillImmediately` paths.
|
||||
- What Step 06 adds: vehicle stock validation and deduction on immediate handover, and the
|
||||
Payment recording endpoint.
|
||||
- `Sale.PaymentStatus` and the `Payment` model were added in pre-planning.
|
||||
- Pricing is already resolved via `PriceListService` inside `SaleService`.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Field Invoice
|
||||
No new entities. Everything needed was added in pre-planning:
|
||||
|
||||
`RideId`, `StoreId`, `CreatedByUserId`, `TotalAmount`, `PaymentStatus` (`Paid`, `PartiallyPaid`,
|
||||
`OnCredit`).
|
||||
| What | Where | Status |
|
||||
|---|---|---|
|
||||
| `Payment` entity | `Models/Payment.cs` | ✅ Done |
|
||||
| `PaymentStatus` enum | `Models/Enums.cs` | ✅ Done |
|
||||
| `PaymentMethod` enum | `Models/Enums.cs` | ✅ Done |
|
||||
| `Sale.PaymentStatus` | `Models/Sale.cs` | ✅ Done |
|
||||
| `OrderItem.QuantityFulfilled` | `Models/OrderItem.cs` | ✅ Done |
|
||||
|
||||
### Field Invoice Line Item
|
||||
|
||||
`FieldInvoiceId`, `ItemId`, `Quantity`, `UnitPrice` (subject to the discount-limit rules in
|
||||
[08_sales_agent_controls.md](./08_sales_agent_controls.md)).
|
||||
|
||||
### Payment Record
|
||||
|
||||
`FieldInvoiceId`, `Amount`, `Method` (`Cash`, `Credit`), `CollectedAt`.
|
||||
Migration needed for: `Payment` table, `Sale.PaymentStatus` column, `OrderItem.QuantityFulfilled` column.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
## Changes to Existing SaleService
|
||||
|
||||
| Function reference | Endpoint intent |
|
||||
|---|---|
|
||||
| `createInvoice(orderId)` (§13) | `POST /api/rides/{rideId}/field-invoices` — orderId is the field invoice itself here |
|
||||
| `recordPayment(invoiceId, amount, method)` (§13) | `POST /api/field-invoices/{id}/payments` |
|
||||
| `applyPricingAndDiscounts(orderId)` (§1) | Applied at line-item entry time, within agent discount limits |
|
||||
`SaleService.CreateAsync` needs two additions when `FulfillImmediately = true` and `RideId` is set:
|
||||
|
||||
1. **Stock check** — for each item, verify vehicle has enough stock (`StockBalance` at
|
||||
`LocationType.Vehicle` with `LocationId = VehicleId`). Reject the whole sale if any item is short.
|
||||
2. **Stock deduction** — debit vehicle balance per item, create one `StockMovement` per item:
|
||||
- `MovementType.Sale`
|
||||
- `FromLocationId = VehicleId`, `FromLocationType = Vehicle`
|
||||
- `ReferenceId = OrderId`
|
||||
|
||||
`VehicleId` is resolved from `Sale.FK_RideId → DeliveryRun.FK_VehicleId`.
|
||||
|
||||
**SaleService constructor additions required:**
|
||||
- `IStockRepository` — for stock balance check and debit
|
||||
- `IRideRepository` — to look up the ride and resolve `FK_VehicleId`
|
||||
|
||||
When `FulfillImmediately = false`, no stock movement — order stays Pending for a delivery ride.
|
||||
|
||||
---
|
||||
|
||||
## New Feature — Payment
|
||||
|
||||
**Feature folder:** `Features/Payment/`
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Route | Who | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/sales/{id}/payments` | Driver/Agent | Record a payment against a sale |
|
||||
| GET | `/api/sales/{id}/payments` | Any | List all payments for a sale |
|
||||
|
||||
### Payment Creation Logic
|
||||
|
||||
On `POST /api/sales/{id}/payments`:
|
||||
1. Validate sale exists and is not `Cancelled`.
|
||||
2. Add `Payment` record (`FK_SaleId`, `FK_StoreId` from sale, `Amount`, `Method`, `CollectedAt`, `CollectedBy`).
|
||||
3. Sum all payments for this sale. Recalculate `Sale.PaymentStatus`:
|
||||
- `sum == 0` → `Pending`
|
||||
- `sum < Sale.TotalAmount` → `PartiallyPaid`
|
||||
- `sum >= Sale.TotalAmount` → `Paid`
|
||||
4. Update `Sale.PaymentStatus` in the same transaction.
|
||||
|
||||
### DTOs
|
||||
|
||||
```csharp
|
||||
public record CreatePaymentRequest(decimal Amount, PaymentMethod Method);
|
||||
public record PaymentResponse(Guid Id, Guid SaleId, Guid StoreId, decimal Amount,
|
||||
PaymentMethod Method, DateTime CollectedAt, Guid CollectedBy);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
- Driver cannot sell more than what is loaded on the vehicle — every line item is checked against
|
||||
the vehicle's current stock balance ([02_stock_ledger.md](./02_stock_ledger.md)) before the
|
||||
invoice is finalized.
|
||||
- Each confirmed sale line creates a `Sale` stock movement against the vehicle location,
|
||||
immediately (not batched at ride close).
|
||||
- Discount applied per line must not exceed the agent's role-level limit (§8).
|
||||
1. Driver cannot sell more than vehicle stock for immediate handover — entire sale rejected if any
|
||||
item is short (no partial sale creation).
|
||||
2. Every sale creates an order, even when all items are handed over immediately.
|
||||
3. `FulfillImmediately = true`:
|
||||
- Vehicle stock debited immediately per item.
|
||||
- Order → `Fulfilled`, Sale → `Completed` at creation time (already in SaleService).
|
||||
- `QuantityFulfilled = Quantity` on all order items (already in SaleService).
|
||||
4. `FulfillImmediately = false`:
|
||||
- No stock movement at sale time.
|
||||
- Order → `Pending`, Sale → `Pending` at creation time.
|
||||
5. Payment can be recorded at any time — sale does not need to be `Completed` first.
|
||||
6. Overpayment is allowed (`sum > TotalAmount`) — `PaymentStatus` stays `Paid`.
|
||||
7. A `Cancelled` sale cannot receive new payments.
|
||||
|
||||
---
|
||||
|
||||
## Fraud / Control Considerations
|
||||
## Implementation Build Order
|
||||
|
||||
- Cash collected must reconcile against invoiced totals at ride close — see
|
||||
[07_ride_reconciliation.md](./07_ride_reconciliation.md).
|
||||
1. Migration — `Payment` table, `Sale.PaymentStatus`, `OrderItem.QuantityFulfilled`
|
||||
2. Extend `SaleService.CreateAsync` — stock check + stock movement on `FulfillImmediately`
|
||||
3. DTOs — `PaymentDTOs.cs` in `Features/Payment/`
|
||||
4. Repository — `IPaymentRepository`, `PaymentRepository`
|
||||
5. Service — `PaymentService`
|
||||
6. Controller — `PaymentController`
|
||||
7. DI registration in `Program.cs`
|
||||
|
||||
**Feature folder:** `Features/Payment/` (new), `Features/Sale/` (extended)
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
## Open Questions — Resolved
|
||||
|
||||
- Are field invoices numbered/sequenced separately from warehouse-issued invoices, or share one
|
||||
sequence?
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
> Fill this section in during planning discussion before writing any code.
|
||||
- ✅ Payment timing — can be recorded at any time, not locked to sale completion
|
||||
- ✅ Overpayment — allowed, status stays Paid
|
||||
- ✅ Stock check scope — entire sale rejected if any item is short, no partial creation
|
||||
- ✅ Non-immediate handover — no stock movement at sale time, order picked up by delivery ride
|
||||
|
||||
@@ -43,8 +43,8 @@ They must be resolved before new feature steps begin because later steps depend
|
||||
| 02 | Stock Ledger | Done | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
|
||||
| 03 | Ride Lifecycle | Done | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
|
||||
| 04 | Loading Event | Done | 03 | [04_loading_event.md](./04_loading_event.md) |
|
||||
| 05 | Delivery Ride Execution | Not Started | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
|
||||
| 06 | Sale Ride Execution | Not Started | 04 | [06_sale_ride.md](./06_sale_ride.md) |
|
||||
| 05 | Delivery Ride Execution | Planning | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
|
||||
| 06 | Sale Ride Execution | Planning | 04 | [06_sale_ride.md](./06_sale_ride.md) |
|
||||
| 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) |
|
||||
| 08 | Sales Agent Controls | Not Started | 07 | [08_sales_agent_controls.md](./08_sales_agent_controls.md) |
|
||||
| 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) |
|
||||
@@ -303,13 +303,17 @@ Introduce the concept of stock per location. Every warehouse (fixed or vehicle)
|
||||
of each item. Stock is never edited directly — all changes happen through movement transactions
|
||||
(load, sale, return, damage, adjustment). The current balance is always the running sum.
|
||||
This is the single most important architectural decision for the whole system.
|
||||
For now, the shop is treated as a business destination, not a tracked stock location in this
|
||||
system. That means warehouse-to-vehicle and vehicle-to-warehouse movements update tracked stock
|
||||
balances, while vehicle-to-shop handover reduces vehicle stock and updates sale/order fulfillment
|
||||
state without creating a shop stock balance.
|
||||
**Gate**: Stock balance for any item at any location can be queried. Every change is a traceable transaction.
|
||||
|
||||
### 03 — Ride Lifecycle
|
||||
A ride is a session that turns a vehicle into a temporary warehouse. It has a state machine:
|
||||
Draft → Open → Active → Closed. A vehicle can only be on one active ride at a time.
|
||||
Two ride types exist: Delivery (pre-orders) and Sale (van sales). The ride tracks who is driving,
|
||||
which route, which vehicle, and when it started and ended.
|
||||
which route, which vehicle, and when it started and ended. For now, one ride uses one route.
|
||||
**Gate**: A ride can be created, opened, and closed. A vehicle's stock ledger is created when a ride opens.
|
||||
|
||||
### 04 — Loading Event
|
||||
@@ -324,14 +328,18 @@ On a delivery ride, the driver fulfills pre-confirmed orders. Each order becomes
|
||||
The driver marks each as delivered, partially delivered, or failed. Partially delivered orders
|
||||
generate a credit note for the undelivered quantity. Customer confirmation (signature or OTP)
|
||||
is required to mark a delivery complete. Stock movements are created per delivery event.
|
||||
Vehicle-to-shop handover reduces vehicle stock only; the shop does not yet exist as a tracked stock
|
||||
location in the ledger.
|
||||
**Gate**: A delivery ride can execute all orders on its route and produce a delivery report.
|
||||
|
||||
### 06 — Sale Ride Execution
|
||||
On a sale ride, the driver visits customers and creates invoices on the spot. No pre-orders exist.
|
||||
The driver selects items, quantities, and prices (within approved discount limits), and the invoice
|
||||
is generated immediately. Payment is collected (cash or credit) at the point of sale.
|
||||
The vehicle's stock ledger decreases with each sale. The driver cannot sell more than what is loaded.
|
||||
**Gate**: A sale ride can generate invoices in the field. Stock on vehicle decreases with each sale.
|
||||
On a sale ride, the sales agent visits customers and creates sales in the field. Every sale creates
|
||||
an order. If items are handed over immediately, the order is fulfilled immediately and the sale is
|
||||
completed immediately. If items are not handed over immediately, the order stays pending for later
|
||||
fulfillment. Payment is collected based on the final sale flow. The vehicle's stock ledger decreases
|
||||
only for quantities actually handed over from the vehicle. The shop is not yet a tracked stock
|
||||
location in the ledger.
|
||||
**Gate**: A sale ride can create sales in the field, create linked orders, and reduce vehicle stock for immediate handover quantities.
|
||||
|
||||
### 07 — Ride Close & Reconciliation
|
||||
When a ride ends, the system performs a full reconciliation. The equation must balance:
|
||||
|
||||
Reference in New Issue
Block a user