Commit Graph

7 Commits

Author SHA1 Message Date
Víctor Falcón 4ba78e54d8
feat(reports): email a monthly CSV of active user emails to the owners (#783)
## What

Once a month, email a CSV with the email address of every non-deleted
user to the owners.

- New `email:user-emails-report` command builds the CSV and sends
`UserEmailsReportEmail` with it attached as `text/csv`.
- Scheduled `monthlyOn(1, '09:05')` in `Europe/Madrid`, next to the
other `email:*` jobs.
- Recipients come from a comma-separated `REPORT_RECIPIENTS` env var.
The command fails loudly (exit 1, nothing sent) when it is unset, rather
than silently skipping.

The `SoftDeletes` global scope on `User` already excludes deleted users,
so no extra `whereNull` is needed.

## ⚠️ Required before this ships

Set `REPORT_RECIPIENTS` in the production environment, or the scheduled
command will fail every month:

```
REPORT_RECIPIENTS=first@example.com,second@example.com
```

Values are trimmed and empty entries dropped, so trailing commas and
spaces are safe.

## QA

No UI surface, so this was QA'd the way it is actually used: running the
command against the real local database (2520 users, 85 soft-deleted)
with mail captured by Mailhog.

| Check | Result |
| --- | --- |
| Command output | `Sent 2435 user email(s) as
user-emails-2026-08-12.csv.` (2520 − 85) |
| Message | 1 email, both recipients on a single `To` |
| Subject | `Monthly user emails export: 2435 users` |
| Attachment | one `text/csv` part,
`filename=user-emails-2026-08-12.csv`, 58 KB |
| CSV contents | `email` header + 2435 rows |
| Soft-deleted leakage | 0 overlap with the 85 soft-deleted addresses |
| Set equality | 0 rows in the CSV missing from the active set, 0 active
users missing from the CSV |
| Body | renders correctly in both the text and HTML parts |
| Schedule | `schedule:list` → `5 7 1 * *` (07:05 UTC = 09:05 CEST),
next due Sept 1 |
| Missing `REPORT_RECIPIENTS` | errors, exit 1, nothing sent |
| Recipient parsing | `" one@example.com , ,two@example.com,"` → two
clean recipients |

Tests: 2136 pass. `pint`, `phpstan`, `jscpd`, `crap`, `prettier` and
`eslint` all clean.

## Review notes

Two findings from review were raised rather than coded, since they are
product calls:

- **The export is not filtered by verification or consent.** It contains
every active address, including ~5.8% unverified ones. Literal "all
users", but those would bounce if the list is imported into a mail tool.
There is no marketing-consent flag anywhere in the schema, so nothing is
being ignored — just don't assume the list is filtered.
- **Privacy posture.** This puts the full user-email list into two
mailboxes every month, indefinitely, with no retention control. Worth a
conscious decision for an app positioned on not sharing user data.

`demo@whisper.money` is intentionally **not** excluded: the request was
every non-deleted user, and the existing exclusion precedent protects
the demo account from deletion, which is a different motive.

CSV formula injection (`=`, `+`, `-`, `@` local parts evaluating on
import into Sheets) was considered and skipped: zero such addresses
exist today and the only sensitive payload is the list itself, which the
recipients already own.
2026-08-12 11:29:28 +02:00
Víctor Falcón d8bb78e5e0
feat(mail): use AWS SES for email delivery (#422)
## Summary
- add AWS SDK required by Laravel SES mail transport
- default mail delivery to SES when MAIL_MAILER is unset
- document SES env vars and keep Resend marked as contact sync only
- add config coverage for SES mail setup

## Tests
- php artisan test --compact tests/Feature/MailConfigurationTest.php
2026-05-25 10:59:57 +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 2763846329
Add weekly bank logo audit command (#211)
## Summary
- add a new `banks:check-logos` console command that validates all
non-null bank logo URLs weekly
- set broken/invalid bank logos to `null` and send an admin report email
to `ADMIN_EMAIL` when updates occur
- add weekly scheduling, admin mail config wiring, and feature tests for
valid/broken/head-fallback flows

## Testing
- vendor/bin/pint --dirty
- php artisan test tests/Feature/Console/CheckBankLogosCommandTest.php

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 11:05:13 +00:00
Víctor Falcón 1b0f3ba24d fix: Disable email verification on dev/local 2026-02-07 17:52:53 +01:00
Víctor Falcón beecff404a
Add subscription cancellation email with manual trigger command (#47)
## Summary

- Add a subscription cancellation email that can be manually triggered
via artisan command
- Include CONTINUE50 coupon code (50% off current and future payments
for monthly/yearly subscriptions)
- Add option to disable all drip emails via `DRIP_EMAILS_ENABLED` env
var

## Usage

```bash
# Send to single user
php artisan email:subscription-cancelled user@example.com

# Send to multiple users
php artisan email:subscription-cancelled user1@example.com,user2@example.com

# Disable all drip emails
DRIP_EMAILS_ENABLED=false
```
2025-12-30 20:32:44 +01:00
Víctor Falcón fb140bbece Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00