critical priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

ContactChapter is an immutable Dart class with fields: id (String), name (String), organizationId (String), and createdAt (DateTime)
ChapterAffiliation is an immutable Dart class with fields: id (String), contactId (String), chapterId (String), isPrimary (bool), and affiliatedAt (DateTime)
ContactChapterRepository is an abstract class (interface) with the following async methods: Future<List<ChapterAffiliation>> getChaptersForContact(String contactId), Future<ChapterAffiliation> addChapterAffiliation(String contactId, String chapterId, {bool isPrimary}), Future<void> removeChapterAffiliation(String affiliationId), Future<ChapterAffiliation> updateAffiliation(String affiliationId, {bool? isPrimary})
MaxAffiliationsExceededException is a typed Exception subclass with a message field and a currentCount (int) field
AffiliationNotFoundException is a typed Exception subclass with a message field and the affiliationId that was not found
AlreadyAssignedToChapterException is a typed Exception subclass indicating the contact is already affiliated with the given chapter
All model classes implement == and hashCode based on the id field
All model classes include a copyWith method for immutable updates
Files are located under lib/domain/models/ and lib/domain/repositories/ following the existing project structure
No Supabase imports appear in any domain model or repository interface file

Technical Requirements

frameworks
Flutter
Dart
data models
ContactChapter
ChapterAffiliation
MaxAffiliationsExceededException
AffiliationNotFoundException
AlreadyAssignedToChapterException
performance requirements
Domain model constructors are synchronous and allocation cost is negligible
security requirements
No PII beyond contactId and chapterId is stored in ChapterAffiliation at the domain layer
Domain layer must not import any Supabase or network packages — keep infrastructure concerns out

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Follow the existing project domain model conventions (check other entities in lib/domain/models/ before creating new files). Use Dart's standard Exception base class rather than Error for domain exceptions — these are expected business conditions, not programming errors. Do not use code generation (freezed/json_serializable) unless the project already uses it for domain models — check existing patterns first. The 5-affiliation business rule is documented as a constant (maxAffiliationsPerContact = 5) in a domain constants file, not hardcoded in each class.

This task produces only interfaces and models; no Supabase code is written here.

Testing Requirements

Write unit tests for all domain model equality checks, copyWith behaviour, and exception field population. Tests should be in test/domain/models/. No mocking required — these are pure value object tests. Verify that MaxAffiliationsExceededException.currentCount is accessible after throw/catch.

Verify that ChapterAffiliation equality is based on id, not reference.

Component
Contact Chapter Repository
data medium
Epic Risks (3)
high impact medium prob technical

The Cross-Chapter Activity Query must avoid N+1 fetches across chapters. If naively implemented as a per-chapter loop, it will cause severe performance degradation for contacts affiliated with 5 chapters on poor mobile connections.

Mitigation & Contingency

Mitigation: Design the query as a single PostgREST join of contact_chapters and activities on contact_id from the start. Add a query performance test with 5 affiliations and 100+ activities to the integration test suite and enforce a maximum execution time threshold.

Contingency: If a performance regression is detected post-merge, introduce a Supabase RPC function (stored procedure) to move the join server-side, bypassing any client-side N+1 pattern.

high impact low prob security

If the Duplicate Warning Event Logger write fails silently (network error, RLS denial), audit entries will be missing from the Bufdir compliance record without the user being aware.

Mitigation & Contingency

Mitigation: Implement the logger with a local fallback queue: if the Supabase write fails, persist the event locally and retry on next launch. Log all failures to a verbose output channel.

Contingency: Add a reconciliation job that compares locally queued events to Supabase entries and re-submits any gaps. Provide a data export of the local queue for manual audit if reconciliation fails.

medium impact low prob technical

Two coordinators simultaneously adding the 5th chapter affiliation for the same contact could bypass the maximum enforcement check if both reads occur before either write completes.

Mitigation & Contingency

Mitigation: Enforce the 5-affiliation maximum as a database-level constraint (CHECK + trigger or RPC with a FOR UPDATE lock) rather than relying solely on application-layer validation.

Contingency: If a constraint violation is detected in production, run a corrective query to end the most recently created excess affiliation and notify the relevant coordinator.