# План проверки: разблокировка смены источника без потери лидов

**Дата:** 2026-06-25
**Связан с:** план [2026-06-25-source-edit-unblock-snapshot-routing.md](../plans/2026-06-25-source-edit-unblock-snapshot-routing.md), разбор швов [2026-06-25-source-edit-block-snapshot-analysis.md](../findings/2026-06-25-source-edit-block-snapshot-analysis.md).
**Назначение:** как мы убедимся, что переделка работает и **ни один лид не потерян, ни одного двойного списания**. Три уровня: (1) автотесты по коду, (2) приёмка глазами по факту, (3) денежная приёмка отдельно.

---

## Принцип (read first)

Готово ⟺ выполнены **все три**:
1. 🟢 **Код:** все автотесты зелёные (Pest + Vitest), регрессия `/regression full` GREEN.
2. 👁 **Глаза:** каждый пользовательский сценарий проверен руками на тест-стенде, со скриншотом «до/после».
3. 💰 **Деньги:** баланс клиента и счётчики сходятся до копейки; журнал доставки — без дублей и без пропаж.

Любой красный пункт = 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+КРЕДИТ` матчит проект с sender `Caranga` + 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) ни одного двойного списания.**

### Метод «баланс до/после» (глазами на стенде)

1. Записать **баланс клиента** и счётчики `delivered_today`/`delivered_in_month` ДО прогона (Биллинг → баланс; карточка проекта → счётчики). Скрин `money-before.png`.
2. Прогнать через имитацию ровно **N лидов** известного состава (часть — смена источника в процессе; часть — webhook+CSV одного лида).
3. Записать баланс и счётчики ПОСЛЕ. Скрин `money-after.png`.
4. **Сверить вручную:**
   - списано = 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 full` GREEN.
- [ ] Денежная приёмка (Раздел 3): баланс/счётчики/сделки сходятся до копейки, 0 дублей, 0 пропаж — со скринами.
- [ ] Ручные сценарии S1–S6 пройдены, скрины «до/после» собраны.
- [ ] Нагрузка: матч не медленнее текущего (EXPLAIN + замер).
- [ ] `rls-reviewer` — без замечаний.
- [ ] `prod-deploy-validator` — вердикт GO.
- [ ] План отката готов (см. ниже).

**Порядок выката (поэтапно, не всё разом):**
1. Сначала **Эпик 2 (ядро)** — матч по слепку + отложенное удаление, БЕЗ снятия блокировки UX. На проде блокировка ещё стоит → клиент ничего не замечает, но ядро уже доводит хвост. Наблюдать сутки: drift=0, нет потерь.
2. Затем **Эпик 4** (онлайн-заморозка) и **Эпик 5** (отчёт) — независимы.
3. Только убедившись по факту, что хвост доезжает — **Эпик 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 с подписью владельца (деньги — его решение).
