critical priority medium complexity backend pending backend specialist Tier 7

Acceptance Criteria

assembleMetricsPayload(period, orgId) returns a BufdirSubmissionPayload object that conforms exactly to the Bufdir submission schema (field names, types, and nesting as required by Bufdir)
Payload includes: reporting period (start/end dates), organization identifier, deduplicated total participant count, activity counts by Bufdir category, and geographic breakdown by region
Audit metadata block is attached to the payload containing: total raw participant count before deduplication, deduplication count removed (raw - deduped), data sources used (list of Supabase table/view names queried), pipeline step timestamps (start and end for each step), pipeline schema version string, and triggering userId
The payload can be serialized to JSON (toJson()) producing output that passes validation against the Bufdir JSON schema
Audit metadata is stored persistently in the database (a bufdir_audit_log table or equivalent) every time a payload is assembled, not only when submitted
If the assembled payload fails Bufdir schema validation, assembleMetricsPayload returns a typed ValidationFailureResult with field-level error details rather than throwing
assembleMetricsPayload is idempotent for the same period+orgId — repeated calls produce the same payload without creating duplicate audit log entries (use upsert on the audit log)
The pipeline schema version is a semver string defined as a constant in the codebase, bumped whenever the aggregation logic changes in a way that affects output

Technical Requirements

frameworks
Flutter
Dart
Riverpod
Supabase Dart SDK
apis
Supabase PostgREST API (audit log persistence)
Bufdir submission schema (JSON Schema validation)
data models
BufdirSubmissionPayload
AuditMetadata
PipelineTimestamps
BufdirAuditLogEntry
ValidationFailureResult
performance requirements
Payload assembly itself (structuring already-computed data into the schema) must complete in under 200ms — all heavy computation happens in upstream pipeline steps
Audit log write to Supabase must be non-blocking: use upsert in a fire-and-forget Future (do not await if it would delay returning the payload to the caller)
security requirements
The assembled payload must not include raw participant names, email addresses, or personal identifiers — only aggregated counts
The audit log entry must be write-only for the service account; only legal compliance roles can read audit logs
Pipeline schema version must be included in the audit log to detect if historical payloads used different aggregation logic

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

Define BufdirSubmissionPayload as a freezed data class with toJson() generated by json_serializable. The Bufdir schema should be documented in a const or loaded from an asset file for validation purposes. For audit metadata, define a separate AuditMetadata freezed class containing all traceability fields. Persist audit log using Supabase upsert on a composite unique key (org_id + reporting_period_id + pipeline_version) to ensure idempotency.

Use Dart's DateTime.now().toUtc().toIso8601String() for all timestamps in audit metadata to ensure consistent timezone handling for legal compliance. Define PIPELINE_SCHEMA_VERSION = '1.0.0' as a top-level constant and reference it everywhere. For the ValidationFailureResult pattern, use a sealed class Result to avoid exception-based control flow at the caller level.

Testing Requirements

Unit tests: assert that assembleMetricsPayload correctly maps deduplicated counts, geographic breakdowns, and category totals into BufdirSubmissionPayload fields. Test toJson() output matches expected JSON structure (snapshot test). Test that audit metadata contains correct deduplication delta (raw - deduped). Test ValidationFailureResult is returned when a required Bufdir schema field is missing.

Test idempotency: call twice, assert audit log has exactly 1 entry (upsert behavior). Integration test: run full pipeline and assert assembled payload JSON passes a Bufdir JSON schema validator (use a Dart JSON schema validation library or manual field-level assertions). Use flutter_test.

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.