Domain 06: Warehouse & Stock
Domain Owner: Backend (ASP.NET Core + EF Core) / Mobile (Flutter + SQLite + Offline Sync) Last Updated: 2026-03-10 Workflows: WF-STOCK-01 through WF-STOCK-07
Domain Introduction
The Warehouse & Stock domain manages the entire lifecycle of agricultural produce once it enters the collection center network -- from initial intake through inter-center transfers, stock outs, loss recording, and movement tracking. The backend uses ASP.NET Core controllers backed by dedicated services (StockService, TransferService, StockOutService, StockLossService, StockAdjustmentService) that record every quantity change as a StockMovement for full audit trail. The mobile client follows an offline-first architecture with local SQLite caching, a sync queue for outbound operations, and delta-sync for inbound stock data.
Key architectural principles:
- Offline-first operations: Transfers and loss records are saved locally first, queued for sync, and pushed to the backend when connectivity is available. Stock dashboard data is cached with a stale-while-revalidate pattern (5-minute in-memory TTL, 1-hour delta sync window).
- FEFO allocation: First Expiry First Out is the primary batch selection strategy for stock outs and quick transfers, ensuring perishable goods are moved before expiry.
- Movement audit trail: Every stock change (intake, transfer, stock out, loss, adjustment, disposal) creates a
StockMovementrecord linking the batch, quantity delta, movement type, and responsible user. - Permission-gated UI: Warehouse screens check
RoleManager.canRecordStockLoss(),canInitiateStockTransfer(),canReceiveStockTransfer(),canApproveStockLoss(), andcanViewStockHistory()to show/hide actions per user role. - Multi-center management: Warehouse managers can view stock across all assigned centers via
AllCentersStockScreen; operators are scoped to their assigned center viaCenterSelectionProvider. - Approval workflows: Loss records and stock adjustments follow a Created -> Pending Approval -> Approved/Rejected lifecycle, with manager review required before the adjustment impacts reported stock.
Stock Movement Tracking Chain
Every stock operation records one or more StockMovement entries through StockMovementRecorder:
Intake (harvest assessed) --> StockMovement(type: Intake, qty: +N)
Transfer Out --> StockMovement(type: TransferOut, qty: -N)
Transfer In --> StockMovement(type: TransferIn, qty: +N)
Stock Out --> StockMovement(type: StockOut, qty: -N)
Loss Approved --> StockMovement(type: Loss, qty: -N)
Adjustment Approved --> StockMovement(type: Adjustment, qty: +/-N)
Disposal --> StockMovement(type: Disposal, qty: -N)
Domain Summary Diagram
Workflows
WF-STOCK-01: Stock Intake
Trigger: Harvest batch passes quality assessment and is accepted at a collection center Frequency: Event-driven (each accepted harvest) Offline Support: Yes -- harvest data is recorded locally and synced; stock intake movement is created server-side upon sync
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Trigger | WF-QA assessment completion | Harvest batch assessment finalised | N/A -- upstream workflow |
| 2 | IF | Backend assessment logic | Check assessmentResult.passed | Rejected batches logged, no stock created |
| 3 | Function | StockService | Create or update HarvestBatch entity with accepted status | DB constraint violations return 400 |
| 4 | Function | StockMovementRecorder.RecordIntake() | Create StockMovement with type Intake, positive quantity | Transaction rollback on failure |
| 5 | Function | StockSyncService.syncCenterStock() | Delta sync via GET /api/stock/centers/{centerId} with modifiedSince | Retry on network failure; cached data served |
| 6 | Function | StockCacheLocalDatasource | Insert/update batch in stock_batch_cache SQLite table | SQLite write errors logged |
| 7 | Function | StockDashboardProvider.loadStock() | Notify listeners to rebuild dashboard UI | Error shown via ErrorState |
Data Transformations
- Input: Accepted
HarvestBatchwithassessedWeight,cropTypeId,collectionPointId,harvestDate,expiryDate,farmerId - Processing: Backend records the batch, creates an Intake movement, increments collection point stock totals
- Output:
StockMovement { type: Intake, batchId, quantity: +assessedWeight, collectionPointId, timestamp }persisted to DB; mobile cache updated on next sync
Error Handling
- Duplicate intake: Backend checks for existing intake movement on the same batch; returns 409 Conflict
- Sync failure: Mobile caches existing stock data; new batches appear on successful sync retry
- Assessment not found: 404 from backend; mobile logs error
Cross-References
- Triggered by: WF-QA (Quality Assessment workflow -- batch acceptance)
- Feeds into: WF-STOCK-06 (Stock Aggregation & Dashboard -- new batch visible)
- Feeds into: WF-STOCK-02/03/04 (batch available for transfers and stock outs)
WF-STOCK-02: Inter-Center Transfer
Trigger: Warehouse operator initiates a stock transfer between collection centers Frequency: On-demand (user action) Offline Support: Yes -- transfer records saved locally, queued for sync
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | WarehouseStockTab | User taps "Create Transfer" button | Permission check via RoleManager.canInitiateStockTransfer() |
| 2 | Form | CreateTransferScreen | 5-step wizard: source, destination, crop type, batches, review | Validation at each step; back navigation supported |
| 3 | Function | StockTransferProvider.submitTransfer() | Build transfer record from wizard state | Validation errors shown in UI |
| 4 | Function | StockTransferLocalDatasource.insertTransfer() | Persist transfer to stock_transfers SQLite table | SQLite constraint errors caught |
| 5 | Function | StockTransferSyncService.syncPendingTransferRecords() | Iterate pending records; call remote for each | Per-record error handling; failed items stay in queue |
| 6 | HTTP Request | StockTransferRemoteDatasource.createTransfer() | POST /api/transfers with CreateTransferRequest body | DioException caught; item marked failed |
| 7 | Function | TransferService.CreateTransferAsync() | Create Transfer entity with Pending status | DB errors return 500; validation errors return 400 |
| 8 | Function | UnifiedSyncStatusService.update() | Update sync status for SyncEntityType.stockTransfer | Non-fatal logging |
Data Transformations
- Input:
CreateTransferRequest { sourceCollectionPointId, destinationCollectionPointId, items: [{ batchId, quantity }], notes? } - Processing: Backend creates Transfer entity with Pending status. Stock is NOT deducted at creation -- deduction happens at completion (WF-STOCK-07).
- Output:
Transfer { id, status: Pending, sourceCollectionPointId, destinationCollectionPointId, items[], createdAt, createdBy }
Error Handling
- Insufficient stock: Backend validates batch available quantities; returns 400 with details
- Invalid center: 404 if source or destination collection point not found
- Sync failure: Transfer stays in local queue with
failedstatus; retry button available on Operations tab - Duplicate sync: Idempotent check via local
syncStatusfield prevents re-submission
Cross-References
- Triggers: WF-STOCK-07 (Transfer Receive -- destination operator sees pending incoming)
- Related: WF-STOCK-03 (Quick Transfer -- simplified single-screen alternative)
- Stock updated: WF-STOCK-06 (Dashboard refreshes after transfer completion)
WF-STOCK-03: Quick Transfer
Trigger: Warehouse operator uses simplified quick transfer for common batch movements Frequency: On-demand (user action) Offline Support: No -- requires network for FEFO preview and execution (server-side allocation)
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | WarehouseStockTab / Quick Actions | User initiates quick transfer | Permission check via RoleManager.canInitiateStockTransfer() |
| 2 | HTTP Request | CenterStockService.previewQuickTransfer() | POST /api/stock/transfers/quick/preview with QuickTransferRequest | DioException shows error; 400 shows validation message |
| 3 | Function | StockService (backend) | FEFO allocation: sort batches by expiryDate ASC, allocate requested qty | 400 if insufficient stock for requested quantity |
| 4 | HTTP Request | CenterStockService.executeQuickTransfer() | POST /api/stock/transfers/quick with confirmed request | DioException caught; error message shown |
| 5 | Function | StockMovementRecorder | Create paired TransferOut/TransferIn movements | Transaction rollback on failure |
| 6 | Function | StockSyncService | Invalidate local cache for source and destination centers | Non-fatal; next dashboard load triggers fresh sync |
Data Transformations
- Input:
QuickTransferRequest { sourceCollectionPointId, destinationCollectionPointId, cropTypeId, quantity } - Processing: Backend sorts batches by expiry (FEFO), allocates quantity across batches, creates completed transfer with paired stock movements
- Output:
QuickTransferExecutionResponse { transferId, batchAllocations: [{ batchId, batchNumber, quantity, expiryDate }], totalTransferred }
Error Handling
- Insufficient stock: Preview endpoint returns 400 with available vs requested quantity
- Partial allocation: FEFO may split across multiple batches; all shown in preview
- Network failure: Quick transfer requires connectivity; error shown with suggestion to use offline-capable full transfer (WF-STOCK-02)
- Concurrent modification: Backend uses optimistic concurrency; 409 Conflict if batch quantities changed between preview and execution
Cross-References
- Alternative to: WF-STOCK-02 (Inter-Center Transfer -- full offline-capable flow)
- Stock updated: WF-STOCK-06 (Dashboard refreshes for both centers)
- Movement recorded: Visible in WF-STOCK-07 (Movement History)
WF-STOCK-04: Stock Out
Trigger: Warehouse operator creates a stock out to fulfill a client order or other distribution Frequency: On-demand (order fulfillment or distribution events) Offline Support: Partial -- draft creation requires network for FEFO preview; finalization requires network
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | Stock Out UI | User starts stock out flow | Permission check; AgentPolicy required on backend |
| 2 | HTTP Request | Mobile | POST /api/stockouts with CreateStockOutRequest | 400 for validation errors; 401 for unauthorized |
| 3 | Function | StockOutService.CreateAsync() | Create StockOut entity with Draft status | DB errors return 500 |
| 4 | HTTP Request | Mobile | POST /api/stockouts/{id}/items or POST /api/stockouts/{id}/allocate-by-crop | 400 if batch not found or insufficient qty |
| 5 | Function | StockOutService.AllocateByCropAsync() | FEFO sort batches by expiry, allocate requested quantity | 400 if total available < requested |
| 6 | HTTP Request | Mobile | POST /api/stockouts/{id}/finalize | 400 if stock changed since draft; 404 if draft not found |
| 7 | Function | StockOutService.FinalizeAsync() | Deduct batch quantities, record stock movements | Transaction rollback on any failure |
| 8 | Function | StockMovementRecorder | Create StockMovement type StockOut per batch item | Part of finalize transaction |
Data Transformations
- Input:
CreateStockOutRequest { collectionPointId, stockOutTypeCode, destinationCenterId?, notes? }thenAddStockOutItemRequest { batchId, quantity }orAllocateByCropRequest { cropTypeId, quantity } - Processing: Draft collects items (manual or FEFO-allocated), finalization deducts stock and records movements atomically
- Output:
StockOutDetailDto { id, status, collectionPointId, items: [{ batchId, batchNumber, quantity, cropTypeName }], totalQuantity, stockOutType, finalizedAt }
Error Handling
- Stale draft: If batch quantities changed between item add and finalize, finalization returns 400 with updated availability
- Partial FEFO: If requested quantity exceeds available stock for a crop type, 400 with current available amount
- Cancel safety: Cancelling a draft has no stock impact since Draft status doesn't deduct
- Network required: All stock out operations require connectivity for real-time stock validation
Cross-References
- Related to: WF-CLI (Client order fulfillment may trigger stock out)
- Stock updated: WF-STOCK-06 (Dashboard reflects deductions after finalization)
- Movement recorded: Visible in WF-STOCK-07 (Movement History)
- FEFO shared with: WF-STOCK-03 (Quick Transfer uses same allocation strategy)
WF-STOCK-05: Loss Recording & Approval
Trigger: Warehouse operator records a stock loss (spoilage, damage, theft, etc.) Frequency: On-demand (when losses are discovered) Offline Support: Yes -- loss records saved locally with photo evidence, queued for sync; approval requires network
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | WarehouseStockTab | User taps "Record Loss" | RoleManager.canRecordStockLoss() gate |
| 2 | Form | RecordLossScreen | Multi-select batches, enter quantities, select reason, add notes/photos | Validation: qty > 0, qty <= available, reason required |
| 3 | Function | BatchLossProvider.recordLoss() | Build loss record from form state | Validation errors surfaced to UI |
| 4 | Function | StockLossLocalDatasource.insertLossRecord() | Persist to stock_loss_records SQLite table | SQLite errors logged |
| 5 | Function | StockLossSyncService.syncPendingLossRecords() | Iterate pending; POST each to server | Per-record error handling |
| 6 | HTTP Request | StockLossRemoteDatasource | POST /api/stock/losses | DioException caught; marked failed |
| 7 | HTTP Request | StockLossSyncService.uploadPendingPhotos() | POST /api/stock/losses/{id}/photos with IFormFile | Photo upload failure non-blocking; retried on next sync |
| 8 | Manual Trigger | LossApprovalScreen | Manager opens pending loss records | RoleManager.canApproveStockLoss() gate |
| 9 | HTTP Request | Manager mobile | PUT /api/stock/losses/{id}/approve or reject | Network required; error shown if offline |
| 10 | Function | StockLossService (backend) | Update loss status; on approve: record movement + deduct stock | Transaction rollback on failure |
| 11 | Function | StockLossSyncService.pullApprovalUpdates() | GET /api/stock/losses/updates with modifiedSince | Network errors silenced; retry on next sync |
Data Transformations
- Input (Recording):
CreateLossRecordRequest { collectionPointId, items: [{ batchId, quantity, reasonCode }], notes?, photos?: File[] } - Processing (Approval): Manager approves -> backend creates
StockMovement(type: Loss, qty: -N)per batch, deducts from available stock - Processing (Rejection): Manager rejects with reason -> no stock impact, loss record marked Rejected
- Output:
LossRecord { id, status: PendingApproval|Approved|Rejected, items[], approvedBy?, rejectedReason?, photos[] }
Error Handling
- Quantity exceeds available: Validation at form level and backend; 400 returned
- Photo upload failure: Non-blocking -- photos retry on next sync cycle; loss record syncs without photos initially
- Approval offline: Manager must have network to approve/reject; loss approval is server-side only
- Sync conflict: If batch quantities changed between recording and sync, backend validates; may reject with updated state
Cross-References
- Stock updated: WF-STOCK-06 (Dashboard reflects approved losses)
- Movement recorded: WF-STOCK-07 (Loss movements visible in history)
- Related: WF-QA (Quality Assessment may identify losses during inspection)
WF-STOCK-06: Stock Aggregation & Dashboard
Trigger: User navigates to Stock Dashboard, or background sync timer fires Frequency: On navigation (foreground) + periodic delta sync (background, 1-hour intervals) Offline Support: Yes -- full offline viewing from local SQLite cache; background refresh when connected
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | WarehouseStockTab / Home Quick Action | User navigates to stock dashboard | Permission implicit -- dashboard visible to warehouse roles |
| 2 | Function | StockDashboardProvider.loadStock() | Check local cache freshness, load or trigger sync | Error state with retry option |
| 3 | Function | StockCacheLocalDatasource.getCenterStock() | Read from center_stock_cache SQLite table | Returns null if no cache |
| 4 | Function | StockSyncService.syncCenterStock() | Three-phase sync: centers, aggregation, batches | Phase failures logged; partial data kept |
| 5 | HTTP Request | CenterStockService.getAllCentersStock() | GET /api/stock/centers | In-memory cache returned on failure; 5-min TTL |
| 6 | HTTP Request | CenterStockService.getCenterStock() | GET /api/stock/centers/{centerId} | SharedPreferences cache fallback |
| 7 | HTTP Request | CenterStockService.getCenterBatches() | GET /api/stock/centers/{centerId}/batches?modifiedSince=X | Delta sync with SyncMetadataService.getLastSyncTime() |
| 8 | Function | StockCacheLocalDatasource | Upsert center summaries, aggregation, and batch records | SQLite transaction for batch inserts |
| 9 | Function | SyncMetadataService.updateSyncTime() | Record last successful sync timestamp per entity | Non-fatal on failure |
Data Transformations
- Input (Sync):
modifiedSincetimestamp fromSyncMetadataService - Processing: Backend returns stock data modified since timestamp; mobile upserts into local cache
- Output (Display): Hierarchy:
Center -> Categories -> CropTypes -> Batcheswith quantities, expiry dates, quality grades - Cache layers:
- Layer 1: In-memory (
Map<String, CenterStockSummary>) with 5-min TTL inCenterStockService - Layer 2:
SharedPreferencesJSON cache per center inCenterStockService - Layer 3: SQLite tables in
StockCacheLocalDatasourcemanaged byStockSyncService
- Layer 1: In-memory (
Error Handling
- Network failure: Stale-while-revalidate -- cached data shown with optional staleness indicator; background retry
- Empty cache + no network: Empty state UI with message
- Delta sync gap: If
modifiedSinceis too old, backend may return full dataset; mobile handles gracefully via upsert - Cache corruption:
StockSyncServicecan force full resync by clearingSyncMetadataServicetimestamps
Cross-References
- Fed by: WF-STOCK-01 (new batches from intake), WF-STOCK-02/03 (transfers change quantities), WF-STOCK-04 (stock outs deduct), WF-STOCK-05 (approved losses deduct)
- Navigates to: WF-STOCK-07 (Batch details and movement history)
- Related: WF-QA (Assessment results may appear in batch detail quality grades)
WF-STOCK-07: Transfer Receive & Movement History
Trigger: (A) Destination operator reviews incoming transfers, or (B) user views batch/center movement history Frequency: On-demand (user action) Offline Support: Partial -- incoming transfer list cached; completion requires network; movement history requires network
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | WarehouseStockTab / WarehouseTransfersTab | User opens Receive Transfer screen | RoleManager.canReceiveStockTransfer() gate |
| 2 | HTTP Request | IncomingTransferProvider | GET /api/transfers/incoming/{destinationCollectionPointId} | Network error shows cached data or empty state |
| 3 | Form | ReceiveTransferScreen | Display pending transfers; quick confirm or discrepancy report | Transfer-level validation |
| 4 | HTTP Request | Mobile | POST /api/transfers/{id}/complete (full receipt) | DioException shown as error |
| 5 | HTTP Request | Mobile | POST /api/transfers/{id}/complete-with-discrepancy (partial receipt) | DioException shown as error |
| 6 | Function | TransferService.CompleteTransferAsync() | Deduct source, add destination, record movements | Transaction rollback on any failure |
| 7 | Function | StockMovementRecorder | Create paired TransferOut + TransferIn movements | Part of completion transaction |
| 8 | Manual Trigger | WarehouseStockTab | User opens Movement History | RoleManager.canViewStockHistory() gate |
| 9 | HTTP Request | MovementHistoryProvider.loadMovements() | GET /api/stock/movements with pagination params | Network required; error state with retry |
| 10 | HTTP Request | BatchDetailsProvider | GET /api/stock/batches/{batchId}/movements | Network required for full timeline |
| 11 | Function | TransferHistoryProvider | Load sync status from local SQLite (pending/synced/failed) | Local-only; no network needed |
Data Transformations
- Transfer Receive Input:
CompleteTransferRequest { transferId }orCompleteWithDiscrepancyRequest { transferId, items: [{ batchId, actualQuantity }] } - Transfer Receive Processing: Backend deducts source batches, credits destination batches, creates paired movements, records any discrepancy
- Transfer Receive Output:
Transfer { status: Completed, completedAt, discrepancyNotes? } - Movement History Input:
GET /api/stock/movements?collectionPointId=X&page=N&pageSize=20&startDate=&endDate= - Movement History Output: Paginated
StockMovement[] { id, batchId, type, quantity, collectionPointId, createdAt, createdBy, notes } - Batch Movement Input:
GET /api/stock/batches/{batchId}/movements - Batch Movement Output: Full timeline of all movements for a specific batch
Error Handling
- Transfer already completed: Backend returns 400 if transfer status is not Pending
- Transfer cancelled: Backend returns 400; mobile removes from pending list
- Discrepancy quantities: Backend validates actual quantities are non-negative and <= expected
- Movement history pagination: Network errors at page boundary show partial results with retry
- Failed sync retry:
StockTransferSyncServicere-attempts failed transfers; updatesUnifiedSyncStatusService
Cross-References
- Completes: WF-STOCK-02 (Inter-Center Transfer -- receive is the final step)
- Stock updated: WF-STOCK-06 (Dashboard refreshes after transfer completion)
- Movement displayed from: WF-STOCK-01 (Intake), WF-STOCK-02/03 (Transfers), WF-STOCK-04 (Stock Outs), WF-STOCK-05 (Losses)
- Related: WF-CLI (Client order movements visible in history)