high priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

All repository calls inside ContactDetailService are wrapped in retry logic with a maximum of 3 attempts
Retry delay follows exponential back-off: 1s after attempt 1, 2s after attempt 2, no further retry after attempt 3
Retry is only applied for transient errors (network timeout, connection reset) — it is NOT applied for notFound (404) or permissionDenied (403)
After exhausting all retries, a ContactDetailError state with errorType: networkFailure is emitted
A ContactDetailError with errorType: notFound is emitted immediately (no retry) when the contact does not exist
A ContactDetailError with errorType: permissionDenied is emitted immediately (no retry) when the user lacks access
The retry logic is implemented as a reusable utility function or extension, not duplicated across each repository call
During retries, the BLoC/Notifier remains in ContactDetailLoading state — no intermediate error state is emitted mid-retry
Error state emission includes a human-readable message string suitable for display (in English)

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Riverpod (flutter_riverpod)
apis
Supabase REST API
data models
ContactDetailErrorType
performance requirements
Maximum total retry delay must not exceed 3 seconds (1s + 2s) before final failure
Retry must not block the UI thread — all delays must use Future.delayed within async context
security requirements
Do not retry permissionDenied errors — retrying a 403 is wasted bandwidth and could indicate a session issue that should surface to the user immediately
Error messages exposed in state must not include raw server error details or stack traces — only typed, safe error types

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement a generic retryAsync helper: Future retryAsync(Future Function() fn, {int maxAttempts = 3, Duration baseDelay = const Duration(seconds: 1)}) that doubles the delay on each attempt. Use this helper to wrap each repository call. Before invoking retryAsync, classify the exception: if it's a Supabase PostgrestException with code indicating auth/permission or row not found, throw a non-retryable typed exception immediately. Only network/timeout exceptions enter the retry path.

Using fake_async in tests is critical here — do not use real delays. If the codebase already has a retry utility in utils/, reuse it rather than creating a duplicate.

Testing Requirements

Unit tests using flutter_test with fake_async: (1) Verify that a transient network error triggers exactly 3 total attempts before emitting ContactDetailError(networkFailure). (2) Verify that delays between retries are approximately 1s and 2s respectively. (3) Verify that a 404/notFound response emits ContactDetailError(notFound) on the first attempt with no retries. (4) Verify that a 403/permissionDenied response emits ContactDetailError(permissionDenied) immediately.

(5) Verify that a successful response on the second attempt (after one transient failure) emits ContactDetailLoaded without an error state. (6) Verify the loading state is maintained throughout the retry cycle.

Component
Contact Detail Service
service medium
Epic Risks (2)
medium impact medium prob technical

Parallel fetching of profile, activity history, and assignment status from contact-detail-service may produce race conditions where partial state is emitted to the UI before all fetches complete, resulting in flickering or incorrect loading indicators.

Mitigation & Contingency

Mitigation: Use Future.wait or a single composed BLoC event that only emits a loaded state once all three futures resolve. Define a strict state machine: initial → loading → loaded/error with no intermediate partial-loaded states emitted to the UI.

Contingency: If parallelism proves unreliable in testing, fall back to sequential fetching with a combined loading indicator. The 500ms target may need to be renegotiated with stakeholders if sequential fetching exceeds it on slow connections.

high impact low prob integration

The partial-field update pattern in contact-edit-service assumes the contact record has not changed between when the edit screen was loaded and when the save is submitted. Concurrent edits by another coordinator could cause the earlier editor's save to silently overwrite the later one.

Mitigation & Contingency

Mitigation: Include an updated_at timestamp in the PATCH request and configure Supabase to reject updates where the server-side timestamp differs from the client's version. Return a 409-equivalent error that the service maps to a user-readable conflict message.

Contingency: If optimistic locking is too complex for initial delivery, implement a simple 'reload and retry' flow: on save error, reload the contact detail and prompt the coordinator to re-apply their changes manually.