# General-Link Communications — QA Checklist

**Feature branch:** `feature/zoom-common-links`
**Scope:** Bulk/single communications to `zoom_generated_registrant_link` recipients (Welcome / Invite / Absent / Program Completion), reusing the seeker templates.

---

## 1. What changed (context for the tester)

- Recipients who joined via a **generated general Zoom link** (no program registration of their own) can now be sent WELCOME / INVITE / ABSENT / PROGRAM_COMPLETION communications, in bulk (whole program or one session) or as a single send to one recipient.
- These sends **reuse the exact same templates** as the real seeker communications (same `WELCOME_EMAIL_SEEKER` / `INVITE_REGULAR_SESSION_EMAIL_SEEKER` / etc. templates already configured per program) — there is no separate "general link" template set to configure.
- Only recipients with a **real `user_id`** (role-matched staff) are ever sent to — anonymous **PLACEHOLDER** batch-slot rows (e.g. "Staff x20") are always excluded, since there's no real person behind them.
- Eligibility is **always enforced**, not optional: status must be `REGISTERED`, and ABSENT only reaches real absentees of the target session — no bypass flag. **INVITE at the final session is NOT gated by prior-session attendance** (unlike the seeker rule) — these recipients are staff/admin, not seekers on a structured multi-session journey, so `occurrence` only picks which template (regular vs. final) is used, it never narrows who receives it.
- `GET /v1/analytics/sessions/:sessionId/general-attendees` now returns a `generatedLinkId` on each known general-attendee row — this is the id the single-send endpoint needs.

---

## 2. Endpoints to test

| Method | Path | Purpose |
|---|---|---|
| POST | `/session-communication/general-link/bulk` | Send to every general-link recipient of a program (or one session) |
| POST | `/session-communication/general-link/single` | Send to one general-link recipient by `generatedLinkId` |
| GET | `/v1/analytics/sessions/:sessionId/general-attendees` | List general attendees — verify `generatedLinkId` is present |

### Sample bulk payload
```json
{
  "programId": 5,
  "sessionId": 12,
  "purpose": "GENERAL_LINK_INVITE"
}
```
(`sessionId` omitted for `GENERAL_LINK_WELCOME` / `GENERAL_LINK_PROGRAM_COMPLETION` — those are program-level.)

### Sample single payload
```json
{
  "programId": 5,
  "generatedLinkId": 4210,
  "purpose": "GENERAL_LINK_INVITE",
  "occurrence": "REGULAR"
}
```
(`occurrence` is required only for `GENERAL_LINK_INVITE` / `GENERAL_LINK_ABSENT`, and must be `REGULAR` or `FINAL`. `sessionId` is **not** a field on this endpoint — `generatedLinkId` already pins the session.)

---

## 3. Functional checklist

### Bulk send
- [ ] `GENERAL_LINK_WELCOME` (no `sessionId`) sends to every eligible general-link recipient across the whole program.
- [ ] `GENERAL_LINK_PROGRAM_COMPLETION` (no `sessionId`) sends program-wide; for a **TAT** program, only recipients who attended the **final session** qualify.
- [ ] `GENERAL_LINK_INVITE` for a regular (non-last) session uses the **regular** template.
- [ ] `GENERAL_LINK_INVITE` for the program's **last session** (multi-session TAT program) uses the **final** template — and reaches every eligible recipient regardless of whether they attended earlier sessions (no attendance gate for this purpose).
- [ ] `GENERAL_LINK_ABSENT` only reaches recipients who did **not** attend the target session — never someone who actually attended.
- [ ] A **PLACEHOLDER** row (e.g. a "Staff" batch slot with no real user) never receives any communication, bulk or single.
- [ ] A generated-link row with `status != REGISTERED` is never sent to.
- [ ] Sending with zero eligible recipients returns a clean "no recipients" error, not a silent empty success.
- [ ] Sending is recorded in the communication status/summary the same way a seeker send is (so it shows up wherever "was this comm sent" is surfaced in the admin UI).

### Single send
- [ ] Sending to a valid `generatedLinkId` for the correct `programId` succeeds.
- [ ] Sending with a `generatedLinkId` that belongs to a **different program** than the one in the payload is rejected (cross-program guard) — this used to silently succeed and must not anymore.
- [ ] Sending to a PLACEHOLDER row's id is rejected ("not applicable").
- [ ] `GENERAL_LINK_INVITE`/`GENERAL_LINK_ABSENT` without `occurrence` is rejected; `GENERAL_LINK_WELCOME`/`GENERAL_LINK_PROGRAM_COMPLETION` **with** an `occurrence` is rejected.
- [ ] `occurrence: FINAL` on `GENERAL_LINK_INVITE` succeeds regardless of the recipient's attendance at earlier sessions (no gate). `occurrence: FINAL`/`REGULAR` on `GENERAL_LINK_ABSENT` still only succeeds when the recipient did not attend *that* session.

### Content / merge fields
- [ ] The email/WhatsApp actually received shows the **recipient's own name**, their **own join link**, and the correct **meeting id / passcode** — not blank, not another recipient's data.
- [ ] Program-level merge fields (program name, session dates, etc.) render correctly same as they do for a real seeker send.

### General-attendees list
- [ ] `GET /v1/analytics/sessions/:sessionId/general-attendees` includes `generatedLinkId` (a number) for every **known** general row (one matched to a generated-link row).
- [ ] `generatedLinkId` is `null` for an **unmatched/unknown** general row (a true walk-in with no generated-link match) and for registered-seeker rows.
- [ ] The `generatedLinkId` returned for a row matches what `/session-communication/general-link/single` expects and successfully resolves that same recipient.

---

## 4. Regression checks (must NOT change)

- [ ] Real seeker WELCOME / INVITE / ABSENT / PROGRAM_COMPLETION sends still work exactly as before — same templates, same recipients, same merge-field values. (No template configuration changed; only a fallback path was added that only activates when there's no registration, which never applies to a real seeker send.)
- [ ] Common Invite and System Links sends (the other registration-less communication types) are unaffected.
- [ ] Existing `/session-communication/bulk` and `/session-communication/single` endpoints (seeker purposes) behave identically to before this change.

---

## 5. Notes for the tester

- No new templates need to be configured for general-link sends — they ride on whatever WELCOME/INVITE/ABSENT/PROGRAM_COMPLETION templates are already set up for the program.
- `applyValidation` is **gone** from both general-link request bodies — eligibility is always applied now, it's not a toggle. If you have old Postman collections/scripts with that field, drop it (it's ignored by validation and will be stripped, not silently accepted).
- If a send appears to do nothing, check: (a) the recipient has a real `user_id` (not PLACEHOLDER), (b) `status = REGISTERED`, (c) for ABSENT, whether they actually didn't attend the target session, (d) the relevant template is actually configured + enabled for that program.
