Appendix B: Entity Relationships
This appendix documents the key entity relationships across the Almafrica system. The backend uses PostgreSQL (via Entity Framework Core) as the source of truth, while the Flutter mobile app maintains a SQLite replica for offline-first operation.
Table of Contents
- User and Access
- Farmer Domain
- Client Domain
- Production
- Orders and Allocation
- Warehouse and Stock
- Quality
- Campaigns and Surveys
- Master Data Entities
- PostgreSQL vs SQLite Schema Differences
1. User and Access
These entities manage authentication, authorization, session tracking, and audit.
erDiagram
ApplicationUser ||--o{ UserSession : "has sessions"
ApplicationUser ||--o{ AspNetUserRoles : "has roles"
AspNetUserRoles }o--|| AspNetRole : "assigned role"
AspNetRole ||--o{ RolePermission : "grants"
ApplicationUser ||--o{ PasswordHistory : "password history"
ApplicationUser ||--o{ TwoFactorBackupCode : "2FA codes"
ApplicationUser ||--o{ LoginHistory : "login audit"
ApplicationUser ||--o{ Notification : "receives"
UserGroup ||--o{ UserGroupMembership : "contains"
ApplicationUser ||--o{ UserGroupMembership : "belongs to"
AspNetRole ||--o{ RolePasswordPolicy : "enforces"
RolePasswordPolicy }o--|| PasswordPolicy : "uses"
AspNetRole ||--o{ RoleAuditLog : "audited by"
Entity Details
| Entity | PK | Key Fields |
|---|
ApplicationUser | string Id | Email, PhoneNumber, FullName (ASP.NET Identity) |
UserSession | Guid Id | UserId, SessionToken, DeviceType, IpAddress, IsActive |
AspNetRole | string Id | Name, NormalizedName (ASP.NET Identity) |
UserGroup | Guid Id | Name, Description |
UserGroupMembership | Guid Id | UserGroupId, UserId |
PasswordPolicy | Guid Id | MinLength, RequireUppercase, MaxAge, etc. |
RolePasswordPolicy | composite | RoleId, PasswordPolicyId |
RoleAuditLog | Guid Id | RoleId, Action, PerformedBy, Timestamp |
PasswordHistory | Guid Id | UserId, PasswordHash, CreatedAt |
LoginHistory | Guid Id | UserId, IpAddress, UserAgent, LoginTime, Success |
TwoFactorBackupCode | Guid Id | UserId, Code, IsUsed |
Notification | Guid Id | UserId, Title, Message, IsRead, Type |
2. Farmer Domain
These entities represent field agents, farmers, their crops, crop offers, and field visits.
erDiagram
Agent ||--o{ Farmer : "manages"
Agent ||--o{ Farmer : "created"
Agent ||--o{ FarmerVisit : "performs"
Agent }o--o| Province : "operates in"
Agent }o--o| Territory : "operates in"
Agent }o--o| Village : "operates in"
Farmer ||--o{ FarmerCrop : "grows"
Farmer ||--o{ FarmerCropOffer : "offers"
Farmer ||--o{ FarmerVisit : "visited during"
Farmer ||--o{ Production : "has cycles"
Farmer ||--o{ Harvest : "harvests"
Farmer ||--o{ CropBatch : "delivers"
Farmer ||--o{ Distribution : "receives"
Farmer }o--o| Province : "lives in"
Farmer }o--o| Territory : "in territory"
Farmer }o--o| Village : "in village"
Farmer }o--o| CollectionPoint : "delivers to"
FarmerCrop }o--|| Crop : "crop type"
FarmerCrop ||--o{ Production : "linked to"
FarmerCrop ||--o{ FarmerCropFertilizer : "uses"
FarmerCropOffer }o--|| Crop : "offers crop"
FarmerCropOffer }o--o| CollectionPoint : "delivers at"
FarmerCropOffer ||--o{ FarmerCropOfferPriority : "has priorities"
FarmerCropOffer ||--o{ FarmerCropOfferIssue : "has issues"
FarmerVisit }o--|| Farmer : "visits"
FarmerVisit }o--|| Agent : "by agent"
FarmerVisit ||--o{ Production : "captures"
Entity Details
| Entity | PK | Key Fields |
|---|
Agent | Guid Id | AuthUserId, FullName, Email, ProvinceId, TerritoryId, VillageId, IsActive |
Farmer | Guid Id | AuthUserId, FirstName, LastName, PhoneNumber, FarmSize, ManagedById, CreatedByAgentId, ApprovalStatus, Status |
FarmerCrop | Guid Id | FarmerId, CropId, VarietyOrLocalName, IsActive |
FarmerCropOffer | Guid Id | FarmerId, CropId, ProductionSource, SupplyFrequency, AvgVolumeValue, ExpectedPriceMin/Max, IsActive |
FarmerCropOfferPriority | composite | FarmerCropOfferId, PurchasePriorityConfigId |
FarmerCropOfferIssue | composite | FarmerCropOfferId, CommonIssueConfigId |
FarmerVisit | Guid Id | FarmerId, AgentId, StartedAt, EndedAt, Purpose, Status, Latitude, Longitude |
3. Client Domain
These entities represent clients (buyers), their crop demands, and business documents.
erDiagram
Client ||--o{ Order : "places"
Client ||--o{ ClientCropDemand : "demands"
Client ||--o{ ClientDocument : "uploads"
Client }o--o| Province : "located in"
Client }o--o| Territory : "in territory"
Client }o--o| Village : "in village"
Client }o--o| BusinessType : "business type"
Client }o--o| CollectionPoint : "uses"
Client }o--o| PaymentMethod : "pays via"
Client }o--o| Currency : "prefers"
Client }o--o| Agent : "assigned to"
ClientCropDemand }o--|| Crop : "demands crop"
ClientCropDemand }o--|| MeasurementUnit : "volume unit"
ClientCropDemand ||--o{ ClientCropDemandImportCountry : "imports from"
ClientCropDemand ||--o{ ClientCropDemandPurchasePriority : "priorities"
ClientCropDemand ||--o{ ClientCropDemandCommonIssue : "faces issues"
ClientDocument }o--|| Client : "belongs to"
Entity Details
| Entity | PK | Key Fields |
|---|
Client | Guid Id | AuthUserId, CompanyName, ContactPersonName, PhoneNumber, BusinessTypeId, ApprovalStatus, Status |
ClientCropDemand | Guid Id | ClientId, CropId, SourcingOrigin, PurchaseFrequency, AvgVolumeValue, UsualBuyPriceMin/Max, IsActive |
ClientCropDemandImportCountry | composite | ClientCropDemandId, CountryConfigId |
ClientCropDemandPurchasePriority | composite | ClientCropDemandId, PurchasePriorityConfigId |
ClientCropDemandCommonIssue | composite | ClientCropDemandId, CommonIssueConfigId |
ClientDocument | Guid Id | ClientId, DocumentType, DocumentUrl, OriginalFileName, FileSize, MimeType |
4. Production
These entities track the agricultural production lifecycle from planting through harvest.
erDiagram
Production }o--|| Farmer : "owned by"
Production }o--|| Crop : "produces"
Production }o--o| CropCategory : "categorized as"
Production }o--o| FarmerCrop : "crop profile"
Production }o--o| FarmerVisit : "captured during"
Production }o--o| MeasurementUnit : "yield unit"
Production ||--o{ Harvest : "yields"
Production ||--o{ ProductionInput : "uses inputs"
Production ||--o{ CropBatch : "delivers to"
Production ||--o{ ProductionAuditEntry : "audit trail"
Harvest }o--|| Production : "from cycle"
Harvest }o--|| Farmer : "by farmer"
Harvest }o--|| Crop : "crop type"
Harvest }o--|| MeasurementUnit : "unit"
ProductionInput }o--|| Production : "for cycle"
Crop }o--|| CropCategory : "belongs to"
Crop ||--o{ CropTranslation : "translations"
CropCategory ||--o{ CropCategoryTranslation : "translations"
Entity Details
| Entity | PK | Key Fields |
|---|
Production | Guid Id | FarmerId, CropId, CropCategoryId, FarmerCropId, FarmerVisitId, AreaPlanted, PlantingDate, ExpectedHarvestStartDate, EstimatedYield, Status, IsDraft |
Harvest | Guid Id | ProductionId, FarmerId, CropId, HarvestDate, Quantity, UnitId, QualityGrade, AvailableQuantity |
ProductionInput | Guid Id | ProductionId, InputType, Name, Quantity, ApplicationDate |
ProductionAuditEntry | Guid Id | ProductionId, Action, FieldName, OldValue, NewValue, PerformedBy |
Crop | Guid Id | Name (translatable), CropCategoryId, IsActive |
CropCategory | Guid Id | Name (translatable), Code |
5. Orders and Allocation
These entities manage the order lifecycle from placement through delivery and payment.
erDiagram
Order }o--|| Client : "placed by"
Order }o--o| CollectionPoint : "sourced from"
Order }o--o| DeliveryMethod : "delivered via"
Order }o--o| LogisticsPartner : "shipped by"
Order }o--o| Currency : "currency"
Order ||--o{ OrderItem : "contains"
Order ||--o{ Payment : "paid by"
Order ||--o| Distribution : "distributed as"
OrderItem }o--|| Crop : "for crop"
OrderItem }o--o| Harvest : "from harvest"
OrderItem }o--|| MeasurementUnit : "unit"
OrderItem ||--o{ OrderAllocation : "allocated from"
OrderItem ||--o{ StockMovement : "creates movements"
OrderAllocation }o--|| OrderItem : "fulfills"
OrderAllocation }o--|| CropBatch : "sources from"
OrderAllocation ||--o{ StockMovement : "records movement"
Payment }o--|| Order : "pays for"
Distribution }o--|| Order : "delivers"
Entity Details
| Entity | PK | Key Fields |
|---|
Order | Guid Id | OrderNumber, ClientId, OrderDate, Status, TotalAmount, DeliveryMethodId, CollectionPointId, CurrencyId |
OrderItem | Guid Id | OrderId, CropId, HarvestId, Quantity, MeasurementUnitId, UnitPrice, TotalPrice |
OrderAllocation | Guid Id | OrderItemId, CropBatchId, AllocatedQuantity, AllocationDate, AllocatedByUserId |
Payment | Guid Id | OrderId, Amount, PaymentDate, PaymentMethod, TransactionReference, Status |
Distribution | Guid Id | OrderId, ScheduledDate, ActualDeliveryDate, DeliveryAddress, DriverName, VehicleNumber |
6. Warehouse and Stock
These entities track stock inventory, movements, transfers, losses, and investigations.
erDiagram
CropBatch }o--|| Farmer : "delivered by"
CropBatch }o--|| Crop : "crop type"
CropBatch }o--|| CollectionPoint : "stored at"
CropBatch }o--|| MeasurementUnit : "quantity unit"
CropBatch }o--o| Production : "from cycle"
CropBatch ||--o| QualityAssessment : "assessed by"
CropBatch ||--o{ OrderAllocation : "allocated to"
CropBatch ||--o{ StockMovement : "movements"
CropBatch ||--o{ TransferItem : "transferred in"
StockMovement }o--|| CropBatch : "for batch"
StockMovement }o--|| MeasurementUnit : "unit"
StockMovement }o--o| CollectionPoint : "source CP"
StockMovement }o--o| CollectionPoint : "destination CP"
StockMovement }o--o| OrderItem : "for order item"
StockMovement }o--o| OrderAllocation : "for allocation"
StockMovement }o--o| StockMovement : "reverses"
Transfer ||--o{ TransferItem : "contains"
Transfer }o--|| CollectionPoint : "from source"
Transfer }o--|| CollectionPoint : "to destination"
Transfer ||--o| Investigation : "triggers"
TransferItem }o--|| Transfer : "belongs to"
TransferItem }o--|| CropBatch : "batch"
TransferItem }o--|| MeasurementUnit : "unit"
StockLossRecord }o--|| CollectionPoint : "occurred at"
StockLossRecord ||--o{ StockLossItem : "items"
StockLossItem }o--|| CropBatch : "batch affected"
Investigation }o--|| Transfer : "investigates"
StockAdjustmentItem }o--|| CropBatch : "adjusts"
Entity Details
| Entity | PK | Key Fields |
|---|
CropBatch | Guid Id | BatchNumber, FarmerId, CropId, CollectionPointId, DeliveryDate, InitialQuantity, AcceptedQuantity, CurrentStockQuantity, Status, AssessmentStatus, BestBeforeDate |
StockMovement | Guid Id | MovementReference, CropBatchId, MovementType, Quantity, StockBefore, StockAfter, SourceCollectionPointId, DestinationCollectionPointId, MovementDate |
Transfer | Guid Id | TransferReference, SourceCollectionPointId, DestinationCollectionPointId, Status, InitiatedByUserId, ApprovedByUserId, HasDiscrepancy |
TransferItem | Guid Id | TransferId, CropBatchId, Quantity, ReceivedQuantity, QuantityUnitId |
StockLossRecord | Guid Id | LossReference, CollectionPointId, Category, LossReason, LossDate, TotalQuantityKg, Status |
StockLossItem | Guid Id | StockLossRecordId, CropBatchId, Quantity |
StockAdjustmentItem | Guid Id | CropBatchId, AdjustmentQuantity, Reason |
Investigation | Guid Id | InvestigationReference, TransferId, Status, ExpectedQuantity, ReceivedQuantity, DiscrepancyQuantity |
7. Quality
These entities manage quality assessment checklists and their answers.
erDiagram
QualityAssessment ||--|| CropBatch : "assesses"
QualityAssessment }o--o| ChecklistVersion : "uses checklist"
QualityAssessment ||--o{ QualityAssessmentAnswer : "has answers"
QualityAssessmentAnswer }o--|| QualityAssessment : "for assessment"
QualityAssessmentAnswer }o--|| QualityAssessmentQuestion : "answers question"
QualityAssessmentAnswer }o--o| QualityAssessmentQuestionOption : "selected option"
QualityAssessmentQuestion ||--o{ QualityAssessmentQuestionOption : "has options"
QualityAssessmentQuestion ||--o{ QualityAssessmentQuestionTranslation : "translations"
QualityAssessmentQuestionOption ||--o{ QualityAssessmentQuestionOptionTranslation : "translations"
ChecklistVersion ||--o{ ChecklistVersionTranslation : "translations"
Entity Details
| Entity | PK | Key Fields |
|---|
QualityAssessment | Guid Id | CropBatchId (unique), AssessmentDate, AssessedByUserId, OverallResult, AcceptedQuantity, RejectedQuantity, ChecklistVersionId, plus crop-category-specific QC fields |
QualityAssessmentAnswer | Guid Id | QualityAssessmentId, QuestionId, QuestionType, NumericValue, BooleanValue, SelectedOptionId, DateValue, TextValue |
QualityAssessmentQuestion | Guid Id | Category, QuestionType, DisplayOrder, IsRequired, IsActive (config entity with translations) |
QualityAssessmentQuestionOption | Guid Id | QuestionId, Value, DisplayOrder (config entity with translations) |
ChecklistVersion | Guid Id | VersionNumber, EffectiveDate, IsActive (config entity with translations) |
8. Campaigns and Surveys
These entities manage survey campaigns, questionnaire versions, responses, and corrections.
erDiagram
Campaign ||--o{ QuestionnaireVersion : "has versions"
Campaign ||--o{ CampaignRun : "has runs"
Campaign ||--o{ SurveyResponse : "collects"
CampaignRun }o--|| Campaign : "belongs to"
CampaignRun }o--|| QuestionnaireVersion : "uses version"
CampaignRun ||--o{ SurveyResponse : "collects"
QuestionnaireVersion }o--|| Campaign : "for campaign"
SurveyResponse }o--|| Campaign : "for campaign"
SurveyResponse }o--|| QuestionnaireVersion : "uses version"
SurveyResponse }o--o| CampaignRun : "in run"
SurveyResponse ||--o{ ResponseCorrection : "corrected by"
ResponseCorrection }o--|| SurveyResponse : "corrects"
Entity Details
| Entity | PK | Key Fields |
|---|
Campaign | Guid Id | Name, Description, Objective, Status, CreatedBy, PublishedAt, ClosedAt, CurrentRunNumber |
QuestionnaireVersion | Guid Id | CampaignId, VersionNumber, Schema (JSONB), Status, PublishedAt |
CampaignRun | Guid Id | CampaignId, RunNumber, QuestionnaireVersionId, StartedAt, ClosedAt, StartedBy, ClosedBy |
SurveyResponse | Guid Id | CampaignId, QuestionnaireVersionId, CampaignRunId, SubmittedBy, ResponseData (JSONB), GpsLocation (JSONB), QualityStatus, SubmittedAt, SyncedAt, IsDraft |
ResponseCorrection | Guid Id | ResponseId, FieldKey, OldValue (JSONB), NewValue (JSONB), Reason, CorrectedBy, CorrectedAt |
9. Master Data Entities
Master data entities are configuration/reference tables that are synced to mobile devices for offline use. Each entity typically has a corresponding *Translation entity for multi-language support (French/English).
Geographic Hierarchy
| Entity | Parent | Purpose |
|---|
Province | -- | Top-level administrative division |
Territory | Province | Sub-division of province |
Village | Territory | Smallest geographic unit |
CollectionPoint | -- | Physical crop collection/warehouse location |
Crop Reference
| Entity | Purpose |
|---|
CropCategory | Crop classification (Cereals, Tubers, Fruits, Coffee/Cacao) |
Crop | Individual crop type |
Farmer Profile Configuration (~26 types synced to mobile)
These are the master data tables synced via MasterDataSyncService:
| # | Entity | SQLite Table | Purpose |
|---|
| 1 | SeedSourceConfig | seed_sources | Where farmers source seeds |
| 2 | FertilizerTypeConfig | fertilizer_types | Fertilizer types used |
| 3 | PesticideTypeConfig | pesticide_types | Pesticide types used |
| 4 | IrrigationSourceConfig | irrigation_sources | Irrigation methods |
| 5 | StorageFacilityTypeConfig | storage_facility_types | Storage facility types |
| 6 | TransportMeansConfig | transport_means | Transport methods to market |
| 7 | ElectricityReliabilityConfig | electricity_reliabilities | Grid reliability levels |
| 8 | FarmToolConfig | farm_tools | Farm equipment types |
| 9 | SoilTypeConfig | soil_types | Soil classifications |
| 10 | WaterAccessTypeConfig | water_access_types | Water source types |
| 11 | MaritalStatusConfig | marital_statuses | Marital status options |
| 12 | EducationLevelConfig | education_levels | Education level options |
| 13 | LandOwnershipTypeConfig | land_ownership_types | Land tenure types |
| 14 | TerrainTypeConfig | terrain_types | Terrain classifications |
| 15 | IncomeSourceTypeConfig | income_sources | Income source types |
| 16 | MonthlyIncomeRangeConfig | monthly_income_ranges | Income bracket ranges |
| 17 | ElectricitySourceTypeConfig | electricity_sources | Electricity source types |
| 18 | FinancialServiceTypeConfig | financial_service_types | Financial service access |
| 19 | CreditProviderTypeConfig | client_credit_provider_types | Credit provider types |
| 20 | MeasurementUnit | measurement_units / client_measurement_units | Units of measure (kg, ton, etc.) |
| 21 | Currency | client_currencies | Supported currencies |
| 22 | BusinessType | client_business_types | Client business classifications |
| 23 | PaymentMethod | client_payment_methods | Payment method options |
| 24 | CollectionPoint | client_collection_points | Collection point reference |
| 25 | DeliveryMethod | client_delivery_methods | Delivery method options |
| 26 | LogisticsPartner | client_logistics_partners | Logistics partner reference |
| 27 | OrderStatusConfig | client_order_statuses | Order status labels |
| 28 | PurchasePriorityConfig | client_purchase_priorities | Purchase priority options |
| 29 | CommonIssueConfig | client_common_issues | Common trade issues |
| 30 | CountryConfig | client_countries | Country reference for imports |
| 31 | StockOutTypeConfig | -- | Stock out type categories |
| 32 | LossCategoryConfig | -- | Loss category classifications |
| 33 | Gender (enum synced as table) | genders | Gender options |
| 34 | ContactMethod (enum synced as table) | contact_methods | Preferred contact methods |
10. PostgreSQL vs SQLite Schema Differences
The mobile SQLite schema is a denormalized, sync-optimized subset of the PostgreSQL backend schema. Key differences:
Structural Differences
| Aspect | PostgreSQL (Backend) | SQLite (Mobile) |
|---|
| Primary Keys | Guid (UUID) | TEXT (UUID stored as string) |
| Foreign Keys | Enforced with ON DELETE/CASCADE | Defined but not always enforced |
| Translations | Separate *Translation tables with FK | Inlined name_en, name_fr columns |
| JSONB columns | Native JSONB type | TEXT storing JSON string |
| Enums | C# enum mapped to int/string | TEXT or INTEGER |
| Value Objects | EF Core owned types (e.g., Quantity, BatchNumber) | Flattened to scalar columns |
| Soft Delete | IsDeleted, DeletedAt columns | is_deleted column or missing records |
| Audit columns | CreatedAt, UpdatedAt on base Entity<T> | created_at, updated_at (nullable) |
Tables Only in SQLite (Sync Infrastructure)
| SQLite Table | Purpose |
|---|
sync_queue | Offline command queue (entity type, payload, retry count) |
sync_metadata | Last sync timestamp per entity type |
sync_conflicts | Detected sync conflicts for resolution |
sync_analytics | Sync performance metrics |
sync_history | Historical sync operation log |
chunked_uploads | Chunked upload state (resume support) |
chunked_upload_parts | Individual part upload tracking |
api_response_cache | Cached API responses for offline |
stock_batch_cache | Cached stock batch data |
stock_aggregation_cache | Cached stock aggregations |
stock_cache_metadata | Stock cache freshness metadata |
center_stock_cache | Per-center stock summary cache |
campaigns_cache | Cached campaign definitions |
survey_master_data_cache | Cached survey reference data |
survey_file_queue | Queued survey file uploads |
master_data_sync | Master data sync metadata |
client_master_data_sync | Client master data sync metadata |
Tables Only in PostgreSQL (Server-Side)
| PostgreSQL Entity | Purpose |
|---|
AspNetUsers / AspNetRoles / AspNetUserRoles | ASP.NET Identity |
UserSession | Session management |
LoginHistory | Login audit trail |
PasswordHistory | Password reuse prevention |
PasswordPolicy / RolePasswordPolicy | Password policy enforcement |
IpWhitelist | IP-based access control |
RoleAuditLog | Role change audit |
PermissionTemplate | Permission template definitions |
StockMovement | Full stock movement ledger |
OrderAllocation | Batch-to-order traceability |
Investigation | Transfer discrepancy investigations |
StockAdjustmentItem | Manual stock adjustments |
ResponseCorrection | Survey response corrections |
Notification | Push notification records |
All *Translation tables | Multi-language support (30+ tables) |
Mobile-Specific Columns
Several mobile SQLite tables include columns that do not exist in PostgreSQL, used for offline workflow:
| Column Pattern | Purpose |
|---|
sync_status | Tracks if record is pending, synced, failed, conflict |
local_id | Temporary local ID before server assigns UUID |
is_draft | Whether record is a local draft not yet submitted |
retry_count | Number of sync retry attempts |
last_error | Last sync error message |
offline_created | Whether record was created offline |
needs_sync | Flag for records requiring push to server |
Migration Strategy
- PostgreSQL: EF Core migrations, versioned in
backend/Almafrica.Infrastructure/Migrations/
- SQLite: Manual migration chain in
DatabaseHelper._performMigrations(), versioned from v1 through v33+ with sequential _migrateVxToVy() methods