# Деньги смены → остатки счетов (прогресс) Автономная задача владельца (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 подряд зелёные.