critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

An AccountingExporterFactory class is created with a single method: Future<ConcreteExporter> resolve(String orgId) that returns the correct exporter instance
The factory reads OrgAccountingConfig from the database (via a repository) using the orgId; no hardcoded org-to-system mappings exist
When accountingSystem == 'xledger', the factory returns an XledgerExporter instance
When accountingSystem == 'dynamics', the factory returns a MicrosoftDynamicsExporter instance
When accountingSystem is null, empty, or an unrecognised value, the factory does not throw but returns null or a Result type, and the orchestrator maps this to ExportResult.failure(errorCode: 'UNSUPPORTED_ACCOUNTING_SYSTEM', errorDetail: ...)
When OrgAccountingConfig does not exist for the given orgId, the factory returns a result that maps to ExportResult.failure(errorCode: 'ORG_CONFIG_NOT_FOUND')
The AccountingExporterService.triggerExport() implementation calls the factory before creating any ExportRun record — a config error must not leave orphaned pending ExportRun rows
All concrete exporter classes implement a common ConcreteExporter abstract interface with an export(List<ApprovedClaim> claims, AccountingCredentials credentials) method
Switching an org's accounting system configuration in the database immediately takes effect on the next triggerExport call — no caching of exporter instances across calls
Unit test with mocked repository verifies each routing branch independently

Technical Requirements

frameworks
Dart
Supabase Dart client (for OrgAccountingConfig fetch)
apis
Supabase PostgREST (read org_accounting_configs table)
data models
OrgAccountingConfig
ConcreteExporter (interface)
XledgerExporter
MicrosoftDynamicsExporter
performance requirements
OrgAccountingConfig fetch should use a Supabase query with .eq('org_id', orgId).single() and resolve in under 500ms
Factory resolution adds no more than one database round-trip to the overall pipeline
security requirements
OrgAccountingConfig must be fetched using the authenticated service context — Row Level Security must prevent cross-org config reads
The accountingSystem value from the database must be compared using a case-insensitive, trimmed comparison to prevent misconfiguration from being silently ignored

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use the Strategy pattern: AccountingExporterFactory is injected into AccountingExporterServiceImpl via constructor, keeping the service decoupled from concrete exporter types. The factory's resolve() method should return a Result (using a custom sealed class or package:result_dart) rather than throwing, so the orchestrator can map errors to ExportResult uniformly. Keep OrgAccountingConfig as a simple Dart class with orgId, accountingSystem (String?), and configuredAt (DateTime) — do not add Xledger-specific or Dynamics-specific credential fields here; those live in AccountingCredentialsVault. Register XledgerExporter and MicrosoftDynamicsExporter as injectable singletons (using get_it or Riverpod providers) so the factory can retrieve them without instantiating new objects per call — this also simplifies mocking in tests.

Testing Requirements

Unit tests (dart test) with mocked OrgAccountingConfigRepository: (1) accountingSystem='xledger' → resolves XledgerExporter. (2) accountingSystem='dynamics' → resolves MicrosoftDynamicsExporter. (3) accountingSystem='XLEDGER' (uppercase) → resolves XledgerExporter (case-insensitive). (4) accountingSystem=null → triggerExport returns ExportResult with status=failed and errorCode='UNSUPPORTED_ACCOUNTING_SYSTEM'.

(5) accountingSystem='sap' (unsupported) → same failure result. (6) OrgAccountingConfig not found → ExportResult with errorCode='ORG_CONFIG_NOT_FOUND'. (7) Verify no ExportRun record is created in cases 4, 5, 6 (mock ExportRunRepository and assert create() is never called). All branches covered, 100% on factory class.

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.