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
+57 -76
View File
@@ -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.