critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

ActivityTypeRepository implements IActivityTypeRepository interface from task-001
fetchActiveByOrg(orgId) queries Supabase activity_types table with .eq('organization_id', orgId).eq('is_active', true).order('sort_order') and maps results to List<ActivityType>
fetchAll(orgId) queries all records (active and inactive) scoped to orgId, ordered by sort_order
fetchById(orgId, id) queries with both organization_id and id filters; returns null (not throws) when no record found
create(ActivityType) inserts a new row and returns the created ActivityType with server-assigned id and createdAt
update(ActivityType) updates mutable fields (labelKey, isActive, triggersReimbursementWorkflow, isTravelExpenseEligible, requiresReport, sortOrder) scoped to matching id and organization_id
softDelete(orgId, id) sets is_active = false rather than deleting the row; does not physically remove data
All methods throw a typed RepositoryException (not generic Exception) on Supabase errors, wrapping the original PostgrestException
All queries include .eq('organization_id', orgId) even though RLS enforces this — defense-in-depth requirement
Repository does not hold mutable state; each method creates a fresh Supabase query
Integration test: fetchActiveByOrg returns only is_active=true rows for the seeded orgId
Integration test: softDelete does not remove the row from the table

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgreSQL 15
Supabase Auth
data models
activity_type
performance requirements
fetchActiveByOrg must complete within 500ms on typical org dataset (< 50 activity types)
Use .select() with explicit column list to avoid over-fetching — do not use SELECT *
No N+1 queries — all methods use single Supabase query per call
security requirements
Every query must include .eq('organization_id', orgId) as application-layer guard complementing RLS
Repository must never expose the Supabase service role key — use anon/user JWT only
softDelete must verify the row's organization_id matches the caller's orgId before updating
No raw SQL string concatenation — use Supabase query builder exclusively to prevent injection

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Place in lib/features/activity_type/data/repositories/activity_type_repository.dart. Inject SupabaseClient via constructor (or Riverpod provider) — never instantiate Supabase.instance directly inside the repository for testability. The Supabase RLS policy on activity_types enforces org isolation server-side, but always include .eq('organization_id', orgId) in every query for defense-in-depth and explicit intent. Map the DB column is_active (boolean) to ActivityType.isActive and triggers_reimbursement_workflow to triggersReimbursementWorkflow — keep the fromJson mapping in the domain model (task-001), not in the repository.

For softDelete, use .update({'is_active': false}).eq('id', id).eq('organization_id', orgId) — the double-eq prevents cross-org soft-deletes even if RLS is misconfigured. Expose a Riverpod Provider returning ActivityTypeRepository so tests can swap in a fake implementation.

Testing Requirements

Integration tests using flutter_test with a test Supabase project (or local Supabase via Docker): (1) fetchActiveByOrg returns correct filtered list with only active types for the correct org, (2) fetchActiveByOrg for org with no active types returns empty list (not error), (3) fetchById returns null for non-existent id rather than throwing, (4) create inserts row and returned object has server-generated id and createdAt, (5) update persists changes and returns updated object, (6) softDelete sets is_active=false without removing row, (7) all methods throw RepositoryException (not PostgrestException directly) on DB error. Unit tests with mocked Supabase client covering error path: network failure wraps in RepositoryException.

Component
Activity Type Service
service medium
Epic Risks (2)
medium impact medium prob scope

Metadata flag combination rules differ between organisations (e.g., Blindeforbundet honorarium thresholds, HLF mutual exclusion of km and transit). Encoding these as generic service-level validation may be insufficient, forcing organisation-specific branching inside the service that becomes unmaintainable as new organisations are onboarded.

Mitigation & Contingency

Mitigation: Model flag validation as a pure function that accepts an ActivityTypeMetadata object and an org configuration record, making org-specific rules data-driven rather than hardcoded. Establish the validation contract in the foundation epic so the service just delegates to the validator.

Contingency: Defer complex cross-flag validation to a lightweight edge function that can be updated without a mobile app release, accepting that initial validation in the mobile service layer is permissive and corrected server-side.

high impact low prob technical

Blindeforbundet users rely on VoiceOver and JAWS. If the selection screen is built with non-semantic widgets that fail accessibility audit late in the sprint, a significant rework of the widget tree may be required, blocking the registration wizard integration.

Mitigation & Contingency

Mitigation: Build the selection screen against the project's established accessibility design tokens and semantics wrapper conventions from the start. Run Flutter's semantic tree inspector and a manual VoiceOver pass before marking any widget task complete.

Contingency: Wrap all tappable items in the project's SemanticsWrapperWidget and schedule a dedicated accessibility review session with a screen reader user from Blindeforbundet before the epic is closed.