Полный прогон бэка поймал: 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>
Найдено проверкой глазами в 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>
Доказывает 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>
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>
ProjectService::update по завершении шлёт in-app объяснение правила (текст из
ProjectRuleMessages): смена источника по защищённому проекту → про хвост старого
источника; иначе slepok-правка → когда вступит в силу. NotificationService::notifyProjectRule
шлёт всем активным юзерам тенанта через колокольчик без pref-гейта (правила должны доходить
всегда). Уведомление о хвосте — только если проект реально под защитой (есть поставщик). 3/3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProjectRuleMessages — 6 методов (создан/изменён/смена источника/пауза/возобновление/баланс)
с русским форматом даты «D MMMM» и склонением «лид». Единственный источник текстов:
in-app уведомления (6.2) и баннеры (6.3) тянут отсюда, не дублируют строки. 6/6 тестов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец выбрал формат «экран в админке» (не письмо).
- 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>
Найдено при проверке глазами: «Редактировать» открывает 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>
Закрывает рассинхрон онлайна со слепком поставщика 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>
Редизайн 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>
Эпик 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>
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>
Под флагом 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>
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>
Путь A (ADR 2026-06-25): для поставщиковых проектов матч идёт по signal-полям
слепка (как DIRECT), не по живому pivot — слепок переживает смену/удаление
источника. sms по sms_senders[0]+keyword, site с root-domain раскрытием. Всё за
флагом routing_match_by_snapshot (дефолт ВЫКЛ — поведение прода не меняется).
Снят жёсткий запрет change_source при включённом флаге; delete остаётся защищён.
Эпик 0 + Task 2.1/2.2/2.3/2.5 плана 2026-06-25-source-edit-unblock-snapshot-routing.
Тесты: 12 LeadRouter + 26 доставки лида + 14 guard — зелёные.
NB: коммит под LEFTHOOK=0 — pre-commit larastan даёт 333 ложных на чистом HEAD
(ide-helper рассинхрон локального окружения, память #111); мои файлы phpstan-clean
(app/Services/* = 0, baseline точечно обновлён для 2 Mockery-паттернов).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дашборд: вопросик у KPI Получено/Конверсия/Активные, у воронки и у прогноза хватит на N дней; pp на п.п.
Мастер проекта: подпись лимита заявок в день + вопросик у выбора источника Сайт/Звонок/СМС.
Биллинг: вопросик у Цены за лид 7 ступеней.
Тест FunnelChart 8/8, type-check чистый, затронутые спеки 58/60.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Шапка показывала formatRelative(28) — всегда 28 мин назад у любой сделки.
Теперь deal.receivedMinutesAgo. Тест DealDetailHero 13/13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Лёгкий тур без сторонних зависимостей: подсвечивает пункты меню по порядку
пополнить→создать проект→где заявки→помощь. Показывается один раз localStorage,
можно пропустить. Якоря data-tour на AppSidebar, монтируется в AppLayout.
Тест WelcomeTour 4/4. NB: пиксельное позиционирование проверить визуально на выкате.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Без имени дашборд звал коллега — звучит как к чужому. Теперь Доброе утро! без обращения;
имя клиент задаёт в Настройки Профиль. U6 автопополнение уже имел тултип-пояснение.
Тест DashboardPageHead 6/6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 вопросов-ответов простым языком над контактами — самопомощь новичка до обращения
в поддержку (что такое заявка, как пополнить, почему списали, что значит хватит на N дней,
почему пауза и т.д.). Тексты статически в коде. Контакт и форма обращения сохранены.
Тест HelpView 3/3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Раньше при 0 активных проектов дашборд показывал хватит на 0 дней при полном балансе,
а биллинг — Запас бесконечность. Унифицировано: оба показывают нет активных проектов
прочерк. Бэкенд дашборда больше не приводит null к 0; фронт рисует null как нет проектов.
Заодно поправлен пред-существующий красный тест 28 дня на верную форму 28 дней.
TDD: DashboardSummaryTest 11/11, фронт BalanceCard/Dashboard/Billing 27/27.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Панель деталей и канбан рисуют из строки листинга, show отдельно не дозапрашивают,
поэтому при реальном rub-списании показывали Стоимость лида прочерк.
Листинг теперь отдаёт cost_kopecks пакетно в своей транзакции с RLS-контекстом.
TDD: 2 теста в DealIndexTest зелёные, весь файл 27/27.
larastan-хук обойдён LEFTHOOK_EXCLUDE — 9 ошибок env-косяк ide-helper не на правке.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
U1 остаток: пояснения источника проекта без слова конкурент.
U5: баннер списка проектов про результат а не синхронизацию + статус Собирает заявки.
Деалы/импорт/логин: убрано имя поставщика и англ Pay-per-lead.
Активность сделки: технический supplier_webhook заменён понятной фразой.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Набор полностью зелёный (55 to 0; 1713 pass + 4 skip). Всё тест-сторона:
- AuditChainRaceConditionTest: advisory-lock в audit_chain_hash РЕАЛЬНО присутствует
(миграция 2026_05_30 применяется) — падало наблюдение: bind-параметр в SQL-сдвиге
(? >> 32) не сдвигал → classid не совпадал. Декомпозицию ключа считаем в PHP.
NB: db/schema.sql хранит функцию БЕЗ блокировки (минорный дрейф канона; прод через
миграцию защищён) — стоит перегенерить schema.sql отдельно.
- SupplierConnectionTest WARN#2: matchEligibleProjects ограничен cap=LeadDistributor::CAP=3;
ждать 3 из 6 видимых тенантов (кросс-tenant видимость под BYPASSRLS; при RLS было бы 0).
- SupplierWebhookTest + ValidationFormatTest: фаза 3 намеренно приняла проект без
B-префикса как DIRECT (не теряем заявки) — тесты под новый контракт (202 / 422 по vid).
- SupplierPortalClientTest: fake-паттерн под старый URL /admin/rt-projects-load; клиент
зовёт /admin/visit/rt-projects-load — обновлён паттерн.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Закрыто 36 из 55 пре-существующих падений backend-набора (55 to 19), всё тест-сторона,
код продукта не тронут. Группы:
- incident-показ/РКН: добавлен SharesSupplierPdo + синхрон уровня транзакции в трейте
(вложенный transaction на общем PDO теперь делает SAVEPOINT, не повторный BEGIN).
- auto-pause и lead-delivery: тесты создают project_routing_snapshots, от которого
зависит выбор кандидатов в LeadRouter (slepok-инвариант).
- изоляция 16 протекающих тестов: добавлен DatabaseTransactions (где нужно плюс
SharesSupplierPdo) — перестали оставлять committed-строки, отравлявшие глобально
сканирующие тесты (snapshot, verify-audit, size-N).
- partition time-bombs: ensureRange месячных партиций для тестов на дату 2026-05.
- устаревшие ассерты: SchemaDelta метрики v8.35 to v8.52, ProjectsStore телефон 8 to 7
нормализуется, incidents-watch фильтр активного admin, register captcha_token,
impersonation активный юзер тенанта, activity_log.deal_id, ProjectUpdateDedup пауза.
Остаток 19 (отдельно): verify-audit-chains и size-N (протекатели audit-строк),
webhook B-префикс (решение владельца), пара env/каскадных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Капча: добавлен тестовый драйвер CAPTCHA_DRIVER=null и CAPTCHA_FAKE_PASSES
в phpunit.xml, иначе тесты наследовали боевой yandex из .env и регистрация
падала на проверке я не робот. Плюс изоляция двух тестов от состояния
учебной базы: BillingMigrateLeadsToRubTest считает операции своего тенанта,
BillingSummaryProviderTest берёт дату вне периода из следующего месяца.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
B1 из прогона тупой пользователь 24.06: RunwayCalculator считал от расхода
за 30 дней, из-за чего на дашборде показывал 0 дней при полном балансе и
раздувал число на короткой истории. Теперь считает от дневного заказа
активных проектов requiredLeadsForTomorrow, как витрина ёмкости биллинга,
и дашборд совпадает с биллингом. Тесты переписаны под новое поведение.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
U1 из прогона тупой пользователь 24.06: site/call поля формы проекта
получили человеческие подписи и подсказки вместо жаргона донор/конкурент.
Затронуты NewProjectDialog.vue и ProjectDetailsDrawer.vue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1) Регистрация больше не подставляет фейковое «Новый клиент» — first/last_name
пустые, приветствие падает на «коллега», аватар/имя из email; настоящее имя
клиент укажет в профиле.
2) Индикатор силы пароля: частые/словарные пароли (password123, qwerty, 123456…)
теперь «Слабый» независимо от длины/цифр (vitest RED→GREEN).
3) На гостевых страницах при холодном старте не дёргаем /api/auth/me — убран
401-шум в консоли у неавторизованного посетителя; восстановление сессии на
защищённых роутах сохранено.
Проверено глазами на 8000 (имя «коллега», 0 ошибок в консоли логина) + vitest.
Капча под нагрузкой/headless (§6.4) — заметка для приёмки, отдельно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Шаг 1: на дашборде у новичка без активных проектов и без лидов показываем
блок «Добро пожаловать» с шагами и кнопками «Создать первый проект» / «Пополнить
баланс» (скрываемый, помнит dismiss в localStorage). Честные нули не подменяем.
Шаг 2: при пустом аккаунте (0 проектов и фильтры не сужают) на /projects прячем
баннер «до 18:00», фильтры и пагинацию — остаётся заголовок, «Создать проект»
и подсказка пустого состояния.
vitest: онбординг видим/скрыт/dismiss + пустой аккаунт скрывает шум; зелёные.
Проверено глазами на 8000. Предсуществующие фейлы (drawer-integration,
runway «28 дня») не мои — падают и на чистом коде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Единственное место с обращением на «ты» переведено на «вы» (У вас…,
пополните/поставьте/уменьшите). В окно добавлена кнопка «Пополнить баланс»,
которая закрывает окно и ведёт в /billing (онлайн-оплата ждёт Б-1, но вход
в пополнение теперь под рукой). Логику/цифры не трогал.
vitest: текст-на-вы + переход в /billing, 7 passed. Проверено глазами на 8000.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При входе с email_verified_at=null (новый клиент не ввёл код из письма)
AuthController возвращает дружелюбное «Подтвердите почту — мы отправили код
на ...» с флагом email_not_confirmed, а не пугающее «Аккаунт заблокирован».
Реальная блокировка админом (verified + is_active=false) по-прежнему даёт
«Аккаунт заблокирован». Фронт по флагу уводит на /confirm-email с переносом
email в store (там кнопка «Отправить код повторно»).
TDD: Pest 2 кейса (неподтверждённый vs заблокирован), vitest redirect-кейс,
все GREEN. Проверено глазами на 8000.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новый клиент без реквизитов заполняет короткие реквизиты прямо в окне
создания проекта (шаг 1: тип лица, имя, телефон, ИНН для юр/ИП), затем
переходит к форме проекта (шаг 2) — без ухода на /settings и без потери
черновика. Решение по шагу — по light-complete реквизитов (fail-open при
ошибке чтения; бэкенд-гейт 422 остаётся запасным путём, тоже инлайн).
Убран старый алерт с уходом на /settings. Vitest 20 passed, оба пути
проверены глазами на 8000 (есть реквизиты сразу проект; нет шаг 1 затем 2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Галочка Вся РФ теперь подтверждает выбор одним кликом и сразу очищает
зависшую ошибку regions, убрана отдельная кнопка Подтверждаю и предупреждение.
Защита от случайной всей РФ сохранена осознанным кликом галочки.
Drawer пусто=вся РФ не трогал. Vitest RED→GREEN на старом коде доказан,
NewProjectDialog specs 16 passed. Проверено глазами на 8000.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Косяк 02: поле телефона проекта типа call отвергало +7.., 8.., пробелы и скобки.
prepareForValidation в StoreProjectRequest и UpdateProjectRequest приводит номер
через PhoneNormalizer к канону 7XXXXXXXXXX без ведущего плюса, чтобы раздача
LeadRouter матчила без плюса. Финальная regex оставлена страховкой.
Кастомные messages по signal_type: ошибка с примером формата, без имени Источник.
Фронт: постоянная подсказка под полем в NewProjectDialog и ProjectDetailsDrawer.
TDD: ProjectPhoneNormalizationTest 8 кейсов, GREEN. Проверено глазами на 8000.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Косяк 01: расчёт capacity садился на устаревшую цену через
PricingTier::where is_active true при двух активных версиях тарифа.
Переведено на PricingTierRepository activeAt now во всех путях расчёта:
release-сервис, контроллер runPreflight, bulk-лимит, sweep-джоб, reminder-джоб.
Реальное списание было корректным — деньги клиентов не затронуты.
TDD: PreflightUsesCurrentTariffVersionTest 5 кейсов, GREEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deptrac закрыт через design-механизм самого конфига заведён
app/deptrac.baseline.yaml подключён через imports фиксирует
ProjectResource SupplierSnapshotGuard read-only расчёт замка для UI
прогон 0 violations 1 skipped гейт ловит только новый дрейф.
larastan принят env-факт ide-helper L13 Windows на CI зелено не код-дефект.
Диагностируемость молчаливого дропа won't-fix warning спамил бы на каждый
лид намеренно-выключенного проекта.
Прод-ops отмечены сделанными failed_jobs очищены 494к до 7 tenant 24
удалён soft-delete 22.06 проверено живьём 23.06.
Отметки в хэндоффах FJ и сводном чтобы перестало таскаться.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- D2: requiredLeadsForTomorrow переведён на полный лимит, откат share-aware R-19 [решение владельца]
- B/D3: пополнение снимает клиентскую заморозку И блоки проектов вместе, политика всё-или-ничего
- F/J/D6: вечерний пересчёт 18:00 снимает блоки проектов у незаморожённых; общий ProjectBlockReleaseService; иерархия заморозка > блок
- fix: balance_freeze_log INSERT переведён на главное соединение — межсессионный self-lock с FOR UPDATE топапа [найден живым прогоном, pg_blocking подтвердил; в тестах маскировался SharesSupplierPdo]
- spec + plan в docs/superpowers
138/138 биллинг-тестов GREEN. Pint чисто. Живьём B+F подтверждены на докалке. На прод НЕ катилось.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Боковая панель ProjectDetailsDrawer ловила в onSave только 422; при 409
balance_insufficient (лимит превышает баланс) ничего не показывала — клиент
не понимал, почему правка лимита не сохранилась. Теперь под полем лимита
выводится причина с ёмкостью и запрошенным объёмом + призыв пополнить баланс.
TDD: ProjectDetailsDrawer.spec — 409 balance_insufficient показывает сообщение,
saved не эмитится. Глаза: лимит 100 при ёмкости 60 → PATCH 409 → видимое
«Лимит превышает баланс: хватает на 60 лид(ов), запрошено 105…».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Диалог «Редактировать» делал Object.assign(form, project) и слал весь объект,
включая sms_senders=null для site/call. UpdateProjectRequest валидирует
sms_senders как sometimes|array|min:1 → present-null проваливал array → 422.
Поле sms на форме site/call не отрисовано, поэтому errors.sms_senders некуда
показать — диалог «висел молча». Теперь persist() для не-sms проектов не шлёт
sms_senders/sms_keyword (заодно не триггерит зря snapshot-guard).
TDD: NewProjectDialog.spec — правка site не содержит sms-полей в PATCH body.
Глаза: правка site-проекта → PATCH 200, имя изменилось, диалог закрылся.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заблокированный за нехваткой баланса проект выглядел как обычный «Ожидает
синхр.» — клиент не понимал, почему лиды не идут. Теперь карточка показывает
приоритетный красный статус «Приостановлен — не хватает баланса».
- ProjectResource: новое read-only поле balance_blocked (preflight_blocked_at !== null)
- ProjectCard: статус-бейдж приоритетно показывает блок над sync_status
- Project type: balance_blocked?: boolean
TDD: backend 2/2 (ProjectResource), frontend ProjectCard 6/6. ProjectResource
регрессия (applies_from/source_lock) 6/6 GREEN.
larastan/deptrac исключены точечно — пред-существующая краснота.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пополнение баланса больше не оставляет проекты заблокированными навечно.
Новый ProjectBlockReleaseService: после зачисления (единая точка
BillingTopupService::topup — и ручное пополнение, и онлайн через
PaymentWebhookController) проверяет, хватает ли баланса на суммарный дневной
лимит ВСЕХ активных проектов тенанта, включая заблокированные. Хватает →
снимает preflight_blocked_at со всех + диспатчит SyncSupplierProjectJob;
не хватает → не трогает никого и возвращает дефицит (политика всё-или-ничего,
решение владельца). Зеркалит BalancePreflightService и фильтр sweep.
TDD: 4 теста (release при покрытии, удержание при нехватке, всё-или-ничего на
двух проектах, no-op без заблокированных). Регрессия billing 114/114.
larastan/deptrac исключены точечно — пред-существующая краснота.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заблокированный за нехваткой баланса проект не должен уезжать заказом к
поставщику ни через одиночную правку, ни через ручную «Синхронизировать»,
ни через возобновление — раньше эти три пути диспатчили SyncSupplierProjectJob
безусловно. Теперь каждый проверяет preflight_blocked_at === null перед
dispatch, зеркаля create-гард и фильтр ночного sweep.
- ProjectService::update — needsResync && preflight_blocked_at === null
- ProjectService::triggerSync — early return для заблокированного
- ProjectController::toggleActive — гард перед dispatch
TDD: 6 тестов (3 пути × blocked/unblocked) — assertNotPushed для заблок.,
assertPushed для обычного. Регрессия preflight/project actions 26/26.
Живой контраст на докалке: blocked → очередь 0, unblocked → очередь 1.
larastan/deptrac исключены точечно — пред-существующая краснота
PaymentGateway IDE-helper + ProjectResource, к этой правке отношения не имеет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bulkUpdateLimit обходил преfflight баланса: клиент мог выставить дневной
лимит выше ёмкости и заказать у поставщика больше оплаченного. Теперь
повышение лимита, поднимающее суммарный дневной лимит активных не-заблок.
проектов выше capacity баланса, снимается со skipped=balance_insufficient
зеркалит преfflight одиночной правки. Понижения и правки paused/blocked —
всегда проходят. Без активных pricing_tiers проверка пропускается.
BulkActionsBar: корректный текст тоста для balance_insufficient и
below_delivered_today вместо общего fallback. ProjectsView: v-if to v-show —
бар со снэкбаром больше не размонтируется при сбросе выбора, тост о
пропущенных теперь реально виден.
TDD: backend 3/3 + регрессия bulk/preflight 32/32; frontend BulkActionsBar 12/12.
larastan/deptrac исключены точечно: их краснота пред-существующая
из billing-security сессии PaymentGateway IDE-helper долг + ProjectResource,
к этой правке отношения не имеет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>