whisper-money/app/Mail
Víctor Falcón eccfaa5a7a
refactor(drip): extract base mailable and job for the drip email family (#641)
## Why

The drip email family was near copy-paste. Each of the eight mailables
repeated the same constructor, `$tries`/`$backoff`, `drip_from`
envelope, `markdown` + `userName` content, and `RateLimited` middleware
— differing only in subject and template (plus a reply-to on one, an
extra view var on another). Each of the eight jobs repeated the
`canReceiveEmails` / `hasReceivedEmail` guards, the `Mail::send` call,
and the `UserMailLog::create` block — differing only in the email type,
the mailable, and a handful of per-email eligibility checks.

## What

Two abstract bases, so each subclass declares only what varies:

- **`DripMail`** — owns the shared mailable shape. Subclasses declare
`dripSubject()` and `template()`; optional `contentData()` (PromoCode's
`FOUNDER` var) and `repliesToSender()` (PaywallFollowUp) hooks. The
hooks are named `dripSubject()`/`template()` deliberately to avoid
colliding with `Illuminate\Mail\Mailable`'s existing public
`subject()`/`markdown()` methods.
- **`SendDripEmailJob`** — owns the shared `handle()` flow (shared
guards → send → `UserMailLog`). Subclasses declare `emailType()` and
`buildMail()`; an optional `shouldSend()` hook carries the per-email
eligibility rules (pro-plan, onboarding, transactions, bank connections,
AI consent).

Each mailable drops from ~68 to ~13 lines and each job from ~48 to ~18.
Net **−459 lines**, with the send/log logic and envelope construction
now in one place.

## Behavior

No functional change. Subjects, `from`/`reply-to`, queue name, rate
limiting, templates, view data, and every per-job eligibility guard are
preserved. Notably, passing `replyTo: []` for the seven non-reply mails
is identical to the previous omission — `Envelope`'s `replyTo` defaults
to `[]`. Verified: `pest --exclude-testsuite=Browser` **1872 passing, 0
failing**; `pint --test`, `phpstan` (level 5), `format`, `lint` clean.

## Review-driven follow-up commits

Two review agents (architecture/duplication/coverage, and
behavior/regressions) ran against the first commit. The behavior agent
verified all eight jobs' guard logic invert correctly (order +
negations) and that `replyTo: []` ≡ omitted — no regressions. The
coverage agent found the two hook-driven behaviors were untested;
applied, each as its own commit:

- Extended the drip-sender parity test to all eight mailables and
asserted `PaywallFollowUpEmail`'s reply-to (and that a default drip mail
sets none).
- Asserted `PromoCodeEmail` injects the `FOUNDER` promo code into its
view data.

## Deliberately out of scope

- Non-drip mailables (`Waitlist*`, `Banking*`, `UpdateEmail`, …) share
the same `RateLimited('emails')->releaseAfter(1)` middleware and could
later share a queued-mail base too — separate concern.
- **Pre-existing** (not introduced here, noted by review): the job sends
the mail before writing `UserMailLog`, so a failure between the two can
re-enqueue a duplicate; there is no lock/unique constraint on `(user_id,
email_type)`. Worth a separate hardening pass.
2026-07-04 20:51:38 +02:00
..
Drip refactor(drip): extract base mailable and job for the drip email family (#641) 2026-07-04 20:51:38 +02:00
BankTransactionsSyncedEmail.php fix: make transaction sync email use default sender (#265) 2026-04-07 13:50:42 +01:00
BankingConnectionAuthFailedEmail.php fix: split drip and default email senders (#263) 2026-04-06 12:16:47 +02:00
BankingConnectionExpiredEmail.php Notify users about expired bank connections (#404) 2026-05-20 09:20:13 +02:00
BrokenBankLogosReportEmail.php fix: split drip and default email senders (#263) 2026-04-06 12:16:47 +02:00
EnableBankingConnectionsCancelledEmail.php Cancel Enable Banking connections for free users (#289) 2026-04-15 16:23:03 +02:00
UpdateEmail.php fix: split drip and default email senders (#263) 2026-04-06 12:16:47 +02:00
UserLeadInvitation.php feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333) 2026-04-30 15:10:28 +01:00
UserLeadReInvitation.php feat(leads): add user lead re-invite campaign (#432) 2026-05-26 08:35:31 +02:00
WaitlistOvertaken.php feat: selective retry of failed lead email jobs (#286) 2026-04-15 08:00:29 +01:00
WaitlistReferralNotification.php feat: selective retry of failed lead email jobs (#286) 2026-04-15 08:00:29 +01:00
WaitlistWelcome.php feat: selective retry of failed lead email jobs (#286) 2026-04-15 08:00:29 +01:00