critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

revokeConsent(mentorId, orgId) atomically updates consent_grants.status to 'revoked' and hard-deletes all rows in mentor_locations for the given mentorId+orgId within a single database transaction
If the delete of location rows fails, the entire transaction rolls back and the consent record remains in its previous state — no partial deletions allowed
A GDPR deletion audit event is inserted into consent_audit_log with fields: event_type='consent_revoked', mentor_id, org_id, revoked_at (UTC timestamp), rows_deleted (integer count), and initiated_by (actor user_id)
The method returns a typed RevokeConsentResult containing success boolean, rows_deleted count, and audit_event_id before the call returns
Revocation is irreversible: no re-grant path reuses the same consent record; a new consent_grants row must be created if the mentor opts in again later
The method rejects if mentorId does not match the calling user's JWT sub or if orgId does not match the user's organization claim, returning a 403 error
Supabase RLS prevents any client from directly deleting mentor_locations; revocation must be executed exclusively through this service method (Edge Function or RPC with service role)
Calling revokeConsent when no active consent exists returns a typed error ConsentAlreadyRevokedException without writing any audit event
Integration test confirms that after successful revocation, a SELECT on mentor_locations for that mentorId+orgId returns zero rows
The GDPR deletion audit log entry is immutable: no UPDATE or DELETE on consent_audit_log is permitted for this record type post-creation

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Edge Functions (Deno)
apis
Supabase PostgreSQL 15 RPC or Edge Function
Supabase Auth JWT validation
data models
consent_grants
mentor_locations
consent_audit_log
performance requirements
Full transaction (revoke + delete + audit insert) must complete within 500ms under normal Supabase load
mentor_locations delete must use indexed lookup on (mentor_id, org_id) — confirm composite index exists before implementation
No N+1 queries: delete all location rows in a single DELETE WHERE statement, not row-by-row
security requirements
Service role key used only inside Edge Function / RPC — never exposed to Flutter client
JWT claims validated before any mutation: sub must equal mentorId, org claim must equal orgId
RLS on mentor_locations must block direct client DELETE; only service-role context may delete
Audit log rows must have an INSERT-only RLS policy — no role may UPDATE or DELETE audit entries
All database writes use parameterized queries — no string interpolation of mentorId/orgId
GDPR compliance: deletion audit record retained for minimum 5 years per Norwegian data retention law

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement as a Supabase Edge Function (Deno) or PostgreSQL RPC function using BEGIN/COMMIT to wrap the three operations atomically. Use RETURNING count(*) from the DELETE to capture rows_deleted without a separate SELECT. Store the function behind a POST endpoint that accepts { mentorId, orgId } in the body; validate these against JWT claims at the top of the handler before touching the database. For the audit log insert, use a separate INSERT after the DELETE inside the same transaction — if the audit insert fails, the whole transaction rolls back, which is the correct GDPR behavior (deletion without audit is worse than no deletion).

Define a typed Dart class RevokeConsentResult and a sealed exception hierarchy (ConsentAlreadyRevokedException, ConsentUnauthorizedException) for clean error handling on the Flutter side. Avoid using Supabase's soft-delete patterns here — GDPR Article 17 requires hard deletion of personal data.

Testing Requirements

Unit tests: mock Supabase client to verify transaction structure (revoke + delete + audit in one tx), verify rollback on simulated delete failure, verify 403 on mismatched JWT claims, verify ConsentAlreadyRevokedException on already-revoked consent. Integration tests against a local Supabase instance: seed mentor_locations with 5 rows, call revokeConsent, assert zero rows remain and audit log contains correct metadata. Test all three organization roles (mentor, coordinator, admin) to confirm only the owning mentor can invoke revocation. Test concurrent invocations to verify no race condition leaves partial data.

Coverage target: 90%+ for revokeConsent and its transaction helper.

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.