critical priority medium complexity integration pending integration specialist Tier 3

Acceptance Criteria

ExportEdgeFunctionClient class wraps supabase.functions.invoke('export-accounting') with correct request payload: { orgId, startDate, endDate, exporterType }
Dates are serialized as ISO 8601 UTC strings in the request payload
Client returns a typed ExportResult(exportRunId: String, fileUrl: String) on HTTP 200 response
Client maps HTTP 401/403 responses to AuthError with message 'Not authorised to export for this organisation'
Client maps HTTP 409 responses to DuplicateError carrying the existingRunId from the response body
Client maps network timeouts and connection errors to NetworkError
Retry logic: on NetworkError, retry up to 3 times with exponential back-off starting at 1s (1s, 2s, 4s); do NOT retry on AuthError or DuplicateError
Progress stream emits ExportProgress.started on first attempt, ExportProgress.retrying(attempt) on retry, ExportProgress.completed on success
BLoC listens to the progress stream and emits Exporting state with progress info for UI consumption
Client is injected into the BLoC via constructor — not instantiated inside the BLoC — enabling test mocking
All network calls include the Supabase JWT authorization header automatically via the supabase_flutter SDK session
Client correctly handles response body parsing failures with a descriptive ParseError

Technical Requirements

frameworks
Flutter
BLoC
Supabase Flutter SDK (supabase_flutter ^2.x)
apis
Supabase Edge Functions invoke API
Xledger REST API (invoked server-side in Edge Function)
Microsoft Dynamics 365 REST API (invoked server-side in Edge Function)
data models
annual_summary
bufdir_export_audit_log
claim_event
performance requirements
Total client timeout (including retries) must not exceed 45 seconds — Edge Functions can take up to 30s for large exports
Set per-attempt HTTP timeout to 30 seconds
Progress stream must emit within 500ms of each state change
security requirements
orgId must be derived from the server-side JWT claims inside the Edge Function — the client-supplied orgId in the payload is used only for routing, not for authorisation
Supabase service role key is never present in the Flutter client — the Edge Function uses it server-side only
Network calls use TLS only (enforced by Supabase SDK defaults)
Never log the full response body if it contains file URLs — log only exportRunId for audit purposes

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use the supabase_flutter SDK's client.functions.invoke() method — it automatically injects the current session's JWT as the Authorization header. Define the retry logic in a standalone retryWithBackoff() utility function (not inside the client class) for reusability. Use Stream.fromFuture() combined with a StreamController to expose the progress stream — emit events before and after each attempt. The ExportResult, AuthError, DuplicateError, NetworkError, and ParseError should all be part of a sealed Result pattern or a custom Either type for clean error handling in the BLoC.

Parse the Edge Function response body as JSON using dart:convert — validate that required fields (exportRunId, fileUrl) are present before constructing ExportResult. The exponential back-off must use Future.delayed() — verify with fake_async in tests to avoid slow tests.

Testing Requirements

Unit tests: mock supabase.functions.invoke using mockito/mocktail. Test success path returns ExportResult. Test 401 response maps to AuthError. Test 409 response maps to DuplicateError with correct existingRunId extracted from response JSON.

Test NetworkError triggers retry exactly 3 times with correct back-off delays (use fake_async to control time). Test AuthError does NOT trigger retry. Test ParseError on malformed response JSON. Integration test (staging environment only): invoke the actual Edge Function with a test org and verify exportRunId is returned.

BLoC integration test: verify BLoC emits Exporting state when progress stream emits, and ExportSuccess on client success. Target 90%+ unit test coverage on client logic.

Component
Accounting Export Screen
ui medium
Epic Risks (2)
medium impact medium prob technical

Export operations may take several seconds, and the UI must handle all intermediate states (loading, partial success, failure, duplicate warning) without leaving the coordinator on a blank or unresponsive screen. Missing state handling causes confusion and potentially double-submissions.

Mitigation & Contingency

Mitigation: Design the BLoC state machine with explicit states for each transition before writing any widget code: ExportIdle, ExportDuplicateWarning, ExportInProgress, ExportSuccess, ExportPartialSuccess, ExportFailed. Each state maps to a distinct UI. Widget tests cover all states.

Contingency: If a loading state is missed in production, surface a generic error state with a retry action rather than leaving the UI stuck. Add a timeout on the Edge Function call (default 30 seconds) that transitions to ExportFailed with a user-readable message.

high impact medium prob technical

The custom Export Date Range Picker may not be fully navigable with VoiceOver if the underlying Flutter date widgets do not expose the correct semantic tree. This is a critical accessibility failure for Blindeforbundet users who rely on screen readers.

Mitigation & Contingency

Mitigation: Use Flutter's built-in DateRangePicker as the base and wrap with explicit Semantics nodes for start and end labels. Test with VoiceOver on a physical iOS device as part of the definition of done for this component. Reference the existing AccessibilityTestHarness pattern used elsewhere in the app.

Contingency: If the custom picker fails accessibility audit, replace it with two independent DatePicker fields (start and end) using Flutter's standard accessible date input, which has broader VoiceOver support than range variants.