critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ApprovedClaimsQueryService.fetchApprovedClaims(orgId, dateRange) is called after exporter resolution and before any external API call
Only claims with status='approved' AND exported_at IS NULL are returned; previously exported claims are excluded
ChartOfAccountsMapper.map(List<ApprovedClaim>) is called on the raw claims list before the result is passed to the concrete exporter
The mapper returns a List<MappedClaim> where each MappedClaim contains the original claim fields plus accountCode and costCenter resolved from the org's chart of accounts
If ChartOfAccountsMapper cannot resolve an account code for a claim (unknown activity type), the pipeline returns ExportResult.failure(errorCode: 'CHART_OF_ACCOUNTS_MAPPING_ERROR') and the unmapped claim is included in errorDetail
If ApprovedClaimsQueryService returns an empty list, the pipeline short-circuits with ExportResult.empty() and no mapper or exporter call occurs
No raw ApprovedClaim objects (pre-mapping) are ever passed to the concrete exporter — typing must enforce this
The MappedClaim type is null-safe: accountCode and costCenter are non-nullable Strings; compilation fails if null is assigned
ApprovedClaimsQueryService and ChartOfAccountsMapper are injected dependencies, not instantiated inside the service
All Supabase queries use parameterised inputs (no string interpolation of user-supplied values in query filters)

Technical Requirements

frameworks
Dart
Supabase Dart client
apis
Supabase PostgREST (expense_claims table read)
ChartOfAccountsMapper (internal service, reads chart_of_accounts table)
data models
ApprovedClaim
MappedClaim
ChartOfAccountsEntry
DateRange
performance requirements
ApprovedClaimsQueryService must use a single Supabase query with compound filters (.eq + .gte + .lte + .is_) rather than client-side filtering
ChartOfAccountsMapper should preload the full chart of accounts for the org in one query and resolve all claims in memory — avoid N+1 queries
Claims fetch must complete in under 3 seconds for up to 500 claims
security requirements
Claims query must filter by orgId using a Supabase RLS policy — do not rely solely on application-level orgId filtering
MappedClaim must not include sensitive personal data beyond what is required for accounting export (apply data minimisation)
Chart of accounts data is org-specific; the mapper must never return entries from a different org's configuration

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define MappedClaim as a distinct Dart class that extends or wraps ApprovedClaim — not a type alias — so the type system prevents passing unmapped claims to the exporter. Use Dart's extension types (Dart 3.3+) for zero-overhead wrapping if available, otherwise use a simple immutable wrapper class. The ChartOfAccountsMapper preloads the chart as a Map keyed by activityType for O(1) lookups during mapping. For the mapping failure path, collect ALL unmapped claims before returning the failure (not fail-fast on the first) so the operator can fix all mapping gaps in one go.

Document the Supabase query used in ApprovedClaimsQueryService as a code comment (the exact .from().select().eq().gte().lte().is_() chain) to make it auditable. Use const constructors for MappedClaim fields that are known at compile time to improve performance in test suites with large fixture sets.

Testing Requirements

Unit tests (dart test) with mocked ApprovedClaimsQueryService and ChartOfAccountsMapper: (1) Normal path — 10 claims returned, all mapped, passed to exporter. (2) Empty claims — fetchApprovedClaims returns [], assert ExportResult.empty() and mapper/exporter never called. (3) Mapping failure for one claim — assert ExportResult.failure() with errorCode='CHART_OF_ACCOUNTS_MAPPING_ERROR' and failing claim ID in errorDetail. (4) Verify typed constraint — attempt to pass an ApprovedClaim directly to the exporter method and confirm compilation fails (type test).

Integration test with Supabase local emulator: seed expense_claims with mixed statuses, assert only approved+unexported claims are returned. Mapper integration test: seed chart_of_accounts table, verify mapping resolves correct accountCode per activity type.

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.