Skip to content

feat(events): waitlist and capacity enforcement#294

Open
BrunaDomingues wants to merge 5 commits into
he4rt:feat/eventsfrom
BrunaDomingues:feat/events-enrollment-capacity-waitlist
Open

feat(events): waitlist and capacity enforcement#294
BrunaDomingues wants to merge 5 commits into
he4rt:feat/eventsfrom
BrunaDomingues:feat/events-enrollment-capacity-waitlist

Conversation

@BrunaDomingues
Copy link
Copy Markdown

Summary

Completes atomic capacity enforcement and waitlist (FIFO) on top of the RSVP flow (#275): when an event is full, participants are waitlisted if the policy allows, or rejected with 422 otherwise. Occupied seats count confirmed, checked_in, and attended enrollments; audit trail and domain events are emitted on enroll.

Capacity & waitlist (EnrollUserAction)

  • scopeActive() on Enrollment: confirmed + checked_in + attended occupy capacity (replaces counting only confirmed in capacity resolution)
  • Inside DB::transaction, lockForUpdate() on event and enrollment policy (unchanged from feat(events): rsvp enrollment end-to-end #275; ensures fresh state under concurrent enrollments)
  • If capacity is null → always confirmed
  • If active count < capacityconfirmed + EnrollmentConfirmed
  • If active count >= capacity and has_waitlist=truewaitlisted with waitlist_position = max(position) + 1 + EnrollmentWaitlisted (no EnrollmentConfirmed)
  • If active count >= capacity and has_waitlist=falseEnrollmentException::eventFull() (HTTP 422)
  • EnrollmentTransition recorded for every enroll (from_status=null, to_status=<initial>, triggered_by=user)

Domain event

  • EnrollmentWaitlisted: implements ShouldDispatchAfterCommit (payload: enrollment_id, event_id, user_id, waitlist_position) — no listener in this slice

App panel (participant)

  • Event detail: persistent copy “You are on the waitlist (position X)” when waitlisted
  • Event detail: “This event is full” when at capacity without waitlist (no Confirm Presence button)
  • Success notification uses waitlist message with position after RSVP
  • canConfirmPresence / isEventFull use active() scope (aligned with backend)

Admin

  • Enrollments relation manager: waitlist_position column (toggleable)
  • Status filter unchanged (confirmed, waitlisted, etc.)

Scopes (Enrollment model)

  • scopeConfirmed(), scopeWaitlisted(), scopeActive()active = capacity-occupying statuses only

Architecture

Events module └── Enrollment domain ├── EnrollUserAction (capacity via active() + lockForUpdate) ├── EnrollmentWaitlisted (domain event) └── Enrollment ├── scopeConfirmed / scopeWaitlisted / scopeActive

App panel └── EventDetail (+ waitlist copy, event full state)

Admin panel └── EnrollmentsRelationManager (+ waitlist_position column)

Files (high level)

Area Count / notes
Actions EnrollUserAction (active count, dispatch EnrollmentWaitlisted)
Domain events EnrollmentWaitlisted
Models Enrollment (scopeActive fix)
Enums EnrollmentStatus::getResponseMessage(?waitlistPosition)
Lang en / pt_BR pages (waitlist position, event full)
App panel EventDetail, event-detail.blade.php
Admin EnrollmentsRelationManager
Tests EnrollmentScopeTest, EnrollUserActionTest (+ capacity/FIFO/422/unlimited), RsvpEnrollmentTest (+ UI)

Out of scope (future slices)

  • Promote from waitlist on cancellation (FIFO promotion job/action)
  • Notifications listener for EnrollmentWaitlisted
  • True parallel-process concurrency test (race on last seat)
  • Gamification listener for EnrollmentConfirmed

Closes #241
Parent: #237
Blocked by / builds on: #240, #275


Test plan

  • php artisan test --filter=EnrollmentScopeTest
  • php artisan test --filter=EnrollUserActionTest
  • php artisan test --filter=RsvpEnrollmentTest
  • ./vendor/bin/pint --test

Admin

  • Edit event with RSVP policy → set capacity and has_waitlist
  • Open Enrollments tab → confirm Waitlist column and filter by waitlisted

App — capacity / waitlist

  • Event without capacity limit → Confirm PresenceConfirmed
  • Fill to capacity with waitlist → next enroll → Waitlisted badge + “position X” on detail + success notification with position
  • Fill to capacity without waitlist → “This event is full”, no Confirm Presence button
  • Existing enrollment in checked_in occupies last seat → new user → waitlisted (validates active() scope)

App — API / action edge cases

  • Enroll when full without waitlist → EnrollmentException / 422 (event_full)
  • Multiple enrollments beyond capacity with waitlist → positions 1, 2, … (FIFO)

Treat confirmed, checked-in, and attended enrollments as occupying seats when resolving RSVP capacity and waitlist placement.
Emit after-commit event when RSVP enrollment is placed on the waitlist, for future notifications.
Display waitlist position after RSVP and when the event has no remaining seats without a waitlist.
Assert 422 on full events, fifo waitlist positions, and that active seats never exceed capacity under rapid enrollments.
@BrunaDomingues BrunaDomingues self-assigned this May 29, 2026
Copy link
Copy Markdown

@GabrielFVDev GabrielFVDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants