Implement audit read-receipt write to Supabase
epic-contact-detail-and-edit-service-layer-task-010 — Implement the core method in ReadReceiptService that writes a read-receipt record to the Supabase read_receipts table. The record must contain the ISO 8601 timestamp, authenticated user ID, contact ID, and the specific field key that was revealed. Use the Supabase client to insert the row and handle the resulting confirmation or error.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Guard at the top of recordFieldReveal: final user = supabase.auth.currentUser; if (user == null) { emit(ReadReceiptError('Ikke innlogget. Logg inn pƄ nytt.')); return; }. Then emit ReadReceiptWriting() before the async call. Use a try/catch around the Supabase insert and cast the caught error to PostgrestException to extract the code for error mapping.
The read_receipts table schema should be: id (uuid, default gen_random_uuid()), user_id (uuid, not null, references auth.users), contact_id (uuid, not null), field_key (text, not null), revealed_at (timestamptz, not null). Index on (contact_id, user_id) for coordinator audit queries. Do not use .upsert() ā each field reveal is a discrete audit event and must not overwrite prior records. This is a legal compliance requirement under Norwegian privacy regulations (personopplysningsloven / GDPR Article 30).
Testing Requirements
Write integration tests using flutter_test with a mocked Supabase client: (1) verify the correct payload is sent to from('read_receipts').insert() including all four required fields, (2) verify revealed_at is a valid ISO 8601 UTC timestamp, (3) verify user_id matches auth.currentUser.id and was NOT passed by the caller, (4) verify state transitions: Idle ā Writing ā Confirmed on success, (5) verify state transitions: Idle ā Writing ā Error on Supabase failure, (6) verify error state is emitted immediately when auth.currentUser is null. Also write a schema validation test ensuring the insert payload matches the read_receipts table column names exactly.
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.
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.