Skip to main content

04 — Client & Order Lifecycle

Domain Overview

The Client & Order Lifecycle domain covers the end-to-end journey from client onboarding through order fulfillment. In the Almafrica platform, clients are business buyers (not farmers) who purchase agricultural produce. The lifecycle begins when a field agent registers a new client via the mobile app's multi-step form, continues through crop demand capture and admin approval, and culminates in order creation, fulfillment, and delivery.

This domain is deeply offline-first: client registration, crop demand capture, and order creation all save to local SQLite first, then sync to the backend via the queue-based sync engine. Documents use chunked uploads for reliability on poor networks.

Key Actors

ActorActions
Field AgentCreates clients, captures crop demands, manages client documents
Admin/ManagerApproves/rejects clients, creates and processes orders
CSA (Customer Service Agent)Views orders, manages delivery logistics
ClientViews their own orders (restricted access via JWT ClientId claim)

Domain Summary Diagram


Workflows


WF-CLI-01: Client Onboarding (Multi-Step)

Trigger: Agent taps "Add Client" button on AgentHomeScreen Frequency: On-demand (per client registration) Offline Support: Yes -- full offline creation with draft persistence and queue-based sync

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerAddClientScreenMultiStepAgent initiates client registrationN/A
2IFClientFormDraftService.hasDraft()Check for existing draft in SharedPreferencesReturns false on error
3FunctionClientFormDraftService.loadDraft()Restore form data, profile photo, documents from draftFalls back to empty form
4FormClientInfoStepCollects: company name, contact person/role, phone, email, province/territory/village, address, GPS coords, years in operation, preferred contact method, business type, registration number, collection point, payment method, bank details, currency, profile photoField-level validation, GPS auto-capture via LocationService
5ValidationClientFormData.isStep1Valid()Requires: company name, contact name, role, phone, province, address, years in operation, GPS (non-zero), preferred contactBlocks progression to Step 2
6FunctionClientFormDraftService.saveDraft()Persists form state to SharedPreferences as JSON (schema version 2)Logs error, continues
7FormCropDemandStepCollects per-crop: crop selection, sourcing origin, provinces/countries, purchase frequency, avg volume, price range, purchase priorities, common issues, notesSeparate validation form key for add-crop section
8FormClientDocumentsFormSectionFile picker (PDF, JPG, PNG, GIF), max 10MB, document type selection dialogSize validation, type restriction
9DatabaseClientDraftService.saveClientLocally()INSERT into clients table with UUID, sync_status=pending, approval_status=PendingApprovalSchema-aware column detection cached
10QueueSyncQueueService.enqueueEntity()Enqueues client entity for background syncPersists to sync_queue table
11DatabaseClientCropDemandDraftService.saveDrafts()Batch INSERT into client_crop_demand_drafts with client local IDBatch commit for performance
12FunctionClientDocumentService.uploadDocument()Copies file to local storage, creates LocalClientDocument, attempts immediate upload if onlineFalls back to pending status
13APIPOST /api/clientsClientsController.Create -- validates via CreateClientRequestDtoValidator, creates Identity user, creates Client entityReturns 400 with validation errors
14FunctionClientService._reconcileAfterSync()Updates local record with server ID, links crop demand drafts to server client IDHandles ID mapping

Data Transformations

  • Input: ClientFormData (company name, contact person, GPS, crop demands, documents)
  • Processing: ClientFormData.toMap() serializes to flat JSON for draft; toApiPayload() on crop demands strips local-only fields; ClientDraftService builds LocalClient model from form data
  • Output: LocalClient row in SQLite (offline) or ClientResponse DTO from backend (online)

Error Handling

  • Draft corruption: _normalizeDraftShape() handles schema migration between draft versions, normalizes paymentAccountNumber/paymentAccount aliasing, and repairs localPath/filePath inconsistencies in document entries
  • GPS failure: Falls back to manual coordinate entry if LocationService cannot acquire position
  • Sync failure: Client remains in local SQLite with sync_status=failed; SyncManager retries on next connectivity event
  • Server validation error: HTTP 400 response stored in sync_error column; client shown to agent with error badge

Cross-References

  • Triggers: WF-CLI-02 (crop demands can be added inline), WF-CLI-06 (documents uploaded during onboarding)
  • Triggered by: Agent home screen navigation
  • Related: WF-SYNC-01 (sync engine processes the queue)

WF-CLI-02: Client Crop Demand Creation

Trigger: Agent adds crop demand during client onboarding (Step 2) or via standalone ClientCropDemandFormScreen Frequency: On-demand (per crop demand entry) Offline Support: Yes -- saved as local draft, synced via CropDemandsSync

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerCropDemandStep / ClientCropDemandFormScreenUser initiates crop demand entrySeparate form key prevents validation conflict with main form
2FormCropSelectionWidgetCategory-filtered crop picker from cached master dataFalls back to text search
3FunctionClientMasterDataResolverResolves local/display IDs to server GUIDs for: measurement units, currencies, countries, purchase priorities, common issuesThrows if critical IDs missing
4DatabaseClientCropDemandDraftService.saveDraftWithPhoto()INSERT into client_crop_demand_drafts with UUID, payload JSON, optional pending photo pathReturns null on error
5APIPOST /api/clients/{clientId}/crop-demandsClientCropDemandService.CreateClientCropDemandAsync -- backend derives cropCategoryId from crop entity400 on validation failure
6FunctionClientCropDemandDraftService.markAsSynced()Deletes draft if no pending photo; keeps draft with server ID if photo upload neededHandles photo lifecycle
7APIPOST /api/clients/{id}/crop-demands/{id}/photoIClientCropDemandService.UpdateCropDemandPhotoAsyncRetried by CropDemandsSync.uploadPendingPhotos()

Data Transformations

  • Input: ClientCropDemandData (cropId, sourcingOrigin, provinces/countries, frequency, volume, price range, priorities, issues, notes)
  • Processing: ClientCropDemandData.toApiPayload() strips cropCategoryId (backend derives it); ClientMasterDataResolver maps display values to server GUIDs; _mergeServerDemandsWithPendingDrafts() overlays local edits on cached server data
  • Output: LocalClientCropDemand row in client_crop_demand_drafts table; ClientCropDemandDto from API

Error Handling

  • Master data missing: Throws descriptive exception (e.g., "Missing measurement unit ... Sync master data and retry") -- UI shows error message
  • 403 Forbidden: Client may have been reassigned; silently skips pull, returns cached data
  • Cache invalidation: _invalidatedClientIds set forces fresh read after local update, bypassing stale cache
  • Offline merge: _mergeServerDemandsWithPendingDrafts() deduplicates by serverDemandId, overlaying local pending edits on top of cached server data

Cross-References

  • Triggers: WF-CLI-06 (photo upload for crop demand)
  • Triggered by: WF-CLI-01 (inline during onboarding), standalone from client details screen
  • Related: WF-SYNC-03 (crop demand sync via CropDemandsSync)

WF-CLI-03: Client Approval Workflow

Trigger: Admin reviews pending client on web dashboard or via API Frequency: Event-driven (per client submission) Offline Support: No -- approval requires backend connectivity

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerAdmin Dashboard / ClientsControllerAdmin initiates reviewN/A
2APIGET /api/clients/{id}Fetches full client details with navigation properties404 if not found
3APIPOST /api/clients/{id}/approveClientsController.ApproveClient delegates to IClientApprovalService403 if not authorized
4APIPOST /api/clients/{id}/rejectClientsController.RejectClient requires reason in body400 if reason missing
5FunctionClientApprovalService.ApproveClientAsync()Wraps in audit scope, calls client.Approve(authUserId) domain method, persistsResult.Failure on not found
6FunctionClientApprovalService.RejectClientAsync()Validates reason, calls client.Reject(authUserId, reason) domain methodArgumentException caught and returned as failure
7Databasecontext.SaveChangesAsync()Persists status change to PostgreSQLTransaction rollback on failure
8SyncMobile sync pullAgent's SyncManager pulls updated client status on next sync cycleStatus reflected in local approval_status column

Data Transformations

  • Input: Client ID + admin decision (approve or reject with reason)
  • Processing: Client.Approve(authUserId) / Client.Reject(authUserId, reason) domain methods mutate entity state, setting ApprovalStatus, ApprovedByUserId, ApprovalDate (or rejection equivalents)
  • Output: Updated Client entity in PostgreSQL; status propagated to mobile via next sync

Error Handling

  • Concurrent modification: EF Core concurrency tokens prevent conflicting updates
  • Invalid state transition: Domain entity validates current state before allowing approve/reject
  • Mobile staleness: Agent may see stale PendingApproval until next sync; ClientService._syncSingleClient() reconciles on pull
  • Rejected client editing: ClientRemovedFromQueueNotification alerts agent if client is locked post-approval

Cross-References

  • Triggers: WF-CLI-04 (approved clients can have orders created)
  • Triggered by: WF-CLI-01 (client submission creates pending approval)
  • Related: WF-AUTH-01 (admin permissions checked via [Authorize])

WF-CLI-04: Order Creation

Trigger: Admin/CSA creates order for a client via CreateOrderScreen Frequency: On-demand (per order) Offline Support: Yes -- local-first with sync queue

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerCreateOrderScreenUser initiates order creationN/A
2FormCreateOrderScreenCollects: client, order items (crop, qty, unit, price, delivery date), delivery address, delivery method, logistics partner, currency, notesField validation
3FunctionOrderService.createOrder()Builds Order object with LOCAL-{uuid} number, calculates total, generates UUIDs for itemsN/A
4DatabaseOrderLocalService.saveOrderOffline()INSERT into orders + order_items tables with sync_status=pendingReturns null on error
5QueueSyncQueueService.enqueueEntity()Enqueues with entityType=order, operation=create, priority 1Persisted to queue table
6APIPOST /api/ordersOrdersController.Create -- sets createdByUserId from JWT, delegates to IOrderService.CreateOrderAsync()400 on validation failure
7FunctionOrderService._reconcileCreatedOrder()Saves server order, updates server_id on old local row, moves order items to server ID, deletes old local rowHandles ID mismatch
8SyncOrderSyncService.syncOrderQueueItem()Background processor for queued creates/updates/deletesReconciles on success, throws on failure

Data Transformations

  • Input: CreateOrderRequest (clientId, orderDate, expectedDeliveryDate, deliveryAddress, deliveryMethodId, logisticsPartnerId, currencyId, notes, list of CreateOrderItemRequest)
  • Processing: Each CreateOrderItemRequest specifies either cropId (existing crop) or cropTypeId (find-or-create); backend generates sequential orderNumber; mobile assigns temporary LOCAL-{uuid.substring(0,8)} number
  • Output: Order model with items in SQLite (offline) or OrderDto from API (online)

Error Handling

  • Network timeout: _isNetworkError() detects connection/send/receive timeouts; order stays queued
  • Server validation failure: Non-network DioException triggers markAsFailed() with extracted error message
  • Duplicate submission: Queue processor de-duplicates by entityId
  • Stale client ID: _resolveServerId() maps local UUIDs to server IDs for clients created offline

Cross-References

  • Triggers: WF-CLI-05 (order enters fulfillment pipeline)
  • Triggered by: WF-CLI-03 (client must be approved to receive orders)
  • Related: WF-STOCK-01 (stock checked during out-for-delivery transition)

WF-CLI-05: Order Fulfillment Flow

Trigger: Admin/CSA progresses order through status transitions Frequency: Event-driven (per status change) Offline Support: Yes -- optimistic local status update with sync queue

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerOrderDetailsScreen / OrdersListScreenUser triggers status transitionN/A
2FunctionOrderService._offlineStatusTransition()Generic handler: reads existing order, applies optimistic status update, enqueues, attempts API callCatches DioException, distinguishes network vs. server errors
3DatabaseOrderLocalService.saveOrderOffline()Overwrites order row with new status, sync_status=pendingReturns null on failure
4QueueSyncQueueService.enqueueEntity()Enqueues with entityType=orderStatusTransition, priority 2 (higher than creates)Persisted to queue
5APIPOST /api/orders/{id}/out-for-deliveryOrdersController.MarkAsOutForDelivery -- passes userId for stock out finalization; backend performs FEFO allocation (First Expiry First Out) to deduct warehouse stock400 if insufficient stock
6APIPOST /api/orders/{id}/deliveredOrdersController.MarkAsDelivered -- optional DeliveryDate parameter400 on invalid transition
7APIPOST /api/orders/{id}/cancelOrdersController.Cancel -- optional reason400 on invalid transition
8APIPOST /api/orders/{id}/paymentOrdersController.RecordPayment -- requires Permission.ManagePayments400 on validation
9SyncOrderSyncService.syncStatusTransitionQueueItem()Resolves server ID, calls correct endpoint based on action field, reconciles responseThrows on non-200 response

Data Transformations

  • Input: Order ID + action string (confirm, start_processing, out_for_delivery, delivered, cancel) + optional reason/delivery date
  • Processing: _offlineStatusTransition() builds optimistic Order.copyWith(status: newStatus), enqueues payload {orderId, action, reason?}. Backend validates state machine transitions, performs stock deductions on out-for-delivery.
  • Output: Updated Order in local SQLite (immediate) and PostgreSQL (on sync); stock movements created in warehouse (for out-for-delivery)

Error Handling

  • Invalid transition: Backend returns 400 if state machine rules violated (e.g., cannot go from Pending to Delivered directly); mobile markAsFailed() stores error
  • Insufficient stock: Out-for-delivery fails if FEFO allocation cannot satisfy all order items; order remains InProgress
  • Optimistic rollback: If sync fails, the local order retains the optimistic status but sync_status=failed alerts the UI; next successful sync reconciles
  • Payment failures: RecordPaymentRequest amount added optimistically to totalPaid; on API failure, local totals may drift until next sync pull

Cross-References

  • Triggers: WF-STOCK-03 (out-for-delivery triggers FEFO stock deduction)
  • Triggered by: WF-CLI-04 (order creation starts in Pending status)
  • Related: WF-SYNC-01 (order sync via OrderSyncService)

WF-CLI-06: Client Document Management

Trigger: Agent attaches document during client registration (Step 3) or from client details screen Frequency: On-demand (per document upload) Offline Support: Yes -- files stored locally first, uploaded when connectivity available

Workflow Diagram

Node Descriptions

#n8n Node TypeComponentFunctionError Handling
1TriggerClientDocumentsFormSectionUser picks file via FilePickerSize/type validation
2Validation_addDocument()Checks file size <= 10MB, extension in [pdf, jpg, jpeg, png, gif]Shows snackbar error
3UIDocument type dialogUser selects from: BusinessLicense, NationalId, TaxCertificate, BankStatement, ProofOfAddress, OtherRequired selection
4FunctionClientDocumentLocalDataSource.copyFileToLocalStorage()Copies original file to app's local document storage directoryThrows on copy failure
5DatabaseClientDocumentLocalDataSource.insertDocument()INSERT LocalClientDocument with UUID, sync_status=pendingError propagated
6FunctionClientDocumentService._uploadToServer()Routes to chunked or direct upload based on 1MB thresholdFalls back from chunked to direct for files under 10MB
7FunctionChunkedUploadService.uploadFile()Splits file into chunks, uploads to DigitalOcean Spaces via presigned URLs, returns CDN URLProgress callback for UI
8APIPOST /api/clients/{id}/documentsClientsController.UploadDocument -- direct multipart upload, 120s timeoutDioException with status code
9APIPOST /api/clients/{id}/documents/registerClientsController.RegisterDocument -- registers chunked upload URL with backendReturns ClientDocumentDto
10DatabaseClientDocumentLocalDataSource.updateSyncStatus()Updates sync status, stores serverId and documentUrl from server responseN/A
11FunctionClientDocumentService.syncPendingDocuments()Iterates pending documents, skips if client not synced, uploads remainingReturns count of synced docs

Data Transformations

  • Input: File path + document type + optional description
  • Processing: _normalizeDocumentType() maps legacy names (e.g., TaxRegistration to TaxCertificate); _getMimeType() derives MIME from extension; chunked upload splits into chunks and reassembles at CDN; direct upload uses FormData with MultipartFile
  • Output: LocalClientDocument in SQLite with documentUrl pointing to CDN (DigitalOcean Spaces)

Error Handling

  • Chunked upload failure: Falls back to direct upload if file size <= 10MB; otherwise fails permanently
  • Client not yet synced: Documents queued as pending; syncPendingDocuments() skips documents whose parent client has no server_id; retried when client syncs
  • File not found: _uploadToServer() checks File(localFilePath).exists() before attempting upload; throws if missing
  • Retry mechanism: retryDocumentSync() method allows manual retry from UI; syncPendingDocuments() runs during each sync cycle
  • Deduplication: getClientDocuments() merges local docs (by clientLocalId) with server docs (by serverId), avoiding duplicates when both sources have the same document

Cross-References

  • Triggers: None directly
  • Triggered by: WF-CLI-01 (documents attached during onboarding), client details screen (post-creation document adds)
  • Related: WF-SYNC-01 (sync cycle calls syncPendingDocuments())

Domain Integration Map


Key Data Tables

SQLite TablePurposeKey Columns
clientsLocal client recordsid, server_id, company_name, approval_status, sync_status, assigned_agent_id
client_crop_demand_draftsCrop demand drafts and cacheid, client_local_id, client_server_id, payload (JSON), sync_status, pending_photo_path
client_documentsDocument metadata and sync trackingid, client_local_id, server_id, document_type, local_file_path, document_url, sync_status
ordersOrder records with offline supportid, server_id, order_number, client_id, status (int), total_amount, sync_status
order_itemsLine items per orderid, order_id, crop_id, quantity, unit_price, total_price
sync_queuePending sync operationsentity_type, entity_id, operation, payload_json, priority

API Endpoints Summary

MethodEndpointPermissionController
POST/api/clientsCreateClientsClientsController.Create
POST/api/clients/{id}/approveApproveClientsClientsController.ApproveClient
POST/api/clients/{id}/rejectApproveClientsClientsController.RejectClient
POST/api/clients/{id}/documentsCreateClientsClientsController.UploadDocument
POST/api/clients/{id}/documents/registerCreateClientsClientsController.RegisterDocument
GET/api/clients/{id}/crop-demandsViewClientsClientsController.GetCropDemands
POST/api/clients/{id}/crop-demandsCreateClientsClientsController.AddCropDemand
PUT/api/clients/{id}/crop-demands/{demandId}EditClientsClientsController.UpdateCropDemand
POST/api/ordersCreateOrdersOrdersController.Create
PUT/api/orders/{id}EditOrdersOrdersController.Update
POST/api/orders/{id}/confirmProcessOrdersOrdersController.Confirm
POST/api/orders/{id}/start-processingProcessOrdersOrdersController.StartProcessing
POST/api/orders/{id}/out-for-deliveryProcessOrdersOrdersController.MarkAsOutForDelivery
POST/api/orders/{id}/deliveredProcessOrdersOrdersController.MarkAsDelivered
POST/api/orders/{id}/cancelCancelOrdersOrdersController.Cancel
POST/api/orders/{id}/paymentManagePaymentsOrdersController.RecordPayment
GET/api/orders/summaryViewOrdersOrdersController.GetSummary