# Спека: UX блокировки смены источника проекта + баннер нового проекта

**Дата:** 2026-06-22
**Статус:** дизайн согласован владельцем. **РЕАЛИЗАЦИЯ ОТЛОЖЕНА** — делает другая сессия по команде владельца. **НЕ реализовывать без явного «делай».**
**Тип:** UX-правка (фронт + одно read-only поле бэкенда). Revenue-ядро НЕ трогаем.

---

## 1. Проблема (откуда выросло)

Клиент (омега, проект 135 «Caranga», sms) внёс **ключевое слово** в активный проект — оно **молча не сохранилось**, у поставщика ничего не создалось. Диагноз:

- Правка источника (`sms_keyword`/`sms_senders`/`signal_identifier`) у **активного** supplier-связанного проекта **блокируется** `SupplierSnapshotGuard::assertCanMutateSource` → возвращает 422 `{errors:{project:[…]}}`.
- Форма (`ProjectDetailsDrawer.vue`) **ловит** 422 и **показывает** баннер `errors.project` (строки 148-149), **но** баннер вверху, а поле источника и кнопка «Сохранить» — ниже, **без тоста и без прокрутки к ошибке** → клиент не видит → ощущение «молча не сохранилось».

## 2. Главное решение: ГАРД ОСТАВЛЯЕМ (не смягчать, не сносить)

Рассматривали «смягчение» (разрешить смену источника на лету + баннер-предупреждение). **Отклонено** — это фундаментальная переделка revenue-ядра. Доказательство из кода:

- Раздача ищет проект для входящего лида **только через pivot** — `LeadRouter::matchEligibleProjects` (`app/app/Services/LeadRouter.php:114`):
  `EXISTS (project_supplier_links psl WHERE psl.project_id=snap.project_id AND psl.supplier_project_id=?)`.
- Смена источника **рвёт этот pivot** и удаляет старый supplier_project — `ProjectService::detachOldSourceSupplierProjects` (`app/app/Services/Project/ProjectService.php:159-176`) → `DeleteSupplierProjectJob::dispatch`.
- Поставщик по СТАРОМУ источнику уже заказал и шлёт лиды (~сутки + хвост). После разрыва связи эти лиды **не находят проект → 0 сделок → потеря** (ровно паттерн «726 zero-deal», что видели на проде).

**Вывод:** гард — несущая стена. Он специально форсирует «пауза → grace → смена», чтобы к моменту смены источника **в полёте не было старых лидов**. Снять его = строить переходную раздачу на два источника одновременно (узлы: `detachOldSourceSupplierProjects`, `DeleteSupplierProjectJob`+чистка, `LeadRouter::matchEligibleProjects`, `RouteSupplierLeadJob`+`project_routing_snapshots`, биллинг). **Не делаем.**

## 3. Что делаем (вариант 3 — только UX, безопасно)

### 3.1 Бэкенд — `ProjectResource` отдаёт 2 read-only поля

Источник расчёта — `SupplierSnapshotGuard` (без изменения его логики):

- `source_locked: bool` = `SupplierSnapshotGuard::isProtected($project)`.
- `source_unlock_at: string|null` (ISO8601, МСК):
  - проект **на паузе в grace** → `computeGraceUntil($project->paused_at)` (твёрдая дата);
  - проект **активен** (паузы ещё нет) → **прогноз** `computeGraceUntil(now())` — «если поставить паузу сейчас»;
  - не заблокирован → `null`.
- Фронту отдаём флаг `source_unlock_projected: bool` (true для активного — чтобы показать «если поставите паузу сейчас»).

### 3.2 Фронт — `ProjectDetailsDrawer.vue`

Когда `source_locked = true`:

- поля источника (`signal_identifier` / `sms_senders` / `sms_keyword`) — **disabled**, серые, с замком 🔒;
- под ними — подсказка (тексты §4);
- остальные поля (название, лимит, регионы, дни) — **редактируются как обычно**.

**Красный баннер `errors.project` НЕ убираем** — он остаётся для других ошибок (например, «нельзя удалить — есть сделки»). Меняем поведение **только для источника**: было «дать нажать и упасть» → стало «закрыть поле и подсказать заранее».

## 4. Тексты (согласованы, без жаргона; «Лидерра», не «поставщик»)

Дата подставляется системой; формат «D MMMM», для разблокировки — «после 21:00».

**Проект РАБОТАЕТ (активен)** — поле серое, замок, дата как прогноз:
> 🔒 Чтобы изменить источник, поставьте проект на паузу.
> Лидерра уже собирает по нему лиды. Поставите паузу сейчас — изменить сможете **{дата} после 21:00**.

**Проект НА ПАУЗЕ (идёт grace)** — поле серое, замок, дата твёрдая:
> 🔒 Изменить источник можно будет **{дата} после 21:00**.
> Лидерра ещё получает лиды по старому источнику.

**Пауза, срок прошёл** — поле обычное, белое, без подсказки.

**Поменял источник и сохранил** — тост:
> ✅ Готово. Лиды по новому источнику пойдут со следующего дня.

**Баннер при создании НОВОГО проекта** (вверху формы / после сохранения):
> 📣 **Проект создан — Лидерра уже ставит его в сбор.** Первые лиды по нему пойдут **с {дата}**.

(тёплый альт, на выбор владельца: «🚀 Готово! Проект создан. Лидерра начнёт собирать по нему лиды — первые ждите с {дата}.»)

## 5. Логика дат (та же «до/после порога»)

- **Разблокировка источника** (`computeGraceUntil`): ближайшие 21:00 МСК после паузы (если пауза ≥21:00 → следующий день) **+ 24 ч**.
  - пауза **до 21:00** → разблокировка **завтра после 21:00**;
  - пауза **после 21:00** → разблокировка **послезавтра после 21:00**.
- **Первые лиды нового проекта / вступление правок** (порог 18:00 МСК — внутренний запас до слепка поставщика 21:00; уже используется в `SupplierSnapshotGuard::appliesFrom`):
  - создал/правил **до 18:00** → лиды **с завтра**;
  - **после 18:00** → лиды **с послезавтра**.

## 6. Вне scope (НЕ делать)

- НЕ убирать/смягчать `SupplierSnapshotGuard`.
- НЕ трогать раздачу (`LeadRouter`/`RouteSupplierLeadJob`), `detachOldSourceSupplierProjects`, `DeleteSupplierProjectJob`, `project_routing_snapshots`, биллинг.

## 7. Тесты (при реализации)

- Бэкенд: `ProjectResource` отдаёт `source_locked`/`source_unlock_at`/`source_unlock_projected` корректно в 3 состояниях (активен / пауза-в-grace / пауза-после-grace / нет supplier-связей).
- Фронт: при `source_locked` поля источника disabled + видна подсказка; другие поля редактируемы; красный баннер `errors.project` остаётся живым для прочих ошибок; новый проект показывает баннер-объявление.

## 8. Файлы-якоря

- `app/app/Services/Project/SupplierSnapshotGuard.php` (источник `isProtected`/`computeGraceUntil` — НЕ менять логику).
- `app/app/Http/Resources/ProjectResource.php` (+ 3 поля).
- `app/resources/js/components/projects/ProjectDetailsDrawer.vue` (disable поля + подсказка).
- `app/resources/js/views/projects/NewProjectDialog.vue` (баннер нового проекта).
- Контекст «почему гард»: `app/app/Services/LeadRouter.php:114`, `app/app/Services/Project/ProjectService.php:159-176`, `app/app/Jobs/RouteSupplierLeadJob.php`.
