# ADR-0006: Participant State Management **Status**: Accepted **Date**: 2025-12-22 **Deciders**: Architect **Phase**: v0.3.0 ## Context Participants need to manage their own profiles and potentially withdraw from exchanges. We need to decide: 1. When can participants update their profiles (name, gift ideas)? 2. When can participants withdraw from an exchange? 3. How should withdrawals be implemented (hard delete vs soft delete)? 4. Should withdrawn participants be visible in any context? 5. Can withdrawn participants re-join the same exchange? These decisions impact data integrity, user experience, and admin workflows. ## Decision ### 1. Profile Update Rules **Profile updates are allowed until matching occurs.** Participants can update their name and gift ideas when the exchange is in: - `draft` state - `registration_open` state - `registration_closed` state Profile updates are **locked** when the exchange is in: - `matched` state - `completed` state **Rationale**: - Gift ideas are sent to Secret Santas during match notification - Allowing changes after matching would create inconsistency (giver sees old version in email) - Name changes after matching could confuse participants about who they're buying for - Registration close is admin's signal to finalize participant list, not to lock profiles - Locking happens at matching, which is the point where profile data is "consumed" by the system ### 2. Withdrawal Rules **Withdrawals are allowed until registration closes.** Participants can withdraw when the exchange is in: - `draft` state - `registration_open` state Withdrawals require admin intervention when the exchange is in: - `registration_closed` state (admin may be configuring exclusions) - `matched` state (would require re-matching) - `completed` state (historical record) **Rationale**: - Before registration closes: minimal impact, just removes one participant - After registration closes: admin is likely configuring exclusions or preparing to match - After matching: re-matching is a significant operation that should be admin-controlled - Clear deadline (registration close) sets expectations for participants - Prevents last-minute dropouts that could disrupt matching ### 3. Withdrawal Implementation (Soft Delete) **Withdrawals use soft delete via `withdrawn_at` timestamp.** Technical implementation: - Set `participant.withdrawn_at = datetime.utcnow()` on withdrawal - Keep participant record in database - Filter out withdrawn participants in queries: `Participant.withdrawn_at.is_(None)` - Cascade rules remain unchanged (deleting exchange deletes all participants) **Rationale**: - **Audit trail**: Preserves record of who registered and when they withdrew - **Email uniqueness**: Prevents re-registration with same email in same exchange (see Decision 5) - **Admin visibility**: Admins can see withdrawal history for troubleshooting - **Simplicity**: No cascade delete complexity or foreign key violations - **Existing pattern**: Data model already includes `withdrawn_at` field (v0.2.0 design) Alternative considered: Hard delete participants on withdrawal - Rejected: Loses audit trail, allows immediate re-registration (see Decision 5) - Rejected: Requires careful cascade handling for tokens, exclusions - Rejected: Complicates participant count tracking ### 4. Withdrawn Participant Visibility **Withdrawn participants are visible only to admin.** Visibility rules: - **Participant list (participant view)**: Withdrawn participants excluded - **Participant list (admin view)**: Withdrawn participants shown with indicator (e.g., grayed out, "Withdrawn" badge) - **Participant count**: Counts exclude withdrawn participants - **Matching algorithm**: Withdrawn participants excluded from matching pool **Rationale**: - **Privacy**: Respects participant's decision to withdraw (no public record) - **Admin needs**: Admin may need to see who withdrew (for follow-up, re-invites, etc.) - **Clean UX**: Participants see only active participants (less confusing) - **Data integrity**: Admin view preserves audit trail ### 5. Re-Registration After Withdrawal **Withdrawn participants cannot re-join the same exchange (with same email).** Technical enforcement: - Unique constraint on `(exchange_id, email)` remains in place - Soft delete doesn't remove the record, so email remains "taken" - Participant must use a different email to re-register **Rationale**: - **Prevents gaming**: Stops participants from withdrawing to see participant list changes, then re-joining - **Simplifies logic**: No need to handle "re-activation" of withdrawn participants - **Clear consequence**: Withdrawal is final (as warned in UI) - **Data integrity**: Each participant registration is a distinct record Alternative considered: Allow re-activation of withdrawn participants - Rejected: Complex state transitions (withdrawn → active → withdrawn → active) - Rejected: Unclear UX (does re-joining restore old profile or create new?) - Rejected: Enables abuse (withdraw/rejoin cycle) If participant genuinely needs to rejoin: - Use a different email address (e.g., alias like user+exchange@example.com) - Or: Contact admin, who can manually delete the withdrawn record (future admin feature) ### 6. Reminder Preferences After Withdrawal **Withdrawn participants do not receive reminder emails.** Technical implementation: - Reminder job queries exclude withdrawn participants: `withdrawn_at IS NULL` - Reminder preference persists in database (for audit) but is not used **Rationale**: - Withdrawn participants have no match to be reminded about - Sending reminders would be confusing and violate withdrawal expectations - Simple filter in reminder job handles this naturally ## Consequences ### Positive 1. **Clear rules**: Participants know when they can update profiles or withdraw 2. **Data integrity**: Matching always uses consistent profile data 3. **Audit trail**: System preserves record of all registrations and withdrawals 4. **Simple implementation**: Soft delete is easier than hard delete + cascades 5. **Privacy**: Withdrawn participants not visible to other participants 6. **Admin control**: Admin retains visibility for troubleshooting ### Negative 1. **No re-join**: Participants who withdraw accidentally must use different email 2. **Email "wastage"**: Withdrawn participants' emails remain "taken" in that exchange 3. **Database growth**: Withdrawn participants remain in database (minimal impact given small datasets) ### Mitigations 1. **Clear warnings**: UI prominently warns that withdrawal is permanent and cannot be undone 2. **Confirmation required**: Withdrawal requires explicit checkbox confirmation 3. **Confirmation email**: Withdrawn participants receive email confirming withdrawal 4. **Admin override** (future): Admin can manually delete withdrawn participants if needed ## Implementation Notes ### State Check Function ```python def can_update_profile(participant: Participant) -> bool: """Check if participant can update their profile.""" exchange = participant.exchange allowed_states = ['draft', 'registration_open', 'registration_closed'] return exchange.state in allowed_states def can_withdraw(participant: Participant) -> bool: """Check if participant can withdraw from the exchange.""" if participant.withdrawn_at is not None: return False # Already withdrawn exchange = participant.exchange allowed_states = ['draft', 'registration_open'] return exchange.state in allowed_states ``` ### Query Pattern for Active Participants ```python # Get active participants only active_participants = Participant.query.filter( Participant.exchange_id == exchange_id, Participant.withdrawn_at.is_(None) ).all() # Count active participants active_count = Participant.query.filter( Participant.exchange_id == exchange_id, Participant.withdrawn_at.is_(None) ).count() ``` ### Admin View Enhancement (Future) ```python # Admin can see all participants including withdrawn all_participants = Participant.query.filter( Participant.exchange_id == exchange_id ).all() # Template can check: participant.withdrawn_at is not None ``` ## Related Decisions - [ADR-0002: Authentication Strategy](0002-authentication-strategy.md) - Participant session management - [ADR-0003: Participant Session Scoping](0003-participant-session-scoping.md) - Session behavior on withdrawal - [v0.2.0 Data Model](../designs/v0.2.0/data-model.md) - `withdrawn_at` field design - [v0.3.0 Participant Self-Management](../designs/v0.3.0/participant-self-management.md) - Implementation details ## Future Considerations ### Phase 6: Admin Participant Management When implementing admin participant removal (Epic 9): - Admin should be able to hard delete withdrawn participants (cleanup) - Admin should be able to remove active participants (sets withdrawn_at + sends notification) - Admin should see withdrawal history in participant list ### Phase 8: Matching Matching algorithm must: - Filter participants by `withdrawn_at IS NULL` - Validate participant count >= 3 (after filtering) - Handle case where withdrawals reduce count below minimum ### Potential Future Enhancement: Re-Activation If user demand requires allowing re-join: - Add `reactivated_at` timestamp - Track withdrawal/reactivation history (audit log) - Clear `withdrawn_at` on re-activation - Send re-activation email - Complexity: High, defer until proven necessary ## References - [Product Backlog](../BACKLOG.md) - Epic 6: Participant Self-Management - [Project Overview](../PROJECT_OVERVIEW.md) - Self-management principles - [v0.2.0 Data Model](../designs/v0.2.0/data-model.md) - Participant table schema --- **Decision Date**: 2025-12-22 **Architect**: Claude Opus 4.5 **Status**: Accepted for v0.3.0