critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ContactDetailService reads organisation configuration (from a config repository or provider) and extracts the list of field keys that require encrypted display
The resolved list of encrypted field keys is included as a typed field (e.g., List<String> encryptedFieldKeys) in the ContactDetailLoaded state
If the organisation config contains no encrypted fields, encryptedFieldKeys is an empty list — never null
The encrypted field resolution logic lives entirely in the service layer; no UI component or widget contains any conditional logic based on organisation config
The list of encrypted field keys is derived at load time and does not require a separate user action to populate
When the organisation configuration changes (e.g., after a config refresh), the service reflects the updated encrypted fields on the next loadContact call
A user-facing notification/warning is NOT triggered by this logic — that concern belongs to the UI layer which reads the encryptedFieldKeys list
The service is testable with a mock organisation config that contains zero, one, or multiple encrypted field keys

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Riverpod (flutter_riverpod)
apis
Supabase REST API (organisation_config table or equivalent)
data models
OrganisationConfig
Contact
EncryptedFieldConfig
performance requirements
Organisation config should be read from a cached provider where possible to avoid an additional network call on every contact load
Config resolution must not block the parallel contact fetch — it should either be pre-cached or fetched in parallel with the contact data
security requirements
The encrypted field keys list must contain only field identifiers (e.g., 'ssn', 'diagnosis') — never the actual encrypted values
Organisation config must be fetched with the authenticated session and must not be accessible to unauthenticated users
Audit log or notification of sensitive field access (as required by GDPR and NHF guidelines) must be triggered by the UI reading the encryptedFieldKeys list — not suppressed here

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Fetch OrganisationConfig in parallel with the contact/activity/assignment fetches using Future.wait to avoid adding latency. If the config is already provided by a top-level Riverpod provider (likely, since org config is global), read it synchronously from the provider rather than making another network call. Define a type alias or small value class (e.g., EncryptedFieldKeys = List) for clarity. The field keys should match the actual database column names or JSON keys used in the Contact model so that the UI can do a simple set-membership check.

Document the expected format of the org config field (e.g., encrypted_fields: ['ssn', 'phone_number']) in a comment near the parsing logic.

Testing Requirements

Unit tests using flutter_test: (1) Verify that when org config specifies ['ssn', 'diagnosis'], ContactDetailLoaded.encryptedFieldKeys equals ['ssn', 'diagnosis']. (2) Verify that when org config has no encrypted field rules, encryptedFieldKeys is an empty list. (3) Verify that a null or missing org config results in an empty encryptedFieldKeys list, not a thrown exception. (4) Verify the config is resolved without blocking the contact profile fetch (use fake_async to check timing).

(5) Verify the service correctly uses the org config from the injected config repository, not a hardcoded source.

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.