Implement ActivityAttachmentRepository soft-delete
epic-document-attachments-foundation-task-005 — Add the softDeleteAttachment(attachmentId) method to ActivityAttachmentRepository. The method sets deleted_at to the current UTC timestamp for the specified record, verifying the caller's org_id matches the record's org_id before updating. Return a typed Result or throw a domain exception on org mismatch or record not found. Write unit tests using a mocked Supabase client.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use Supabase's `.update({'deleted_at': DateTime.now().toUtc().toIso8601String()}).eq('id', attachmentId).eq('org_id', orgId).select('id')` pattern and check that the returned list is non-empty to distinguish not-found from org-mismatch (or use `.count()` with `CountOption.exact`). Define the error hierarchy as a sealed class or freezed union: `AttachmentRepositoryError` with variants `notFound`, `orgMismatch`, `unexpected(Object error)`. Do NOT hard-delete the record — the physical file deletion in storage is handled by a separate flow. Ensure all active-query methods in the repository already use `.isNull('deleted_at')` (added in task-004); if not, add that filter guard here.
Keep the method signature consistent with the rest of the repository (async, returns `Future
Testing Requirements
Unit tests only (flutter_test). Use mockito or mocktail to mock the SupabaseClient. Test cases: (1) happy path — update is called with correct eq filters and deleted_at value; (2) org mismatch — Supabase returns 0 updated rows for org filter mismatch, repository throws/returns OrgMismatchException; (3) record not found — Supabase returns 0 rows, repository throws/returns AttachmentNotFoundError; (4) active-query exclusion — getAttachmentsForActivity query includes .isNull('deleted_at') filter and is verified by checking query builder arguments. Target 100% branch coverage for the softDeleteAttachment method.
Supabase RLS policies may not cover all query paths (e.g., service-role key usage in edge functions), potentially exposing attachment metadata or objects from another organisation to an unauthorised actor, breaching GDPR requirements.
Mitigation & Contingency
Mitigation: Add org_id scoping as an explicit WHERE clause at the Dart repository level as a second line of defence. Document which queries use the anon key versus service-role key, and audit all edge function calls that touch the storage bucket.
Contingency: If a bypass is discovered post-deployment, immediately revoke the affected signed URLs, rotate the service-role key, add the missing org_id filter, and deploy a patch. Notify affected organisations per GDPR breach protocol.
Supabase free/pro tier storage quotas may be exceeded earlier than expected if organisations upload large PDFs frequently, causing upload failures with no graceful degradation for users.
Mitigation & Contingency
Mitigation: Configure a 10 MB per-file cap enforced in the upload service (Epic 2), and add a storage usage monitoring alert at 80% of the allocated quota. Document the upgrade path in runbooks.
Contingency: If the quota is hit, temporarily disable new uploads via the org-level feature flag (attachments_enabled) and upgrade the Supabase plan. Communicate clearly to affected coordinators with an estimated restoration time.
The feature documentation specifies a migration order dependency: the activity_attachments table must be created after the activities table and before the Bufdir export join query is updated. Running migrations out of order will cause foreign-key or join failures.
Mitigation & Contingency
Mitigation: Add the migration to the numbered Supabase migration sequence immediately after the activities table migration. Add a CI check that runs migrations in order against a clean schema.
Contingency: If a deployment runs migrations out of order, roll back via the Supabase migration rollback script, reorder, and redeploy. No data loss occurs as attachments do not exist yet at that point.