critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

ClaimApprovalRepository is an abstract interface; SupabaseClaimApprovalRepository is the concrete implementation
recordDecision(RecordDecisionRequest request) inserts a row with coordinator_snapshot and threshold_at_decision populated from the request — these fields are never null
recordDecision() returns the persisted ClaimApprovalDecision with server-assigned decision_id and decided_at
getDecisionsForClaim(String claimId) returns Future<List<ClaimApprovalDecision>> ordered by decided_at ASC
getPendingDecisionsForCoordinator(String coordinatorId) returns Future<List<PendingClaimSummary>> — claims with status pending_approval in the coordinator's chapters
Supabase check constraint violation (invalid decision value) maps to InvalidDecisionValueException
Supabase FK violation on claim_id maps to ClaimNotFoundDomainException
Supabase permission denied (42501) maps to ApprovalPermissionException
RecordDecisionRequest value class includes: claimId, decision, justification?, coordinatorSnapshot, thresholdAtDecision, and is validated before the network call (rejects empty justification for rejected/escalated decisions)
Repository is injectable via Riverpod and fully mockable
getPendingDecisionsForCoordinator() uses a Supabase view or RPC to avoid N+1 queries

Technical Requirements

frameworks
Flutter
Supabase
Riverpod
apis
Supabase REST API — claim_approval_decisions table
Supabase RPC (for pending decisions aggregation)
data models
ClaimApprovalDecision
CoordinatorSnapshot
PendingClaimSummary
RecordDecisionRequest
performance requirements
getPendingDecisionsForCoordinator() must use server-side filtering, never client-side filtering of a full table scan
All methods must be non-blocking and return Futures
recordDecision() target latency under 3 seconds on standard mobile connection
security requirements
coordinatorSnapshot must be assembled from the verified auth session profile, never from client-supplied raw data
thresholdAtDecision must be fetched from the threshold configuration service before calling recordDecision(), not hardcoded
Justification required for rejection and escalation — validated client-side in RecordDecisionRequest and enforced at domain layer before network call

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

getPendingDecisionsForCoordinator() should call a Supabase RPC function (e.g., get_pending_claims_for_coordinator(coordinator_id UUID)) that performs the join between expense_claims, claim_approval_decisions, and coordinator_chapter_memberships on the server side. This avoids sending the full claims table to the client. Define a PendingClaimSummary Dart model that is a lightweight projection — claim_id, submitter_name, amount, submitted_at, chapter_name — not the full claim object. The RecordDecisionRequest validation should be in the domain layer, not the repository, so that BLoC tests can verify validation without a Supabase stub.

Follow the same abstract interface + concrete implementation pattern as ClaimEventsRepository for consistency.

Testing Requirements

Unit tests: recordDecision() success returns populated model, recordDecision() with empty justification on rejection throws ValidationException before network call, error mapping for each PostgrestException code, getPendingDecisionsForCoordinator() maps RPC response correctly. Integration tests: full round-trip insert and retrieve for all three decision types, verify coordinator_snapshot stored correctly matches input, verify threshold_at_decision stored matches input, getPendingDecisionsForCoordinator() returns only claims in coordinator's chapter, cross-chapter insert attempt returns ApprovalPermissionException.

Epic Risks (3)
medium impact medium prob technical

Optimistic locking in ExpenseClaimStatusRepository may produce excessive concurrency exceptions in high-volume coordinator sessions where multiple coordinators process the same queue simultaneously, causing confusing UI errors and coordinator frustration.

Mitigation & Contingency

Mitigation: Design the locking strategy with a short retry window (1-2 automatic retries with 200ms back-off) before surfacing the error to the UI. Document the concurrency model clearly so the UI layer can display a contextual 'claim was already actioned' message rather than a generic error.

Contingency: If contention remains high under load testing, switch to a last-writer-wins update with a conflict notification rather than a hard block, and log all concurrent edits for audit purposes.

medium impact medium prob integration

FCM device tokens stored for peer mentors may be stale (app reinstalled, token rotated) causing push notifications for claim status changes to silently fail, leaving submitters unaware their claim was approved or rejected.

Mitigation & Contingency

Mitigation: Implement token refresh on every app launch and store updated tokens in Supabase. ApprovalNotificationService should fall back to in-app Realtime delivery when FCM returns an invalid-token error and should queue a token refresh request.

Contingency: If FCM delivery rates fall below acceptable thresholds in production monitoring, add a polling fallback in the peer mentor claim list screen that checks status on foreground resume.

high impact low prob dependency

Supabase Realtime has per-project channel and connection limits. If many coordinators and peer mentors are simultaneously subscribed across multiple screens, the project may hit quota limits causing subscription failures.

Mitigation & Contingency

Mitigation: Design RealtimeApprovalSubscription to use a single shared channel per user session rather than per-screen subscriptions. Implement subscription reference counting so channels are only opened once and reused across screens.

Contingency: Upgrade the Supabase plan tier if limits are reached, and implement graceful degradation to polling with a 30-second interval when Realtime is unavailable.