critical priority medium complexity database pending database specialist Tier 0

Acceptance Criteria

Migration creates bufdir_category_mappings table with columns: id (UUID PK), internal_type_id (UUID NOT NULL), bufdir_code (VARCHAR NOT NULL), mapping_version (INT NOT NULL), effective_from (DATE NOT NULL), effective_to (DATE nullable), description (TEXT), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ)
UNIQUE constraint enforced on (internal_type_id, mapping_version)
Migration is idempotent — running it twice does not produce errors
Seed data covers all activity types for NHF, Blindeforbundet, HLF, and Barnekreftforeningen with at least mapping_version = 1
Migration rollback (down migration) drops the table and all seed data cleanly
RLS policy is added: only authenticated users belonging to the correct org can read rows; only service_role can insert/update/delete
All internal_type_id values in seed data reference valid rows in the activity_types table (FK or documented assumption)
Migration file is named with a timestamp prefix and placed in the Supabase migrations directory
Index on (bufdir_code, mapping_version) exists for lookup performance during report generation

Technical Requirements

frameworks
Supabase Migrations (SQL)
apis
Supabase CLI (supabase migration new, supabase db push)
data models
bufdir_category_mappings
activity_types (referenced)
performance requirements
Lookup query SELECT bufdir_code FROM bufdir_category_mappings WHERE internal_type_id = $1 AND mapping_version = $2 must execute in under 2ms with index
Seed data insert must complete within 5 seconds for up to 200 seed rows
security requirements
RLS must prevent app-level users from modifying mapping definitions
bufdir_code values must be validated against a known enum or CHECK constraint if Bufdir taxonomy is finite
Migration must not expose internal_type_id values in error messages

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Coordinate with the product team or Bufdir documentation to confirm the complete list of official bufdir_codes before writing seed data — incorrect codes will cause Bufdir submission rejections. Use `ON CONFLICT DO NOTHING` in the seed INSERT statements to make seeding idempotent. If the Bufdir taxonomy is expected to evolve, consider a CHECK constraint that can be updated rather than an ENUM type (which requires DDL changes to extend). effective_to = NULL means 'currently active'.

The mapping_version column should be a simple incrementing integer per org/taxonomy cycle, not a semver string, to keep comparisons simple in the RPC function. Document each seed row with a comment explaining which org uses that activity type.

Testing Requirements

SQL-level tests: (1) verify UNIQUE constraint rejects duplicate (internal_type_id, mapping_version) pairs, (2) verify RLS blocks non-service_role writes, (3) verify seed data is complete and all bufdir_codes are non-null. Run `supabase db reset` in CI to confirm migration applies cleanly from scratch. Verify rollback migration restores the database to pre-migration state. Test that a JOIN between activities and bufdir_category_mappings on internal_type_id returns the correct bufdir_code for each org's activity types.

Component
Activity Category Mapping Configuration
infrastructure low
Epic Risks (3)
high impact medium prob security

Supabase RLS policies may not propagate correctly into RPC function execution context, causing org-scoping predicates to be silently ignored when the function is invoked with service_role key. This could lead to cross-org data exposure in production without any obvious error.

Mitigation & Contingency

Mitigation: Invoke all RPCs using the anon/authenticated key rather than service_role, write explicit WHERE org_id = auth.uid()::org_id predicates inside the RPC body as a secondary control, and include automated cross-org leakage tests in the CI pipeline from day one.

Contingency: If RLS bypass is discovered post-deployment, immediately revoke service_role usage in all aggregation paths and hotfix with explicit org_id parameters passed as function arguments validated server-side.

high impact medium prob dependency

Bufdir may update its official reporting category taxonomy between the mapping configuration being defined and the annual submission deadline. If the ActivityCategoryMappingConfig is compiled as a static Dart constant, it cannot be updated without an app release, potentially causing mapping failures that block submission.

Mitigation & Contingency

Mitigation: Store the mapping as a remote-configurable table (bufdir_category_mappings) in Supabase with a version field rather than as a hardcoded Dart constant. Fetch the current mapping at aggregation time so updates can be pushed without a new app release.

Contingency: If a mapping mismatch is detected during an active reporting cycle, coordinators can be temporarily directed to the manual Excel fallback while an emergency mapping update is pushed to the Supabase table.

high impact low prob technical

For large organisations like NHF with 1,400 local chapters and potentially tens of thousands of activity records per reporting period, the Supabase RPC aggregation query may exceed the default PostgREST statement timeout, causing the aggregation to fail with a 503 error.

Mitigation & Contingency

Mitigation: Add partial indexes on (organization_id, created_at) and (organization_id, activity_type_id) to the activities table before writing the RPC. Profile the query plan against a realistic fixture of 50,000 records during development and increase the statement_timeout setting for the RPC role if needed.

Contingency: Implement chunked aggregation fallback: split the period into monthly sub-ranges and aggregate each chunk client-side, merging results with UNION-style Dart logic before assembling the final payload.