Skip to main content

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:

  1. Event must exist and be published
  2. User must be an active guild member
  3. For invite_only events, user must already have an invited participation
  4. No duplicate signups (unique constraint on eventId + userId)
  5. Character (if provided) must belong to the user and have guild membership
  6. 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:

FieldTypeDescription
eventIdUUIDTarget event
rosterMemberIdUUIDUnclaimed guild member
invitedByUUIDOfficer who sent the invite
roleKeystringAssigned slot role

Conversion trigger: When a user claims a roster member (character sync or manual claim), ConvertPendingInvitesUseCase runs:

  1. Find all pending invites for the roster member
  2. For each: check if participation already exists (skip if so)
  3. Create participation with status=invited
  4. Publish SignupCreatedEvent for each
  5. Delete all pending invites

Domain Events​

EventPublished WhenUsed By
SignupCreatedEventNew signup or invite createdDiscord announcements
SignupRemovedEventUser withdraws from eventDiscord 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 a user:<userId> key so it still appears in results.
  • roleBreakdown aggregates the roleKey field on each participation. Empty array when no role data was recorded.
  • Roles are ordered: tank → healer → dps → (others alphabetically).
  • Results sorted by percentage descending, then attended descending.
  • Only attended, benched, and absent statuses are counted. invited, confirmed, waitlist, and declined are excluded.

API Endpoints​

MethodPathAuthDescription
POST/events/:id/signupRequiredSelf-signup
DELETE/events/:id/signupRequiredRemove own signup
POST/events/:id/inviteOfficerInvite member
POST/events/:id/attendanceOfficerRecord attendance
PATCH/events/:id/participations/:uidOfficerUpdate participation
GET/organizations/:id/attendance-statsOfficerPer-character stats