whisper-money/app/Http/Requests/Settings
Víctor Falcón 0351dbfb38
feat(budgets): add per-budget email notifications (#731)
## What

Adds opt-in **email notifications per budget** for three events, plus a
dedicated **Notifications** settings page.

Each budget can independently enable:
- **New transaction** — a transaction was assigned to the budget
- **Close to limit** — spending crossed 90% of the limit
- **Over limit** — spending reached/passed the limit

A **Default (new budgets)** row lets you set the toggles that newly
created budgets inherit.

The existing *bank-transactions-synced* email toggle was **moved** from
the account page into this new Notifications page, so all email
preferences live in one place.

## UI

`Settings → Notifications`: one "Email notifications" section containing
the bank-sync toggle and a "Budgets" table (rows = Default + each
budget, columns = the three events).

<img width="1265" height="638" alt="budget-notifications-settings"
src="https://github.com/user-attachments/assets/b8386410-98dd-4a5e-9dee-6e5bf7effa31"
/>

## How it works

- Notifications fire from the **live transaction-assignment path**
(`BudgetTransactionService::assignTransaction`, already a queued
listener), never from historical backfill when a budget is created.
- Only the budget's **current period** is considered (the emails
describe the live "available before the limit" state).
- **New transaction** fires for a genuinely newly-assigned transaction;
the email includes the transaction.
- **Close/over** use two atomic per-period flags
(`close_to_limit_notified` / `over_limit_notified`) flipped with a
compare-and-set `UPDATE`, so exactly one email is sent per crossing even
under concurrent queue workers. The flags reset once spending drops back
below the close threshold, so a later crossing can notify again.
- Budgets with no limit (`allocated_amount <= 0`) never send close/over
emails; the "new transaction" email for such a budget omits the
limit/available rows.
- Every email shows the budget's current status (period, spent, limit,
available or over-by). Subcopy links back to the Notifications page.
- New budgets inherit the user's default toggles at creation time.

## Data model

- `budgets`: `notify_on_new_transaction`, `notify_on_close_to_limit`,
`notify_on_over_limit` (default `false`)
- `user_settings`: `budget_notify_on_*` defaults for new budgets
(default `false`)
- `budget_periods`: `close_to_limit_notified`, `over_limit_notified`
(dedup flags)

## Deliberate decisions

- **Threshold is a fixed 90%** for "close to limit" (not configurable) —
kept simple; easy to make per-budget later if requested.
- **"New transaction" sends one email per assigned transaction.** It is
opt-in and defaults to off. A bank sync importing many matching
transactions into an opted-in budget will therefore send several emails;
if this proves noisy we can batch per sync run (mirroring the daily
bank-sync digest). Flagged here rather than pre-building batching.
- **Limit basis is `allocated_amount`** (matches the budget
cards/spending chart), carry-over excluded.

## Tests

- Preference endpoints: page renders budgets + defaults, per-budget
toggle update, cross-user update forbidden, user-settings defaults
update, new budget inherits defaults.
- Sending: new-transaction opt-in/out, over-limit send, close-limit at
90%, no-resend dedup, re-notify after dropping below and crossing again,
no-limit budget skips close/over, historical backfill sends nothing,
notify after enabling the preference while already over. Plus a render
test covering all three email variants.

## Notes

- CI-enforced `es.json` keys for the new page and emails are included.
- All new PHP follows existing mail/queue conventions (`ShouldQueue`
mailable, `emails` queue, `RateLimited` middleware).
2026-07-24 12:52:03 +02:00
..
Concerns feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
ApplyAutomationRuleRequest.php Apply automation rules to existing transactions (#413) 2026-05-22 08:36:18 +02:00
DeleteCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
ProfileUpdateRequest.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
StoreAccountRequest.php refactor(requests): share account detail validation rules (#481) 2026-06-03 19:01:03 +02:00
StoreAutomationRuleRequest.php refactor(requests): extract user-owned exists rules to trait (#477) 2026-06-03 17:33:54 +02:00
StoreBankRequest.php Accounts: Allow users to create custom banks with his own logo, and name (#12) 2025-12-04 15:24:04 +01:00
StoreCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
StoreLabelRequest.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
StoreMcpTokenRequest.php feat(mcp): add write tools (Phase 2) (#690) 2026-07-17 15:25:03 +00:00
TwoFactorAuthenticationRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateAccountRequest.php refactor(requests): share account detail validation rules (#481) 2026-06-03 19:01:03 +02:00
UpdateAutomationRuleRequest.php refactor(requests): dedupe UpdateAutomationRuleRequest (#476) 2026-06-03 17:29:23 +02:00
UpdateBudgetNotificationPreferencesRequest.php feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00
UpdateCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
UpdateChartColorSchemeRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateLabelRequest.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
UpdateNetWorthChartLoanPreferenceRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateNetWorthChartRealEstatePreferenceRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateNotificationPreferencesRequest.php feat(settings): let users disable bank transactions email (#472) 2026-06-02 12:24:39 +02:00
UpdateTimezoneRequest.php fix(user): persist detected timezones (#296) 2026-04-16 11:36:57 +01:00