critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

removeAffiliation returns ChapterMembershipFailure(AffiliationNotFound) when no active affiliation exists for the given contactId + chapterId combination, and no delete is performed
AffiliationNotFound error type carries contactId and chapterId so callers can surface a meaningful message
When the affiliation exists, exactly one delete is made to ContactChapterRepository
removeAffiliation returns ChapterMembershipSuccess with the updated affiliation list (excluding the removed entry) after a successful delete
The method is safe to call twice with the same arguments — the second call returns AffiliationNotFound rather than causing an unhandled error (idempotency guard)
Repository exceptions during delete are caught and wrapped in a typed domain failure
The method does not return a raw repository boolean or row count — it returns ChapterMembershipResult only
removeAffiliation returns ChapterMembershipFailure(ChapterNotFound — contact variant) when the contactId does not exist
Unit tests cover: affiliation exists and is removed, affiliation absent (returns AffiliationNotFound), second call after removal (idempotency), repository delete throws exception

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase REST API (via ContactChapterRepository abstraction)
data models
ChapterAffiliation
ChapterMembershipResult
Contact
performance requirements
Method must complete within 2 seconds on a standard mobile connection
security requirements
RLS policies must prevent a coordinator from removing affiliations for contacts outside their organisation
contactId and chapterId must be validated as non-empty UUID-format strings before any repository call
Removal of the last affiliation is permitted — do not enforce a minimum affiliation count unless a business rule requires it (none currently stated)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Fetch the current affiliation list using the internal helper that backs getAffiliationsForContact — do not make a separate repository call. Check affiliations.any((a) => a.chapterId == chapterId) for existence. If not found, return AffiliationNotFound immediately. If found, call repository.deleteAffiliation(contactId, chapterId), then return the updated list by filtering the in-memory list (or re-fetching — prefer filtering for fewer round trips).

Idempotency is guaranteed by the existence check: a second call will find no affiliation and return AffiliationNotFound cleanly. Do not suppress AffiliationNotFound for idempotency purposes — callers (e.g., coordinators removing a stale association) benefit from knowing the record was already absent. Add AffiliationNotFound to the domain error sealed class introduced in task-001.

Testing Requirements

Unit tests with fake/mock repository: (1) affiliation exists → delete called once, success returned with updated list; (2) affiliation absent → AffiliationNotFound returned, delete never called; (3) simulate second call after first success by having repository return empty on second fetch → AffiliationNotFound on second call; (4) repository delete throws → wrapped failure returned. Assert with Mockito's verifyNever that delete is not called in failure scenarios. Target 100% branch coverage of removeAffiliation. Add one integration-style test (can be skipped in CI, tagged @integration) that exercises the full flow against a Supabase local emulator if available.

Component
Multi-Chapter Membership Service
service medium
Epic Risks (2)
medium impact medium prob scope

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.

medium impact medium prob technical

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.