critical priority medium complexity infrastructure pending infrastructure specialist Tier 1

Acceptance Criteria

The Edge Function independently fetches all expense lines for the given claim_id from the database using the service role client
Expense line amounts are summed using integer arithmetic (in øre/cents) to avoid floating-point rounding errors
The organisation-specific approval threshold is fetched from the org configuration table — never hardcoded
If server-computed total is below or equal to the threshold, response returns approval_path: 'auto_approved'
If server-computed total exceeds the threshold, response returns approval_path: 'manual_required'
The response includes server_total, threshold_applied, and the server-authoritative approval_path
The client-supplied client_computed_result is logged for discrepancy detection but does NOT influence the server's decision
A discrepancy between client result and server result is logged as a warning with claim_id, client value, and server value for fraud/bug detection
Claims belonging to a different organisation than the authenticated user's org return HTTP 403
If the claim_id does not exist, the function returns HTTP 404
If expense lines cannot be fetched (database error), the function returns HTTP 500 with a non-sensitive error message
The algorithm output is deterministic — same inputs always produce the same output

Technical Requirements

frameworks
Deno
TypeScript
Supabase Edge Functions runtime
apis
Supabase PostgreSQL 15 via service role client (expense lines fetch)
Supabase PostgreSQL 15 via service role client (org threshold config fetch)
data models
claim_event
activity
performance requirements
Full threshold evaluation (2 DB queries + computation) must complete within 800ms
Database queries must use indexed lookups on claim_id — no full table scans
security requirements
Service role client used only for fetching expense data — never returns service role credentials to client
organisation_id from JWT used as RLS scope for all queries — prevents cross-org data access even with service role
client_computed_result treated as untrusted input — logged only, never used in computation
Discrepancy logging must not include PII — only claim ID and numeric values
Function input validated: claim_id must be a valid UUID format before any DB query

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Extract the threshold evaluation into a pure TypeScript function `evaluateThreshold(expenseLines: ExpenseLine[], thresholdConfig: ThresholdConfig): ThresholdResult` that takes no database dependencies — this makes it trivially unit-testable. The Edge Function handler is then a thin orchestration layer: fetch data → call pure function → return result. Use integer arithmetic throughout: store and compute amounts as integers in the smallest currency unit (øre for NOK) and only convert to decimal for the response payload. The discrepancy logger should use a structured log format: `console.warn(JSON.stringify({ event: 'threshold_discrepancy', claim_id, client_total: clientResult.total, server_total, diff }))` — this allows easy log aggregation and alerting.

Mirror the Dart ThresholdEvaluationService algorithm exactly, including edge cases for zero-line claims and claims with mixed expense types.

Testing Requirements

Deno unit tests (threshold_logic_test.ts) for the pure computation function with parameterised test cases: (1) total exactly at threshold → auto_approved, (2) total one øre below threshold → auto_approved, (3) total one øre above threshold → manual_required, (4) zero-amount claim → auto_approved, (5) multiple expense lines correctly summed. Integration test against Supabase staging: insert test claim with known expense lines, call function, verify server_total matches sum and approval_path is correct. Test discrepancy detection: supply mismatched client_computed_result, verify warning log emitted. Test 403 for cross-org claim access.

Component
Threshold Validation Supabase Edge Function
infrastructure medium
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.