critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

Class `ActivityAggregationRepository` exists in `lib/repositories/activity_aggregation_repository.dart`
Method `getOrgAggregation({required String organisationId, required Period period})` returns `Future<AggregationResult>` by calling the `get_activity_aggregation_for_org` RPC
Method `getPeerMentorAggregation({required String organisationId, required String userId, required Period period})` returns `Future<AggregationResult>` by calling the `get_activity_aggregation_for_peer_mentor` RPC
`AggregationResult` is an immutable Dart class with `sessionCount` (int), `totalHours` (double), `periodStart` (DateTime UTC), `periodEnd` (DateTime UTC), and `organisationId` (String) — the latter two are echoed from the request for traceability
Both methods pass `period.start.toIso8601String()` and `period.end.toIso8601String()` as RPC parameters — no silent timezone conversion
`PostgrestException` from Supabase is caught and re-thrown as a typed `AggregationException` with a human-readable message and the original error code preserved
Riverpod `autoDisposeProvider<ActivityAggregationRepository>` is registered in `lib/providers/repository_providers.dart` using the Supabase client from the existing app provider
Repository constructor accepts `SupabaseClient` as a named parameter to support dependency injection in tests
No hardcoded organisation IDs or user IDs — all identity data flows in via method parameters
Repository logs RPC call duration at debug level using the app's existing logger

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
supabase.rpc('get_activity_aggregation_for_org')
supabase.rpc('get_activity_aggregation_for_peer_mentor')
data models
AggregationResult
Period
PeriodType
performance requirements
Repository must not cache results — caching is the responsibility of the BLoC/Cubit layer
RPC calls must use `await` without blocking the UI thread — Dart async/await handles this natively
Timeout: apply a 10-second timeout to each RPC call via `.timeout(Duration(seconds: 10))`
security requirements
Never log the full RPC response payload — only log metadata (duration, session_count) to avoid logging PII
organisationId must always be explicitly passed to the RPC — never infer from client state or cached values
Use the authenticated Supabase client (not service role) so RLS policies apply

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The Supabase Flutter SDK's `.rpc()` method returns `PostgrestFilterBuilder` — call `.select()` then `.single()` if the RPC returns exactly one row, or handle the list and take `first`. Map the response map directly: `AggregationResult(sessionCount: response['session_count'] as int, totalHours: (response['total_hours'] as num).toDouble(), ...)`. Use `AggregationException` as a sealed class with subtypes `AggregationNetworkException`, `AggregationPermissionException`, `AggregationUnknownException` to allow BLoC to handle each case distinctly. Pass `Period` from `PeriodCalculatorService` directly — do not recompute boundaries in the repository.

Register the provider as `autoDispose` since aggregation results are session-scoped and should not persist across navigation.

Testing Requirements

Unit tests in `test/repositories/activity_aggregation_repository_test.dart` using `flutter_test` and `mockito` (or `mocktail`). Mock `SupabaseClient` and the RPC call chain. Test cases: (1) successful org aggregation RPC response is correctly deserialised into `AggregationResult`, (2) successful peer mentor RPC response is correctly deserialised, (3) `PostgrestException` is caught and rethrown as `AggregationException` with correct message, (4) network timeout throws `AggregationException`, (5) RPC returns `session_count=0, total_hours=0` (no activities) — assert `AggregationResult` reflects zeros without error. Integration test (optional): use a local Supabase instance to verify the full round-trip including RLS enforcement.

Component
Activity Aggregation Repository
data medium
Epic Risks (3)
high impact medium prob security

Supabase RLS policies for aggregation views are more complex than single-table policies. A misconfigured policy could silently allow a coordinator in one organisation to see data from another, causing a data breach and breaking trust with participating organisations.

Mitigation & Contingency

Mitigation: Write automated RLS integration tests that create two separate organisations with distinct data, then assert that queries authenticated as org-A users return only org-A rows. Run these tests in CI on every PR touching the database layer.

Contingency: If an RLS bypass is discovered post-deployment, immediately disable the periodic summaries feature flag, revoke affected sessions, audit access logs, notify affected organisations, and patch the policy before re-enabling.

medium impact medium prob technical

Activity records may span multiple sessions types, proxy registrations, and coordinator bulk entries. Incorrect JOIN logic or missing filters in the aggregation query could double-count sessions or omit activity types, producing inaccurate summaries that erode user trust.

Mitigation & Contingency

Mitigation: Build a fixture dataset covering all activity registration paths (direct, proxy, bulk) and assert expected aggregated counts in integration tests before any UI consumes the repository.

Contingency: If inaccurate counts are reported post-launch, mark affected summaries as invalidated in the database and re-trigger generation once the query is corrected. Communicate transparently to affected users via an in-app banner.

low impact low prob integration

The local cache must be invalidated when a new summary arrives via push notification. If the push token is stale or the FCM/APNs delivery is delayed, the device may show an outdated summary for an extended period, confusing users who see different numbers online versus offline.

Mitigation & Contingency

Mitigation: Implement a TTL on cached summaries (max 48 hours) so stale data is auto-cleared even without a push notification. Also trigger cache refresh on app foreground if the current period's summary is older than 24 hours.

Contingency: Provide a manual pull-to-refresh on the summary card that bypasses the cache and fetches directly from Supabase when a network connection is available.