Domain 08: Quality Assessment
Domain Owner: Backend (ASP.NET Core) / Mobile (Flutter offline-first) Last Updated: 2026-03-10 Workflows: WF-QA-01 through WF-QA-04
Domain Introduction
The Quality Assessment domain governs how agricultural produce is inspected, graded, and accepted (or rejected) at collection points. It is the bridge between field-level crop batch creation and warehouse stock recording: a crop batch enters the system via StartAssessmentAsync, is evaluated against a dynamic question checklist, and -- upon completion -- triggers a StockIn movement that feeds into the Stock Management domain (see WF-STOCK-01).
Key architectural principles:
- Dynamic checklists per crop category: Assessment questions are configured per
CropCategory(Cereals, Tubers, Fruits/Vegetables, Coffee/Cacao, Legumes) with category-specific QC fields. Questions support four types:Numeric,Boolean,SelectOptions, andDate. - Offline-first with draft auto-save: The mobile client stores assessment drafts locally with a 2-second debounce. If the device is offline when the agent completes the form, the assessment is queued in a local pending table and synced when connectivity returns.
- LOCAL_ batch ID pattern: Batches created offline carry a
LOCAL_prefixed ID. The sync strategy resolves these to server-generated IDs before submitting answers. - StockIn on acceptance: When
AcceptedQuantity > 0,CompleteAssessmentAsynccallsIStockMovementRecorder.RecordMovementAsyncto create aStockInmovement at the collection point -- directly linking quality assessments to inventory. - Delta sync for questions: The mobile client tracks a
modifiedSincetimestamp and fetches only updated questions, minimising bandwidth for field agents. - Photo pipeline: Photos are uploaded to DigitalOcean Spaces via
/api/upload/image, then attached to the assessment viaPOST /api/quality-assessments/{id}/photos.
Domain Summary Diagram
Workflows
WF-QA-01: Assessment Question Configuration
Trigger: Admin creates or updates assessment questions via the admin panel Frequency: On-demand (configuration changes by admin users) Offline Support: No (admin panel is web-only)
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | Admin Panel UI | Admin initiates question CRUD action | Validation errors shown inline |
| 2 | HTTP Request | QualityAssessmentQuestionsController | Routes CRUD to service; requires AdminPolicy | Returns 403 if user lacks admin role |
| 3 | Function | QualityAssessmentQuestionService.CreateQuestionAsync() | Validates constraints, creates question + options + translations | Returns Result.Failure with descriptive message |
| 4 | Function | QualityAssessmentQuestionService.UpdateQuestionAsync() | Replaces options (delete old, insert new), updates translations | 404 if question not found or soft-deleted |
| 5 | Function | QualityAssessmentQuestionService.DeleteQuestionAsync() | Soft-delete via ISoftDelete.IsDeleted = true | 404 if question not found |
| 6 | Function | ValidateQuestionConstraints() | Ensures SelectOptions has options, Numeric has valid min/max range | Collects all errors into a list, returns joined message |
| 7 | Database | AlmafricaDbContext.SaveChangesAsync() | Persists question, options, and translation records | DbUpdateException on constraint violation |
Data Transformations
- Input (Create):
CreateQuestionRequest { CropCategoryId, QuestionText, QuestionType, IsRequired, DisplayOrder, MinValue?, MaxValue?, UnitId?, Options[], Translations[] } - Processing: Validate constraints (type-specific rules), create
QualityAssessmentQuestionentity with childQualityAssessmentQuestionOptionentities andQualityAssessmentQuestionTranslation/QualityAssessmentQuestionOptionTranslationrecords - Output:
QualityAssessmentQuestionDtowith ID, all fields, nested options and translations - Export:
GET /api/admin/quality-assessment-questions/exportproduces an Excel file viaIExcelExportService
Error Handling
- Missing translations: Both
enandfrtranslations are required; validation rejects requests missing either language - Invalid type/option combo:
SelectOptionstype must have at least one option;Numerictype must haveMinValue < MaxValuewhen both are specified - Soft-delete protection: Deleted questions (ISoftDelete) are excluded from all query results; hard deletes are never performed
- Concurrent modification: EF Core optimistic concurrency via
UpdatedAttimestamps
Cross-References
- Consumed by: WF-QA-02 (questions displayed in the assessment form)
- Synced to mobile via: WF-QA-03 (delta sync picks up new/updated questions)
WF-QA-02: Field Assessment Execution
Trigger: Field agent selects a farmer and initiates a quality assessment Frequency: On-demand (per crop batch delivery at a collection point) Offline Support: Full -- batch creation, form answers, draft save, and completion all work offline
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | Agent Home UI | Agent taps "New Assessment" button | N/A |
| 2 | Function | RoleManager.canCreateAssessments() | Check CreateAssessments permission (storage-first, fallback to feature matrix) | Shows unauthorized message if false |
| 3 | Screen | SelectFarmerForAssessmentScreen | Farmer search with location/cooperative/recent filters via FarmerLocalService | Empty state if no farmers match |
| 4 | Screen | StartAssessmentScreen | Collect crop category, crop, quantity, unit, collection point, notes | Validates required fields before proceeding |
| 5 | HTTP Request | AssessmentRepositoryImpl.startAssessment() | POST /api/quality-assessments/start | Falls back to local batch creation on network error |
| 6 | Function | QualityAssessmentService.StartAssessmentAsync() | Validate farmer (approved), crop, collection point, unit; create CropBatch + empty QualityAssessment | Returns Result.NotFound or Result.Failure |
| 7 | Function | GenerateBatchNumberAsync() | Generate unique batch number | Concurrency-safe via DB query |
| 8 | Screen | AssessmentFormScreen | Dynamic question rendering, photo capture, quantity acceptance | Shows question-level validation errors |
| 9 | Function | AssessmentFormProvider | State manager: answers map, draft auto-save (2s debounce), quantity validation | Catches and surfaces all error types |
| 10 | Function | AssessmentFormProvider.completeAssessment() | Build CompleteAssessmentRequest, call repository, clear draft on success | Parses API errors, quantity mismatch, network, auth errors |
| 11 | Function | QualityAssessmentService.CompleteAssessmentAsync() | Validate quantities, determine result, save answers, trigger StockIn | Result.Failure for quantity mismatch, already-completed, missing checklist |
| 12 | Function | IStockMovementRecorder.RecordMovementAsync() | Create StockIn movement at collection point for accepted quantity | Errors propagate and roll back the transaction |
| 13 | Database | AlmafricaDbContext.SaveChangesAsync() | Persist assessment, answers, batch update, stock movement atomically | Transaction rollback on failure |
Data Transformations
- Start Input:
StartAssessmentRequest { FarmerId, CropId, CollectionPointId, InitialQuantity, QuantityUnitId, Notes? } - Start Output:
BatchCreatedResponse { BatchId, BatchNumber, Message } - Complete Input:
CompleteAssessmentRequest { CropBatchId, AcceptedQuantity, RejectedQuantity, RejectionReason?, Notes?, ExpiryDate?, CorrectiveActions?, Answers[] } - Each Answer:
{ QuestionId, QuestionType, NumericValue?, BooleanValue?, SelectedOptionId?, DateValue?, TextValue? } - Complete Output:
QualityAssessmentDtowith OverallResult enum:Accepted,PartiallyAccepted,FullyRejected
Error Handling
- Farmer not approved: Backend returns 400 with message "Farmer is not approved for quality assessments"
- Quantity mismatch:
AcceptedQuantity + RejectedQuantitymust equalInitialQuantitywithin 0.01 tolerance; returns descriptive error - Already completed: If
AssessmentStatusisCompletedorFullyRejected, returns "Assessment has already been completed" - No active checklist: If no
ChecklistVersionexists for the crop category, returns 400 - Offline completion: Saved to
pending_assessmentstable withPendingAssessmentStatus.pending; picked up by WF-QA-03 - Draft persistence failure: Logged but non-fatal; agent can still complete the form
Cross-References
- Uses questions from: WF-QA-01 (question configuration)
- Triggers stock recording: WF-STOCK-01 (StockIn movement via
IStockMovementRecorder) - Offline sync via: WF-QA-03 (pending assessment sync)
- Production cycle link: Assessment can link to a production cycle via
productionCycleIdonStartAssessmentScreen
WF-QA-03: Assessment Sync Flow
Trigger: Connectivity restored after offline assessment completion, or periodic sync Frequency: Event-driven (on connectivity change) and periodic (after main data sync) Offline Support: This workflow IS the offline recovery mechanism
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Trigger | AssessmentSyncIntegration.syncPendingAssessments() | Entry point; delegates to strategy, exposes progress stream | Catches and logs top-level errors |
| 2 | Function | AssessmentSyncStrategy.syncPendingAssessments() | Loads pending + retryable assessments, iterates with progress | Returns AssessmentSyncResult with counts |
| 3 | Function | AssessmentSyncStrategy._syncSingleAssessment() | Handles LOCAL_ ID resolution, builds request, uploads photos, calls complete | Catches per-assessment errors, marks failed |
| 4 | HTTP Request | AssessmentRemoteDataSource.startAssessment() | POST /api/quality-assessments/start for LOCAL_ batch resolution | AssessmentApiException with isRetryable flag |
| 5 | HTTP Request | AssessmentPhotoService.uploadPhotos() | Sequential POST /api/upload/image per photo (multipart/form-data) | Per-photo error logging; continues with remaining photos |
| 6 | HTTP Request | AssessmentPhotoService.attachPhotosToAssessment() | POST /api/quality-assessments/{id}/photos with URL list | Logged; non-fatal (assessment still valid without photos) |
| 7 | HTTP Request | AssessmentRemoteDataSource.completeAssessment() | POST /api/quality-assessments/complete with full payload | AssessmentApiException checked for retryability |
| 8 | Database | AssessmentLocalDataSource | Delete synced assessments, update retry counts, manage pending queue | SQLite errors caught and logged |
| 9 | Function | AssessmentQuestionsSyncHelper.syncAssessmentQuestions() | Bulk delta sync via modifiedSince parameter | Falls back to full sync on error |
| 10 | HTTP Request | AssessmentRemoteDataSource.getAllQuestions() | GET /api/quality-assessments/questions?modifiedSince=... | Network errors skip sync silently |
Data Transformations
- Pending assessment (local): SQLite row with batchId, farmerId, cropCategoryId, answers JSON, photo file paths, retryCount, status
- LOCAL_ batch resolution:
LOCAL_{uuid}batch ID replaced with server-generatedGuidafterstartAssessmentcall - Photo upload pipeline: Local
XFilepath -> multipart upload to/api/upload/image-> returned URL string -> attached via/api/quality-assessments/{id}/photos - Question delta sync:
modifiedSinceISO timestamp -> server returns only changedQualityAssessmentQuestionDto[]-> merged into local SQLite viasaveQuestions() - Sync result:
AssessmentSyncResult { success: bool, totalAttempted: int, successCount: int, failureCount: int }
Error Handling
- LOCAL_ batch creation failure: Assessment marked as failed with incremented
retryCount; retried on next sync cycle - Photo upload failure: Logged and continued; the assessment can still be completed without photos
- Complete API failure (retryable):
retryCount < 3-> mark for retry;retryCount >= 3-> mark aspermanentlyFailed - Idempotency:
OfflineCommandMetadataheaders ensure duplicate submissions are safely handled by the backend - Question sync failure: Caught silently; mobile continues using cached questions until next successful sync
- Token expiry during sync:
AssessmentPhotoServiceDio interceptor handles 401 with automatic token refresh
Cross-References
- Syncs assessments created in: WF-QA-02 (field assessment execution)
- Triggers on backend: WF-STOCK-01 (StockIn movement recorded during
CompleteAssessmentAsync) - Called after: Main data sync cycle (
syncAfterMainSync()extension method) - Question sync uses:
SyncConfig.isAssessmentQuestionsFresh()andSyncMetadataServicefor cache management
WF-QA-04: Warehouse Pending Assessments
Trigger: Warehouse operator opens the pending assessments view for a collection point Frequency: On-demand (warehouse staff reviewing pending and completed assessments) Offline Support: Partial -- cached assessments viewable offline; requires connectivity for fresh data
Workflow Diagram
Node Descriptions
| # | n8n Node Type | Component | Function | Error Handling |
|---|---|---|---|---|
| 1 | Manual Trigger | Warehouse Home UI | Operator navigates to pending assessments for a collection point | N/A |
| 2 | Screen | WarehousePendingAssessmentsScreen | Displays pending and completed assessments with tabs; receives WarehousePendingAssessmentsArgs | Shows empty state or error messages |
| 3 | HTTP Request | AssessmentRepositoryImpl.getPendingBatchesByCollectionPoint() | GET /api/quality-assessments/batches/pending?collectionPointId=X | Falls back to local cache on network error |
| 4 | Function | QualityAssessmentService.GetPendingBatchesAsync() | Query CropBatches filtered by collection point, status = PendingAssessment, with search and paging | Returns empty paged result on no matches |
| 5 | Function | QualityAssessmentService.GetAssessedBatchesAsync() | Query assessed batches with multi-field filters (farmer, crop, date range, result, search) | Returns empty paged result on no matches |
| 6 | HTTP Request | AssessmentRemoteDataSource.editAssessment() | PUT /api/quality-assessments/{assessmentId} | AssessmentApiException propagated to UI |
| 7 | Function | QualityAssessmentService.EditAssessmentAsync() | Update existing assessment: answers, quantities, result, notes | 404 if assessment not found; 400 for validation errors |
| 8 | Function | RoleManager.canEditAssessments() | Check EditAssessments permission | Hides edit button if false |
Data Transformations
- Input (Pending):
collectionPointId(Guid) + optional search string + page/pageSize - Output (Pending):
PagedResult<CropBatchDto>with batch details, farmer name, crop, quantities, assessment status - Input (Assessed):
QualityAssessmentFilterDto { FarmerId?, CropId?, StartDate?, EndDate?, OverallResult?, SearchTerm?, CollectionPointId?, Page, PageSize } - Output (Assessed):
PagedResult<CropBatchDto>with full assessment details including result, accepted/rejected quantities - Edit Input:
EditAssessmentRequestwith updated answers, quantities, notes - Edit Output: Updated
QualityAssessmentDto
Error Handling
- Network failure on load: Falls back to locally cached assessments from
completed_assessmentstable; shows stale-data indicator - Edit validation failure: Backend returns 400 with specific error (quantity mismatch, already-completed, etc.); displayed in UI snackbar
- Permission denied: Edit button hidden if
canEditAssessmentsis false; backend returns 403 if permission header is forged - Empty collection point: Shows empty state message; no error thrown
Cross-References
- Assessment execution: WF-QA-02 (navigates to
StartAssessmentScreenfor pending batches) - Stock impact: WF-STOCK-01 (completed assessments with
AcceptedQuantity > 0already triggeredStockInvia WF-QA-02/WF-QA-03) - Warehouse context: This workflow is accessed from the warehouse dashboard (accessible to
warehouseManagerandwarehouseOperatorroles) - Edit flow re-triggers:
EditAssessmentAsynccan update quantities and answers but does NOT re-trigger stock movements (stock adjustment is a separate flow)
Source File Reference
Backend
| File | Purpose |
|---|---|
backend/Almafrica.Domain/Entities/QualityAssessment.cs | Core assessment entity (1:1 with CropBatch) |
backend/Almafrica.Domain/Entities/QualityAssessmentAnswer.cs | Individual answer per question |
backend/Almafrica.Domain/Entities/Configurations/QualityAssessmentQuestion.cs | Question definition with type, constraints, translations |
backend/Almafrica.Domain/Entities/Configurations/QualityAssessmentQuestionOption.cs | Select-option choices with translations |
backend/Almafrica.Domain/Entities/Configurations/QualityAssessmentQuestionTranslation.cs | Question i18n (en/fr) |
backend/Almafrica.Domain/Entities/Configurations/QualityAssessmentQuestionOptionTranslation.cs | Option i18n (en/fr) |
backend/Almafrica.API/Controllers/QualityAssessmentsController.cs | REST endpoints for assessments (agent-facing) |
backend/Almafrica.API/Controllers/Admin/QualityAssessmentQuestionsController.cs | Admin CRUD endpoints for questions |
backend/Almafrica.Infrastructure/Services/QualityAssessmentService.cs | Core business logic: start, complete, edit, query |
backend/Almafrica.Infrastructure/Services/QualityAssessmentQuestionService.cs | Question CRUD with validation and soft-delete |
backend/Almafrica.Application/Interfaces/IQualityAssessmentService.cs | Service interface (16 methods) |
backend/Almafrica.Application/Interfaces/IQualityAssessmentQuestionService.cs | Question service interface |
Mobile
| File | Purpose |
|---|---|
mobile/mon_jardin/lib/presentation/select_farmer_for_assessment/select_farmer_for_assessment_screen.dart | Farmer selection screen with search/filter |
mobile/mon_jardin/lib/presentation/assessment/start_assessment_screen.dart | Assessment initiation: crop, quantity, collection point |
mobile/mon_jardin/lib/presentation/assessment/assessment_form_screen.dart | Dynamic question form with photo capture |
mobile/mon_jardin/lib/presentation/warehouse_pending_assessments/warehouse_pending_assessments_screen.dart | Warehouse view of pending/completed assessments |
mobile/mon_jardin/lib/presentation/providers/assessment_form_provider.dart | Form state management: answers, drafts, validation |
mobile/mon_jardin/lib/domain/repositories/assessment_repository.dart | Repository interface |
mobile/mon_jardin/lib/data/repositories/assessment_repository_impl.dart | Offline-first repository implementation (24h cache) |
mobile/mon_jardin/lib/data/datasources/local/assessment_local_datasource.dart | SQLite storage: questions, drafts, pending, completed |
mobile/mon_jardin/lib/data/datasources/remote/assessment_remote_datasource.dart | Dio HTTP client for assessment API |
mobile/mon_jardin/lib/data/services/assessment_sync_integration.dart | Sync coordinator singleton |
mobile/mon_jardin/lib/data/services/sync/strategies/assessment_sync_strategy.dart | Retry logic, LOCAL_ ID resolution, photo upload |
mobile/mon_jardin/lib/data/services/sync/assessment_questions_sync.dart | Question delta sync helper |
mobile/mon_jardin/lib/data/services/assessment_photo_service.dart | Photo upload to DigitalOcean Spaces + attach to assessment |