# Online Session — Feature 1: Create Zoom Session (Webinar / Meeting)

> Provider-agnostic API to provision a Zoom **webinar** or **meeting** against an
> existing `program_session`. The HTTP layer never talks to Zoom directly — it
> goes through `OnlineSessionService` (facade) → provider registry → `ZoomProvider`
> → `WebinarService` / `MeetingService` → `ZoomApiService`.

---

## 1. Endpoint

| | |
|---|---|
| **Method / Path** | `POST /online-session` |
| **Handler** | `OnlineSessionController.create()` — [online-session.controller.ts:91](../../src/online-session/controllers/online-session.controller.ts#L91) |
| **Auth** | `CombinedAuthGuard` + `RolesGuard`, role `admin` |
| **Success** | `201 Created` |
| **Validation** | `ValidationPipe({ transform: true, whitelist: true })` — unknown fields are stripped |
| **Bulk variant** | `POST /online-session/bulk` → `createBulk()` ([:113](../../src/online-session/controllers/online-session.controller.ts#L113)) |

> ⚠️ Because `whitelist: true` strips unknown properties, the request **must** be
> sent with `Content-Type: application/json`. (See the known content-type pitfall.)

---

## 2. Request body — `CreateOnlineSessionDto`

[create-online-session.dto.ts](../../src/online-session/dto/create-online-session.dto.ts)

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `programSessionId` | `int` | ✅ | — | `program_session.id` the session attaches to |
| `startAt` | ISO 8601 | ✅ | — | Session start time |
| `duration` | `int` (≥1) | ✅ | — | Minutes |
| `provider` | enum | — | `zoom` | Only `zoom` today |
| `onlineType` | `webinar` \| `meeting` | — | `webinar` | Selects which handler runs |
| `requireRegistration` | `boolean` | — | `true` | **Meetings only.** `true` → per-user registrant links; `false` → one shared link + passcode. Ignored for webinars |
| `title` | `string` (≤500) | — | program session `name` | Webinar/meeting topic |
| `hostEmail` | email | — | `ZOOM_ADMIN_EMAIL` | Zoom account the resource is created under |
| `password` | `string` (≤100) | — | — | Optional session password |
| `launchMode` | `sdk` \| `zoomClient` | — | `sdk` | Where attendees launch from |
| `status` | enum | — | `draft` | `draft` / `internalTesting` / `published` / `completed` |
| `joinOpensMinutesBefore` | `int` (0–1440) | — | `15` | When the join link opens before start |
| `registrationStartsAt` | ISO 8601 | — | — | Registration window open |
| `registrationEndsAt` | ISO 8601 | — | — | Registration window close |
| `createdBy` / `updatedBy` | `int` | — | from auth | Overwritten by `req.user.id` in the controller |

**Example**

```jsonc
POST /online-session
Content-Type: application/json
{
  "programSessionId": 42,
  "onlineType": "webinar",
  "title": "Monthly Satsang",
  "startAt": "2026-07-01T10:00:00Z",
  "duration": 60,
  "status": "draft",
  "joinOpensMinutesBefore": 30
}
```

---

## 3. Flow

```
POST /online-session
  │
OnlineSessionController.create()          controllers/online-session.controller.ts:91
  │  sets dto.createdBy / updatedBy = req.user.id
  ▼
OnlineSessionService.create(dto)          services/online-session.service.ts
  │  resolves provider (defaults zoom) via the provider registry
  ▼
ZoomProvider.create(input)                zoom/zoom.provider.ts
  │  dispatches by onlineType → Webinar or Meeting handler
  ▼
WebinarService.create()  /  MeetingService.create()    zoom/sessions/*.service.ts
  1. loadSession(programSessionId)        — 404 if missing (ZOOM_WEBINAR_NOTFOUND)
  2. ensureNotProvisioned(session)        — 409 if already has externalId
  3. resolveHostEmail()                   — dto.hostEmail ?? ZOOM_ADMIN_EMAIL
  4. zoomApi.createWebinar/createMeeting() — HTTP POST to Zoom
  5. build OnlineSession entity from the Zoom response
  6. applyCommonFields()                  — reg window, launchMode, joinOpensMinutesBefore, audit
  7. persistWithRollback()                — save; on DB failure delete the Zoom resource
  ▼
Controller.toResponse(session)            — strips host startUrl before returning
```

### Key guards (shared base — [zoom-session.handler.ts](../../src/zoom/sessions/zoom-session.handler.ts))

- **`loadSession`** ([:27](../../src/zoom/sessions/zoom-session.handler.ts#L27)) — throws `InifniNotFoundException` (`ZOOM_WEBINAR_NOTFOUND`) if the program session does not exist.
- **`ensureNotProvisioned`** ([:46](../../src/zoom/sessions/zoom-session.handler.ts#L46)) — a program session may back only **one** online session; throws `InifniConflictException` (`ONLINE_SESSION_ALREADY_EXISTS`) if an `externalId` already exists. Delete the old one first.
- **`persistWithRollback`** ([:80](../../src/zoom/sessions/zoom-session.handler.ts#L80)) — if the DB save fails after Zoom created the resource, it deletes the Zoom resource so nothing is left orphaned.

---

## 4. Webinar create — [webinar.service.ts:37](../../src/zoom/sessions/webinar.service.ts#L37)

Calls `zoomApi.createWebinar()` and builds an `OnlineSession` with
`type = WEBINAR`, `provider = zoom`, copying `externalId`, `joinUrl`, `password`,
`registrationUrl`, `startUrl`, and setting `panelistUrl = ''` (panelist links are
filled in later when panelists are added).

### Zoom payload — [zoom-api.service.ts:114](../../src/zoom/services/zoom-api.service.ts#L114)

`POST https://api.zoom.us/v2/users/{hostEmail}/webinars`

```jsonc
{
  "topic": "<title | session.name>",
  "type": 5,                         // SCHEDULED_WEBINAR
  "start_time": "<startAt>",
  "timezone": "Asia/Calcutta",
  "duration": <minutes>,
  "password": "<optional>",
  "template_id": "<ZOOM_TEMPLATE_ID if set>",
  "settings": {
    "panelists_video": true,
    "allow_multiple_devices": true,
    "approval_type": 0               // AUTOMATIC
  }
}
```

---

## 5. Meeting create — [meeting.service.ts:37](../../src/zoom/sessions/meeting.service.ts#L37)

Same shape as webinar but `type = MEETING` and it persists `requireRegistration`
on the `OnlineSession`. `requireRegistration` defaults to `true` unless explicitly
set to `false`.

### Zoom payload — [zoom-api.service.ts:154](../../src/zoom/services/zoom-api.service.ts#L154)

`POST https://api.zoom.us/v2/users/{hostEmail}/meetings`

```jsonc
{
  "topic": "<title | session.name>",
  "type": 2,                         // SCHEDULED_MEETING
  "start_time": "<startAt>",
  "timezone": "Asia/Calcutta",
  "duration": <minutes>,
  "password": "<optional>",
  "settings": {
    "approval_type": 0,              // 0 AUTOMATIC if requireRegistration, else 2 NONE
    "join_before_host": false,
    "waiting_room": <!requireRegistration>,
    "registrants_email_notification": <requireRegistration>
  }
}
```

| Behaviour | `requireRegistration: true` (default) | `requireRegistration: false` |
|---|---|---|
| Join | Per-user registrant links | One shared link + passcode |
| `approval_type` | `0` (automatic) | `2` (none) |
| `waiting_room` | off | on |
| Registrant emails | on | off |

---

## 6. What gets persisted

A successful create writes:

- **`program_session`** — `onlineType` set to `webinar`/`meeting`, plus
  registration-window fields.
- **`hdb_online_session`** (`OnlineSession`) — the provider record: `externalId`
  (Zoom id), `joinUrl`, `password`, `registrationUrl`, `panelistUrl`, `hostEmail`,
  `startUrl`, `status`, `launchMode`, `joinOpensMinutesBefore`, `requireRegistration`
  (meetings), audit fields. See [online-session.entity.ts](../../src/common/entities/online-session.entity.ts).

---

## 7. Response

`toResponse()` ([online-session.controller.ts:72](../../src/online-session/controllers/online-session.controller.ts#L72))
maps the entity to `webinarDetails` / `meetingDetails` and **strips `startUrl`**
(the host-control URL must never reach a client).

```jsonc
{
  "success": true,
  "data": {
    "id": 42,
    "programSessionId": 42,
    "name": "Monthly Satsang",
    "onlineType": "webinar",
    "startsAt": "2026-07-01T10:00:00Z",
    "endsAt": "...",
    "registrationStartsAt": "...",
    "registrationEndsAt": "...",
    "webinarDetails": { "externalId": "...", "joinUrl": "...", "registrationUrl": "...", "startUrl": null /* stripped */ },
    "meetingDetails": null
  },
  "error": null
}
```

---

## 8. Errors

| Condition | Exception / code | HTTP |
|---|---|---|
| Program session not found | `ZOOM_WEBINAR_NOTFOUND` | 404 |
| Program session already provisioned | `ONLINE_SESSION_ALREADY_EXISTS` | 409 |
| Zoom integration disabled (`ENABLE_ZOOM != 'true'`) | `ZOOM_DISABLED` | 400 |
| Zoom rejected request (scopes/payload) | `ZOOM_API_ERROR` (Zoom's message forwarded) | 400 |
| DB save fails after Zoom create | `ZOOM_WEBINAR_SAVE_FAILED` (Zoom resource rolled back) | mapped |

---

## 9. Relevant config (env)

`ENABLE_ZOOM`, `ZOOM_ACCOUNT_ID`, `ZOOM_CLIENT_ID`, `ZOOM_CLIENT_SECRET`,
`ZOOM_BASE_URL`, `ZOOM_AUTH_URL`, `ZOOM_ADMIN_EMAIL`, `ZOOM_TEMPLATE_ID`.
Auth is OAuth2 client-credentials via `ZoomOAuthService`; `ZoomApiService` injects
the bearer token and retries once on `401` after invalidating the cached token.
