critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

assign(userId, unitId, {bool isPrimary = false}) persists the assignment to Supabase, enforces that the user has at most one primary assignment per organization, and returns the created UserUnitAssignment
When isPrimary is true and the user already has a primary assignment in the same organization, the old primary is atomically demoted to secondary before the new one is set — no window where two primaries exist simultaneously
assign() calls HierarchyStructureValidator.validateUserDepthLimit() before writing; returns AssignmentConstraintException with message 'Maximum 5 chapter assignments reached' when limit is exceeded
When a new primary assignment is set, ActiveChapterCubit.switchChapter() is called within the same operation so downstream services immediately receive the updated session context
unassign(userId, unitId) removes the assignment; if it was the primary, ActiveChapterCubit transitions to ActiveChapterEmpty state
listAssignments(userId) returns all assignments for the user ordered by isPrimary descending then createdAt ascending, sourced via the RLS-scoped Supabase client
All three methods are idempotent: assigning an already-assigned unit returns the existing record without error; unassigning a non-existent assignment is a no-op
Unit tests cover: assign new, assign as primary with demotion, assign exceeding 5-chapter limit, unassign primary (ActiveChapterCubit notified), unassign non-primary, listAssignments ordering

Technical Requirements

frameworks
Flutter
Dart
BLoC (flutter_bloc)
apis
Supabase REST (unit_assignments table via RLS)
Supabase RPC (atomic primary demotion)
data models
UserUnitAssignment
OrganizationUnit
AssignmentConstraintException
performance requirements
assign() completes end-to-end (validation + write + cubit notification) in under 500ms on a standard connection
listAssignments() for a user with up to 5 assignments returns within 200ms
Primary demotion and new primary assignment must execute in a single atomic Supabase RPC to prevent race conditions
security requirements
Service must not accept a userId that differs from auth.uid() unless the caller has coordinator or national_admin role — enforce this in the service layer before reaching Supabase
RLS on unit_assignments (from task-006) is the authoritative access control; service-layer checks are defence-in-depth
Audit log entry written for every assign() and unassign() call including caller userId, target userId, unitId, and timestamp

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Implement atomic primary demotion as a Supabase RPC function set_primary_assignment(p_user_id uuid, p_unit_id uuid) that in a single transaction: (1) sets is_primary = false on all existing primary assignments for that user in that organization, (2) inserts or updates the new assignment with is_primary = true. This prevents the dual-primary race condition that would occur if Dart issued two sequential UPDATE/INSERT calls. In the Dart service, call this RPC via supabase.rpc('set_primary_assignment', params: {...}). The 5-chapter limit check should query COUNT from unit_assignments WHERE user_id = userId before calling the RPC — pass the snapshot count to HierarchyStructureValidator.validateUserDepthLimit() to keep the validator pure.

Inject ActiveChapterCubit via the service constructor; call switchChapter() after a successful primary assignment write to ensure the state update is not skipped on RPC error.

Testing Requirements

Unit tests using mockito/mocktail to mock UnitAssignmentRepository, HierarchyStructureValidator, and ActiveChapterCubit. Cover: (1) assign success, (2) assign with primary demotion — assert repository receives demotion call before new assignment, (3) assign rejected at depth limit — assert no repository write occurs, (4) assign idempotency, (5) unassign primary — assert ActiveChapterCubit.switchToEmpty() called, (6) unassign non-primary — assert cubit not notified, (7) listAssignments correct ordering. Integration test against local Supabase: create two chapter assignments for a test user, mark second as primary, assert first is demoted atomically. Minimum 85% branch coverage.

Component
Unit Assignment Service
service medium
Dependencies (4)
Build the BLoC/Cubit managing the currently active chapter context for a session. Expose streams for the selected organizational unit, handle chapter switching events, persist active chapter selection to secure local storage, and notify downstream services when active chapter changes so session context updates correctly. epic-organizational-hierarchy-management-assignment-aggregation-task-004 Build the domain service encapsulating all hierarchy tree operations: create node, move node, delete node with cascade checks, get subtree, get ancestors, and resolve level types. Orchestrate calls to Organization Unit Repository and Hierarchy Cache, enforce business invariants (no orphaned nodes, valid level sequences), and expose a clean API consumed by higher-layer services. epic-organizational-hierarchy-management-assignment-aggregation-task-007 Implement the Supabase-backed repository for the junction table linking users to organizational units. Provide create, read, update, delete operations for user-unit assignments, including queries for fetching all units a user belongs to and all users in a given unit. Enforce uniqueness constraints at the repository level. epic-organizational-hierarchy-management-assignment-aggregation-task-002 Implement and configure Supabase Row Level Security policies governing access to unit_assignments and organization_units tables. Coordinators see only units within their scope, peer mentors see only their own assignments, national admins see the full tree. Integrate with the assignment repository so all queries automatically apply tenant-scoped filters. epic-organizational-hierarchy-management-assignment-aggregation-task-006
Epic Risks (3)
high impact medium prob technical

Recursive aggregation queries across four hierarchy levels (national → region → local) with 1,400 leaf nodes may be too slow for real-time dashboard requests, exceeding the 200ms target and causing spinner timeouts.

Mitigation & Contingency

Mitigation: Implement aggregation as a Supabase RPC using a single recursive CTE rather than multiple round-trip queries. Pre-compute aggregations nightly via a scheduled Edge Function and cache results. For real-time needs, aggregate only the immediate subtree on demand.

Contingency: Surface a 'Refreshing...' indicator and serve stale cached aggregations immediately. Queue an async recalculation and push updated data via Supabase Realtime when ready, avoiding blocking the admin dashboard.

medium impact medium prob scope

The 5-chapter limit and primary-assignment constraint are NHF-specific. Applying these rules globally may break HLF and Blindeforbundet configurations where different limits apply, requiring per-organization configuration that was not initially scoped.

Mitigation & Contingency

Mitigation: Make the maximum assignment count a configurable value stored in the organization's feature-flag or settings table rather than a hardcoded constant. Design the assignment service to read this limit at runtime per organization.

Contingency: Default the limit to a high value (e.g., 100) for organizations other than NHF, effectively making it non-restrictive, while keeping the enforcement logic intact for when per-org configuration is fully implemented.

medium impact low prob technical

The searchable parent dropdown in HierarchyNodeEditor must search across up to 1,400 units efficiently. Client-side filtering of the full hierarchy may be slow; server-side search adds complexity and latency.

Mitigation & Contingency

Mitigation: Use the in-memory hierarchy cache as the search corpus — since the cache already holds the flat unit list, client-side filtering with a debounced input is sufficient and avoids extra Supabase calls. Pre-build a search index on cache load.

Contingency: Cap the dropdown to showing the 50 most recently accessed units by default, with a 'search all' option that triggers a server-side full-text query. This keeps the common case fast while supporting edge cases.