whisper-money/tests/Feature
Víctor Falcón ab3902af39
fix(budgets): keep labeled expenses out of the catch-all budget (#781)
## Problem

A catch-all budget ("Not budgeted") is supposed to absorb every expense
no other
budget covers. It decided that by looking at the **categories** other
budgets
track — labels were never considered. So for a user whose other budgets
track
spending **by label**, nothing was ever "claimed" and the catch-all
absorbed
everything, double counting it.

Found in production: a user with three label-only budgets (Padel, Yearly
Padel,
Miami Flight) had every labeled expense sitting in their catch-all as
well. Their
current catch-all period read **289,136 / 170,000 (170%, over budget)**
where the
right figure is **75,281 / 170,000 (44%)**. 185 assignment rows are
wrong across
2 users.

## Fix

Precedence is now decided by the budget periods that actually match the
transaction: if any budget already counts it — by category **or** by
label, in a
period covering its date — the catch-all stays out. The historical
backfill
mirrors that rule in SQL, and only treats a category or label as claimed
when the
claiming budget has a period overlapping the range being backfilled.

That last part matters: keying purely on "some budget tracks this label"
would
have dropped expenses whose label budget has no period covering their
date,
leaving them in **no** budget at all (23 rows of one production user,
~2,204 of
spend that would have silently disappeared from their budget view). Both
review
passes flagged it; there are now tests for it on both paths.

## Repairing existing data

```bash
php artisan budgets:reassign-labeled --user=<email> --dry-run
php artisan budgets:reassign-labeled --user=<email>
```

It re-derives every budget assignment of the labeled transactions
currently
sitting in a catch-all budget, with notifications suppressed — these are
historical rows, so a limit email would announce a threshold crossed
weeks ago.
Reassignment (rather than deleting the bad rows) is deliberate: 44 of
the
affected transactions are not in their label budget either, so a plain
delete
would have left them nowhere.

## Known follow-ups (not in this PR)

- **The stale state can reappear.** Catch-all membership now depends on
labels,
but three paths mutate labels without firing `TransactionUpdated`, so
nothing
  reassigns: `AutomationRuleService::applyActions` (`saveQuietly` +
  `syncWithoutDetaching`), its bulk `applyRuleActionsToTransactions`
(`LabelTransaction::insertOrIgnore`), and the `LabelTransaction` MCP
tool. This
is what left the 44 Miami rows out of their budget — the label was
attached ~10
  minutes after the transaction's last save. The web paths are fine.
- Creating a label budget next to an existing catch-all does not release
the
  catch-all's rows, and deleting one does not hand them back.
- The repaired catch-all periods keep their `over_limit_notified` flag
until the
next expense lands in them (the flag reset lives in the notification
path we
  skip). Self-heals on the next assignment.

## Testing

`tests/Feature/CatchAllBudgetTest.php` — 11 tests: label claimed by
another
budget, label no budget tracks (with a *different* label claimed, so
"any claim"
is not enough), claiming budget with no covering period on both the
per-transaction and historical paths, and the repair command end to end
including `Mail::assertNothingSent()`.
2026-08-11 15:45:19 +00:00
..
Ai feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
Api
…
Auth
…
Console fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756) 2026-08-10 12:48:13 +00:00
Events
…
Jobs
…
Listeners
…
Mcp feat(mcp): budget tools — read, create, edit and delete (#779) 2026-08-11 14:28:53 +00:00
Onboarding fix(onboarding): don't trap users on the syncing step when a bank sync fails (#745) 2026-08-09 18:40:49 +02:00
OpenBanking fix(binance): value past days from Binance's own snapshot total (#763) 2026-08-11 11:04:06 +00:00
Services
…
Settings
…
Spaces
…
Sync
…
AccountBalanceControllerTest.php
…
AccountControllerTest.php feat(accounts): count shared accounts at the owner's percentage (#750) 2026-08-11 13:26:54 +00:00
AccountImportConfigTest.php
…
AccountUserCurrencyServiceTest.php
…
AiConsentSettingsTest.php
…
AiConsentTest.php
…
AlignAccountsEncryptedFlagMigrationTest.php
…
ApplyRealEstateRevaluationTest.php
…
AuthenticatedLayoutSafeAreaTest.php
…
AutomationRuleApplicationTest.php
…
AutomationRuleEvaluationTest.php
…
AutomationRuleTest.php
…
BackfillAccountIbansCommandTest.php
…
BackfillXxxAccountCurrenciesTest.php
…
BalanceLookupTest.php
…
BudgetHistoricalAssignmentTest.php
…
BudgetNotificationTest.php
…
BudgetPeriodDateTest.php
…
BudgetPeriodServiceTest.php
…
BudgetTest.php feat(mcp): budget tools — read, create, edit and delete (#779) 2026-08-11 14:28:53 +00:00
BudgetTransactionServiceTest.php
…
BulkUpdateTransactionsTest.php refactor(transactions): build the bulk selection once in bulkUpdate (#774) 2026-08-11 13:14:19 +00:00
CancelFreeEnableBankingConnectionsCommandTest.php
…
CashflowAnalyticsTest.php
…
CashflowPageTest.php
…
CatchAllBudgetTest.php fix(budgets): keep labeled expenses out of the catch-all budget (#781) 2026-08-11 15:45:19 +00:00
CategoryMonthlyBreakdownTest.php
…
CrapCommandTest.php ci: add duplication and complexity quality checks (#765) 2026-08-11 13:29:37 +02:00
CurrencyConversionServiceTest.php
…
DashboardAnalyticsTest.php
…
DashboardTest.php
…
DecryptTransactionsTest.php
…
DeleteUserCommandTest.php
…
DemoAccountRestrictionsTest.php fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756) 2026-08-10 12:48:13 +00:00
DisconnectBankingConnectionsCommandTest.php
…
DiscordReportEmbedLimitsTest.php feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
DiscordWebhookTest.php
…
EncryptionTest.php
…
ExampleTest.php
…
ExchangeRateServiceTest.php
…
GenerateStripePromotionCodesCommandTest.php chore(deps): update composer dependencies to latest (#764) 2026-08-11 11:15:27 +00:00
IdorVulnerabilityTest.php
…
ImportDataTest.php
…
InertiaSharedDataTest.php
…
IntegrationRequestTest.php
…
LabelTest.php
…
LoanTest.php refactor(accounts): split store/update in the account controller (#776) 2026-08-11 13:58:47 +00:00
LocalizationTest.php feat(currencies): add the Danish Krone (DKK) (#754) 2026-08-10 12:08:56 +02:00
LoggingConfigTest.php
…
MailSenderTest.php
…
NewTransactionsMarkerTest.php
…
OpenAiAppsChallengeTest.php feat(mcp): serve the ChatGPT app directory domain challenge (#749) 2026-08-10 07:46:49 +00:00
PersistUpsellSourceFromStripeTest.php
…
PlaintextTransactionsTest.php
…
PlanFeatureTest.php
…
PopoverSafeAreaTest.php
…
PostStripeEventToDiscordTest.php
…
PurgeResidualEncryptionArtifactsJobTest.php
…
PwaTest.php
…
QueueConfigTest.php
…
ReEvaluateTransactionRulesTest.php
…
RealEstateAvailabilityTest.php
…
RealEstateTest.php
…
ResendSyncCommandTest.php
…
RoadmapTest.php feat(roadmap): add a public roadmap page mirrored from UserJot (#778) 2026-08-11 14:14:59 +00:00
RouteNotificationForMailTest.php
…
RuleEngineParityTest.php
…
SavedFilterTest.php
…
SelfServeRefundTest.php fix(demo): stop demo:reset from colliding on the fake Stripe subscription id (#756) 2026-08-10 12:48:13 +00:00
SendAiCohortReportCommandTest.php feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
SendAiConsentFollowUpEmailsCommandTest.php
…
SendDailyStatsReportCommandTest.php feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
SendExperimentFunnelReportCommandTest.php feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
SendStuckCohortReportCommandTest.php
…
SendSubscriptionFunnelReportCommandTest.php feat(stats): post the Discord stats reports in Spanish, opened by an AI summary (#752) 2026-08-10 10:13:40 +02:00
SentryConfigTest.php
…
SentryUserMiddlewareTest.php
…
SetLocaleTest.php
…
SharedAccountOwnershipTest.php feat(accounts): count shared accounts at the owner's percentage (#750) 2026-08-11 13:26:54 +00:00
SitemapTest.php
…
StrayHttpRequestGuardTest.php
…
StripeSubscriptionStatsCommandTest.php
…
SubscriptionExperimentTest.php chore(deps): update composer dependencies to latest (#764) 2026-08-11 11:15:27 +00:00
SubscriptionTest.php
…
SuggestionPersistenceTest.php
…
SyncBankingConnectionsCommandTest.php
…
SyncStripePricesCommandTest.php
…
TrackLastActiveAtTest.php
…
TransactionAnalysisTest.php
…
TransactionFilterTest.php
…
TransactionSideClassificationTest.php
…
TransactionTest.php
…
WelcomeBanksOrderingTest.php
…