critical priority high complexity backend pending backend specialist Tier 5

Acceptance Criteria

BufdirAggregationService exposes a primary method runAggregationPipeline(orgId, year) that returns a complete BufdirAggregationResult
Pipeline step 1: calls ReportingPeriodService.resolvePeriod(orgId, year) to determine the canonical reporting period; aborts with a typed error if period cannot be resolved
Pipeline step 2: calls ParticipantDeduplicationService.getDedupedCounts(period, orgId) and includes the result in the final output
Pipeline step 3: calls GeographicDistributionService.getRegionBreakdown(period, orgId) and includes the full regional breakdown in output
Pipeline step 4: applies ActivityCategoryMappingConfiguration to translate internal activity type IDs to Bufdir category codes (delegated to task-014, but the invocation point is wired here)
If any pipeline step fails, the error is caught, typed, and surfaced in the result with a clear stepName and errorMessage — partial results are not returned silently
BufdirAggregationResult is an immutable Dart data class containing: period, dedupedCounts, regionBreakdown, categoryMappedTotals, auditMetadata
The service is Riverpod-injectable and uses constructor-injected dependencies (no static/singleton calls to sub-services)
runAggregationPipeline is idempotent: calling it twice for the same orgId+year produces the same result (no side effects on the pipeline itself)
The service logs pipeline step start/completion timestamps for audit metadata assembly (used in task-015)

Technical Requirements

frameworks
Flutter
Dart
Riverpod
Supabase Dart SDK
apis
ReportingPeriodService (internal)
ParticipantDeduplicationService (internal)
GeographicDistributionService (internal)
ActivityCategoryMappingConfiguration (internal)
data models
BufdirAggregationResult
ReportingPeriod
DedupedParticipantCounts
RegionBreakdown
CategoryMappedTotals
AuditMetadata
PipelineStepError
performance requirements
Total pipeline execution time target: under 5 seconds for a full NHF-scale run (combining all sub-service calls)
Sub-service calls that are independent (deduplication and geographic distribution share the same period but are logically independent) should be executed concurrently using Dart Future.wait([])
Pipeline must not hold Supabase connections open longer than the duration of a single step call
security requirements
orgId must be validated against the authenticated user's organization before any pipeline step executes
BufdirAggregationResult must not include raw participant identifiers — only aggregated counts
Audit metadata must record which user (userId) triggered the aggregation run for legal compliance traceability

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Model BufdirAggregationService as a Riverpod Provider (StateNotifierProvider or simple Provider depending on whether the caller needs reactive state). The primary method runAggregationPipeline should be async and structured as a try/catch pipeline with step tracking: record stepStartTime at each step, wrap each sub-service call in its own try/catch, and accumulate results. For concurrent execution: use Future.wait([deduplicationFuture, geographicFuture]) since both depend only on the resolved period, not on each other. Use Dart's sealed classes or a Result pattern for typed error propagation rather than throwing exceptions across async boundaries.

Define BufdirAggregationResult as a freezed data class for immutability and equality. The category mapping application (step 4) should call an injected ActivityCategoryMappingService — keep the wiring point here even though the implementation lands in task-014.

Testing Requirements

Unit tests: mock all four sub-services and assert pipeline calls them in the correct order with correct parameters. Test that Future.wait is used for concurrent steps (verify both deduplication and geographic distribution are initiated before either completes). Test error propagation: mock ReportingPeriodService to throw, assert BufdirAggregationResult contains a typed PipelineStepError with stepName='resolve-period'. Test idempotency: call pipeline twice with same args, assert both return equal results.

Use flutter_test with mockito or manual mock classes. Integration tests: run full pipeline against test Supabase instance seeded from task-012.

Component
Bufdir Aggregation 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.