The eight drip emails and their eight send jobs were near copy-paste. Every
mailable 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 and an extra view var on another). Every job
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.
Introduce two abstract bases:
- DripMail owns the shared mailable shape; subclasses declare dripSubject() and
template(), with optional contentData() (PromoCode's promo code) and
repliesToSender() (PaywallFollowUp) hooks.
- SendDripEmailJob owns the shared handle() flow; subclasses declare emailType()
and buildMail(), with an optional shouldSend() hook for the per-email
eligibility rules (pro-plan, onboarding, transactions, AI consent, ...).
Each mailable drops from ~68 to ~13 lines and each job from ~48 to ~18, with the
shared send/log logic now living in one place.
Behavior unchanged: subjects, from/reply-to, queue, rate limiting, templates,
view data and every eligibility guard are preserved. Full suite green
(1868 passing, 0 failing) and drip job/command/listener tests pass (71/71).
## Summary
- route drip mailables through `Álvaro and Víctor <hi@whisper.money>`
and send the rest from `Whisper Money <no-reply@whisper.money>`
- remove legacy per-mailable `Victor` sender overrides so non-drip mail
falls back to the default sender consistently
- add focused sender coverage for drip, non-drip, and verification mail
paths
## Testing
- `php artisan test --compact tests/Feature/MailSenderTest.php
tests/Feature/Jobs/Drip/SendWelcomeEmailJobTest.php`
## Summary
- When a transaction ID is provided during sync, check if it already
exists before creating
- If it exists, return the existing transaction with 200 status instead
of failing with duplicate key error
- Prevents duplicate transactions when sync retries occur due to network
issues
## Test plan
- [x] Attempt to create a transaction with a specific ID
- [x] Attempt to create another transaction with the same ID
- [x] Verify the second request returns the existing transaction instead
of an error
## Summary
- Implement event-driven drip email campaign system for new user signups
- Add 5 targeted emails based on user journey (welcome, onboarding
reminder, promo code, import help, feedback)
- Configure Laravel Horizon with Redis queue for reliable job processing
- Track sent emails in `user_mail_logs` table to prevent duplicates
## Email Schedule
| Email | Timing | Condition |
|-------|--------|-----------|
| Welcome | 30 min after signup | All users |
| Onboarding Reminder | 1 day after signup | Not onboarded |
| Promo Code (FOUNDER) | 1 day after signup | Onboarded + has
transactions + not subscribed |
| Import Help | 1 day after signup | Onboarded + no transactions + not
subscribed |
| Feedback | 5 days after signup | Not subscribed |
## Architecture
```
User Registers → ScheduleDripEmailsListener
├─> SendWelcomeEmailJob (30 min delay)
├─> SendOnboardingReminderEmailJob (1 day delay)
├─> SendPromoCodeEmailJob (1 day delay)
├─> SendImportHelpEmailJob (1 day delay)
└─> SendFeedbackEmailJob (5 days delay)
```
Each job checks conditions at execution time and either sends the email
+ logs it, or silently completes.
## Test plan
- [x] All 23 drip email tests pass
- [x] Migration creates `user_mail_logs` table
- [x] Jobs dispatch with correct delays
- [x] Emails only sent when conditions are met
- [x] Duplicate emails prevented via UserMailLog check
- [ ] Verify Horizon processes jobs in production