Participation System
The participation system manages event signups, invitations, and attendance tracking. It works alongside the Event Lifecycle and Event Slots systems.
Core Flows
Self-Signup
A guild member signs up for a published event:
flowchart LR
A[User] -->|"POST /events/:id/signup"| B[Validate membership]
B --> C[Check slot capacity]
C --> D[Create participation]
Validation chain:
- Event must exist and be
published - User must be an active guild member
- For
invite_onlyevents, user must already have aninvitedparticipation - No duplicate signups (unique constraint on
eventId+userId) - Character (if provided) must belong to the user and have guild membership
- Target slot must exist
Invite (Officer Action)
Officers can invite members to an event. Two paths exist:
Claimed Member (has a user account)
flowchart LR
A[Officer] -->|"POST /events/:id/invite {userId}"| B["Create participation (status=invited)"]
The invited user sees the event and can convert their invite to a confirmed signup.
Unclaimed Roster Member
For guild members who haven't claimed their character yet:
flowchart LR
A[Officer] -->|"POST /events/:id/invite {rosterMemberId}"| B[Create PendingEventInvite]
When the roster member later claims their character, ConvertPendingInvitesUseCase automatically converts pending invites into real participations.
Remove Signup
Users can withdraw from events that haven't started:
flowchart LR
A[User] -->|"DELETE /events/:id/signup"| B[Validate ownership]
B --> C[Check event status]
C --> D[Delete participation]
Withdrawal is blocked for active and completed events.
Record Attendance (Officer Action)
After an event, officers record who showed up:
flowchart LR
A[Officer] -->|"POST /events/:id/attendance"| B["Upsert participations (attended/absent)"]
Uses upsert — creates participation if it doesn't exist, updates if it does.
Status Transitions
stateDiagram-v2
invited --> confirmed: self-signup
invited --> waitlist: self-signup (full)
invited --> declined: self-action
confirmed --> waitlist: officer toggle
confirmed --> attended: post-event
confirmed --> benched: post-event
confirmed --> absent: post-event
waitlist --> confirmed: officer promote
waitlist --> attended: post-event
waitlist --> benched: post-event
waitlist --> absent: post-event
See Event Lifecycle for phase-aware status rules.
Pending Event Invites
For unclaimed roster members, invites are stored in PendingEventInvite:
| Field | Type | Description |
|---|---|---|
eventId | UUID | Target event |
rosterMemberId | UUID | Unclaimed guild member |
invitedBy | UUID | Officer who sent the invite |
roleKey | string | Assigned slot role |
Conversion trigger: When a user claims a roster member (character sync or manual claim), ConvertPendingInvitesUseCase runs:
- Find all pending invites for the roster member
- For each: check if participation already exists (skip if so)
- Create participation with
status=invited - Publish
SignupCreatedEventfor each - Delete all pending invites
Domain Events
| Event | Published When | Used By |
|---|---|---|
SignupCreatedEvent | New signup or invite created | Discord announcements |
SignupRemovedEvent | User withdraws from event | Discord cleanup |
Attendance Statistics
Officers can view per-character attendance stats across completed events via:
GET /organizations/:id/attendance-stats?since=<ISO date>&eventType=<type>
Auth: Requires canViewAttendance permission (automatically granted to roles with canManageEvents).
Response shape
{
"data": [
{
"key": "char-uuid",
"characterId": "char-uuid",
"characterName": "Alicehealz",
"characterClass": "Priest",
"characterSpec": "Holy",
"userId": "user-uuid",
"userDisplayName": "Alice",
"attended": 8,
"benched": 1,
"absent": 1,
"total": 10,
"percentage": 80,
"roleBreakdown": [
{
"roleKey": "healer",
"attended": 7,
"benched": 1,
"absent": 1,
"total": 9,
"percentage": 78
},
{
"roleKey": "dps",
"attended": 1,
"benched": 0,
"absent": 0,
"total": 1,
"percentage": 100
}
]
}
],
"meta": {
"totalCompletedEvents": 10,
"since": "2025-11-01T00:00:00.000Z",
"eventType": "raid"
}
}
Design notes
- Grouped by character (not user). One user with multiple alts produces multiple rows — each character's attendance is independent.
- If a participation has no
characterId, it falls back to auser:<userId>key so it still appears in results. roleBreakdownaggregates theroleKeyfield on each participation. Empty array when no role data was recorded.- Roles are ordered:
tank → healer → dps → (others alphabetically). - Results sorted by
percentagedescending, thenattendeddescending. - Only
attended,benched, andabsentstatuses are counted.invited,confirmed,waitlist, anddeclinedare excluded.
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /events/:id/signup | Required | Self-signup |
| DELETE | /events/:id/signup | Required | Remove own signup |
| POST | /events/:id/invite | Officer | Invite member |
| POST | /events/:id/attendance | Officer | Record attendance |
| PATCH | /events/:id/participations/:uid | Officer | Update participation |
| GET | /organizations/:id/attendance-stats | Officer | Per-character stats |