whisper-money/app/Http/Requests
Víctor Falcón 22043ced29
fix(budgets): remove Custom period type to fix duplicate-key crash (#355)
## Why merge this

Production bug
**[PHP-LARAVEL-1B](https://whisper-money.sentry.io/issues/PHP-LARAVEL-1B)**
— `/budgets/{budget}` throws a 500 (`UniqueConstraintViolationException`
on `budget_periods_budget_id_start_date_unique`) for the affected user.
Page is unusable for them until fixed.

## Root cause

The Custom period type let users create budgets with arbitrary
`period_duration`. Combined with `calculatePeriodDates()`'s day-of-month
snap (`startDate->day(period_start_day)`), short durations produced
periods where `end_date == start_date == period_start_day`. On the next
visit after that day, regeneration computed a new period whose start
snapped backward to the same day → unique key collision.

Concrete trace from the Sentry event:

- Budget: `Seguro de Salud`, `period_type=custom, period_duration=1,
period_start_day=1`, created `2026-05-05`.
- Initial period created at budget creation: `start=2026-05-01,
end=2026-05-01` (already 4 days stale because Custom snapped
`now()=05-05` back to day 1, then `addDays(1).subDay()` produced same
date).
- Today `05-05` → `getCurrentPeriod()` returns null → `generatePeriod()`
snaps back to `05-01` again → `INSERT (budget_id, 05-01)` → 1062.

Verified in prod DB: this is the **only** budget in the entire database
with `period_type='custom'` and the only period row with `end_date <=
start_date`.

## Fix

Custom is the only period type that exposes this snap-back collision
(Monthly/Weekly/Biweekly all use a self-consistent advance). It also has
no defensible UX — every legitimate use case is covered by
Monthly/Weekly/Biweekly. So we remove it end-to-end:

- `BudgetPeriodType::Custom` enum case dropped.
- `BudgetPeriodService`: Custom branches removed from
`calculatePeriodDates()` and `calculateNextPeriodStartDate()`.
- `Budget` model: `period_duration` removed from `$fillable` and
`casts()`.
- `StoreBudgetRequest` / `UpdateBudgetRequest`: `period_duration` rule
removed.
- `BudgetController`: `period_duration` no longer passed in
store/update.
- Create + edit budget dialogs: Custom option and `period_duration`
input removed.
- `ResetDemoAccountCommand`: Custom switch case removed.
- `BudgetFactory`: `custom()` state and `period_duration` default
removed.
- `types/budget.ts`: `'custom'` removed from `BUDGET_PERIOD_TYPES` and
`Budget.period_duration` field dropped.
- Migration `2026_05_05_132023_convert_custom_budgets_to_monthly`:
rewrites any existing `custom` rows to `monthly` with
`period_duration=null, period_start_day=1` so the dropped enum case
can't crash on hydration.

Period generation logic for Monthly/Weekly/Biweekly is **unchanged**.

## Manual prod cleanup (separate, after merge)

The single buggy budget in prod will be deleted manually:

```sql
DELETE FROM budget_periods WHERE id = '019df7fd-6073-731b-9c08-f3723515292b';
DELETE FROM budgets WHERE id = '019df7fd-6071-7219-b190-ece22ccdb63f';
```

## Tests

`tests/Feature/BudgetPeriodServiceTest.php` covers Monthly + Weekly
advance and Monthly first-period generation. Existing
`BudgetPeriodDateTest` unaffected.

## Risk

- `period_duration` column kept in DB (nullable) to avoid data loss; no
migration needed.
- Migration ensures any environment with `period_type='custom'` rows is
converted before the enum case is removed, preventing hydration errors.
- No customers actively rely on Custom: prod query confirmed only the
single broken budget uses it, and that one is being deleted.

Fixes PHP-LARAVEL-1B
2026-05-05 16:07:16 +02:00
..
Api feat: Decrypt encrypted transactions on key unlock (#123) 2026-02-16 10:37:43 +01:00
OpenBanking feat(open-banking): remove feature flag gating (#297) 2026-04-17 10:20:05 +02:00
Settings refactor(real-estate): remove Pennant gating (#308) 2026-04-20 13:31:49 +01:00
BulkReEvaluateRulesRequest.php feat(transactions): re-add select all matching filters to bulk actions bar (#169) 2026-03-01 10:27:43 +00:00
BulkUpdateTransactionsRequest.php fix: Check IDOR vulnerabilities (#60) 2026-01-17 09:58:52 +01:00
IndexTransactionRequest.php Move transaction filtering from client-side to server-side (#126) 2026-02-16 12:49:04 +01:00
SetupEncryptionRequest.php E2E Encryption 2025-11-07 14:21:25 +00:00
StoreAccountBalanceRequest.php feat: investment benefits — show gains/losses on investment accounts (#140) 2026-02-23 13:59:10 +01:00
StoreBudgetRequest.php fix(budgets): remove Custom period type to fix duplicate-key crash (#355) 2026-05-05 16:07:16 +02:00
StoreRealEstateDetailRequest.php feat(accounts): add real estate asset tracking (#241) 2026-03-24 10:21:32 +00:00
StoreTransactionRequest.php Remove plaintext-transactions feature flag & E2E references (#116) 2026-02-13 11:10:21 +01:00
StoreUserLeadRequest.php feat(waitlist): waiting list with referral system (#199) 2026-03-04 12:36:47 +01:00
UpdateBudgetRequest.php fix(budgets): remove Custom period type to fix duplicate-key crash (#355) 2026-05-05 16:07:16 +02:00
UpdateCurrentAccountBalanceRequest.php feat: investment benefits — show gains/losses on investment accounts (#140) 2026-02-23 13:59:10 +01:00
UpdateLoanDetailRequest.php feat(accounts): add loan amortization projections for loan accounts (#246) 2026-03-26 15:06:09 +01:00
UpdateRealEstateDetailRequest.php feat(accounts): add market value and annual revaluation to real estate accounts (#245) 2026-03-26 11:02:20 +01:00
UpdateTransactionRequest.php Remove plaintext-transactions feature flag & E2E references (#116) 2026-02-13 11:10:21 +01:00