Заметка для следующей сессии: на ветке fix/supplier-platform-prefix
(origin) лежит spec фикса корневой причины с пустым префиксом name
у проектов на crm.bp-gr.ru. Кода ещё нет — следующий шаг writing-plans.
Также в той же ветке lежит инфра-fix хука extractTestMetrics
(распознавание Vitest passed | N skipped формата).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec для фикса root-cause обнаруженной 26.05.2026 при разборе скриншота
админки поставщика: 11 из первых 12 наших проектов в crm.bp-gr.ru имеют
name без префикса B1_/B2_/B3_, в то время как старые ручные — с префиксом.
Корень в SupplierPortalClient::toPayload() строка 468: name=uniqueKey
без префикса. Допущение портал префиксует сам автоматически (комментарий
2026-05-19, recon Playwright) не подтверждено живым listProjects.
Решения брейншторма (заказчик подтвердил):
- toPayload префиксует name через helper prefixedName():
"B<n>_<uniqueKey>" если platforms содержит ровно 1 элемент,
иначе throw LogicException (инвариант 1 POST = 1 платформа).
- saveProjectMultiFlag реструктуризируется: один POST со всеми
srcrt+srcbl+srcmt -> N последовательных POST'ов, по одному на платформу,
external_id из ответа rt-project-save напрямую.
- updateProject без изменений сигнатуры -- уже вызывается per-platform,
через тот же toPayload автоматически реализует нормализацию на лету
для 11 legacy без префикса.
- partial-failure не откатываем: Laravel job retry создаст возможные
дубли, чистим вручную (флоу отработан 26.05).
- К1 учебник вебмастера НЕ правим в этом скоупе.
- AjaxProjectChannel read-side не трогаем -- 26.05 фикс DIRECT для
legacy продолжает работать естественно.
Tests: unit для toPayload, feature для saveProjectMultiFlag с моком HTTP,
live smoke на боевом через UI Лидерры + tinker listProjects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-fix all three regexes in extractTestMetrics fell through when Vitest
output contained " | N skipped" between "passed" and "(TOTAL)" — so any
test suite with .skip()'ed tests produced sentinel result=fail (false
negative), blocking subsequent git commit.
Two new patterns:
- "Tests N passed | M skipped (TOTAL)"
- "Tests X failed | N passed | M skipped (TOTAL)"
Companion tests in tools/enforce-verify-record.test.mjs (new file matches
TDD-gate basename heuristic) and tools/enforce-verify-before-push.test.mjs.
Verified RED to GREEN: 38/38 tests pass after fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:33:02 +03:00
5 changed files with 247 additions and 1 deletions
**Автор:** controller (Opus 4.7) совместно с заказчиком
**Статус:** approved (брейншторм закрыт, переход к writing-plans)
**Триггер:** заказчик заметил, что в админке поставщика `crm.bp-gr.ru` первые 11 наших проектов имеют названия без префикса `B1_/B2_/B3_`, в то время как старые ручные — с префиксом.
---
## 1. Корневая причина (подтверждено кодом и живым API)
Отправляется голый `uniqueKey` (домен / телефон / sender+keyword). Платформа кодируется отдельными bool-флагами `srcrt` / `srcbl` / `srcmt`. Комментарий 435–437 утверждает: *«портал префиксует "B<n>_" автоматически»*. **Это допущение неверно.** Живой ответ `/admin/visit/rt-projects-load?src=none` для номера `79135191264` (3 записи `id=12742042/43/44`) показал `name="79135191264"` у всех трёх — поставщик сохраняет `name` ровно так, как мы прислали.
Origin allowed assumption: при recon 2026-05-19 разработчик увидел в `listProjects()` имена вида `B1_<key>` и решил, что префиксует портал. Фактически — это были проекты, заведённые **вручную через UI** поставщика (старые `B2_Caranga`, `B3_Caranga`, `B3_EDA-PROMO+скидка`, `B6_78002000010`).
Связанный костыль на read-side: `app/app/Services/Supplier/Channel/AjaxProjectChannel.php` строка 50 — `preg_match('/^(B[123])_/', $name, $m)` → для проектов без префикса возвращает `null`, и фикс 2026-05-26 (commit `0da72778..` цепочка) подставил `DIRECT` в качестве компенсации. Симптом лечили на чтении, корень — на отправке.
---
## 2. Цель и инвариант
**Цель.**В payload `/admin/visit/rt-project-save` поле `name` теперь несёт префиксованную форму `"B<n>_<uniqueKey>"`, где `<n>` — единственная активная площадка в этом POSTе.
**Инвариант.** «Один POST `rt-project-save` = ровно одна платформа.» Это согласовано с явным комментарием в `toPayload()` (строки 430–433); фактический multi-flag в `saveProjectMultiFlag()` инвариант нарушал — приводим в соответствие.
**Поле `content` остаётся равным `uniqueKey`** (без префикса) — на нём поставщик строит свои матчинги номера/домена и read-side в `saveProjectMultiFlag()` уже завязан на него.
---
## 3. Архитектура изменений
Один файл — `app/app/Services/Supplier/SupplierPortalClient.php`. Три точки правок + новый private helper.
'prefixedName requires exactly one platform per payload; got '.count($platforms)
);
}
return$platforms[0].'_'.$dto->uniqueKey;
}
```
Жёсткий throw при нарушении инварианта (Развилка 1 закрыта заказчиком — «громко падать»). Если кто-то в будущем снова попытается послать multi-platform DTO в `toPayload` — упадём с понятным сообщением, не запишем мусор в портал.
### 3.2. `toPayload()` — подключение helper'а
```php
// было:
'name'=>$dto->uniqueKey,
// стало:
'name'=>$this->prefixedName($dto),
```
Остальные поля payload без изменений (`content`, `srcrt/bl/mt`, `tag`, лимиты, регионы, расписание).
**Побочные эффекты улучшения:** больше не нужен `listProjects()` после save (был костылём, поскольку multi-flag POST возвращал id только последнего созданного проекта). Минус один лишний запрос, плюс ID берётся напрямую из ответа.
### 3.4. `updateProject(int $externalId, SupplierProjectDto $dto)` — без изменений сигнатуры
Уже вызывается с per-platform DTO (`SyncSupplierProjectJob.php:307` и `SyncSupplierProjectsJob.php:402`). После правки `toPayload()` он автоматически кладёт префиксованный `name` — реализуется «нормализация на лету» для 11 уже существующих проектов без префикса (при следующем обычном update — лимит/регионы/расписание/статус — их имя на портале приводится к корректному виду без отдельного миграционного прохода).
### 3.5. `saveProject(SupplierProjectDto $dto)` — без изменений
Однопроектный save через тот же `toPayload()` — автоматически получает префикс.
---
## 4. Закрытые развилки
### Развилка 1: «странный» DTO в `toPayload` (0 или 2+ платформ)
**Решение:** throw `\LogicException`. Громко падать лучше, чем тихо записывать мусор. Прецедент — неделя тихого допущения «портал префиксует сам» (зафиксировано в комментарии 19.05.2026, выявлено на скриншоте от заказчика 26.05.2026; этот спек закрывает именно такую ситуацию).
### Развилка 2: partial-failure в `saveProjectMultiFlag`
**Решение:** **ничего не откатывать.** Если POST для B1 прошёл, а для B2 упал — исключение поднимается наверх, Laravel job retry попробует снова → возможны дубли на портале (B1 будет создан второй раз). Это терпимо:
- Сценарий редкий (требует ошибки 500/таймаута поставщика именно между POSTами).
- Дубли видны глазами в админке поставщика, флоу cleanup уже отработан (2026-05-26, 26 пар дублей вычищены скриптом).
- Альтернатива — try/catch + deleteProject уже созданных — добавляет место отказа (само удаление может упасть) и тестов. На редкий кейс — лишний риск.
- Вызов с`platforms=[B1,B2,B3]` → проверяем, что было **ровно 3 POST'а** к `/rt-project-save` (никаких `/rt-projects-load` после).
- Каждый POST содержит правильный `name` (`B1_X`, `B2_X`, `B3_X`) и правильную тройку флагов (один true, два false).
- Возвращаемый массив = `[B1=>id1, B2=>id2, B3=>id3]` в порядке появления.
- Вариант с одной площадкой `platforms=[B2]` → ровно 1 POST.
### 5.3. Живая проверка на боевом (post-deploy smoke)
После деплоя:
1. Через UI Лидерры создать тестовый проект (любой tenant, тестовый домен/телефон).
2. Через tinker на боевом — `SupplierPortalClient::listProjects()` → отфильтровать по `content == <тестовый identifier>`.
3. Убедиться: 3 записи, у каждой `name = "B<n>_<identifier>"`, `src` соответствует префиксу.
4. Удалить тестовый проект через UI Лидерры → убедиться, что у поставщика тоже удалилось.
---
## 6. Деплой
Стандартный для текущей фазы:
1. Ветка `fix/supplier-platform-prefix`.
2. TDD: сначала падающий тест (unit + feature), потом фикс кода.
3. Local Pest + Vitest зелёные.
4. Pre-flight через агент `prod-deploy-validator` → GO/NO-GO.
5. Tar + scp + ssh extract + `php artisan optimize` под www-data + restart queue. **НЕ через `redeploy.sh`** (он не делает git pull). Лог по [memory feedback_environment.md квирк 107].
6. Post-deploy smoke (см. 5.3).
---
## 7. Что НЕ входит в scope
- **Учебник К1 в `memory/project_webmaster.md`** — не правим. Брейншторм К1 на паузе по другому багу (baseline-баг маршрутизатора), вернёмся к К1 в его собственной сессии.
- **Старые 11 проектов без префикса** — не переименовываем ни руками, ни одноразовым скриптом. Нормализуются «на лету» при следующем `updateProject` каждого.
- **`AjaxProjectChannel::preg_match` (read-side)** — не трогаем. Логика «`DIRECT` для проектов без B-префикса» (commit 26.05) продолжает работать для legacy естественно: по мере прихода префиксов через update — доля `DIRECT` падает.
- **Структура `supplier_projects` в нашей БД** — не меняется. Матчинг внутри Лидерры по `external_id`, поле `name` не используется как ключ.
---
## 8. Риски и наблюдения
- **Нагрузка к поставщику.** Создание проекта теперь = 3 POST'а вместо 1. Создание новых проектов — единицы в день, разница ничтожна.
- **B3 transient delay.** При создании B3-площадка иногда появляется с задержкой (фиксировано в `cfe94d91`). Раньше это било внутри multi-flag POSTа; теперь — на конкретном per-platform POSTе, обработка ретраев та же.
- **Параллельность.** `saveProjectMultiFlag` теперь не атомарный (3 POSTа последовательно). Время выполнения метода × 3 — приемлемо, проектов в день мало.
- **Логирование.** Желательно при каждом POSTе писать debug-лог с парой `(platform, identifier)` — упрощает разбор partial-failure. Добавим в реализации, не в спеке.
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.