## What
The `encryption:delete-accounts` and `encryption:notify-removal`
commands target users who still hold legacy client-side encrypted data.
They should **never** warn or delete an account that is still being
billed.
## Changes
- Add `excludeBilledUsers()` to the shared
`FindsUsersWithLegacyEncryption` trait. It drops any user where
`hasActiveSubscriptionOrTrial()` is true — a valid subscription outside
its grace period, or an active generic trial. This reuses the existing
domain method (whose docblock already states such users must cancel
before deletion) instead of reimplementing Cashier's subscription-status
logic in SQL.
- Both commands apply the filter right after fetching their candidates.
- Eager-load `subscriptions` in the base query to avoid an N+1 when the
filter runs.
## Tests
- `it never deletes a subscribed user` (delete command)
- `it never warns a subscribed user` (notify command)
Both new tests plus the existing suite pass (8/8). Pint clean.
## What
Two artisan commands to retire the remaining legacy client-side
encrypted data in production (transactions with
`description_iv`/`notes_iv`, accounts with `name_iv`).
- **`encryption:notify-removal`** — queues a re-engagement/deletion
warning to every user who still has an encrypted transaction or account.
Reuses `SendUpdateEmailJob` (queue `emails`, dedup via `UserMailLog`,
50/day pacing). The email is framed as an **inactivity** cleanup (no
mention of encryption) and pitches recent additions (AI insights, bank
integrations, etc.).
- **`encryption:delete-accounts`** — soft-deletes and anonymizes
(`User::markAsDeleted()`) those users **only** if they have not signed
in within the grace window (`--days`, default 7, via `last_active_at`).
Anyone who came back is spared, and the demo account is always excluded.
Both support `--dry-run` and `--force`, and share the target query via
the `FindsUsersWithLegacyEncryption` concern so the warning and the
deletion always target the same set.
## Intended manual run
`notify-removal --dry-run` → run for real → wait 7 days →
`delete-accounts --dry-run` → run for real.
## Notes / scope
- Deletion is a **soft-delete + email anonymization** (accounts become
unusable); it does not hard-purge rows, and does not cancel Stripe
subscriptions or revoke bank connections (unlike `user:delete`). Legacy
inactive accounts are unlikely to have either; handle separately if
needed.
- Email copy is plain English (matches the existing `mail/updates/*`
convention), no new `__()` keys.
## Tests
`tests/Feature/Console/NotifyEncryptedDataRemovalCommandTest.php` and
`DeleteEncryptedDataAccountsCommandTest.php` — 6 passing (targeting,
queue, dry-run, grace window, demo exclusion).