Skip to content

Feat/event waitlist auto promotion - #256

Open
FedericoV21 wants to merge 6 commits into
programaconnosotros:testingfrom
FedericoV21:feat/event-waitlist-auto-promotion
Open

Feat/event waitlist auto promotion#256
FedericoV21 wants to merge 6 commits into
programaconnosotros:testingfrom
FedericoV21:feat/event-waitlist-auto-promotion

Conversation

@FedericoV21

@FedericoV21 FedericoV21 commented Apr 10, 2026

Copy link
Copy Markdown

Resumen

Se implementó soporte de inscripciones fuera de cupo mediante lista de espera para eventos con capacidad limitada, incluyendo promoción automática del siguiente usuario cuando se libera un lugar.

Cambios principales

  • Se agregó el modelo EventWaitlistEntry en Prisma y su migración.
  • Se incorporó lógica centralizada de cupo/lista de espera/promoción en src/actions/events/event-capacity.ts.
  • Se actualizó registerEvent para:
    • confirmar inscripción cuando hay cupo,
    • agregar a lista de espera cuando el cupo está completo,
    • evitar duplicados de inscripción/lista de espera por usuario.
  • Se actualizó cancelRegistration y deleteRegistration para:
    • liberar cupo,
    • promover automáticamente al siguiente usuario en espera (FIFO).
  • Se actualizó la UI del detalle de evento:
    • botón dinámico (Inscribirme / Sumarme a lista de espera),
    • estado de usuario en waitlist y posición.
  • Se actualizó la vista admin de inscripciones para mostrar demanda fuera de cupo y tabla de espera activa.
  • Se mantuvieron notificaciones a admins para altas, bajas y promociones.

Test plan

  • Crear evento con capacity baja (ej: 1 o 2).
  • Inscribir usuarios hasta completar cupo.
  • Verificar que nuevas inscripciones entren a lista de espera.
  • Verificar visualización de demanda fuera de cupo en detalle del evento y vista admin.
  • Cancelar o eliminar una inscripción activa y validar promoción automática del primero en espera.
  • Verificar salida manual de lista de espera.
  • Verificar que no se permitan duplicados (inscripción activa o waitlist repetida por usuario/evento).

Evidencia (UI)

  • Captura/video: cupo completo.
image image - [ ] Captura/video: alta en lista de espera. image image - [ ] Captura/video: promoción automática al liberar cupo. image

Summary by CodeRabbit

Release Notes

  • New Features

    • Event waitlist functionality—users can now join a waiting list when events reach full capacity
    • Waitlist position tracking—users can view their position in the queue
    • Automatic promotion—users are promoted from the waitlist when registration spots become available
    • Admin waitlist visibility—administrators can view and manage event waitlists
  • Bug Fixes

    • Improved meetup page redirect behavior
    • Updated external sponsor link

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

A new event waitlist system is introduced with database schema changes, server-side capacity and promotion logic, and updated UI components to allow users to register or join waitlists when events reach capacity.

Changes

Cohort / File(s) Summary
Database Schema
prisma/migrations/20260410120000_add_event_waitlist/migration.sql, prisma/schema.prisma
Added EventWaitlistEntry table with relationships to Event and User. Defined unique constraint on (eventId, userId) pair and indexes on eventId, userId, cancelledAt, and promotedAt. Extended Event and User models with inverse relationships.
Capacity Management
src/actions/events/event-capacity.ts
New module providing getCapacitySnapshot() to compute active registrations and waitlist counts, getWaitlistPosition() to retrieve user's position, and promoteNextFromWaitlist() to safely promote waitlisted users via serializable transactions with cascade handling for existing registrations.
Event Registration Flow
src/actions/events/register-event.ts
Refactored registration into a single serializable transaction that discriminates between registered and waitlisted outcomes. Returns RegistrationResult with status-specific metadata (registrationId or waitlistId+position). Updated notifications and redirects based on registration status.
Cancellation & Deletion
src/actions/events/cancel-registration.ts, src/actions/events/delete-registration.ts
Extended cancellation to handle both active registrations and waitlist entries. Added capacity-aware side effects: cancelling a registration triggers promoteNextFromWaitlist(). Return type now includes cancellation status ('cancelled_registration' or 'cancelled_waitlist'). Notifications differentiate between registration and waitlist cancellations.
Capacity Queries
src/actions/events/check-event-capacity.ts
Refactored to use getCapacitySnapshot() instead of direct Prisma queries. Extended return payload to include waitlistCount. Updated messaging to reflect waitlist availability when at capacity.
Event Detail Pages
src/app/(platform)/eventos/[id]/page.tsx, src/app/(platform)/eventos/[id]/inscripciones/page.tsx
Updated to fetch and display waitlist information. Added isWaitlisted and waitlistPosition state. Event detail page now shows waitlist count in capacity info. Inscriptions page now displays a new "Lista de espera" card with position, user details, and entry date.
Event UI Components
src/components/events/event-detail-client.tsx, src/components/events/register-event-button.tsx, src/components/events/cancel-registration-button.tsx
Extended EventDetailClient to accept isWaitlisted and waitlistPosition props. Updated RegisterEventButton to call registerEvent() directly and branch on result status. Added mode prop to CancelRegistrationButton to differentiate between registration cancellation and waitlist removal. Updated button labels, toasts, and disabled states accordingly.
Miscellaneous
src/app/meetup/page.tsx, src/app/(platform)/testimonios/[id]/page.tsx, src/components/home/sponsors-section.tsx
Updated meetup page redirect from registration endpoint to event detail page and title formatting. Minor formatting change to testimonials metadata. Updated sponsor URL for Endpoint Consulting.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Event UI Component
    participant RegisterAction as registerEvent Action
    participant DB as Database Transaction
    participant CapacityAction as promoteNextFromWaitlist
    participant NotifyAction as notifyAdmins

    User->>UI: Click "Inscribirme"
    UI->>RegisterAction: registerEvent(eventId)
    RegisterAction->>DB: Start Serializable Transaction
    DB->>DB: Check event capacity
    alt Capacity Available
        DB->>DB: Create eventRegistration
        DB->>DB: Cancel any pending waitlist entry
        DB-->>RegisterAction: { status: 'registered', registrationId }
        RegisterAction->>NotifyAction: emit event_registration_created
    else Capacity Full
        DB->>DB: Create eventWaitlistEntry
        DB->>DB: Calculate waitlist position
        DB-->>RegisterAction: { status: 'waitlisted', waitlistId, position }
        RegisterAction->>NotifyAction: emit event_registered_to_waitlist
    end
    RegisterAction-->>UI: RegistrationResult
    UI->>User: Show status-specific toast & redirect

    User->>UI: Delete Registration
    UI->>RegisterAction: deleteRegistration(registrationId)
    RegisterAction->>DB: Remove eventRegistration
    RegisterAction->>CapacityAction: promoteNextFromWaitlist(eventId)
    CapacityAction->>DB: Start Serializable Transaction
    alt Waitlist Exists
        DB->>DB: Get next waitlisted user
        DB->>DB: Create/reactivate registration
        DB->>DB: Mark waitlist entry as promoted
        DB-->>CapacityAction: { registrationId, userId, userName }
        CapacityAction-->>RegisterAction: Promotion details
        RegisterAction->>NotifyAction: emit event_waitlist_promoted
    end
    RegisterAction-->>UI: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #17: Adds foundational Event model and initial server actions for event management; this PR extends the same domain with waitlist infrastructure, capacity queries, and updated registration flows that depend on the Event schema.

Poem

🐰 Hop, hop, the waitlist grows,
When events are full, as everyone knows,
We queue and we wait in lines so neat,
Then promoted with glee when spots are sweet! 🎫✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/event waitlist auto promotion' accurately summarizes the main change—adding event waitlist functionality with automatic promotion when capacity becomes available.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@agustin-sanc

Copy link
Copy Markdown
Collaborator

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 8

Caution

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

⚠️ Outside diff range comments (1)
src/app/meetup/page.tsx (1)

67-86: ⚠️ Potential issue | 🟡 Minor

Keep the metadata titles aligned.

title and twitter.title were updated, but openGraph.title still uses the old format on Line 74. Shares from social crawlers will keep showing a different title than the page itself.

Suggested fix
   return {
     title: `${nextMeetup.name} (PCN)`,
@@
     openGraph: {
-      title: `${nextMeetup.name} - Meetup - PCN`,
+      title: `${nextMeetup.name} (PCN)`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/meetup/page.tsx` around lines 67 - 86, Update the Open Graph title to
match the page and Twitter title format: change the openGraph.title expression
in the returned metadata object so it uses the same template as title and
twitter.title (i.e. `${nextMeetup.name} (PCN)`), ensuring openGraph.title,
title, and twitter.title are identical and use nextMeetup.name consistently.
🧹 Nitpick comments (1)
prisma/schema.prisma (1)

219-223: Add a composite index for the waitlist hot path.

The new queries in src/actions/events/event-capacity.ts and src/actions/events/register-event.ts consistently filter by eventId, cancelledAt, and promotedAt, then sort/count by createdAt. Single-column indexes won’t help that pattern much once waitlists grow.

Suggested change
 model EventWaitlistEntry {
   …
   @@unique([eventId, userId])
+  @@index([eventId, cancelledAt, promotedAt, createdAt])
   @@index([eventId])
   @@index([userId])
   @@index([cancelledAt])
   @@index([promotedAt])
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@prisma/schema.prisma` around lines 219 - 223, Add a composite index to the
Prisma model indexes to support the waitlist hot path used in
src/actions/events/event-capacity.ts and src/actions/events/register-event.ts:
create an @@index that includes eventId, cancelledAt, promotedAt and createdAt
(e.g., @@index([eventId, cancelledAt, promotedAt, createdAt])) alongside the
existing @@unique([eventId, userId]) and single-column @@index entries so
queries filtering by eventId/cancelledAt/promotedAt and ordering/counting by
createdAt will use the composite index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@prisma/migrations/20260410120000_add_event_waitlist/migration.sql`:
- Around line 26-27: The current unique index
EventWaitlistEntry_eventId_userId_key prevents legitimate re-joins because it
enforces uniqueness across all historical rows; replace it with a partial unique
index that only covers active rows (e.g., unique on eventId,userId WHERE
cancelledAt IS NULL AND promotedAt IS NULL) or alternatively remove the global
unique constraint and implement reactivation of historical rows in the
registration flow; update the migration to drop the existing
EventWaitlistEntry_eventId_userId_key and create the scoped unique index, and
adjust Prisma-side logic (registration/reactivation code) to match the new “one
active waitlist row per user/event” semantics.

In `@src/actions/events/cancel-registration.ts`:
- Around line 92-115: The cancellation currently calls
prisma.eventRegistration.update before calling promoteNextFromWaitlist(),
risking a freed seat without a successful promotion; change this to perform the
cancellation and the waitlist promotion in the same Prisma transaction (use
prisma.$transaction with an async callback or refactor promoteNextFromWaitlist
to accept a transactional Prisma client), run prisma.eventRegistration.update
and the promotion logic using that same transactional client, and only after the
transaction successfully commits call notifyAdmins for the promoted user (keep
function names: prisma.eventRegistration.update, promoteNextFromWaitlist or a
new promoteNextFromWaitlistTx that accepts a client, and notifyAdmins).

In `@src/actions/events/delete-registration.ts`:
- Around line 44-58: The notifyAdmins call can throw and must not abort the
delete flow; wrap the notifyAdmins(...) invocation (the block handling promoted
from promoteNextFromWaitlist(registration.eventId)) in a try/catch, so any
exception is caught and handled (log the error and continue) instead of
rethrowing; ensure the code still returns success for the deletion path even if
notifyAdmins fails, referencing promoteNextFromWaitlist, promoted, notifyAdmins
and event in your fix.

In `@src/actions/events/event-capacity.ts`:
- Around line 130-137: The code cancels a stale waitlist entry when
existingRegistration is present (within the block using
tx.eventWaitlistEntry.update on nextWaitlistEntry) but then returns null, which
stops processing further waitlist entries and leaves capacity unfilled; change
the control flow so that after marking nextWaitlistEntry.cancelledAt you do not
return but instead continue processing the loop/logic that promotes the next
eligible waitlist entry (i.e., remove the early return and either continue to
the next iteration or let the promotion logic that follows run), ensuring the
rest of the function (promotion code that reads nextWaitlistEntry and creates a
registration) can run for subsequent entries.

In `@src/actions/events/register-event.ts`:
- Around line 168-177: Count the waitlist position using the same sort key as
getWaitlistPosition by adding an id tie-breaker to the where clause: instead of
only counting entries with createdAt <= waitlistCreatedAt, count entries where
createdAt < waitlistCreatedAt OR (createdAt = waitlistCreatedAt AND id < = <this
entry's id>), so use the current waitlist entry's id (e.g. waitlistEntryId / id)
and waitlistCreatedAt in tx.eventWaitlistEntry.count to ensure stable FIFO
ordering that matches getWaitlistPosition.

In `@src/app/`(platform)/eventos/[id]/inscripciones/page.tsx:
- Around line 81-98: The admin waitlist ordering differs from the promotion
ordering; update the waitlist query used to build activeWaitlistEntries so it
orders identically to promoteNextFromWaitlist() by using orderBy: [{ createdAt:
'asc' }, { id: 'asc' }] in the prisma.eventWaitlistEntry.findMany call (keep the
same where and include clauses and the existing cancelledAt/promotedAt
filtering), ensuring the admin table position matches the actual promotion
order.

In `@src/components/events/event-detail-client.tsx`:
- Line 46: The component currently only flips justWaitlistedLocally and doesn't
persist the returned waitlist position, so the UI keeps showing the stale server
prop waitlistPosition; add a local state like waitlistPositionLocal
(useState<number | null>(null)) and on successful waitlist join in the handlers
referenced (the functions that call setJustWaitlistedLocally at the blocks
around lines 46, ~80-89, ~114-124, and ~163-170) set waitlistPositionLocal to
the position returned by the server response; then update rendering to prefer
waitlistPositionLocal if non-null (falling back to props.waitlistPosition) so
the “Tu posición actual…” message updates immediately.

In `@src/components/events/register-event-button.tsx`:
- Line 14: Update the onSuccess prop and its usage to forward the waitlist
position returned by registerEvent: change the prop signature (onSuccess) to
accept either a payload object or an extra optional parameter that includes both
status and position (e.g., { status: 'registered' | 'waitlisted', position?:
number }) and, inside the register-event-button component where registerEvent()
is called, pass the returned position when status === 'waitlisted' (use the
position property from registerEvent's response). Ensure all calls to onSuccess
(and its type) are updated so callers can read position without requiring a
refresh.

---

Outside diff comments:
In `@src/app/meetup/page.tsx`:
- Around line 67-86: Update the Open Graph title to match the page and Twitter
title format: change the openGraph.title expression in the returned metadata
object so it uses the same template as title and twitter.title (i.e.
`${nextMeetup.name} (PCN)`), ensuring openGraph.title, title, and twitter.title
are identical and use nextMeetup.name consistently.

---

Nitpick comments:
In `@prisma/schema.prisma`:
- Around line 219-223: Add a composite index to the Prisma model indexes to
support the waitlist hot path used in src/actions/events/event-capacity.ts and
src/actions/events/register-event.ts: create an @@index that includes eventId,
cancelledAt, promotedAt and createdAt (e.g., @@index([eventId, cancelledAt,
promotedAt, createdAt])) alongside the existing @@unique([eventId, userId]) and
single-column @@index entries so queries filtering by
eventId/cancelledAt/promotedAt and ordering/counting by createdAt will use the
composite index.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4c41aeea-46a7-4ffd-975f-d78985207183

📥 Commits

Reviewing files that changed from the base of the PR and between 7865bd6 and 1a7d5b8.

📒 Files selected for processing (15)
  • prisma/migrations/20260410120000_add_event_waitlist/migration.sql
  • prisma/schema.prisma
  • src/actions/events/cancel-registration.ts
  • src/actions/events/check-event-capacity.ts
  • src/actions/events/delete-registration.ts
  • src/actions/events/event-capacity.ts
  • src/actions/events/register-event.ts
  • src/app/(platform)/eventos/[id]/inscripciones/page.tsx
  • src/app/(platform)/eventos/[id]/page.tsx
  • src/app/(platform)/testimonios/[id]/page.tsx
  • src/app/meetup/page.tsx
  • src/components/events/cancel-registration-button.tsx
  • src/components/events/event-detail-client.tsx
  • src/components/events/register-event-button.tsx
  • src/components/home/sponsors-section.tsx

Comment on lines +26 to +27
-- CreateIndex
CREATE UNIQUE INDEX "EventWaitlistEntry_eventId_userId_key" ON "EventWaitlistEntry"("eventId", "userId");

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.

⚠️ Potential issue | 🟠 Major

This unique index blocks legitimate re-joins after cancel/promotion.

Because waitlist rows are kept as history via cancelledAt/promotedAt, UNIQUE(eventId, userId) means a user can never join the waitlist again for the same event after leaving it or after being promoted once. That’s stricter than “no duplicate active waitlist entry”.

If the intended rule is “one active waitlist row per user/event”, this needs either:

  • reusing/reactivating the historical row in the registration flow, or
  • a partial unique index scoped to active rows only, with the Prisma-side logic adjusted to match.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@prisma/migrations/20260410120000_add_event_waitlist/migration.sql` around
lines 26 - 27, The current unique index EventWaitlistEntry_eventId_userId_key
prevents legitimate re-joins because it enforces uniqueness across all
historical rows; replace it with a partial unique index that only covers active
rows (e.g., unique on eventId,userId WHERE cancelledAt IS NULL AND promotedAt IS
NULL) or alternatively remove the global unique constraint and implement
reactivation of historical rows in the registration flow; update the migration
to drop the existing EventWaitlistEntry_eventId_userId_key and create the scoped
unique index, and adjust Prisma-side logic (registration/reactivation code) to
match the new “one active waitlist row per user/event” semantics.

Comment on lines +92 to +115
if (registration) {
await prisma.eventRegistration.update({
where: { id: registration.id },
data: {
cancelledAt: new Date(),
},
});

const promoted = await promoteNextFromWaitlist(eventId);
if (promoted) {
await notifyAdmins({
type: 'event_waitlist_promoted',
title: 'Promoción automática desde lista de espera',
message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
metadata: {
eventId,
eventName: event.name,
registrationId: promoted.registrationId,
userId: promoted.userId,
userName: promoted.userName,
userEmail: promoted.userEmail,
},
});
}

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.

⚠️ Potential issue | 🟠 Major

Make cancellation and auto-promotion atomic.

Line 93 commits the cancellation before Line 100 calls promoteNextFromWaitlist(), and that helper runs in a separate transaction. If promotion aborts after the cancellation succeeds, the seat stays freed with nobody promoted, which breaks the auto-promotion contract of this PR. Based on learnings: "The events system handles invalid/non-existent IDs through two layers: 1. Prisma's findUnique returns null for invalid IDs 2. The EventDetailPage component displays a user-friendly message 'No se encontró el evento solicitado' when the event is null".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/events/cancel-registration.ts` around lines 92 - 115, The
cancellation currently calls prisma.eventRegistration.update before calling
promoteNextFromWaitlist(), risking a freed seat without a successful promotion;
change this to perform the cancellation and the waitlist promotion in the same
Prisma transaction (use prisma.$transaction with an async callback or refactor
promoteNextFromWaitlist to accept a transactional Prisma client), run
prisma.eventRegistration.update and the promotion logic using that same
transactional client, and only after the transaction successfully commits call
notifyAdmins for the promoted user (keep function names:
prisma.eventRegistration.update, promoteNextFromWaitlist or a new
promoteNextFromWaitlistTx that accepts a client, and notifyAdmins).

Comment on lines +44 to +58
const promoted = await promoteNextFromWaitlist(registration.eventId);
if (promoted && event) {
await notifyAdmins({
type: 'event_waitlist_promoted',
title: 'Promoción automática desde lista de espera',
message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
metadata: {
eventId: registration.eventId,
eventName: event.name,
registrationId: promoted.registrationId,
userId: promoted.userId,
userName: promoted.userName,
userEmail: promoted.userEmail,
},
});

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.

⚠️ Potential issue | 🟠 Major

Don’t fail the delete flow on notification errors.

At this point the registration is already deleted and the promotion may already be committed. If notifyAdmins() throws, the action returns an error even though the core mutation succeeded.

Suggested fix
   const promoted = await promoteNextFromWaitlist(registration.eventId);
   if (promoted && event) {
-    await notifyAdmins({
-      type: 'event_waitlist_promoted',
-      title: 'Promoción automática desde lista de espera',
-      message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
-      metadata: {
-        eventId: registration.eventId,
-        eventName: event.name,
-        registrationId: promoted.registrationId,
-        userId: promoted.userId,
-        userName: promoted.userName,
-        userEmail: promoted.userEmail,
-      },
-    });
+    try {
+      await notifyAdmins({
+        type: 'event_waitlist_promoted',
+        title: 'Promoción automática desde lista de espera',
+        message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
+        metadata: {
+          eventId: registration.eventId,
+          eventName: event.name,
+          registrationId: promoted.registrationId,
+          userId: promoted.userId,
+          userName: promoted.userName,
+          userEmail: promoted.userEmail,
+        },
+      });
+    } catch (error) {
+      console.error('Failed to notify admins about waitlist promotion', error);
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const promoted = await promoteNextFromWaitlist(registration.eventId);
if (promoted && event) {
await notifyAdmins({
type: 'event_waitlist_promoted',
title: 'Promoción automática desde lista de espera',
message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
metadata: {
eventId: registration.eventId,
eventName: event.name,
registrationId: promoted.registrationId,
userId: promoted.userId,
userName: promoted.userName,
userEmail: promoted.userEmail,
},
});
const promoted = await promoteNextFromWaitlist(registration.eventId);
if (promoted && event) {
try {
await notifyAdmins({
type: 'event_waitlist_promoted',
title: 'Promoción automática desde lista de espera',
message: `${promoted.userName} obtuvo un cupo en "${event.name}"`,
metadata: {
eventId: registration.eventId,
eventName: event.name,
registrationId: promoted.registrationId,
userId: promoted.userId,
userName: promoted.userName,
userEmail: promoted.userEmail,
},
});
} catch (error) {
console.error('Failed to notify admins about waitlist promotion', error);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/events/delete-registration.ts` around lines 44 - 58, The
notifyAdmins call can throw and must not abort the delete flow; wrap the
notifyAdmins(...) invocation (the block handling promoted from
promoteNextFromWaitlist(registration.eventId)) in a try/catch, so any exception
is caught and handled (log the error and continue) instead of rethrowing; ensure
the code still returns success for the deletion path even if notifyAdmins fails,
referencing promoteNextFromWaitlist, promoted, notifyAdmins and event in your
fix.

Comment on lines +130 to +137
if (existingRegistration && existingRegistration.cancelledAt === null) {
await tx.eventWaitlistEntry.update({
where: { id: nextWaitlistEntry.id },
data: {
cancelledAt: new Date(),
},
});
return null;

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.

⚠️ Potential issue | 🟠 Major

Don’t stop promotion after removing a stale waitlist row.

If the first waitlisted user already has an active registration, Lines 130-137 cancel that waitlist entry and return null. When capacity is still available, the next eligible person never gets promoted, so the event can keep an empty seat until some later action retries promotion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/events/event-capacity.ts` around lines 130 - 137, The code
cancels a stale waitlist entry when existingRegistration is present (within the
block using tx.eventWaitlistEntry.update on nextWaitlistEntry) but then returns
null, which stops processing further waitlist entries and leaves capacity
unfilled; change the control flow so that after marking
nextWaitlistEntry.cancelledAt you do not return but instead continue processing
the loop/logic that promotes the next eligible waitlist entry (i.e., remove the
early return and either continue to the next iteration or let the promotion
logic that follows run), ensuring the rest of the function (promotion code that
reads nextWaitlistEntry and creates a registration) can run for subsequent
entries.

Comment on lines +168 to +177
const position = await tx.eventWaitlistEntry.count({
where: {
eventId,
cancelledAt: null,
promotedAt: null,
createdAt: {
lte: waitlistCreatedAt,
},
},
});

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.

⚠️ Potential issue | 🟡 Minor

Count waitlist position with the same sort key used elsewhere.

getWaitlistPosition() orders by createdAt, id, but this count only uses createdAt <= waitlistCreatedAt. If two entries share the same timestamp, both can get the same returned position, so the immediate response can disagree with the actual FIFO order after reload.

Suggested change
         const position = await tx.eventWaitlistEntry.count({
           where: {
             eventId,
             cancelledAt: null,
             promotedAt: null,
-            createdAt: {
-              lte: waitlistCreatedAt,
-            },
+            OR: [
+              { createdAt: { lt: waitlistCreatedAt } },
+              {
+                createdAt: waitlistCreatedAt,
+                id: { lte: waitlistId },
+              },
+            ],
           },
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const position = await tx.eventWaitlistEntry.count({
where: {
eventId,
cancelledAt: null,
promotedAt: null,
createdAt: {
lte: waitlistCreatedAt,
},
},
});
const position = await tx.eventWaitlistEntry.count({
where: {
eventId,
cancelledAt: null,
promotedAt: null,
OR: [
{ createdAt: { lt: waitlistCreatedAt } },
{
createdAt: waitlistCreatedAt,
id: { lte: waitlistId },
},
],
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/events/register-event.ts` around lines 168 - 177, Count the
waitlist position using the same sort key as getWaitlistPosition by adding an id
tie-breaker to the where clause: instead of only counting entries with createdAt
<= waitlistCreatedAt, count entries where createdAt < waitlistCreatedAt OR
(createdAt = waitlistCreatedAt AND id < = <this entry's id>), so use the current
waitlist entry's id (e.g. waitlistEntryId / id) and waitlistCreatedAt in
tx.eventWaitlistEntry.count to ensure stable FIFO ordering that matches
getWaitlistPosition.

Comment on lines +81 to +98
prisma.eventWaitlistEntry.findMany({
where: {
eventId: id,
},
include: {
user: true,
},
orderBy: {
createdAt: 'asc',
},
}),
]);

const activeRegistrations = registrations.filter((r) => r.cancelledAt === null);
const cancelledRegistrations = registrations.filter((r) => r.cancelledAt !== null);
const activeWaitlistEntries = waitlistEntries.filter(
(entry) => entry.cancelledAt === null && entry.promotedAt === null,
);

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.

⚠️ Potential issue | 🟡 Minor

Match the admin waitlist order to the promotion order.

The table position is derived from this query order, but promoteNextFromWaitlist() uses createdAt ASC, id ASC. With same-timestamp entries, the admin page can show #1/#2 in a different order than the user who will actually be promoted next.

Suggested fix
     prisma.eventWaitlistEntry.findMany({
       where: {
         eventId: id,
+        cancelledAt: null,
+        promotedAt: null,
       },
       include: {
         user: true,
       },
-      orderBy: {
-        createdAt: 'asc',
-      },
+      orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
     }),
   ]);
@@
-  const activeWaitlistEntries = waitlistEntries.filter(
-    (entry) => entry.cancelledAt === null && entry.promotedAt === null,
-  );
+  const activeWaitlistEntries = waitlistEntries;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(platform)/eventos/[id]/inscripciones/page.tsx around lines 81 - 98,
The admin waitlist ordering differs from the promotion ordering; update the
waitlist query used to build activeWaitlistEntries so it orders identically to
promoteNextFromWaitlist() by using orderBy: [{ createdAt: 'asc' }, { id: 'asc'
}] in the prisma.eventWaitlistEntry.findMany call (keep the same where and
include clauses and the existing cancelledAt/promotedAt filtering), ensuring the
admin table position matches the actual promotion order.

const [isAutoRegistering, setIsAutoRegistering] = useState(false);
// Estado local para saber si ya se registró en esta sesión
const [justRegisteredLocally, setJustRegisteredLocally] = useState(false);
const [justWaitlistedLocally, setJustWaitlistedLocally] = useState(false);

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.

⚠️ Potential issue | 🟡 Minor

Persist the returned waitlist position in local state.

Both waitlist success paths only flip a boolean. After a successful waitlist join, this component still renders the server prop waitlistPosition, so the new “Tu posición actual…” message stays blank/stale until a refresh.

Suggested change
   const [justRegisteredLocally, setJustRegisteredLocally] = useState(false);
   const [justWaitlistedLocally, setJustWaitlistedLocally] = useState(false);
+  const [localWaitlistPosition, setLocalWaitlistPosition] = useState(waitlistPosition);
…
           if (result.status === 'registered') {
             setJustRegisteredLocally(true);
             setShowSuccessDialog(true);
             setJustWaitlistedLocally(false);
+            setLocalWaitlistPosition(null);
           } else {
             setJustWaitlistedLocally(true);
+            setLocalWaitlistPosition(result.position);
             toast.success('Te sumaste a la lista de espera de este evento.');
           }
…
-    setJustWaitlistedLocally(true);
+    setJustWaitlistedLocally(true);
+    setLocalWaitlistPosition(result.position);
     toast.success('Te sumaste a la lista de espera de este evento.');
…
-            {waitlistPosition ? ` Tu posición actual es #${waitlistPosition}.` : ''}
+            {localWaitlistPosition ? ` Tu posición actual es #${localWaitlistPosition}.` : ''}

Also applies to: 80-89, 114-124, 163-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/events/event-detail-client.tsx` at line 46, The component
currently only flips justWaitlistedLocally and doesn't persist the returned
waitlist position, so the UI keeps showing the stale server prop
waitlistPosition; add a local state like waitlistPositionLocal (useState<number
| null>(null)) and on successful waitlist join in the handlers referenced (the
functions that call setJustWaitlistedLocally at the blocks around lines 46,
~80-89, ~114-124, and ~163-170) set waitlistPositionLocal to the position
returned by the server response; then update rendering to prefer
waitlistPositionLocal if non-null (falling back to props.waitlistPosition) so
the “Tu posición actual…” message updates immediately.

isAuthenticated: boolean;
capacityAvailable: boolean;
onSuccess?: () => void;
onSuccess?: (status: 'registered' | 'waitlisted') => void;

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.

⚠️ Potential issue | 🟡 Minor

Preserve the waitlist payload in onSuccess.

registerEvent() now returns position for waitlisted users, but this prop only forwards status. That prevents callers from rendering the promised waitlist position without an extra refresh.

Suggested change
-  onSuccess?: (status: 'registered' | 'waitlisted') => void;
+  onSuccess?: (result: { status: 'registered'; registrationId: string } | { status: 'waitlisted'; waitlistId: string; position: number }) => void;-        onSuccess(result.status);
+        onSuccess(result);

Also applies to: 42-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/events/register-event-button.tsx` at line 14, Update the
onSuccess prop and its usage to forward the waitlist position returned by
registerEvent: change the prop signature (onSuccess) to accept either a payload
object or an extra optional parameter that includes both status and position
(e.g., { status: 'registered' | 'waitlisted', position?: number }) and, inside
the register-event-button component where registerEvent() is called, pass the
returned position when status === 'waitlisted' (use the position property from
registerEvent's response). Ensure all calls to onSuccess (and its type) are
updated so callers can read position without requiring a refresh.

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.

2 participants