critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ThresholdEvaluationService is a pure Dart class with zero Flutter framework imports — verified by dart analyze with no flutter dependency in the file's import list
evaluateClaim(claimAmount, distanceKm, hasReceipts, orgThresholdConfig) returns ThresholdResult.autoApprove when claimAmount < org auto-approve threshold AND distanceKm < org distance threshold
evaluateClaim returns ThresholdResult.requiresReceipt when claimAmount >= receipt threshold but hasReceipts is false
evaluateClaim returns ThresholdResult.requiresManual when claimAmount exceeds manual review threshold regardless of other parameters
OrgThresholdConfig is loaded from Supabase with org-specific overrides — default config used as fallback when org record not found
ThresholdResult sealed class exposes autoApprove, requiresManual, requiresReceipt variants with descriptive reason strings
Service logic is bit-for-bit equivalent to the Edge Function Deno implementation — confirmed by shared test fixture JSON running identical assertions
No mutable state in service — all methods are pure functions that accept config and return results deterministically
OrgThresholdConfig includes: autoApproveMaxAmount (double), autoApproveMaxDistanceKm (double), receiptRequiredAboveAmount (double), manualReviewAboveAmount (double)
evaluateClaim handles null/zero distanceKm gracefully — treats as 0.0 without throwing

Technical Requirements

frameworks
Dart (pure, no Flutter dependency)
BLoC (consumed downstream by ApprovalBLoC)
apis
Supabase PostgREST — read org_threshold_configs table
Supabase Edge Function parity — logic must mirror threshold_evaluator Deno module
data models
activity (expense claim amounts, distance_km)
assignment (organization_id for config lookup)
performance requirements
evaluateClaim must complete in < 1ms (pure computation, no I/O)
OrgThresholdConfig fetch cached in memory per session — no repeated Supabase calls for same org
security requirements
Service is client-side ONLY for UX feedback — all actual approval decisions enforced server-side via Edge Function
OrgThresholdConfig fetched via authenticated Supabase client — RLS restricts to user's own organization
No threshold config values hard-coded in source — all values from database to prevent binary patching bypass

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Define ThresholdResult as a sealed class with three subclasses to enable exhaustive pattern matching in BLoC. OrgThresholdConfig should be a simple immutable data class with copyWith. Load config lazily on first call and cache — use a simple in-memory Map keyed by organizationId. The critical invariant: this service must NEVER be the authoritative decision-maker — it only provides UX hints (e.g., 'You will need manual approval').

Place the service in lib/domain/services/ to signal it is domain-layer, not infrastructure. Mirror the exact same threshold comparison operators (< vs <=) as the Edge Function — document which boundary belongs to which result to prevent off-by-one divergence.

Testing Requirements

Unit tests using flutter_test (dart:test compatible). Test suite: (1) pure logic tests — 10+ parameterized cases covering boundary values for each ThresholdResult variant, (2) config loading tests — mock Supabase client returning org config, default fallback when not found, error propagation on network failure, (3) parity tests — shared JSON fixture file with 20 test vectors also run against Edge Function in CI to guarantee identical behavior. Target 100% branch coverage on evaluateClaim and all ThresholdResult constructors.

Component
Threshold Evaluation Service
service 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.