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