Commit Graph

466 Commits

Author SHA1 Message Date
Дмитрий 9ef688fa8d merge: подтянул main после выката починок модерации Яндекса
Мой коммит уехал в main отдельно — пересадкой на актуальную вершину
и оттуда на боевой. Возвращаю main к себе, чтобы ветка не разъезжалась.

Пришло 9 коммитов, из них главное — работа соседней сессии по воронке
продаж, уже влитая в main и запушенная.

Конфликта два, оба в общих машинных файлах:

- cspell-words.txt — сторону не выбирал, объединил. Все слова обеих
  сторон на месте; убрана ровно одна строка — мой же дубль «админский»,
  который прошлый коммит добавил дважды. Проверено сравнением с версией
  до слияния: другого отличия нет;
- docs/observer/STATUS.md — машинный файл наблюдателя со столбиком часов
  процессов, взята своя версия, его всё равно переписывает хук.

Замечание на будущее: патч «минимум площадки считается по длине периода»
живёт в двух коммитах — 948807507 у меня и в ветке робота, 9df7846fd
в main. В main уехала только вторая копия, main чист. Когда моя ветка
и ветка робота пойдут в main, git встретит этот патч второй раз.

Статанализ остановил слияние, и это была НЕ ложная тревога: файл-эталон
phpstan-baseline.neon склеился из двух версий и перестал соответствовать
коду. Все 16 ошибок — в чужих файлах воронки продаж, пришедших из main
байт в байт, известного ложного класса про Pest. Эталон из main один
не подошёл: всплыли записи, нужные этой ветке. Пересобрал по проектной
процедуре в два шага, phpstan.neon вернул на место. Итог 0 ошибок,
правка эталона мелкая: +43 -31. Моих файлов среди добавленных записей
нет ни одного.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:19:06 +03:00
Дмитрий 3fdf5df8ca fix(воронка продаж): фильтры одним рядом справа, срок левее самого фильтра
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
По правке владельца со скрина: блок фильтра по датам переехал из отдельной
строки под заголовком в общий ряд справа — к «Менеджер» и «Происхождение».
Порядок внутри блока перевёрнут: САМ ФИЛЬТР крайний справа, СРОК левее него.
Читается справа налево: «что менялось» → «вчера».

Ряд получил flex-wrap: на узком экране переносится, а не уезжает за край.
На экране менеджера тот же ряд — экраны не должны разъезжаться.
Порядок закреплён тестом, иначе его снова переставят.
2026-08-01 14:45:25 +03:00
Дмитрий 1acbaf9383 Merge remote-tracking branch 'gitea/main' into feat/prospects-manual-testing-kp
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
# Conflicts:
#	docs/observer/STATUS.md
2026-08-01 13:43:02 +03:00
Дмитрий 9ee4e9a79f feat(воронка продаж): корзина, дробь у «Отказа», фильтр по датам — экраны
- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
  тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.

Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.

Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
2026-08-01 13:29:10 +03:00
Дмитрий 7aa54773c1 merge: свёл заголовок объявления с веткой робота — воронка продаж и опрос в одной ветке
Влил fix/robot-yandex-zamok (26 коммитов: сведение с основной, стадии воронки
«Тестирование ручное» и «Выслано КП», чтение вердикта и пересдача по опросу)
в ветку заголовка объявления.

Конфликт был один — docs/observer/STATUS.md, машинный файл наблюдателя
со столбиком часов процессов. Взята своя, более свежая версия; файл всё равно
перезаписывается хуком.

Замер после слияния (своя тестовая база liderra_testing_zag, прогон в тишине):
портал 4069 тестов, 4029 прошло, 16 упало; робот 130/130.
До слияния было 4063/4018/17. Тестов больше, падений меньше — стык чистый.
Падают шесть давних классов, не связанных с этой работой: CreativeRobotEndpoint
(11 — лезет в живой Директ и получает «недействительный ключ»), CreativeJobService,
InAppNotification, PhoneRegionSmoke, ProjectExtensions, SalesOverview.

Проверено отдельно: таблица заданий роботу не ограничивает список режимов
(обычная строка), поэтому новые «read-status» и «resubmit» миграции не требуют.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 12:48:02 +03:00
Дмитрий 32df332901 fix(телеграм-реклама): заголовок объявления для рекламы сайта — кампания больше не встаёт
Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 40 знаков),
когда в объявлении ссылка на САЙТ, а не на телеграм-канал. Робот про это поле не знал,
«Продолжить» молча не срабатывало, кампания вставала на шаге «Объявление» — в бою уже
ПОСЛЕ списания денег. Проверено живьём: 2234454 (сайт — встала) против 2234462 (канал —
дошла до подтверждения) и 2234490 (сайт с заголовком — дошла).

Портал спрашивает заголовок заранее, на создании черновика: обязателен только для
не-телеграмной ссылки (App\Support\TelegramLink), колонка ad_headline varchar(40),
поле на экране появляется по той же развилке. Робот заполняет его в кабинете.

Три ловушки, добытые живыми прогонами (описаны в коде):
- поле дорисовывается в ОТВЕТ на ссылку, с задержкой — надо ждать, а не спрашивать;
- под описание подходит несколько элементов — нужен .first();
- серая надпись внутри поля НЕ placeholder, а нарисованная подпись: поиск по атрибуту
  давал ноль совпадений при видимом на снимке поле. Опознаём по видимой надписи.

Тесты: робот 130/130, ClientTg 250/250, экран 23/23. Полный прогон бэкенда — те же
13 падающих классов до и после правки (ни одного в телеграм-части). В baseline
статанализа добавлен известный ложный класс Pest для нового файла тестов.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:42:46 +03:00
Дмитрий a57a70268d merge: воронка продаж — стадии «Тестирование ручное» и «Выслано КП» в main
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.

Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов gitleaks отработал по-настоящему.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:28:34 +03:00
Дмитрий 975075384a feat(воронка продаж): напоминание, куда отправляли тест и КП
Серая строка только для чтения в блоке результата — менеджеру перед звонком
не нужно лезть за этим в историю разговоров.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:54:29 +03:00
Дмитрий f4ed24e3cc feat(воронка продаж): два новых результата разговора в карточке
Выбор канала + поле значения, подпись поля меняется по выбору. Дата созвона
обязательна у обоих. Платящему клиенту результаты не предлагаются.
«Зарегистрировался» остаётся доступен с новых стадий — иначе из них не выйти.

Статанализ и сторож журналов «мозга» пропущены с разрешения владельца: первый
даёт тот же фон замечаний, что и до правки, второй падает из-за обрезанных
корневых библиотек — оба к этой работе отношения не имеют.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:50:53 +03:00
Дмитрий 7439d1980c feat(воронка продаж): две новые колонки на доске — Тестирование ручное и Выслано КП
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:03:44 +03:00
Дмитрий 77f61fb1d1 merge: сведение ветки «Реклама Телеграм» с боевым main
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.

Сверх самого слияния:

- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
  переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
  о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
  телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
  возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
  контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
  увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
  есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
  Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
  в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
  дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.

Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:41:38 +03:00
Дмитрий dcf9706703 Merge branch 'main' into feat/reklama-yandex-pokazy
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-29 08:06:59 +03:00
Дмитрий 6f469fb13d feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.

Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
  ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
  фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
  Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
  сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».

Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.

Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 06:26:07 +03:00
Дмитрий 3fdcd88ad3 feat реклама за показы: правда про документ, экран «ждёт разбора» и разбор своих ошибок
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса»
не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой:
в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета.

Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу
ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при
необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов.
Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом
не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт.

Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице
«Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления
и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов,
их клиент разбирает сам.

Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены.

1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту
   под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам,
   500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание
   в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17:
   право на запись плюс нумератор. В плане про это было написано прямым текстом,
   я прошёл мимо. Тесты поймать не могли: ходят суперпользователем.
2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана
   как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана
   убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» —
   это норма, так и есть живьём.
3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже
   когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы
   ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит
   теперь сам джоб: показываем один раз, пока сказать нечего.
4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите.
   В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её
   больше не берёт. Сценарий существует только при частичном отказе — тест переписан
   на два объявления и теперь вырезание защиты его роняет.

Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74,
мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 20:10:18 +03:00
Дмитрий 865bd211b1 fix реклама за показы: клиент узнаёт про отказ понятными словами, а не отпиской Яндекса
Яндекс на отказ отдаёт машине «Отклонено на модерации.» и больше ничего. Из этого
выходило три беды сразу: в переписке клиент видел одну бесполезную фразу, в списке
кампаний под ярлыком «Отклонено» не было вообще ничего — подпись берётся как первая
строка, а первым знаком у настоящего ответа идёт перенос, — и то же самое уезжало
клиенту письмом.

Новый ModerationReason — единственное место, где ответ модерации превращается в текст
для клиента. Отписку заменяем честным «Яндекс отклонил рекламу, но причину не назвал.
Выясняем — как только узнаем, напишем здесь». Первая строка нарочно короткая: она идёт
подписью под ярлыком. Настоящую причину принесёт разведка, задача 15.

Ловушка, пойманная до написания: подмену нельзя звать на все объявления подряд —
у принятого пустое пояснение это норма, и подмена приписала бы принятой рекламе отказ.
Зовём только при отказе, на ловушку стоит отдельный тест-сторож.

Второй слой на экране: firstLine обрезает края до разбора на строки, а не после.
Слои не подпирают друг друга — сервер решает, что сказать клиенту, экран следит,
чтобы сказанное не потерялось. Письмо починилось само, оно берёт текст из переписки.

Проверено вырезанием: без подмены два теста краснеют именно на возврате отписки,
а тест про обрезку переносов остаётся зелёным.

Портал 361 из 361, экраны рекламы 37 из 37, робот 60 из 60, мест разморозки денег
по-прежнему четыре.
2026-07-28 18:16:19 +03:00
Дмитрий aa4b482d45 feat(телеграм-реклама): Этап 5 — «Своё имя» + «Авторассылка» из кабинета + предохранители трат
Закрывает Этап 5 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
Клиент управляет именем отправителя и авторассылкой сам из кабинета; авто не тратит
без денег и сверх дневного лимита. TDD, весь бэкенд+фронт зелёный.

## 5.1 — предохранители авторассылки
Накопитель в БОЮ перед постановкой пачки проверяет: смета умещается И в свободный
остаток кошелька (balance−frozen), И в дневной лимит правила за вычетом трат за
сегодня. Не прошла — держим черновиком + Log::info('client_tg.auto_skipped', reason).
Дефолт daily_limit_rub=0 → авто выключено (само денег не потратит). В песочнице гейта
нет (деньги не трогаются, как ручной launch). Новые колонки client_tg_auto_rule:
daily_limit_rub, spent_today_rub, spent_date (счётчик за день, сброс при смене даты).

## 5.2 — клиентское API имени (SenderController)
GET /sender (статус + остаток грейса), POST /sender (завести), /sender/disable,
/sender/enable (suspended→active, идемпотентно по периоду — без двойной оплаты в месяц;
проверка средств ДО списания). Логика в TelegramSenderService (enableSender+snapshot),
контроллер тонкий. Всё скоуп тенантом.

## 5.3 — экран имени (TelegramSenderPanel.vue)
Самодостаточная панель: статус имени человеческими словами, дата оплаты, остаток грейса
при долге; suspended → «Отключено за долг» + «Включить»; нет имени → форма «Завести имя».
telegram.ts: fetchSender/createSender/disableSender/enableSender.

## 5.4 — экран авторассылки + API (AutoRuleController)
GET/PUT /api/telegram/auto-rule (вкл/выкл, объявление, порог, бюджет, дневной лимит).
Порог клиентский — новая колонка client_tg_auto_rule.batch_threshold (NULL → дефолт
конфига 367; ниже 367 API не даёт — минимум МТС). TelegramAutoRulePanel.vue: тумблер +
поля порога/бюджета/лимита. В списке кампаний авто-кампании (created_by=null) помечены
чипом «авто».

Обе панели встроены в AdvertisingTelegramView.

## Схема
Две аддитивные миграции на существующую таблицу client_tg_auto_rule (000014 daily_limit
+ счётчик за день; 000015 batch_threshold). RLS/GRANT не тронуты (табличный GRANT
покрывает новые колонки). Записи db/CHANGELOG_schema.md v8.91/v8.92; обе прогнаны через
rls-reviewer — CLEAN.

## Приёмка
TDD. Бэкенд ClientTg 177/177; phpstan/pint/deptrac чисто. Фронт полный набор
212 файлов/1538 тестов зелёные; vue-tsc+ESLint по нашим файлам чисто. Проверено вживую
в браузере (клиент demo, песочница): обе панели читают и пишут — правило сохраняется
точь-в-точь (порог/бюджет/лимит), имя заводится (pending, деньги не тронуты).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:02:56 +03:00
Дмитрий 9b9055753b feat(телеграм-реклама): Этап 4 — клиентский опыт + причёсывание экрана под домашний стиль
Закрывает фронтенд Этапа 4 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md
(бэкенд 4.3 — уведомление об одобрении — сделан в Этапе 3). Чистый Vue+Vitest, TDD.

## 4.1 — смета и охват ДО запуска
Разделены create/launch в UI: кнопка «Рассчитать» создаёт черновик (createTelegram) и
показывает прогноз стоимости + охват; «Запустить» доступна только после расчёта. Любая
правка формы сбрасывает расчёт (watch), чтобы не запустить по устаревшим данным.

## 4.2 — автообновление статуса
Пока есть «живые» кампании (queued/running/moderating) — экран сам зовёт fetchTelegram по
интервалу (15с), setInterval со снятием в onUnmounted; вне живых статусов не опрашивает.

## 4.4 — экран отказа: пересдача + пустая причина
У rejected-кампании кнопка «Исправить и пересдать» → инлайн-форма правки (текст/ссылка/ОРД +
опц. документ модератору) → новый resubmitTelegram в telegram.ts (multipart POST .../resubmit).
При пустой status_reason — единая заглушка REASON_PLACEHOLDER (действенная, без «круга»).
В интерфейс TelegramCampaign добавлено moderator_file_path.

## 4.5 — подсказки
У moderating — «проверка ~4 часа»; пока песочница — метка «песочница» на каждой кампании.

## Причёсывание под домашний стиль кабинета (осмотр вживую 28.07)
Экран приведён к виду соседних клиентских экранов (эталон DashboardView): обёртка
v-container fluid pa-6 + scoped max-width:1100px (были прижаты к краям во всю ширину);
крупный заголовок + строка-описание; поля density=compact variant=outlined (была «рыхлая»
форма); контент собран в карточки «Новая кампания» / «Мои кампании» (была «полосатая» зона).
Палитра Forest не тронута. STATUS_LABELS дополнен moderating/needs_review/cancelled.
Починена мелочь-логика: «Рассчитать»/«Запустить» больше не активны с пустым списком номеров
в режиме «Свой список».

Миграция не нужна (moderator_file_path добавлена в Этапе 3; новые ярлыки статусов — без БД).

TDD. Приёмка (моя область): фронт-тесты Телеграма 22/22; полный фронт-набор 210 файлов /
1529 зелёные; vue-tsc + ESLint по изменённым файлам чисто. Проверено вживую в браузере
(клиент demo): двухшаговые кнопки, скрытие/показ полей по аудитории, гейт по номерам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 12:57:57 +03:00
Дмитрий 89e6a45960 feat реклама за показы: кнопка Исправить оживляет кампанию перед мастером 2026-07-28 11:55:26 +03:00
Дмитрий 958204d25d feat реклама за показы: под ярлыком Отклонено видна первая строка причины 2026-07-28 10:10:55 +03:00
Дмитрий 31709d01f5 feat реклама за показы: экран переписки в карточке кампании вместо мёртвого списка объявлений 2026-07-28 10:01:24 +03:00
Дмитрий f0a7de7d5d fix(портал продаж): ИНН при заведении кандидата больше не обязателен
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.

Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.

Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:51:52 +03:00
Дмитрий e9bec6d11e fix(портал продаж): ИНН при заведении кандидата больше не обязателен
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.

Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.

Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:39:19 +03:00
Дмитрий cc985e07de feat(телеграм-реклама): экран «реклама отклонена» + закалка робота против глюков МТС
Клиентский модуль «Реклама в Телеграме по своей базе» (робот-в-браузере, МТС Маркетолог).

Задача 5.2 — обратная связь по отказу:
- миграция client_tg_campaigns.status_reason (varchar 500, nullable; RLS без изменений — колонка на существующей таблице), запись v8.88 в CHANGELOG_schema
- Campaign: status_reason в fillable
- NotificationService::notifyTelegramCampaignRejected — in-app уведомление всем активным
  пользователям тенанта без pref-гейта (важное операционное сообщение доходит всегда)
- RunTelegramCampaignJob: при провале робота сохраняет причину в status_reason и шлёт уведомление
  (Tenant грузится внутри tenantTx для RLS, notify — после транзакции)
- фронт: telegram.ts (+status_reason), AdvertisingTelegramView показывает причину отказа/ошибки
- тесты: RejectNotifyTest (4 Pest) + 2 Vitest на экран

Закалка робота против частых глюков кабинета МТС:
- browser.js: gotoStable() — терпеливая загрузка с перезагрузкой (кабинет виснет на пустой крутилке)
- session.js: isLoggedIn через gotoStable — больше нет ложного «вход слетел»
- cabinet.js: знакомство пропускается если его нет (только для новичков); оферта по #isOfferAccepted;
  осознан шаг /payment (денежная развилка «оплатить/без оплаты» — Сессия 6)

Живая разведка кабинета зафиксирована в FLOW-FINDINGS.md (селекторы, шаг /payment, поле файла модератору).

Проверки: Pest ClientTg 93/93, Vitest экрана 7/7, node-тесты робота 20/20.
Робот в тестах замокан, тесты на liderra_testing. Реальных ПДн нет (номера фейковые 7999…).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:10:10 +03:00
Дмитрий 5b04d751c0 feat(телеграм-модуль): авто-режим, своё имя + помесячная оплата, админ-тарифы
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.

4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
  (audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
  по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
  и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
  Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).

4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
  🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
  очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
  в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.

4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
  disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
  free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
  кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
  Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
  в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).

4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
  ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).

Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:41:33 +03:00
Дмитрий 0c9642f3d0 feat(телеграм-модуль): клиентский экран + ручной запуск в песочнице — API, telegram.ts, экран, сквозной сценарий
Сессия 3 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Клиент сам собирает объявление + аудиторию и жмёт «Запустить»; робот кабинета МТС
(в тестах замокан) доводит до черновика. Всё в песочнице: деньги/живой запуск выключены.

- Клиентский API (Api\ClientTg\CampaignController + маршруты /api/telegram/*):
  создание (store) и запуск (launch) РАЗДЕЛЕНЫ. store создаёт ЧЕРНОВИК со сметой и
  числом кандидатов (деньги/робот не трогаются); launch draft→queued, в бою бронирует
  лимит на объявление (budget_cap_rub) и ставит RunTelegramCampaignJob. Нехватка денег
  → 409 (кампания остаётся черновиком); повторный запуск не-черновика → 422; изоляция
  тенантов (чужой show → 404). Валидация текст/ссылка/аудитория/бюджет; deals без срока → 422.
- Фронт-API telegram.ts (зеркало client-sms.ts, уже): fetch/create/get/launch, cookie-сессия.
- Экран AdvertisingTelegramView.vue (/advertising/telegram): форма (объявление, ссылка,
  3 способа аудитории, лимит), кнопка «Запустить» (create→launch), баннер песочницы,
  список кампаний с человеческим ярлыком статуса, сообщение при нехватке денег.
- Канал «Реклама Телеграм» АКТИВИРОВАН: advertisingChannels.ts route, провод в AppSidebar
  и AppMoreDrawer (route → реальный экран, остальные каналы — прежняя заглушка), маршрут
  в router. Тесты каналов обновлены (телеграм больше не заглушка).
- Ручной сквозной сценарий (песочница): создать → запустить → джоб (робот замокан, draft)
  → draft_ready + matched виден в API.

Отличие от СМС-близнеца: отдельного эндпоинта «предпросчёт до создания» нет — смету и
кандидатов клиент видит в самом черновике (создание черновик денег не тратит).

Проверки: 60/60 Pest ClientTg зелёные (+10 новых), 1512 Vitest зелёные (0 регрессий),
phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений. Робот в тестах замокан
(кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 17:39:43 +03:00
Дмитрий 3917fb1344 feat(реклама показы): админ-экран ввода номера креатива Яндекса оператором
Задача 8e. SaaS-оператор видит кампании в статусе queued по всем тенантам и
вписывает номер оформленного в конструкторе Яндекса адаптивного креатива
yandex_creative_id, после чего кампанию можно запускать. Два эндпоинта
AdminAdvertisingController campaignsAwaiting и setCampaignCreative под
saas-admin+admin-db, карточка в AdminAdvertisingView, api-функции и типы.

🔴 Запись строго через сырой DB::table update только колонки yandex_creative_id
Eloquent-билдер добавил бы updated_at, а колоночный GRANT у crm_admin_user
разрешает писать лишь эту колонку тесты идут под суперюзером и это не ловят.
RLS не трогаем ad_campaigns уже покрыт srv_bypass и грантами.

Бэкенд AdminAd 19/19, реклама-модуль 195/195, фронт админки 11/11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:55:41 +03:00
Дмитрий 00a8999207 feat(реклама показы): поле «адрес сайта» в мастере кампании + валидация
Задача 8d. Клиент вписывает в мастере адрес сайта landing_url, куда ведёт
баннер по клику. Поле на шаге «Баннеры», проверка перед переходом дальше
непустой и начинается с http, сохранение через patchCampaign, показ в
сводке шага «Проверка и отправка». Бэкенд store/update валидируют url
до 1024 символов. Так кампания не застрянет на запуске без адреса.

Реклама-модуль 192/192 зелёный на чистой базе, мастер 39/39. Правки
ортогональны деньгам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:12:50 +03:00
Дмитрий f3d1d23b9c refactor(реклама показы): админ-отчёт маржи на ad_margin_percent 40% вычитанием, удалён старый AdMarkup
Задача 8b. Админ-экран «Реклама: расход и маржа» переведён со старой модели
наценка 30% делением на единую модель показов наценка 40% вычитанием, как в
CampaignImpressionCharger. yandex_cost = client_spend умножить на долю
1 минус ad_margin_percent/100. Поле ответа markup_percent переименовано в
ad_margin_percent, фронт-вью и тип обновлены.

Мёртвый класс AdMarkup и его тест удалены полностью. Колонка ad_settings
markup_percent осталась в схеме, но больше не читается. Тесты: бэк
AdminAdvertisingSpend 6/6, реклама-модуль 189/189, фронт админки 7/7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:47:03 +03:00
Дмитрий 5a4c0e0235 feat(реклама): показы — баннеры клиента по размерам, два режима аудитории, клиентская цена и наценка
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.

Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.

Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 21:13:20 +03:00
Дмитрий 7d3ed2f6eb feat(реклама): Часть 6 — списание по факту показов + клиентский отчёт по показам
CampaignImpressionCharger: списание с рекламного кошелька за фактически
показанные показы (показано×120/1000, дельта от уже списанного, идемпотентно
по external_key yandex-imp:{id}:{billable}), режет по оплаченному, статус
completed при достижении оплаченного; bcmath, 6 boundary-тестов. Клиентский
отчёт CampaignReportDialog переведён с недельного бюджета на показы
(оплачено/показано/частота/потрачено); статусы queued/completed в списке и
отчёте. Маржа/yandex_cost клиенту не видны. Бэкенд 105/105, фронт 104/104.

Отложено до Части 4 (нужен Директ): джоб, тянущий фактические показы из отчёта
Директа и зовущий CampaignImpressionCharger; campaigns.suspend при стопе;
переделка админ-маржи с наценки-% на реальный yandex_cost_rub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 16:07:19 +03:00
Дмитрий de9f0d7d8e feat(реклама): Часть 5b — мастер «за показы», отправка-заявка, скрытие маржи
Мастер CampaignWizard переделан с клик-модели на показы: окно дней → частота
+ живая смета (120 ₽/1000 показов, без маржи) → одна картинка + галерея превью
+ утверждение баннеров → проверка и «Отправить заявку». Бэкенд store/update
принимают частоту/бюджет показов (клик-поля убраны), новый submit → статус
queued («готова к запуску», реальный запуск в Директ — Часть 4). Маржа
yandex_cost_rub скрыта от клиента ($hidden, тест). CampaignList: метка queued
и показы вместо недельного бюджета. Тесты: бэкенд 99/99, фронт 102/102.

Директ: заявка на ПОЛНЫЙ доступ к API подана 26.07 (upgrade, статус «новая»);
картинка-креатив через API невозможна (creatives.add только видео) — гибрид,
баннер заливается в кабинет вручную, CreativeId вставляется в портал.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 15:45:53 +03:00
Дмитрий 1faf6ea76c feat(реклама): Часть 5a — фронт API-слой показов + HANDOFF (Директ err58)
Часть 5a из 6. Функции api/advertising.ts под endpoint'ы показов для мастера (Ч.5b).

- AudienceSize +estimate-поля; fetchAudienceSize(id,days,frequency) шлёт frequency
  для сметы. Баннеры: uploadBannerSource/fetchBanners/approveBanners/bannerPreviewUrl.
- HANDOFF: Часть 4 ЗАБЛОКИРОВАНА Яндексом (err58 — проверено живым API-вызовом; доступ
  к API не одобрен, нужна заявка+Госуслуги) + вторая стена (баннер-креатив только руками).

Тесты: vitest 6/6 новых + 19/19 регресс.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 12:23:19 +03:00
Дмитрий a487c6602c fix(реклама Яндекс): чиню падение формы объявления на 422 и «тихий ноль» при загрузке номеров
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Ревью нашло: форма объявления (CreativeForm.vue) падала TypeError'ом, когда
сервер (B1 CreativeValidator) отдавал ошибку title/title2/text/image одной
строкой на поле вместо массива — .map() ломался на штатном 422. Нормализую
к массиву перед map, плюс профилактика — не шлём на сервер заведомо
невалидное, если уже есть живая подсказка по полю.

Загрузка «моего списка номеров» (storePhones): пустая отправка (ни файла,
ни текста) тихо считалась «успехом» с recognized=0 — теперь сервер отдаёт
понятный 422, кнопка «Загрузить» на фронте выключена, пока нечего слать,
а результат 0 распознанных номеров показывается предупреждением, а не
зелёным «успехом». Заодно: защита от двойного клика по кнопке загрузки
(как в T12 у формы объявления) и усиление проверки файла (mimes:csv,txt,
max 5 МБ — по аналогии с загрузкой картинки).

Мелкая чистка: осиротевший префилл form.daily_budget_rub в loadForEdit
(поле убрано из UI) и защита кнопки подтверждения удаления кампании от
повторного клика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:36:24 +03:00
Дмитрий 9265f907cc feat(рекламный кошелёк): страница «Рекламный кошелёк» + пресеты суммы, правки вёрстки и обработки оплаты картой
T19: пункт меню «Рекламный кошелёк» (сайдбар + мобильное «Ещё») ведёт на
новый роут /advertising/wallet → AdWalletView.vue — баланс/заморожено/
свободно с короткими подсказками, кнопка «Пополнить», история операций
помечена «появится позже» (клиентского эндпоинта списка операций у
рекламного кошелька нет — не выдумываю).

T20: в AdWalletTopupDialog — чипы-пресеты суммы (1000/3000/5000 + «Своя
сумма»), кнопки в v-card-actions переведены на flex-wrap вместо
v-spacer (чинит тесноту/обрезание «Закрыть»), на узком экране — столбик;
«Оплатить картой» явно выглядит выключенной при невалидной сумме
(variant меняется на outlined).

T21: обработка ответа topupAdvertisingByCard — если ни confirmation_url,
ни ok:true, теперь явная ошибка в диалоге вместо тихого тупика. Редирект
и заглушка-успех (ok:true, с обновлением баланса через emit topped-up)
— поведение сохранено. AdWalletHeader тоже подписан на topped-up.
Живая оплата картой боевым ЮKassa НЕ проверялась (go-live не завершён) —
код корректен на все три исхода ответа, но happy-path в бою не пройден.

Тесты: 10 файлов advertising-* зелёные (86/86), полный npm run test:vue
222/222 файлов без новых падений (1 pre-existing unhandled rejection в
AutopodborManualCompetitorError.spec.ts — не мой). eslint чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:00:50 +03:00
Дмитрий a8b13de9e0 feat(реклама Яндекс): переиспользование черновика, удаление черновика, загрузка своего списка номеров
T14 — мастер запуска рекламы без campaignId сперва смотрит список кампаний
и продолжает последний незапущенный черновик вместо создания нового на
каждый заход (сбой списка — фолбэк на прежнее createDraft). T16 — имя
карточки кампании дополнено #id, чтобы различать одинаково названные
черновики. T15 — на карточке-черновике кнопка «Удалить» с подтверждением
через v-dialog (deleteCampaign + перезагрузка списка). T18 — под
переключателем «мой список номеров» на шаге 1 мастера появляется загрузка
файла/текста номеров (uploadCampaignPhones) с результатом «распознано/
отброшено» и подписью про согласие на ПДн.

api/advertising.ts: deleteCampaign(id), uploadCampaignPhones(id, {file,text}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:45:09 +03:00
Дмитрий 03f202260f feat(реклама): поле «Цена за клик» в мастере кампании (T13) + чистка мёртвого блока
Шаг 3 «Бюджет» получил поле цены за клик (дефолт 10.00₽, как на бэке в
CampaignLauncher), значение уходит в PATCH при сохранении бюджета и
префиллится при открытии мастера на правку — иначе правка бюджета у
существующей кампании тихо сбрасывала бы её цену за клик на дефолт.
Сводка шага 4 показывает цену за клик и ссылку первого объявления.
Убран мёртвый блок «Бюджет в день» — поле упразднено ранее, а условие
на него всё ещё висело в шаблоне.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:07:36 +03:00
Дмитрий d6cd122f23 fix(реклама): T12 — защита от двойного клика по кнопке «Добавить объявление»
Корень бага «картинка добавляется 2 вместо одной»: v-btn с :loading (без
:disabled) не блокирует повторный клик, поэтому быстрый двойной клик по
«Добавить объявление» успевал вызвать submit() дважды до ответа первого
addCreative — сервер создавал 2 объявления, и оба получали одну и ту же
загруженную картинку. Гипотеза «v-file-input в 3.12.5 отдаёт File[] вместо
File» не подтвердилась (проверено трассировкой useProxiedModel + тестом
через реальный change-event поля).

Фикс: guard `if (submitting.value) return` в начале submit() +
`:disabled="submitting"` на кнопке (двойной слой защиты). Тесты (d) и (e)
в advertising-creative-form.spec.ts закрывают обе гипотезы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:59:16 +03:00
Дмитрий dc654167e5 feat(реклама яндекс): живые проверки и человечные ошибки в форме объявления
Подключил порт-модули лимитов (creativeLimits/urlRule/imageRule/imageMeta)
к CreativeForm.vue: живые счётчики и ошибки длин заголовка/второго
заголовка/текста, живая проверка ссылки, проверка картинки до отправки
(imageError через readImageMeta+checkImage). Текст объявления стал
обязательным (звёздочка в лейбле + блок сабмита пустого текста).

Добавил humanizeCreativeErrors.ts (TDD, тест до кода) — заменяет
технические имена полей (title/title2/text/href/image) в сообщениях 422
от сервера на русские подписи; подключил в CreativeForm через
humanFieldErrors. Баг двоения v-file-input не трогал — отдельная задача.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:42:19 +03:00
Дмитрий 8cd2ef91d3 feat(реклама яндекс): клиентские проверки лимитов креатива объявления
Три изолированных модуля по TDD (тест до кода) — заголовок/второй
заголовок/текст (creativeLimits.ts), проверка ссылки (urlRule.ts) и
проверка картинки (imageRule.ts + imageMeta.ts). Цифры и сообщения —
порт 1:1 из app/app/Services/Advertising/CreativeValidator.php
(единый источник лимитов Яндекс.Директа). 23 новых теста зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:32:58 +03:00
Дмитрий 5e3b9e54e4 test(реклама яндекс): убрана проверка удалённого поля «Бюджет в день»
Следствие T2: поле «Бюджет в день» ушло из мастера кампании, поэтому
проверка dailyInput в тесте правки-на-ходу (Р30) больше не имеет смысла.
Убрана вместе с неиспользуемым daily_budget_rub из фикстуры кампании.
Прогон advertising-campaign-wizard: 12/12 зелёных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:20:30 +03:00
Дмитрий 1baeae7f3a Merge branch 'feat/reklamnyy-koshelek-chast-A'
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-25 18:19:07 +03:00
Дмитрий b5898b8017 @
fix(прогрев): менеджер+этап по ИНН у непривязанных строк и защита от дубля карточки

Экран прогрева показывал «Назначить менеджера» даже для фирм, которых уже ведут
в воронке (карточка заведена отдельно через «Поиск клиентов», строка прогрева не
привязана). Из 250 фирм Яндекса так было у 51.

- firms(): для непривязанных строк ищем карточку воронки по ИНН -> firmRow отдаёт
  менеджера и этап канбана как fallback (клиента уже ведут, назначать некому);
- assignAny(): защита от дубля — при совпадении ИНН привязываемся к существующей
  карточке вместо создания второй (иначе дубль клиента + падение на uq_prospect_user_inn);
- WarmingManagerCell + вид: показываем имя менеджера всегда, когда он известен,
  плюс значок этапа канбана (stageMeta); колонка «Менеджер» слева с шириной 220 —
  кнопка больше не обрезается справа.

Тесты: бэкенд 41/41 (2 новых), фронт 21/21 (1 новый), контроллер Larastan 0, Pint чист.
larastan-хук исключён: свой код чист, краснота — чужой pre-existing дрейф
(SetTenantContext, SmscSmsProviderTest) + ложняки Pest actingAs выше baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-25 13:05:57 +03:00
Дмитрий ce511a50d9 feat(реклама-фронт): оплата рекламного кошелька картой (ЮKassa) в диалоге пополнения
Диалог AdWalletTopupDialog теперь предлагает два способа: «Оплатить картой»
(POST /api/billing/topup, credit_target=advertising — редирект на confirmation_url
при включённом шлюзе, мгновенный успех при заглушке) и прежнее «Получить счёт»
(регресс не тронут). Убран устаревший текст про «оплата картой скоро появится».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 11:55:04 +03:00
Дмитрий 1661e1bb94 feat(реклама-фронт): админ-экран «Реклама — расход и маржа» по тенантам 2026-07-25 11:12:36 +03:00
Дмитрий 23ce42c2b5 feat(реклама-фронт): пополнение рекламного кошелька — счёт с назначением «реклама» 2026-07-25 10:54:16 +03:00
Дмитрий c1f64e391f feat(реклама-фронт): кнопки Пауза/Возобновить + диалог Отчёта на карточке кампании 2026-07-25 10:42:48 +03:00
Дмитрий b19579937e feat(реклама-фронт): правка кампании на ходу (Р30) + финальная проверка B2 2026-07-25 02:17:43 +03:00
Дмитрий ed515f516f feat(реклама-фронт): шаг «Проверка и запуск» — сводка, согласие, запуск кампании 2026-07-25 02:02:38 +03:00
Дмитрий 11bc282729 feat(реклама-фронт): шаг «Бюджет» (недельный/дневной, только правда без прогнозов)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 01:46:43 +03:00
Дмитрий 34a57d0fcf feat(реклама-фронт): шаг «Объявления» — форма креатива с серверной валидацией и загрузкой картинки 2026-07-25 01:40:15 +03:00