critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

UnitAssignmentStatus is a Dart enum with values: active, revoked; includes fromString factory and toJson() returning the lowercase string
UnitAssignment is an immutable Dart class with fields: id (String), userId (String), unitId (String), isPrimary (bool), assignedAt (DateTime), assignedBy (String), revokedAt (DateTime?)
UnitAssignment.status getter returns UnitAssignmentStatus.active when revokedAt is null, UnitAssignmentStatus.revoked otherwise
UnitAssignment.fromJson(Map<String, dynamic>) deserializes all fields with correct snake_case key mapping (user_id, unit_id, is_primary, assigned_at, assigned_by, revoked_at)
UnitAssignment.toJson() returns Map<String, dynamic> with snake_case keys matching the Supabase junction table columns
UnitAssignment.copyWith() allows overriding any field including setting revokedAt to null (for restore operations)
UnitAssignment overrides == and hashCode based on id
Unit tests pass for all serialization round-trips and status getter logic
Model file is placed in the correct domain layer directory consistent with OrganizationUnit model placement

Technical Requirements

frameworks
Flutter (Dart)
data models
UnitAssignment
UnitAssignmentStatus
performance requirements
Model construction and serialization must be synchronous and complete in microseconds
security requirements
revokedAt must be treated as sensitive audit data — do not strip it from toJson as it is needed for compliance queries
assignedBy stores a user ID — ensure it is never confused with a display name in UI code

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

This is a straightforward value object — keep it simple and consistent with the OrganizationUnit model written in the preceding task. The status computed getter is the only non-trivial piece: it derives UnitAssignmentStatus from revokedAt rather than storing it as a separate field, which keeps the model in sync with the database without redundancy. For copyWith with nullable fields like revokedAt, use the sentinel pattern (a private object) to distinguish 'not provided' from 'explicitly set to null' — this is a common Dart challenge. Example: define a static const _absent = Object() and use it as the default for revokedAt in copyWith.

Follow the same DateTime parsing approach as OrganizationUnit (DateTime.parse for ISO 8601 strings from Supabase). Place this model in the same package/directory as UnitAssignmentRepository to keep the data layer cohesive.

Testing Requirements

Write unit tests using flutter_test. Test file: test/models/unit_assignment_test.dart. Scenarios: (1) fromJson with all fields including revokedAt populated. (2) fromJson with revokedAt = null.

(3) status getter returns active when revokedAt is null. (4) status getter returns revoked when revokedAt is set. (5) toJson produces correct snake_case keys. (6) copyWith with revokedAt override produces updated instance, original unchanged.

(7) Two UnitAssignment instances with same id are ==. (8) UnitAssignmentStatus.fromString with unknown value throws ArgumentError. Target 100% branch coverage on model logic.

Component
Unit Assignment Repository
data medium
Epic Risks (3)
high impact medium prob technical

Recursive CTE queries for large hierarchies (1,400+ nodes) may exceed Supabase query timeouts or produce unacceptably slow responses, degrading tree load time beyond the 1-second target.

Mitigation & Contingency

Mitigation: Implement Supabase RPC functions for subtree fetches rather than client-side recursive calls. Use materialized path or closure table as a supplemental index for depth-first traversal. Benchmark with realistic NHF data volumes during development.

Contingency: Fall back to a pre-computed flat unit list stored in the hierarchy cache with client-side tree reconstruction, trading freshness for speed. Add a background refresh job to keep the cache warm.

medium impact low prob technical

Concurrent writes from multiple admin sessions could cause cache staleness, leading to stale tree views and incorrect ancestor path computations that corrupt aggregation results.

Mitigation & Contingency

Mitigation: Use optimistic versioning on cache entries with a short TTL (5 minutes) as a safety net. Subscribe to Supabase Realtime on the organization_units table to push invalidation events to all connected clients.

Contingency: Provide a manual 'Refresh Hierarchy' action in the admin portal that forces a full cache bust, and display a staleness warning banner when the cache age exceeds the TTL.

high impact low prob security

Persisting the flat unit list to local storage may expose organization structure data if the device is compromised or the storage is not properly encrypted, violating data protection requirements.

Mitigation & Contingency

Mitigation: Use flutter_secure_storage (AES-256 backed by Keychain/Keystore) for the local unit list cache rather than SharedPreferences. Include only unit IDs, names, and types — no member PII.

Contingency: Disable local-storage persistence entirely and rely on in-memory cache only. Accept the trade-off of no offline hierarchy access for the security guarantee.