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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.5 KiB
8.5 KiB
Деньги смены → остатки счетов (прогресс)
Автономная задача владельца (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)по Postedmoney_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 — с остатком счёта.
План
- МойСклад reference →
docs/money-moysklad-reference.md. - Миграция
Phase34_ShiftMoneyMovements(manual, idempotent):money_transactions.Category,.CashShiftId,organizations.AllowNegativeBalance. Применилась на стенде (в history + колонки есть). MoneyService.CloseShiftAsync→MaterializeShiftMovementsAsync(Posted-движения по типам, link на смену, идемпотентно).- Guard:
PostAsync(расход > остатка счёта → InsufficientFunds 409) +AddDrawerOperationAsync(выплата > нала смены); настройкаAllowNegativeBalanceв OrgSettings (PUT только Admin); InsufficientFunds→409 в MapResult/Drawer. - Остатки/отчёты правильные автоматически (всё читает money_transactions);
Categoryв DTO для типов. docs/money.md— раздел «Как деньги смены попадают в остатки» + «Отрицательный остаток».- Остаток вычисляемый → пересчёт не нужен; новые смены материализуются при закрытии. (Историч. закрытые смены прошлых тест-орг не бэкфиллим — чистятся prune'ом; демо пересоздан прогоном B.)
- full-chain:
indepBalance()— независимый расчёт (нач + платежи + смены: касса += факт−размен, эквайринг += картой−возвраты), сверяется с API /balances; шаг 8 — оплата ≤ остатка + отказ при превышении. Unit (MoneyMathTests, +1 = 26 ✓) + integration (MoneyModuleTests, +2 = 10 ✓). - deploy-stage; post-deploy C 16/16 зелёный; репро до деплоя — 4 шага красные (подтверждено).
- Коммиты в main + push (после 2×C + B зелёных).
Найденное/исправленное
- Движения смены не попадали в остаток счёта (касса/эквайринг игнорировали продажи/возвраты/ внесения/выплаты/расхождение). Фикс: материализация при закрытии смены в money_transactions по типам.
- Касса уходила в минус без предупреждения (расход больше остатка). Фикс: guard InsufficientFunds (409) на расход money-tx и выплату из смены; настройка орг «Разрешить отрицательные остатки» (по умолч. выкл., Admin).
- Дыра в тесте: инвариант сверял остаток с суммой из того же 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 подряд зелёные.