Полный комплект под фичу «матч источника по слепку, без потери лидов»: - 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>
19 KiB
План проверки: разблокировка смены источника без потери лидов
Дата: 2026-06-25 Связан с: план 2026-06-25-source-edit-unblock-snapshot-routing.md, разбор швов 2026-06-25-source-edit-block-snapshot-analysis.md. Назначение: как мы убедимся, что переделка работает и ни один лид не потерян, ни одного двойного списания. Три уровня: (1) автотесты по коду, (2) приёмка глазами по факту, (3) денежная приёмка отдельно.
Принцип (read first)
Готово ⟺ выполнены все три:
- 🟢 Код: все автотесты зелёные (Pest + Vitest), регрессия
/regression fullGREEN. - 👁 Глаза: каждый пользовательский сценарий проверен руками на тест-стенде, со скриншотом «до/после».
- 💰 Деньги: баланс клиента и счётчики сходятся до копейки; журнал доставки — без дублей и без пропаж.
Любой красный пункт = NO-GO. Деньги — приоритет: при сомнении в денежном инварианте останавливаемся и зовём владельца.
Раздел 0. Где тестируем (НЕ на боевом)
- Автотесты — локально/CI,
composer test --parallel,npm run test:vue. - Ручная приёмка money-flow — на тест-стенде + имитация поставщика (
app/tests/Support/Imitation/—LeadInjector,SnapshotForge, сценарииScenarioG3_OrphanLeadTestи др.). Имитация позволяет прогнать поток лидов и хвост источника БЕЗ реального поставщика и без риска для боевых денег. - Боевой liderra.ru — только финальная визуальная сверка UI (замок/баннеры) на своём проекте, без денежных операций, и только после GO. Перед любым выкатом — агент
prod-deploy-validator.
⛔ Хвостовой лид по старому источнику и двойное списание нельзя проверять на боевом руками — только имитацией на стенде. Это деньги.
Раздел 1. Автотесты по коду (привязка к задачам плана)
| Эпик / задача | Тест-файл | Что доказывает | Цвет до→после |
|---|---|---|---|
| 0.1 baseline | SourceMatchCharacterizationTest |
лид по источнику доезжает (фиксируем «как было») | 🟢→🟢 |
| 2.1→2.2 ядро | SourceMatchAfterChangeTest |
лид по старому источнику доезжает на след. день после смены | 🔴→🟢 |
| 2.3 группа | SharedSourceUnaffectedTest |
сосед по общему источнику не пострадал | 🟢 |
| 2.4 удаление | DeleteSupplierProjectTailGuardTest |
sp не удаляется пока летит хвост/есть потребитель/есть снимок | 🔴→🟢 |
| 2.5 разблок | SupplierSnapshotGuardTest |
смена источника больше не кидает 422; delete защищён | 🔴→🟢 |
| 2.6 источник из слепка | SyncSourceFromSnapshotTest |
батч заказывает по источнику слепка, не live (окно 18:02→18:05) | 🔴→🟢 |
| 2.7 деньги | NoDoubleChargeOnSourceChangeTest |
webhook+CSV одного лида = одно списание | 🟢→🟢 (страховка) |
| 1.1 / 3.1 / 3.2 UX | ProjectResourceSourceLockTest, ProjectDetailsDrawer.spec.ts |
поля статуса; баннеры/подтверждение; поля не залочены | 🔴→🟢 |
| 4.1 / 4.2 онлайн | OnlineDeferWindowTest, FlushDeferredOnlineSyncTest |
онлайн молчит 18:00→00:00, досыл в 00:05 | 🔴→🟢 |
| 5.1 отчёт | SyncSummaryReportTest |
сводка завершения заливки шлётся | 🔴→🟢 |
Дополнительные тесты под швы из §9b/§9c (добавить в соответствующие задачи):
- sms по
sms_senders[0]+keyword (§9c-Q1, §9b sms):SmsSourceSnapshotMatchTest— лидCaranga+КРЕДИТматчит проект с senderCaranga+ keywordКРЕДИТ; лидCaranga(без keyword) НЕ матчит проект с keyword; коллизия «sender с keyword vs без» разводится верно. - root-domain (§9b):
RootDomainSnapshotMatchTest— лид по корнюcarmoney.ruдоезжает до подписчика на субдоменkrasnoyarsk.carmoney.ru; обратное (лид-субдомен → корневой проект) НЕ матчит (направление сохранено). - DIRECT+sms фикс (§9c-Q5):
DirectSmsNowMatchesTest— DIRECT+sms-лид, который раньше терялся, теперь доезжает (матч по sms_senders). - stale pivot (§9c-Q4):
StalePivotCleanedOnSourceChangeTest— после смены источника старая связь(project_id, old_sp_id)удалена, проект не висит на двух источниках. - no_snapshot (§9b):
NoSnapshotNoSilentLossTest— активный проект без слепка не теряет лид молча (либо слепок гарантирован, либо явная диагностика).
Команда прогона блока: cd app && composer test -- --filter='SourceMatch|SharedSource|DeleteSupplierProjectTailGuard|SupplierSnapshotGuard|SyncSource|NoDoubleCharge|SmsSource|RootDomain|DirectSms|StalePivot|NoSnapshot'
Раздел 2. Приёмка ГЛАЗАМИ по факту (ручные сценарии)
Каждый сценарий: предусловие → шаги (что нажать) → что должен увидеть → скриншот. Скрины складывать как accept-S<N>-<до|после>.png.
S1 — Замка «жди 2 дня» больше нет, есть понятное подтверждение
- Предусловие: активный supplier-связанный проект (как Caranga на скрине владельца).
- Шаги: Проекты → открыть проект → попытаться изменить «Источник — отправители SMS» → Сохранить.
- Видеть: НЕ красный замок «изменить можно будет 27 июня». Вместо — окно подтверждения: «Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. Послезавтра — уже нет. Подтвердите смену.» После подтверждения — тост «✅ Готово. Лиды по новому источнику пойдут со следующего дня.»
- Провал: остался жёсткий замок, или сохранение молча не прошло.
S2 — Количество / регион / дни меняются свободно, с честным объявлением
- Шаги: в том же проекте изменить «Лимит лидов в день» (или регион/дни) → Сохранить.
- Видеть: поля НЕ серые/не заблокированы; после сохранения — объявление «Мы уже ведём сбор на завтра. Изменения вступят в силу с {дата}». Сохранение прошло (200), значение в карточке обновилось.
- Провал: поле заблокировано, или нет объяснения когда вступит.
S3 — Новый проект: понятно, когда пойдут первые лиды
- Шаги: Создать проект → заполнить → Сохранить.
- Видеть: баннер «📣 Проект создан — Лидерра уже ставит его в сбор. Первые лиды по нему пойдут с {дата}».
S4 — Хвостовой лид по старому источнику доезжает (ТОЛЬКО имитация на стенде)
- Предусловие: на стенде проект с источником
Caranga, снят слепок на завтра, сменён источник наNewSender. - Шаги: через
LeadInjectorвлить лид по СТАРОМУCaranga«на следующий день». - Видеть в UI стенда: в проекте появилась сделка по этому лиду (Сделки → новая запись). Лид не потерян.
- Провал: сделки нет (лид улетел в никуда) — это потеря денег, СТОП.
S5 — Отчёт о завершении вечерней заливки приходит
- Шаги: на стенде прогнать вечерний цикл (слепок 18:02 → заказ 18:05) с имитацией.
- Видеть: письмо/экран «Вечерняя заливка завершена в HH:MM — групп обновлено N, создано M, в ручной очереди K, расхождений 0». До 21:00 (успеть вмешаться).
- Провал: «нормальный» прогон прошёл, а отчёта нет (как сейчас).
S6 — Онлайн-правка в окне 18:00→00:00 не уходит поставщику сразу
- Шаги: на стенде выставить время 19:00, режим online, изменить лимит проекта.
- Видеть (по журналу/очереди стенда): правка НЕ ушла поставщику немедленно, легла в отложенную очередь; в 00:05 досыл прошёл.
S7 — Клиент ПОНИМАЕТ правила (уведомления и объяснения)
- Предусловие: обычный клиент в своём кабинете.
- Шаги и что видеть:
- создать проект → колокольчик/баннер «Проект в сборе. Первые лиды с {дата}»;
- изменить лимит после 18:00 → «Изменения вступят в силу с {дата}»;
- сменить источник → «Лиды по старому источнику придут до {дата}, дальше — по новому»;
- поставить на паузу → «Сбор остановится сегодня в 18:00. До {дата} ещё могут прийти уже заказанные лиды — это нормально» (клиент НЕ пугается, что после паузы капают лиды);
- возобновить → «Проект снова в сборе. Лиды пойдут с {дата}».
- Видеть: уведомления в колокольчике + баннеры на экране; тексты понятные, без жаргона, с конкретными датами; тот же текст в баннере и в колокольчике (единый источник).
- Провал: клиент видит изменение, но не понимает когда оно сработает; или после паузы пугается «хвоста»; или разные тексты в баннере и уведомлении.
Раздел 3. ДЕНЕЖНАЯ приёмка (отдельно, главное)
Цель — доказать два инварианта и кодом, и глазами: (И1) ни один лид не потерян. (И2) ни одного двойного списания.
Метод «баланс до/после» (глазами на стенде)
- Записать баланс клиента и счётчики
delivered_today/delivered_in_monthДО прогона (Биллинг → баланс; карточка проекта → счётчики). Скринmoney-before.png. - Прогнать через имитацию ровно N лидов известного состава (часть — смена источника в процессе; часть — webhook+CSV одного лида).
- Записать баланс и счётчики ПОСЛЕ. Скрин
money-after.png. - Сверить вручную:
- списано = N_доставленных × цена_тарифа (ровно, до копейки);
- сделок создано = N_доставленных (Сделки → счётчик);
- нет двух сделок по одному физическому лиду (по телефону+проекту);
delivered_countв слепке сходится с числом сделок.
Что проверяем по каждому шву (деньги)
| Инвариант | Как проверить глазами | Авто-страховка |
|---|---|---|
| И2: webhook+CSV = 1 списание | один лид влить и webhook'ом, и CSV-recovery; в Сделках — одна сделка, баланс упал на одну цену | NoDoubleChargeOnSourceChangeTest |
| И1: хвост доезжает | S4 выше — сделка появилась | SourceMatchAfterChangeTest |
| И1: сосед не потерял | смена источника у клиента A, лид по общему — у клиента B сделка есть | SharedSourceUnaffectedTest |
| Тариф не съехал | после прогона ступень тарифа клиента не «перепрыгнула» из-за лишних списаний | покрыто И2 |
| Счётчики сходятся | live delivered_today = число сделок = delivered_count слепка |
NoSnapshotNoSilentLoss… |
Сверка журналом (после прогона)
- CsvReconcile drift = 0 (нет «лид пришёл, никому не доставлен»).
failed_webhook_jobs— нет новых записей по нашим лидам (нет retry-шторма).- Журнал
supplier_lead_deliveries— по каждому лиду максимум одна строка на tenant.
Раздел 4. Регрессия и нагрузка
- Полная регрессия:
/regression full(Pest --parallel, Larastan, Vitest, Vite build, lychee, gitleaks) — вердикт GREEN. Денежные тесты должны быть внутри зелёного прогона. - Нагрузка (важно — 30k лидов/сутки): на стенде прогнать пиковый поток имитацией, убедиться что новый матч по слепку не просел по времени (матч теперь по индексу
(snapshot_date, signal_type, lower(signal_identifier))+ sms по JSONB — проверить план запросаEXPLAIN). Цель: не медленнее текущего pivot-матча. - RLS-ревью: агент
rls-reviewerна новые таблицы (supplier_deferred_sync) и изменённые запросы — изоляция tenantّов цела.
Раздел 5. GO / NO-GO перед боевым
Чеклист (все ✅ = GO):
- Все автотесты Раздела 1 зелёные.
/regression fullGREEN.- Денежная приёмка (Раздел 3): баланс/счётчики/сделки сходятся до копейки, 0 дублей, 0 пропаж — со скринами.
- Ручные сценарии S1–S6 пройдены, скрины «до/после» собраны.
- Нагрузка: матч не медленнее текущего (EXPLAIN + замер).
rls-reviewer— без замечаний.prod-deploy-validator— вердикт GO.- План отката готов (см. ниже).
Порядок выката (поэтапно, не всё разом):
- Сначала Эпик 2 (ядро) — матч по слепку + отложенное удаление, БЕЗ снятия блокировки UX. На проде блокировка ещё стоит → клиент ничего не замечает, но ядро уже доводит хвост. Наблюдать сутки: drift=0, нет потерь.
- Затем Эпик 4 (онлайн-заморозка) и Эпик 5 (отчёт) — независимы.
- Только убедившись по факту, что хвост доезжает — Эпик 3 (снятие замка + баннеры). Это последним: пока он не выкачен, обещание клиенту не дано.
Откат: Эпик 2 — за флагом матча (slepok vs pivot), чтобы откатить раздачу на старый pivot-путь одним переключателем без релиза. Эпик 3 (UX) откатывается возвратом старого замка. Зафиксировать флаг в спеке 2.0.
Раздел 6. Пострелизный мониторинг (первая неделя)
- Ежедневно: отчёт о вечерней заливке (Эпик 5) — групп/создано/ручная очередь/расхождения.
- CsvReconcile business-drift — держать 0; рост = лиды теряются или дублируются.
- Sentry — нет всплеска ошибок матча/списания.
- Точечно глазами: на 2-3 реальных проектах после смены источника убедиться, что на след. день сделки по новому источнику пошли, а хвост по старому довёлся (по журналу).
- Счётчик двойных списаний — отдельный запрос «два
supplier_lead_deliveries/две сделки по одному телефону+проекту за сутки» = должно быть 0.
Сводка артефактов проверки
- Скрины:
accept-S1..S6-{до|после}.png,money-before.png,money-after.png. - Логи прогона имитации (состав N лидов, ожидаемые списания).
- Вердикт
/regression full,rls-reviewer,prod-deploy-validator. - Итоговый GO/NO-GO с подписью владельца (деньги — его решение).