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

8.5 KiB
Raw Blame History

Деньги смены → остатки счетов (прогресс)

Автономная задача владельца (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. МойСклад reference → docs/money-moysklad-reference.md.
  2. Миграция Phase34_ShiftMoneyMovements (manual, idempotent): money_transactions.Category, .CashShiftId, organizations.AllowNegativeBalance. Применилась на стенде (в history + колонки есть).
  3. MoneyService.CloseShiftAsync → MaterializeShiftMovementsAsync (Posted-движения по типам, link на смену, идемпотентно).
  4. Guard: PostAsync (расход > остатка счёта → InsufficientFunds 409) + AddDrawerOperationAsync (выплата > нала смены); настройка AllowNegativeBalance в OrgSettings (PUT только Admin); InsufficientFunds→409 в MapResult/Drawer.
  5. Остатки/отчёты правильные автоматически (всё читает money_transactions); Category в DTO для типов.
  6. docs/money.md — раздел «Как деньги смены попадают в остатки» + «Отрицательный остаток».
  7. Остаток вычисляемый → пересчёт не нужен; новые смены материализуются при закрытии. (Историч. закрытые смены прошлых тест-орг не бэкфиллим — чистятся prune'ом; демо пересоздан прогоном B.)
  8. full-chain: indepBalance() — независимый расчёт (нач + платежи + смены: касса += факт−размен, эквайринг += картой−возвраты), сверяется с API /balances; шаг 8 — оплата ≤ остатка + отказ при превышении. Unit (MoneyMathTests, +1 = 26 ✓) + integration (MoneyModuleTests, +2 = 10 ✓).
  9. 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 подряд зелёные.