# WhatsApp Nudge Testing — Interview Reminder Family (R5 / R6 / J3 / J4)

**Created:** 2026-06-08
**Branch:** `feat/wa-nudge-r5-r6-j3-j4-interview-reminders-2026-06-08`
**Service:** [`app/Services/InterviewReminderNudgeService.php`](../app/Services/InterviewReminderNudgeService.php)
**Cron entry-points:**
- [`cron/recruiter-interview-reminder-1day.php`](../cron/recruiter-interview-reminder-1day.php) (R5)
- [`cron/recruiter-interview-reminder-10min.php`](../cron/recruiter-interview-reminder-10min.php) (R6)
- [`cron/jobseeker-interview-reminder-1day.php`](../cron/jobseeker-interview-reminder-1day.php) (J3)
- [`cron/jobseeker-interview-reminder-10min.php`](../cron/jobseeker-interview-reminder-10min.php) (J4)

---

## What this family does

Four cron-driven WhatsApp reminders fired through Azmarq, with in-app mirror notifications:

| Nudge | Recipient | When | Template (Azmarq) |
|---|---|---|---|
| R5 | Recruiter | 1 day before interview | `cron_interview_reminder_recruiter_1_day` |
| R6 | Recruiter | ~10 min before interview | `cron_interview_reminder_recruiter_10_min` |
| J3 | Jobseeker | 1 day before interview | `cron_interview_reminder_jobseeker_1_day` |
| J4 | Jobseeker | ~10 min before interview | `cron_interview_reminder_jobseeker_10_min` |

**Filter (all four):** `users_job.schedule_status = 1` AND `users_job.job_status IN (3, 4)` — only scheduled (and not cancelled) interviews where the application is in "schedule pending" or "schedule confirmed".

**1-day window:** `DATE(schedule_call_date) = CURDATE() + INTERVAL 1 DAY` — any time of day tomorrow.

**10-min window:** `CONCAT(schedule_call_date, ' ', start_time) BETWEEN NOW() + 9 MIN AND NOW() + 12 MIN` — 3-minute envelope, matching the per-minute cron cadence with a safety overlap.

**Idempotency:** every send is logged to `wa_nudge_log` with a per-nudge `nudge_key` (`r5_recruiter_interview_1day`, `r6_recruiter_interview_10min`, `j3_jobseeker_interview_1day`, `j4_jobseeker_interview_10min`). Re-running the cron in the same window will skip the already-logged rows.

---

## Step 1 — Pre-deploy: dry-run on UAT

**Why:** Confirms SQL + joins + class wiring are valid against the live UAT schema. Doesn't send any messages.

**Do:**
```bash
# from your laptop, with the UAT SSH tunnel up (or use the standalone test script):
ssh -i /Users/apple/Projects/kaam-ec2/kaabil-cc-key-pair.pem -fN \
    -L 13308:kaabil-uat-1.c1asckcosor2.ap-south-1.rds.amazonaws.com:3306 \
    ubuntu@65.1.107.228

cd /Users/apple/Projects/kaabil-platform/UAT/recruiter
php cron/recruiter-interview-reminder-1day.php  --test --env=uat
php cron/recruiter-interview-reminder-10min.php --test --env=uat
php cron/jobseeker-interview-reminder-1day.php  --test --env=uat
php cron/jobseeker-interview-reminder-10min.php --test --env=uat
```

**Expected outcome (proves wiring works):**
Each cron prints:
```
=== <cron-name> cron (env=uat, dry_run=YES) ===
[RESULT] candidates=N, sent_whatsapp=0, failed_whatsapp=0, skipped_already=0, sent_in_app=0
```

**What it means:**
- `candidates=N` — number of rows matching the window+filter at this exact moment. Often 0 on UAT (we have very few scheduled interviews in the relevant time range).
- All other counters 0 because dry-run never calls Azmarq or writes to `users_notification`.
- Exit code 0.

**If this fails:** SQL error or missing column → fix the service before deploying.

---

## Step 2 — SCP deploy to UAT

**Why:** The recruiter repo does NOT auto-deploy via CodeBuild — manual SCP is the canonical path per the platform CLAUDE.md.

**Do:**
```bash
cd /Users/apple/Projects/kaabil-platform/UAT/recruiter
PEM=/Users/apple/Projects/kaam-ec2/kaabil-cc-key-pair.pem
FILES=(
  app/Services/NotificationService.php
  app/Services/InterviewReminderNudgeService.php
  cron/recruiter-interview-reminder-1day.php
  cron/recruiter-interview-reminder-10min.php
  cron/jobseeker-interview-reminder-1day.php
  cron/jobseeker-interview-reminder-10min.php
  docs/whatsapp-nudge-strategy-2026-06-08.md
  docs/wa-nudge-interview-reminder-family-testing.md
)
tar -cf /tmp/wa-r5-r6-j3-j4.tar "${FILES[@]}"
scp -i $PEM /tmp/wa-r5-r6-j3-j4.tar ubuntu@65.1.107.228:/tmp/
ssh -i $PEM ubuntu@65.1.107.228 "
  cd /var/www/uat.kaabil.cc/recruiter
  sudo tar -xf /tmp/wa-r5-r6-j3-j4.tar
  sudo chown -R www-data:www-data app/Services cron docs
  rm /tmp/wa-r5-r6-j3-j4.tar
"
```

**Expected:** tar extracted cleanly, file owners are `www-data`.

---

## Step 3 — Smoke test on UAT (live run with no data, --test omitted)

**Why:** Proves the cron behaves identically in the UAT environment to the local dry-run.

**Do (SSH to UAT EC2):**
```bash
ssh -i /Users/apple/Projects/kaam-ec2/kaabil-cc-key-pair.pem ubuntu@65.1.107.228
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/recruiter-interview-reminder-1day.php  --test
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/recruiter-interview-reminder-10min.php --test
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-1day.php  --test
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-10min.php --test
```

**Expected:** identical output to Step 1 (candidates=N, all other counters 0).

---

## Step 4 — End-to-end fire test (synthetic test row)

**Why:** UAT has no live interviews scheduled in the near future, so we need to create a controlled test row to actually exercise the Azmarq path.

**Setup:**
1. Pick a test user pair you control: a recruiter account + a jobseeker account, both with WhatsApp-active mobile numbers you can monitor.
2. SSH to UAT bastion + open the UAT DB:
   ```bash
   ssh -i $PEM -fN -L 13308:kaabil-uat-1.c1asckcosor2.ap-south-1.rds.amazonaws.com:3306 ubuntu@65.1.107.228
   mysql -h 127.0.0.1 -P 13308 -u missionevenuat -p mission_even_uat
   ```
3. Insert a synthetic interview row scheduled for tomorrow:
   ```sql
   INSERT INTO users_job
     (user_id, company_job_id, schedule_slot_master_id,
      schedule_call_date, schedule_status, job_status, created_date)
   VALUES
     (<jobseeker_user_id>, <your_test_company_job_id>, <any_slot_id_with_valid_start_time>,
      DATE_ADD(CURDATE(), INTERVAL 1 DAY), 1, 4, NOW());
   ```
4. Note the new `users_job.id` (the LAST_INSERT_ID).

**Fire test:**
```bash
# R5 + J3 (1-day window — your row is tomorrow):
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/recruiter-interview-reminder-1day.php
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-1day.php
```

**Expected:**
- Output: `candidates=1, sent_whatsapp=1, failed_whatsapp=0, skipped_already=0, sent_in_app=1`
- WhatsApp message arrives at the recruiter's mobile within 30-60 seconds.
- WhatsApp message arrives at the jobseeker's mobile within 30-60 seconds.
- Both recipients see an in-app red dot when they log in to the recruiter / jobseeker UI.

**Verify in DB:**
```sql
SELECT id, nudge_key, channel, status, template_name, recipient_mobile, sent_at, error_text
FROM wa_nudge_log
WHERE entity_id = <users_job_id_from_step_4>
ORDER BY id DESC;
```

Expected: 4 rows (2 per nudge — one `whatsapp` + one `in_app`), all with `status = 'sent'`, no `error_text`.

---

## Step 5 — Idempotency test

**Why:** Re-running the cron should NOT double-send.

**Do:**
```bash
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/recruiter-interview-reminder-1day.php
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-1day.php
```

**Expected output:**
```
[RESULT] candidates=1, sent_whatsapp=0, failed_whatsapp=0, skipped_already=1, sent_in_app=0
```

`skipped_already=1` is the key — the row is in `wa_nudge_log` from Step 4, so this run sees it and skips.

**Verify in DB:** no new rows in `wa_nudge_log` after this run.

---

## Step 6 — 10-min window test (R6 + J4)

**Why:** R5/J3 fire on a daily-granular window; R6/J4 fire on a 3-minute window. The SQL is different and needs separate verification.

**Setup:**
1. Pick a `schedule_slot_master_id` whose `start_time` is exactly 10 minutes from now.
   - Check the slot table: `SELECT id, start_time FROM schedule_slot_master WHERE start_time BETWEEN DATE_ADD(NOW(), INTERVAL 9 MINUTE) AND DATE_ADD(NOW(), INTERVAL 12 MINUTE);`
   - If no slot exists in that window, temporarily UPDATE an unused slot's `start_time` to `NOW() + 10 MIN`.
2. Insert another synthetic users_job row with `schedule_call_date = CURDATE()` and that slot id.

**Fire test:**
```bash
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/recruiter-interview-reminder-10min.php
sudo -u www-data php /var/www/uat.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-10min.php
```

**Expected:** `candidates=1, sent_whatsapp=1, ..., sent_in_app=1`.

WhatsApp arrives at both recipients within ~60 seconds.

---

## Step 7 — Production cron schedule (for the deploy commands later)

Once Step 6 passes on UAT, add these to PROD's crontab:

```cron
# Interview reminders — 1 day out (single 9 AM IST run)
30 3 * * * /var/www/tools.kaabil.cc/recruiter/cron/cron-with-alert.sh "R5-Recruiter-Interview-1Day-PROD" "php /var/www/tools.kaabil.cc/recruiter/cron/recruiter-interview-reminder-1day.php" /var/www/tools.kaabil.cc/recruiter/logs/cron.log
30 3 * * * /var/www/tools.kaabil.cc/recruiter/cron/cron-with-alert.sh "J3-Jobseeker-Interview-1Day-PROD" "php /var/www/tools.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-1day.php" /var/www/tools.kaabil.cc/recruiter/logs/cron.log

# Interview reminders — 10 min out (per-minute, with 3-min window)
* * * * * /var/www/tools.kaabil.cc/recruiter/cron/cron-with-alert.sh "R6-Recruiter-Interview-10Min-PROD" "php /var/www/tools.kaabil.cc/recruiter/cron/recruiter-interview-reminder-10min.php" /var/www/tools.kaabil.cc/recruiter/logs/cron.log
* * * * * /var/www/tools.kaabil.cc/recruiter/cron/cron-with-alert.sh "J4-Jobseeker-Interview-10Min-PROD" "php /var/www/tools.kaabil.cc/recruiter/cron/jobseeker-interview-reminder-10min.php" /var/www/tools.kaabil.cc/recruiter/logs/cron.log
```

(UAT same lines under `/var/www/uat.kaabil.cc/recruiter/` once verification completes.)

---

## Pass criteria summary

A nudge is considered live and verified when:
- ✅ Step 1 dry-run exits cleanly with valid JSON
- ✅ Step 2 SCP completes; files owned by `www-data` on UAT
- ✅ Step 3 smoke test mirrors Step 1
- ✅ Step 4 fire test produces real WhatsApp + in-app delivery within 60s
- ✅ Step 5 idempotency check shows `skipped_already=1` on re-run
- ✅ Step 6 (R6/J4 only) 10-min window fires identically
- ✅ Cron schedule added to UAT crontab

Mark the row in [`whatsapp-nudge-strategy-2026-06-08.md`](whatsapp-nudge-strategy-2026-06-08.md) as ✅ after each nudge passes all 7 gates.
