docs реклама за показы: заход 2 закрыт — кусок переписки готов, ход работ и снимок состояния

This commit is contained in:
Дмитрий
2026-07-28 10:19:40 +03:00
parent fb63994a66
commit 6bc00ba67b
2 changed files with 126 additions and 35 deletions
@@ -606,3 +606,77 @@ v9.07. Чинить не в этой ветке.
догоняющая миграция с `GRANT SELECT` по образцу
`2026_07_27_100100_grant_admin_ad_campaign_banners.php`. 🪤 Без неё админский экран
увидит ноль строк молча, без единой ошибки. Записано в CHANGELOG v9.10.
---
# Заход 2 закрыт 28.07.2026 — кусок 1 «переписка» готов целиком
Клиент теперь видит, за что Яндекс отклонил его рекламу, и отвечает прямо в портале.
## Что сделано, задача за задачей
**Задача 5 — ручка списка сообщений** (`5e5a9c7f`). `GET /api/advertising/campaigns/{id}/messages`
отдаёт ленту своей кампании по порядку. Чужая — 404, без входа — 401.
🪤 Красный был правильный не сразу очевидно: на незнакомый адрес отвечает страница сайта
с кодом 200, поэтому в тестах проверяется содержимое, а не голый `assertOk()`.
**Задача 6 — ответ клиента с документом** (`27367245`). `POST .../messages` принимает
текст и файл (pdf/jpg/png, до 10 МБ) на закрытый диск; `GET .../messages/{id}/file`
отдаёт файл только своему тенанту.
**Задача 7 — экран переписки** (`31709d01`). `CampaignMessages.vue` встал в карточку
кампании **вместо мёртвого списка объявлений**: он читал `ad_campaign_ads` — таблицу
старой модели, которую поток «за показы» не заполняет, поэтому клиент всегда видел
«Объявлений пока нет» рядом с красным ярлыком.
**Задача 8 — подпись под ярлыком** (`958204d2`). `index()` кампаний отдаёт
`moderation_reason`, в списке под чипом «Отклонено» видна первая строка причины.
**Добор** (`fb63994a`). Отдельный тест на строку приёмочного листа «в переписке нет
наценки»: ручка отдаёт строго перечисленные поля.
## Что проверено вырезанием
- Фильтр по тенанту в списке ленты → чужой клиент получил 200 вместо 404. ✅
- Правила `mimes` и `max:10240` → покраснели оба файловых теста. ✅
- Условие «пусто и без файла» → покраснел свой тест. ✅
- Фильтр по тенанту в отдаче файла → покраснел свой тест. ✅
- Перехват упавшей загрузки ленты → покраснел «упавшая загрузка не роняет экран». ✅
- Подпись под ярлыком → покраснел «видна первая строка причины». ✅
- Отдача модели целиком вместо перечня полей → покраснел «в ленте нет наценки». ✅
## Три находки, которых не было в плане
1. 🪤 **План забыл `ensureCsrfCookie()`** в отправке ответа. Все прочие POST'ы модуля
её берут; без неё кнопка «Отправить» молча ломалась бы в браузере при зелёных тестах.
2. 🪤 **Фильтр по тенанту на самом сообщении ничего не сторожит** — его вырезание
оставило всё зелёным, потому что настоящий замок стоит на поиске кампании. Оставлен
вторым ремнём, но доказуемая защита там одна.
3. 🪤 **Диалог отчёта падал, если лента не загрузилась** — поймали существующие тесты
фронта. Теперь упавшая лента пишет строчку и не роняет остальной отчёт.
Заодно вскрылось: наивная отдача модели целиком утекла бы `file_path` — путь файла
на боевом сервере.
## Приёмочный лист замысла §8, кусок 1 — закрыт целиком
| Строка | Чем закрыта |
|---|---|
| Пояснение Яндекса появляется в переписке само | `SyncCampaignModerationJobTest` (заход 1) |
| Текст сохраняется целиком, длиннее 255 | `CampaignMessageServiceTest` |
| Повторный опрос второго сообщения не создаёт | `CampaignMessageServiceTest` |
| Клиент видит свою переписку и не видит чужую | `CampaignMessageEndpointsTest` |
| Ответ с файлом, файл на закрытом диске | `CampaignMessageEndpointsTest` |
| Файл отдаётся только своему тенанту | `CampaignMessageEndpointsTest` |
| Не тот тип или тяжелее 10 МБ — 422, на диск ничего | `CampaignMessageEndpointsTest` |
| Письмо и колокольчик на слова Яндекса | `CampaignMessageNotifyTest` (заход 1) |
| Ярлык «Отклонено» показывает первую строку причины | `advertising-campaign-list.spec.ts` |
| В переписке нет наценки | `CampaignMessageEndpointsTest` |
## Зелёная отметка на конец захода 2, коммит `fb63994a`
- портал: **322/322, 1090 проверок, ~54 с** (было 311/311 — 11 новых тестов);
- фронт: **227 файлов, 1668 тестов** зелёные (единственная посторонняя ошибка —
в чужом `autopodbor`, к этой работе отношения не имеет);
- `npm run type-check` — те же **6 чужих** ошибок, ни одной новой;
- робот: **60/60**;
- вызовов снятия заморозки — **четыре**, пятого не появилось.
@@ -3,27 +3,28 @@
Файл пишется в конце каждого захода. Читать его **первым делом** после компакта: всё,
что нужно следующей сессии, лежит здесь, а не в переписке.
Обновлено: 28.07.2026, конец захода 1.
Обновлено: 28.07.2026, конец захода 2.
---
## Где мы
- Worktree `.claude/worktrees/reklama-pokazy`, ветка `feat/reklama-yandex-pokazy`.
- Последний коммит — **`4ddfa7b0`**.
- **Заход 1 закрыт целиком** (задачи 1–4). Заходы 2, 3, 4 не начинались.
- Последний коммит — **`fb63994a`**.
- **Заходы 1 и 2 закрыты целиком** (задачи 1–8). **Кусок 1 замысла «переписка» готов.**
Заходы 3 и 4 не начинались.
- Никуда не пушили, на боевой не выкатывали, рубильник Директа выключен, в живой кабинет
Яндекса никто не заходил.
Коммиты захода 1:
Коммиты захода 2:
```text
4ddfa7b0 feat реклама за показы: письмо клиенту и колокольчик на ответ Яндекса
70b421e1 feat реклама за показы: джоб модерации кладёт пояснение Яндекса в ленту кампании целиком
83753fc0 feat реклама за показы: сервис ленты сообщений и защита от повторов одной и той же причины
9c332cc2 feat реклама за показы: лента сообщений по кампании — таблица и модель
bae6b95f docs реклама за показы: план работ по отказам модерации — четыре захода с точками компакта
bb39703e docs реклама за показы: замысел — окно передачи между Яндексом и клиентом по отказам модерации
fb63994a test реклама за показы: в ленте не может быть наценки и пути файла на сервере
958204d2 feat реклама за показы: под ярлыком Отклонено видна первая строка причины
31709d01 feat реклама за показы: экран переписки в карточке кампании вместо мёртвого списка объявлений
27367245 feat реклама за показы: ответ клиента с документом и отдача файла только своему тенанту
5e5a9c7f feat реклама за показы: ручка списка сообщений кампании — только своя лента
ffeffe35 docs реклама за показы: заход 1 закрыт — ход работ и снимок состояния
```
Не закоммичено — только три устаревших промта (`chast4-v3`, `chast4-v4`,
@@ -31,7 +32,11 @@ bb39703e docs реклама за показы: замысел — окно пе
## Зелёные числа на этом коммите
- портал: **311/311, 1054 проверки, ~51 с**`cd app && DB_DATABASE=liderra_testing_reklama php artisan test --filter=Advertising`
- портал: **322/322, 1090 проверок, ~54 с**`cd app && DB_DATABASE=liderra_testing_reklama php artisan test --filter=Advertising`
- фронт: **227 файлов, 1668 тестов**`cd app && npx vitest run`
🪤 единственная посторонняя ошибка в выводе — `autopodbor.ts::fetchProposalGroups`,
чужая и давняя, к этой работе отношения не имеет.
- `npm run type-check`**6 чужих** ошибок, ни одной новой. Чужие не чинить.
- робот: **60/60**`cd bots/yandex-creatives && node --test`
- мест снятия заморозки денег — **четыре**: `AdvertisingCampaignController:392`,
`SyncCampaignModerationJob:165`, `PauseCampaignsOnAdStop:48`, `CampaignImpressionCharger:96`.
@@ -39,45 +44,57 @@ bb39703e docs реклама за показы: замысел — окно пе
## Что теперь работает
Яндекс отклонил объявление → джоб опроса модерации кладёт пояснение модератора **целиком**
в ленту кампании (`ad_campaign_messages`) → клиенту уходит письмо и загорается колокольчик.
Повторный обход с тем же текстом второго сообщения не создаёт. Обрезанное до 255 знаков
по-прежнему живёт в `ad_campaign_banners.moderation_reason` — оно для ярлыка.
Круг «Яндекс → клиент → Яндекс» замкнут в одну сторону целиком:
**Клиент ленту пока не видит:** экранов и ручек ещё нет, это заход 2.
1. Яндекс отклоняет объявление;
2. джоб опроса модерации кладёт пояснение модератора **целиком** в `ad_campaign_messages`;
3. клиенту уходит письмо и загорается колокольчик;
4. клиент открывает карточку кампании и **видит переписку**, а в списке кампаний под
красным ярлыком «Отклонено» — первую строку причины;
5. клиент отвечает текстом и может приложить документ (pdf/jpg/png до 10 МБ);
6. документ лежит на закрытом диске и отдаётся только своему тенанту.
## Что дальше — заход 2, задачи 5–8
**Чего пока нет:** документ никуда не едет — робот его не носит (кусок 3), а кнопка
«Исправить» по-прежнему ведёт в тупик (кусок 2, заход 3).
## Что дальше — заход 3, задачи 9–12
По плану `docs/superpowers/plans/2026-07-28-yandex-otkazy-okno-peredachi.md`:
- **Задача 5** — ручка списка сообщений `GET /api/advertising/campaigns/{id}/messages`.
- **Задача 6** — ответ клиента с файлом: `pdf/jpg/png`, до 10 МБ, закрытый диск.
- **Задача 7** — отдача файла только своему тенанту.
- **Задача 8** — экраны: лента `CampaignMessages.vue` в карточке кампании и подпись
под ярлыком «Отклонено» в списке кампаний.
К концу захода 2 кусок 1 замысла закрыт целиком.
- **Задача 9** — `YandexDirectClient::deleteAds()`, удаление отклонённых объявлений.
- **Задача 10** — `CampaignReviveService`: возврат кампании в черновик, обнуление номеров
креатива и объявления у отклонённых баннеров; **номера кампании, группы и сегмента
уцелевают** — на них держится возобновляемый запуск.
- **Задача 11** — ручка «Исправить» и **узкое** исключение в замке правки: по статусу
`rejected`, а не по «есть ли номер кампании», иначе исключение откроет правку
и работающей рекламе.
- **Задача 12** — кнопка «Исправить» на экране.
🔴 **Заход 4 не начинать, пока не снята живая разметка кабинета** (задача 13, вместе
с владельцем). Код по воображаемым экранам даст зелёные тесты и мёртвую программу.
## Свежие грабли этого захода
## Свежие грабли захода 2
- 🪤 **Тест-пустышка про «беду с лентой».** Первая редакция брала одну принятую кампанию
и проходила одинаково с защитой и без неё. Настоящая беда — обрыв разбора кампании
на первом баннере: остальные остаются без вердикта, деньги молча не возвращаются.
Урок прежний: **защиту проверять вырезанием, а не глазами** — иначе тест зелёный,
а защиты нет.
- 🪤 **`crm_admin_user` не получил гранта** на `ad_campaign_messages`. Админского экрана
по ленте нет, поэтому по least privilege грант не выдан. Когда появится список
«ждёт разбора» — нужна догоняющая миграция `GRANT SELECT` по образцу
- 🪤 **План забыл `ensureCsrfCookie()`** в отправке ответа клиента. Все прочие POST'ы
модуля её берут; без неё кнопка «Отправить» молча ломалась бы в браузере при зелёных
тестах. Проверять это на каждой новой мутации фронта.
- 🪤 **Фильтр по тенанту на самом сообщении ничего не сторожит:** его вырезание оставило
всё зелёным, потому что настоящий замок — на поиске кампании. Урок общий: вырезание
показывает не только «защита есть», но и «эта защита ничего не держит».
- 🪤 **Мутация фронта ломает чужие тесты не там, где ждёшь.** Встроенная в диалог отчёта
лента уронила два чужих spec'а: их `vi.mock` не знал новых функций api-модуля.
Диалог теперь переживает упавшую ленту, а специи получили заглушки.
- 🪤 **Наивная отдача модели целиком утекает `file_path`** — путь файла на боевом
сервере. Ручка обязана перечислять поля вручную; тест это ловит.
- 🪤 **`\n` внутри heredoc-питона превращается в настоящий перенос строки** и ломает
`.vue` разбором «Unterminated string constant». Такие строки править инструментом Edit.
- 🪤 `crm_admin_user` гранта на `ad_campaign_messages` **не получил** — когда появится
админский список «ждёт разбора», нужна догоняющая миграция `GRANT SELECT` по образцу
`2026_07_27_100100_grant_admin_ad_campaign_banners.php`, иначе экран увидит ноль
строк молча.
- 🪤 В worktree нет `lefthook` — каждый коммит печатает `Can't find lefthook in PATH`.
Это нормально; gitleaks гоняется руками:
`"…/Документация/bin/gitleaks.exe" protect --staged --no-banner`.
- 🪤 Помощник тестов джоба называется `makeModeratedCampaignWithBanners($adIds, $yandexCampaignId)`
и уже есть в файле — своего писать не нужно. Http-заглушка ставится на `'*/json/v5/ads'`.
## Красные линии, которые действуют всегда