Commit Graph

7 Commits

Author SHA1 Message Date
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
Víctor Falcón ce5692cb30
fix: split drip and default email senders (#263)
## 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`
2026-04-06 12:16:47 +02:00
Víctor Falcón 70b603e901
feat: Spanish localization (#74)
## Pending
- [x] Translate landing page
- [x] Dashboard
- [x] Accounts list page
- [x] Account page
- [x] Cashflow
- [x] Budgets
- [x] Transactions
- [x] Settings

## Screenshots
<img width="1210" height="969" alt="image"
src="https://github.com/user-attachments/assets/c7935e5c-488d-4941-8f19-8834e5668257"
/>
<img width="1211" height="972" alt="image"
src="https://github.com/user-attachments/assets/e94e1daf-233a-4a49-aa65-5678c772d178"
/>
2026-02-08 11:58:08 +01:00
Víctor Falcón 3cbe0a7879 fix(sync): make transaction creation idempotent (#38)
## 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
2025-12-30 07:22:19 +01:00
Víctor Falcón 2662c1f873 Add retry mechanism to email mailables and update supervisor config 2025-12-30 07:22:18 +01:00
Víctor Falcón 3d0d6c8bef feat(queue): Implement queueable email jobs with rate limiting 2025-12-30 07:22:18 +01:00
Víctor Falcón 46c5b13739 feat: Implement drip email campaign system (#35)
## 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
2025-12-30 07:22:18 +01:00