Implement expense type catalogue repository
epic-travel-expense-registration-foundation-task-002 — Build the ExpenseTypeCatalogueRepository Dart class that reads expense types and exclusive group rules from Supabase, implements offline caching via Hive or shared_preferences, and exposes reactive streams for live updates. Ensure mutual-exclusion group metadata is surfaced so upstream services can enforce 'km + bus ticket cannot both be selected' rules.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Fetch expense types and exclusive group members in a single Supabase query using a join: supabase.from('expense_types').select('*, expense_type_exclusive_group_members(exclusive_group_id)'). This returns each expense type with a nested array of its group memberships — map this to exclusiveGroupIds in the Dart model. For the reactive stream, use a StreamController> in the repository; on init, load from cache (emit immediately), then fetch from Supabase (emit updated data), then set a timer for the 24-hour refresh. Do not use Supabase Realtime for this data — expense type catalogues change very infrequently and the added complexity of a Realtime subscription is not justified.
Use shared_preferences with a JSON-encoded list for the cache and store a 'catalogue_fetched_at' timestamp key alongside it. The isConflicting helper should be a pure function — extract it to a standalone function in a separate file so it can be tested without the repository.
Testing Requirements
Unit tests with flutter_test: (1) getExpenseTypes() returns correctly mapped ExpenseType list when Supabase returns valid rows; (2) isConflicting(['km-godtgjoerelse-id', 'kollektiv-id']) returns true (shared exclusive group); (3) isConflicting(['km-godtgjoerelse-id', 'parkering-id']) returns false (different groups); (4) isConflicting with single item always returns false; (5) cache hit path: Supabase client never called when valid cache exists within 24 hours; (6) cache miss after 24 hours: Supabase called and cache refreshed; (7) Supabase throws on first launch with no cache: emits error state. Use a FakeSupabaseClient and a FakeSharedPreferences. Verify the Riverpod provider override works correctly in a ProviderContainer.
Row-level security policies for expense claims must correctly scope data to organisation, role (peer mentor sees own claims only, coordinator sees org-wide queue), and claim status. Incorrect RLS can expose claims cross-organisation or prevent coordinators from accessing the attestation queue.
Mitigation & Contingency
Mitigation: Define RLS policies in code-reviewed migration files. Write integration tests that attempt cross-org reads with different JWT roles and assert access denial. Review with a second engineer before merging migrations.
Contingency: If RLS is misconfigured post-deployment, disable the affected policy temporarily and apply a hotfix migration within the same release window. No claim data is exposed publicly due to Supabase project-level auth requirement.
The auto-approval Edge Function is triggered server-side on expense insert. Cold-start latency or Edge Function failures can block the submission response and degrade UX, especially on mobile networks.
Mitigation & Contingency
Mitigation: Implement the auto-approval Edge Function client with a timeout and graceful fallback: if no result is received within 5 seconds, treat the claim as 'pending' and poll for the status update via Supabase Realtime. Keep the Edge Function warm with a periodic ping.
Contingency: If Edge Function reliability is unacceptable, move auto-approval evaluation to a database trigger or Postgres function as an interim measure, accepting that threshold configuration changes require a migration rather than a settings update.
The expense type catalogue and threshold configuration are cached locally for offline use. If an organisation updates their catalogue exclusion rules or thresholds while a peer mentor is offline, the local cache may allow submissions that violate the new policy.
Mitigation & Contingency
Mitigation: Cache entries include a TTL (24 hours). On connectivity restore, refresh cache before allowing new submissions. Server-side validation in the Edge Function and save functions provides a second enforcement layer.
Contingency: If a stale-cache submission passes client validation but fails server validation, surface a clear error message explaining that the expense type rules have been updated and prompt the user to review their selection with the refreshed catalogue.