high priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

PeriodRecordCountService exposes: Future<int> getRecordCount(DateTimeRange range, String orgId) and bool isFuturePeriod(DateTimeRange range)
isFuturePeriod returns true if and only if range.end is strictly after DateTime.now().toUtc()
getRecordCount delegates to BufdirAggregationRepository.countActivitiesInRange(start, end, orgId) and returns the integer count
periodRecordCountServiceProvider is a Riverpod AsyncNotifierProvider.family<PeriodRecordCountNotifier, int, PeriodRecordCountParams> where PeriodRecordCountParams encapsulates DateTimeRange + orgId
When BufdirAggregationRepository throws a network exception, the AsyncNotifier transitions to AsyncError state — no silent failures
When the DateTimeRange represents a future period, getRecordCount still executes the query and returns 0 (or actual count if pre-seeded data exists) — it does not short-circuit
Unit tests cover: past period with records, future period (isFuturePeriod == true), repository network error transitions to AsyncError, zero-record period returns 0
Provider is exported from the providers barrel and overridable in tests via ProviderContainer

Technical Requirements

frameworks
Dart
Riverpod (AsyncNotifierProvider.family)
flutter_test
apis
Supabase PostgreSQL 15 (via BufdirAggregationRepository abstraction)
data models
activity
annual_summary
bufdir_export_audit_log
performance requirements
getRecordCount must not block the UI thread — always awaited asynchronously via AsyncNotifierProvider
Repository query must complete within 2 seconds on production Supabase under normal load
security requirements
orgId validated as non-empty string — throw ArgumentError on invalid input
Supabase query must not expose row counts across organisation boundaries — RLS enforced server-side; service must not attempt to aggregate across orgIds
Activity records contain PII (peer_mentor_id, contact_id) — service returns only a count integer, never raw row data

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use AsyncNotifierProvider.family so the provider is keyed by (DateTimeRange, orgId) and Riverpod automatically disposes stale queries when the key changes (user selects a different period). Implement PeriodRecordCountParams as a data class with == and hashCode based on both fields — required for the family key to work correctly. Inject a nowFn: DateTime Function() parameter into the notifier or service for testable isFuturePeriod logic. Do not cache the count in the service layer — Riverpod's provider caching handles invalidation when the family key changes.

Expose isFuturePeriod as a pure synchronous method on the service class (not in the Notifier state) since it only depends on the range and current time, not async data. This keeps the UI free from having to await a boolean.

Testing Requirements

Unit tests with mocked BufdirAggregationRepository using Riverpod ProviderContainer overrides. Test cases: (1) repository returns 42 for a past period → provider emits AsyncData(42), (2) range.end is tomorrow → isFuturePeriod == true and count query still executes, (3) repository throws SocketException → provider emits AsyncError with the exception, (4) range.end == DateTime.now() boundary → isFuturePeriod == false (not strictly after), (5) empty organisation (zero records) → AsyncData(0). Use a fake clock (injected nowFn) for isFuturePeriod determinism. Verify AsyncLoading is emitted before AsyncData in the provider lifecycle using a listener or container.listen().

Component
Period Record Count Service
service low
Epic Risks (2)
medium impact high prob dependency

Detecting overlap with previously submitted reports requires querying a report history table that may not yet exist or may not have a reliable submitted_at / period_end field, making the validator dependent on an incomplete upstream feature (Bufdir Report History & Audit Log).

Mitigation & Contingency

Mitigation: Define the minimum interface (a single repository method: getSubmittedPeriods(orgId) → List<DateTimeRange>) as an abstract class in this epic. Implement a stub that returns an empty list until the history feature is available, so the validator compiles and passes tests without a real data source.

Contingency: If the history feature is delayed beyond this feature's delivery window, ship the validator with the stub returning an empty list (overlap check disabled) and surface a feature-flag-controlled warning banner explaining that overlap detection will be enabled in a future update.

medium impact medium prob scope

Bufdir's structural requirements for reporting periods (complete calendar months, grant-year span restrictions) may be ambiguous or subject to change, causing the validator to enforce rules that are incorrect or overly restrictive for some organisations.

Mitigation & Contingency

Mitigation: Document the specific Bufdir rules being enforced in the validator's source code as named constants with references to the relevant Bufdir guidelines. Review the rules with at least one coordinator representative before implementation is finalised.

Contingency: Expose a per-org configuration flag (strict_bufdir_validation: bool) in the period configuration repository so that rule enforcement can be relaxed for specific organisations without a code deployment.