critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ActivityTypeRepository is defined as an abstract interface with a concrete SupabaseActivityTypeRepository implementation to allow test mocking
fetchAll(orgId) queries activity_types with .eq('org_id', orgId) and .eq('is_active', true) and returns List<ActivityType> ordered by name ascending
fetchById(id) queries by primary key with org_id scoping and returns ActivityType or throws ActivityTypeNotFound if no row is returned
create(activityType) inserts a new row and returns the server-assigned ActivityType (including generated id and timestamps)
update(activityType) performs a partial update via .update() scoped to id and org_id, returns the updated ActivityType
softDelete(id) sets is_active = false scoped to org_id — no hard DELETE is performed on the activity_types table
Every method catches PostgrestException and maps to the appropriate ActivityTypeError subtype: 42501 (permission denied) → ActivityTypePermissionDenied, PGRST116 (no rows) → ActivityTypeNotFound, all others → ActivityTypeFetchFailure
Every query includes .eq('org_id', orgId) — no query ever touches rows belonging to another organisation
Repository is injected via a Riverpod Provider so consumers never instantiate it directly
All methods return either the success type or throw a typed ActivityTypeError — no raw PostgrestException escapes the repository boundary

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase REST API — activity_types table (SELECT, INSERT, UPDATE with is_active=false for soft delete)
data models
ActivityType
ActivityTypeError
performance requirements
fetchAll must complete within 2 seconds on a standard mobile connection for up to 200 activity types
Use .select() with explicit column list rather than select('*') to minimise payload size
security requirements
org_id filter applied on every query without exception — enforced at repository level in addition to database RLS
No raw SQL or RPC calls that could bypass RLS
Repository must not expose the SupabaseClient directly — it is an internal implementation detail

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Define `abstract interface class ActivityTypeRepository` in lib/domain/activity_type/activity_type_repository.dart and the Supabase implementation in lib/data/activity_type/supabase_activity_type_repository.dart. This separation lets BLoC/Riverpod consumers depend on the interface, making unit testing trivial. Use Riverpod's riverpod_annotation or manual Provider: `final activityTypeRepositoryProvider = Provider((ref) => SupabaseActivityTypeRepository(ref.watch(supabaseClientProvider)));`. For error mapping, a private `_mapError(Object e, StackTrace st)` helper keeps each method clean.

Supabase returns an empty list (not an exception) for 'no rows found' on .select() — check for empty list explicitly in fetchById before deciding to throw ActivityTypeNotFound. For softDelete, use `.update({'is_active': false}).eq('id', id).eq('org_id', orgId)` — avoid calling delete() on the table to preserve audit history.

Testing Requirements

Unit tests with flutter_test using a MockSupabaseClient (mockito or mocktail): (1) fetchAll returns correct list when Supabase returns valid rows; (2) fetchAll returns empty list when no rows match; (3) fetchById returns ActivityType on hit; (4) fetchById throws ActivityTypeNotFound when Supabase returns empty list; (5) create returns inserted ActivityType with server-generated id; (6) update returns modified ActivityType; (7) softDelete sets is_active=false without deleting; (8) PostgrestException with code 42501 is mapped to ActivityTypePermissionDenied; (9) unexpected PostgrestException is mapped to ActivityTypeFetchFailure with original exception preserved. Integration test (optional, against local Supabase): full CRUD lifecycle on a test organisation. Aim for 90%+ line coverage on the repository implementation.

Component
Activity Type Repository
data medium
Epic Risks (3)
high impact medium prob technical

The JSONB metadata column has no enforced schema at the database level. If the Dart model and the stored JSON diverge (e.g., a field is renamed or a new required flag is added without a migration), the metadata resolver will silently return null or throw at parse time, breaking conditional wizard logic for all organisations.

Mitigation & Contingency

Mitigation: Define a versioned Dart Freezed model for ActivityTypeMetadata and add a Supabase check constraint or trigger that validates the JSONB structure on write. Document the canonical metadata schema in a shared constants file and require schema review for any metadata field additions.

Contingency: Implement a lenient parse path in ActivityTypeMetadataResolver that returns safe defaults for missing fields and logs a structured warning to Supabase edge logs, allowing the app to degrade gracefully rather than crash.

high impact low prob security

If RLS policies on the activity_types table are misconfigured, a coordinator from one organisation could read or mutate activity types belonging to another organisation, violating data isolation guarantees required by all three client organisations.

Mitigation & Contingency

Mitigation: Write integration tests against the Supabase local emulator that explicitly assert cross-org isolation: a token scoped to org A must receive zero rows when querying org B activity types, and upsert attempts must return permission-denied errors.

Contingency: Apply an emergency RLS policy patch via Supabase dashboard without a code deploy. Audit all activity_type rows for cross-org contamination and restore from backup if any data leakage is confirmed.

medium impact medium prob integration

If the cache invalidation call in ActivityTypeService is not reliably triggered after an admin creates, edits, or archives an activity type, peer mentors on the same device will see stale data in the registration wizard until the next app restart, leading to confusion and potential misregistrations.

Mitigation & Contingency

Mitigation: Enforce a strict pattern: ActivityTypeService always calls cacheProvider.invalidate() inside the same try block as the successful Supabase mutation, before returning to the caller. Write a widget test that verifies the cache notifier emits an updated list after a service mutation.

Contingency: Add a background Supabase Realtime subscription on the activity_types table that triggers cache invalidation automatically, providing an independent safety net independent of the service call path.