Implement 5-chapter maximum enforcement in addAffiliation
epic-multi-chapter-membership-handling-core-services-task-004 — Implement the addAffiliation method with full business rule enforcement: (1) fetch existing affiliations for the contact, (2) throw MaxChaptersExceeded if count >= 5, (3) throw AlreadyAssignedToChapter if the chapter is already present, (4) delegate the insert to ContactChapterRepository, and (5) return the updated complete affiliation set. All rules must be enforced atomically before any write occurs.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Reuse the getAffiliationsForContact implementation from task-003 to fetch the current affiliation set — do not duplicate the repository call pattern. Check affiliations.length >= kMaxChapterAffiliations first, then check affiliations.any((a) => a.chapterId == chapterId) for the duplicate check. Verify chapter existence via a ChapterRepository.exists(chapterId) call before attempting the insert. The atomicity requirement is a domain-level concern here (read-validate-write), not a database transaction — true database-level atomicity could be enforced later via a Supabase RPC/stored procedure if race conditions become a concern in production.
Document this caveat in code comments. The 5-chapter rule directly implements NHF's requirement for handling members in multiple local chapters while preventing double-reporting.
Testing Requirements
Unit tests with fake/mock repository: (1) contact has 5 affiliations → MaxChaptersExceeded, no insert called; (2) contact has 4 affiliations, chapter already present → AlreadyAssignedToChapter, no insert called; (3) contact has 4 affiliations, chapter not present, chapter exists → success, insert called once, returns 5-item list; (4) chapterId does not exist → ChapterNotFound; (5) contactId does not exist → appropriate failure; (6) repository insert throws exception → wrapped failure. Assert that the repository insert mock was never called in all failure scenarios using Mockito's verifyNever. Target 100% branch coverage of addAffiliation.
The ±1 day duplicate detection tolerance is specified in the acceptance criteria but timezone handling is not defined. A coordinator in UTC+2 submitting at 23:00 and another in UTC+0 submitting at 01:00 the next calendar day could trigger or miss a duplicate depending on which timezone the comparison uses.
Mitigation & Contingency
Mitigation: Define and document the authoritative timezone for all date comparisons (UTC stored in Supabase, all comparisons performed in UTC). Add timezone boundary unit tests covering the ambiguous ±1 day edges.
Contingency: If false positives or false negatives are reported in production, provide a coordinator-visible audit trail of duplicate detections so erroneous flags can be investigated and cleared manually.
The Duplicate Activity Detection Service performs a cross-chapter join query synchronously during the activity submission flow. On slow mobile connections this could cause a perceptible stall on the submission confirmation step, degrading user experience.
Mitigation & Contingency
Mitigation: Pre-fetch the cross-chapter activity dataset for the selected contact immediately when the contact is selected in the activity wizard (not only at submit time), storing the result in the state manager for instant comparison at submission.
Contingency: If latency is still unacceptable, implement a loading indicator on the submit action and add a configurable server-side timeout with graceful degradation: if the check times out, allow submission with a logged 'check skipped' audit entry rather than blocking the user.