critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

LocationConsentService class implements grantConsent(mentorId, orgId, consentVersion) which upserts a consent_grants row, sets granted_at to now(), sets revoked_at to null, and writes a 'granted' audit event atomically
grantConsent throws ConsentVersionMismatchException if consentVersion does not match the current version from LocationPrivacyConfig
checkConsent(mentorId, orgId) returns ConsentStatus.granted when an active (non-revoked) consent row exists, ConsentStatus.notGranted when no row exists, and ConsentStatus.revoked when revoked_at IS NOT NULL
checkConsent always reads from the database — it does not cache results — to ensure revocation is immediately effective
revokeConsent(mentorId, orgId) sets revoked_at on the consent_grants row, writes a 'revoked' audit event, and calls delete_mentor_location_data() atomically
auditConsentEvent(event) writes a row to consent_audit_log, including computed ip_hash, and never throws for valid inputs
All three mutating operations (grantConsent, revokeConsent, and the audit write) execute within a Supabase RPC transaction — if the audit write fails, the grant/revoke is rolled back
LocationConsentService is provided as a Riverpod provider and depends on the Supabase client provider
A ConsentGatingMixin or ConsentGuard utility is provided that wraps any service method and calls checkConsent before proceeding, throwing ConsentRequiredException if status is not ConsentStatus.granted
Unit tests cover all ConsentStatus return paths and the ConsentVersionMismatchException case

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
BLoC (for UI state if consent dialog is needed)
apis
Supabase PostgREST (consent_grants, consent_audit_log)
Supabase RPC (delete_mentor_location_data)
data models
ConsentGrant
ConsentAuditLog
ConsentStatus (enum)
ConsentEvent
performance requirements
checkConsent completes in under 500ms on a normal mobile connection — it is a gating call in the hot path
grantConsent and revokeConsent complete in under 2 seconds end-to-end including audit write
checkConsent must NOT cache results — freshness is a correctness requirement, not just performance
security requirements
checkConsent must be called before every location data read or write — this is enforced by the ConsentGatingMixin pattern
consentVersion must be validated against the current version from LocationPrivacyConfig before granting — stale consent versions are rejected
ip_hash is computed in Dart using SHA-256 of the device's reported IP before being written to Supabase — raw IP never transmitted to database
revokeConsent must call delete_mentor_location_data atomically — partial revocation (consent revoked but location data retained) is a GDPR violation
Service methods must validate that the calling authenticated user's mentorId matches the provided mentorId parameter — no cross-mentor consent manipulation

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define a `ConsentStatus` enum with three values: `granted`, `notGranted`, `revoked`. The distinction between `notGranted` and `revoked` is important: `revoked` triggers a different UI message ('You previously consented but have withdrawn — re-grant to use the map') vs `notGranted` ('Enable location sharing to appear on the peer mentor map'). For atomicity of grant + audit write, implement as a Supabase RPC function `grant_consent(p_mentor_id, p_org_id, p_version, p_ip_hash)` that performs both writes in a PostgreSQL transaction — do not rely on two sequential Supabase client calls from Flutter, as network interruption between them can leave data in an inconsistent state. The ConsentGatingMixin should be implemented as a Dart mixin with a single method `Future withConsentCheck(String mentorId, String orgId, Future Function() operation)` — call `checkConsent` first, throw `ConsentRequiredException` if not granted, then call `operation()`.

This pattern makes it impossible to accidentally bypass consent checks in new location service methods. Expose `LocationConsentService` via a Riverpod `Provider` (not `AsyncNotifier`) since the service itself is synchronously constructible — only its async methods return futures.

Testing Requirements

Unit tests using flutter_test and mocktail for all service methods. Test cases: (1) grantConsent with matching version — verify upsert called with correct parameters and audit event written; (2) grantConsent with mismatched version — verify ConsentVersionMismatchException thrown and no database write attempted; (3) checkConsent when active row exists — verify returns ConsentStatus.granted; (4) checkConsent when no row exists — verify returns ConsentStatus.notGranted; (5) checkConsent when revoked_at IS NOT NULL — verify returns ConsentStatus.revoked; (6) revokeConsent — verify revoked_at set, audit event written, and delete_mentor_location_data RPC called; (7) Transaction rollback simulation — mock the audit write to throw, verify the grant/revoke was also rolled back (i.e., the mock upsert was called but the overall operation throws); (8) ConsentGatingMixin — verify a wrapped method is blocked with ConsentRequiredException when checkConsent returns notGranted. Integration tests (separate) should verify the actual RLS enforcement using a local Supabase instance.

Component
Location Consent Service
service medium
Epic Risks (2)
medium impact medium prob scope

If the privacy policy text or consent terms change after mentors have already opted in, existing consent records may become legally insufficient, requiring re-consent from all opted-in mentors which could temporarily reduce map coverage.

Mitigation & Contingency

Mitigation: Store a consent_version field on every consent record. Implement a consent version check in location-consent-service that compares the stored version against the current policy version from location-privacy-config and flags stale consents for re-consent prompting.

Contingency: If a policy update invalidates existing consents, suppress affected mentors from the map, queue them for re-consent notification via the existing in-app notification system, and restore map visibility only after new consent is recorded.

medium impact medium prob scope

A poorly designed consent dialog may lead to low opt-in rates, reducing map utility for coordinators to the point where the feature delivers insufficient value to justify maintenance cost.

Mitigation & Contingency

Mitigation: Follow plain-language writing guidelines from the cognitive accessibility feature. User-test the dialog with 2-3 peer mentors from Blindeforbundet before implementation is finalised. Ensure the dialog explains the benefit to the mentor, not just the data collection facts.

Contingency: If opt-in rate after launch is below 40%, conduct a targeted usability study and iterate on dialog copy and layout. The coordinator can also send a bulk opt-in invitation notification (per the user story) to non-consenting mentors.