# Sprint 1.2 + 1.3 — Recipes Complete + Campaign Dispatcher Combined sprint delivering all four warmup recipes and live campaign-aware sending. ## What shipped ### Sprint 1.2 — Recipe engine (complete) - **Grow**, **Flat**, **Randomized**, and **Custom** recipes via `generateSchedule()` in `recipe.service.ts` - Randomized uses a seeded PRNG (`campaignId` at start) plus the 48h volume cap - Custom validates per-day sends in `[min, max]` and fills gaps with hold-last - `POST /api/recipes/preview` accepts all four recipes (optional `customSchedule` for custom) - `GET /api/recipes/templates` returns templates for all four recipes - `POST /api/campaigns/:id/start` persists the correct schedule per recipe ### Sprint 1.3 — Campaign dispatcher (complete) - **`campaign-dispatcher.job.ts`** — satellite → primary sends for active/maintenance campaigns - **`campaign-rollover.job.ts`** + **`syncCampaignDay()`** — day rollover and maintenance transition - **`send-window.ts`** — timezone-aware send windows (luxon) - **`lastSendAt`** on `WarmupCampaign` — 45 min minimum jitter between sends - Campaign-aware **rescuer** — primary → satellite replies with `actualReplies` tracking - Manual job endpoints: `POST /api/jobs/dispatcher/run`, `POST /api/jobs/rollover/run` - Legacy flat dispatcher still runs when **zero** campaigns exist ## Traffic rules | Direction | Job | | ------------------- | --------- | | Satellite → Primary | Dispatcher | | Primary → Satellite | Rescuer (reply only) | ## Scheduler | Job | Schedule | | ---------------------- | --------------------- | | Campaign dispatcher | `dispatcher_cron` setting | | Legacy dispatcher | Same cron (gated) | | Rescuer | `rescuer_cron` setting | | Campaign rollover | Hourly (`0 * * * *`) | ## Manual QA ```bash # 1. Create primary + 2+ satellites (see sprint-1.1.md) # 2. Create campaign (recipe: grow), assign satellites, start # 3. Trigger dispatch: curl -X POST http://localhost:3000/api/jobs/dispatcher/run -H "x-api-key: $API_KEY" # 4. Check logs: curl http://localhost:3000/api/logs?inboxId= -H "x-api-key: $API_KEY" # 5. Wait for rescuer cron or trigger rescuer manually via cron interval # 6. Repeat with recipe:flat on a second primary to verify recipe router ``` ## Preview examples ```bash # Flat 30-day preview curl -X POST http://localhost:3000/api/recipes/preview \ -H "Content-Type: application/json" -H "x-api-key: $API_KEY" \ -d '{"recipe":"flat","durationDays":30,"maxEmailsPerDay":40}' # Custom schedule curl -X POST http://localhost:3000/api/recipes/preview \ -H "Content-Type: application/json" -H "x-api-key: $API_KEY" \ -d '{"recipe":"custom","durationDays":14,"customSchedule":[{"day":1,"sends":2},{"day":2,"sends":5}]}' ``` ## Deferred to Sprint 1.4 - Probabilistic reply rate (~30% sampling) - `engaged_no_reply` status ## Key files - `src/services/recipe.service.ts` - `src/services/campaign.service.ts` - `src/jobs/campaign-dispatcher.job.ts` - `src/jobs/campaign-rollover.job.ts` - `src/jobs/rescuer.job.ts` - `src/lib/send-window.ts`