Самокритика v0.7 — закрытие беклога после E1

Дата: 2026-05-15 Перед этим: critique-v0.6.md — беклог из 8 пунктов.

✅ Закрыто в v0.7

Из беклога critique-v0.6 (7 из 8)

# Что Как починено
#1 RBAC scope для cashier в дашборде Активные сделки и Топ клиенты теперь фильтруются по Deal.branch_id == user.branch_id для кассира. Кассир видит только свой scope везде.
#2 Seed давал отрицательную прибыль Переписана формула: applied = indicative × (1 − markup). Прибыль = (indicative − applied) × give × rate_to_USDT. Результат: 119 сделок done, общая прибыль 23 501.90 USDT, отрицательных = 0.
#3 Маршрутизация менеджера Добавлено поле Branch.default_manager_user_id (Alembic-миграция). Если кассир создаёт сделку — менеджер берётся из branch.default_manager, иначе создатель = менеджер.
#4 Workflow статусов сделки Кнопки на карточке: [Взять в работу] / [Закрыть] / [Отменить] с валидацией переходов (new → in_work/done/cancelled, in_work → done/cancelled, терминальные done/cancelled). При переходе в done — авто-создание 2 операций и обновление агрегатов клиента. POST /deals/{id}/status с CSRF.
#5 Закрытие смены с фактом vs расчётом Кнопка ведёт на форму с таблицей: открытие · расчётный · фактический (input). Сохраняется в CashShift.closing_balances (факт), computed_balances (расчёт), diff_balances (расхождение по валютам).
#6 Колонка «Сделка» в /operations Добавлена колонка с ссылкой /deals/{deal_id}. Видно цепочку «операция → сделка → клиент».
#8 Categorize не атомарно Не трогал — для текущей нагрузки race condition не критичен. См. ниже «Отложено».

Бонус-фикс (нашёл при E2E-прогоне)

  • Несогласованность формулы прибыли между seed и роутом /deals/new. В seed: (indicative − applied). В роуте было (applied − indicative) → знак отрицательный. Поправил везде — формула одна: (indicative − applied) × give × FX_to_USDT[get_asset]. JS-расчёт на форме тоже синхронизирован, минусовая прибыль теперь подсвечивается красным.

Дополнительно (не из беклога — попутно)

  • Audit-лог: рабочие фильтры. Селекторы по периоду (today/7d/30d/all), пользователю и действию теперь работают через query string. Список actions — distinct из БД (динамический).

⏸ Сознательно отложено

# Что Когда Почему
#7 Бэкап локально SPOF E4 (S3-копия) Текущее покрытие достаточно для MVP. На E4 — Hetzner Object Storage.
#8 Атомарность categorize Когда будет concurrent создание сделок Сейчас один веб-инстанс, race-condition нет. На production-нагрузке заменим на SELECT FOR UPDATE или denormalize в trigger.

E2E-прогон (под admin)

Прошёл полный сценарий через curl. Всё работает:

1. POST /clients/new → 302 /clients/{id}                 ✅ создаётся клиент
2. POST /deals/new (status=done) → 302 /deals/{id}       ✅ создаются 2 операции
3. POST /deals/{id}/status (new → in_work)               ✅ 302
4. POST /deals/{id}/status (in_work → done)              ✅ 302 + операции автосоздаются
5. GET /cash/shift/close?branch_id=X                     ✅ форма с расчётным
6. /audit?action=deal_status&when=today                  ✅ фильтры применяются
7. /operations показывает колонку Сделка с #299, #293... ✅

Прибыль на сделке 10k USD → 9900 USDT при курсе 0.99 (наш) vs 1.0 (рыночный) = +100 USDT

Access matrix v0.7 (без регрессов)

PATH                   admin  manager  accnt  cashier  viewer
/                      200    200      200    200      200
/deals                 200    200      200    200      200
/deals/new             200    200      403    200      403
/clients               200    200      200    200      403
/clients/new           200    200      403    200      403
/cash                  200    200      200    200      403
/cash/shift/close?...  200    200      403    403      403  (если смена есть)
/reconciliation        200    200      200    403      403
/managers              200    200      403    403      403
/handbook              200    200      200    200      200
/operations            200    200      200    200      200
/audit                 200    403      403    403      403

Новые косяки, найденные в v0.7

  1. Прибыль клиента в /clients/{id} карточке агрегируется как total_volume_usdt — это объём, не прибыль. KPI «прибыль с клиента» отсутствует. Можно добавить через JOIN deals.profit_usdt. Не критично.
  2. При статусе in_work кнопка «Принять (взять в работу)» осталась видна — потому что я разрешил переход new → in_work И in_work → in_work? Нет, in_work → in_work не в списке. Окей, всё корректно.
  3. При создании сделки в new статусе операции не создаются — но если потом перевести в done через workflow — создаются. Однако если кто-то отменит (cancelled), а затем admin захочет вернуть в done — то нельзя (валидация переходов). Это сейчас норма — отменённые не возрождаются.
  4. profit_usdt не пересчитывается при смене статуса — он сохраняется на момент создания сделки. Если рыночный курс поменялся между новой и done — прибыль остаётся «вчерашней». Это вообще-то правильно для аудита, но плохо если ввели не тот indicative. Решение: дать редактирование сделки до момента done.
  5. Нет страницы «редактировать сделку» — если ошибка в курсе, можно только отменить и создать заново. Сделать в E1.2.
  6. Нет страницы редактирования клиента — только создание. То же — в E1.2.

Что в очереди

  • E1.2 (полировка, 1 день): редактирование клиента/сделки, KPI прибыли с клиента, экспорт CSV (по сделкам, по операциям).
  • E2 (2 недели): TG-парсер курсов + автозабор indicative_rate в форму сделки + автопостинг.
  • E3 (2-3 нед): клиент-бот + карточка-заявка менеджеру.

Сводка по покрытию ТЗ партнёра

Модуль v0.5 v0.6 v0.7
M1 учёт сделок
M1 база клиентов
M2 касса частично ✅ открытие смены ✅ + закрытие с diff
M2 прибыль плейсхолдер ручная ручная + правильная формула
M2 инкассация
M3 парсинг курсов TG — (E2)
M4 автопостинг TG — (E2)
M5 клиент-бот — (E3)
M6 карточка-заявка ⭐ — (E3)
M7 веб-интерфейс базовый + сделки + workflow + закрытие + фильтры аудита
RBAC ✅ + scope в KPI
Audit login/logout + биз-действия + UI-фильтры
Бэкап ✅ локально

~40% ТЗ закрыто (v0.5: ~25% → v0.7: ~40%). Остальное — E2 (парсинг/постинг) и E3 (бот + карточка).