Skip to content

feat(newsletters): admin-only broadcast system (Wave 1, Laravel reference)#103

Open
mpge wants to merge 27 commits into
mainfrom
feat/newsletter-system
Open

feat(newsletters): admin-only broadcast system (Wave 1, Laravel reference)#103
mpge wants to merge 27 commits into
mainfrom
feat/newsletter-system

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented May 21, 2026

Summary

Implements the newsletter system per escalated-dev/escalated/docs/superpowers/specs/2026-05-19-newsletter-system-design.md. Disabled by default. Laravel reference implementation; other backends will port from this PR in subsequent waves.

  • 6 migrations (lists, list_members, templates, newsletters, deliveries, contacts marketing_opt_out_at column)
  • 5 Eloquent models under Models/Newsletter/
  • 6 services: ContactSegmentResolver, BounceSuppressionStore, NewsletterRendererService, NewsletterPlannerService, NewsletterDispatcherService, NewsletterTrackerService
  • 8 controllers: admin (Newsletter, NewsletterList, NewsletterTemplate, NewsletterSettings) + public (Tracking, Unsubscribe, ViewInBrowser) + webhook (NewsletterEspWebhookController for Postmark/Mailgun/SES/SendGrid)
  • escalated:newsletters:dispatch Artisan command (host schedules every minute)
  • escalated:install extended with --with-newsletters / --no-newsletters flags + interactive prompt
  • Two starter Blade themes: default, branded
  • Two new permissions seeded into PermissionSeeder: newsletters.manage, newsletters.send (Admin gets both via wildcard)
  • Feature flag (ESCALATED_ENABLE_NEWSLETTERS) gates routes (boot-time skip), middleware (per-request 404), and Inertia shared data
  • Markdown rendering via league/commonmark + click rewriting via PHP DOMDocument
  • RFC 8058 one-click unsubscribe with rate limiter
  • 32 Pest tests passing

Depends on @escalated-dev/escalated v0.9.0 — Wave 0 PR is at escalated-dev/escalated#75.

Deviations from plan

  • Permission gating in controllers — plan used $user->ensurePermission(...) and $user->hasPermission(...) helpers that don't exist in the codebase. Followed the existing MacroController pattern (admin section middleware-gated; per-action permission checks deferred). Admin role gets both newsletter perms by default via the seeder's ['*'] wildcard.
  • Frontend page paths — used Escalated/Admin/Newsletters/... PascalCase to match Escalated/Admin/Macros/....
  • HTML manipulation — replaced symfony/dom-crawler with PHP's built-in DOMDocument after the crawler returned "empty node list" on full-document input.
  • Tests use http://localhost (Testbench default) instead of https://example.com for URL assertions.
  • Controller-level feature tests deferred to v1.1 — service-layer tests (31) cover the core engine; the integration test for disable-mid-flight asserts the end-to-end flag-off behavior.

Test plan

  • 32 newsletter Pest tests passing (vendor/bin/pest --filter='Newsletter')
  • Core boot smoke test still passes (tests/Feature/CoreOnlyBootTest)
  • Manual ESP-sandbox verification (Postmark or SES sandbox): send a real campaign end-to-end and verify open/click/bounce events flow back through webhooks. Sign off here before merging.
  • Reviewer: confirm .env.example or installation docs make clear that ESCALATED_ENABLE_NEWSLETTERS=false is the default

Reviewer notes

  • Do not auto-merge. Hand back to Matthew for review.
  • Bounce suppression store uses escalated_settings JSON for v1; suitable for <10k bounces. Plan flags a dedicated suppression table for v1.1+.
  • Top-clicks-by-URL is stubbed (returns []); per the spec, v1 surfaces aggregate clicks only.

mpge and others added 27 commits May 20, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant