critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

getUniqueParticipantCount(ReportingPeriod period, String bufdirCategory, String orgId) method returns a UniqueParticipantCountResult object
UniqueParticipantCountResult contains: uniqueCount (int), totalRawRecords (int), duplicatesRemovedByProxy (int), duplicatesRemovedByMultiChapter (int), auditEntries (List<DuplicateAuditEntry>)
Both proxy deduplication (task-005) and multi-chapter deduplication (task-006) passes are applied in the correct sequence before returning the count
Result is deterministic: identical input parameters always produce identical output
Method correctly handles all 4 Bufdir quarterly periods and returns 0 for an org with no activity in the requested period
Audit trail is sufficiently detailed for Bufdir grant verification: each removed duplicate includes original record ID, normalized participant ID, removal reason, and contributing chapter IDs (for multi-chapter removals)
Method does not silently swallow errors — it throws typed exceptions (e.g., InvalidPeriodException, OrgNotFoundException) that callers can handle
Integration test confirms end-to-end: raw Supabase records → both dedup passes → correct unique count returned for a known test dataset
The audit trail can be persisted to a `deduplication_audit_log` Supabase table for regulatory compliance — provide a persistAuditTrail(UniqueParticipantCountResult) method
Calling getUniqueParticipantCount for all 4 Bufdir categories for one org and one year completes in under 3 seconds total

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase REST API (activity_records, participants, member_chapter_affiliations, deduplication_audit_log tables)
data models
ReportingPeriod
ActivityRecord
DeduplicationResult
UniqueParticipantCountResult
DuplicateAuditEntry
BufdirCategory
performance requirements
All 4 Bufdir categories computed for one org+year in under 3 seconds
Supabase data fetch is a single query per period+org — results cached in memory for the duration of the method call
Audit trail persistence is async and does not block the return of the count
security requirements
orgId must be validated against the authenticated user's org memberships via Supabase RLS — do not trust caller-supplied orgId without verification
Audit log entries written to Supabase must include the authenticated user ID and timestamp for traceability
UniqueParticipantCountResult must not be cacheable across org boundaries

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

This method is the public API surface for the entire deduplication pipeline — design it for clarity and stability. Internally it orchestrates: (1) fetch raw ActivityRecord list from Supabase for the given period+org+category, (2) call resolveProxyDuplicates from task-005 to get proxy-cleaned records, (3) call resolveMultiChapterDuplicates from task-006 on the proxy-cleaned records, (4) compute final count from the doubly-cleaned set, (5) merge audit entries from both passes into the result object. Define BufdirCategory as a Dart enum with values matching Bufdir's official category codes (confirm these with the Bufdir integration team). The persistAuditTrail method should use Supabase upsert with a composite unique key (period_id, org_id, bufdir_category, participant_normalized_id) to be idempotent — safe to call multiple times.

Register this method as a Riverpod FutureProvider.family keyed by (period, bufdirCategory, orgId) so the UI layer can call it reactively.

Testing Requirements

Covered by task-008. For this task, implement an end-to-end integration test using a Supabase test project with a known dataset: insert 20 raw activity records for org X in Q1, including 3 proxy duplicates and 2 multi-chapter duplicates. Call getUniqueParticipantCount and assert uniqueCount == 15, duplicatesRemovedByProxy == 3, duplicatesRemovedByMultiChapter == 2. Also test: empty org returns 0 count without error, invalid period throws InvalidPeriodException, wrong orgId returns 0 (RLS filters it out, no unauthorized data leak).

Component
Participant Deduplication 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.