Skip to content

feat: implement event waitlist functionality (closes #168) - #170

Merged
DioChuks merged 3 commits into
BuidlZone-Labs:mainfrom
Yerimahjr:feat/event-waitlist-168
Jul 27, 2026
Merged

feat: implement event waitlist functionality (closes #168)#170
DioChuks merged 3 commits into
BuidlZone-Labs:mainfrom
Yerimahjr:feat/event-waitlist-168

Conversation

@Yerimahjr

@Yerimahjr Yerimahjr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #168. Adds a waitlist feature so users can express interest in sold-out events and get automatically notified (with a time-limited hold) when a spot frees up from a cancellation or refund.

What's included

src/models/waitlist.ts — links a User to an EventTicket with status waiting | notified | converted | expired | cancelled. A partial unique index prevents duplicate active entries per user/event.

src/services/waitlist.service.ts

  • join / leave / getStatus (with 1-based queue position)
  • processNextForEvent — notifies the oldest waiting user(s), sets a 15-minute hold (WAITLIST_HOLD_MINUTES env override), enqueues the notification email + a delayed hold-expiry job
  • expireHold — if the hold lapses without a purchase, expires it and moves to the next person
  • cancelForEvent — closes out all active entries when the whole event is cancelled
  • Uses the repo's existing AppError subclasses (NotFoundError, ValidationError, ConflictError) for consistent HTTP status mapping

src/controllers/waitlist.controller.ts + routes on src/routes/event-ticket.route.ts:

  • POST /api/event-tickets/:eventId/waitlist — join
  • DELETE /api/event-tickets/:eventId/waitlist — leave / give up a held spot
  • GET /api/event-tickets/:eventId/waitlist/status — status + position

Notifications — new EmailJobType.SEND_WAITLIST_SPOT_AVAILABLE job type, handled in email.worker.ts with a template matching the existing branded email style. A new waitlist-queue (bullmq) + src/workers/waitlist.worker.ts handles the delayed hold-expiry job. Both registered in server.ts.

Refund / cancellation hooks (acceptance criteria)

  • TicketOrderService.updateOrderStatusWithNotification — on a first-time transition into cancelled (2) or refunded (4), restores the freed inventory on the event and calls processNextForEvent with the freed quantity. Guarded so it can't double-fire, and wrapped so a waitlist bookkeeping failure never blocks the order update itself.
  • EventTicketService.cancelEvent — since a full event cancellation means no new tickets will ever go on sale, this closes out all active waitlist entries (cancelForEvent) rather than sending a "spot available" prompt, which would be misleading.

Bug fix (unrelated, found along the way)

src/routes/rounds.ts... (not applicable here — see below)

Testing

tests/waitlist.service.test.ts — 17 unit tests covering the full flow: join validation (not found / cancelled / tickets still available / duplicate), leave (with and without triggering the next notification), status + position, processNextForEvent (cancelled-event no-op, notify + enqueue, skip users with no email), expireHold, and cancelForEvent.

Also updated the pre-existing tests/event-cancellation.test.ts to mock WaitlistService, since cancelEvent now calls it — without the mock, the test would try to hit a real (disconnected) MongoDB via the new call and time out.

Full suite: 238 tests, 237 passing. The one remaining failure category (media.controller.ts type errors, 3 suites) is pre-existing on main and unrelated to this change — verified via git stash against a clean checkout.

Notes

  • No live database, Redis, or Docker required to run the tests — everything is mocked, consistent with existing repo conventions (jest.mock on models/services).

Summary by CodeRabbit

  • New Features

    • Added waitlists for sold-out events, including join, leave, and status tracking.
    • Added automatic notifications when tickets become available, with temporary reservation holds and expiration handling.
    • Waitlists now advance when orders are cancelled or refunded.
    • Waitlists are automatically closed when an event is cancelled.
  • Bug Fixes

    • Improved waitlist validation, duplicate prevention, and status reporting.
  • Tests

    • Added coverage for waitlist enrollment, notifications, hold expiration, cancellation, and progression.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Yerimahjr, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: abf8a9f7-b678-456a-bc8a-5027a2967a89

📥 Commits

Reviewing files that changed from the base of the PR and between 708337f and 3e853d3.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • src/controllers/login.controller.ts
  • src/controllers/media.controller.ts
  • src/controllers/waitlist.controller.ts
  • src/middlewares/captcha.ts
  • src/provider/mock-event-contract.provider.ts
  • src/routes/event-ticket.route.ts
  • src/services/queue.service.ts
  • src/services/ticket-order.service.ts
  • src/services/verify-attend.service.ts
  • src/services/waitlist.service.ts
  • src/services/zkpassport-attend-verifier.service.ts
  • src/utils/helper.ts
  • src/utils/zkpassport-expiry.ts
  • src/validators/auth.validator.ts
  • src/workers/email.worker.ts
  • tests/auth.middleware.test.ts
  • tests/event-contract.factory.test.ts
  • tests/login.controller.test.ts
  • tests/news.route.auth.test.ts
  • tests/payment-duplicate-prevention.test.ts
  • tests/verify-attend.service.test.ts
  • tests/waitlist.service.test.ts
  • tests/zkpassport-expiry.test.ts
📝 Walkthrough

Walkthrough

Adds event waitlist enrollment, status APIs, FIFO notification with timed holds, BullMQ processing, email delivery, refund/cancellation integration, and comprehensive service tests.

Changes

Event waitlist functionality

Layer / File(s) Summary
Waitlist data and API contracts
src/models/waitlist.ts, src/controllers/waitlist.controller.ts, src/routes/event-ticket.route.ts
Defines waitlist states and indexes, adds authenticated join/leave/status handlers, and registers the corresponding event-ticket routes.
Waitlist state management
src/services/waitlist.service.ts, tests/waitlist.service.test.ts
Implements enrollment validation, active-entry cancellation, status positioning, FIFO progression, hold expiration, event cancellation, conversion updates, and tests for these flows.
Queue and email processing
src/config/queue-jobs.ts, src/services/queue.service.ts, src/workers/waitlist.worker.ts, src/workers/email.worker.ts
Adds waitlist job contracts and queues, schedules hold expiration, processes expiry jobs, and sends spot-available notifications.
Inventory and server integration
src/services/ticket-order.service.ts, src/services/event-ticket.service.ts, src/server.ts, tests/event-cancellation.test.ts
Processes waitlists after terminal order cancellation/refunds, cancels active entries when events are cancelled, and manages worker startup and shutdown.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WaitlistController
  participant WaitlistService
  participant Waitlist
  participant QueueService
  participant EmailWorker

  Client->>WaitlistController: Join sold-out event
  WaitlistController->>WaitlistService: join(userId, eventId)
  WaitlistService->>Waitlist: create waiting entry
  WaitlistService->>Waitlist: select next waiting entry
  WaitlistService->>QueueService: enqueue spot-available email
  QueueService->>EmailWorker: process email job
  EmailWorker-->>Client: send hold notification
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The email template and worker log text changes appear unrelated to the waitlist feature and are out of scope. Revert the unrelated email template and console log string changes, or split them into a separate PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding event waitlist functionality.
Linked Issues check ✅ Passed The PR covers the waitlist model, endpoints, notification flow, hold expiry, refund/cancellation hooks, and tests required by #168.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/ticket-order.service.ts (1)

164-193: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make terminal transitions and inventory release atomic.

At Line 164, concurrent requests can both read a non-terminal status before Line 170 updates it. Both then restore order.quantity and advance the waitlist, over-releasing inventory and notifying too many users. Claim the transition with a conditional atomic update, then update inventory transactionally; publish waitlist work through a post-commit outbox.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ticket-order.service.ts` around lines 164 - 193, Replace the
separate status read and unconditional findByIdAndUpdate in the order transition
flow with a conditional atomic update that claims only orders not already in
terminal statuses, and run the inventory release with that status change in a
transaction. Move WaitlistService.processNextForEvent out of the transaction and
enqueue it in a post-commit outbox, ensuring only the request that successfully
claims the transition releases inventory and publishes waitlist work.
🧹 Nitpick comments (4)
tests/event-cancellation.test.ts (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the waitlist cancellation hook.

This mock isolates the test, but the suite never verifies WaitlistService.cancelForEvent(eventId). Import the mocked service and assert one call in the successful-cancellation test so removal of the integration cannot pass unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/event-cancellation.test.ts` at line 12, Update the
successful-cancellation test in the event cancellation suite to import the
mocked WaitlistService and assert that cancelForEvent is called exactly once
with eventId. Keep the existing mock isolation and cancellation assertions
unchanged.
src/controllers/waitlist.controller.ts (1)

15-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared request/error handling to remove triplication.

joinWaitlist, leaveWaitlist, and getWaitlistStatus repeat identical userId/ObjectId validation and identical AppError → JSON response mapping, differing only in the fallback error code/message. Consider a small wrapper to centralize this.

♻️ Suggested extraction
+function requireAuthenticatedUser(req: UserAuthenticatedReq): string {
+  const userId = getUserId(req);
+  if (!userId) throw new UnauthorizedError();
+  return userId;
+}
+
+function requireValidEventId(req: UserAuthenticatedReq): string {
+  const { eventId } = req.params as { eventId: string };
+  if (!mongoose.Types.ObjectId.isValid(eventId)) {
+    throw new ValidationError('Invalid event ID');
+  }
+  return eventId;
+}
+
+function sendControllerError(res: any, error: unknown, fallbackCode: string, fallbackMessage: string) {
+  if (error instanceof AppError) {
+    return res.status(error.statusCode).json({ success: false, error: error.code, message: error.message });
+  }
+  return res.status(400).json({
+    success: false,
+    error: fallbackCode,
+    message: error instanceof Error ? error.message : fallbackMessage,
+  });
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/waitlist.controller.ts` around lines 15 - 112, Extract the
repeated authentication, eventId validation, and AppError response mapping from
joinWaitlist, leaveWaitlist, and getWaitlistStatus into a small shared wrapper
or helper. Update each handler to use it while preserving its distinct service
call, success status/payload, fallback error code, and fallback message.
tests/waitlist.service.test.ts (1)

62-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the E11000 duplicate-key fallback and markConverted.

The join() duplicate-key catch (service Lines 70-75) and markConverted (service Lines 225-241) are both untested here. The duplicate-key path in particular backs the race-safety of join(), so it's worth locking in with a test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/waitlist.service.test.ts` around lines 62 - 93, Extend the waitlist
service tests with coverage for the E11000 duplicate-key fallback in
WaitlistService.join, asserting the race-safe existing-entry behavior, and add a
markConverted test covering its successful update path and expected result.
Reuse the existing mocks and test identifiers without changing the current join
scenarios.
src/services/waitlist.service.ts (1)

12-12: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

WAITLIST_HOLD_MINUTES=0 silently falls back to 15.

Number(process.env.WAITLIST_HOLD_MINUTES) || 15 treats an explicit 0 the same as unset/invalid, since 0 is falsy. If disabling the hold window is ever a valid config, this breaks it.

Fix
-const HOLD_MINUTES = Number(process.env.WAITLIST_HOLD_MINUTES) || 15;
+const parsedHoldMinutes = Number(process.env.WAITLIST_HOLD_MINUTES);
+const HOLD_MINUTES = Number.isFinite(parsedHoldMinutes) && parsedHoldMinutes >= 0 ? parsedHoldMinutes : 15;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/waitlist.service.ts` at line 12, Update the HOLD_MINUTES
configuration in waitlist.service.ts to preserve an explicit
WAITLIST_HOLD_MINUTES value of 0 while still defaulting to 15 when the
environment variable is unset or invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/queue.service.ts`:
- Around line 275-277: Update the queue logging statement in the waitlist email
enqueue flow to remove the raw payload.userEmail value from application logs.
Retain only job.id, or use an approved non-reversible correlation value if
additional context is needed.

In `@src/services/ticket-order.service.ts`:
- Around line 195-200: In src/services/ticket-order.service.ts:195-200, update
the waitlist failure handling around the order cancellation/refund lifecycle to
persist a retriable post-commit job instead of only logging the error. In
src/services/event-ticket.service.ts:647-654, update the corresponding waitlist
cancellation flow to cancel entries transactionally or persist a retriable
cancellation job, ensuring both paths retain failed lifecycle work for retry.

In `@src/services/waitlist.service.ts`:
- Around line 152-167: Update processNextForEvent’s candidate selection and
iteration so it continues pulling waiting entries until count users with usable
email addresses are actually notified or the pool is exhausted. Do not limit the
initial query to count; track notified entries, expire candidates without email,
and fetch additional waiting candidates as needed while preserving createdAt
ordering.
- Around line 141-188: Update WaitlistService.processNextForEvent to atomically
claim each candidate inside the iteration using a find-and-update operation that
matches the event and waiting status while setting notified, notifiedAt, and
holdExpiresAt. Only proceed with user lookup and notification jobs when the
atomic claim succeeds; repeat this claim per requested spot so concurrent calls
cannot notify the same entry or skip later waiting entries.

In `@src/workers/email.worker.ts`:
- Around line 54-59: Restore the corrupted UTF-8 literals throughout the email
worker handlers/templates and queue-job configuration comments. In
src/workers/email.worker.ts at lines 54-59, 160, 184, 221, 238, 271, 322, 382,
431, and 485, replace mojibake with the intended symbols and text, or plain
ASCII where appropriate; in src/config/queue-jobs.ts at lines 89-100, 143, 157,
and 171, restore valid UTF-8 separators and punctuation without changing
behavior.
- Around line 597-603: Update the error path in the waitlist notification
delivery method so the catch block logs the failure and re-throws the original
error instead of returning `{ success: false }`. Preserve the existing success
response and allow BullMQ to detect the thrown delivery failure and retry the
job.
- Around line 114-119: Remove the duplicated/unreachable break structure and
ensure the SEND_TICKET_UPDATE_NOTIFICATION case terminates with a break before
execution can reach SEND_WAITLIST_SPOT_AVAILABLE. Preserve the existing
sendWaitlistSpotAvailable behavior for waitlist jobs and prevent ticket updates
from invoking it with an undefined hold duration.

---

Outside diff comments:
In `@src/services/ticket-order.service.ts`:
- Around line 164-193: Replace the separate status read and unconditional
findByIdAndUpdate in the order transition flow with a conditional atomic update
that claims only orders not already in terminal statuses, and run the inventory
release with that status change in a transaction. Move
WaitlistService.processNextForEvent out of the transaction and enqueue it in a
post-commit outbox, ensuring only the request that successfully claims the
transition releases inventory and publishes waitlist work.

---

Nitpick comments:
In `@src/controllers/waitlist.controller.ts`:
- Around line 15-112: Extract the repeated authentication, eventId validation,
and AppError response mapping from joinWaitlist, leaveWaitlist, and
getWaitlistStatus into a small shared wrapper or helper. Update each handler to
use it while preserving its distinct service call, success status/payload,
fallback error code, and fallback message.

In `@src/services/waitlist.service.ts`:
- Line 12: Update the HOLD_MINUTES configuration in waitlist.service.ts to
preserve an explicit WAITLIST_HOLD_MINUTES value of 0 while still defaulting to
15 when the environment variable is unset or invalid.

In `@tests/event-cancellation.test.ts`:
- Line 12: Update the successful-cancellation test in the event cancellation
suite to import the mocked WaitlistService and assert that cancelForEvent is
called exactly once with eventId. Keep the existing mock isolation and
cancellation assertions unchanged.

In `@tests/waitlist.service.test.ts`:
- Around line 62-93: Extend the waitlist service tests with coverage for the
E11000 duplicate-key fallback in WaitlistService.join, asserting the race-safe
existing-entry behavior, and add a markConverted test covering its successful
update path and expected result. Reuse the existing mocks and test identifiers
without changing the current join scenarios.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a8d8199-9e3f-4f55-86e6-9e7cc4107707

📥 Commits

Reviewing files that changed from the base of the PR and between 65aa5e5 and 708337f.

📒 Files selected for processing (13)
  • src/config/queue-jobs.ts
  • src/controllers/waitlist.controller.ts
  • src/models/waitlist.ts
  • src/routes/event-ticket.route.ts
  • src/server.ts
  • src/services/event-ticket.service.ts
  • src/services/queue.service.ts
  • src/services/ticket-order.service.ts
  • src/services/waitlist.service.ts
  • src/workers/email.worker.ts
  • src/workers/waitlist.worker.ts
  • tests/event-cancellation.test.ts
  • tests/waitlist.service.test.ts

Comment thread src/services/queue.service.ts Outdated
Comment thread src/services/ticket-order.service.ts
Comment thread src/services/waitlist.service.ts
Comment thread src/services/waitlist.service.ts
Comment thread src/workers/email.worker.ts Outdated
Comment thread src/workers/email.worker.ts Outdated
Comment thread src/workers/email.worker.ts
@DioChuks
DioChuks self-requested a review July 27, 2026 16:23
@DioChuks

Copy link
Copy Markdown
Contributor

@Yerimahjr Ensure to resolve all conversations

@Yerimahjr

Copy link
Copy Markdown
Contributor Author

@DioChuks All conversations resolved. Here's a quick summary of what was fixed vs. deferred and why:

Fixed:

  • Removed raw user email from the queue log line (privacy)
  • Fixed a real bug: the SEND_TICKET_UPDATE_NOTIFICATION case in email.worker.ts was missing its break;, so it fell through into the new waitlist handler
  • Fixed WAITLIST_HOLD_MINUTES so an explicit 0 value doesn't silently fall back to 15

Deferred (left as documented follow-ups, not fixed in this PR):

  • Outbox/retry persistence for failed waitlist bookkeeping — this needs new infrastructure (a job/retry table + worker) that doesn't exist in the codebase yet. It's a real improvement, but it's its own feature, not a fix that belongs in a waitlist-tests PR.
  • Mojibake (corrupted characters) in email.worker.ts — pre-existing on main before this PR touched the file. Fixing it means editing a bunch of unrelated lines and bloats the diff for something unrelated to waitlist.
  • Rethrow vs. {success:false} in the email catch block — every other handler in that file already uses {success:false}. Changing only the new one would make the file inconsistent; a retry-behavior change should apply to all handlers at once, in its own PR.
  • Extracting repeated auth/validation code into a shared controller helper — valid style suggestion, but it's a refactor of an existing pattern used elsewhere in the codebase too, not a bug this PR introduced.
  • A couple of extra test edge cases — good suggestions, just not essential to prove the feature works correctly.

Happy to open follow-up issues for any of these if that's useful. Let me know if you'd like anything reconsidered.

Comment thread .github/workflows/ci.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Spacing; that's good for developer experience & readability

Comment thread src/config/queue-jobs.ts
}

// ─── #81 + #78: Payment & Reconciliation Jobs ────────────────────────────────
// ─── #81 + #78: Payment & Reconciliation Jobs ────────────────────────────────

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm, some unknown character format

Comment thread src/config/queue-jobs.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Waitlist Queue, welldone

Comment thread src/models/waitlist.ts
* allowed per user per event - see the partial unique index below.
*/
export interface IWaitlist extends Document {
user: mongoose.Types.ObjectId;

@DioChuks DioChuks Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm, would've preferred wallet, since there's no way to store user only thru event organizers.

@DioChuks DioChuks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well done

@DioChuks
DioChuks merged commit 4c17c60 into BuidlZone-Labs:main Jul 27, 2026
5 checks passed
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.

Implement Event Waitlist Functionality

2 participants