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
graph LR
subgraph "Mobile Client"
WH_HOME((Warehouse Dashboard))
STOCK_DASH[Stock Dashboard Screen]
TRANSFER[Create Transfer Screen]
QUICK_TX[Quick Transfer]
RECEIVE[Receive Transfer Screen]
STOCK_OUT[Stock Out Flow]
LOSS_REC[Record Loss Screen]
LOSS_APP[Loss Approval Screen]
MOVE_HIST[Movement History Screen]
SYNC_TX[StockTransferSyncService]
SYNC_LOSS[StockLossSyncService]
SYNC_STOCK[StockSyncService]
end
subgraph "Backend API"
STOCK_CTRL[StockController]
XFER_CTRL[TransfersController]
OUT_CTRL[StockOutsController]
end
subgraph "Backend Services"
STOCK_SVC[StockService]
XFER_SVC[TransferService]
OUT_SVC[StockOutService]
LOSS_SVC[StockLossService]
ADJ_SVC[StockAdjustmentService]
MOVE_REC[StockMovementRecorder]
end
subgraph "Storage"
LOCAL_DB[[SQLite - Local Cache]]
REMOTE_DB[[AlmafricaDbContext]]
SP[[SharedPreferences]]
end
WH_HOME --> STOCK_DASH
WH_HOME --> TRANSFER
WH_HOME --> RECEIVE
WH_HOME --> LOSS_REC
WH_HOME --> LOSS_APP
WH_HOME --> MOVE_HIST
WH_HOME --> STOCK_OUT
STOCK_DASH -->|cached reads| LOCAL_DB
SYNC_STOCK -->|delta sync| STOCK_CTRL
STOCK_CTRL --> STOCK_SVC
STOCK_SVC --> REMOTE_DB
TRANSFER -->|save locally| LOCAL_DB
SYNC_TX -->|POST /api/transfers| XFER_CTRL
XFER_CTRL --> XFER_SVC
XFER_SVC --> MOVE_REC
MOVE_REC --> REMOTE_DB
QUICK_TX -->|POST /api/stock/transfers/quick| STOCK_CTRL
STOCK_CTRL --> STOCK_SVC
RECEIVE -->|POST complete/complete-with-discrepancy| XFER_CTRL
STOCK_OUT -->|draft + finalize| OUT_CTRL
OUT_CTRL --> OUT_SVC
OUT_SVC --> MOVE_REC
LOSS_REC -->|save locally| LOCAL_DB
SYNC_LOSS -->|POST /api/stock/losses| STOCK_CTRL
STOCK_CTRL --> LOSS_SVC
LOSS_SVC --> REMOTE_DB
LOSS_APP -->|PUT approve/reject| STOCK_CTRL
STOCK_CTRL --> LOSS_SVC
MOVE_HIST -->|GET movements| STOCK_CTRL
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
graph TD
A((Harvest Assessed - WF-QA)) --> B{Assessment passed?}
B -->|No| C[Reject - no stock created]
B -->|Yes| D[Backend: Create/update HarvestBatch record]
D --> E[Set batch status to Accepted]
E --> F[StockMovementRecorder.RecordIntake]
F --> G[Create StockMovement - type: Intake, qty: assessed weight]
G --> H[Update batch available quantity]
H --> I[Assign to collection point stock]
I --> J[Mobile: StockSyncService detects new batch on next delta sync]
J --> K[Update local SQLite cache via StockCacheLocalDatasource]
K --> L[StockDashboardProvider notifies UI]
L --> M[Stock Dashboard reflects new batch]
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
graph TD
A((User taps Create Transfer)) --> B{canInitiateStockTransfer?}
B -->|No| C[Action hidden / unauthorized]
B -->|Yes| D[Step 1: Select source center]
D --> E[Step 2: Select destination center]
E --> F[Step 3: Select crop type]
F --> G[Load available batches for crop type at source]
G --> H[Step 4: Select batches and quantities]
H --> I[Step 5: Review transfer summary]
I --> J[User confirms]
J --> K[StockTransferProvider.submitTransfer]
K --> L[Save to local SQLite via StockTransferLocalDatasource]
L --> M[Add to sync queue with status: pending]
M --> N[Show success - Transfer queued]
N --> O{Network available?}
O -->|Yes| P[StockTransferSyncService.syncPendingTransferRecords]
O -->|No| Q[Remains in queue until connectivity]
P --> R[POST /api/transfers via StockTransferRemoteDatasource]
R --> S{Backend: Validate transfer}
S -->|Invalid| T[Mark sync item as failed with error]
S -->|Valid| U[TransferService.CreateTransferAsync]
U --> V[Create Transfer entity - status: Pending]
V --> W[NO stock deducted yet - Pending state]
W --> X[Return transfer ID to mobile]
X --> Y[Update local record with server ID]
Y --> Z[UnifiedSyncStatusService.update - stockTransfer]
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
graph TD
A((User selects Quick Transfer)) --> B[Select source center]
B --> C[Select destination center]
C --> D[Select crop type and quantity]
D --> E[POST /api/stock/transfers/quick/preview]
E --> F{Backend: FEFO Allocation}
F --> G[Return batch allocations sorted by expiry]
G --> H[Display preview: batches, quantities, expiry dates]
H --> I{User confirms?}
I -->|No| J[Cancel - return to form]
I -->|Yes| K[POST /api/stock/transfers/quick]
K --> L{Backend: Execute transfer}
L --> M[Create Transfer entity - status: Completed]
M --> N[StockMovementRecorder: TransferOut from source]
N --> O[StockMovementRecorder: TransferIn at destination]
O --> P[Update batch quantities at both centers]
P --> Q[Return QuickTransferExecutionResponse]
Q --> R[Mobile: Show success with allocation details]
R --> S[StockSyncService invalidates cache for both centers]
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
graph TD
A((User initiates Stock Out)) --> B[POST /api/stockouts - Create draft]
B --> C[StockOutService creates draft with status: Draft]
C --> D[Return stockOutId]
D --> E{Add items to draft}
E --> F{Manual batch selection?}
F -->|Yes| G[POST /api/stockouts/{id}/items - manual batch + qty]
F -->|No - Auto FEFO| H[POST /api/stockouts/{id}/allocate-by-crop]
H --> I[FEFO allocation across batches for requested crop qty]
G --> J[Item added to draft]
I --> J
J --> K{More items?}
K -->|Yes| E
K -->|No| L[Review draft summary]
L --> M{User action}
M -->|Cancel| N[POST /api/stockouts/{id}/cancel]
N --> O[Draft cancelled - no stock impact]
M -->|Remove item| P[DELETE /api/stockouts/{id}/items/{itemId}]
P --> E
M -->|Finalize| Q[POST /api/stockouts/{id}/finalize]
Q --> R{Backend: Validate all items have stock}
R -->|Insufficient| S[Return 400 with details]
R -->|Valid| T[StockOutService.FinalizeAsync]
T --> U[Deduct quantities from batches]
U --> V[StockMovementRecorder: StockOut for each batch]
V --> W[Set status: Finalized]
W --> X[Return finalized StockOut details]
X --> Y[Mobile: Show finalization success]
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
graph TD
A((User taps Record Loss)) --> B{canRecordStockLoss?}
B -->|No| C[Action hidden]
B -->|Yes| D[RecordLossScreen: Load batches for center]
D --> E[Display batches sorted by FEFO - expired first]
E --> F[User selects batches - multi-select or select all expired]
F --> G[Enter quantities per batch]
G --> H[Select loss reason code]
H --> I[Add notes and optional photos]
I --> J[User confirms]
J --> K[BatchLossProvider.recordLoss]
K --> L[Save to local SQLite via StockLossLocalDatasource]
L --> M[Save photos locally with file paths]
M --> N[Add to sync queue - status: pending]
N --> O[Show success - Loss record queued]
O --> P{Network available?}
P -->|No| Q[Remains in local queue]
P -->|Yes| R[StockLossSyncService.syncPendingLossRecords]
R --> S[POST /api/stock/losses for each pending record]
S --> T{Backend: Validate loss}
T -->|Invalid| U[Mark sync item failed]
T -->|Valid| V[StockLossService: Create loss record - PendingApproval]
V --> W[Upload photos via POST /api/stock/losses/{id}/photos]
W --> X[Update local record with server ID]
X --> Y[Loss visible to managers]
Y --> Z((Manager opens Loss Approval))
Z --> AA[GET /api/stock/losses/pending - by center]
AA --> AB[LossApprovalScreen: Display pending losses]
AB --> AC{Manager decision}
AC -->|Approve| AD[PUT /api/stock/losses/{id}/approve]
AD --> AE[StockLossService: Approve]
AE --> AF[StockMovementRecorder: Loss movement - qty: -N]
AF --> AG[Update batch quantities]
AG --> AH[Loss status: Approved]
AC -->|Reject| AI[PUT /api/stock/losses/{id}/reject - with reason]
AI --> AJ[Loss status: Rejected - no stock impact]
AH --> AK[StockLossSyncService.pullApprovalUpdates]
AJ --> AK
AK --> AL[Mobile: Update local loss record status]
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
graph TD
A((User opens Stock Dashboard)) --> B[StockDashboardProvider.loadStock]
B --> C{Local cache exists and fresh?}
C -->|Yes - cache hit| D[Return cached CenterStockResponse from SQLite]
C -->|No - stale or empty| E[Attempt network fetch]
D --> F[Display: Categories > Crop Types > Batch summaries]
D --> G[Background refresh via StockSyncService]
E --> H{Network available?}
H -->|No| I{Stale cache exists?}
I -->|Yes| J[Return stale cache with staleness indicator]
I -->|No| K[Show empty state - no data available]
H -->|Yes| L[StockSyncService.syncCenterStock - centerId]
L --> M[Phase 1: GET /api/stock/centers - center summaries]
M --> N[StockCacheLocalDatasource: upsert center summaries]
N --> O[Phase 2: GET /api/stock/centers/{id} - aggregation]
O --> P[Cache aggregation - categories and crop types]
P --> Q[Phase 3: GET /api/stock/centers/{id}/batches?modifiedSince=X]
Q --> R[Delta sync: only changed batches since last sync]
R --> S[StockCacheLocalDatasource: upsert batches]
S --> T[SyncMetadataService: update lastSyncTime]
T --> U[Notify StockDashboardProvider]
U --> F
G --> V{Cache age > 5 min?}
V -->|No| W[Skip background refresh]
V -->|Yes| L
F --> X{User taps batch?}
X -->|Yes| Y[BatchDetailsScreen - WF-STOCK-07]
F --> Z{User taps All Centers?}
Z -->|Yes| AA[AllCentersStockScreen]
AA --> AB[CenterStockProvider.loadAllCenters]
AB --> AC[GET /api/stock/centers - all centers summary]
AC --> AD[Display center cards with stock totals]
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
graph TD
subgraph "Transfer Receive"
A((User opens Receive Transfer)) --> B{canReceiveStockTransfer?}
B -->|No| C[Action hidden]
B -->|Yes| D[IncomingTransferProvider.loadPendingTransfers]
D --> E[GET /api/transfers/incoming/{destinationId}]
E --> F[Display pending incoming transfers with batch details]
F --> G{User action on transfer}
G -->|Quick Confirm| H[POST /api/transfers/{id}/complete]
G -->|Report Discrepancy| I[Enter actual received quantities per batch]
I --> J[POST /api/transfers/{id}/complete-with-discrepancy]
H --> K{Backend: TransferService.CompleteTransferAsync}
J --> K
K --> L[Deduct stock from source center batches]
L --> M[Add stock to destination center batches]
M --> N[StockMovementRecorder: TransferOut at source]
N --> O[StockMovementRecorder: TransferIn at destination]
O --> P{Discrepancy reported?}
P -->|Yes| Q[Record discrepancy details - expected vs actual]
P -->|No| R[Transfer status: Completed]
Q --> R
R --> S[Mobile: Remove from pending list]
S --> T[Refresh stock cache for both centers]
end
subgraph "Movement History"
U((User opens Movement History)) --> V{canViewStockHistory?}
V -->|No| W[Action hidden]
V -->|Yes| X[MovementHistoryProvider.loadMovements]
X --> Y[GET /api/stock/movements?collectionPointId=X&page=1]
Y --> Z[Display paginated movement list]
Z --> AA{Scroll to bottom?}
AA -->|Yes| AB[Load next page - scroll pagination]
AB --> Y
Z --> AC{User taps batch?}
AC --> AD[GET /api/stock/batches/{batchId}/movements]
AD --> AE[BatchDetailsScreen with full movement timeline]
end
subgraph "Transfer History"
AF((User opens Transfer History)) --> AG[TransferHistoryProvider.loadHistory]
AG --> AH[Load from local SQLite + sync status]
AH --> AI[Display: pending sync, synced, failed counts]
AI --> AJ{Failed transfers?}
AJ -->|Yes| AK[Retry button - StockTransferSyncService.syncPendingTransferRecords]
AJ -->|No| AL[All transfers synced]
end
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)