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


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 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

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

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

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

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

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

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)