critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

submitClaim() returns ClaimTransitionFailure(reason: invalidState) if the claim's current state is not ClaimStateDraft
submitClaim() returns ClaimTransitionFailure(reason: claimNotFound) if no claim exists for the given claimId
All ExpenseLine amounts for the claim are fetched and summed; the sum is passed to ThresholdEvaluationService.evaluate(totalAmount, organizationId)
ThresholdEvaluationService returns ApprovalPath.autoApproval or ApprovalPath.manualReview based on the configured threshold for the organization
ExpenseClaimStatusRepository.updateStatus(claimId, ClaimStateSubmitted(...)) is called exactly once on success
A ClaimAuditEvent is recorded with: eventType=submitted, actorUserId, timestamp=DateTime.now(UTC), approvalPath
submitClaim() returns ClaimTransitionSuccess(newState: ClaimStateSubmitted(...), approvalPath: computed) on success
If ExpenseClaimStatusRepository throws, submitClaim() catches the exception and returns ClaimTransitionFailure(reason: repositoryError) โ€” no exception propagates to caller
The method is idempotent for the submitted state: calling submitClaim() on an already-submitted claim returns invalidState failure without side effects

Technical Requirements

frameworks
Dart
Flutter
apis
ThresholdEvaluationService.evaluate(double totalAmount, String organizationId)
ExpenseClaimStatusRepository.getClaimWithLines(String claimId)
ExpenseClaimStatusRepository.updateStatus(String claimId, ClaimState newState)
ClaimAuditEventRepository.record(ClaimAuditEvent event)
Supabase (via repository layer)
data models
ExpenseClaim
ExpenseLine
ClaimState
ClaimTransitionResult
ApprovalPath
ClaimAuditEvent
performance requirements
Line total aggregation must use a single DB query (SELECT SUM) via the repository โ€” do not fetch all lines and sum in Dart
Total round-trip for submitClaim() must complete within 2 seconds under normal Supabase latency
security requirements
actorUserId must be validated as the owner of the claim before the transition is permitted; submitting another user's claim must return insufficientPermissions
Total amount calculation must be done server-side or re-verified server-side โ€” never trust a client-provided total
Audit event must capture the actorUserId for non-repudiation

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Create ApprovalWorkflowServiceImpl in lib/src/approval_workflow/approval_workflow_service_impl.dart implementing the abstract interface. Use constructor injection for all dependencies (ThresholdEvaluationService, ExpenseClaimStatusRepository, ClaimAuditEventRepository) โ€” never use service locators inside the class. For line total aggregation, add an aggregateLineTotals(claimId) method to ExpenseClaimStatusRepository that delegates to a Supabase RPC or a SELECT SUM query. Wrap the entire submitClaim() body in a try-catch that maps exceptions to ClaimTransitionFailure โ€” use specific exception types if the repository layer throws them.

The threshold evaluation and status update should happen in sequence (not parallel) because the approval path must be known before persisting state. Do not start the auto-approval transition inside submitClaim() โ€” that is the responsibility of the caller (task-003).

Testing Requirements

Unit tests (flutter_test): mock ThresholdEvaluationService, ExpenseClaimStatusRepository, ClaimAuditEventRepository. Test cases: (1) draft claim below threshold โ†’ returns autoApproval path; (2) draft claim above threshold โ†’ returns manualReview path; (3) non-draft claim โ†’ returns invalidState failure; (4) claim not found โ†’ returns claimNotFound failure; (5) repository throws on updateStatus โ†’ returns repositoryError failure; (6) audit event is recorded with correct fields in success case; (7) actor is not claim owner โ†’ returns insufficientPermissions. Use verify() to assert repository methods are called the expected number of times.

Component
Approval Workflow Service
service high
Epic Risks (3)
high impact high prob technical

The ThresholdEvaluationService is described as shared Dart logic used both client-side and in the Edge Function. Supabase Edge Functions run Deno/TypeScript, not Dart, meaning the threshold logic must be maintained in two languages and can diverge, causing the server to reject legitimate client submissions.

Mitigation & Contingency

Mitigation: Implement the threshold logic as a single TypeScript module in the Edge Function and call it via a thin Dart HTTP client wrapper for client-side preview feedback only. The server is always authoritative; the client version is purely for UX (showing the user whether their claim will auto-approve before they submit).

Contingency: If dual-language maintenance is unavoidable, create a shared golden test file (JSON fixtures with inputs and expected outputs) that is run against both implementations in CI to detect divergence immediately.

medium impact medium prob technical

A peer mentor could double-tap the submit button or a network retry could trigger a duplicate submission, causing the ApprovalWorkflowService to attempt two concurrent state transitions from draftโ†’submitted for the same claim, potentially resulting in two audit events or conflicting statuses.

Mitigation & Contingency

Mitigation: Implement idempotency in the ApprovalWorkflowService using a database-level unique constraint on (claim_id, from_status, to_status) per transition, combined with a UI-level submission lock (disable button after first tap until response returns).

Contingency: Add a deduplication check at the start of every state transition method that returns the existing state if an identical transition is already in progress or completed within the last 10 seconds.

high impact medium prob scope

Claims with multiple expense lines (e.g., mileage + parking) must have their combined total evaluated against the threshold. If individual lines are added asynchronously or the evaluation runs before all lines are persisted, the auto-approval decision may be computed on an incomplete set of expense lines.

Mitigation & Contingency

Mitigation: The Edge Function always fetches all expense lines from the database (not from the client payload) before computing the threshold decision. Define a clear claim submission contract that requires all expense lines to be persisted before the submit action is called.

Contingency: Add a validation step in ApprovalWorkflowService that counts expected vs. persisted expense lines before allowing the transition, returning a validation error if lines are missing.