whisper-money/app/Services
Víctor Falcón 9312d24f93
fix(balances): propagate retroactive transaction changes to later balances (#682)
## Problem

When a transaction is created (or deleted) with the **update balance**
option on
a **past date**, only that day's balance snapshot was adjusted. Balances
are
stored as sparse per-date snapshots read with carry-forward semantics
(`BalanceLookup::getBalanceAt` returns the most recent snapshot
on/before a
date), so later snapshots — most importantly *today's current balance* —
kept
their stale value.

Reproduction: yesterday = 10, today = 10. Add a −5 expense dated
yesterday with
"update balance" on. Before this fix, yesterday became 5 but today
stayed 10.
Expected: both become 5.

## Fix

`ManualBalanceAdjuster` now shifts the transaction's own day **and every
later
snapshot** by the same delta, on both create and delete:

- `applyCreatedTransaction` seeds a snapshot on the transaction's date
from the
carried-forward balance (when none exists yet), then increments that
date and
  all later snapshots by `+amount`.
- `reverseDeletedTransaction` now applies the exact inverse from the
transaction's own date forward (previously it only ever adjusted
*today*,
which left the past day stale and would corrupt a create→delete
round-trip of
  a back-dated transaction).

The shared forward-shift is extracted into `shiftBalancesFrom(delta)`,
used by
both paths.

## Also in this PR (small UI fix)

The balance history modal (`BalancesModal`) rendered each row's
date/balance
text top-aligned while the action icons were centered, because the base
`TableCell` defaults to `align-top`. Center the data-row cells so every
column
lines up. Scoped to this modal — the shared `TableCell` is untouched.

## Notes / intended semantics

- **Later user-set balances shift too.** Snapshots are treated as points
on one
running balance, so backfilling/removing a past transaction moves
today's
balance as well. That is exactly the reported expectation ("today should
also
change"). We deliberately do **not** try to distinguish "anchor"
snapshots
(a value the user reconciled by hand) from transaction-derived ones —
there is
no such distinction in the schema, and adding one is a separate product
  decision.
- **Pre-existing, out of scope:** editing a transaction's amount/date
never
  adjusts balances (`amount`/`transaction_date` aren't editable via
`UpdateTransactionRequest`), and the create vs. delete "update balance"
toggles are independent — creating with the box off but deleting with it
on
  can over-correct. Neither is introduced here.

## Tests

- New: creating a past-dated transaction updates that date **and** every
later
  balance (the exact reported bug).
- Rewrote the delete test to assert the reverse propagates from the
transaction's date forward (the old test encoded the now-removed "always
write
  today" behavior).
- Full `TransactionTest` suite green (49 passed).

## QA

Verified end-to-end in the running app on a manual account (yesterday =
10.00,
today = 10.00): created a −5.00 expense dated yesterday with "update
balance"
on. The balance history modal shows **both** records — yesterday and
today —
drop from 10.00 to 5.00, and the DB confirms both snapshots = 500.

## Demo

https://github.com/user-attachments/assets/18aa043d-37aa-4073-990c-a269c982a818
2026-07-16 08:38:02 +02:00
..
Ai fix(ai): don't report expected transient provider overloads (PHP-LARAVEL-44) (#655) 2026-07-07 10:55:02 +00:00
Banking fix(banking): treat EnableBanking upstream 5xx as transient, not reportable (#678) 2026-07-15 06:40:55 +00:00
Demo Remove encryption from browser tests and demo user (#129) 2026-02-17 11:45:27 +01:00
Discord feat: add Discord admin feed for daily stats and Stripe events (#458) 2026-05-30 18:14:46 +02:00
Stats fix(stats): correct the trial/pricing experiment funnel report (#679) 2026-07-15 09:17:32 +02:00
Stripe feat(discord): enrich Stripe event messages and dedupe deliveries (#460) 2026-05-31 12:50:37 +02:00
Subscriptions feat(subscriptions): trial/pricing A/B/C experiment (#600) 2026-06-27 18:00:15 +02:00
AccountMetricsService.php feat(dashboard): add accounts manager dialog with visibility toggle and reorder (#604) 2026-06-27 16:11:25 +00:00
AccountUserCurrencyService.php fix(open-banking): stop storing the XXX no-currency placeholder on accounts (#602) 2026-06-27 16:01:21 +00:00
AuthEntryPointService.php fix: route new PWA guests to signup (#313) 2026-04-21 10:53:05 +01:00
AutomationRuleService.php refactor: consolidate duplicated financial calculations (#643) 2026-07-04 22:26:44 +02:00
BalanceLookup.php fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183) 2026-03-02 12:22:30 +00:00
BudgetPeriodService.php fix(budgets): make period generation idempotent (#533) 2026-06-15 12:44:44 +02:00
BudgetTransactionService.php feat: add catch-all budgets (#527) 2026-06-15 16:07:19 +00:00
CashflowSummaryService.php refactor: consolidate duplicated financial calculations (#643) 2026-07-04 22:26:44 +02:00
CategorySpendingService.php refactor: consolidate duplicated financial calculations (#643) 2026-07-04 22:26:44 +02:00
CategoryTree.php feat(analysis): break down spending by sub-category (#508) 2026-06-08 14:53:36 +02:00
CurrencyConversionService.php fix(currency): make rate fetching resilient to slow CDN (#502) 2026-06-08 09:10:38 +02:00
CurrencyOptions.php feat(settings): centralize currency options and split profile/account support (#256) 2026-04-02 19:23:10 +02:00
ExchangeRateService.php Fix PHP-LARAVEL-1V exchange rate cache race (#383) 2026-05-12 12:45:45 +02:00
LandingAuthOverrideService.php feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333) 2026-04-30 15:10:28 +01:00
LeadCohortResolver.php feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333) 2026-04-30 15:10:28 +01:00
LeadPromoCodeAllocator.php feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333) 2026-04-30 15:10:28 +01:00
LoanAmortizationService.php fix(dashboard): avoid month overflow in real estate projection (#340) 2026-04-30 13:53:01 +00:00
LoanBalanceGeneratorService.php fix(balances): stop historical-balance generator OOM on ancient purchase/loan dates (PHP-LARAVEL-49) (#661) 2026-07-08 21:34:43 +00:00
ManualBalanceAdjuster.php fix(balances): propagate retroactive transaction changes to later balances (#682) 2026-07-16 08:38:02 +02:00
PeriodComparator.php feat(cashflow): add savings and period views (#424) 2026-05-25 16:41:00 +02:00
RealEstateBalanceGeneratorService.php fix(balances): stop historical-balance generator OOM on ancient purchase/loan dates (PHP-LARAVEL-49) (#661) 2026-07-08 21:34:43 +00:00
ResendService.php feat: sync user leads to resend (#283) 2026-04-13 19:56:08 +01:00