# Ralph Wiggum Loop — Task List

> *"I'm helpin'!"*

Ordered, atomic tasks for the email + SMS marketing build. Each task is the atomic unit a self-correcting coder loop picks up, implements, verifies, and commits. **One task in-progress at a time, max 3 retry attempts per task before escalation.**

**Legend**
- **ID:** stable task identifier (`T-001` … referenced by commits)
- **Phase:** maps to [PRD.md](PRD.md) §10 phases
- **Files:** primary files touched (loop guardrail: ≤5 per task)
- **Verify:** the deterministic pass/fail check the loop must run before marking complete
- **Depends on:** task IDs that MUST be complete first

---

## Pre-flight (humans only — loop will not auto-run these)

| ID | Title | Owner | Action |
|---|---|---|---|
| **P-1** | Move repo out of iCloud Drive | Geo | Move to `~/Sites/tteotk-devotionals` or similar non-synced path. |
| **P-2** | Production backup | Geo | `mysqldump` from cPanel + `rsync` `storage/app/`; store offsite. |
| **P-3** | Delete `public/_*.php` and root `_*.php` maintenance scripts | Geo | Rotate admin password first, then delete. |
| **P-4** | Remove `.env` + `credentials.md` from git history | Geo | `git filter-repo`; rotate `APP_KEY`. |
| **P-5** | Resend account + DNS access | Geo | Sign up; ready to add SPF/DKIM/DMARC at T-008. |
| **P-6** | Anthropic + Gemini + Twilio accounts | Geo | API keys in hand by T-024 / T-031. |
| **P-7** | Twilio A2P 10DLC brand registration started | Geo | 3–6 week process; start NOW so it's ready by Phase 7. |

---

## Phase 0 — Foundations

| ID | Title | Files | Verify | Depends on |
|---|---|---|---|---|
| **T-001** | Add composer packages (resend, league/csv, mustache, twilio) | `composer.json` | Human runs `composer install`; loop runs `composer validate` and verifies `vendor/resend/resend-laravel` exists. | P-1 |
| **T-002** | Add npm packages (grapesjs, grapesjs-mjml, mjml-browser, alpinejs, chart.js) | `package.json` | Human runs `npm install`; loop verifies `node_modules/grapesjs/package.json` exists. | P-1 |
| **T-003** | Add Resend driver block to `config/mail.php` already present; add `MAIL_REPLY_TO` to env config | `config/mail.php`, `.env.example` | `php artisan config:show mail.from.address` matches expected. | T-001 |
| **T-004** | Add `services.php` blocks for `anthropic`, `gemini`, `twilio`, `resend.webhook_secret`, `mail.postal_address` | `config/services.php`, `.env.example` | `php artisan config:show services.anthropic` returns array. | T-001 |
| **T-005** | Switch `QUEUE_CONNECTION=database` and publish queue migrations | `.env.example`, `database/migrations/*queue*` | `php artisan queue:table && php artisan queue:failed-table`; migrations file exists. | T-001 |
| **T-006** | Register scheduled `queue:work` task | `routes/console.php` | `php artisan schedule:list` shows the queue:work job every minute. | T-005 |
| **T-007** | cPanel cron `* * * * * php artisan schedule:run` | `EMAIL_SETUP.md` | Document only; verify by tail of `storage/logs/laravel.log` after install. | P-1 |
| **T-008** | Write `EMAIL_SETUP.md` (DNS records, cron line, A2P 10DLC steps) | `EMAIL_SETUP.md` | File exists, contains SPF/DKIM/DMARC examples for `mail.throughtheeyesoftheking.com`. | T-004 |
| **T-009** | Add safety: rate limit `/login` and `/register` (`throttle:6,1`) | `routes/web.php` | `php artisan route:list` shows throttle middleware on those routes. | — |

---

## Phase 1 — Subscribers, Tags, Signup, Compliance

### Database
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-101** | ✅ Migration: subscribers + tags + subscriber_tag + subscription_events + suppressions | `database/migrations/2026_04_29_000001_create_email_marketing_core_tables.php` | **DONE.** `php artisan migrate` succeeds; `Schema::hasTable('subscribers')` true. |
| **T-102** | Seeder: `general` tag (is_default=true, is_public=true) | `database/seeders/TagSeeder.php`, `DatabaseSeeder.php` | `Tag::where('slug','general')->exists()` true after `db:seed --class=TagSeeder`. |

### Models
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-103** | ✅ Models: Subscriber, Tag, SubscriptionEvent, Suppression | `app/Models/{Subscriber,Tag,SubscriptionEvent,Suppression}.php` | **DONE.** `Subscriber::factory()` not needed; tinker create + retrieve works. |
| **T-104** | Add `subscriber()` relation to `User` model (by email) | `app/Models/User.php` | Tinker: `User::first()->subscriber` returns null or Subscriber instance. |

### Services
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-105** | `App\Services\SubscriberService` (subscribe, confirm, unsubscribe, addTags, removeTags, suppress) | `app/Services/SubscriberService.php` | Feature test `tests/Feature/SubscriberServiceTest.php` covers DOI flow + suppression dedup; passes. |
| **T-106** | `App\Services\Compliance\UnsubscribeTokenService` (sign + verify URLs) | `app/Services/Compliance/UnsubscribeTokenService.php` | Unit test covers sign → verify roundtrip + tampered token rejected. |
| **T-107** | `App\Services\Compliance\PersonalizationRenderer` (Mustache wrapper, default-token support) | `app/Services/Compliance/PersonalizationRenderer.php` | Unit test: `{{first_name|default:"Friend"}}` renders correctly with empty + non-empty values. |

### Mailables + templates
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-108** | `App\Mail\ConfirmationEmail` mailable + Blade template | `app/Mail/ConfirmationEmail.php`, `resources/views/emails/confirmation.blade.php` | `Mail::fake(); SubscriberService::subscribe(...); Mail::assertQueued(ConfirmationEmail::class)`. |
| **T-109** | `App\Mail\WelcomeEmail` mailable + Blade template | `app/Mail/WelcomeEmail.php`, `resources/views/emails/welcome.blade.php` | Same pattern as T-108 after confirm. |
| **T-110** | Email layout w/ logo, postal address footer, unsubscribe link, view-in-browser | `resources/views/emails/layouts/transactional.blade.php` | Snapshot test renders without errors; contains `{{unsubscribe_url}}`. |

### Public routes + controllers
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-111** | `SubscribeController` (GET /subscribe, POST /subscribe) | `app/Http/Controllers/SubscribeController.php`, `routes/web.php`, `resources/views/subscribe/index.blade.php` | Feature test: POST with valid email → 302 + pending subscriber row + queued ConfirmationEmail. |
| **T-112** | `UnsubscribeController` (GET preference center, POST one-click, POST update prefs) | `app/Http/Controllers/UnsubscribeController.php`, `resources/views/subscribe/preferences.blade.php`, `routes/web.php` | Feature test: GET valid token → 200 with tag toggles; POST One-Click → status flips to unsubscribed. |
| **T-113** | `GET /confirm/{token}` confirmation endpoint | `app/Http/Controllers/SubscribeController.php` (`confirm` action) | Feature test: valid token flips status to active, fires WelcomeEmail, logs `double_opt_in_confirmed` event. |
| **T-114** | Rate limit subscribe + confirm + unsubscribe routes | `routes/web.php` | `route:list` shows `throttle:10,1` on POST /subscribe. |

### Auto-subscribe registrants
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-115** | Hook `AuthController::register()` → `SubscriberService::subscribe(skipConfirmation: true, source: 'registration', tags: ['general'])` | `app/Http/Controllers/AuthController.php` | Feature test: registering a user creates active subscriber row + general tag + opt_in event. |

### Home-page popup
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-116** | Alpine.js subscribe popup partial (30-day localStorage cookie) | `resources/views/partials/subscribe-popup.blade.php`, `resources/views/home.blade.php` (include) | Manual smoke: visit /, modal appears, dismiss → no reappear; submit → success state; refresh → no reappear. |
| **T-117** | Footer link "Manage email subscriptions" + `/subscribe` link in main layout | `resources/views/layouts/app.blade.php` | Visit /, footer link visible, navigates to /subscribe. |

### Smoke tests
| ID | Title | Files | Verify |
|---|---|---|---|
| **T-118** | Phase 1 e2e feature test (signup → confirm → preference toggle → one-click unsub → suppression check) | `tests/Feature/SubscriberLifecycleTest.php` | `php artisan test --filter=SubscriberLifecycleTest` green. |
| **T-119** | Verify `List-Unsubscribe` headers on Confirmation + Welcome | `tests/Feature/ListUnsubscribeHeaderTest.php` | Test inspects rendered Symfony Email and asserts both headers present. |

---

## Phase 2 — Campaigns + GrapesJS Editor + Sending

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-201** | Migration: `email_templates`, `campaigns`, `campaign_tag` (with `mode` include/exclude), `campaign_recipients` | `database/migrations/2026_04_29_000002_*.php` | Migrate succeeds; indexes on `campaign_recipients(campaign_id, status)`. |
| **T-202** | Models with relations | `app/Models/{EmailTemplate,Campaign,CampaignRecipient}.php` | Tinker: relations work. |
| **T-203** | `Admin\TemplateController` CRUD | `app/Http/Controllers/Admin/TemplateController.php`, `routes/web.php`, `resources/views/admin/templates/*` | Feature test: create + list + delete. |
| **T-204** | `Admin\CampaignController` CRUD (no editor yet) | `app/Http/Controllers/Admin/CampaignController.php`, `routes/web.php`, `resources/views/admin/campaigns/*` | Feature test: create draft, edit subject, save. |
| **T-205** | GrapesJS bundle wired through Vite | `resources/js/editor/grapesjs-init.js`, `vite.config.js`, `package.json` (already in T-002) | `npm run build` produces `public/build/assets/grapesjs-*.js`. |
| **T-206** | Editor view loads GrapesJS, MJML preset, asset manager pointed at `/admin/media` | `resources/views/admin/campaigns/editor.blade.php` | Manual: open editor, drop a section, type text, see preview. |
| **T-207** | `POST /admin/campaigns/{id}/save` accepts mjml_source + compiled_html, runs CSS inliner | `app/Http/Controllers/Admin/CampaignController.php` | Feature test: POST roundtrip persists both columns. |
| **T-208** | Personalization token UI + insert | `resources/views/admin/campaigns/editor.blade.php` (sidebar) | Manual: insert `{{first_name}}` token into MJML. |
| **T-209** | Send-test-email endpoint | `app/Http/Controllers/Admin/CampaignController.php`, `app/Mail/CampaignTestMail.php` | Manual: enter address, mail logged in `storage/logs/laravel.log` (Mail::fake in test). |
| **T-210** | Pre-send checklist guard + estimated reach | `app/Http/Controllers/Admin/CampaignController.php`, `resources/views/admin/campaigns/preflight.blade.php` | Feature test: campaign without unsubscribe token → returns validation error. |
| **T-211** | `BuildCampaignRecipientsJob` (expand tag membership minus suppressions/unsubs/bounces) | `app/Jobs/BuildCampaignRecipientsJob.php` | Feature test: 3 subscribers, 1 suppressed → 2 recipient rows. |
| **T-212** | `DispatchCampaignBatchJob` (batch 200, render personalized, send via Resend, mark sent) | `app/Jobs/DispatchCampaignBatchJob.php`, `app/Mail/CampaignMail.php` | Feature test with `Mail::fake()`: batch flips queued → sent. |
| **T-213** | Schedule modes (now / datetime / per-tz / cron recurring) | `app/Http/Controllers/Admin/CampaignController.php`, `app/Console/Commands/DispatchScheduledCampaigns.php` | Feature test: scheduled_for past → status flips to sending. |
| **T-214** | Pause / cancel campaign | `app/Http/Controllers/Admin/CampaignController.php` | Feature test: status flips correctly; in-flight batch finishes. |
| **T-215** | View-in-browser endpoint `/email/view/{recipient_token}` | `app/Http/Controllers/EmailViewController.php`, `routes/web.php` | Feature test: valid token → 200 with rendered HTML; invalid → 404. |

---

## Phase 3 — Webhooks + Deliverability

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-301** | `POST /webhooks/resend` HMAC-verified handler | `app/Http/Controllers/EmailWebhookController.php`, `routes/web.php` | Feature test: forged signature → 401; valid → 200 + status update + event log. |
| **T-302** | Auto-suppress on hard bounce / complaint | (same controller + SubscriberService) | Feature test: bounce webhook → row in `suppressions` + subscriber status `bounced`. |
| **T-303** | Auto-pause campaigns when complaint rate >0.3% | `app/Console/Commands/MonitorComplaintRate.php` (scheduled hourly) | Feature test seeds complaints, runs command, asserts campaign paused. |
| **T-304** | Daily admin digest email | `app/Console/Commands/SendAdminDigest.php`, `app/Mail/AdminDigestMail.php` | Feature test with Mail::fake. |
| **T-305** | Inject `List-Unsubscribe` + `List-Unsubscribe-Post` on every CampaignMail | `app/Mail/CampaignMail.php` | Test asserts both headers present in rendered Symfony Email. |

---

## Phase 4 — Analytics

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-401** | Open pixel `/e/o/{recipient_token}.gif` | `app/Http/Controllers/TrackingController.php` | Feature test: returns 1x1 GIF, logs open event, sets `opened_at`. |
| **T-402** | Click redirect `/e/c/{recipient_token}/{link_hash}` with auto-UTM | (same controller) | Feature test: 302 to original URL with `utm_*` appended. |
| **T-403** | Link rewriter applied during personalization | `app/Services/Mail/LinkRewriter.php` | Unit test: `<a href="x">` → tracked URL, mailto/anchors skipped. |
| **T-404** | Per-campaign report page (sent/delivered/opens/clicks/CTR/bounce/complaint/unsub, top links, hour chart) | `app/Http/Controllers/Admin/ReportController.php`, `resources/views/admin/reports/show.blade.php` | Manual: visit report, charts render. |
| **T-405** | RFM engagement score nightly job | `app/Console/Commands/RecomputeEngagementScores.php` | Unit test: known input → expected score. |

---

## Phase 5 — AI Assist

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-501** | `App\Services\AI\ClaudeService` (subject, preheader, body, rewrite) | `app/Services/AI/ClaudeService.php` | Unit test mocks HTTP; asserts payload + parses response. |
| **T-502** | `App\Services\AI\NanoBananaService` (Gemini 2.5 Flash Image) | `app/Services/AI/NanoBananaService.php` | Unit test mocks HTTP; asserts saved image record. |
| **T-503** | `Admin\AiAssistController` endpoints + auth + rate limit | `app/Http/Controllers/Admin/AiAssistController.php`, `routes/web.php` | Feature test: admin → 200; non-admin → 403; throttled. |
| **T-504** | AI panel in GrapesJS editor (prompt, tone, insert/replace) | `resources/js/editor/grapesjs-init.js`, `resources/views/admin/campaigns/editor.blade.php` | Manual: generate copy + image, insert into editor. |
| **T-505** | `ai_usage` table + cost logging | `database/migrations/2026_04_29_000003_create_ai_usage_table.php`, `app/Models/AiUsage.php` | Each AI call writes a row. |

---

## Phase 6 — Automations

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-601** | Migrations: automations, automation_steps, automation_runs | `database/migrations/2026_04_29_000004_*.php` | Migrate clean. |
| **T-602** | Models + factories | `app/Models/{Automation,AutomationStep,AutomationRun}.php` | Tinker: relations work. |
| **T-603** | `AdvanceAutomationsJob` (every 5 min) | `app/Jobs/AdvanceAutomationsJob.php`, `routes/console.php` | Feature test: subscribe → step 1 fires same minute, step 2 fires after wait. |
| **T-604** | Trigger hooks (on_subscribe, on_tag_added) | `app/Services/SubscriberService.php` (event dispatch), `app/Listeners/AutomationTriggerListener.php` | Feature test: subscribe → automation_run row created. |
| **T-605** | Date-based + on_devotional_published triggers | `app/Console/Commands/CheckAutomationTriggers.php` | Feature test: stub today's devotional → trigger fires. |
| **T-606** | Seed: Welcome series, Daily Devotional, Re-engagement | `database/seeders/AutomationSeeder.php` | After seed, 3 active automations visible in admin. |
| **T-607** | A/B subject testing on campaigns | `app/Jobs/ResolveAbTestJob.php`, campaign editor UI | Feature test: variants sent, winner selected after 4h, remainder sent. |
| **T-608** | Admin automations UI (list + visual builder) | `app/Http/Controllers/Admin/AutomationController.php`, `resources/views/admin/automations/*` | Manual: create welcome series via UI, save, run. |

---

## Phase 7 — SMS (Twilio)

| ID | Title | Files | Verify |
|---|---|---|---|
| **T-701** | Migration: `sms_subscribers`, `sms_subscriber_tag`, `sms_messages` | `database/migrations/2026_04_29_000005_*.php` | Clean migrate. |
| **T-702** | Models + Twilio SDK wrapper service | `app/Models/SmsSubscriber.php`, `app/Services/Sms/TwilioService.php` | Unit test mocks Twilio client. |
| **T-703** | Public `/sms-signup` with explicit consent text | `app/Http/Controllers/SmsSubscribeController.php`, `resources/views/sms/signup.blade.php`, `routes/web.php` | Feature test: phone normalized to E.164 + opt-in event. |
| **T-704** | Twilio inbound webhook (STOP / START / HELP) | `app/Http/Controllers/SmsWebhookController.php`, `routes/web.php` | Feature test: STOP → status flips. |
| **T-705** | Admin SMS campaign CRUD + dispatch job | `app/Http/Controllers/Admin/SmsController.php`, `app/Jobs/DispatchSmsBatchJob.php` | Feature test with mocked Twilio: rows flip sent. |
| **T-706** | A2P 10DLC docs in `EMAIL_SETUP.md` | `EMAIL_SETUP.md` | Section present. |

---

## Cross-cutting / cleanup

| ID | Title | Files | Verify |
|---|---|---|---|
| **X-1** | Replace Tailwind CDN with Vite build | `resources/views/layouts/app.blade.php`, `vite.config.js`, `resources/css/app.css` | `npm run build`; layout no longer references `cdn.tailwindcss.com`. |
| **X-2** | Add `App\Mail\backup-script` for `mysqldump` weekly cron | `app/Console/Commands/BackupDatabase.php`, `routes/console.php` | Manual: `php artisan backup:db` produces gzipped dump in `storage/backups`. |
| **X-3** | GDPR right-to-be-forgotten endpoint | `app/Http/Controllers/UnsubscribeController.php` (`forget` action) | Feature test: deletes subscriber + events; suppression added to prevent re-import. |
| **X-4** | Admin "Compliance" page (consent ledger viewer + export) | `app/Http/Controllers/Admin/ComplianceController.php`, `resources/views/admin/compliance/*` | Manual: view per-subscriber timeline. |

---

## Definition of Done (per task)

A task is **DONE** when:
1. All files in the **Files** column exist with the specified responsibility.
2. The **Verify** check passes on a fresh `php artisan migrate:fresh && composer test` run.
3. No new PHP errors in `storage/logs/laravel.log`.
4. Code passes `vendor/bin/pint --test` (PSR-12).
5. Commit message follows `[ralph] T-### <title>` format with verify output in body.
6. Markdown progress checked off in this file.

## Definition of Done (per phase)

A phase is **DONE** when:
1. All tasks in the phase pass Verify.
2. End-to-end smoke test of the phase passes manually.
3. PRD success metrics for that phase trend in the right direction.

---

## Currently complete

- ✅ **T-101** — Phase 1 core migration created
- ✅ **T-103** — Subscriber, Tag, SubscriptionEvent, Suppression models created

**Next up for the loop:** **T-001** (composer packages) and **P-1**, **P-3**, **P-4** human pre-flight items.
