# 04 — Loading Event **Status**: Planning **Depends On**: 03 — Ride Lifecycle --- ## Goal 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. --- ## 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` 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 ### New Entity — LoadingSheet | 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 | | ### New Entity — LoadingSheetItem | 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 | ### New Enum — LoadingSheetStatus ```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 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 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 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 - ✅ 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 **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.