food-market/docs/money-shift-balances-progress.md
nns 0977c639cd
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
docs(money): модель «деньги смены → остатки» + МойСклад reference + прогресс
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:11:10 +05:00

80 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Деньги смены → остатки счетов (прогресс)
Автономная задача владельца (08.10.2026, нашёл Mac-Claude на стенде). Деньги смены
(продажи нал/карта, возвраты, внесения/выплаты, расхождение) НЕ попадают в остаток
счёта. Делать ДО кассы — касса опирается на эти остатки.
## Факты со стенда (Демо после B)
- «Касса (e2e)»: нач. 50 000, текущий −98 812 (= 50 000 − 148 812 оплата приёмки нал).
Смена С-2026-000001: продажи нал 3 775, возвраты нал 1 500, внесение 10 000, выплата 3 000,
факт 14 775 — НИЧЕГО не попало в остаток счёта. Эквайринг: 0 при продажах картой 2 325.
- Оплата 148 812 нал при остатке 50 000 → касса в минус, никто не остановил.
- Инвариант теста «счёт ≡ начальный + Σ операций» прошёл только потому, что движения смены
не считаются операциями счёта — дыра в тесте.
## Текущая модель (как есть)
- Остаток счёта = `OpeningBalance + Σ(PaymentIn/CashIn/TransferIn) − Σ(PaymentOut/CashOut/TransferOut)`
по Posted `money_transactions` (MoneyAccountsController.List + MoneyReportsController.Balances).
- Смена (`CashShift`) хранит SalesCash/SalesCard/ReturnsCash/ReturnsCard/DrawerIn/DrawerOut/
ExpectedCash/ActualCash/CashDifference — считаются ПРИ ЗАКРЫТИИ из связанных retail_sales +
cash_drawer_operations. В `money_transactions` НЕ пишутся → в остаток счёта не входят.
## Выбранная модель (материализация движений смены при закрытии)
Единый реестр — `money_transactions`. При закрытии смены создаём Posted-транзакции на счета смены,
и существующая формула остатка автоматически их учитывает; отчёт «Движение» показывает их по типам.
- Касса смены (CashAccount): `+SalesCash` (Розничные продажи), `−ReturnsCash` (Возвраты),
`+DrawerIn` (Внесение), `−DrawerOut` (Выплата), `±CashDifference` (Излишек/Недостача).
- Эквайринг смены (AcquiringAccount): `+SalesCard` (Продажи картой), `−ReturnsCard` (Возвраты по карте).
- Размен (OpeningFloat) — НЕ движение (деньги уже в кассе). Итог: остаток кассы после закрытия =
старый остаток + (SalesCash−ReturnsCash+DrawerIn−DrawerOut+Diff) = ActualCash, если до смены на
кассе был только размен. Σ по типам = ActualCash − OpeningFloat.
Движения помечаем `Category` (enum) + `CashShiftId` (идемпотентность). Создаются только ненулевые.
## Отрицательный остаток
- Расход (CashOut/PaymentOut/TransferOut при post money-tx) и Выплата из смены (drawer Out) больше
доступных денег → 409 «На счёте «…» недостаточно денег: остаток X ₸, нужно Y ₸».
- Настройка орг `AllowNegativeBalance` (по умолч. выкл., менять только админ).
- Выплата из смены сверяется с ЖИВЫМ налом смены (OpeningFloat + SalesCash−ReturnsCash+DrawerIn−DrawerOut
на текущий момент), расход money-tx — с остатком счёта.
## План
1. [x] МойСклад reference → `docs/money-moysklad-reference.md`.
2. [x] Миграция `Phase34_ShiftMoneyMovements` (manual, idempotent): `money_transactions.Category`,
`.CashShiftId`, `organizations.AllowNegativeBalance`. Применилась на стенде (в history + колонки есть).
3. [x] `MoneyService.CloseShiftAsync` → `MaterializeShiftMovementsAsync` (Posted-движения по типам, link на смену, идемпотентно).
4. [x] Guard: `PostAsync` (расход > остатка счёта → InsufficientFunds 409) + `AddDrawerOperationAsync` (выплата > нала смены);
настройка `AllowNegativeBalance` в OrgSettings (PUT только Admin); InsufficientFunds→409 в MapResult/Drawer.
5. [x] Остатки/отчёты правильные автоматически (всё читает money_transactions); `Category` в DTO для типов.
6. [x] `docs/money.md` — раздел «Как деньги смены попадают в остатки» + «Отрицательный остаток».
7. [x] Остаток вычисляемый → пересчёт не нужен; новые смены материализуются при закрытии.
(Историч. закрытые смены прошлых тест-орг не бэкфиллим — чистятся prune'ом; демо пересоздан прогоном B.)
8. [x] full-chain: `indepBalance()` — независимый расчёт (нач + платежи + смены: касса += факт−размен,
эквайринг += картой−возвраты), сверяется с API /balances; шаг 8 — оплата ≤ остатка + отказ при превышении.
Unit (MoneyMathTests, +1 = 26 ✓) + integration (MoneyModuleTests, +2 = 10 ✓).
9. [x] deploy-stage; post-deploy C 16/16 зелёный; репро до деплоя — 4 шага красные (подтверждено).
10. [ ] Коммиты в main + push (после 2×C + B зелёных).
## Найденное/исправленное
1. **Движения смены не попадали в остаток счёта** (касса/эквайринг игнорировали продажи/возвраты/
внесения/выплаты/расхождение). Фикс: материализация при закрытии смены в money_transactions по типам.
2. **Касса уходила в минус без предупреждения** (расход больше остатка). Фикс: guard InsufficientFunds (409)
на расход money-tx и выплату из смены; настройка орг «Разрешить отрицательные остатки» (по умолч. выкл., Admin).
3. **Дыра в тесте**: инвариант сверял остаток с суммой из того же API. Фикс: независимый расчёт в тесте.
## Демо после фикса (прогон B, проверено руками)
- Касса: нач. 50 000 → текущий **31 275** (= 50 000 − 30 000 оплата приёмки нал
+ 9 775 смена С-001 (факт14775−размен5000) + 1 500 смена С-003). Раньше было −98 812.
- Касса-2: 3 000 → **4 550** (+1 550 смена С-004). Банк: 200 000 → **181 610** (−18 390 оплата №2).
- Эквайринг: 0 → **2 325** (продажи картой смены С-001). Раньше 0. Эквайринг-2: 0.
- Отрицательных остатков нет. 8 движений смен материализованы по типам (продажи нал/карта,
возвраты, внесение, выплата, излишек). Отказ при оплате больше остатка кассы — 409.
## Статус: ГОТОВО ✅
Движения смены попадают в остатки; отрицательный остаток запрещён (с настройкой). B 16/16 (36 скринов),
2× C подряд зелёные (16/16). unit 26 ✓, integration money 10 ✓.
## Журнал
- 08.10 — старт; модель (материализация при закрытии); миграция+код+guard+настройка+отчёт; docs.
Репро до деплоя: 4 шага красные. После деплоя: C 16/16 зелёный; unit 26 ✓, integration money 10 ✓.
Демо B зелёный, остатки сверены (см. выше). 2× C подряд зелёные.