whisper-money/tests/Feature/Console
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
..
AgentDatabaseCommandTest.php feat(console): add agent:db command for querying local and prod DB (#522) 2026-06-12 18:35:14 +02:00
BackfillTransactionDescriptionsCommandTest.php fix(banking): strip the ISO 20022 remittance tag from transaction descriptions (#744) 2026-08-09 18:11:05 +02:00
CheckBankLogosCommandTest.php Add weekly bank logo audit command (#211) 2026-03-06 11:05:13 +00:00
DeleteAiConsentCommandTest.php feat(ai): add command to delete a user's AI consent by email (#729) 2026-07-23 08:35:28 +00:00
DeleteEncryptedDataAccountsCommandTest.php fix(encryption): never email or delete users who are still being billed (#656) 2026-07-07 16:04:27 +00:00
DeleteManualAccountDataCommandTest.php feat(commands): delete transactions and balances of a user's non-connected accounts (#728) 2026-07-23 08:26:11 +00:00
FeatureEnableCommandTest.php refactor(ai): remove AiConsentSettings feature flag (#619) 2026-07-01 09:47:55 +02:00
NotifyEncryptedDataRemovalCommandTest.php feat(encryption): report count of users still holding encrypted data (#687) 2026-07-17 09:29:33 +00:00
ResetDemoAccountCommandTest.php fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756) 2026-08-10 12:48:13 +00:00
ResetUserCategoriesCommandTest.php fix(categories): expose cashflow setting on create (#448) 2026-05-29 11:05:04 +02:00
SendPaywallFollowUpEmailsCommandTest.php feat(drip): email users stuck on the paywall a day after onboarding (#562) 2026-06-19 14:11:12 +00:00
SendSubscriptionCancelledEmailCommandTest.php Add subscription cancellation email with manual trigger command (#47) 2025-12-30 20:32:44 +01:00
SendUpdateEmailCommandTest.php Support soft-deleted users with reusable emails (#316) 2026-04-22 11:41:41 +01:00
SendUserEmailsReportCommandTest.php feat(reports): email a monthly CSV of active user emails to the owners (#783) 2026-08-12 11:29:28 +02:00
SetBankLogoCommandTest.php fix(banks:set-logo): add JPEG support test coverage and prompt for missing arguments (#214) 2026-03-07 16:33:54 +00:00
SetupMainUserCommandTest.php Command: Setup Main User 2025-11-15 22:16:41 +01:00