critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ExpenseClaimStatusRepository is an abstract interface; SupabaseExpenseClaimStatusRepository is the concrete implementation
getStatus(String claimId) returns Future<ExpenseClaimStatus> and throws ClaimNotFoundException if no row exists
updateStatus(UpdateStatusRequest request) issues UPDATE WHERE claim_id = ? AND version = ? and checks affected row count
If the UPDATE affects 0 rows (version mismatch), updateStatus() throws OptimisticLockException containing the freshly re-fetched current status
If the UPDATE affects 1 row, updateStatus() returns the new ExpenseClaimStatus with incremented version
watchStatus(String claimId) returns Stream<ExpenseClaimStatus> using Supabase Realtime channel subscribed to the specific claim_id row
Stream emits the updated status within 2 seconds of a database change in a local/test environment
Stream subscription is properly cancelled when the consumer calls cancel() — no memory leaks
UpdateStatusRequest validates the transition is valid before making a network call (throws InvalidTransitionException for disallowed transitions)
All Supabase errors are mapped to typed domain exceptions: permission denied → StatusPermissionException, constraint violation → InvalidTransitionException, network error → retried up to 3 times
Repository is injectable via Riverpod and fully mockable for BLoC tests
100% branch coverage on optimistic lock detection logic

Technical Requirements

frameworks
Flutter
Supabase
Riverpod
BLoC
apis
Supabase REST API — expense_claim_status table
Supabase Realtime — PostgreSQL changes channel
data models
ExpenseClaimStatus
ClaimStatus
UpdateStatusRequest
performance requirements
Realtime stream must reconnect automatically on connection drop — use Supabase's built-in reconnect logic
updateStatus() target latency under 3 seconds on standard mobile connection
Stream must not emit duplicate events for the same version — deduplicate by version in the stream transformer
security requirements
updated_by in the UPDATE payload must always be auth.uid() from the session — never accept it as a parameter
Stream channel must be scoped to a single claim_id — never subscribe to the full table
Realtime channel must use authenticated JWT — Supabase client handles this automatically but verify session is active before subscribing
OptimisticLockException must not expose internal version numbers in user-facing error messages — log at debug level only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Optimistic locking with Supabase: use `.update(payload).eq('claim_id', claimId).eq('version', currentVersion)` — the version filter means 0 rows affected = conflict. Supabase's `.update()` does not return a count by default; use `.select()` chained after `.update()` to get the updated row back, then check if the list is empty (conflict) or has one item (success). Do NOT use a separate SELECT to check version before UPDATE — this creates a TOCTOU race condition that defeats the purpose of optimistic locking. For Realtime, use `supabase.channel('claim-status-$claimId').onPostgresChanges(...)` scoped to `filter: 'claim_id=eq.$claimId'`.

In the stream transformer, parse the payload directly from the Realtime event rather than issuing a follow-up SELECT, to minimize latency and round-trips. Provide a StreamController wrapper that handles the Supabase RealtimeChannel lifecycle (subscribe on listen, unsubscribe on cancel) within a single Dart Stream.

Testing Requirements

Unit tests using flutter_test and mock SupabaseClient: getStatus() success, getStatus() with empty result throws ClaimNotFoundException, updateStatus() with matching version succeeds, updateStatus() with mismatched version throws OptimisticLockException with re-fetched status, watchStatus() emits values on stream, stream cancellation removes Supabase Realtime subscription, InvalidTransitionException thrown before network call for disallowed transitions, all three retry scenarios for network errors. Integration tests: concurrent update simulation — two clients read version=1, first update succeeds returning version=2, second update returns OptimisticLockException, Realtime stream emits event within 2 seconds of direct database UPDATE, stream reconnects after simulated network drop.

Component
Expense Claim Status Repository
data medium
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.