# Чек-лист: замечания по проектам (боевой liderra.ru) — 22.05.2026

> **Процесс заказчика для КАЖДОГО пункта (обязателен):**
>
> 1. **Тест на практике** — воспроизвести на боевом сайте + у поставщика.
> 2. **Чистка №1** — удалить тестовые данные в Лидерре И у поставщика.
> 3. **Фикс** — исправить код (TDD: сначала падающий Pest/Vitest-тест, потом код).
> 4. **Ре-тест** — снова проверить на практике, что починилось.
> 5. **Чистка №2** — снова удалить тестовые данные в Лидерре И у поставщика.
> 6. **Закрыто** — только когда всё выше сделано и проверено.

**Среда:** боевой `liderra.ru` (SSH `ubuntu@111.88.246.137`), боевой поставщик `crm.bp-gr.ru`.
**Важно:** код на боевом ВПЕРЕДИ origin/main (ветки `feat/test-deploy`, `feat/root-domain-auto-link`). Каждый баг проверять на РЕАЛЬНОМ боевом, не только в репо.

---

## ⚠️ Соглашения о безопасности тестирования на боевом

Поставщик — общий и реальный. Тест создаёт реальные проекты у поставщика → риск реальных заказов лидов и списаний. Поэтому:

- **Фиктивные источники только.** Несуществующие, но валидные по формату: домены `qa-liderra-test-NN.ru`, телефоны `7999000NNNN`, SMS-отправитель `QATESTNN`. Никогда не использовать источник реального клиента.
- **Минимальный лимит** (1 лид/день) — даже если заказ уйдёт, объём копеечный.
- **Создал → проверил → сразу почистил.** Не оставлять тестовые проекты «на ночь» (чтобы ночной батч не заказал).
- **Снимок «до».** Перед каждым тестом снять список проектов у поставщика (`listProjects()`), чтобы точно знать, что удалять после.
- **Тестовые клиенты** изолированы (отдельные tenant'ы `qa-test-1..5`), в конце удаляются полностью.
- **Деньги.** Тестовым клиентам не пополнять баланс реально; если списание произойдёт — зафиксировать и откатить.

**Инструменты на боевом (по SSH):**

- Наблюдать поставщика: `php artisan tinker` → `app(\App\Services\Supplier\SupplierPortalClient::class)->listProjects()`.
- Чистить у поставщика: `\App\Jobs\Supplier\DeleteSupplierProjectJob::dispatch([<supplier_project_id>...])` (sync-выполнить через `queue:work --once` или `Bus::dispatchSync`).
- Чистить в Лидерре: удалить `projects`/`supplier_projects`/`project_supplier_links` тестовых клиентов; удалить тестовых tenant+users.
- БД боевого: PostgreSQL `liderra`, supplier-операции под ролью `crm_supplier_worker` (BYPASSRLS), connection `pgsql_supplier`.

---

## ФАЗА 0 — Подготовка (один раз, до пунктов)

- [ ] **0.1 Снять реальное состояние боевого кода.** По SSH сверить `git hash-object` боевых файлов с origin/main и ветками: `SyncSupplierProjectJob.php`, `SyncSupplierProjectsJob.php`, `SupplierQuotaAllocator.php`, `ProjectController.php`, `ProjectService.php`, `ProjectsView.vue`, `ProjectDetailsDrawer.vue`. Зафиксировать в файле «боевая база» (что реально развёрнуто). Узнать `SupplierExportMode` боевого (online/batch).
- [ ] **0.2 Снять снимок поставщика «до всего».** `listProjects()` → сохранить весь список (id, name, limit, status) в `docs/audit/supplier-snapshot-2026-05-22-before.json`. Это эталон чистоты.
- [ ] **0.3 Завести 5 тестовых клиентов** на боевом (`qa-test-1..5`): tenant + 1 user каждому, лимит проектов ≥ 5. Зафиксировать их tenant_id.
- [ ] **0.4 Проверить процедуру чистки на «пустышке».** Создать 1 тестовый проект с фиктивным источником → убедиться, что он появился у поставщика → удалить его и в Лидерре, и у поставщика → `listProjects()` совпал со снимком 0.2. Только после этого процедура чистки считается рабочей.

---

## ГРУППА 1 — Баги синхронизации с поставщиком (один клиент)

### П1 — Приостановка/возобновление не доходит до поставщика (замечание #10)

**Подозрение (по коду):** `toggleActive` ([ProjectController.php:129](app/app/Http/Controllers/Api/ProjectController.php#L129)) и bulk pause/resume ([ProjectService.php:130-131](app/app/Services/Project/ProjectService.php#L130-L131)) не диспатчат sync; `needsResync` ([ProjectService.php:38-43](app/app/Services/Project/ProjectService.php#L38-L43)) не включает `is_active`; DTO всегда `status: 'active'`; ночной джоб берёт только `is_active=true`.

- [ ] **Тест:** создать тест-проект, дождаться синка, увидеть его активным у поставщика → нажать «Приостановить» → проверить `listProjects()`: остался ли «зелёным/активным».
- [ ] **Чистка №1.**
- [ ] **Фикс:** пауза должна сообщать поставщику (status/обнуление лимита); `is_active` → в needsResync + toggleActive/bulk диспатчат sync; для общего источника — пересчёт без paused-доли (см. П7).
- [ ] **Ре-тест:** пауза → у поставщика остановлен/обнулён; возобновление → снова активен.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П2 — Смена источника создаёт новый проект вместо изменения + дубли (замечания #9, #8)

**Подозрение (по коду):** `unique_key` строится из источника ([SupplierProjectGrouping.php:30-45](app/app/Services/Supplier/SupplierProjectGrouping.php#L30-L45)); смена источника → новый ключ → `existingSps` пусто ([SyncSupplierProjectJob.php:126-132](app/app/Jobs/SyncSupplierProjectJob.php#L126-L132)) → create нового; старый supplier-проект и pivot не чистятся.

- [ ] **Тест A (изменение):** создать проект, источник `qa-liderra-test-01.ru`, синк → запомнить supplier-id. Сменить источник на `qa-liderra-test-02.ru` → `listProjects()`: сколько проектов (ожидаем баг = 2).
- [ ] **Тест B (история #8):** по SSH поднять историю реального инцидента с источником `79135191264` (`activity_logs`/`saas_admin_audit_log`/`supplier_sync_logs`) — как создавался и менялся, откуда 2 проекта.
- [ ] **Чистка №1.**
- [ ] **Фикс:** при смене источника — найти supplier-проекты по pivot (а не по новому ключу), обновить/переименовать у поставщика ИЛИ удалить старые + создать новые; почистить осиротевшие. Учесть шаринг (старый источник мог быть у другого клиента — тогда только отвязать).
- [ ] **Ре-тест:** смена источника → у поставщика РОВНО один проект с новым источником, старый убран/переотвязан.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П3 — Смена дня поставки не доходит до поставщика (замечание #2)

**Подозрение:** `delivery_days_mask` в needsResync есть; проверить, реально ли `updateProject`/`saveProjectMultiFlag` шлёт workdays и в каком режиме (online/batch); batch только создаёт каркас без update.

- [ ] **Тест (убрать день):** создать проект со всеми днями, синк → у поставщика дни. Убрать один день → `listProjects()`/деталь проекта у поставщика: изменились ли дни.
- [ ] **Тест (добавить день):** добавить ранее убранный день → проверить у поставщика.
- [ ] **Чистка №1.**
- [ ] **Фикс:** обеспечить передачу workdays поставщику при изменении (в актуальном режиме боевого).
- [ ] **Ре-тест:** убрать день → у поставщика убрался; добавить → добавился.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П4 — Деление лимита B1/B2/B3 (замечание #1)

**Подозрение:** `distributeForPlatform` уже на боевом (`e6beff6`), re-split форсом выполнен, НО «уже-`ok` проекты со старыми ×N батч сам не перечинивает» (ПИЛОТ §2). Возможно остались проекты с дублированным лимитом.

- [ ] **Тест:** найти на боевом проекты, у которых сумма лимитов B1+B2+B3 ≠ заказу (старый ×N). Создать новый тест-проект с лимитом, напр. 30 → проверить у поставщика: 10/10/10 (правильно) или 30/30/30 (баг).
- [ ] **Чистка №1** (для тест-проекта).
- [ ] **Фикс:** если новые делятся правильно — добить force-resync «уже-ok» реальных проектов с ×N (без удаления, in-place); если новые делятся неправильно — починить деление.
- [ ] **Ре-тест:** новый проект делится корректно; реальные ×N перечинены (Σ = заказу).
- [ ] **Чистка №2** (тест-данные; реальные проекты остаются исправленными).
- [ ] **Закрыто.**

### П5 — Убрать один регион из двух → слетают все (замечание #3)

**Подозрение:** drawer шлёт `regions` целиком ([ProjectDetailsDrawer.vue:78-83](app/resources/js/components/projects/ProjectDetailsDrawer.vue#L78-L83)); если пусто → трактуется как «вся РФ». Проверить, что реально уходит при удалении одного из двух регионов и как поставщик это видит.

- [ ] **Тест:** проект с 2 регионами (напр. Москва+Питер), синк → у поставщика 2 региона/tag. Убрать Питер → проверить: остался ли только Питер убран (Москва есть) или слетели оба → «вся РФ».
- [ ] **Чистка №1.**
- [ ] **Фикс:** убирание одного региона оставляет остальные и у нас, и у поставщика.
- [ ] **Ре-тест:** убрать один из двух → второй остаётся.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

---

## ГРУППА 2 — Сценарии 5+ клиентов одновременно (общий источник у поставщика)

> Базовый сетап для всех: ≥3 тест-клиента с ОДНИМ источником `qa-liderra-test-shared.ru`, разные регионы/лимиты/дни. Синхронизировать → у поставщика ОДИН проект (заказ = сумма по формуле). Снять снимок-эталон. После каждого пункта — чистка обоих сторон.

### П6 — Один из группы меняет регион/лимит/дни днём → затирает остальных (сценарии C2, C3)

**Подозрение:** online-синк одного проекта пишет регионы/лимит/дни ТОЛЬКО этого проекта ([SyncSupplierProjectJob.php:112,118,123](app/app/Jobs/SyncSupplierProjectJob.php#L112)), а ночной — union группы. Дневная правка одного клиента стирает данные остальных.

- [ ] **Тест:** 5 клиентов на общем источнике (регионы A,B,C,D,E; лимиты разные). Клиент 1 меняет свой регион среди дня → проверить у поставщика: остался union(A..E) или стал только регион клиента 1.
- [ ] **Чистка №1.**
- [ ] **Фикс:** online-синк одного проекта должен пересчитывать union/заказ по ВСЕЙ группе (как ночной), а не по одному проекту.
- [ ] **Ре-тест:** правка одного клиента → у поставщика union всех сохранён, заказ = сумме.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П7 — Один из группы ставит паузу → заказ не уменьшается на его долю (сценарий C4)

- [ ] **Тест:** 5 клиентов на общем источнике, заказ = X. Клиент 3 ставит паузу → проверить у поставщика: заказ уменьшился на долю клиента 3 или остался X.
- [ ] **Чистка №1.**
- [ ] **Фикс:** пауза участника группы → пересчёт заказа без него (зависит от П1).
- [ ] **Ре-тест:** пауза → заказ пересчитан; возобновление → вернулся.
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П8 — Один из группы удаляет проект → не трогать общий supplier-проект (сценарий C5)

- [ ] **Тест:** 5 клиентов на общем источнике. Клиент 2 удаляет свой проект → проверить: общий supplier-проект жив (4 ещё работают), заказ пересчитан без клиента 2, лиды/списания остальных целы.
- [ ] **Чистка №1.**
- [ ] **Фикс (если нужно):** удаление участника группы не удаляет общий supplier-проект, только отвязывает + пересчитывает заказ.
- [ ] **Ре-тест.**
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П9 — Двое одновременно сохраняют общий источник → гонка (сценарий C6)

- [ ] **Тест:** клиент 1 и клиент 2 (общий источник) почти одновременно жмут «Сохранить» с разными изменениями → проверить: оба изменения учтены или последний затёр первого / дубль у поставщика.
- [ ] **Чистка №1.**
- [ ] **Фикс (если нужно):** блокировка/идемпотентность на уровне группы (advisory-lock по identifier).
- [ ] **Ре-тест.**
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П10 — Двое одновременно создают один новый источник → два проекта вместо одного (сценарий C7, родственно #8)

- [ ] **Тест:** клиент 4 и клиент 5 одновременно создают проект с одним новым источником `qa-liderra-test-race.ru` → проверить `listProjects()`: один supplier-проект или два.
- [ ] **Чистка №1.**
- [ ] **Фикс (если нужно):** идемпотентное создание (advisory-lock/уникальный ключ на identifier).
- [ ] **Ре-тест.**
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

### П11 — Смена источника на занятый другим клиентом → слияние групп (сценарий C8)

- [ ] **Тест:** клиент 1 (источник X) меняет источник на Y, который уже у клиента 2 → проверить: группы слились в одну у поставщика (заказ = сумма) или создался дубль.
- [ ] **Чистка №1.**
- [ ] **Фикс (если нужно):** смена источника на существующий → присоединение к группе (зависит от П2).
- [ ] **Ре-тест.**
- [ ] **Чистка №2.**
- [ ] **Закрыто.**

---

## ГРУППА 3 — Внешний вид и удобство страницы «Проекты»

> UI чистить не нужно (нет данных у поставщика). Цикл: проверил визуально на боевом → фикс → перепроверил.

### П12 — После Сохранить/Приостановить панель и галочка не исчезают (замечание #4)

**Подозрение:** `onDrawerSaved` делает только fetch без снятия выбора ([ProjectsView.vue:147-149](app/resources/js/views/ProjectsView.vue#L147-L149)); `onPause` в drawer не закрывает панель ([ProjectDetailsDrawer.vue:58-61](app/resources/js/components/projects/ProjectDetailsDrawer.vue#L58-L61)).

- [ ] **Тест:** выбрать проект (галочка) → открылась правая панель. Нажать «Сохранить» → панель и галочка остаются? То же для «Приостановить», «Удалить», «Отмена».
- [ ] **Фикс:** после любого из 4 действий — закрыть панель + снять галочку (clearSelection).
- [ ] **Ре-тест:** все 4 кнопки закрывают панель и снимают галочку.
- [ ] **Закрыто.**

### П13 — Отступ от тёмных границ как в канбане (замечание #5)

- [ ] **Тест:** сравнить отступ страницы «Проекты» с канбаном на боевом.
- [ ] **Фикс:** выровнять отступы (CSS `.projects-view`).
- [ ] **Ре-тест:** визуально совпадает.
- [ ] **Закрыто.**

### П14 — Селектор «показывать по 20/50/100/200» (замечание #6)

**Текущее:** бэкенд режет `per_page` до max 100 ([ProjectController.php:70](app/app/Http/Controllers/Api/ProjectController.php#L70)); фронт без селектора (есть только пагинация на боевом).

- [ ] **Тест:** проверить, есть ли выбор количества на боевом (нет).
- [ ] **Фикс:** селектор 20/50/100/200 (как на «Сделках»); поднять серверный лимит до 200.
- [ ] **Ре-тест:** выбор работает, список перезагружается.
- [ ] **Закрыто.**

### П15 — Сортировка по лидам + фильтры регион/дни + дефолт-сортировка (замечание #7)

**Текущее:** бэкенд сортирует жёстко `created_at desc` ([ProjectController.php:71](app/app/Http/Controllers/Api/ProjectController.php#L71)); фильтров регион/дни нет.

- [ ] **Тест:** убедиться, что сортировки/фильтров нет.
- [ ] **Фикс:** сортировка по кол-ву лидов; фильтр по региону; фильтр по дням; дефолт-сортировка по доставленным лидам за текущий день (`delivered_today`).
- [ ] **Ре-тест:** при заходе сразу сортировка по лидам за сегодня; фильтры/сортировки работают.
- [ ] **Закрыто.**

---

## ФАЗА ЗАВЕРШЕНИЯ

- [ ] **Финальная чистка:** удалить всех 5 тест-клиентов и все их проекты (Лидерра + поставщик). `listProjects()` == снимку 0.2.
- [ ] **Снимок поставщика «после»** — сверить с «до»: ни одного лишнего проекта.
- [ ] **Деплой фиксов** на боевой (копированием, по процедуре ПИЛОТ §2: фронт — локальный build, бэк — config:cache + restart queue + reload fpm).
- [ ] **Обновить ПИЛОТ.md** («обнови пилот» от заказчика).
