critical priority medium complexity backend pending backend specialist Tier 5

Acceptance Criteria

ExpenseAttestationService exposes a Riverpod AsyncNotifier<PaginatedResult<PendingClaim>> that loads without error when the authenticated coordinator has claims pending attestation
Queue is scoped strictly to the coordinator's chapter: claims from other chapters never appear in the result, verified by RLS policy enforcement
Each PendingClaim object includes: claimId, claimantFullName, expenseTypeLabels (array), totalAmountNOK, submittedAt timestamp, and receiptRequired boolean
Pagination works correctly: page size defaults to 20, requesting page 2 returns the next 20 items, and total count is accurate
Default sort order is submittedAt ascending (oldest first) so coordinators process claims in order of submission
Empty queue returns AsyncData with an empty list, not AsyncError
When the coordinator's chapter scope is undefined or not yet loaded, the notifier returns AsyncLoading and does not query Supabase
Fetching fails gracefully: on Supabase error the notifier emits AsyncError with a user-readable message and does not crash the app
The notifier's ref.invalidate() triggers a fresh fetch that reflects any new claims added between the first and second load

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgreSQL 15 REST/PostgREST
Supabase Auth (JWT claims for chapter scope)
data models
assignment
activity
contact
performance requirements
Initial queue fetch completes within 2 seconds on a 4G connection
Pagination cursor-based to avoid offset drift when new claims are inserted between pages
Maximum 1 Supabase round-trip per page fetch — join claimant name server-side, not in Dart
security requirements
RLS policy on expense_claims table must restrict SELECT to rows where chapter_id matches the coordinator's JWT claim
JWT chapter scope extracted from Supabase Auth claims, never from client-supplied parameters
No PII (claimant name, amount) logged to console or crash reporting tools

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use AsyncNotifier> from Riverpod 2.x — avoid StateNotifier. The chapter scope should come from a separate chapterScopeProvider so it can be mocked in tests. Build the Supabase query with .select('id, submitted_at, total_amount, expense_types, contacts(first_name, last_name)').eq('status', 'pending-attestation').eq('chapter_id', chapterScope).order('submitted_at').range(offset, offset+pageSize-1). Map the PostgREST response to PendingClaim in a dedicated mapper function — keep the notifier free of mapping logic.

PendingClaim should be an immutable Dart class (use freezed or hand-written copyWith). Avoid joining more columns than needed to keep payload small.

Testing Requirements

Unit tests using flutter_test with a fake ExpenseRepository: (1) happy path returns correct PendingClaim list with all fields populated, (2) empty chapter returns empty list not error, (3) repository throws PostgrestException → notifier emits AsyncError, (4) pagination: second page fetch sends correct range header. Integration test with a Supabase test project: verify RLS blocks cross-chapter claims (coordinator A cannot see coordinator B's chapter claims). All tests must pass with flutter_test; no real Supabase credentials in CI.

Component
Expense Attestation Service
service medium
Epic Risks (3)
high impact medium prob scope

Mutual exclusion rules are stored in the expense type catalogue's exclusive_groups field. If the catalogue schema or group definitions differ between HLF and Blindeforbundet, the validation service must handle multiple group configurations without hardcoding organisation-specific logic.

Mitigation & Contingency

Mitigation: Design the validation service to be purely data-driven: it reads exclusive_groups from the cached catalogue and enforces whichever groups are defined, with no hardcoded organisation names. Write parameterised unit tests covering at least 4 different catalogue configurations to verify generality.

Contingency: If an organisation requires non-standard exclusion semantics (e.g. partial exclusion within a group), introduce an exclusion_type field to the catalogue schema and extend the service, treating it as a catalogue configuration change rather than a code fork.

medium impact high prob technical

The attestation service subscribes to Supabase Realtime for live queue updates. On mobile, Realtime WebSocket connections can be dropped during network transitions, causing the coordinator queue to become stale without the user being aware.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management: reconnect on network-change events, show a 'reconnecting' indicator when the subscription is broken, and perform a full queue refresh on reconnect rather than relying solely on delta events.

Contingency: Add a manual pull-to-refresh gesture on the attestation queue screen as a guaranteed fallback. If Realtime proves unreliable in production, switch to periodic polling (30-second interval) as a degraded but functional mode.

medium impact medium prob integration

If a peer mentor submits a draft while offline and then submits the same claim again after connectivity is restored (thinking the first attempt failed), duplicate claims may be persisted in Supabase.

Mitigation & Contingency

Mitigation: Assign a client-generated idempotency key (UUID) to each draft at creation time. The submission service sends this key as an upsert key to Supabase, preventing duplicate inserts. The draft is marked 'submitted' locally after first successful upload.

Contingency: Implement a server-side duplicate detection trigger on the expense_claims table checking (activity_id, claimant_id, created_date) within a 24-hour window and returning the existing record ID rather than inserting a duplicate.