8.3 KiB
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:
-qtyatLocationType.Warehouse - Credit vehicle:
+qtyatLocationType.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
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:
-QuantityFinalatLocationType.Warehouse,FK_LocationId = SourceWarehouseId - Credit vehicle:
+QuantityFinalatLocationType.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
- A loading sheet can only be created for a ride in
Openstatus. - A ride can only have one active loading sheet (not
ResolvedorReceiptConfirmed) at a time. QuantityDispatched != QuantityReceivedon any line →DiscrepancyFlagged. No partial activation.- Stock movements are created only once — at
ReceiptConfirmedorResolved. Never on dispatch. - Discrepancies must be resolved per line item with an explicit
QuantityResolvedvalue. No bulk "accept driver" or "accept warehouse" shortcut. - All list endpoints return paginated responses.
- 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.
QuantityResolvedis permanently stored per line for audit purposes, even after the sheet is resolved.- See 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
QuantityResolvedper line item. - ✅ Ride activation trigger —
confirm-receiptandresolveauto-triggerOpen → Activeinternally. - ✅ Item population — Delivery rides auto-populated from orders; Sale rides manually entered.
Implementation Notes
Build order:
- Enum — add
LoadingSheetStatustoModels/Enums.cs; addLoadtoStockMovementType - Entities —
LoadingSheet,LoadingSheetItem(new) AppDBContext— new DbSets + Fluent config- Migration —
LoadingEvent(schema) +LoadingPermissions(4 new permissions seeded) - DTOs —
LoadingSheetDTOs.cs - Repository —
ILoadingSheetRepository,LoadingSheetRepository - Service —
LoadingSheetService - Controller —
LoadingSheetController - DI registration in
Program.cs
Feature folder: Features/LoadingEvent/
What was built:
- ✅ Enums — added
LoadingSheetStatusandLoadtoMovementTypeinModels/Enums.cs - ✅ Entities —
LoadingSheet,LoadingSheetItemcreated inModels/ - ✅
AppDBContext—LoadingSheets,LoadingSheetItemsDbSets added; Fluent config for FK restrictions and ride/warehouse relationships - ✅ DTOs —
LoadingSheetDTOs.csinFeatures/LoadingEvent/ - ✅ Repository —
ILoadingSheetRepository,LoadingSheetRepositoryinFeatures/LoadingEvent/ - ✅ Service —
LoadingSheetServiceinFeatures/LoadingEvent/ - ✅ Controller —
LoadingSheetControllerinFeatures/LoadingEvent/ - ✅ DI registration in
Program.cs - ⚠️ 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 inAppDBContextModelSnapshot.cs(ID20000000-0000-0000-0000-000000000040is assigned to bothcategories.deleteanddriver_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 updateafter migration is generated.