critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

AutoApprovalEvaluator.evaluate(MileageClaim claim) is called synchronously within submitClaim() after successful assembly and before any call to MileageClaimRepository.
Claims where `distanceKm` is below the org config's auto-approval threshold AND `hasAdditionalExpenses` is false receive `ClaimStatus.silentAutoApproved`.
Claims where `distanceKm` meets or exceeds the threshold OR `hasAdditionalExpenses` is true receive `ClaimStatus.pendingReview`.
The resolved ClaimStatus is attached to the MileageClaim (via a `resolvedStatus` field or equivalent) before passing to the repository in task-005.
No coordinator notification, push message, or side-effect is triggered within submitClaim() for `silentAutoApproved` claims — notification dispatch is exclusively the responsibility of a downstream consumer, not the service.
AutoApprovalEvaluator is not called if field validation (task-003) has already returned SubmissionFailure — the evaluator is only reached for valid claims.
If AutoApprovalEvaluator throws (unexpected runtime error), submitClaim() returns SubmissionFailure — no exception propagates to the caller.
Unit tests cover all four decision branches: (below threshold, no expenses) → silentAutoApproved; (below threshold, has expenses) → pendingReview; (at/above threshold, no expenses) → pendingReview; (at/above threshold, has expenses) → pendingReview.
The threshold value used in evaluation comes from the orgRateConfigSnapshot on the assembled MileageClaim — not from a re-fetch.

Technical Requirements

frameworks
Flutter
Dart
apis
AutoApprovalEvaluator.evaluate(MileageClaim)
data models
MileageClaim
ClaimStatus
OrgRateConfig
SubmissionOutcome
performance requirements
AutoApprovalEvaluator.evaluate() must be synchronous — no async/await permitted. The threshold comparison is pure arithmetic.
No additional network calls are made during status evaluation — all required data is in the assembled MileageClaim.
security requirements
The auto-approval threshold must be read from the immutable orgRateConfigSnapshot — not re-fetched from the repository at evaluation time (prevents TOCTOU race).
Silent auto-approval must not bypass any audit trail — the resolved ClaimStatus must still be persisted to the database in task-005.

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

AutoApprovalEvaluator should be implemented as a pure function class: `ClaimStatus evaluate(MileageClaim claim)`. The boundary condition for threshold (at exactly 50km) must be `>=` for pending review, not `>`, matching HLF's documented rule that automatic approval applies only below threshold. The no-notification requirement for silent approvals is critical: do NOT trigger any notification service, Supabase edge function, or push mechanism from within submitClaim(). If the coordinator notification system needs to be aware of pending-review claims, it should subscribe to database changes (e.g., a Supabase Realtime listener on the claims table) rather than being called inline.

Document this architectural decision with a comment in the service code. The fact that the threshold comes from the snapshot (not a re-fetch) is a deliberate TOCTOU protection: if an admin changes the threshold during a submission, the claim is evaluated under the rules that were in effect when the user started filling out the form.

Testing Requirements

Unit tests using `flutter_test`. Mock AutoApprovalEvaluator with `mocktail` or `mockito`. Required test cases: (1) distance 30km, threshold 50km, no expenses → silentAutoApproved; (2) distance 30km, threshold 50km, has expenses → pendingReview; (3) distance 50km, threshold 50km, no expenses → pendingReview (boundary: at-threshold is pending); (4) distance 75km, threshold 50km, no expenses → pendingReview; (5) distance 75km, threshold 50km, has expenses → pendingReview; (6) AutoApprovalEvaluator throws → submitClaim returns SubmissionFailure; (7) AutoApprovalEvaluator is NOT called when prior validation returned SubmissionFailure (verify mock was never called). Use `verifyNever()` for test case 7.

Component
Mileage Claim Service
service medium
Epic Risks (2)
high impact medium prob integration

The auto-approval rule requires checking whether any additional expense lines are attached to the claim. The interface between the mileage claim and any co-submitted expense items is not fully defined within this feature's component scope. If the domain model does not include an explicit additionalExpenses collection, the evaluator cannot make a correct determination, which could auto-approve claims that should require manual review.

Mitigation & Contingency

Mitigation: Define the MileageClaim domain object interface with an explicit additionalExpenses: List field (nullable/empty for mileage-only claims) before implementing the service. Coordinate with the Expense Type Selection feature team to agree on the shared domain contract.

Contingency: If the cross-feature contract cannot be finalised before implementation, implement the evaluator to treat any non-null additionalExpenses list as requiring manual review and document the assumption for review during integration testing.

medium impact medium prob technical

A peer mentor who taps the submit button multiple times rapidly (e.g. due to slow network) could cause MileageClaimService to be invoked concurrently, resulting in duplicate claim records being persisted with the same trip data.

Mitigation & Contingency

Mitigation: Implement a submission-in-progress guard in MileageClaimService using a BLoC/Cubit state flag that prevents re-entrant calls. The UI layer (implemented in Epic 4) will also disable the submit button during processing.

Contingency: Add a Supabase-level unique constraint or idempotency key on (user_id, origin, distance, submitted_at truncated to minute) to prevent duplicate rows reaching the database even if the application guard fails.