Дашборд: вопросик у 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>
KanbanView: удалён тест клика по убранной кнопке «Новая сделка».
BalanceCapacityIndicator: Vuetify-плагин в mount (v-icon) + формат «1 000 ₽».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрыт хвост из billing-audit: webhook при зачислении пишет id строки ledger
в saas_transactions.balance_transaction_id (жёсткая прослеживаемость). Колонка
BIGINT nullable без FK (balance_transactions партиционирована). schema.sql v8.52
+ миграция 2026_06_22_170000 (guarded) + CHANGELOG. Тест проверяет связку. 115/115.
TopupResult допускает confirmation_url; TopupDialog при нём редиректит на
страницу ЮKassa (через тестируемый redirectTo), иначе прежнее мгновенное
зачисление. BillingView показывает баннер «платёж обрабатывается» при
возврате ?topup=return. Пресеты сумм уже были.
Публичный роут /api/webhook/payment (CSRF-exempt). Cross-tenant поиск через
pgsql_supplier (BYPASSRLS), зачисление под SET LOCAL app.current_tenant_id,
атомарный claim pending->success (идемпотентность), защита от несовпадения
суммы, делегирование зачисления BillingTopupService.
Флаг ВКЛ → создание платежа через OnlineTopupService + confirmation_url;
ВЫКЛ → прежнее мгновенное зачисление. Биндинг PaymentGatewayDriver в
AppServiceProvider. Также мелкая гигиена SystemSettingsHelperTest
(DatabaseTransactions для отката).
ProjectDetailsDrawer: при source_locked поля источника disabled + подсказка
с датой; красный баннер прочих ошибок не тронут. NewProjectDialog: баннер
«первые лиды с DD» (правило слепка 18:00). Тексты согласованы с владельцем.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SupplierSnapshotGuard::lockState (pure, без DB) + ProjectResource отдаёт source_locked/source_unlock_at/source_unlock_projected; ProjectController withCount(supplierProjects). Логика гарда не изменена.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>