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
graph LR
subgraph "Admin Panel"
QCONFIG((Configure Questions))
EXPORT[Export to Excel]
end
subgraph "Mobile Client"
SELECT_FARMER[Select Farmer]
START[Start Assessment]
FORM[Assessment Form + Photos]
DRAFT[[Local Draft Storage]]
PENDING[[Pending Assessment Queue]]
SYNC[Assessment Sync Strategy]
Q_SYNC[Question Delta Sync]
end
subgraph "Backend API"
QA_CTRL[QualityAssessmentsController]
QQ_CTRL[QualityAssessmentQuestionsController]
QA_SVC[QualityAssessmentService]
STOCK_REC[IStockMovementRecorder]
end
subgraph "Storage"
DB[[AlmafricaDbContext]]
SPACES[[DigitalOcean Spaces]]
end
QCONFIG -->|CRUD| QQ_CTRL
QQ_CTRL --> DB
EXPORT -->|GET /export| QQ_CTRL
SELECT_FARMER --> START
START -->|POST /start| QA_CTRL
START --> FORM
FORM -->|auto-save| DRAFT
FORM -->|complete| QA_CTRL
FORM -.->|offline| PENDING
PENDING --> SYNC
SYNC -->|POST /complete| QA_CTRL
SYNC -->|upload photos| SPACES
SYNC -->|attach URLs| QA_CTRL
Q_SYNC -->|GET /questions| QA_CTRL
QA_CTRL --> QA_SVC
QA_SVC -->|StockIn movement| STOCK_REC
QA_SVC --> DB
STOCK_REC --> DB
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
graph TD
A((Admin opens question manager)) --> B{Create or Update?}
B -->|Create| C[POST /api/admin/quality-assessment-questions]
B -->|Update| D[PUT /api/admin/quality-assessment-questions/:id]
B -->|Delete| E[DELETE /api/admin/quality-assessment-questions/:id]
C --> F[QualityAssessmentQuestionService.CreateQuestionAsync]
D --> G[QualityAssessmentQuestionService.UpdateQuestionAsync]
E --> H[QualityAssessmentQuestionService.DeleteQuestionAsync]
F --> I{Validate constraints}
G --> I
I -->|SelectOptions type - no options| J[Return 400: Options required]
I -->|Numeric - MinValue >= MaxValue| K[Return 400: Min must be less than Max]
I -->|Missing en/fr translations| L[Return 400: en and fr translations required]
I -->|Valid| M[Save to QualityAssessmentQuestions table]
H --> N[Soft-delete: set IsDeleted = true]
N --> O[[AlmafricaDbContext.SaveChangesAsync]]
M --> P{Has Options?}
P -->|Yes| Q[Save QualityAssessmentQuestionOptions with translations]
P -->|No| R[Save question translations only]
Q --> O
R --> O
O --> S[Return QuestionDto to admin]
S --> T((Mobile clients pick up changes via delta sync - WF-QA-03))
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
graph TD
A((Agent taps 'New Assessment')) --> B{Permission check}
B -->|canCreateAssessments = false| C[Show unauthorized message]
B -->|canCreateAssessments = true| D[SelectFarmerForAssessmentScreen]
D --> E[Search / filter farmers via FarmerLocalService]
E --> F[Agent selects a farmer]
F --> G[StartAssessmentScreen]
G --> H[Select crop category + crop]
H --> I[Enter quantity + unit]
I --> J[Select collection point]
J --> K{Production cycle match?}
K -->|Yes - link existing| L[Set productionCycleId]
K -->|No match or skip| M[Continue without cycle]
L --> N{Device online?}
M --> N
N -->|Yes| O[POST /api/quality-assessments/start]
N -->|No| P[Create LOCAL_ prefixed batch ID]
O --> Q[Backend: StartAssessmentAsync]
Q --> R[Validate farmer approved + crop + collection point + unit]
R --> S[Generate batch number via GenerateBatchNumberAsync]
S --> T[Create CropBatch - status: PendingAssessment]
T --> U[Create empty QualityAssessment record]
U --> V[Return BatchCreatedResponse with batchId + batchNumber]
P --> W[Store batch locally with LOCAL_ ID]
V --> X[Navigate to AssessmentFormScreen]
W --> X
X --> Y[Load questions for crop category]
Y --> Z[AssessmentFormProvider manages form state]
Z --> AA[Agent answers questions]
AA --> AB{Answer type?}
AB -->|Numeric| AC[setNumericAnswer - validate min/max]
AB -->|Boolean| AD[setBooleanAnswer]
AB -->|SelectOptions| AE[setSelectOptionAnswer]
AB -->|Date| AF[setDateAnswer]
AC --> AG[_scheduleDraftSave - 2s debounce]
AD --> AG
AE --> AG
AF --> AG
AG --> AH[[Save draft to local SQLite]]
AA --> AI[Capture photos via camera]
AI --> AJ[Store photo XFiles in provider]
AA --> AK[Enter accepted + rejected quantities]
AK --> AL{isQuantityValid?}
AL -->|accepted + rejected != initial| AM[Show validation error]
AL -->|Valid| AN[Agent taps Complete]
AN --> AO{Device online?}
AO -->|Yes| AP[POST /api/quality-assessments/complete]
AP --> AQ[Backend: CompleteAssessmentAsync]
AQ --> AR[Validate batch not already completed]
AR --> AS[Validate quantities: accepted + rejected = initial within 0.01 tolerance]
AS --> AT[Determine OverallResult: Accepted / PartiallyAccepted / FullyRejected]
AT --> AU[Save QualityAssessmentAnswers]
AU --> AV[batch.MarkAsAssessed]
AV --> AW{AcceptedQuantity > 0?}
AW -->|Yes| AX[StockMovementRecorder.RecordMovementAsync - StockIn]
AW -->|No| AY[Skip StockIn movement]
AX --> AZ[[SaveChangesAsync]]
AY --> AZ
AZ --> BA[Return QualityAssessmentDto]
BA --> BB[Clear local draft]
AO -->|No| BC[Save to pending_assessments table]
BC --> BD[Mark for sync - WF-QA-03]
BD --> BB
BB --> BE((Assessment complete - navigate back))
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
graph TD
A((Sync triggered)) --> B[AssessmentSyncIntegration.syncPendingAssessments]
B --> C[AssessmentSyncStrategy.syncPendingAssessments]
C --> D[Load pending + retryable-failed assessments from SQLite]
D --> E{Any pending?}
E -->|No| F[Return AssessmentSyncResult - success, 0 attempted]
E -->|Yes| G[Iterate assessments with progress stream]
G --> H[_syncSingleAssessment]
H --> I{Batch ID starts with LOCAL_?}
I -->|Yes| J[POST /api/quality-assessments/start to create server batch]
J --> K{Server batch created?}
K -->|Yes| L[Replace LOCAL_ ID with server batchId]
K -->|No| M[Mark assessment as failed + increment retryCount]
I -->|No| N[Use existing server batchId]
L --> O[Build CompleteAssessmentRequest from local data]
N --> O
O --> P{Has photos?}
P -->|Yes| Q[AssessmentPhotoService.uploadPhotos]
Q --> R[POST /api/upload/image per photo to DigitalOcean Spaces]
R --> S[Collect uploaded photo URLs]
S --> T[POST /api/quality-assessments/:id/photos to attach URLs]
P -->|No| U[Skip photo upload]
T --> V[POST /api/quality-assessments/complete]
U --> V
V --> W[Backend: CompleteAssessmentAsync]
W --> X{Success?}
X -->|Yes| Y[Delete from pending_assessments table]
Y --> Z[Emit SyncStage.completed progress]
X -->|No| AA{Retry count < 3?}
AA -->|Yes| AB[Increment retryCount, mark retryable]
AB --> AC[Emit SyncStage.retrying progress]
AA -->|No| AD[Mark as permanentlyFailed]
AD --> AE[Emit SyncStage.failed progress]
Z --> AF{More pending?}
AC --> AF
AE --> AF
AF -->|Yes| G
AF -->|No| AG[Return AssessmentSyncResult]
AG --> AH((Sync complete))
subgraph "Question Delta Sync"
QS((Main data sync completes)) --> QA[AssessmentQuestionsSyncHelper.syncAssessmentQuestions]
QA --> QB{SyncConfig.isAssessmentQuestionsFresh?}
QB -->|Yes - cache fresh| QC[Skip sync]
QB -->|No - stale| QD[GET /api/quality-assessments/questions?modifiedSince=timestamp]
QD --> QE{Response has questions?}
QE -->|Yes - delta| QF[Merge updated questions into local SQLite]
QE -->|No changes| QG[Update last sync timestamp only]
QF --> QH[Update SyncMetadataService + local sync time]
QG --> QH
QH --> QI((Questions up to date))
end
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
graph TD
A((Warehouse operator opens Pending Assessments)) --> B[WarehousePendingAssessmentsScreen]
B --> C[Receive collectionPointId + collectionPointName via args]
C --> D[Load assessments for collection point]
D --> E{Device online?}
E -->|Yes| F[GET /api/quality-assessments/batches/pending?collectionPointId=X]
E -->|No| G[Load from local completed_assessments cache]
F --> H[Backend: GetPendingBatchesAsync]
H --> I[Query CropBatches WHERE CollectionPointId = X AND AssessmentStatus = PendingAssessment]
I --> J[Apply search filter + pagination]
J --> K[Return PagedResult of CropBatchDto]
G --> L[Display cached assessment list]
K --> L
L --> M[Show tabs: Completed / Pending]
M --> N{Operator selects an assessment?}
N -->|Pending batch| O[Navigate to StartAssessmentScreen - WF-QA-02]
N -->|Completed assessment| P[View assessment details]
P --> Q[Display: result, quantities, answers, photos]
Q --> R{Has edit permission?}
R -->|canEditAssessments = true| S[Show Edit button]
R -->|No| T[Read-only view]
S --> U[Navigate to edit assessment flow]
U --> V[PUT /api/quality-assessments/:assessmentId]
V --> W[Backend: EditAssessmentAsync]
W --> X[Update answers, quantities, result]
X --> Y[[SaveChangesAsync]]
Y --> Z[Return updated QualityAssessmentDto]
Z --> AA((Refresh assessment list))
subgraph "Assessed Batches View"
AB((Operator views assessed batches)) --> AC[GET /api/quality-assessments/batches/assessed]
AC --> AD[Backend: GetAssessedBatchesAsync]
AD --> AE[Query with filters: farmerId, cropId, startDate, endDate, result, search]
AE --> AF[Return PagedResult of CropBatchDto with assessment details]
end
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 |