# Waitlist System — Implementation Documentation & Gap Analysis

## Table of Contents
1. [System Overview](#1-system-overview)
2. [Registration Statuses](#2-registration-statuses)
3. [Waitlist Logic — Full Flow](#3-waitlist-logic--full-flow)
4. [SeatAvailabilityService — Decision Engine](#4-seatavailabilityservice--decision-engine)
5. [Seat Count Mechanics](#5-seat-count-mechanics)
6. [Admin Bulk Operations](#6-admin-bulk-operations)
7. [API Endpoints](#7-api-endpoints)
8. [Admin KPI Filters](#8-admin-kpi-filters)
9. [Notifications](#9-notifications)
10. [Key Files Reference](#10-key-files-reference)
11. [Gap Analysis & Suggested Fixes](#11-gap-analysis--suggested-fixes)

---

## 1. System Overview

The waitlist is a program-level feature. When a program's registrations reach a configurable threshold (`waitlistTriggerCount`), new submissions are placed into `WAITLISTED` status instead of `PENDING`. A waitlist sequence number (`waitListRegistrationSeqNumber`) is assigned at registration time and never overwritten — it preserves FIFO order for promotion.

**Config fields on the `program_v1` table (`Program` entity):**

| Field | Column | Type | Default | Purpose |
| --- | --- | --- | --- | --- |
| `limitedSeats` | `limited_seats` | boolean | false | Whether seat count is enforced |
| `totalSeats` | `total_seats` | int | 0 | Maximum allowed registrations |
| `filledSeats` | `filled_seats` | int | 0 | Currently allocated seats (incremented atomically) |
| `waitlistApplicable` | `waitlist_applicable` | boolean | false | Whether waitlist feature is enabled |
| `waitlistTriggerCount` | `waitlist_trigger_count` | int | 0 | Threshold: waitlist fires when `filledSeats >= this` |
| `isWaitlistTriggered` | `is_waitlist_triggered` | boolean | false | Set to `true` once the first registration is waitlisted |
| `releasedCount` | `released_count` | int | 0 | Count of admin-released waitlist seats (incremented on release, decremented on deny if reserved) |
| `allocateSeatIfOfflinePending` | `allocate_seat_if_offline_pending` | boolean | false | If true: allocate seat at OFFLINE_PENDING; if false: wait until payment marked received |
| `requiresApproval` | `requires_approval` | boolean | false | If true, approval stage controls seat allocation; bypasses waitlist |

**Waitlist fields on the `hdb_program_registration` table (`ProgramRegistration` entity):**

| Field | Column | Type | Purpose |
| --- | --- | --- | --- |
| `registrationStatus` | `registration_status` | enum | Tracks the registration state; value `waitlisted` means in queue |
| `waitingListSeqNumber` | `waiting_list_seq_number` | int (nullable) | Integer queue position (1, 2, 3 …); null before waitlist triggered |
| `waitListRegistrationSeqNumber` | `wait_list_registration_seq_number` | varchar(50) (nullable) | Formatted queue ID, e.g. `HDB25-WL-001`; never overwritten |
| `reservedLink` | `reserved_link` | boolean | `true` after admin releases this registration via `waitlist-release`; allows payment even while `registrationStatus = waitlisted` |
| `isFreeSeat` | `is_free_seat` | boolean | `true` if admin granted a free seat; blocks payment initiation |
| `seatAllocated` | `seat_allocated` | boolean | `true` once a physical seat has been secured via `incrementSeatCounts()` |

---

## 2. Registration Statuses

Defined in `src/common/enum/registration-status.enum.ts`:

| Status | Description |
| --- | --- |
| `DRAFT` | Partially filled, not submitted |
| `SAVE_AS_DRAFT` | Explicitly saved as draft |
| `ATTEMPTED` | Submitted but failed |
| `PENDING_APPROVAL` | Awaiting admin approval |
| `PENDING` | Approved or no approval needed; awaiting payment |
| `WAITLISTED` | Program full; assigned a queue position |
| `COMPLETED` | All requirements met (payment + travel + invoice) |
| `CANCELLED` | Cancelled by user or admin; terminal |
| `REJECTED` | Denied by admin; terminal |
| `ON_HOLD` | Temporarily held; can be reactivated |
| `ARCHIVED` | Archived by admin; seats freed |

---

## 3. Waitlist Logic — Full Flow

### Step 1 — Registration Submission

`registration.service.create()` → `registration.repository.ts`

**Decision tree at submission:**

```
limitedSeats = false?
  └─ Status = PENDING (no seat allocated yet)

limitedSeats = true AND waitlistApplicable = true?
  └─ filledSeats >= waitlistTriggerCount?
        YES → Status = WAITLISTED, assign waitListRegistrationSeqNumber
        NO  → Seats available?
                YES → Status = PENDING
                NO  → Below trigger threshold?
                        YES → Status = WAITLISTED
                        NO  → Status = PENDING (isRegistrationsExceeded = true)

limitedSeats = true AND waitlistApplicable = false?
  └─ Seats full?
        YES → Status = PENDING (isRegistrationsExceeded = true)
        NO  → Status = PENDING
```

> A `WAITLISTED` registration receives a `waitListRegistrationSeqNumber` via `RegistrationSequenceService`. This number is FIFO-ordered and never overwritten.

---

### Step 2 — Approval Stage (if program.requiresApproval = true)

`registration-approval.service.ts` → `registration-approval.repository.ts`

- Admin reviews and selects: `APPROVED`, `REJECTED`, or `ON_HOLD`.
- On `APPROVED`: `incrementSeatCounts()` is called; `seatAllocated = true`.
- If the registration has an `allocatedProgram` or `allocatedSession`, seats are checked against those instead of the main program.
- Approval bypasses all seat/waitlist checks — **approval alone controls seat allocation for requiresApproval programs**.

---

### Step 3 — Payment Stage

`payment.service.initiatePayment()` → `payment.repository.ts`

Three sub-cases during payment for waitlisted registrations:

| Sub-case | Condition | Action |
| --- | --- | --- |
| 3a | Registration already `WAITLISTED` | Check if seat now available; if yes, call `incrementSeatCounts()` and move to `PENDING` |
| 3b | Registration NOT `WAITLISTED` | Check seats; if full, move to `WAITLISTED`; if available, allocate |
| 3c | `allocateSeatIfOfflinePending = true` | Allocate seat immediately on `OFFLINE_PENDING` payment |

**Payment status → seat allocation:**

```
ONLINE_COMPLETED    → allocate seat if not yet allocated
OFFLINE_PENDING     → allocate only if allocateSeatIfOfflinePending = true
OFFLINE_COMPLETED   → allocate seat if not yet allocated
```

---

### Step 4 — Auto-Completion Check

After each payment event, the system checks if all requirements are met:

| Requirement | Check |
| --- | --- |
| Payment | `paymentStatus = ONLINE_COMPLETED or OFFLINE_COMPLETED` |
| Travel | Travel status complete (if required) |
| Invoice | Invoice status complete (if required) |

If all are met AND the status is NOT `WAITLISTED`, `CANCELLED`, or `REJECTED` → status auto-transitions to `COMPLETED`.

---

### Step 5 — Seat Release (Cancellation / Rejection / Archive)

When a registration is cancelled, rejected, or archived:

1. `decrementSeatCounts(manager, programId, sessionId, registrationId)` is called.
2. `filledSeats` on the program is decremented by 1.
3. `reservedSeats` on the session is decremented (if applicable).
4. `seatAllocated` on the registration is set to `false`.

> **Seats are only decremented if the registration was previously `APPROVED` (seatAllocated = true). Direct PENDING → REJECTED/ON_HOLD does NOT decrement.**

---

### Step 6 — Admin Waitlist Actions (Bulk Operations)

See [Section 6](#6-admin-bulk-operations).

---

## 4. SeatAvailabilityService — Decision Engine

**File:** `src/common/services/seat-availability.service.ts`

The `SeatAvailabilityService` is the single source of truth for all seat and waitlist decisions. It is called at three points in the lifecycle.

### 4.1 Main Method — `checkSeatAvailability(program, session, registrationLevel)`

Returns a `SeatAvailabilityResult`:

```typescript
interface SeatAvailabilityResult {
  limitedSeats: boolean;
  totalSeats: number;
  filledSeats: number;
  seatsRemaining: number;
  waitlistApplicable: boolean;
  waitlistTriggerCount: number;
  isWaitlistTriggered: boolean;
  isRegistrationsExceeded: boolean;
  canRegister: boolean;
  shouldWaitlist: boolean;
  shouldReject: boolean;
  statusMessage: string;
  capacityStatus: 'NORMAL' | 'WAITLIST' | 'EXCEEDED' | 'UNLIMITED';
}
```

### 4.2 Decision Tree

**Case 1 — Unlimited seats** (`limitedSeats = false`):

```
canRegister = true  |  shouldWaitlist = false  |  capacityStatus = UNLIMITED
```

**Case 2 — Limited seats, no waitlist** (`limitedSeats = true`, `waitlistApplicable = false`):

```
filledSeats < totalSeats  →  canRegister = true,  capacityStatus = NORMAL
filledSeats >= totalSeats →  canRegister = false, shouldReject = true, capacityStatus = EXCEEDED
```

**Case 3 — Limited seats with waitlist** (`limitedSeats = true`, `waitlistApplicable = true`):

```
filledSeats < waitlistTriggerCount
  └─ canRegister = true, shouldWaitlist = false, capacityStatus = NORMAL

filledSeats >= waitlistTriggerCount (threshold reached)
  └─ canRegister = true, shouldWaitlist = true, isWaitlistTriggered = true, capacityStatus = WAITLIST

filledSeats > totalSeats (over capacity but waitlist enabled)
  └─ canRegister = true, shouldWaitlist = true, isWaitlistTriggered = true, capacityStatus = WAITLIST
     (never marks EXCEEDED when waitlist is on)
```

> Note: `shouldWaitlist` is additionally suppressed to `false` inside `PaymentService.shouldWaitlist()` when `program.requiresApproval = true`, because approval controls seat allocation.

### 4.3 Validation Call Points

| Method | Called From | Purpose |
| --- | --- | --- |
| `validateAtRegistrationLevel()` | `registration.service.ts` on create/update | Decides PENDING vs WAITLISTED at submission |
| `validateAtApprovalLevel()` | `registration-approval.repository.ts` on approve | Checks allocated program/session capacity |
| `validateAtPaymentLevel()` | `payment.repository.ts` before seat increment | Final check before seat is secured |

### 4.4 API — `GET /program/:id/seat-availability`

Returns the full `SeatAvailabilityResult` for a program (or session if `?sessionId=` provided). Use `isWaitlistTriggered` to determine if new registrations will be waitlisted.

---

## 5. Seat Count Mechanics

**File:** `src/common/utils/seat-count.util.ts`

### `incrementSeatCounts(manager, programId, sessionId?, registrationId?)`

1. Loads the program row.
2. If `requiresApproval = true`: skips all seat checks (approval controls allocation).
3. If `limitedSeats = true` and `filledSeats >= totalSeats`: returns `{ seatSecured: false }`.
4. Otherwise: `manager.increment(Program, { id }, 'filledSeats', 1)`.
5. If `sessionId` provided: increments `reservedSeats` on the session.
6. Sets `registration.seatAllocated = true`.

### `decrementSeatCounts(manager, programId, sessionId?, registrationId?)`

1. `manager.decrement(Program, { id }, 'filledSeats', 1)` — unconditional.
2. Decrements `reservedSeats` on session if provided.
3. Sets `registration.seatAllocated = false`.

---

## 6. Admin Bulk Operations

### 6.1 Waitlist Deny — `POST /registration/waitlist-deny`

**Flow:** `RegistrationController` → `WaitlistActionService` → `RegistrationRepository`

- Accepts up to 50 registration IDs.
- Validates each registration is in `WAITLISTED` status; others are skipped.
- Changes status: `WAITLISTED` → `REJECTED`.
- Sends notifications:
  - Unpaid: `WAITLIST_DENIED_EMAIL_SEEKER` + `WAITLIST_DENIED_WATI_SEEKER`
  - Paid: `WAITLIST_DENIED_PAID_EMAIL_SEEKER` + `WAITLIST_DENIED_PAID_WATI_SEEKER`
- Returns `{ processed: number[], skipped: number[] }`.

### 6.2 Waitlist Release — `POST /registration/waitlist-release`

**Flow:** `RegistrationController` → `WaitlistActionService` → `RegistrationRepository`

Release types (enum `WaitlistReleaseTypeEnum`):

| Type | Status Transition | Notification |
| --- | --- | --- |
| `PAYMENT` | `WAITLISTED` → `WAITLISTED` (stays; payment link sent) | `WAITLIST_RELEASE_PAYMENT_*` |
| `OFFLINE` | `WAITLISTED` → `PENDING` | `WAITLIST_RELEASE_OFFLINE_*` |
| `INVOICE` | `WAITLISTED` → `COMPLETED` | `WAITLIST_RELEASE_INVOICE_*` |

### 6.3 Archive — `PUT /registration/archive`

- Changes status to `ARCHIVED`.
- Calls `AllocationClearingService` to free allocations.
- Calls `decrementSeatCounts()` if `seatAllocated = true`.
- Returns freed seat to pool (but does NOT automatically promote next waitlisted user).

---

## 7. API Endpoints

| Method | Route | Auth | Purpose |
| --- | --- | --- | --- |
| `POST` | `/registration` | Seeker | Create registration; response includes `waitlisted: true` and `waitlistPosition` when placed in queue |
| `PUT` | `/registration` | Seeker / Admin | Update registration; may trigger waitlist |
| `POST` | `/payment/initiate/:registrationId` | Seeker | Initiate payment; response includes `waitlisted: true` if seat unavailable at payment time |
| `PUT` | `/payment/update/:registrationId` | Admin | Confirm / update payment status |
| `GET` | `/program/:id/seat-availability` | Any | Returns full seat and waitlist status including `isWaitlistTriggered`, `shouldWaitlist`, `capacityStatus` |
| `GET` | `/registration/:id/waitlist-position` | Seeker / Admin | Returns `{ position, total, seqNumber }` for a waitlisted registration |
| `POST` | `/registration/waitlist-release` | Admin | Release up to 50 waitlisted registrations (sets `reservedLink = true`, sends payment link) |
| `POST` | `/registration/waitlist-deny` | Admin | Deny up to 50 waitlisted registrations (`WAITLISTED → REJECTED`) |
| `GET` | `/registration/registration-list-view` | Admin | List with waitlist filters (see Section 8) |

---

## 8. Admin KPI Filters

The registration list view (`GET /registration/registration-list-view`) supports `waitlistCategory` and `seatReleased` query filters.

| Filter Key | Value | Condition |
| --- | --- | --- |
| `waitlistCategory` | `"before"` | `registrationStatus = waitlisted` AND `waitingListSeqNumber IS NULL` (registered before waitlist triggered) |
| `waitlistCategory` | `"after"` | `registrationStatus = waitlisted` AND `waitingListSeqNumber IS NOT NULL` (assigned a queue position) |
| `seatReleased` | `true` | `registrationStatus = waitlisted` AND `reservedLink = true` |

KPI category constants (`src/common/constants/constants.ts`):

| Constant | Value | Meaning |
| --- | --- | --- |
| `KPI_FILTERS.WAITLISTED_ALL` | `waitlisted` | All waitlisted |
| `KPI_FILTERS.WAITLISTED_BEFORE_SEQ` | `waitlisted_before_seq` | No seq number yet |
| `KPI_FILTERS.WAITLISTED_WITH_SEQ` | `waitlisted_with_seq` | Has seq number |
| `KPI_FILTERS.WAITLISTED_SEAT_RELEASED` | `waitlisted_seat_released` | `reservedLink = true` |
| `KPI_FILTERS.WAITLISTED_SEAT_PENDING` | `waitlisted_seat_pending` | `reservedLink = false` + has seq |

---

## 9. Notifications

All templates are defined in `src/common/enum/communication-template-access-key.enum.ts`.

| Event | WhatsApp Template | Email Template |
| --- | --- | --- |
| Waitlisted at online payment | `REGISTRATION_WAITINGLIST_WATI_SEEKER` | — |
| Waitlisted at offline payment | `OFFLINE_REGISTRATION_WAITINGLIST_WATI_SEEKER` | — |
| Release — PAYMENT type (link sent) | `WAITLIST_RELEASE_PAYMENT_WATI_SEEKER` | `WAITLIST_RELEASE_PAYMENT_EMAIL_SEEKER` |
| Release — OFFLINE type | `WAITLIST_RELEASE_OFFLINE_WATI_SEEKER` *(currently commented out)* | `WAITLIST_RELEASE_OFFLINE_EMAIL_SEEKER` *(commented out)* |
| Release — INVOICE type | `WAITLIST_RELEASE_INVOICE_WATI_SEEKER` *(commented out)* | `WAITLIST_RELEASE_INVOICE_EMAIL_SEEKER` *(commented out)* |
| Deny — not paid | `WAITLIST_DENY_WATI_SEEKER` | `WAITLIST_DENIED_EMAIL_SEEKER` |
| Deny — paid | `WAITLIST_PAID_DENY_WATI_SEEKER` | `WAITLIST_DENIED_PAID_EMAIL_SEEKER` |

Notifications are sent fire-and-forget (`.catch(logger.error)`). Only the PAYMENT release type currently sends notifications; OFFLINE and INVOICE release notifications are commented out in `registration.repository.ts`.

---

## 10. Key Files Reference

| File | Role |
| --- | --- |
| `src/registration/registration.service.ts` | Orchestration, business logic |
| `src/registration/registration.repository.ts` | DB layer, waitlist assignment at creation |
| `src/registration/registration.controller.ts` | API endpoints |
| `src/registration/registration-action.service.ts` | Archive and bulk action handling |
| `src/registration-approval/registration-approval.service.ts` | Approval flow |
| `src/registration-approval/registration-approval.repository.ts` | Seat allocation on approval |
| `src/payment/payment.service.ts` | Payment initiation, waitlist sub-cases |
| `src/payment/payment.repository.ts` | Payment DB layer, waitlist transitions |
| `src/common/utils/seat-count.util.ts` | `incrementSeatCounts` / `decrementSeatCounts` |
| `src/common/services/allocation-clearing.service.ts` | Clears allocations on archive |
| `src/common/enum/registration-status.enum.ts` | All status values |
| `src/common/enum/waitlist-release-type.enum.ts` | Release type enum |
| `docs/REGISTRATION_FLOW.md` | Mermaid flow diagram |
| `docs/waitlist-deny-release-implementation.md` | Deny/release feature spec |
| `docs/archive-registration-implementation.md` | Archive feature spec |

---

## 11. Gap Analysis & Suggested Fixes

---

### GAP-01 — CRITICAL: Race Condition in Seat Allocation

**Severity:** Critical  
**Location:** `src/common/utils/seat-count.util.ts:26-49`, `payment.repository.ts` sub-case 3b

**Problem:**  
Two concurrent requests can both read `filledSeats = 9` on a 10-seat program, both pass the `filledSeats < totalSeats` check, and both call `manager.increment(...)` — resulting in `filledSeats = 11` (overbooking).

```
Thread A: read filledSeats=9 < totalSeats=10 ✓
Thread B: read filledSeats=9 < totalSeats=10 ✓
Thread A: increment → filledSeats=10
Thread B: increment → filledSeats=11  ← over capacity
```

**Suggested Fix:**  
Replace the read-then-increment pattern with an atomic conditional update (optimistic locking):

```typescript
// In incrementSeatCounts — replace the findOne + increment with:
const result = await manager
  .createQueryBuilder()
  .update(Program)
  .set({ filledSeats: () => 'filled_seats + 1' })
  .where('id = :id AND filled_seats < total_seats AND limited_seats = true', { id: programId })
  .returning(['id', 'filled_seats', 'total_seats'])
  .execute();

if (result.affected === 0) {
  return { seatSecured: false }; // seats full; no update happened
}
return { seatSecured: true };
```

This single SQL statement is atomic — no separate read needed.

---

### GAP-02 — CRITICAL: No Automatic Waitlist Promotion When Seat Freed

**Severity:** Critical  
**Location:** `src/registration/registration.service.ts` (cancel), `src/registration/registration-action.service.ts` (archive)

**Problem:**  
When a seat is freed (cancellation, rejection, archive), `decrementSeatCounts()` runs but nothing promotes the first-in-queue waitlisted user. Freed seats go back to the pool silently. Admin must manually call `/waitlist-release` or a new registrant gets the seat.

**Suggested Fix:**  
Add a `promoteNextWaitlisted` helper that runs immediately after `decrementSeatCounts()`:

```typescript
// In a new file: src/registration/utils/waitlist-promotion.util.ts
export async function promoteNextWaitlisted(
  registrationRepository: RegistrationRepository,
  communicationService: CommunicationService,
  programId: number,
  manager: EntityManager,
): Promise<void> {
  const next = await registrationRepository.findNextWaitlistedByProgram(programId);
  if (!next) return;

  await manager.update(ProgramRegistration, { id: next.id }, {
    status: RegistrationStatusEnum.PENDING,
  });

  communicationService
    .sendWaitlistPromotionNotification(next)
    .catch(err => logger.error(`Promotion notification failed for reg ${next.id}`, err.stack));
}
```

Also add `findNextWaitlistedByProgram` in `RegistrationRepository`:

```typescript
async findNextWaitlistedByProgram(programId: number): Promise<ProgramRegistration | null> {
  return this.registrationRepo
    .createQueryBuilder('r')
    .select(['r.id', 'r.waitListRegistrationSeqNumber', 'r.userId'])
    .where('r.programId = :programId', { programId })
    .andWhere('r.status = :status', { status: RegistrationStatusEnum.WAITLISTED })
    .andWhere('r.deletedAt IS NULL')
    .orderBy('r.waitListRegistrationSeqNumber', 'ASC')
    .getOne();
}
```

---

### GAP-03 — HIGH: `adjustSeatCountsOnCancel` Skips Decrement for Non-AllocatedProgram Registrations (Seat Leak)

**Severity:** High  
**Location:** `src/registration/registration.repository.ts:1400-1408`

**Problem:**  
`adjustSeatCountsOnCancel()` only decrements `filledSeats` if `registration.allocatedProgram` is set:

```typescript
private async adjustSeatCountsOnCancel(manager, registration) {
  if (registration?.allocatedProgram) {   // ← guard is wrong
    await decrementSeatCounts(manager, registration.allocatedProgram.id, ...);
    await decrementSeatCounts(manager, registration.program.id, ...);
  }
  // If allocatedProgram is null → NOTHING is decremented
}
```

For standard programs (no sub-program allocation), `allocatedProgram` is null. Cancelling or archiving such a registration leaves `filledSeats` unreduced — the seat leaks and is never returned to the pool. This means available seat capacity shrinks permanently with every cancellation on non-Mahatria programs.

**Suggested Fix:**  
Replace the `allocatedProgram` guard with a `seatAllocated` check:

```typescript
private async adjustSeatCountsOnCancel(
  manager: EntityManager,
  registration: ProgramRegistration,
) {
  if (!registration.seatAllocated) return; // no seat was ever allocated

  await decrementSeatCounts(manager, registration.program.id, registration.programSession?.id, registration.id);

  if (registration.allocatedProgram) {
    await decrementSeatCounts(manager, registration.allocatedProgram.id, registration.allocatedSession?.id, registration.id);
  }
}
```

Also ensure the `registration` loaded in `cancelRegistration()` and `archiveRegistration()` includes the `seatAllocated` field and `programSession`/`allocatedSession` relations.

---

### GAP-04 — HIGH: No Notifications for Waitlist State Changes

**Severity:** High  
**Location:** `registration.service.ts` (on `WAITLISTED` assignment), `registration.repository.ts`

**Problem:**  
Users placed on the waitlist receive no notification confirming their queue position. Users are also not notified when they are automatically promoted (once GAP-02 is fixed).

**Suggested Fix:**  
1. At registration creation, when `status = WAITLISTED`, call the communication service:
   ```typescript
   await communicationService.sendWaitlistConfirmation({
     registrationId: reg.id,
     userId: reg.userId,
     programId: reg.programId,
     waitlistPosition: reg.waitListRegistrationSeqNumber,
   });
   ```
2. In `promoteNextWaitlisted()` (from GAP-02 fix), send a promotion notification.
3. Add template keys: `WAITLIST_CONFIRMATION_EMAIL_SEEKER`, `WAITLIST_PROMOTION_EMAIL_SEEKER`.

---

### GAP-05 — ~~HIGH: No Waitlist Position API Endpoint~~ RESOLVED

**Status:** Resolved — `GET /registration/:id/waitlist-position` is implemented.  
Returns `{ position, total, seqNumber }`. Throws 400 if registration is not waitlisted.

---

### GAP-06 — HIGH: Approval Allocates Seats Without Checking `allocatedProgram` Capacity

**Severity:** High  
**Location:** `src/registration-approval/registration-approval.repository.ts` ~line 190

**Problem:**  
When approval provides an `allocatedProgram` or `allocatedSession`, the code checks seat availability for those — but there is no explicit validation that the allocated program/session still has seats before approving. If all seats in the allocated program are filled after the check (concurrent approvals), overbooking occurs.

**Suggested Fix:**  
Use the same atomic conditional update from GAP-01 for allocated programs/sessions as well. Any `incrementSeatCounts()` call on an `allocatedProgram` must use the atomic SQL pattern to prevent concurrent over-allocation.

---

### GAP-07 — HIGH: Partial Bulk Operation — No Rollback of Successful IDs

**Severity:** High  
**Location:** `src/registration/registration-action.service.ts` (waitlist deny / release bulk ops)

**Problem:**  
Bulk deny/release processes IDs individually. If IDs 1–30 succeed and IDs 31–50 fail, the 30 already-processed registrations are changed and notifications sent. There is no rollback. The caller only receives `{ processed: [1..30], skipped: [] }` with no indication of the partial failure.

**Suggested Fix:**  
Wrap the entire bulk operation in a single transaction. If any individual operation fails after the loop, roll back all updates:

```typescript
await this.dataSource.transaction(async (manager) => {
  for (const id of ids) {
    await this.processOneWaitlistAction(id, action, manager);
  }
});
```

Alternatively, if partial success is acceptable, collect failures separately and return them:

```typescript
return { processed, skipped, failed: failedIds };
```

---

### GAP-08 — MEDIUM: Notification Fire-and-Forget With No Retry

**Severity:** Medium  
**Location:** Bulk action notification calls in `WaitlistActionService`

**Problem:**  
Notifications are sent with `.catch(err => logger.error(...))` — fire-and-forget. If the notification service is down, no retry happens and the user never receives the message.

**Suggested Fix:**  
Route all notifications through the existing `WhatsAppQueueService` (already injected in the repo) and an equivalent email queue. Queue-based delivery provides retries and delivery guarantees:

```typescript
// Instead of direct send:
await this.whatsAppQueueService.enqueue({
  registrationId: reg.id,
  templateKey: notificationTemplateKey,
  mergeData,
});
```

---

### GAP-09 — MEDIUM: No Waitlist Expiry / TTL

**Severity:** Medium  
**Location:** Missing feature

**Problem:**  
Registrations can remain in `WAITLISTED` status indefinitely. There is no automatic expiry or reminder after a configurable period (e.g., 30 days).

**Suggested Fix:**  
Add a scheduled job (cron) that:
1. Finds `WAITLISTED` registrations older than `program.waitlistExpiryDays` (new config field).
2. Auto-transitions them to `REJECTED` with reason `WAITLIST_EXPIRED`.
3. Sends an expiry notification.
4. Calls `promoteNextWaitlisted()` for each freed slot.

---

### GAP-10 — MEDIUM: Waitlist Sequence Number Has No Unique DB Constraint

**Severity:** Medium  
**Location:** `registration.repository.ts` — sequence generation via `RegistrationSequenceService`

**Problem:**  
If two registrations are created simultaneously, both could receive the same `waitListRegistrationSeqNumber` — breaking FIFO ordering.

**Suggested Fix:**  
1. Add a unique constraint in the migration: `UNIQUE(program_id, waitlist_registration_seq_number)`.
2. In `RegistrationSequenceService`, use a `SELECT ... FOR UPDATE` on the sequence row to serialize generation, or use a database sequence object (`CREATE SEQUENCE`).

---

### GAP-11 — MEDIUM: Program Config Changes Not Propagated to Existing Waitlist

**Severity:** Medium  
**Location:** Program update endpoint / admin panel

**Problem:**  
If `waitlistApplicable` is toggled from `true` to `false`, or `waitlistTriggerCount` is changed after users are already `WAITLISTED`, there is no migration logic. Existing waitlisted users remain stuck and no notification is sent.

**Suggested Fix:**  
Add a hook on `program.update()` that:
- If `waitlistApplicable` set to `false`: auto-release or notify all `WAITLISTED` registrations.
- If `waitlistTriggerCount` increased: check if newly-available slots can promote current waitlisted users.

---

### GAP-12 — MEDIUM: Audit Trail Missing for Waitlist Transitions

**Severity:** Medium  
**Location:** `@Auditable()` decorator coverage

**Problem:**  
Status changes are audited, but `waitListRegistrationSeqNumber` assignment and waitlist promotion/demotion events are not explicitly included in audit metadata. Tracing "why did this user move from waitlist to pending?" is difficult.

**Suggested Fix:**  
In the `@Auditable()` decorator or audit interceptor, include `waitListRegistrationSeqNumber` in the before/after diff whenever registration status transitions involve `WAITLISTED`.

---

### GAP-13 — LOW: No Waitlist Re-entry Policy After Rejection/Cancellation

**Severity:** Low  
**Location:** Business logic gap

**Problem:**  
If a user is released from waitlist to `PENDING`, then rejected or cancels without paying, there is no defined policy: do they re-enter the waitlist at their original position or end of queue?

**Suggested Fix:**  
Define and document the policy explicitly. Recommended: re-entry goes to end of queue (new `waitListRegistrationSeqNumber`). Implement by clearing the old seq number on re-registration and generating a new one.

---

### GAP-14 — LOW: `totalSeats` Reduction After Approval Has No Guard

**Severity:** Low  
**Location:** Program update endpoint

**Problem:**  
If a program already has 100 approved registrations and an admin reduces `totalSeats` to 80, `filledSeats > totalSeats` — but no logic triggers to handle the 20 over-capacity registrations.

**Suggested Fix:**  
Add a validation guard on `program.update()`:

```typescript
if (dto.totalSeats < program.filledSeats) {
  throw new InifniBadRequestException(ERROR_CODES.CANNOT_REDUCE_SEATS_BELOW_FILLED);
}
```

---

## Summary Priority Table

| # | Gap | Severity | Area |
| --- | --- | --- | --- |
| GAP-01 | Race condition: concurrent seat allocation causes overbooking | Critical | Seat management |
| GAP-02 | No automatic waitlist promotion when seat freed | Critical | Waitlist flow |
| GAP-03 | `decrementSeatCounts` runs even when `seatAllocated = false` | High | Seat management |
| GAP-04 | No notifications for waitlist placement or promotion | High | Notifications |
| GAP-05 | No API endpoint for waitlist position query | High | API |
| GAP-06 | Approval seat allocation lacks atomic concurrency guard | High | Approval flow |
| GAP-07 | Bulk operations have no rollback for partial failures | High | Bulk operations |
| GAP-08 | Notifications are fire-and-forget with no retry | Medium | Reliability |
| GAP-09 | No waitlist expiry / TTL logic | Medium | Waitlist flow |
| GAP-10 | Waitlist sequence number lacks DB unique constraint | Medium | Data integrity |
| GAP-11 | Program config changes not propagated to existing waitlist | Medium | Config management |
| GAP-12 | Audit trail incomplete for waitlist transitions | Medium | Observability |
| GAP-13 | No re-entry policy after rejection/cancellation | Low | Business logic |
| GAP-14 | No guard when `totalSeats` reduced below `filledSeats` | Low | Data integrity |
