whisper-money/tests/Feature/OpenBanking
Víctor Falcón e6cb0459f6
fix(binance): value past days from Binance's own snapshot total (#763)
Last of the crypto-pricing family, after #759 and #761. The issues queue
has produced nothing new for four cycles, so this came out of reading
the code the logs pointed at.

## What was wrong

`syncHistoricalBalances` rebuilds past daily rows in `account_balances`
— the data behind the net-worth chart. It priced each holding by handing
its raw ticker to `CurrencyConversionService`:

```php
$converted = $this->currencyConverter->convert($asset, $targetCurrency, $quantity, $date);

if ($converted == 0.0) {
    $skippedAssets[$asset] = true;   // and the holding leaves the day's total
    continue;
}
```

That provider carries fiat currencies and a couple of majors. Everything
it did not recognise was dropped from the day's total, so an altcoin
portfolio produced a chart history far below what the user actually held
— while **today's** balance was correct, because the live path prices
through Binance's own tickers. The new test demonstrates it: a day
holding 10 SOL comes out as **0** on the old code.

I first went looking for the USD hop that fixed the Coinbase equivalent,
and there isn't one to add here — a past day needs a *dated* price, and
both the asset leg and any USD leg would come from the same provider
that does not know the asset.

## What it does instead

The snapshot already carried the answer and the service was throwing it
away. Binance sends `totalAssetOfBtc` — what it valued the whole spot
account at, on that day — in the same `data` object as the balances. One
BTC→fiat conversion, on the one ticker the rate provider always covers,
replaces the per-asset loop and covers every holding including the ones
nothing else can price.

Both are spot-only (`accountSnapshot?type=SPOT` and `api/v3/account`),
so historical and current days stay on the same basis — no discontinuity
at the join.

Second commit is a review follow-up: the day's balance now rests
entirely on that one field, so a response that stops carrying it gets
its own counter in the summary log rather than looking like a day with
no snapshot.

## ⚠️ Correction to the ops advice I gave in #759 and #761

I twice suggested `php artisan banking:sync --full` to repair the
understated Coinbase rows. **Do not run that unscoped.** With no filter
it forces `isFirstSync = true` for *every* active connection, which
would also rewrite up to 180 days of history for all 11 Binance accounts
in one shot as a side effect. `--connection=<id>` already exists — use
it:

```
php artisan banking:sync --full --connection=<coinbase-connection-id>
```

For Binance the same command is the repair, but it should be a
deliberate decision per connection, not collateral damage. **Nothing
repairs the existing 11 accounts automatically**: incremental sync only
fills the gap after `MAX(balance_date)` and never revisits older rows,
so their charts stay understated until someone asks for it.

## Deliberate behaviour changes, called out

- **An empty day now charts as zero.** Previously a snapshot with no
balances was skipped, leaving a gap that the frontend forward-fills — so
a day the account was actually empty showed the *previous* non-zero
value. It now writes 0. More correct, but it is a change beyond the
stated fix.
- **A day whose BTC value is positive but unconvertible is left alone**
rather than written as zero, so it disappears into the frontend's
forward-fill instead of reading as a portfolio that briefly vanished.

## Tests

`historical sync values a holding the rate provider cannot price` — a
day holding SOL with deliberately no `sol` rate available. **Verified to
fail on the old implementation: 0 instead of 100000.**

The three touched historical fixtures gained `totalAssetOfBtc` and keep
their original expected cents, recomputed on the new basis (2.0 BTC ÷
0.000019 = 10526316; 1.0 ÷ 0.000018 = 5555556; 0.02 ÷ 0.00002 = 100000).

`tests/Feature/OpenBanking` green at 341/341 locally.

## Not covered

Coinbase's historical path already prices via dated per-asset candles
with a USD route, so it is not exposed to this at the same scale, and
Coinbase exposes no portfolio-level BTC equivalent to substitute.
Bitpanda has no historical sync at all.
2026-08-11 11:04:06 +00:00
..
AccountMappingTest.php fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
AuthorizationControllerTest.php feat(open-banking): finalize bank connection without a session via state token (#498) 2026-06-06 12:12:07 +02:00
BalanceSyncServiceTest.php Batch historical balance writes (#356) 2026-05-05 15:26:28 +01:00
BankingConnectionSyncerFactoryTest.php feat(banking): add Interactive Brokers sync via Flex Web Service (#581) 2026-06-23 11:39:24 +02:00
BinanceBalanceSyncTest.php fix(binance): value past days from Binance's own snapshot total (#763) 2026-08-11 11:04:06 +00:00
BinanceControllerTest.php feat(open-banking): remove feature flag gating (#297) 2026-04-17 10:20:05 +02:00
BitpandaBalanceSyncTest.php fix(banking): stop crypto holdings quoted only in USD from vanishing from the balance (#759) 2026-08-11 02:56:03 +00:00
BitpandaControllerTest.php feat(open-banking): remove feature flag gating (#297) 2026-04-17 10:20:05 +02:00
CoinbaseBalanceSyncTest.php fix(coinbase): stop one unlisted stablecoin from unpricing the whole portfolio (#761) 2026-08-11 08:24:14 +00:00
CoinbaseControllerTest.php feat: Coinbase banking integration (#388) 2026-05-13 19:53:30 +02:00
ConnectionAccountTest.php feat(open-banking): enable manage bank accounts for everyone (#572) 2026-06-20 17:35:43 +00:00
ConnectionControllerTest.php feat(banking): let Wise credentials be updated (#588) 2026-06-23 09:54:31 +00:00
DailyEmailTransactionsIndexTest.php perf(db): index transactions for the daily synced-email slow query (PHP-LARAVEL-3X) (#622) 2026-07-02 15:51:15 +02:00
EnableBankingProviderTest.php fix(banking): stop unclassified bank responses from silently killing a connection (#742) 2026-08-09 17:26:34 +02:00
IndexaCapitalBalanceSyncTest.php fix(banking): treat Indexa Capital performance 404 as empty data (#386) 2026-05-13 09:34:28 +01:00
IndexaCapitalControllerTest.php feat(open-banking): remove feature flag gating (#297) 2026-04-17 10:20:05 +02:00
InstitutionControllerTest.php feat(open-banking): remove feature flag gating (#297) 2026-04-17 10:20:05 +02:00
InteractiveBrokersBalanceSyncTest.php feat(banking): add Interactive Brokers sync via Flex Web Service (#581) 2026-06-23 11:39:24 +02:00
InteractiveBrokersControllerTest.php refactor(open-banking): extract shared connect-controller flow into a base class (#639) 2026-07-04 20:24:54 +02:00
OpenBankingFeatureFlagTest.php refactor: remove HIDE_AUTH_BUTTONS launch gate and waitlist apparatus (#717) 2026-07-22 08:51:48 +02:00
SyncBankingConnectionJobTest.php fix(banking): stop requesting a year of Enable Banking history on every sync (#755) 2026-08-10 15:56:45 +02:00
SyncBinanceHistoricalBalancesJobTest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
SyncRetryAndLoggingTest.php fix(banking): stop Wise outages from silently parking a bank connection (#757) 2026-08-10 16:40:09 +02:00
TransactionSyncServiceTest.php feat(transactions): allow manual transactions on bank-connected accounts (#747) 2026-08-10 05:32:40 +02:00
WiseBalanceSyncTest.php feat: add Wise open banking integration with balance sync (#525) 2026-06-16 13:23:25 +00:00
WiseControllerTest.php feat: add Wise open banking integration with balance sync (#525) 2026-06-16 13:23:25 +00:00
WiseTransientErrorTest.php fix(banking): stop Wise outages from silently parking a bank connection (#757) 2026-08-10 16:40:09 +02:00