# R7 — Job Expiring 3 Days — Testing Checklist

**Nudge key:** `r7_job_expiry_3day`
**Trigger:** Daily cron at 9:00 AM IST
**Recipient:** Recruiter who posted the job (`company_jobs.created_by`)
**Channels:** WhatsApp (via Azmarq) + in-app red dot
**WhatsApp template:** `cron_job_expiry_alert_recruiter_3_days`
**Body params (order):** `first_name`, `job_title`, `repost_url`

---

## Pre-deployment — verify before any UAT push

1. ✅ Migration `migrations/011_wa_nudge_log.sql` applied on UAT DB.
   - **How:** `mysql -h … mission_even_uat < migrations/011_wa_nudge_log.sql`
   - **Expected:** `wa_nudge_log` table exists, `DESC wa_nudge_log` returns the 11 columns documented in the migration. Re-running the migration produces no error (uses `CREATE TABLE IF NOT EXISTS`).

2. ✅ Azmarq template registered + Meta-approved for `cron_job_expiry_alert_recruiter_3_days` with body params in this exact order: `first_name`, `job_title`, `repost_url`.
   - **How:** Azmarq dashboard → Templates → search for the name → check "Body params" matches.
   - **Expected:** 3 params, status = Approved.
   - **If not approved yet:** Steps below will still run (dry-run) but live mode will receive Azmarq error. Add the template name to the dashboard, wait for Meta approval, then re-run.

3. ✅ `NotificationService::WA_TEMPLATE_JOB_EXPIRY_3DAY` constant matches the registered Azmarq template name exactly (case-sensitive).
   - **How:** `grep WA_TEMPLATE_JOB_EXPIRY_3DAY app/Services/NotificationService.php`
   - **Expected:** Returns `cron_job_expiry_alert_recruiter_3_days` — same string.

---

## Test 1 — Dry-run on local (no real send)

**Why:** Validates the SQL, the payload shape, and the log writes. No real WhatsApp leaves the machine.

```bash
cd /Users/apple/Projects/kaabil-platform/UAT/recruiter
php cron/job-expiring-soon.php --test
```

**Expected:**
- Header line: `=== job-expiring-soon cron (env=local, days_out=3, mode=wa, dry_run=YES) ===`
- Result line: `[RESULT] candidates=N, sent_whatsapp=N, failed_whatsapp=0, skipped_already=0, sent_in_app=N, failed_in_app=0`
- `wa_nudge_log` gains N rows with `status='dry_run'` and `channel='whatsapp'` + N rows with `status='dry_run'` and `channel='in_app'`.

**How to verify:**
```sql
SELECT id, nudge_key, entity_id, channel, status, template_name, recipient_mobile, sent_at
  FROM wa_nudge_log
 WHERE nudge_key = 'r7_job_expiry_3day' AND status = 'dry_run'
 ORDER BY id DESC LIMIT 10;
```

**If candidates = 0:** PROD/UAT may not currently have any job whose expiry is exactly 3 days out. Bump the window to flush something out:
```bash
php cron/job-expiring-soon.php --test --days=30
```

**Failure modes to investigate:**
| Symptom | Likely cause |
|---|---|
| Header prints but no result | `wa_nudge_log` table missing — re-apply migration |
| `Class WhatsappNudgeService not found` | autoloader not picking the require_once — check the path in JobExpiry3DayNudgeService.php top |
| Result has `failed_whatsapp=N`, all candidates failed | `sendWhatsApp` failing — usually missing `AZMARQ_API_KEY` in `config.php` (local dev). Expected if you haven't set local Azmarq creds. NOT a blocker for the test — the dry-run path doesn't actually call Azmarq. |

---

## Test 2 — Idempotency

**Why:** Confirms re-running the cron the same day doesn't double-fire.

```bash
# Run live once (or simulate via the next test)
php cron/job-expiring-soon.php

# Immediately re-run
php cron/job-expiring-soon.php
```

**Expected:** Second run prints `skipped_already=N` matching the first run's `sent_whatsapp=N`. No new rows are inserted into `wa_nudge_log` for the `whatsapp` channel on the same `entity_id`.

**SQL to verify:**
```sql
SELECT entity_id, COUNT(*) AS sends
  FROM wa_nudge_log
 WHERE nudge_key='r7_job_expiry_3day' AND channel='whatsapp' AND status IN ('sent', 'short_circuited')
 GROUP BY entity_id
 HAVING sends > 1;
-- Expected: 0 rows.
```

---

## Test 3 — Live UAT run (after deploy)

**Why:** Real Azmarq send + real in-app insert against UAT DB.

**Pre-conditions:**
- `wa_nudge_log` migration applied on UAT RDS.
- Azmarq template `cron_job_expiry_alert_recruiter_3_days` is APPROVED on the Azmarq side.
- The UAT recruiter container has valid `AZMARQ_API_KEY` in its config.

**Steps:**
1. SSH to the UAT recruiter EC2 (uat.kaabil.cc).
2. Find or create a test job that will expire in exactly 3 days:
   ```sql
   -- Use a job you control + whose `created_by` is YOUR test recruiter
   UPDATE company_jobs SET job_expiry = DATE_ADD(CURDATE(), INTERVAL 3 DAY)
    WHERE id = <YOUR_TEST_JOB_ID>;
   ```
3. Make sure your `users.mobile` is set to a WhatsApp number YOU control.
4. Run the cron:
   ```bash
   sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/job-expiring-soon.php
   ```
5. **Expected within 30–60s:**
   - WhatsApp arrives on your phone with: `Hi {first_name}, your job post for {job_title} will expire in 3 days. Repost now: {repost_url}` (or whatever Azmarq has registered as the template body).
   - `wa_nudge_log` shows 2 new rows for your job: `channel='whatsapp', status='sent'` + `channel='in_app', status='sent'`.
   - The recruiter dashboard for that user shows the red dot, and the notification row links to the job detail (routsId=3).

**Failure modes:**
| Symptom | Likely cause |
|---|---|
| Result shows `sent_whatsapp` > 0 but no WhatsApp arrives | Template not approved on Meta side, or Azmarq webhook says `Buttons variables value mis-match`. Check Azmarq dashboard → Send Reports. |
| `failed_whatsapp > 0` | `AZMARQ_API_KEY` missing/invalid in config, or mobile number malformed. Check error log: `tail -200 /var/log/apache2/error.log \| grep WhatsApp` |
| Result shows `skipped_already` but you DID change the job | The `wa_nudge_log` already has a row from an earlier test run — clear it: `DELETE FROM wa_nudge_log WHERE nudge_key='r7_job_expiry_3day' AND entity_id=<your_job_id>;` |

---

## Test 4 — Mode fallback (CleverTap path still works)

**Why:** Confirms we didn't break the legacy code path. If CleverTap journey becomes available later, this is how it gets re-enabled.

```bash
php cron/job-expiring-soon.php --test --mode=ct
```

**Expected:**
- Header includes `mode=ct`.
- Per-job lines print `[DRY-RUN]`.
- No new `wa_nudge_log` rows (the CT path doesn't write there — it would write to CleverTap's event store on a live run).

---

## Test 5 — Edge cases

| Case | Setup | Expected |
|---|---|---|
| Job's recruiter has no `users.mobile` | `UPDATE users SET mobile='' WHERE id=<recruiter_id>` | Candidate is EXCLUDED from the SQL (`AND u.mobile <> ''`). Result `candidates` doesn't count it. |
| Job has 0 applications | Default state for new jobs | Still fires — expiry nudge isn't application-conditional (unlike inactivity nudge) |
| Job is `status=4` (closed) | `UPDATE company_jobs SET status=4 WHERE id=<>` | Candidate EXCLUDED (`WHERE cj.status=2`). No nudge. |
| Job already nudged today | Run #2 directly after run #1 | Skipped. `wa_nudge_log` shows only one `sent` row for the channel. |
| Cron fails mid-run | Simulate via `kill -9` during a long run | Re-run picks up at the next candidate; previously-sent jobs are idempotency-skipped. No duplicates. |

---

## Post-test cleanup (UAT)

```sql
-- If you set a test job's expiry, you can revert it:
UPDATE company_jobs SET job_expiry = <original_value> WHERE id = <your_test_job_id>;

-- Test rows in wa_nudge_log can be left in place (they're informative for audit).
-- To purge specifically your test:
DELETE FROM wa_nudge_log
 WHERE nudge_key = 'r7_job_expiry_3day'
   AND entity_id = <your_test_job_id>;
```

---

## Definition of done

R7 is marked **complete** in [docs/whatsapp-nudge-strategy-2026-06-08.md](whatsapp-nudge-strategy-2026-06-08.md) only after:

- ✅ Migration applied to UAT DB
- ✅ Test 1 (dry-run) passes locally
- ✅ Test 2 (idempotency) passes
- ✅ Test 3 (live UAT) — WhatsApp received on a test phone AND in-app notification visible on dashboard
- ✅ Test 4 (mode fallback) passes
- ✅ Test 5 (edge cases) all pass
- ✅ MR merged via GitLab UI (audit-friendly, no direct push)
