critical priority low complexity database pending database specialist Tier 1

Acceptance Criteria

ClaimApprovalDecisionRepository exposes: saveDecision(ApprovalDecision), getDecisionByClaimId(String claimId), and getDecisionsByCoordinator(String coordinatorId, {DateRange? range})
ApprovalDecision model contains: id, claim_id, coordinator_id, decision_type (enum: approved, rejected, more_info_requested), justification (String, required for rejection and more_info_requested), decided_at (server-set timestamp), chapter_id
justification field is mandatory when decision_type is rejected or more_info_requested — enforced via a Postgres CHECK constraint AND validated in the repository before the insert
coordinator_id is always set from auth.uid() server-side — client cannot supply a different coordinator_id
RLS policy: coordinators can INSERT decisions only for claims in their chapter; coordinators can SELECT only their own decisions; admins can SELECT all decisions in their org
saveDecision returns Either<Failure, ApprovalDecision> with the server-set decided_at timestamp
Duplicate decision guard: if a decision already exists for a claim, saveDecision returns a DuplicateDecisionFailure rather than inserting a second row
Repository correctly propagates the ClaimEvents created in task-001 — after saving a decision, it calls ClaimEventsRepository.createEvent() with the matching event type
All audit fields (decided_at, coordinator_id) are immutable after creation — no update path exposed in the repository interface

Technical Requirements

frameworks
Flutter
Dart
supabase_flutter
Riverpod or BLoC for DI
apis
Supabase PostgREST REST API
Supabase Auth
data models
ApprovalDecision
ExpenseClaim
ClaimEvent
CoordinatorProfile
performance requirements
getDecisionByClaimId must use a unique index on claim_id for O(1) lookup
getDecisionsByCoordinator must support date-range filtering pushed to the database, not in-memory
security requirements
decided_at must be set by `now()` default in Postgres — not trusted from the client
coordinator_id column must have a DEFAULT of auth.uid() in Postgres and be excluded from INSERT payload
justification text must be sanitised — maximum 2000 characters, no HTML injection risk (store as plain text)
RLS must prevent one coordinator from viewing another chapter's decision history

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Coordinate with the ClaimEventsRepository (task-001) by injecting it as a dependency and calling createEvent() within the same logical operation — consider wrapping both in a Supabase RPC or a Dart transaction wrapper if atomicity is required. If full atomicity is needed, implement a Postgres function (RPC) that inserts the decision and the event in a single transaction to avoid partial state. Use an upsert-with-onConflict-error strategy to enforce the one-decision-per-claim rule at the database level. Keep the repository thin — no business logic beyond mapping and persistence; threshold evaluation and notification triggering belong in service layers (tasks 005 and 006).

Testing Requirements

Unit tests: verify saveDecision calls the correct Supabase insert with exactly the fields that should come from the client (claim_id, decision_type, justification); verify coordinator_id and decided_at are NOT in the insert payload. Test DuplicateDecisionFailure path when a decision already exists. Test that missing justification on a rejection throws ValidationFailure before reaching Supabase. Integration tests: confirm RLS blocks cross-chapter decision reads; confirm Postgres CHECK constraint rejects a rejection with empty justification.

Use flutter_test. Target 90%+ line coverage.

Epic Risks (3)
medium impact medium prob technical

Maintaining multi-select state across paginated list pages is architecturally complex in Flutter with Riverpod/BLoC. If the selection state is stored in the widget tree rather than the state layer, page transitions and list redraws can silently clear selections, causing coordinators to lose their multi-select and re-enter it.

Mitigation & Contingency

Mitigation: Store the selected claim ID set in a dedicated Riverpod StateNotifier outside the paginated list widget tree. The paginated list reads selection state from this provider and does not own it. Selection persists independently of list scroll position or page loads.

Contingency: If cross-page selection proves prohibitively complex, limit bulk selection to the currently visible page (add a clear warning in the UI) and prioritise single-page bulk approval for the initial release.

medium impact medium prob integration

If a coordinator has the queue open while another coordinator approves claims from the same queue (possible in large organisations with shared chapter coverage), the Realtime update may arrive out of order or be missed during a reconnect, leaving the first coordinator's view stale and allowing them to attempt to approve an already-actioned claim.

Mitigation & Contingency

Mitigation: The ApprovalWorkflowService's optimistic locking (from the foundation epic) will catch the concurrent edit at the database level. The CoordinatorReviewQueueScreen should handle the resulting ConcurrencyException by removing the claim from the local list and showing a brief snackbar: 'This claim was already actioned by another coordinator.'

Contingency: Add a queue staleness indicator (a subtle 'last updated X seconds ago' label) and a manual refresh button as a fallback for coordinators who notice inconsistencies.

low impact high prob dependency

The end-to-end test requirement that a peer mentor receives a push notification within 30 seconds of coordinator approval depends on FCM delivery latency, which is outside the application's control and can vary significantly in CI/CD environments.

Mitigation & Contingency

Mitigation: Structure end-to-end tests to verify notification intent (correct FCM payload dispatched, correct Realtime event emitted) rather than actual device delivery timing. Use test doubles for FCM delivery in automated tests and reserve real-device delivery tests for manual pre-release validation.

Contingency: If notification timing requirements must be validated in automation, instrument the ApprovalNotificationService with a test hook that records dispatch timestamps and assert against those rather than actual FCM callbacks.