- 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>
567 lines
18 KiB
Markdown
567 lines
18 KiB
Markdown
# System Overview - v0.3.0
|
|
|
|
**Version**: 0.3.0
|
|
**Date**: 2025-12-22
|
|
**Status**: Phase 3 Design
|
|
|
|
## Introduction
|
|
|
|
This document describes the design for Phase 3 of Sneaky Klaus, building on the participant authentication foundation established in v0.2.0. This phase focuses on participant self-management capabilities and pre-matching participant experience.
|
|
|
|
**Phase 3 Scope**: Implement Epic 6 (Participant Self-Management) and Epic 4.5 (View Participant List Pre-Matching), enabling participants to manage their own profiles and see who else has registered before matching occurs.
|
|
|
|
## Phase Goals
|
|
|
|
The primary goals for v0.3.0 are:
|
|
|
|
1. **Enable participant self-service**: Allow participants to update their profiles without admin intervention
|
|
2. **Support graceful withdrawals**: Handle participants who need to drop out before matching
|
|
3. **Build social engagement**: Let participants see who else is participating (pre-matching only)
|
|
4. **Maintain data integrity**: Ensure profile changes and withdrawals don't break the exchange flow
|
|
|
|
## User Stories in Scope
|
|
|
|
### Epic 6: Participant Self-Management
|
|
|
|
- **6.1 Update Profile**: Participants can edit their name and gift ideas before matching
|
|
- **6.2 Withdraw from Exchange**: Participants can opt out before registration closes
|
|
- **6.3 Update Reminder Preferences**: Participants can toggle reminder emails on/off
|
|
|
|
### Epic 4: Participant Registration (Continuation)
|
|
|
|
- **4.5 View Participant List (Pre-Matching)**: Registered participants can see who else has joined
|
|
|
|
## Out of Scope for v0.3.0
|
|
|
|
The following features are explicitly **not** included in this phase:
|
|
|
|
- Post-matching participant experience (Epic 11) - deferred to Phase 4
|
|
- Matching system (Epic 8) - deferred to Phase 5
|
|
- Admin exchange management beyond what v0.1.0 provided
|
|
- Participant removal by admin (Epic 9) - deferred to Phase 6
|
|
- Notification emails beyond registration confirmation (Epic 10) - partial in Phase 2, remainder in Phase 7
|
|
|
|
## Architecture Overview
|
|
|
|
Phase 3 builds incrementally on the v0.2.0 architecture with no new infrastructure components:
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Phase 3 Additions"
|
|
ProfileUpdate[Profile Update UI]
|
|
WithdrawUI[Withdraw UI]
|
|
ParticipantList[Participant List View]
|
|
|
|
ProfileUpdate --> ParticipantRoutes
|
|
WithdrawUI --> ParticipantRoutes
|
|
ParticipantList --> ParticipantRoutes
|
|
end
|
|
|
|
subgraph "Existing v0.2.0 Foundation"
|
|
ParticipantRoutes[Participant Routes]
|
|
ParticipantAuth[Participant Auth Decorator]
|
|
ParticipantModel[Participant Model]
|
|
ExchangeModel[Exchange Model]
|
|
Database[(SQLite)]
|
|
|
|
ParticipantRoutes --> ParticipantAuth
|
|
ParticipantRoutes --> ParticipantModel
|
|
ParticipantRoutes --> ExchangeModel
|
|
ParticipantModel --> Database
|
|
ExchangeModel --> Database
|
|
end
|
|
```
|
|
|
|
## Technical Design Principles
|
|
|
|
This phase adheres to the project's core principles established in v0.1.0 and v0.2.0:
|
|
|
|
1. **No new database tables**: All features use existing Participant and Exchange models
|
|
2. **No new external dependencies**: Pure Flask/SQLAlchemy implementation
|
|
3. **Soft deletes**: Withdrawals use `withdrawn_at` timestamp rather than hard deletes
|
|
4. **State-based permissions**: Operations restricted based on exchange state
|
|
5. **TDD approach**: Write tests first, implement to pass
|
|
|
|
## Key Design Decisions
|
|
|
|
### 1. Profile Update Restrictions
|
|
|
|
**Decision**: Profile updates are only allowed when exchange is in `draft`, `registration_open`, or `registration_closed` states. After matching, profiles are locked.
|
|
|
|
**Rationale**:
|
|
- Gift ideas are sent to givers during match notification - changes after matching would create inconsistency
|
|
- Name changes after matching could confuse participants
|
|
- Clear state transition prevents data inconsistency
|
|
|
|
**Implementation**:
|
|
```python
|
|
def can_update_profile(participant: Participant) -> bool:
|
|
"""Check if participant can update their profile."""
|
|
exchange = participant.exchange
|
|
return exchange.state in ['draft', 'registration_open', 'registration_closed']
|
|
```
|
|
|
|
### 2. Withdrawal Rules
|
|
|
|
**Decision**: Withdrawals are only allowed before registration closes. After registration closes, admin intervention required.
|
|
|
|
**Rationale**:
|
|
- Pre-closure: minimal impact, just removes one participant
|
|
- Post-closure: admin likely already configuring exclusions or has matched
|
|
- Post-matching: requires re-match, admin should make this decision
|
|
- Clear deadline prevents last-minute dropouts
|
|
|
|
**Implementation**:
|
|
```python
|
|
def can_withdraw(participant: Participant) -> bool:
|
|
"""Check if participant can withdraw themselves."""
|
|
exchange = participant.exchange
|
|
return exchange.state in ['draft', 'registration_open']
|
|
```
|
|
|
|
### 3. Withdrawal Implementation (Soft Delete)
|
|
|
|
**Decision**: Use existing `withdrawn_at` timestamp field, don't delete participant records.
|
|
|
|
**Rationale**:
|
|
- Data model already supports soft deletes (v0.2.0 design)
|
|
- Preserves audit trail of who registered
|
|
- Prevents re-use of email during same exchange
|
|
- Allows admin to see withdrawal history
|
|
- Simplifies database constraints (no cascade delete issues)
|
|
|
|
**Implementation**:
|
|
```python
|
|
def withdraw_participant(participant: Participant):
|
|
"""Mark participant as withdrawn."""
|
|
participant.withdrawn_at = datetime.utcnow()
|
|
db.session.commit()
|
|
```
|
|
|
|
### 4. Participant List Visibility
|
|
|
|
**Decision**: Show participant list to all registered participants, but only show names (not emails or gift ideas).
|
|
|
|
**Rationale**:
|
|
- Social aspect: participants want to know who's participating
|
|
- Privacy: emails are admin-only
|
|
- Security: gift ideas are for givers only (post-matching)
|
|
- Pre-matching only: post-matching handled in Phase 4
|
|
|
|
**Display Rules**:
|
|
- Show: participant names, count
|
|
- Hide: emails, gift ideas, withdrawn participants
|
|
- Filter: exclude withdrawn participants from list
|
|
|
|
### 5. Reminder Preference Changes
|
|
|
|
**Decision**: Allow reminder preference changes at any time before exchange completes.
|
|
|
|
**Rationale**:
|
|
- Low-impact change (doesn't affect matching or other participants)
|
|
- User preference should be flexible
|
|
- No technical reason to restrict after matching
|
|
- Allows opting out if circumstances change
|
|
|
|
**Implementation**: Simple boolean toggle, no state restrictions.
|
|
|
|
### 6. Form Validation Strategy
|
|
|
|
**Decision**: Use server-side validation with WTForms, add client-side hints for UX.
|
|
|
|
**Rationale**:
|
|
- Consistent with Phase 2 implementation decisions
|
|
- Security: never trust client-side validation
|
|
- UX: client-side provides immediate feedback
|
|
- Progressive enhancement: works without JavaScript
|
|
|
|
## Data Flow
|
|
|
|
### Profile Update Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant P as Participant Browser
|
|
participant F as Flask App
|
|
participant DB as SQLite Database
|
|
|
|
P->>F: GET /participant/profile/edit
|
|
F->>F: Check @participant_required
|
|
F->>DB: Load participant & exchange
|
|
F->>F: Check can_update_profile()
|
|
F-->>P: Render edit form (or error)
|
|
|
|
P->>F: POST /participant/profile/edit (name, gift_ideas)
|
|
F->>F: Validate form
|
|
F->>F: Check can_update_profile()
|
|
F->>DB: Update participant record
|
|
DB-->>F: Success
|
|
F-->>P: Redirect to dashboard with success message
|
|
```
|
|
|
|
### Withdrawal Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant P as Participant Browser
|
|
participant F as Flask App
|
|
participant DB as SQLite Database
|
|
participant E as Email Service
|
|
|
|
P->>F: POST /participant/withdraw (with confirmation)
|
|
F->>F: Check @participant_required
|
|
F->>DB: Load participant & exchange
|
|
F->>F: Check can_withdraw()
|
|
F->>DB: Set withdrawn_at timestamp
|
|
DB-->>F: Success
|
|
F->>E: Send withdrawal confirmation email
|
|
E-->>F: Email sent
|
|
F->>F: Clear session (log out participant)
|
|
F-->>P: Redirect to public page with confirmation
|
|
```
|
|
|
|
### Participant List Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant P as Participant Browser
|
|
participant F as Flask App
|
|
participant DB as SQLite Database
|
|
|
|
P->>F: GET /participant/dashboard
|
|
F->>F: Check @participant_required
|
|
F->>DB: Load participant's exchange
|
|
F->>DB: Query active participants (withdrawn_at IS NULL)
|
|
DB-->>F: Participant list (names only)
|
|
F-->>P: Render dashboard with participant list
|
|
```
|
|
|
|
## State Machine Impact
|
|
|
|
Phase 3 doesn't add new exchange states, but adds participant-level state:
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Registered: Registration
|
|
Registered --> UpdatedProfile: Edit profile
|
|
UpdatedProfile --> UpdatedProfile: Edit again
|
|
UpdatedProfile --> Withdrawn: Withdraw
|
|
Registered --> Withdrawn: Withdraw
|
|
UpdatedProfile --> Matched: Admin matches
|
|
Registered --> Matched: Admin matches
|
|
Withdrawn --> [*]
|
|
|
|
note right of Withdrawn
|
|
Soft delete: withdrawn_at set
|
|
Cannot re-activate
|
|
Must re-register with new email
|
|
end note
|
|
|
|
note right of UpdatedProfile
|
|
Only before matching
|
|
Name and gift_ideas editable
|
|
end note
|
|
```
|
|
|
|
## Component Design
|
|
|
|
### Routes (participant_bp)
|
|
|
|
New routes added to existing `src/routes/participant.py`:
|
|
|
|
| Route | Method | Auth | Description |
|
|
|-------|--------|------|-------------|
|
|
| `/participant/profile/edit` | GET, POST | @participant_required | Edit profile (name, gift ideas) |
|
|
| `/participant/preferences` | POST | @participant_required | Update reminder preference |
|
|
| `/participant/withdraw` | POST | @participant_required | Withdraw from exchange |
|
|
|
|
Existing routes used:
|
|
- `/participant/dashboard` - enhanced to show participant list
|
|
|
|
### Forms (src/forms/participant.py)
|
|
|
|
New forms:
|
|
|
|
**ProfileUpdateForm**:
|
|
```python
|
|
class ProfileUpdateForm(FlaskForm):
|
|
name = StringField('Name', validators=[DataRequired(), Length(max=255)])
|
|
gift_ideas = TextAreaField('Gift Ideas', validators=[Length(max=10000)])
|
|
submit = SubmitField('Save Changes')
|
|
```
|
|
|
|
**ReminderPreferenceForm**:
|
|
```python
|
|
class ReminderPreferenceForm(FlaskForm):
|
|
reminder_enabled = BooleanField('Send me reminders')
|
|
submit = SubmitField('Update Preferences')
|
|
```
|
|
|
|
**WithdrawForm**:
|
|
```python
|
|
class WithdrawForm(FlaskForm):
|
|
confirm = BooleanField('I understand this cannot be undone',
|
|
validators=[DataRequired()])
|
|
submit = SubmitField('Withdraw from Exchange')
|
|
```
|
|
|
|
### Templates
|
|
|
|
New templates in `templates/participant/`:
|
|
|
|
- `profile_edit.html` - Profile update form
|
|
- `withdraw.html` - Withdrawal confirmation page (with warnings)
|
|
- `participant_list.html` - Reusable component for displaying participant names
|
|
|
|
Enhanced templates:
|
|
- `dashboard.html` - Add participant list section
|
|
|
|
### Email Templates
|
|
|
|
New email template in `templates/emails/participant/`:
|
|
|
|
- `withdrawal_confirmation.html` - Sent when participant withdraws
|
|
- `withdrawal_confirmation.txt` - Plain text version
|
|
|
|
## Security Considerations
|
|
|
|
### 1. Authorization Checks
|
|
|
|
Every participant operation must verify:
|
|
- User is authenticated as participant (@participant_required)
|
|
- User owns the resource (participant_id matches session)
|
|
- Exchange state allows the operation (can_update_profile, can_withdraw)
|
|
|
|
### 2. CSRF Protection
|
|
|
|
All POST operations require CSRF tokens (WTForms automatic).
|
|
|
|
### 3. Input Validation
|
|
|
|
- Name: 1-255 characters, required
|
|
- Gift ideas: 0-10,000 characters, optional
|
|
- All inputs sanitized via Jinja2 auto-escaping
|
|
|
|
### 4. Privacy
|
|
|
|
Participant list shows:
|
|
- Names only (public within exchange)
|
|
- Not emails (admin-only)
|
|
- Not gift ideas (giver-only post-matching)
|
|
- Not withdrawn participants (respect withdrawal privacy)
|
|
|
|
### 5. Rate Limiting
|
|
|
|
No new rate limiting needed:
|
|
- Profile updates: legitimate user operation, no abuse vector
|
|
- Withdrawals: self-limiting (can only withdraw once)
|
|
- Participant list: read-only, authenticated
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
Test business logic in isolation:
|
|
- `can_update_profile()` logic for each exchange state
|
|
- `can_withdraw()` logic for each exchange state
|
|
- Soft delete implementation (withdrawn_at timestamp)
|
|
- Participant list filtering (exclude withdrawn)
|
|
|
|
### Integration Tests
|
|
|
|
Test full request/response cycles:
|
|
- Profile update happy path
|
|
- Profile update when locked (post-matching)
|
|
- Withdrawal happy path
|
|
- Withdrawal when not allowed
|
|
- Participant list rendering
|
|
|
|
### Test Data Setup
|
|
|
|
Reuse existing test fixtures from Phase 2, add:
|
|
- Multiple participants in same exchange
|
|
- Participants in different states (registered, withdrawn)
|
|
- Exchanges in different states
|
|
|
|
### Coverage Target
|
|
|
|
Maintain 80%+ coverage established in previous phases.
|
|
|
|
## Error Handling
|
|
|
|
### Expected Errors
|
|
|
|
| Scenario | HTTP Status | User Message | Action |
|
|
|----------|-------------|--------------|--------|
|
|
| Update profile after matching | 400 | "Your profile is locked after matching" | Redirect to dashboard |
|
|
| Withdraw after registration closes | 400 | "Withdrawal deadline has passed. Contact admin." | Redirect to dashboard |
|
|
| Invalid form data | 400 | Field-specific errors | Re-render form with errors |
|
|
| Already withdrawn | 400 | "You have already withdrawn" | Redirect to public page |
|
|
|
|
### Unexpected Errors
|
|
|
|
- Database errors: Log, flash generic error, redirect safely
|
|
- Missing participant record: Clear session, redirect to landing page
|
|
- Orphaned session: Clear session, redirect to landing page
|
|
|
|
## Performance Considerations
|
|
|
|
### Database Queries
|
|
|
|
**Participant List Query**:
|
|
```python
|
|
# Efficient query with single DB hit
|
|
participants = Participant.query.filter(
|
|
Participant.exchange_id == exchange_id,
|
|
Participant.withdrawn_at.is_(None)
|
|
).order_by(Participant.name).all()
|
|
```
|
|
|
|
**Profile Load**:
|
|
- Already loaded by @participant_required decorator (uses `g.participant`)
|
|
- No additional query needed
|
|
|
|
### Caching
|
|
|
|
Not needed for v0.3.0:
|
|
- Participant counts are small (3-100 typical)
|
|
- Updates are infrequent
|
|
- Reads are authenticated (no public caching)
|
|
|
|
## Migration Requirements
|
|
|
|
**No database migrations required for v0.3.0.**
|
|
|
|
All fields already exist:
|
|
- `participant.name` - editable
|
|
- `participant.gift_ideas` - editable
|
|
- `participant.reminder_enabled` - editable
|
|
- `participant.withdrawn_at` - set on withdrawal
|
|
|
|
## Deployment Impact
|
|
|
|
### Breaking Changes
|
|
|
|
None. v0.3.0 is fully backward compatible with v0.2.0.
|
|
|
|
### Configuration Changes
|
|
|
|
None. No new environment variables or configuration needed.
|
|
|
|
### Data Migration
|
|
|
|
None. Existing data fully compatible.
|
|
|
|
## User Experience Flow
|
|
|
|
### Participant Journey (Pre-Matching)
|
|
|
|
1. **Register** (Phase 2)
|
|
- Receive confirmation email with magic link
|
|
|
|
2. **View Dashboard** (Phase 2 + Phase 3)
|
|
- See own information
|
|
- **NEW**: See list of other participants (names only)
|
|
|
|
3. **Update Profile** (Phase 3)
|
|
- Click "Edit Profile" from dashboard
|
|
- Update name or gift ideas
|
|
- Save changes
|
|
- See confirmation message
|
|
|
|
4. **Change Reminder Preference** (Phase 3)
|
|
- Toggle reminder checkbox on dashboard
|
|
- See confirmation message
|
|
|
|
5. **Withdraw (if needed)** (Phase 3)
|
|
- Click "Withdraw" from dashboard
|
|
- See warning about permanence
|
|
- Confirm withdrawal
|
|
- Receive confirmation email
|
|
- Logged out and redirected to public page
|
|
|
|
## Admin Experience Impact
|
|
|
|
Phase 3 adds visibility for admins:
|
|
|
|
### Exchange Detail View (Enhancement)
|
|
|
|
Add to existing exchange detail page:
|
|
- Show participant list with withdrawn indicator
|
|
- Display: "5 active, 1 withdrawn"
|
|
- Allow admin to see withdrawn participants (grayed out)
|
|
|
|
**Implementation Note**: This is a minor enhancement to existing admin routes, not a major feature. Can be implemented alongside participant features or deferred to Phase 6 (Admin Participant Management).
|
|
|
|
## Future Considerations
|
|
|
|
### Phase 4 Dependencies
|
|
|
|
Phase 3 sets up foundations for Phase 4 (Post-Matching Experience):
|
|
- Participant list already implemented (will be reused post-matching)
|
|
- Profile lock logic (can_update_profile) prevents changes after matching
|
|
- Dashboard structure ready to show match assignment
|
|
|
|
### Potential Enhancements (Out of Scope)
|
|
|
|
Not included in v0.3.0 but possible future additions:
|
|
- Email notification to admin when participant withdraws (Epic 10.5)
|
|
- Allow participants to indicate dietary restrictions or allergies
|
|
- Allow participants to add profile pictures
|
|
- Allow participants to message admin (anonymous contact form)
|
|
|
|
## Acceptance Criteria
|
|
|
|
Phase 3 is complete when:
|
|
|
|
1. ✅ Participants can update their name and gift ideas before matching
|
|
2. ✅ Participants cannot update profile after matching occurs
|
|
3. ✅ Participants can withdraw before registration closes
|
|
4. ✅ Participants cannot withdraw after registration closes
|
|
5. ✅ Withdrawn participants receive confirmation email
|
|
6. ✅ Withdrawn participants are logged out and removed from participant list
|
|
7. ✅ Participants can view list of other registered participants (names only)
|
|
8. ✅ Participant list excludes withdrawn participants
|
|
9. ✅ Participants can toggle reminder preferences at any time
|
|
10. ✅ All operations require participant authentication
|
|
11. ✅ All operations validate exchange state appropriately
|
|
12. ✅ All user stories have passing integration tests
|
|
13. ✅ Code coverage remains at 80%+
|
|
|
|
## Implementation Phases
|
|
|
|
Recommended implementation order (TDD, vertical slices):
|
|
|
|
### Phase 3.1: Participant List View
|
|
- Story 4.5: View Participant List (Pre-Matching)
|
|
- Simplest feature, no state changes
|
|
- Sets up dashboard enhancements
|
|
|
|
### Phase 3.2: Profile Updates
|
|
- Story 6.1: Update Profile
|
|
- Core self-management feature
|
|
- Tests state-based permissions
|
|
|
|
### Phase 3.3: Reminder Preferences
|
|
- Story 6.3: Update Reminder Preferences
|
|
- Simple toggle, low risk
|
|
- Quick win
|
|
|
|
### Phase 3.4: Withdrawal
|
|
- Story 6.2: Withdraw from Exchange
|
|
- Most complex (state changes, email, logout)
|
|
- Benefits from previous features being solid
|
|
|
|
## References
|
|
|
|
- [v0.2.0 System Overview](../v0.2.0/overview.md)
|
|
- [v0.2.0 Data Model](../v0.2.0/data-model.md)
|
|
- [Product Backlog](../../BACKLOG.md)
|
|
- [Project Overview](../../PROJECT_OVERVIEW.md)
|
|
- [ADR-0002: Authentication Strategy](../../decisions/0002-authentication-strategy.md)
|
|
- [ADR-0003: Participant Session Scoping](../../decisions/0003-participant-session-scoping.md)
|
|
|
|
---
|
|
|
|
**End of Phase 3 Design Overview**
|