critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

A constant SYSTEM_ACTOR_ID is defined (e.g., 'system:auto-approval') and used as the actorUserId in all auto-approval audit events and decision records
Auto-approval is triggered only when submitClaim() returns ApprovalPath.autoApproval — it must never be triggered for manualReview path
ClaimApprovalDecisionRepository.saveDecision(claimId, decision: approved, actorUserId: SYSTEM_ACTOR_ID, comment: 'Auto-approved: below threshold') is called
ExpenseClaimStatusRepository.updateStatus(claimId, ClaimStateApproved(approvedAt: DateTime.now(UTC), approvedByUserId: SYSTEM_ACTOR_ID)) is called
A ClaimAuditEvent is recorded with eventType=autoApproved, actorUserId: SYSTEM_ACTOR_ID, timestamp, and no human comment
The combined submitClaim() + auto-approval flow returns ClaimTransitionSuccess(newState: ClaimStateApproved(...), approvalPath: autoApproval) to the original caller
If auto-approval persistence fails, the claim remains in submitted state (no partial rollback of the submitted state); the failure is returned as ClaimTransitionFailure(reason: repositoryError)
Auto-approval is atomic with respect to the approval decision and status update — both must succeed or both must be rolled back (use a DB transaction via Supabase)

Technical Requirements

frameworks
Dart
Flutter
apis
ClaimApprovalDecisionRepository.saveDecision()
ExpenseClaimStatusRepository.updateStatus()
ClaimAuditEventRepository.record()
Supabase transaction support (rpc or withTransaction)
data models
ClaimApprovalDecision
ClaimState
ClaimAuditEvent
ClaimTransitionResult
performance requirements
Auto-approval must complete within 1 second after submitClaim() determines the path — the submitting user must not wait perceptibly longer for below-threshold claims
Decision and status update must be wrapped in a single Supabase transaction to prevent partial writes
security requirements
SYSTEM_ACTOR_ID must be a reserved identifier that cannot be claimed by any human user — enforce this at the user creation level
Auto-approval must not be callable directly from the public service interface — it is an internal method triggered only by the auto-approval path logic
Audit trail must make it clear the approval was system-initiated, not human-initiated, to satisfy Bufdir audit requirements

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement auto-approval as a private method _executeAutoApproval(String claimId) on ApprovalWorkflowServiceImpl, called from within submitClaim() after the approval path is determined. This method should use a Supabase database function (RPC) to atomically update both the claim status and insert the decision record in a single transaction — this avoids partial write scenarios that cannot be easily recovered in a client-side try-catch. Define the Supabase function as auto_approve_claim(claim_id UUID) and call it via supabase.rpc('auto_approve_claim', params: {'claim_id': claimId}). The SYSTEM_ACTOR_ID constant should be defined in a shared constants file so it can be referenced consistently in audit queries and reports.

Document clearly in dartdoc that auto-approval is idempotent — calling it on an already-approved claim should be a no-op (return success with existing state).

Testing Requirements

Unit tests (flutter_test): mock ClaimApprovalDecisionRepository, ExpenseClaimStatusRepository, ClaimAuditEventRepository. Test cases: (1) below-threshold claim → final state is ClaimStateApproved with SYSTEM_ACTOR_ID; (2) verify saveDecision called with correct actor and comment; (3) verify audit event recorded with eventType=autoApproved; (4) repository failure during decision save → claim stays in submitted state, returns repositoryError; (5) verify auto-approval is NOT triggered when approvalPath is manualReview. Integration test: submit a below-threshold claim against a Supabase test environment and assert the final DB row has status='approved' and decision actor='system:auto-approval' in a single DB round-trip check.

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.