critical priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

OrgAdminScope model contains: adminUserId (String), organisationId (String), subtreeIds (List<String>), role (AdminRole enum with values orgAdmin, superAdmin), and grantedAt (DateTime)
AdminUser model contains: id (String), email (String), displayName (String), role (AdminRole), organisationId (String), isActive (bool), createdAt (DateTime), and lastLoginAt (DateTime?)
AdminActivity model contains: id (String), title (String), activityType (String), organisationId (String), createdByUserId (String), occurredAt (DateTime), durationMinutes (int), participantCount (int), and attachments (List<String>)
AdminReimbursement model contains: id (String), userId (String), organisationId (String), amount (double), currency (String, default 'NOK'), status (ReimbursementStatus enum), submittedAt (DateTime), approvedAt (DateTime?), approvedByUserId (String?), and expenseItems (List<ExpenseItem>)
AdminOrganisation model contains: id (String), name (String), parentOrganisationId (String?), orgType (String), isActive (bool), and memberCount (int)
OrgSubtree model contains: rootOrganisationId (String) and subtreeOrganisationIds (List<String>), with a contains(String orgId) helper method
All models implement fromJson(Map<String, dynamic>) factory constructor and toJson() method for Supabase response deserialization
All models override == and hashCode using all fields (or use Dart's built-in record equality or the equatable package consistently with the rest of the codebase)
All models implement copyWith({...}) method supporting partial updates with all fields optional
All models are immutable — all fields are final
No model imports Flutter framework packages — these are pure Dart domain models usable in non-Flutter contexts
Unit tests verify fromJson round-trips (fromJson(model.toJson()) == model) for all six models
Unit tests verify copyWith changes only the specified field and leaves all others unchanged
Models are placed in lib/domain/admin/ following the project's existing domain layer conventions

Technical Requirements

frameworks
Dart (latest stable)
equatable (if used elsewhere in project) OR Dart records equality
apis
Supabase REST API response shapes (snake_case JSON keys)
data models
OrgAdminScope
AdminUser
AdminActivity
AdminReimbursement
AdminOrganisation
OrgSubtree
ExpenseItem
AdminRole (enum)
ReimbursementStatus (enum)
performance requirements
fromJson must complete in under 1ms for single model deserialization
Models must support list deserialization of 500+ items without noticeable lag (simple Dart, no heavy computation)
security requirements
Models must not store raw JWT tokens or authentication secrets
Sensitive personal data fields (email, displayName) must be documented with a comment indicating they are PII-classified
OrgAdminScope.subtreeIds must be treated as authoritative scope boundary — no model should allow cross-scope data mixing

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Follow the existing domain model pattern in the codebase. Use final fields with a const constructor where possible. For fromJson, use the pattern: field = json['field_name'] as Type — add explicit casts and null checks. For DateTime parsing, use DateTime.parse(json['created_at'] as String) and .toLocal() if the UI displays local times.

For the OrgSubtree.contains() helper, implement as bool contains(String orgId) => subtreeOrganisationIds.contains(orgId) || orgId == rootOrganisationId. The AdminRole and ReimbursementStatus enums should have a fromString(String) factory for JSON deserialization using a switch expression. Avoid using code generation (json_serializable, freezed) unless the project already uses it — adding new build_runner dependencies mid-project adds friction. If the project does use freezed, use it consistently.

Place enums in a separate file lib/domain/admin/admin_enums.dart to keep model files focused.

Testing Requirements

Unit tests only — no widget or integration tests needed for pure domain models. Test file: test/domain/admin/admin_models_test.dart. Required test groups: (1) Serialization — fromJson produces correct field values from a fixture JSON map for each model; toJson produces the correct snake_case map; fromJson(toJson(model)) equals original model; (2) Equality — two models with identical fields are equal; models differing in one field are not equal; (3) CopyWith — copyWith with no arguments returns equal model; copyWith with one argument changes only that field; copyWith supports null for nullable fields; (4) Edge cases — fromJson handles null optional fields without throwing; DateTime fields parse ISO 8601 strings correctly; enum fields throw on invalid values. Use const fixture maps defined at the top of the test file.

Component
Admin Data Repository
data high
Epic Risks (3)
high impact medium prob security

Missing RLS policies on one or more tables (e.g., a newly added join table or a Supabase view) could expose cross-org data to org_admin queries, creating a GDPR-reportable data breach.

Mitigation & Contingency

Mitigation: Enumerate all tables and views accessed by admin queries before writing any policy. Create an automated test that attempts a cross-org query for each table from an org_admin JWT and asserts an empty result set.

Contingency: If a gap is discovered post-deployment, immediately disable the affected query surface and deploy a hotfix policy before re-enabling. Log the incident and notify DPO if any cross-org data was returned.

high impact medium prob technical

The recursive CTE for NHF's deeply nested org tree (up to 5 levels, 1,400 local chapters) may exceed the 2-second dashboard load target when resolving large subtrees on every request.

Mitigation & Contingency

Mitigation: Benchmark the recursive CTE against a synthetic NHF-scale dataset during development. Introduce a short-TTL server-side cache for subtree resolution results. Index the parent_id column on the organisations table.

Contingency: If CTE performance remains insufficient, materialise the org subtree as a precomputed closure table updated on org structure changes, and switch the RLS guard to query the closure table instead.

high impact low prob security

Incorrect JWT claim injection in AdminRlsGuard (e.g., wrong claim key name or missing refresh on org switch) could silently apply the wrong org scope, causing org_admin to see a different organisation's data without an explicit error.

Mitigation & Contingency

Mitigation: Write unit tests for the guard that verify the injected claim value against the authenticated user's org_id for every admin route. Add a server-side assertion that the claim matches the user's database record before executing any query.

Contingency: Roll back the guard to a deny-all fallback, invalidate active admin sessions, and re-issue corrected JWTs. Audit query logs to identify any sessions that received incorrect scope.