critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

recordFieldReveal(contactId, fieldKey) inserts a row into the read_receipts table with: revealed_at (ISO 8601 UTC), user_id (from Supabase auth.currentUser.id), contact_id, field_key
The service transitions to ReadReceiptWriting immediately before the Supabase insert call
On successful insert, the service transitions to ReadReceiptConfirmed(fieldKey: fieldKey)
On Supabase error, the service transitions to ReadReceiptError with a Norwegian user-readable message
The authenticated user_id is obtained from the Supabase auth session — never passed as a parameter (prevents impersonation)
revealed_at is generated as DateTime.now().toUtc().toIso8601String() inside the service, not by the caller
RLS policy on read_receipts table allows the authenticated user to insert rows where user_id = auth.uid() (policy definition documented in a comment)
The insert does not attempt an upsert — each reveal creates a new audit record (no deduplication)
Integration test with a mocked Supabase client verifies the correct payload shape is sent

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
apis
Supabase client.from('read_receipts').insert({...})
Supabase auth.currentUser for authenticated user_id
data models
ReadReceiptRecord (revealed_at, user_id, contact_id, field_key)
ReadReceiptState
performance requirements
Insert must complete within 3 seconds on a standard mobile connection
Do not await the insert result before updating UI state to Writing — emit Writing state synchronously
security requirements
user_id must always come from auth.currentUser.id — never from a method parameter
Supabase RLS must enforce that only the authenticated user can insert their own receipts (auth.uid() = user_id)
No field_value (the actual sensitive data) is ever stored in read_receipts — only the field_key identifier
If auth.currentUser is null, throw an unauthenticated error and emit ReadReceiptError immediately without calling Supabase

Execution Context

Execution Tier
Tier 1

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.

Component
Read Receipt 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.