loading event

This commit is contained in:
Kalana B. Thilakarathna
2026-08-24 11:23:32 +05:30
parent f381e7a634
commit ac1b2fbc4b
33 changed files with 3398 additions and 22189 deletions
+149 -34
View File
@@ -1,6 +1,6 @@
# 04 — Loading Event
**Status**: Not Started
**Status**: Planning
**Depends On**: 03 — Ride Lifecycle
---
@@ -8,76 +8,191 @@
## Goal
Move stock from a source warehouse onto an opened ride's vehicle, with two-party confirmation.
Covers the dispatch half of [mainPlan.md §6](./mainPlan.md#6-warehouse-management):
`generatePickList`, `confirmPicking`, `packOrder`, `buildDispatchLoad`, and `calculateLoadPlan`
from §5.
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.
---
## Background / Context
This is the first place the [02_stock_ledger.md](./02_stock_ledger.md) movement model gets
exercised end-to-end: a `Load` movement debits the warehouse and credits the vehicle location
created when the ride opened ([03_ride_lifecycle.md](./03_ride_lifecycle.md)).
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`
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).
---
## Data Model Changes
### Loading Sheet
### New Entity — LoadingSheet
Header record: `RideId`, `SourceWarehouseId`, `Status` (`Pending`, `DispatchConfirmed`,
`ReceiptConfirmed`, `DiscrepancyFlagged`, `Resolved`).
| Column | Type | Notes |
|---|---|---|
| Id | Guid | PK |
| 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 |
| ReceiptConfirmedAt | DateTime? | Set when driver confirms receipt |
| ReceiptConfirmedBy | Guid? | UserId of driver |
| CreatedAt | DateTime | |
### Loading Line Item
### New Entity — LoadingSheetItem
`LoadingSheetId`, `ItemId`, `QuantityDispatched` (set by warehouse manager), `QuantityReceived`
(set by driver) — a mismatch between the two is a discrepancy.
| Column | Type | Notes |
|---|---|---|
| Id | Guid | PK |
| 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 |
### Two-Party Confirmation
### New Enum — LoadingSheetStatus
Warehouse manager confirms what was dispatched (`generatePickList` → `confirmPicking` →
`packOrder`); driver separately confirms what was received onto the vehicle. Both confirmations are
required before the ride can move `Open → Active`.
```csharp
public enum LoadingSheetStatus
{
Pending,
DispatchConfirmed,
ReceiptConfirmed,
DiscrepancyFlagged,
Resolved
}
```
No changes to existing entities. Vehicle capacity check is deferred to Step 13 (Fleet Management).
---
## State Machine
```
Pending → DispatchConfirmed → ReceiptConfirmed (all lines match)
↘ 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`. |
---
## Item Population by Ride Type
| 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) |
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.
---
## Stock Movements
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`
`QuantityFinal` = `QuantityReceived` on a clean confirmation, or `QuantityResolved` when a discrepancy was resolved.
Movement `Type` = `Load` (new `StockMovementType` value to add to enums).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `generatePickList(orderIds[])` (§6) | `POST /api/loading-sheets` |
| `confirmPicking(pickListId, pickedItems[])` (§6) | `POST /api/loading-sheets/{id}/confirm-dispatch` |
| `packOrder(orderId, packageDetails)` (§6) | `POST /api/loading-sheets/{id}/pack` |
| `buildDispatchLoad(vehicleId, orderIds[])` (§6) | Groups multiple orders into one loading sheet |
| `calculateLoadPlan(vehicleId, orderIds[])` (§5) | Validates total volume/weight against vehicle capacity before dispatch confirmation |
| — | `POST /api/loading-sheets/{id}/confirm-receipt` — driver-side confirmation |
Base route: `/api/loading-sheets`
| Method | Route | Who | Description |
|---|---|---|---|
| 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}/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 |
---
## Business Rules
- `QuantityDispatched != QuantityReceived` on any line flags the sheet `DiscrepancyFlagged` and
blocks ride activation until resolved.
- `calculateLoadPlan` must reject a load exceeding vehicle capacity before dispatch is confirmed,
not after.
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.
6. All list endpoints return paginated responses.
7. Vehicle capacity check is not enforced in this step — deferred to Step 13.
---
## Permissions to Seed
| Code | Who |
|---|---|
| `loading.view` | All roles |
| `loading.dispatch` | Warehouse manager and above |
| `loading.receive` | Driver (AreaManager and above for testing) |
| `loading.resolve` | Supervisor and above |
---
## Fraud / Control Considerations
- Discrepancies must be immediately visible, not silently averaged into stock adjustments — see
[09_fraud_controls.md](./09_fraud_controls.md).
- 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.
---
## Open Questions
- Does `Vehicle` need a `Capacity` field added now (volume/weight), or is `calculateLoadPlan`
deferred until that's available?
- ✅ 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.
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
**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.
+1 -1
View File
@@ -41,7 +41,7 @@ They must be resolved before new feature steps begin because later steps depend
| 01 | Item Catalog | Done | 00 | [01_item_catalog.md](./01_item_catalog.md) |
| 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 | Not Started | 03 | [04_loading_event.md](./04_loading_event.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) |
| 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) |