critical priority high complexity backend pending backend specialist Tier 3

Acceptance Criteria

ApprovalWorkflowService.approveClaim(claimId, coordinatorId, notes) transitions claim status from pending/under_review to approved
ApprovalWorkflowService.rejectClaim(claimId, coordinatorId, rejectionReason) transitions claim status to rejected
Both methods throw a ClaimNotApprovableException if the claim is already in a terminal state (approved, rejected, auto_approved)
An approval_decisions row is written with decision_type, coordinator_id, claim_id, notes/reason, and decided_at timestamp
A claim_events row is emitted with event_type='coordinator_approved' or 'coordinator_rejected', actor_id=coordinatorId, and relevant notes
ApprovalNotificationService.notifyClaimant() is called after every successful decision
All three writes (status update, decision record, event record) occur atomically — if any fails, all are rolled back
Service validates that the coordinatorId has permission to approve claims in the claim's organisation before proceeding
Service is injectable and mockable — no static calls or singleton state
Returns an ApprovalResult value object with the updated claim status and decision ID
All public methods are async and return Future — no synchronous blocking operations

Technical Requirements

frameworks
Flutter (Dart)
Riverpod (dependency injection)
Supabase
apis
Supabase REST API (expense_claims, approval_decisions, claim_events tables)
ApprovalNotificationService interface
data models
expense_claims (id, status, organisation_id, submitted_by)
approval_decisions (id, claim_id, coordinator_id, decision_type, notes, decided_at)
claim_events (id, claim_id, event_type, actor_id, notes, created_at)
performance requirements
Single approval decision completes < 3 seconds end-to-end including notification trigger
All DB writes in a single Supabase RPC transaction — no multi-round-trip writes
security requirements
Verify coordinator's organisation membership before allowing approval
Reject approval attempts where coordinatorId does not match authenticated Supabase user
Sanitise notes/reason fields — max 2000 characters, no HTML injection
Row Level Security on approval_decisions must restrict reads to same organisation

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Define an abstract ApprovalWorkflowService interface and a concrete SupabaseApprovalWorkflowService implementation — this keeps the BLoC layer testable without a live database. Implement the three-write atomicity using a Supabase Postgres function (RPC) rather than three sequential Dart awaits; this guarantees all-or-nothing at the DB level. The Dart service method calls the RPC and maps the result. Keep the permission check as a first-class step before any writes — fail fast.

For the notification trigger, call ApprovalNotificationService after the RPC succeeds, inside a try/catch that logs but does not rethrow notification failures (notification failure must not roll back the approval). Register the service with Riverpod as a Provider so tests can override with a mock. Define ApprovalResult as an immutable Dart class with copyWith.

Testing Requirements

Unit tests (flutter_test): mock all four dependencies (ThresholdEvaluationService, ClaimEventsRepository, ExpenseClaimStatusRepository, ApprovalNotificationService) and verify the orchestration logic for approve and reject paths. Test that terminal-state claims throw ClaimNotApprovableException. Test that all three repository calls are made and that a failure in any one triggers no partial writes (mock the third to throw and assert the first two were not committed). Test permission check: coordinator from a different org throws UnauthorisedException.

Integration test: run against a local Supabase instance and assert all three DB rows are created correctly. Target >= 85% branch 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.