high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

getPeriodById(String id) returns the matching ReportingPeriod or null if not found; never throws for a missing ID
getPeriodByDate(DateTime date) returns the ReportingPeriod whose interval contains the given date (UTC); returns null if no period matches; if multiple custom periods overlap on that date, returns the oldest submitted one (FIFO for historical consistency)
listHistoricalPeriods({PeriodType? filterType, DateTime? before, int limit = 50, int offset = 0}) returns periods ordered by startDate descending, optionally filtered by type and/or end date before a cutoff
A period is marked as submitted (is_submitted = true) after a Bufdir submission is confirmed; submitted periods cannot be updated or deleted — any attempt returns ReportingPeriodException(code: immutable_period)
Re-aggregation for a submitted period creates a new correction period (PeriodType.custom, label='correction', linked via correction_of_id foreign key) rather than mutating the original
correction_of_id is validated on creation: the referenced period must exist and must be submitted; linking to a non-submitted period returns ReportingPeriodException(code: invalid_correction_target)
Supabase RLS policy prevents UPDATE and DELETE on rows where is_submitted = true
getPeriodById() and getPeriodByDate() are read-only and do not require org_admin role — any authenticated user can call them
All historical lookup methods accept nullable filter parameters and apply them only when non-null (no filter = return all)

Technical Requirements

frameworks
supabase_flutter
riverpod
apis
Supabase PostgREST API (reporting_periods table)
Supabase Auth
data models
ReportingPeriod
PeriodType
ReportingPeriodException
CorrectionPeriod (ReportingPeriod extension with correction_of_id)
performance requirements
getPeriodById() must execute a single indexed lookup by primary key — no full table scan
getPeriodByDate() must use a database-side date range query (startDate <= date AND endDate > date)
listHistoricalPeriods() with pagination must not load more than limit rows into memory
security requirements
RLS policy: UPDATE and DELETE blocked on is_submitted=true rows at the database level (defense in depth beyond service-layer checks)
correction_of_id integrity enforced by a Supabase foreign key constraint, not only by service-layer validation
Read-only lookup methods (getPeriodById, getPeriodByDate, listHistoricalPeriods) accessible to all authenticated roles; write paths restricted to org_admin

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Add is_submitted (boolean, default false) and correction_of_id (uuid, nullable, FK to reporting_periods.id) columns to the reporting_periods Supabase table via a migration file in supabase/migrations/. Add a database trigger or check constraint: IF is_submitted = true THEN raise exception 'immutable_period'. The Dart service layer should check is_submitted before issuing any mutating call and return a typed exception rather than letting the database error bubble up raw — this keeps error messages user-friendly. For the correction flow, expose a dedicated createCorrectionPeriod(String originalPeriodId, DateTime start, DateTime end) method that sets correction_of_id automatically and enforces the is_submitted pre-condition, rather than reusing createPeriod() with an optional parameter — this makes the intent explicit and prevents misuse.

Align correction period naming with Bufdir's official terminology for correction submissions (korrigering) if Norwegian labels are used in the UI.

Testing Requirements

Unit tests with mocked Supabase client for: getPeriodById() found and not-found cases, getPeriodByDate() with exact start boundary (inclusive), with endDate boundary (exclusive), with no matching period, listHistoricalPeriods() with all filter combinations (null, filterType only, before only, both), createPeriod() for a correction linked to a valid submitted period, createPeriod() for a correction linked to a non-submitted period (expect exception). Integration tests against Supabase local emulator for: RLS blocking update on submitted period, foreign key constraint on correction_of_id. Also add a regression test asserting that calling updatePeriod() (if such a method exists) on a submitted period returns the immutable_period error code, not a database-level constraint violation leaking to the UI.

Component
Reporting Period Service
service medium
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.