Карта отображения баланса¶
Слово «Баланс» встречается на семи экранах, и за ним стоят разные величины. Документ фиксирует, какое поле читает каждый экран и какими равенствами эти величины связаны.
Повод для карты: 29.07.2026 портфель показал $3 500.48, а страница вывода в тот же момент $1 496.06, обе подписи гласили «Баланс». Причина была не в расчёте, а в том, что связь между этими числами нигде не была ни описана, ни проверена (SI-461).
Три величины¶
Все три считает canonical/balance_service.go, других источников быть не должно.
| Величина | Что это | Где считается |
|---|---|---|
totalValue |
Живой баланс на сейчас | BalanceService.GetUserBalance |
availableBalance |
Доступно к выводу | Равно totalValue по бизнес-правилу от 2025-12-18 (изъятие и вывод объединены) |
displayedBalance |
Снапшот на конец вчера, если он положителен, иначе живой итог | canonical.PickDisplayedBalance |
Снапшот нужен, чтобы заголовок не дрожал от внутридневных реконсиляций. Плата за это — расхождение с живым остатком в течение дня, и закрывать его обязана плашка «Сегодня».
Инварианты¶
displayedBalance + todayNetChange == totalValueс точностью до доходности, начисленной за текущие сутки. Плашка «Сегодня» намеренно показывает только подтверждённые движения денег, без начислений. Проверяется:TestDashboardHeadlineReconcilesWithLiveBalance.availableBalance == totalValue. Проверяется там же.- Заявка на вывод меняет баланс в день выплаты, а не в день подачи. Единственный источник
правила —
canonical.WithdrawalBalanceEffect. Проверяется:TestWithdrawalBalanceEffect,TestSplitTodayActivity_WithdrawalCountsOnTheDayMoneyLeft,TestAnalyticsWithdrawalPeriod_MatchesCanonicalRule. - Контракт
/api/user/dashboardсуществует в одном экземпляре: Go-структураtypes.UserDashboardResponse, из неё генерируется TypeScript. Рукописных копий быть не должно. Проверяется:frontend/user-app/src/__tests__/contract/api-contracts.test.ts.
Экраны¶
user-app¶
| Экран | Подпись | Поле | Комментарий |
|---|---|---|---|
| Портфель, карточка | «Баланс» | displayedBalance |
Плюс расшифровка «Сегодня» из todayUserDeposits / todayGiftDeposits / todayWithdrawals |
| Вывод средств | «Баланс» | availableBalance |
Живой остаток: выводить можно только то, что есть сейчас |
| Депозит | баланс в шапке | totalValue |
Живой остаток |
| Реестр операций | «Итого за период → Баланс» | displayedBalance + todayNetChange |
Тот же заголовок, что в портфеле |
| График баланса | ось «Баланс» | /api/user/balance/daily-export |
Ежедневная таблица; последняя точка — вчера |
Портфель и страница вывода показывают разные числа в течение дня по устройству, а не по ошибке. Разницу объясняет плашка «Сегодня»; если она пуста, а числа расходятся — это баг.
admin-app¶
| Экран | Подпись | Поле | Комментарий |
|---|---|---|---|
| Карточка клиента | «Баланс» | financialSummary.displayedBalance |
Тот же снапшот, что видит клиент |
| Список клиентов | колонка «Баланс» | bal.Total (живой) |
Расходится с карточкой в течение дня |
Последняя строка — известное расхождение, требующее продуктового решения: оператор видит в списке живой остаток, а в карточке того же клиента — снапшот на вчера. Оба числа корректны, но подписаны одинаково. Варианты: показывать в списке снапшот (оператор видит ровно то же, что клиент) либо в карточке живой остаток (оператор всегда видит фактические деньги). До решения расхождение остаётся, но теперь оно записано, а не спрятано.
Что ломалось раньше¶
- Дашборд относил вывод к дню подачи заявки, а баланс — к дню выплаты. Ручная выплата идёт несколько суток (на PROD 6 из 7 завершённых выводов закрыты не в день заявки), поэтому плашка «Сегодня» пустовала, а заголовок показывал уже потраченные деньги.
- Структура ответа дашборда объявлялась внутри функции-обработчика, генератор типов её не видел, и фронт держал три рукописные копии контракта с разными наборами полей.
- Регресс-тест на согласованность сравнивал
displayedBalanceсам с собой и в комментарии описывал контракт, которого в коде не было.