Commit Graph

2593 Commits

Author SHA1 Message Date
Дмитрий c7e015a9ac refactor(fe): убрать мёртвый repositionMenuAfterOpen - ядро внутреннее
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Старый per-instance экспорт больше не используется (заменён глобальным
installMenuRepositionFix). Старый тест-файл удалён - механизм покрыт
installMenuRepositionFix.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:30:06 +03:00
Дмитрий 11dcd04173 refactor(fe): снять ручные обходы меню - заменены глобальным установщиком
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:27:46 +03:00
Дмитрий c78b69fcaf feat(fe): подключить installMenuRepositionFix при запуске SPA
Также: привести resizeSpy в тесте к EventListener (тип-чистота vue-tsc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:23:55 +03:00
Дмитрий 9f013ec591 feat(fe): глобальный installMenuRepositionFix + тест механизма
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:20:08 +03:00
Дмитрий 4fd4e390af docs(план): реализация глобального фикса позиционирования меню - 4 TDD-задачи
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:15:09 +03:00
Дмитрий 4044885c3e docs(спека): глобальный фикс позиционирования выпадающих меню - корневой обход вместо ручных пометок
Корень дефекта живого клиента 27.06: список Тип лица в окне создания
проекта уезжал за экран, реквизиты не сохранялись 422. Обход вешался
вручную на каждый список и забыт в 3 окнах. Решение - включать обход
автоматически глобально через MutationObserver, убрать ручные пометки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:09:50 +03:00
Дмитрий 9d0999d49a style(админка): pint — new UseAdminConnection без скобок в тесте
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:17:39 +03:00
Дмитрий b38fe0c875 feat(админка): admin-db middleware в группе saas-admin + SharesAdminPdo для тестов
bootstrap: alias admin-db=UseAdminConnection; web.php: группа saas-admin теперь
['saas-admin','admin-db'] (swap default→pgsql_admin после гейта). Тест: admin-db
в пайплайне /api/admin/tenants, saas-admin не потерян.

SharesAdminPdo (зеркало SharesSupplierPdo) применён глобально к Feature suite
(Pest.php): admin-db висит на всей группе → admin-эндпоинты в тестах читают
через pgsql_admin (separate PDO) и не видели бы засеянные в транзакции данные;
sharing PDO даёт cross-connection visibility. baseline: +trait.unused
(Pest применяет трейт в рантайме, phpstan не видит uses() из Pest.php).
261 supplier+admin тестов зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:54:23 +03:00
Дмитрий 1c72f6dec2 feat(админка): middleware UseAdminConnection — swap default на pgsql_admin
Меняет default-подключение на pgsql_admin на время admin-запроса и
восстанавливает прежнее в finally (важно для Pest: несколько запросов в
одном процессе). Ставится после saas-admin. Tests: swap+restore и
restore при исключении downstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:39:27 +03:00
Дмитрий d5c972c3f2 feat(админка): connection pgsql_admin под ролью crm_admin_user (Путь А)
AdminTenantsController/AdminBillingController ходят под default-подключением;
новое pgsql_admin (crm_admin_user, srv_bypass) даст им cross-tenant доступ
через middleware-переключатель (следующий коммит). На dev fallback на
DB_USERNAME. Test: pgsql_admin делит базовый pgsql-конфиг, роль из DB_ADMIN_*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:37:42 +03:00
Дмитрий 819d74292f fix(phpstan): resync larastan baseline drift (pre-existing, не от admin-правок)
Дрейф выше старого baseline: счётчики ignore Pest-хелперов (postJson/actingAs/
$tenant на PendingCalls\TestCall) выросли в тест-файлах + 2 PaymentGateway
'strict comparison int/null always false' (PHPDoc-certainty). Все pre-existing,
ни одного в admin-правках. Регенерация по quirk 25 (2 шага). NB deferred-проверка:
PaymentGateway.php:38 и AdminPaymentGatewayController.php:35 — глянуть отдельно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:37:15 +03:00
Дмитрий 2c876162d5 fix(deptrac): baseline ProjectResource→ProjectRuleMessages (Эпик 6, ADR-005)
Pre-existing нарушение: ProjectRuleMessages (Service) — read-only текст правил
сбора для UI-баннеров, тот же класс что уже принятый SupplierSnapshotGuard.
По ADR-005 такие read-only UI-вычисления принимаются в baseline (перенос в
контроллер усложнил бы коллекции без выигрыша). Не от текущих admin-правок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:32:24 +03:00
Дмитрий 737d2e192b docs(админка): уточнённая спецификация + план фикса доступа через crm_admin_user
Поправка по факту кода: реально сломаны только AdminTenantsController и
AdminBillingController (ходят под default crm_app_user); Incidents/Pd/
SupplierIntegration/Impersonation уже используют pgsql_supplier и работают.
План: connection pgsql_admin + middleware UseAdminConnection (admin-db).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:21:00 +03:00
Дмитрий 1b3158dd45 docs(админка): спецификация фикса доступа к данным через crm_admin_user (Путь А)
Корень: после переезда на Managed PG админка ходит под crm_app_user без
cross-tenant доступа; штатная роль crm_admin_user готова, но не подключена.
Способ A: pgsql_admin connection + middleware-переключатель на админ-группе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:14:04 +03:00
Дмитрий a8aa79e75f chore(safety): гард от сноса боевой базы + указатель на живую БД (защита от параллельных сессий)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Повод: 26.06.2026 параллельная сессия выполнила yc managed-postgresql database
delete liderra + recreate на боевом кластере → переналила схему со старыми
небезопасными RLS-политиками → вход в портал лёг (см. db/CHANGELOG_schema.md v8.57).

- .claude/hooks/prod-db-guard.mjs (PreToolUse Bash|PowerShell): блокирует ТОЛЬКО
  снос/пересоздание боевой базы/кластера (yc database/cluster delete, DROP DATABASE
  liderra). Обычную работу (чтение, запросы, тесты на liderra_testing, migrate)
  НЕ трогает. Override владельца: маркер PROD-DESTROY-OK или env ALLOW_PROD_DB_DESTROY=1.
  Проверено 7 сценариями + живым запуском (echo с паттерном заблокирован).
- .claude/hooks/prod-db-pointer.mjs (SessionStart): инжектит указатель «живая база =
  кластер c9q2cvtjpq3hgq6l0r96, старая копия на VM не трогать, тесты на liderra_testing»
  — чтобы сессия не путала актуальную БД со stale-копией и не «пересобирала».
- .claude/settings.json: deny-паттерны (yc database/cluster delete) + оба хука.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:40:23 +03:00
Дмитрий a17e72a52e fix(billing): ЮKassa — формируем чек 54-ФЗ при онлайн-пополнении (фикс 400 Receipt is missing)
Магазин ЮKassa (1392092) с включённой фискализацией требует секцию receipt на
каждом платеже. OnlineTopupService передавал receipt=null → ЮKassa отклоняла
создание платежа 400 "Receipt is missing or illegal" (Server Error при пополнении).

- OnlineTopupService::start теперь формирует receipt: customer.email (почта
  пользователя, fallback на mail.from), items[] с vat_code=1 («без НДС», ИП на УСН),
  payment_mode=full_prepayment, payment_subject=service. Передаём всегда (магазин
  требует чек безусловно). Формат проверен живым запросом к боевому API → HTTP 200.
- YooKassaDriver: в исключение createPayment/verifyPayment добавлено тело ответа
  (body=...), чтобы причина 4xx была видна в логе сразу.
- OnlineTopupServiceTest: withArgs гарантирует, что receipt передаётся (email,
  vat_code=1, amount, payment_subject) — защита от регресса к null.

Проверено: Pest passed, Pint clean, формат чека → HTTP 200 на api.yookassa.ru.
larastan/deptrac пропущены (LEFTHOOK_EXCLUDE) — падения предсуществующие (Mockery/
Pest-stub ложные в тестах; код-файлы OnlineTopupService/YooKassaDriver — 0 ошибок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:39:38 +03:00
Дмитрий 08558df8ee fix(rls): NULLIF-хардненинг GUC во всех 44 политиках tenant_isolation — фикс входа на Managed PG
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Инцидент 26.06: вход в портал падал на резолве users (60 ошибок 22P02/42704)
под PgBouncer transaction pooling. current_setting('app.current_tenant_id')::bigint
падал при пустом ('' -> 22P02) или незаданном (-> 42704) GUC на auth-bootstrap
(резолв users/auth_log ДО tenant-контекста, на auth-роутах без 'tenant' middleware).

- все 44 политики -> NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
  (флаг ,true убирает 42704; NULLIF(...,'') убирает 22P02; пусто/не задано -> 0 строк,
  изоляция при заданном tenant НЕ меняется)
- 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes,
  user_sessions) получили ветку "NULLIF(...) IS NULL OR ..." — доступ до tenant-контекста
- миграция 2026_06_26_153000 применена на боевой кластер (44 safe / 0 unsafe, lead_charges
  FORCE RLS сохранён, изоляция проверена deals empty=0/tenant2=1013, вход endpoint=422)
- schema.sql v8.57 + CHANGELOG_schema.md + guard-тест RlsGucHardeningGuardTest (зелёный)
- rls-reviewer: APPROVE-WITH-NITS (изоляция при заданном tenant не ослаблена)

Larastan/deptrac пропущены через LEFTHOOK_EXCLUDE: их падения предсуществующие и не
связаны с этим коммитом (larastan — 109 ложных Pest-stub ошибок в чужих файлах, в новом
тесте 0; deptrac — 1 нарушение в app/app/**, тест вне слоёв). Проверено прямым прогоном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:15:22 +03:00
Дмитрий d6ffa0a6d0 docs: ЮKassa go-live шаги 1-4а — обновлены ПИЛОТ.md и ранбук
Договор ЮKassa подписан, магазин 1392092, на проде в кластерной базе заведены legal_entities ИП id=1 + payment_gateways yookassa id=2 active + webhook payment.succeeded. Осталось включить флаг billing_yookassa_enabled + живой тест 100р. Поправка к ранбуку: после переезда на Managed PG данные приложения заводить через app/Eloquent, не через peer-psql на VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:59:37 +03:00
Дмитрий 1b809d6abc docs(ПИЛОТ): снимок 26.06 — боевая БД переехала на Managed PG (переезд + хвосты + откат)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:47:49 +03:00
Дмитрий 662ebd6e8b feat/db-path-a: прод переключён на Managed PG + verify-full SSL + хвосты закрыты
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
- config/database.php: добавлен sslrootcert (env DB_SSLROOTCERT) для sslmode=verify-full
- ПИЛОТ.md §3: боевая БД = Yandex Managed PG; старая локальная БД = откат >=7 дней
- etap3-prod-cutover-DONE: отчёт переезда (деньги ДО==ПОСЛЕ, HTTP200, изоляция, откат)
- cspell-words: +рус. жаргон из снимков

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:22:06 +03:00
Дмитрий 1b5316b2c8 feat/db-path-a: anon(152-ФЗ)+схема+изоляция проверены на боевом Managed PG; 02_grants портирован под управляемую базу
- anon 1.3.2 включён и проверен на кластере (static masking работает) — 152-ФЗ закрыт
- schema.sql v8.56 применяется под mdb_admin: 90 таблиц/44 RLS/159 функций (1 безвредный артефакт FK-порядка)
- 02_grants.sql: GRANT членства роли обёрнут в DO/EXCEPTION — падал на Managed (нет ADMIN OPTION), членство выдаётся через yc control plane; теперь 0 ошибок на обеих средах
- 03_service_bypass: 44 srv_bypass политики; изоляция арендаторов и srv_bypass проверены вживую
- отчёт: docs/superpowers/findings/2026-06-26-db-migration/etap2-managed-cluster-results.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:24:30 +03:00
Дмитрий 7b23118856 fix(db): шов E — убраны мёртвые ссылки в 02_grants.sql (Путь А)
REVOKE на tenant_subscriptions (нет в продукте) и ALTER OWNER на webhook_log
(удалена в v8.35 legacy-webhook removal) вызывали ошибки при провижене ролей.
Убраны. Проверено: повторный прогон 02_grants.sql на полигоне — без ошибок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:46:36 +03:00
Дмитрий 347bc3a13b feat(db): Путь А — пересчёт аудита через GUC + политики srv_bypass вместо BYPASSRLS
Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.

Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).

Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.

LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:39:19 +03:00
Дмитрий 7efe9e3e83 fix/tests: idempotency 2 auth-тестов — SharesSupplierPdo против утечки регистрации мимо отката
Accessibility (Pa11y live) / a11y (push) Has been cancelled
AuthFlowIntegrationTest и AuthLogCoverageTest писали регистрацию через BYPASSRLS pgsql_supplier без SharesSupplierPdo. Юзер коммитился мимо DatabaseTransactions и не откатывался; на грязной или повторной БД register отдавал 422 email уже существует — это часть прод-прогона 1730/11. Добавлен uses SharesSupplierPdo: тесты идемпотентны 16/16 дважды, 0 утечки. На свежей migrate-БД весь набор 1757 прошло 0 упало 1 skip. Разбор 11 в findings tails-doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:43:30 +03:00
Дмитрий 77107c9cb8 docs/source-edit: пост-выкатная сверка байт-в-байт + полный прогон тестов на проде (1730/11)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Сверка прод===gitea===локалка (1105 файлов, 0 расхождений). Полный прогон на боевом Linux в изолированной liderra_testing: 1730 прошло, 11 упало (инфра-зависимые, не баги); AutoPause/SchemaDelta/--parallel подтверждены как окруженческие. Рецепт безопасного прода-прогона + грабли зафиксированы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:53:14 +03:00
Дмитрий fbf982e12c docs: обновление состояния — фича на проде, флаг ВКЛ, тумблер; ПИЛОТ снимок 26.06; CLAUDE §6
Accessibility (Pa11y live) / a11y (push) Has been cancelled
ПИЛОТ.md — снимки выката source-edit + включения флага и тумблера. findings tails-doc — статус ВЫКАЧЕНО НА БОЕВОЙ. CLAUDE.md §6 последняя продуктовая фича обновлена, снята устаревшая ремарка про синк квинтета (закрыто в PSR/Tooling), плюс досессионная правка Б-1 ИП/ЮKassa. Нормативный квинтет Pravila/PSR/Tooling без изменений (агент normative-sync подтвердил).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:38:22 +03:00
Дмитрий f9f86ca05f feat/admin: тумблер разблокировки смены источника на экране интеграции с поставщиком
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Дружелюбный переключатель ВКЛ/ВЫКЛ флага routing_match_by_snapshot для владельца — без правки БД и без 30-символьного основания общего edit-flow. GET/POST source-edit-flag в AdminSupplierIntegrationController пишут в system_settings type=bool + audit-журнал. На экране карточка с VSwitch и диалогом подтверждения, бамп ключа возвращает тумблер к факту при отмене. TDD: 5 эндпоинт-тестов + фронт-спек. Larastan чист, baseline дополнен Pest-шумом. Проверено глазами через Playwright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:27:32 +03:00
Дмитрий f82596c527 docs/pilot: снимок выката source-edit-snapshot-routing на боевой + пометка ИП/ЮKassa
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:02:38 +03:00
Дмитрий e84342319c docs/source-edit: §9a закрыт экраном вечерней заливки, хвосты сессии и порядок выката
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
§9a отчёта о завершении вечерней заливки закрыт: формат — экран в админке Эпик 5 supplier_sync_runs плюс AdminSupplierIntegrationView, решение владельца 26.06.2026.

В файл хвостов добавлен итог ночной сессии до выката: полная регрессия 1750 из 1756, разбор 2 падений SchemaDelta мой починен и AutoPause чужой time-of-day квирк, вердикт нормативки правок не нужно, вердикт готовности боевого GO, порядок выката и чек-лист. Добавлен утренний хэндофф-runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 03:35:29 +03:00
Дмитрий 63a2d53255 fix/projects: смена лимита-региона-дней на защищённом проекте больше не блокируется ложно как смена источника
Симптом: на проекте, по которому уже идут лиды от поставщика, правка только лимита, региона или дней отдавала 422 «Изменить источник можно будет после N» — хотя источник не менялся. Найдено приёмкой 25.06.2026 глазами через Playwright. Дефект на main, то есть живой на боевом liderra.ru.

Корень: ProjectService::update вычислял sourceFieldsTouched по присутствию ключа signal_identifier, а дроуэр site и call всегда его шлёт даже неизменённым.

Фикс: новый метод sourceValueChanged сравнивает фактическое значение источника, а не присутствие ключа. Guard срабатывает только на реальную смену источника.

TDD: добавлен падавший тест test_update_does_not_invoke_guard_when_signal_identifier_present_but_unchanged. Larastan чист, phpstan-baseline обновлён под Mockery-шум. Также project_rule добавлен в тип уведомлений и icon-map колокольчика; SchemaDeltaTest приведён к метрикам схемы v8.55 после 2 новых таблиц.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 03:34:55 +03:00
Дмитрий 7854c9fe63 docs: хвосты сессии разблокировки смены источника — что проверить/исправить
Полный список открытых пунктов: глазами не смотрел (баннер до-18:00, колокольчик 6.2,
онлайн-заморозка 4); давний фронт-тест-долг 22 красных (не моя регрессия); нормативка
+ OPEN-§9a + prod-deploy-validator перед боем; флаг ВЫКЛ. Зафиксировано перед компактом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:57:59 +03:00
Дмитрий 91690c4fd9 fix/test: ProjectServiceGuardWiring — мок ждёт isProtected (Эпик 6.2 добавил вызов)
Полный прогон бэка поймал: update() теперь зовёт isProtected() до assertCanMutateSource
(для уведомления о хвосте, Эпик 6.2), а wiring-мок этого не ждал → Mockery error.
Добавлено shouldReceive('isProtected'). 3/3. Единственная регрессия из полного прогона (883 теста).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:53:16 +03:00
Дмитрий ca923e4da4 fix/ui: объявления о датах — актуальны по времени суток (правило 18:00/21:00 МСК)
Найдено проверкой глазами в 19:27 МСК (после 18:00):
1. Баннер правок количества/региона/дней говорил «вступят со следующего дня» — врал
   после 18:00 (правка не попадает в сегодняшний слепок → реально послезавтра). Теперь
   показывает АКТУАЛЬНУЮ дату через firstLeadDate (до 18:00 → завтра, после → послезавтра):
   «…вступят в силу с 27 июня». Дроуэр + окно «Редактировать».
2. Сообщение блокировки удаления/смены источника в SupplierSnapshotGuard было захардкожено
   «мы увидим это сегодня в 18:00 … можно будет послезавтра» — после 18:00 «сегодня в 18:00»
   уже прошло. Теперь time-aware через computeGraceUntil: «…лиды придут до 26 июня … можно
   будет после 26 июня».

Проверено глазами: баннер лимита (27 июня), подтверждение источника (до 26 июня),
блок удаления (после 26 июня) — все согласованы и меняются по времени суток. Тесты:
guard 30/30, фронт 38/38, leadDate (18:00 порог) зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:40:04 +03:00
Дмитрий d98fc3c834 test/router: сквозной поток лида для изменённого и удалённого проекта (вопрос владельца)
Доказывает end-to-end (лид → RouteSupplierLeadJob → сделка):
- изменён источник, проект ЖИВ: лид по СТАРОМУ источнику доезжает до сделки (слепок
  сегодня помнит старый источник, INNER JOIN projects проходит);
- удалён проект: лид по его источнику НЕ падает в сироту и не роняет раздачу (INNER JOIN
  projects ON id=snap.project_id отсекает удалённый проект, сделка не создаётся).
2/2 зелёные. Закрывает пробел: раньше тестировался только матч-запрос, не поток до сделки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:21:55 +03:00
Дмитрий 29c08d6078 docs/plan: все эпики 1-6 закрыты + проверка глазами (Playwright)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:11:47 +03:00
Дмитрий 64a76a21c3 feat/ui: текст правила смены источника — единый из API, без дублей в JS (Эпик 6.3)
ProjectResource.source_change_message = ProjectRuleMessages.sourceChanged (тот же текст,
что in-app уведомление 6.2). Диалоги подтверждения (дроуэр + окно Редактировать) тянут его
из API с fallback на локальный текст. Бэкенд — единственный источник строк правил, экран и
колокольчик не расходятся. Проверено глазами (epic6-unified-rule-text-confirm.png). Тесты:
ProjectResource 5/5, дроуэр 27/27, EditProjectDialog 7/7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:08:47 +03:00
Дмитрий e655af6298 feat/notifications: in-app уведомления клиенту о правилах сбора при действиях с проектом (Эпик 6.2)
ProjectService::update по завершении шлёт in-app объяснение правила (текст из
ProjectRuleMessages): смена источника по защищённому проекту → про хвост старого
источника; иначе slepok-правка → когда вступит в силу. NotificationService::notifyProjectRule
шлёт всем активным юзерам тенанта через колокольчик без pref-гейта (правила должны доходить
всегда). Уведомление о хвосте — только если проект реально под защитой (есть поставщик). 3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:03:24 +03:00
Дмитрий be08239634 feat/projects: единый источник текстов правил сбора для клиента (Эпик 6.1)
ProjectRuleMessages — 6 методов (создан/изменён/смена источника/пауза/возобновление/баланс)
с русским форматом даты «D MMMM» и склонением «лид». Единственный источник текстов:
in-app уведомления (6.2) и баннеры (6.3) тянут отсюда, не дублируют строки. 6/6 тестов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:56:47 +03:00
Дмитрий 1d18933d9e feat/supplier: экран отчёта о вечерней заливке (Эпик 5)
Владелец выбрал формат «экран в админке» (не письмо).
- SyncSupplierProjectsJob по завершении пишет строку-сводку в новую supplier_sync_runs
  (групп/синк/ручная/отложено/упало + status ok|partial|failed|aborted) через finally —
  пишется и при раннем abort (time-budget/mass-fail/auth).
- Эндпоинт GET /api/admin/supplier-integration/sync-runs + метод syncRuns.
- Экран SaaS-admin «Интеграция с поставщиком» → карточка «Вечерняя заливка проектов
  поставщику»: таблица заливок со статусом человеческим языком (Всё ровно/Частично/Сбой).
- Схема v8.55 +1 таблица (SaaS-level без RLS как supplier_csv_reconcile_log), миграция
  2026_06_25_130000, RLS-ревью 7/7. Проверено глазами в браузере (epic5-sync-runs-admin-screen.png).

Тесты: бэк 24/25 (1 skip) + фронт-экран 5/5 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:52:39 +03:00
Дмитрий 941b4b9f67 feat/ui: окно «Редактировать» — источник редактируем + объявление + подтверждение (Эпик 3)
Найдено при проверке глазами: «Редактировать» открывает NewProjectDialog (mode=edit),
где источник был :readonly — отдельный от дроуэра путь без UX замка. Привёл к тому же
поведению Эпика 3:
- источник site/call больше не readonly в edit;
- баннер np-applies-from-banner при правке количества/региона/дней на залоченном проекте;
- диалог np-source-change-confirm при смене источника, PATCH только после подтверждения.
Оба окна (панель справа + «Редактировать») теперь консистентны. Проверено в браузере.
EditProjectDialog 7/7, NewProjectDialog регрессия 20/22 зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:28:14 +03:00
Дмитрий eaa21540fe docs/plan: статус — Эпики 1/3/4 закрыты, Эпик 5 ждёт решения по формату отчёта
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:07:54 +03:00
Дмитрий 9d703ccb2a feat/supplier: онлайн-заморозка 18:00→00:00 + досыл очередью в 00:05 (Эпик 4)
Закрывает рассинхрон онлайна со слепком поставщика 21:00.
- 4.1: SyncSupplierProjectJob в окне 18:00→00:00 МСК кладёт проект в новую очередь
  supplier_deferred_sync вместо немедленной отправки (перезаписала бы зафиксированный
  слепок). Вне окна — как раньше.
- 4.2: FlushDeferredOnlineSyncJob в 00:05 МСК досылает отложенное вне окна и чистит очередь.
- Схема: +1 таблица supplier_deferred_sync (project_id PK, без RLS — системная очередь как
  supplier_manual_sync_queue), миграция 2026_06_25_120000, schema.sql v8.54 + CHANGELOG.
  RLS-ревью пройдено (no-RLS консистентно прецеденту; формулировки GRANT/метрик уточнены).

Тесты 6/6 + регрессия онлайн-синка 33/33 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:07:15 +03:00
Дмитрий 7337bd23ce feat/ui: смена источника — объявление + подтверждение вместо жёсткого замка (Эпик 3)
Редизайн UX (design-gate 2026-06-25): раздача доводит хвост по старому источнику
через слепок, поэтому замок не нужен.
- 3.1: поля источника больше НЕ disabled; при правке количества/региона/дней на
  залоченном проекте — информ-баннер pdd-applies-from-banner (вступит со след. дня).
- 3.2: смена источника на залоченном проекте → диалог подтверждения
  pdd-source-change-confirm; PATCH только после подтверждения.
Старые тесты замка (disabled-поля) переписаны под новое поведение. 26/26 зелёных.

⚠️ Не выкатывать раньше включения флага routing_match_by_snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:48:34 +03:00
Дмитрий b1fe11b7ae test/projects: 3-е состояние source-lock — пауза в grace даёт твёрдую дату (projected=false)
Эпик 1 Task 1.1: поля source_locked/unlock_at/projected в ProjectResource уже отдаются
из SupplierSnapshotGuard::lockState (спека source-edit-lock-ux). Добавлен недостающий
характеристический тест состояния (в) — проект на паузе в grace-окне.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:42:26 +03:00
Дмитрий cfd8dec4c6 docs/plan: Эпик 2 закрыт — 2.6/2.7/2.4 закоммичены, регрессия 66 зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:37:07 +03:00
Дмитрий 35d51df6f7 fix/supplier: отложенное удаление источника пока летит хвост слепка или есть другие потребители
DeleteSupplierProjectJob перед удалением донора проверяет hasActiveSnapshotTail:
если за сегодня/завтра есть снимок маршрутизации с источником этого донора
(sms по sender+keyword, site с поддоменом, call по identifier — зеркало LeadRouter),
удаление откладывается до следующего CleanupInactiveSupplierProjectsJob (02:00).
Иначе удалив донора оборвём матч хвостового лида по старому источнику.

Эпик 2 Task 2.4. baseline +1 (Mockery once-noise, как DeleteSupplierProjectJobTest).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:35:58 +03:00
Дмитрий b7379e7a03 test/billing: страховка от двойного списания CSV+webhook вокруг смены источника
Под флагом routing_match_by_snapshot=ВКЛ: CSV-recovered лид + догоняющий webhook
одного физлида (sms-источник Caranga) сходятся в ОДИН Deal/charge. Доказывает шов §9b —
новый матч по слепку не разводит project_id, merge срабатывает, баланс не списан дважды.

Эпик 2 Task 2.7. baseline +4 записи (Pest higher-order $this-noise, как CsvWebhookRaceTest).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:30:47 +03:00
Дмитрий c8ec7a6616 fix/supplier: офлайн-батч берёт источник из слепка — закрыт рассинхрон 18:02→18:05
collectEligibleProjects переопределяет signal_identifier/sms_senders/sms_keyword
значениями слепка (как daily_limit/regions). Клиент мог сменить источник в окне
18:02 (snapshot) → 18:05 (sync), но завтрашний заказ поставщику фиксируется слепком
(slepok-инвариант) — раздача доводит хвост по старому источнику.

Тест-хелпер insertSnapshotForTomorrow теперь зеркалит источник проекта в слепок
(как прод SnapshotProjectRoutingJob), иначе группировка офлайн-батча пустеет.

Эпик 2 Task 2.6. Под LEFTHOOK=0 (окруженческий larastan-шум, см. память #111).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:26:28 +03:00
Дмитрий d7e42b7934 docs/plan: статус реализации — ядро (Эпик 0+2.1/2.2/2.3/2.5) закоммичено под флагом
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:59:58 +03:00
Дмитрий 675fba6d77 docs: разбор швов + план + приёмка + design-gate разблокировки смены источника
Полный комплект под фичу «матч источника по слепку, без потери лидов»:
- findings: ультра-гранулярный аудит швов денежного ядра (9 субагентов, §9a-9c)
- plans: реализация в 6 эпиков (TDD, флаг отката, порядок выката)
- runbooks: план проверки (код + глаза + деньги, GO/NO-GO)
- specs: design-gate Путь A — согласован владельцем 25.06.2026

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:57:37 +03:00