critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

A public method (e.g., loadContact(String contactId)) triggers all three repository calls — contact profile, activity history, and assignment status — in parallel using Future.wait
ContactDetailLoading state is emitted immediately when loadContact is called, before any await completes
On success, all three data payloads are combined into a single ContactDetailLoaded state emission
If any single parallel fetch fails, a ContactDetailError state is emitted with the appropriate ContactDetailErrorType (networkFailure, notFound, or permissionDenied)
Repository layer errors are mapped to ContactDetailErrorType before emission — the UI never receives raw exceptions
The method is idempotent: calling loadContact while already loading cancels or debounces the in-flight request rather than creating duplicate fetches
ActivityHistory is ordered chronologically (most recent first) before being placed into state
The method accepts a contactId parameter and does not rely on global or ambient state to determine which contact to load

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Riverpod (flutter_riverpod)
apis
Supabase REST API (contacts table)
Supabase REST API (activity_history table)
Supabase REST API (assignments table)
data models
Contact
ActivityHistory
AssignmentStatus
performance requirements
All three repository calls must be fired concurrently — sequential awaiting is not acceptable
Total expected round-trip for parallel fetch must be bounded by the slowest single call, not the sum of all calls
Debounce or cancellation must prevent redundant network calls when the same contact is loaded in rapid succession
security requirements
contactId must be validated as a non-empty string before firing any network requests
Repository calls must include the authenticated Supabase session — never fetch without a valid session token

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Future.wait([profileFuture, historyFuture, assignmentFuture]) for true parallelism. Destructure the result list positionally. Wrap the entire Future.wait in a try/catch and inspect the exception type to determine which ContactDetailErrorType to emit. For idempotency in a BLoC, check if the current state is already ContactDetailLoading before emitting it again; in Riverpod, use a CancelToken or store the in-flight Future ref.

Do NOT use separate sequential awaits — this doubles latency unnecessarily. Map Supabase PostgrestException codes to ContactDetailErrorType: code '42501' or HTTP 403 → permissionDenied, empty result set → notFound, network/timeout → networkFailure. Sort activityHistory by timestamp descending before constructing the loaded state.

Testing Requirements

Unit tests using flutter_test with mocked repositories: (1) Verify loading state is emitted before any response arrives. (2) Verify ContactDetailLoaded is emitted when all three mocks return successfully. (3) Verify ContactDetailError with networkFailure is emitted when any repository throws a network exception. (4) Verify ContactDetailError with notFound is emitted when the contact repository returns null.

(5) Verify ContactDetailError with permissionDenied is emitted for 403 responses. (6) Verify that when loadContact is called twice rapidly, only one set of states is emitted for the second call (idempotency/debounce). Use fake_async or similar to control async timing in tests.

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.