high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

DoubleExportGuard.check(orgId, dateRange) is the first async operation in triggerExport, before any ExportRun creation or claims fetch
If a completed or pending ExportRun already exists for the same orgId and overlapping dateRange, triggerExport immediately returns ExportResult.duplicate(existingRunId) without any further database writes
The guard checks for overlap (not exact match): an export for Jan 1–Jan 31 blocks a new export for Jan 15–Feb 15 unless the operator explicitly overrides (override flag is out of scope for this task β€” simply block and return duplicate)
ExportRunRepository.createPending(orgId, dateRange, requestingUserId) is called after the guard passes and before claims are fetched; the returned exportRunId is propagated through the pipeline
On successful export completion, ExportRunRepository.markCompleted(exportRunId, fileUrl, recordCount, exportedAt) is called and the ExportRun row reflects status='completed' in the database
On any failure between createPending and markCompleted, ExportRunRepository.markFailed(exportRunId, errorCode, errorDetail) is called in the catch block β€” no ExportRun is left in 'pending' state beyond the current request lifecycle
ExportRun persisted on success includes: exportRunId (UUID), orgId, dateRangeStart, dateRangeEnd, requestingUserId, recordCount, fileUrl, status='completed', createdAt, completedAt
DoubleExportGuard and ExportRunRepository are injected; no direct Supabase calls are made inside AccountingExporterServiceImpl
The guard query uses a database-level unique constraint or index on (org_id, date_range_start, date_range_end) to prevent race conditions on concurrent requests
Unit tests cover: guard blocks duplicate, guard allows non-overlapping range, pending run marked failed on exception

Technical Requirements

frameworks
Dart
Supabase Dart client
apis
Supabase PostgREST (export_runs table read/write)
Supabase RPC (optional: use a Postgres function for atomic check-and-insert to prevent race conditions)
data models
ExportRun
ExportRunStatus (enum: pending, completed, failed)
DateRange
performance requirements
DoubleExportGuard check must complete in under 500ms; it is a blocking gate on every export
ExportRunRepository.createPending() must use an upsert or insert with conflict detection (ON CONFLICT DO NOTHING) to handle concurrent requests gracefully
markCompleted and markFailed must each be a single Supabase update call
security requirements
ExportRun rows must be scoped to the authenticated org via RLS β€” an org cannot read or modify another org's export runs
requestingUserId stored in ExportRun must come from the verified JWT, not from the request body, to prevent impersonation
The guard's overlap query must use parameterised date inputs to prevent SQL injection via date strings

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement DoubleExportGuard as a separate injectable class (not a static method) to allow mocking in tests. Its check() method should query export_runs where org_id = orgId AND status IN ('pending','completed') AND NOT (dateRangeEnd < existingStart OR dateRangeStart > existingEnd) β€” standard interval overlap logic. For the race condition between guard check and createPending, use a Postgres function called via Supabase rpc() that performs the check and insert atomically: IF NOT EXISTS (overlap query) THEN INSERT RETURNING id ELSE RETURN NULL. This eliminates the TOCTOU vulnerability of a two-step check-then-insert.

In the Dart service layer, wrap createPending in a try/catch for PostgrestException with code '23505' (unique violation) as a secondary safety net. Use a try/finally pattern: createPending in the try block, the exporter pipeline in the try body, markFailed in the catch, markCompleted after the exporter call succeeds β€” the finally block should only be used for logging, not for DB writes, since you need to distinguish success from failure.

Testing Requirements

Unit tests (dart test) with mocked DoubleExportGuard and ExportRunRepository: (1) Guard returns existing run β†’ triggerExport returns ExportResult.duplicate(existingRunId), createPending never called. (2) Guard returns null (no duplicate) β†’ createPending called with correct orgId/dateRange. (3) Exporter throws exception after createPending β†’ markFailed called with errorCode, markCompleted never called. (4) Full success path β†’ markCompleted called with correct fileUrl and recordCount.

(5) Non-overlapping date range after a completed export β†’ guard returns null, new run created. Integration test with Supabase local emulator: (6) Two concurrent triggerExport calls for same org/dateRange β€” assert only one ExportRun row created (race condition test using Future.wait). (7) Verify export_runs table row after success contains all required fields. Test coverage requirement: 100% branch coverage on the guard integration and persistence paths within AccountingExporterServiceImpl.

Component
Accounting Exporter Service
service high
Epic Risks (3)
high impact medium prob technical

The Edge Function may exceed Supabase's execution time limit (default 150 seconds, but effectively constrained by the 10-second client SLA) when processing large batches of claims with complex chart-of-accounts mapping, causing the export to fail after partial processing.

Mitigation & Contingency

Mitigation: Implement the export pipeline with early termination on timeout and an in-progress export run status. Add a benchmark test in CI that runs the full pipeline against 500 claims and fails if it exceeds 8 seconds. Optimize the approved claims query with indexes on status, org_id, and date fields.

Contingency: If performance targets cannot be met synchronously, convert the Edge Function to an async job pattern: the function queues the export and returns a job ID immediately; the client polls a status endpoint and downloads the file when ready. This requires a job queue table and a polling UI state.

high impact medium prob security

Supabase Vault access from the Edge Function may require specific service role key configuration that differs between staging and production environments, causing credential retrieval to fail silently and producing export runs that appear successful but have no valid accounting system target.

Mitigation & Contingency

Mitigation: Test Vault read access in the Edge Function in staging before implementing any business logic. Add an explicit credential validation step at Edge Function startup that fails fast with a clear error if Vault is unreachable or the secret is missing.

Contingency: If Vault access fails in production, fall back to environment variable-based credentials temporarily (never returned to client) while the Vault configuration is corrected. Alert on-call via a monitoring rule that fires if credential retrieval fails.

medium impact low prob technical

AccountingExporter Service may become tightly coupled to specific exporter implementations if the factory pattern is not implemented cleanly, making it difficult to add a third exporter in the future without modifying the orchestrator.

Mitigation & Contingency

Mitigation: Define an AccountingExporter abstract class with a strict interface contract before implementing any concrete class. Use a registry pattern (Map<orgType, AccountingExporter>) in the factory rather than conditionals. Code review should verify no concrete class is imported directly in the orchestrator.

Contingency: If tight coupling is discovered after implementation, refactor the factory before the Edge Function epic ships so the interface is stable before any external callers are wired in.