critical priority high complexity backend pending backend specialist Tier 0

Acceptance Criteria

GeographicDistributionService exposes getChaptersByRegion(String regionId) returning a List<Chapter> of all chapters in that region
getRegionForChapter(String chapterId) returns the correct parent region for any of the 1,400 local chapters
getNationalAssociationForChapter(String chapterId) returns the correct top-level national association (one of 12) for any chapter
Full 3-tier traversal (national → region → chapter) is resolved in a single Supabase query per service initialization — subsequent lookups are O(1) from the in-memory memoized tree
Memoization cache is populated lazily on first call and invalidated after 24 hours (configurable via a const in the service)
Recursive traversal correctly handles the complete NHF structure: all 12 national associations, all 9 regions, all 1,400 chapters, with no missing or duplicate entries
If a chapter is orphaned (not assigned to any region), it is returned in a special 'unassigned' bucket and a warning is logged — the method does not throw
getActivityDistribution(ReportingPeriod period, String orgId) returns a Map<String, int> of regionId → unique participant count, using GeographicDistributionService for hierarchy resolution
Tree structure built from Supabase in under 1 second for the full 1,400-chapter dataset on first initialization
Service is registered as a Riverpod Provider with singleton scope so the memoized tree is shared across all callers in the same app session

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase REST API (org_hierarchy table or equivalent: organisations, org_hierarchy_nodes, chapters, regions, national_associations tables)
data models
OrgHierarchyNode
Chapter
Region
NationalAssociation
OrgTree
ActivityDistributionResult
performance requirements
Initial tree build from Supabase in under 1 second for 1,400 nodes
Post-memoization lookups are O(1) using HashMap — verified by benchmark test
getActivityDistribution for one period+org completes in under 500ms
Single Supabase query to load full org hierarchy (no per-node queries)
security requirements
Org hierarchy data is read-only from the app — no write operations in this service
Supabase RLS on org_hierarchy tables must restrict visibility to orgs the authenticated user belongs to
Memoization cache must not persist between authenticated user sessions (clear on logout)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Model the org hierarchy as a tree of OrgHierarchyNode objects: `class OrgHierarchyNode { final String id; final String name; final OrgTier tier; final String? parentId; final List children; }`. Build a `Map` indexed by node ID from a single flat Supabase query — this is the memoization cache. Recursive traversal for `getChaptersByRegion` then walks children of the given region node, collecting all leaf nodes (chapters).

Use a depth-first search with a visited set to guard against circular references in malformed data. The memoization timestamp should be stored alongside the cache map; check `DateTime.now().difference(lastBuildTime).inHours >= 24` before each public method call. For the 24-hour TTL, use Riverpod's `ref.invalidateSelf()` triggered by a Timer set in the Provider's build method — this ensures proper lifecycle management. When building the tree, handle the case where NHF's 9 regions may map to different subsets of the 12 national associations — do not assume a flat 3-tier mapping.

Confirm the exact Supabase schema for org hierarchy with the backend team before implementing the query.

Testing Requirements

Unit tests using flutter_test with a mock Supabase client returning a fixture org tree. Test cases: (1) getChaptersByRegion for each of the 9 regions returns correct chapter subset, (2) getRegionForChapter for a known chapter returns correct region, (3) getNationalAssociationForChapter returns correct top-level org, (4) orphaned chapter returns in 'unassigned' bucket without throwing, (5) memoization: after first call, second call returns identical result without triggering a new Supabase query (verify using call count on the mock), (6) cache invalidation: after 24 hours (advance mock clock), next call re-fetches from Supabase, (7) performance benchmark: fixture with 1,400 nodes builds tree in under 1 second (use Stopwatch). Integration test: load real NHF hierarchy structure from Supabase test instance and verify node count matches expected 1+12+9+1400 nodes.

Component
Geographic Distribution Service
service high
Epic Risks (4)
high impact high prob integration

NHF members can belong to up to 5 local chapters. When a participant has activities registered under different chapter IDs within the same reporting period, deduplication requires a reliable cross-chapter identity key. If national IDs are absent for some members (a known data quality issue in NHF's systems), the deduplication service may fail to identify duplicates, resulting in inflated counts submitted to Bufdir.

Mitigation & Contingency

Mitigation: Implement a multi-attribute identity matching strategy: primary match on national_id, fallback to (full_name + birth_year + municipality) composite key. Expose a low-confidence match list in DeduplicationAnomalyReport that coordinators can review and manually resolve before submission.

Contingency: If identity data quality is too poor for reliable automated deduplication for specific organisations, add an organisation-level config flag that disables cross-chapter deduplication for that org and requires coordinators to manually review the anomaly report before submitting.

high impact medium prob integration

The geographic distribution algorithm must resolve NHF's 1,400 local chapter hierarchy to regional aggregates. If the organizational unit hierarchy in the database is incomplete (missing parent-child relationships for some chapters), the geographic service will silently drop activities from unmapped chapters, producing an understated geographic breakdown.

Mitigation & Contingency

Mitigation: Add a hierarchy completeness validation step in GeographicDistributionService that counts activities without a resolvable region assignment and surfaces them as an 'unmapped_activities' field in the distribution result. Block export if unmapped_activities > 0.

Contingency: Provide a 'national' fallback bucket for activities from chapters with no region assignment, clearly labelled in the preview screen so coordinators are alerted to fix the org hierarchy data before re-running aggregation.

high impact low prob technical

BufdirAggregationService orchestrates four dependent services. If one service (e.g., GeographicDistributionService) throws mid-pipeline, the partially assembled metrics payload may be silently cached or returned as if complete, resulting in a Bufdir submission missing the geographic breakdown section.

Mitigation & Contingency

Mitigation: Implement the orchestrator as a transactional pipeline using Dart's Result type pattern: each stage returns Either<AggregationError, PartialResult>, and the orchestrator only proceeds if all stages succeed. The final payload is only assembled and persisted when all stages return success.

Contingency: If a partial failure state reaches the UI, the AggregationProgressIndicator must display a specific stage failure message with a retry option that re-runs only the failed stage rather than the full pipeline.

medium impact medium prob scope

Internal activity types that have no corresponding Bufdir category in the mapping configuration will cause the aggregation to silently exclude those activities from the final counts. Coordinators may not notice the omission until Bufdir queries why submission totals are lower than expected.

Mitigation & Contingency

Mitigation: BufdirAggregationService must produce an unmapped_activity_types list as part of its output. If any internal activity types are unmapped, display a blocking warning in the AggregationSummaryWidget listing the unmapped types before allowing the coordinator to proceed to export.

Contingency: Allow coordinators to temporarily assign unmapped activity types to a Bufdir 'other' catch-all category as an emergency workaround, with an audit flag indicating manual override was applied for that submission.