chore: add production deployment config and upgrade path requirements
- Add docker-compose.yml and docker-compose.example.yml for production deployment - Add .env.example with all required environment variables - Update architect agent with upgrade path requirements - Update developer agent with migration best practices - Add Phase 3 design documents (v0.3.0) - Add ADR-0006 for participant state management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
252
docs/decisions/0006-participant-state-management.md
Normal file
252
docs/decisions/0006-participant-state-management.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user