Skip to main content

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 StockMovement record linking the batch, quantity delta, movement type, and responsible user.
  • Permission-gated UI: Warehouse screens check RoleManager.canRecordStockLoss(), canInitiateStockTransfer(), canReceiveStockTransfer(), canApproveStockLoss(), and canViewStockHistory() 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 via CenterSelectionProvider.
  • 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 TypeComponentFunctionError Handling
1TriggerWF-QA assessment completionHarvest batch assessment finalisedN/A -- upstream workflow
2IFBackend assessment logicCheck assessmentResult.passedRejected batches logged, no stock created
3FunctionStockServiceCreate or update HarvestBatch entity with accepted statusDB constraint violations return 400
4FunctionStockMovementRecorder.RecordIntake()Create StockMovement with type Intake, positive quantityTransaction rollback on failure
5FunctionStockSyncService.syncCenterStock()Delta sync via GET /api/stock/centers/{centerId} with modifiedSinceRetry on network failure; cached data served
6FunctionStockCacheLocalDatasourceInsert/update batch in stock_batch_cache SQLite tableSQLite write errors logged
7FunctionStockDashboardProvider.loadStock()Notify listeners to rebuild dashboard UIError shown via ErrorState

Data Transformations

  • Input: Accepted HarvestBatch with assessedWeight, 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 TypeComponentFunctionError Handling
1Manual TriggerWarehouseStockTabUser taps "Create Transfer" buttonPermission check via RoleManager.canInitiateStockTransfer()
2FormCreateTransferScreen5-step wizard: source, destination, crop type, batches, reviewValidation at each step; back navigation supported
3FunctionStockTransferProvider.submitTransfer()Build transfer record from wizard stateValidation errors shown in UI
4FunctionStockTransferLocalDatasource.insertTransfer()Persist transfer to stock_transfers SQLite tableSQLite constraint errors caught
5FunctionStockTransferSyncService.syncPendingTransferRecords()Iterate pending records; call remote for eachPer-record error handling; failed items stay in queue
6HTTP RequestStockTransferRemoteDatasource.createTransfer()POST /api/transfers with CreateTransferRequest bodyDioException caught; item marked failed
7FunctionTransferService.CreateTransferAsync()Create Transfer entity with Pending statusDB errors return 500; validation errors return 400
8FunctionUnifiedSyncStatusService.update()Update sync status for SyncEntityType.stockTransferNon-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 failed status; retry button available on Operations tab
  • Duplicate sync: Idempotent check via local syncStatus field 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 TypeComponentFunctionError Handling
1Manual TriggerWarehouseStockTab / Quick ActionsUser initiates quick transferPermission check via RoleManager.canInitiateStockTransfer()
2HTTP RequestCenterStockService.previewQuickTransfer()POST /api/stock/transfers/quick/preview with QuickTransferRequestDioException shows error; 400 shows validation message
3FunctionStockService (backend)FEFO allocation: sort batches by expiryDate ASC, allocate requested qty400 if insufficient stock for requested quantity
4HTTP RequestCenterStockService.executeQuickTransfer()POST /api/stock/transfers/quick with confirmed requestDioException caught; error message shown
5FunctionStockMovementRecorderCreate paired TransferOut/TransferIn movementsTransaction rollback on failure
6FunctionStockSyncServiceInvalidate local cache for source and destination centersNon-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 TypeComponentFunctionError Handling
1Manual TriggerStock Out UIUser starts stock out flowPermission check; AgentPolicy required on backend
2HTTP RequestMobilePOST /api/stockouts with CreateStockOutRequest400 for validation errors; 401 for unauthorized
3FunctionStockOutService.CreateAsync()Create StockOut entity with Draft statusDB errors return 500
4HTTP RequestMobilePOST /api/stockouts/{id}/items or POST /api/stockouts/{id}/allocate-by-crop400 if batch not found or insufficient qty
5FunctionStockOutService.AllocateByCropAsync()FEFO sort batches by expiry, allocate requested quantity400 if total available < requested
6HTTP RequestMobilePOST /api/stockouts/{id}/finalize400 if stock changed since draft; 404 if draft not found
7FunctionStockOutService.FinalizeAsync()Deduct batch quantities, record stock movementsTransaction rollback on any failure
8FunctionStockMovementRecorderCreate StockMovement type StockOut per batch itemPart of finalize transaction

Data Transformations

  • Input: CreateStockOutRequest { collectionPointId, stockOutTypeCode, destinationCenterId?, notes? } then AddStockOutItemRequest { batchId, quantity } or AllocateByCropRequest { 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 TypeComponentFunctionError Handling
1Manual TriggerWarehouseStockTabUser taps "Record Loss"RoleManager.canRecordStockLoss() gate
2FormRecordLossScreenMulti-select batches, enter quantities, select reason, add notes/photosValidation: qty > 0, qty <= available, reason required
3FunctionBatchLossProvider.recordLoss()Build loss record from form stateValidation errors surfaced to UI
4FunctionStockLossLocalDatasource.insertLossRecord()Persist to stock_loss_records SQLite tableSQLite errors logged
5FunctionStockLossSyncService.syncPendingLossRecords()Iterate pending; POST each to serverPer-record error handling
6HTTP RequestStockLossRemoteDatasourcePOST /api/stock/lossesDioException caught; marked failed
7HTTP RequestStockLossSyncService.uploadPendingPhotos()POST /api/stock/losses/{id}/photos with IFormFilePhoto upload failure non-blocking; retried on next sync
8Manual TriggerLossApprovalScreenManager opens pending loss recordsRoleManager.canApproveStockLoss() gate
9HTTP RequestManager mobilePUT /api/stock/losses/{id}/approve or rejectNetwork required; error shown if offline
10FunctionStockLossService (backend)Update loss status; on approve: record movement + deduct stockTransaction rollback on failure
11FunctionStockLossSyncService.pullApprovalUpdates()GET /api/stock/losses/updates with modifiedSinceNetwork 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 TypeComponentFunctionError Handling
1Manual TriggerWarehouseStockTab / Home Quick ActionUser navigates to stock dashboardPermission implicit -- dashboard visible to warehouse roles
2FunctionStockDashboardProvider.loadStock()Check local cache freshness, load or trigger syncError state with retry option
3FunctionStockCacheLocalDatasource.getCenterStock()Read from center_stock_cache SQLite tableReturns null if no cache
4FunctionStockSyncService.syncCenterStock()Three-phase sync: centers, aggregation, batchesPhase failures logged; partial data kept
5HTTP RequestCenterStockService.getAllCentersStock()GET /api/stock/centersIn-memory cache returned on failure; 5-min TTL
6HTTP RequestCenterStockService.getCenterStock()GET /api/stock/centers/{centerId}SharedPreferences cache fallback
7HTTP RequestCenterStockService.getCenterBatches()GET /api/stock/centers/{centerId}/batches?modifiedSince=XDelta sync with SyncMetadataService.getLastSyncTime()
8FunctionStockCacheLocalDatasourceUpsert center summaries, aggregation, and batch recordsSQLite transaction for batch inserts
9FunctionSyncMetadataService.updateSyncTime()Record last successful sync timestamp per entityNon-fatal on failure

Data Transformations

  • Input (Sync): modifiedSince timestamp from SyncMetadataService
  • Processing: Backend returns stock data modified since timestamp; mobile upserts into local cache
  • Output (Display): Hierarchy: Center -> Categories -> CropTypes -> Batches with quantities, expiry dates, quality grades
  • Cache layers:
    • Layer 1: In-memory (Map<String, CenterStockSummary>) with 5-min TTL in CenterStockService
    • Layer 2: SharedPreferences JSON cache per center in CenterStockService
    • Layer 3: SQLite tables in StockCacheLocalDatasource managed by StockSyncService

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 modifiedSince is too old, backend may return full dataset; mobile handles gracefully via upsert
  • Cache corruption: StockSyncService can force full resync by clearing SyncMetadataService timestamps

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 TypeComponentFunctionError Handling
1Manual TriggerWarehouseStockTab / WarehouseTransfersTabUser opens Receive Transfer screenRoleManager.canReceiveStockTransfer() gate
2HTTP RequestIncomingTransferProviderGET /api/transfers/incoming/{destinationCollectionPointId}Network error shows cached data or empty state
3FormReceiveTransferScreenDisplay pending transfers; quick confirm or discrepancy reportTransfer-level validation
4HTTP RequestMobilePOST /api/transfers/{id}/complete (full receipt)DioException shown as error
5HTTP RequestMobilePOST /api/transfers/{id}/complete-with-discrepancy (partial receipt)DioException shown as error
6FunctionTransferService.CompleteTransferAsync()Deduct source, add destination, record movementsTransaction rollback on any failure
7FunctionStockMovementRecorderCreate paired TransferOut + TransferIn movementsPart of completion transaction
8Manual TriggerWarehouseStockTabUser opens Movement HistoryRoleManager.canViewStockHistory() gate
9HTTP RequestMovementHistoryProvider.loadMovements()GET /api/stock/movements with pagination paramsNetwork required; error state with retry
10HTTP RequestBatchDetailsProviderGET /api/stock/batches/{batchId}/movementsNetwork required for full timeline
11FunctionTransferHistoryProviderLoad sync status from local SQLite (pending/synced/failed)Local-only; no network needed

Data Transformations

  • Transfer Receive Input: CompleteTransferRequest { transferId } or CompleteWithDiscrepancyRequest { 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: StockTransferSyncService re-attempts failed transfers; updates UnifiedSyncStatusService

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)