high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Edge function is deployed to Supabase and reachable via authenticated POST request with a valid expense claim payload
Function loads the organisation's threshold configuration from the database using the org_id from the JWT claim
Auto-approval fires when distance_km < configured threshold AND no receipts are attached; manual attestation is required otherwise
On auto-approval the function writes: auto_approved_at timestamp, auto_approval_threshold_snapshot (JSON of the config at decision time), and status='auto_approved' to expense_claims atomically
On manual-required outcome the function returns status='pending_attestation' without mutating the claim record
Structured JSON response includes: decision ('auto_approved'|'pending_attestation'), reason_code, threshold_snapshot, and evaluated_at
Unit tests cover exact boundary values: distance = threshold (should NOT auto-approve), distance = threshold - 1 (should auto-approve), distance = 0, receipt present with distance < threshold (should NOT auto-approve)
Function returns HTTP 400 with a structured error body for malformed payloads (missing org_id, missing claim_id, invalid distance format)
Function returns HTTP 403 when the caller's JWT does not match the claim's submitter_id
Cold start latency is under 800 ms at p95; threshold config is cached in-memory for the function's lifetime
All database writes are wrapped in a single transaction; partial writes are not possible
Function is idempotent: calling it twice with the same claim_id returns the same decision and does not create duplicate audit rows

Technical Requirements

frameworks
Deno (Supabase Edge Functions runtime)
TypeScript
apis
Supabase REST API (threshold config read)
Supabase service-role client (expense_claims write)
Supabase Auth (JWT verification)
data models
expense_claims
org_threshold_config
expense_line_items
performance requirements
p95 cold start under 800 ms
Threshold config cached in module-level variable for warm invocations
Single DB round-trip for config fetch (select by org_id with index)
Atomic transaction for decision write — no multi-round-trip commits
security requirements
JWT must be verified via Supabase Auth; unauthenticated requests return 401
org_id extracted from JWT claims, never from the request body, to prevent cross-org data access
Service-role key stored in Supabase secrets, never hardcoded
auto_approval_threshold_snapshot stored as immutable snapshot so rule changes do not retroactively alter audit trail
RLS policies on expense_claims must allow service-role bypass for the write but deny direct client writes to the decision fields

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement the function in `supabase/functions/evaluate-auto-approval/index.ts`. Structure: (1) verify JWT and extract org_id, (2) fetch threshold config with a prepared query (cache result), (3) run decision logic in a pure function `evaluateClaim(claim, config): Decision` — this is what unit tests target, (4) if auto-approved, open a Supabase transaction and update expense_claims, (5) return structured response. Keep the decision logic in a separate module `decision-engine.ts` with no Supabase imports so it is trivially unit-testable in Deno without mocks. Store the threshold snapshot as `JSON.stringify(config)` — do not reference the live config row by ID.

The snapshot is the audit record. Use `crypto.randomUUID()` for any new IDs; do not rely on Postgres sequences from the edge function layer. Watch for the HLF rule: '< 50 km AND no receipts = auto-approved' — confirm the exact operator with the product team; implement as a named constant `AUTO_APPROVE_DISTANCE_KM_LIMIT` so it can be overridden per org in future.

Testing Requirements

Unit tests (Deno test runner): threshold boundary matrix (at, below, above threshold; with and without receipts — 6 combinations minimum). Mock the Supabase client to assert exact SQL mutations. Integration test: deploy to local Supabase emulator, submit a real claim payload, assert database state after function returns. Test idempotency by calling the function twice.

Test error paths: missing fields, wrong org, expired JWT. Aim for 100% branch coverage of the decision logic. CI pipeline must run unit tests on every PR; integration tests run on merge to main.

Component
Auto-Approval Edge Function Client
infrastructure medium
Epic Risks (3)
high impact medium prob security

Row-level security policies for expense claims must correctly scope data to organisation, role (peer mentor sees own claims only, coordinator sees org-wide queue), and claim status. Incorrect RLS can expose claims cross-organisation or prevent coordinators from accessing the attestation queue.

Mitigation & Contingency

Mitigation: Define RLS policies in code-reviewed migration files. Write integration tests that attempt cross-org reads with different JWT roles and assert access denial. Review with a second engineer before merging migrations.

Contingency: If RLS is misconfigured post-deployment, disable the affected policy temporarily and apply a hotfix migration within the same release window. No claim data is exposed publicly due to Supabase project-level auth requirement.

medium impact medium prob technical

The auto-approval Edge Function is triggered server-side on expense insert. Cold-start latency or Edge Function failures can block the submission response and degrade UX, especially on mobile networks.

Mitigation & Contingency

Mitigation: Implement the auto-approval Edge Function client with a timeout and graceful fallback: if no result is received within 5 seconds, treat the claim as 'pending' and poll for the status update via Supabase Realtime. Keep the Edge Function warm with a periodic ping.

Contingency: If Edge Function reliability is unacceptable, move auto-approval evaluation to a database trigger or Postgres function as an interim measure, accepting that threshold configuration changes require a migration rather than a settings update.

medium impact low prob scope

The expense type catalogue and threshold configuration are cached locally for offline use. If an organisation updates their catalogue exclusion rules or thresholds while a peer mentor is offline, the local cache may allow submissions that violate the new policy.

Mitigation & Contingency

Mitigation: Cache entries include a TTL (24 hours). On connectivity restore, refresh cache before allowing new submissions. Server-side validation in the Edge Function and save functions provides a second enforcement layer.

Contingency: If a stale-cache submission passes client validation but fails server validation, surface a clear error message explaining that the expense type rules have been updated and prompt the user to review their selection with the refreshed catalogue.