fdbff6e2d40e36c5abf97dfe7bbde69c05ee967c
39 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
337f6b9897 |
feat телеграм: дата старта показов, признак живости системы и человеческий язык ошибок
Дата старта (решение владельца 06.08.2026). Портал не передаёт кабинету МТС ни одной даты — их ставит кабинет своими умолчаниями. Живой прогон показал: старт оказался ЗАВТРАШНИМ, тогда как экран обещал клиенту показы «7 дней», подразумевая сегодня. Робот читает день начала из ТОЙ ЖЕ строки списка, куда и так ходит за вердиктом — ни одного лишнего захода в кабинет; портал хранит его в client_tg_campaigns.starts_on и показывает клиенту «Показы начнутся 7 августа». Проверено на ЖИВОМ кабинете: три задания подряд вернули startDate 2026-08-07 по кампании МТС 2237821. Мастер перестал молчать о том, что день начала ставит кабинет, а не мы. Ф-2, карточка приёмки Т-Ф4. У кампаний в движении видно «Проверяли 5 минут назад». Считаются только ЗАКОНЧЕННЫЕ проверки, включая неудачные: задание в очереди работой не является, а неудачная проверка — всё равно признак жизни. Именно в такой тишине владелец 36 часов не знал, что робот вообще не может войти в кабинет. Ф-3. Имена полей в ошибках формы по-русски: «Лимит на объявление не может быть меньше 1 ₽» вместо «Поле budget cap rub должно быть не меньше 1». Серая кнопка «Запустить» называет причину и шаг, куда вернуться, а не гаснет молча. Карточки Т-Р3 и Т-Р4 закрыты тестами (живьём не воспроизвести). Попутно найдено: обе защиты, стерегущие ЕДИНСТВЕННОЕ место траты живых денег роботом, были без единого теста. Сторожа доказаны вырезанием. Тесты: ClientTg 378 зелёных, экраны 2158, робот 197. Статанализ 0, стиль 0, типы чисто. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f83b845658 |
fix телеграм: экран разбора показывал смету вместо живых денег
Поймано на боевых данных 06.08.2026, ДО первого нажатия кнопки. В списке застрявших оказались три кампании, а не одна — и у двух из них заморозку уже отпустили, денег за ними не осталось. Экран же показывал смету и подписывал её «Заморожено у клиента: 268,80 ₽». Владелец нажал бы «вернуть деньги», не вернулось бы ничего, а он считал бы, что вернул. Обещать возврат того, чего нет, — то же враньё, что зелёная галочка над невыполненной работой. Теперь в списке идёт живая заморозка, посчитанная одним запросом на весь список. Когда её нет — так и написано, и кнопка меняет обещание на «просто закрыть кампанию». Отдельно предупреждаем, когда списание пойдёт прямо с баланса клиента, а не из отложенного: у кампании №10 на боевом ровно этот случай — кабинету уплачено 180 ₽, а заморозки уже нет. Проверено: модуль 355 тестов, экраны 16 тестов, статанализ 0, типы чисты. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
13ce301ab3 |
feat телеграм: разбор застрявших кампаний и человеческий язык отказов
Кампания, брошенная роботом на полпути, попадала в needs_review или draft_ready — статусы, из которых не вело ни одного перехода. Замороженные деньги клиента запирались навсегда, снять их мог только программист правкой боевой базы. В бою 06.08.2026 так заперло 268,80 ₽ по кампании №14. Теперь в админке есть карточка «Застрявшие кампании»: владелец видит номер кампании в кабинете МТС, запертую сумму и уже уплаченную МТС сумму — и решает сам. Кнопка «списать по факту» показывается ТОЛЬКО когда МТС уже уплачено; иначе списывать было бы нечего, кроме сметы — ровно та беда, ради которой заморозку и заводили. Заодно: клиенту больше не показывают внутренние адреса и команды запуска — причина отказа переводится на человеческий язык. Портал перестал принимать пустой отчёт робота как успешный. Проверено: модуль 354 теста, экраны 13 тестов, статанализ 0 ошибок. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f70527c3df |
fix,деньги: телеграм-реклама переведена на рекламный кошелёк с заморозкой — робот наконец платит МТС
🔴 Найдено чтением кода, а не по памяти: робот НИКОГДА не оплачивал кампанию. finalize доводил боевой запуск до кассы кабинета и уходил (launched:false, stoppedAt:'payment'), денежные кнопки ему были запрещены наглухо, а «реальная оплата» отложена на «Сессию 6», которой не случилось. Песочницу при этом выключили 02.08 в 07:40. Итог на бою: клиент жал «Запустить» → у него списывалась ВСЯ смета по размеру списка → в кабинете оставался неоплаченный черновик → на модерацию он не уходил → реклама не показывалась ни разу → деньги не возвращались никогда (статус draft_ready терминальный, возврата не имеет). То есть беда была не «клиент переплачивает разницу», как записали накануне, а «клиент платит сто процентов ни за что». Слепки установленного у владельца робота совпали с веткой до буквы — на боевой машине тот же код. Владелец решил: боевой не трогать (стоит как стоит), роботу денежную кнопку разрешить, но с потолком. ── Робот теперь платит ──────────────────────────────────────────────────────── submitWithPayment на шаге /payment: сперва ЧИТАЕТ сумму к оплате, потом гонит её через гейт против меньшего из двух потолков (лимит кампании и общий потолок робота), и только потом ищет кнопку и жмёт. Сумма не прочиталась — не платим: не знаем, что списываем. Не ушли со /payment после клика — падаем громко, портал по отказу отпустит заморозку. Общий чёрный список денежных кнопок НЕ ослаблен: та же кнопка остаётся запретной для всех прочих путей, включая пересдачу. Разрешение точечное. Прочитанная сумма — это НАШИ расходы у МТС; она едет в портал полем actualCostRub, которое до сих пор было пустой заготовкой. ── Кошелёк вместо общего баланса ────────────────────────────────────────────── Порядок зеркалит сам МТС (билинг снят живьём 27–28.07, FLOW-FINDINGS «Задача 2.0»: кабинет резервирует сумму, окончательно списывает по факту показов, остаток возвращает): запуск → морозим смету на ad_wallets (канал telegram) робот заплатил → фактическая сумма легла в кампанию (mts_cost_rub, v9.66) модерация «да» → списываем по факту × наценка, остаток отпускаем модерация «нет» → отпускаем всё, ни рубля не списано сбой до кабинета → отпускаем всё Заморозка была убрана 29.07 намеренно — тогда рассуждали «сумма известна в момент запуска, морозить нечего». Рассуждение верно ровно до вопроса владельца: сумма известна, а сколько человек из списка вообще есть в телеграме — нет. Факта нет, а модерация одобрила — НЕ списываем ничего и кричим в журнал. Списать «по оценке» значило бы вернуть ровно ту беду, ради которой всё и делалось. Идемпотентность больше не самодельная: бронь уникальна по кампании, списание — по ключу события. Прежнее «сальдо проводок» стало не нужно, CampaignChargeServiceTest удалён — его предмет (charge/refund по общему балансу) больше не существует, замена KoshelekTelegramaTest. ── Экран ────────────────────────────────────────────────────────────────────── Карточка денег показывала общий баланс портала. После переезда это стало прямым враньём: клиент видел бы «денег хватает» там, где запуск отвечает 409. Теперь ручка отдаёт СВОБОДНЫЕ деньги кошелька (баланс минус заморозка) и заморозку отдельной графой, а кнопка пополнения ведёт в рекламный кошелёк — прежняя клала бы деньги в общий баланс, и запустить рекламу всё равно было бы нельзя. ── Проверено вырезанием, а не только зелёным ───────────────────────────────── - убрал запрет «сумма не прочитана» — покраснели 2 датчика оплаты; - вернул списание по смете вместо факта — датчик поймал 315 ₽ там, где должно быть 210 ₽. Замеры: телеграм-модуль 330/330, вместе с рекламой и СМС 1078/1078, экраны 251 файл / 2033 теста / 0 падений, робот 160/160, статанализ 0, типы 0, формат чисто. Сторож денег под боевой ролью переписан: класс «тихий ноль» закрылся сам — AdWalletService ставит контекст клиента сам, а не надеется на вызывающего. 🔴 На боевой НЕ выкачено. Осталось открытым: показать клиенту строкой «заморожено / списано по факту / возвращено» на карточке кампании; пересдача по-прежнему шлёт «без оплаты» (кампания вернётся на модерацию неоплаченной); числа 0,720 и 0,816 за показ с медиа так и не замерены живьём. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f2ac3f4c7f |
fix,телеграм: подсказка обещала цену вдвое ниже настоящей + образец файла для базы номеров
Приёмка владельца на боевом, два замечания из трёх (третье — про рекламный кошелёк и заморозку — отложено, кусок большой). 1. Подсказка «?» у поля медиа обещала «600 ₽ за тысячу с картинкой, 680 ₽ с видео». Клиент платит 1008 и 1142,40 ₽. Числа были вбиты в текст руками и протухли в ту минуту, когда миграция client_tg_cena_po_media поменяла тариф. Лечение в корень, а не подстановкой верных чисел: цену называет тот, кто её считает. Ручка оценки отдаёт ceny_za_tysyachu по каждому виду медиа (себестоимость × наценка), подсказка собирается из них функцией podskazkaProMedia. Пока сервер не ответил — текст без единой цифры: подставить «примерные» числа значило бы вернуть ровно эту беду. Прайс держим отдельно от охвата: охват на ошибке гасим (устаревший хуже никакого), а цены от настроек аудитории не зависят — иначе подсказка мигала бы на каждой опечатке в поле. 2. Замечание дословно: «нету скачать файл с примером как надо заполнить для нас файл». Кнопка «Скачать образец» рядом с полем загрузки — подпись клиент читает уже ПОСЛЕ того, как файл отклонили. Пять строк, написания разные (с плюсом, с восьмёркой, со скобками), номера синтетические 7999. Заголовка-строки в образце намеренно нет: разборщик нормализует первый столбец КАЖДОЙ строки, и слово «Телефон» попало бы в «не похоже на номер» — наш собственный образец показал бы клиенту ошибку. Проверено вырезанием, а не только зелёным: - вернул в подсказку вбитые 600/680 — покраснели 4 датчика, включая тот, что прямо запрещает эти два числа; - вставил в образец строку-заголовок — покраснели 3. Полный прогон поймал две мои же поломки, обе настоящие: - значок mdi-file-download-outline на новой кнопке ОТСУТСТВОВАЛ в карте Lucide — на экране стал бы вопросом в кружке. Поймал сторож значков, у которого вчера опустошили список поблажек. Добавлен (Download — точного «файла со стрелкой» в Lucide нет); - датчик подсказок ждал PODSKAZKI.media строкой, а её больше нет. Замеры: экраны 245 файлов / 1870 тестов / 0 падений; телеграм-модуль 327 / 0; статанализ 0; формат чисто; типы 6 — все в чужих файлах, столько же было до; сборка 3,80 с. Счётчик в phpstan-baseline сдвинут 13→15 (новые тесты на Pest), diff проверен глазами: изменилась ровно эта строка. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
00cc072e2f |
feat,телеграм-реклама: «Моя база номеров» заработала — пункт больше не ведёт в никуда
Это снимает запрет на выкат телеграм-рекламы. Пункт «Моя база номеров» стоял в выборе аудитории с 27.07, а писать в таблицу client_tg_contacts не умела НИ ОДНА строка кода: ни экрана загрузки, ни серверной ручки, ни переноса из сделок. У любого клиента пункт всегда показывал ноль. Клиент выбрал бы «свою базу», увидел пустоту и решил, что портал потерял его клиентов. Песочница выключена, деньги живые — катить в таком виде было нельзя. Читающая половина при этом была готова с самого начала: TelegramAudienceService умеет и нормализацию, и схлопывание дублей, и вычитание стоп-листа. Не хватало только записи, поэтому работа вышла куда меньше, чем казалось. Сервер: - TelegramBazaService — разбор файла, замена базы, очистка. Номер берётся по одному в строке либо первым столбцом таблицы, разделители запятая, точка с запятой, табуляция. Мусорные строки не роняют разбор, а считаются отдельно. - три ручки: GET, POST и DELETE /api/telegram/contacts. - потолок 200 000 номеров и 10 МБ на файл; обрыв по потолку не молчит, а возвращается признаком. Экран, в шаге «Кому показываем»: - сколько номеров в базе, заливка файла, очистка; - итог заливки целиком: принято, повторов, не похоже на номер. «Принято 1200» без остального читалось бы как «файл зашёл полностью»; - после заливки счётчик охвата пересчитывается сам. Решения, которые стоит знать: - заливка ЗАМЕНЯЕТ базу, а не добавляет. Так предсказуемее: клиент держит базу у себя и заливает заново. Подмешивание копило бы номера, от которых он не смог бы избавиться — построчного удаления в интерфейсе нет. На экране это написано до нажатия, молчаливой потери нет. - файл без единого годного номера отклоняется, старая база остаётся цела. Иначе клиент залил бы файл не того формата и потерял всё. - замена сделана удалением и вставкой, а не upsert: право UPDATE на таблице не выдано, upsert упёрся бы в это на бою и молча правил бы ноль строк. - наружу отдаём только счётчики. Номера — персональные данные, экрану они не нужны и в ответах не появляются. Приёмка: 12 тестов сервера и 9 тестов экрана, все до кода и все красные по верной причине — сервер отвечал 405, блока на экране не было. Замеры: телеграм-модуль 325 тестов 0 падений, экраны 244 файла 1855 тестов 0 падений, статанализ 0, форматтер чисто. Проверка типов 6 ошибок, все чужие, столько же было до работы. Для выката, проверить на бою: право USAGE на счётчике client_tg_contacts_id_seq. Замерил в тестовой базе — счётчик без права, но ровно так же выглядит и счётчик client_tg_campaigns_id_seq, в который портал на бою пишет. То есть новых прав эта работа не требует, но проверка дешёвая, а пропущенный grant на бою даёт отказ, которого на dev не видно. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3864f6f11a |
feat,телеграм-реклама: цена показа по виду медиа вместо ступеней по объёму
Мы продавали дешевле, чем покупали. Тариф давал скидку за объём — 0.45 → 0.36 ₽ за показ, — а у МТС такой скидки нет: прайс кабинета, стр. 13, берёт 0,48 ₽ за показ плоско. Скидку давали мы, а нам её не давал никто: на объёме от 50 000 наценка 1.40 превращалась в 5%. Второе. Кабинет показывает CPM БЕЗ НДС — колонка списка так и названа. Счёт кампании 2231134 сошёлся: 420 × 400 ₽/1000 × 1,2 = 201,60 ₽. Мы ИП на УСН, НДС не возмещается, это расход. Себестоимость с НДС: 0.480 без медиа, 0.720 с картинкой, 0.816 с видео. Третье. Вид медиа до расчёта не доходил вовсе — объявление с видео продавалось по цене объявления без картинки. На видео уходили в минус до 176 ₽ с тысячи, и портал нигде свою цену с ценой МТС не сравнивал. Песочница выключена с 02.08 — деньги живые. Что сделано: - миграция client_tg_cena_po_media: min_qty → media_kind, точность цены 6,2 → 6,3, три строки вместо пяти ступеней, media_kind на кампании и авто-правиле; - вид медиа определяется по СОДЕРЖИМОМУ файла, а не по расширению имени; - экран админки переделан: три фиксированные строки по видам медиа, рядом цена за тысячу для сверки с кабинетом, добавлять и удалять нечего; - запись v9.65 в журнале схемы. Замеры этой смены: телеграм-модуль 313 тестов 0 падений, экраны 242 файла 1841 тест 0 падений, статанализ 0, форматтер чисто. Проверка типов — 6 ошибок, все в чужих файлах, были до нас. Тест экрана тарифов проверен вырезанием, а не только зелёным: сломал передачу media_kind — красный; сломал расчёт цены за тысячу — красный. Осталось незакрытым: числа 0.720 и 0.816 — вывод по правилу «кабинет пишет без НДС», а не замер. Счёт кампании с картинкой живьём не снимали, шаг «Стоимость» недостижим без загрузки живых номеров. Замерите — правится одной строкой. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
76cb44b91d |
feat(телеграм-реклама): пачка 4б — мастер шагов, живой счётчик охвата и серверная ручка оценки
Замечание владельца: «мастер шагов вместо простыни» и «живой счётчик охвата вместо кнопки Рассчитать». Форма была одна длинная, охват узнавался только по нажатию кнопки. Экран: - мастер из четырёх шагов: Кому показываем → Объявление → Деньги → Проверка; - живой счётчик охвата на первом шаге, пересчитывается сам при смене источника людей, периода или списка номеров (с задержкой, чтобы не дёргать сервер на каждую букву); - кнопки «Рассчитать» больше нет: «Запустить» создаёт кампанию, прикладывает картинку и отправляет в кабинет одним нажатием; - на шаге «Деньги» рядом смета и остаток баланса — нехватка денег видна ДО запуска, а не отказом после; - сводка на последнем шаге повторяет всё выбранное. Сервер — новая ручка POST /api/telegram/campaigns/estimate: 🔴 Живой счётчик обязан считать на каждое изменение поля, а охват до сих пор возвращала только POST /campaigns — и она СОЗДАЁТ черновик в базе. Через неё счётчик наплодил бы десятки кампаний-призраков в списке клиента. Новая ручка только читает: ни записи, ни денег, ни робота. На это стоит отдельный тест. Ручка считает ТЕМ ЖЕ кодом, что и создание кампании (общее тело выборки из сделок вынесено на оба входа TelegramAudienceService). Отдельный тест сверяет два ответа: разойдись они — клиент видел бы на экране одно число, а платил по другому. 🔴 Запуск запрещён, когда людей меньше 367 — правило площадки, не наше. Тот же предохранитель стоит на сервере (AudienceGateTest); на экране он объясняет заранее, вместо отказа после оплаты. Новых прав в базе ручка не требует: читает строго то же, что уже читает создание кампании, и не пишет никуда. Тесты: +EstimateApiTest (11 на PHP), +telegram-master-shagov (27 на экранах). 14 прежних проверок формы переехали в набор мастера вместе с кодом — сперва переписаны на новом месте и там проверены, потом убраны со старого. Устарели по смыслу только две («Рассчитать создаёт черновик», «правка сбрасывает смету»): считать вручную больше нечего, устареть расчёту негде. Замер: экраны 240 файлов, 1829 тестов, 0 падений (было 239/1813); телеграм- модуль на PHP 292 теста, 0 падений (было 281); статанализ 0 (базовая линия пересобрана — добавлен только новый Pest-файл, удалений нет); типы 6 чужих ошибок вместо 7; pint чист. На боевой НЕ выкачено. Глазами не принято — приёмка в пачке 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7922bffb6d |
feat(телеграм-реклама): заголовок и картинка в авто-режиме + честная проверка медиа по требованиям МТС
Пачка 1 замечаний владельца о едином виде рекламных экранов. Песочница выключена 02.08.2026 — каждая дыра ниже стоила живых денег. 1. Заголовок объявления в АВТО-правиле. Кабинет МТС требует его для рекламы сайта; утром 02.08 поле довезли до разовой формы, а в авто его не было вовсе — накопитель создавал кампанию без ad_headline, и она сгорела бы в кабинете уже после списания. Колонка client_tg_auto_rule.ad_headline, приём в контроллере (обязателен только при включённом авто и не-телеграмной ссылке), перенос в кампанию, поле на экране. 2. Картинка/видео в авто-правиле. Колонка media_path, отдельная загрузка POST /api/telegram/auto-rule/media, перенос в кампанию, поле на экране. 3. Проверка медиа переписана по настоящим требованиям кабинета, снятым глазами 02.08. Было mimes:png,jpg,jpeg,gif,mp4|max:51200 — врало по пяти пунктам: принимало GIF, пропускало вдвое больший вес, не смотрело пиксели и длительность, зря отказывало в mov/webm. Стало правило App\Rules\ClientTg\MtsMedia: картинка JPEG/PNG до 25 МБ и 640x360...5120x2880, видео до 20 МБ, 3-55 секунд, от 640x360. Формат картинки определяется по содержимому, длительность и кадр видео читает App\Support\Mp4Probe из контейнера — без внешних программ. Отказ человеческий: «Картинка слишком маленькая: 300x200. Нужна не меньше 640x360». 4. Цена показа с медиа — 600 руб. с картинкой, 680 с видео — теперь видна ДО загрузки файла, на обоих экранах. Сверх плана, найдено по дороге: 5. Предохранитель накопителя: правило, сохранённое до 02.08 со ссылкой на сайт и пустым заголовком, всё равно ушло бы в кабинет. Теперь такая пачка держится черновиком, в журнал пишется причина no_headline. 6. Отказ сервера доходит до клиента его словами. Оба экрана глушили ответ общей фразой «Не удалось рассчитать кампанию», и человек не понимал, что не так с файлом. Границы честности: длительность и размер кадра читаются только у mp4/mov/m4v; у webm/mkv/mpeg/wmv проверяются формат и вес — так и записано в Mp4Probe. Проверено: 281 тест телеграм-модуля, 1753 фронтенд-теста, статанализ 0, Pint чист. Глазами НЕ принимали — приёмка живьём в пачке 5. На боевой не выкачено. Журнал схемы: v9.64 — номер взят как максимум по всем веткам плюс один, чтобы не повторить столкновения 29.07 и 01.08. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c72560139 |
fix(телеграм): ложное «вход слетел» больше не хоронит кампанию клиента
Приёмка Г1 02.08.2026 прошла: задание прошло насквозь, робот создал кампанию 2234762 в кабинете МТС, заголовок объявления и ссылка проверены ГЛАЗАМИ на шаге «Объявление». Песочница, 445 номеров из 450 опознано, деньги не двигались. Следы пробы убраны, копия — app/storage/app/proba-priemki-2026-08-02.json. Приёмка вскрыла мину. Первое из двух заданий упало с «Вход слетел — нужен повторный логин», хотя вход был ЖИВ: тем же кодом робота минутой позже проверка ответила «да». Кабинет МТС завис на пустой странице, робот принял это за разлогин. Цена — одна осечка сразу хоронила кампанию: задание в failed, кампания в failed, второй попытки нет. На приёмке это 1 прогон из 2. Робот (bots/mts-telegram-ads): - ensureLoggedIn в src/session.js — три попытки с паузой 5с; сбой самой проверки считается попыткой, а не падением; - SessionLostError несёт признак retryable; - runner.js во всех трёх режимах (запуск, чтение вердикта, пересдача) зовёт ensureLoggedIn и протаскивает retryable в отчёт; - bin/poll.js протаскивает retryable из своей обёртки. Портал (app): - RobotResult читает retryable (только при ok:false); - TgRobotController::done на временный отказ возвращает задание в очередь (queued, taken_at=null) и кампанию НЕ трогает, пока attempts < max_attempts; - client_tg.robot.max_attempts, по умолчанию 3, env TG_ROBOT_MAX_ATTEMPTS. Считаются выдачи задания, а не отказы: тем же счётчиком attempts пользуется возврат по сроку аренды, поэтому бесконечного круга не выйдет. Тесты писались красными: портал 254/254 (720 проверок, +4 новых), робот 134/134 (+4 новых). Pint чист, статанализ 0 ошибок. В phpstan-baseline.neon дописаны 27 записей про $this в Pest-замыканиях — тем же порядком, что у соседнего TgRobotDoneTest. На боевой НЕ выкачено. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
e724311e38 |
feat телеграм-робот: чтение вердикта и пересдача переведены на опрос
Теперь на опрос переведены все три работы робота, а не только запуск кампании. Портал кладёт задание в таблицу, робот сам приходит за ним и отчитывается. Это нужно потому, что робот живёт не на машине очереди - МТС не пускает адреса дата-центров. Что сделано по плану docs/superpowers/plans/2026-07-31-tg-poll-read-status-resubmit.md: - два новых режима задания: чтение вердикта модерации и пересдача; - применение вердикта вынуто из джоба в TelegramModerationVerdictApplier, применение итога пересдачи - в TelegramResubmitResultApplier; оба переноса построчные, денежная логика не менялась ни в одном символе; - у обоих джобов появилась развилка по каналу: на опросе задание ставится, робот не запускается; - приёмщик отчёта различает режимы - иначе отчёт о чтении вердикта применился бы как отчёт о запуске и сдвинул кампанию не туда; - номера телефонов больше не выдаются режимам, которым они не нужны: чтению вердикта и пересдаче аудитория не требуется, она у кампании уже есть; - у робота развилка по режиму вынесена в отдельный src/poll-plan.js, чтобы её можно было проверять без браузера и без сети; - сторож прав на бою: роль портала обязана иметь право ставить задания. Новых миграций и новых прав НЕ понадобилось: все три места ставят задание под подключением по умолчанию, то есть под ролью портала, у которой права уже есть. Схема БД не менялась, запись в CHANGELOG не требуется. Приёмка вырезанием: убираем ограничение по режиму в выдаче номеров - два теста падают, возвращаем - зелёные. Проверено: телеграм на портале 244 из 244 (было 219, старые тесты в том числе) робот 124 из 124 (было 120) статанализ 0 настоящих замечаний полный прогон 4039 тестов, 3995 прошло, 20 упало Из 20 падений 19 - те же давние, что были до работы. Двадцатое - ExternalServiceDownAlertTest, в одиночку проходит 3 из 3 и вместе с телеграм- тестами тоже; падает только в полном прогоне от накопленных данных. Это известная слабость: у большинства файлов нет изоляции между тестами. Заодно сборка тестовой БД переведена с migrate:fresh на связку db:wipe --drop-types + migrate: первая спотыкалась на призрачном типе legal_entities, вторая на тех же состояниях отрабатывала без отказов. На бой не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
146f0a7a35 |
feat телеграм-робот: джоб запуска умеет опросный канал
При transport=poll портал кладёт задание и выходит, робот заберёт его сам. Процессный путь оставлен рабочим и остаётся умолчанием. Полный набор тестов телеграма: 219 из 219 зелёные. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
81d6de7588 |
feat телеграм-робот: приём отчёта роботом и общее применение итога
Применение итога вынуто из джоба в сервис без изменения логики — теперь его зовут оба пути, процессный и опросный. Отчёт принимается только по заданию в работе. Сторож принят вырезанием: без проверки статуса повторный отчёт проходит с 200. Отдельно закрыта мина, найденная ревью защиты и отсутствовавшая в плане: роль crm_admin_user имела на client_tg_campaigns только чтение, а канал робота пишет туда итог — на бою приём отчёта упал бы по правам. Миграция v9.29 даёт UPDATE. Полный набор тестов телеграма: 217 из 217 зелёные, старые в том числе. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4663dcb0b |
feat телеграм-робот: выдача номеров роботу только по заданию в работе
Номера не кладём в задание и не пишем в журнал — отдельный запрос, без кеша. Сторож принят вырезанием: без проверки статуса тест отдаёт 200 вместо 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c6bd19cede |
feat телеграм-робот: канал выдачи задания роботу
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d42334674c |
feat телеграм-робот: сервис-токен канала, пустой токен закрывает канал
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e75bb76adf |
feat телеграм-робот: очередь заданий — поставить, выдать по одному, вернуть зависшее
Выдача строго по одному: у робота один профиль браузера и одна сессия кабинета. Задание, по которому робот не отчитался за срок аренды, возвращается в очередь. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
542fb7a371 |
feat телеграм-робот: модель задания роботу
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
32c8fa2881 |
feat телеграм-робот: таблица заданий роботу с построчной защитой
Номера телефонов в задание не кладём — только текст, ссылка и смета. После выката на бой перезапустить db/03_service_bypass_policies.sql. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f26c22c33 |
feat телеграм-робот: переключатель канала process/poll и сервис-токен
Пока только настройки, поведение не меняется: по умолчанию старый процессный путь. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
77f61fb1d1 |
merge: сведение ветки «Реклама Телеграм» с боевым main
Слияние feat/client-telegram-ads с main
|
||
|
|
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>
|
||
|
|
7a2b052f0d |
feat(телеграм-реклама): связка Laravel → робот mode:resubmit — пересдача чинит ту же кампанию
Хвост «связка» из STATE. Раньше пересдача отклонённой кампании создавала в кабинете
МТС НОВУЮ кампанию (RunTelegramCampaignJob, очищала mts_campaign_id). Теперь портал
поручает роботу ЧИНИТЬ ту же кампанию: робот заходит в неё через «Исправить» по
mts_campaign_id, вносит правки и переотправляет на модерацию «без оплаты» (0 ₽).
Робот-режим mode:'resubmit' уже проверен живьём (коммит
|
||
|
|
68fe632cca |
fix(телеграм-реклама): правки по сводному код-ревью ветки — деньги, статус-машина, робот, RLS
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное. Поведение в песочнице не меняется; правки готовят ветку к боевому включению. F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по 4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа 23505 в боевом режиме. F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200; длинное значение даёт ошибку поля, а не замаскированный 422 от БД. F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и считает дневной лимит под lockForUpdate строки правила. F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем. F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони. F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке domClickButton, finalize целит только кнопку отправки на модерацию. F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment; не дошли до /payment → падаем громко. F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком. F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в 000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql. Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
18135b47b4 |
feat(телеграм-реклама): Этап 3.5/3.6 + 4.3 — чтение вердикта, пересдача, уведомление об одобрении
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
«На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
{ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.
## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
(.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
+ freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).
## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.
TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.
Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a7e1dbeaf7 |
feat(телеграм-реклама): Этап 3.4 — опросчик вердикта модерации МТС
Этап 3 «Жизненный цикл и модерация», задача 3.4. Живая отправка кампании
ставит статус `moderating` (задача 3.2), но одобрение/отказ приходит от МТС
позже и без API. Опросчик раз в цикл робот-читалкой заходит в кабинет по
`mts_campaign_id` и применяет вердикт — КОНСЕРВАТИВНО с деньгами.
- PollTelegramModerationJob: раз в 15 минут перечисляет `moderating`-кампании
с `mts_campaign_id` (без id на модерацию уйти не могли — пропускаем) и читает
вердикт роботом РОВНО ОДИН РАЗ на кампанию (каждый вызов = заход в браузер):
· `rejected` («Отклонена») → статус `rejected` + причина из кабинета; возврат
брони кошелька (в бою, ключ совпадает с freeze); уведомление «отклонена»;
· `approved` («Одобрена») → `launched`; уведомление «одобрена». Деньги НЕ
трогаем — бронь под потолок держится, фактическую стоимость спишет билинг
по факту показов (2.4 → позже);
· `moderating` / робот не прочитал вердикт (null) → НЕ трогаем, ждём цикла.
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; возврат брони и уведомление —
ПОСЛЕ транзакции статуса, каждый в своём try/catch. Повторная проверка
status===moderating под lockForUpdate (гонка). Зеркалит SweepStuckTelegramCampaignsJob.
- RobotResult: поле `moderationStatus` (approved|rejected|moderating|null) + разбор
в fromRobotJson.
- TelegramRobotRunner: метод `readModeration($mtsCampaignId)` (режим read-status,
Node-сторона — задача 3.5); `campaignId` проброшен в task-payload.
- NotificationService: `notifyTelegramCampaignApproved` (зеркало Rejected, in-app
без pref-гейта) + событие EVENT_TG_CAMPAIGN_APPROVED.
- console.php: расписание опросчика everyFifteenMinutes с heartbeat-трекингом.
Селекторы экрана отказа — из живой разведки Part B (bots/mts-telegram-ads/
FLOW-FINDINGS.md, «Разведка Сессии 6»); повторного захода в кабинет не потребовалось.
Миграция не нужна — новых колонок/статусов нет (moderating/rejected уже были).
TDD. Приёмка (моя область, чистый прогон): PollModerationTest 6/6 (отклонена/
одобрена/ещё-на-модерации/не-прочиталось/без-id + деньги: при отказе бронь
возвращена, при одобрении не тронута) + весь набор ClientTg 143/143.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a736ff087d |
feat(телеграм-реклама): Этап 3.3 — уборщик зависших кампаний + таймаут
Этап 3 «Жизненный цикл и модерация», задача 3.3. Робот в браузере может
оборваться на полпути (воркер убит, сеть отвалилась) — кампания навсегда
виснет в `running`, а замороженные деньги залипают. Уборщик добивает статус,
но КОНСЕРВАТИВНО с деньгами.
- SweepStuckTelegramCampaignsJob: раз в 10 минут ищет `running` старше 15 минут
(штатный робот ≤ 420с). Развилка по факту создания черновика в кабинете:
· нет mts_campaign_id (черновик не создан) → кампания заведомо не ушла →
`failed` + возврат брони кошелька;
· есть mts_campaign_id (черновик создан) → могла уйти на модерацию МТС →
деньги вслепую НЕ возвращаем, `needs_review` до ручной сверки (задача 3.4).
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; release гейтится на !sandbox,
ключ совпадает с freeze. Повторная проверка status===running под lockForUpdate
(гонка с finalize). Зеркалит связку ChargeTgNameFeeJob + RunTelegramCampaignJob.
- Campaign.php: константа STATUS_NEEDS_REVIEW; переходы running→needs_review
(терминальный) и queued→failed (для failed() джоба при раннем сбое). Миграция
не нужна — на колонке status нет CHECK, машина статусов в модели.
- RunTelegramCampaignJob: public int $timeout = 420 (робот ~300с + навигации);
failed() теперь умеет queued→failed (переход добавлен в модель).
- console.php: расписание уборщика everyTenMinutes с heartbeat-трекингом.
TDD. Приёмка (моя область, чистый прогон): SweepStuckTest 7/7 (в т.ч. деньги —
с черновиком не тронуты, без черновика возвращены) + StatusMachine/Moderating/
ExternalId/RunCampaignJob/LaunchIdempotency/RobotResult/RobotRunner 49/49.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
86c560e0c4 |
feat(телеграм-реклама): Этап 3.2 — статус «на модерации» + переходы
Этап 3 «Жизненный цикл и модерация», задача 3.2. Живая отправка робота в МТС означает «ушло на модерацию», а не «запущено»: кампания получает статус `moderating`, а `launched` придёт позже, когда опросчик вердикта (задача 3.4) увидит одобрение. - Campaign.php: константа STATUS_MODERATING + переходы running→moderating, moderating→launched|rejected, rejected→queued (пересдача, задача 3.6). running→launched оставлен ради обратной совместимости. Миграция НЕ нужна: на колонке status нет CHECK-ограничения (машина статусов — в модели), `moderating` влезает в varchar(16). - RunTelegramCampaignJob.finalize(): живой успех (launched=true) → moderating вместо launched; песочница (черновик) по-прежнему → draft_ready. Тест-инфра (побочно, но необходимо для проверки): tests/TestCase.php получил `protected $dropTypes = true`. RefreshDatabase's migrate:fresh дропал таблицы, но НЕ типы Postgres; при заблокированном дропе таблицы её composite row-type переживал db:wipe, и перезагрузка db/schema.sql падала на дубле типа («legal_entities … уже существует»), оставляя ЧАСТИЧНУЮ схему — давний интермиттентный флак «migrate:fresh иногда прерывается» (site_events/ client_tg_tariffs случайно отсутствовали). Дроп типов на каждом refresh резко снизил флак (было 15–46/130 → стало 93–126/130). Только для APP_ENV=testing, на прод не влияет. Остаточный редкий обрыв — отдельная пред-существующая проблема, не этой задачи. TDD, робот замокан, песочница. Приёмка (моя область, чистый прогон): ModeratingStatusTest 8/8 + StatusMachineTest/RunCampaignJobTest/ExternalIdTest/ RobotRunnerTest 35/35 суммарно; phpstan (Campaign+Job) 0, deptrac 0, pint чисто. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
73bd36b6ce |
feat(телеграм-реклама): Этап 3.1 — id кампании МТС хранится рано и при отказе (+ разведка экрана отказа)
Этап 3 «Жизненный цикл и модерация», задача 3.1. Плюс закрыта задача 3.0 (живая разведка экрана отказа) — вердикт МТС по кампании «займ» (2231134) пришёл: «Отклонена». Разведка read-only, деньги не тронуты. Задача 3.1 — колонка mts_campaign_id + РАННЕЕ и надёжное сохранение: - Миграция client_tg_campaigns.mts_campaign_id (varchar(32) NULL, после status_reason) + запись CHANGELOG_schema v8.89 (предварит., ветка). RLS не меняется; rls-reviewer не требуется (nullable-колонка данных). - Робот (Node): чистый хелпер parseCampaignId(url) в cabinet.js (покрыт тестом); runner.js захватывает id СРАЗУ после создания черновика (шаг аудитории) и печатает маркер MTS_CAMPAIGN_ID=<id> в stderr; id теперь идёт и в ветке ОТКАЗА (раньше терялся). - Обёртка (PHP): TelegramRobotRunner восстанавливает id из stderr-маркера во всех путях (таймаут/непарсабельный вывод/JSON без id); RobotResult::failed принимает id. - Джоб: finalize сохраняет mts_campaign_id при ЛЮБОМ исходе (успех/отказ), не затирая ранее сохранённый id. Метод failed() не трогали — туда результат не доходит (осознанный residual, закроют уборщик 3.1b и sweeper 3.3). Разведка отказа (3.0) записана в bots/mts-telegram-ads/FLOW-FINDINGS.md: причина показана текстом в слайд-модалке «Причины отклонения кампании» (кнопка «Причины»); поля загрузки файла на экране отказа нет — документ грузится через «Исправить» → шаг «Сообщение» → «Комментарий для модератора»; кнопка пересдачи — «Исправить». TDD, робот замокан, тесты на liderra_testing (номера 7999…). Приёмка: Node 48/48 (npm test), Pest ExternalIdTest 2/2 + регрессия ClientTg 122/122, phpstan (4 боевых файла) 0, deptrac 0 нарушений, pint чисто. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5211048bce |
feat(телеграм-реклама): Этап 2 закрыт — робот несёт фактическую стоимость (2.2), билинг МТС подтверждён
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ (резерв → списание по факту показов → возврат остатка, экран /payment). Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения). Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2. - 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка, чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял. - 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа → null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена (селектор строки стоимости подтвердим живьём в Этапе 3). - Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт). TDD, робот замокан, тесты на liderra_testing. Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest (потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0, pint чисто, deptrac 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2e5db6737b |
feat(телеграм-реклама): Этап 2 (часть) — статус «отменена» и возврат брони при отказе/отмене
Закрыты денежно-независимые куски Этапа 2 (находки #1, часть). Списание по факту (2.2/2.4) ждёт подтверждения билинг-модели МТС. - 2.1 Статус кампании «cancelled»: константа STATUS_CANCELLED, переходы draft→cancelled и queued→cancelled (отмена только ДО запуска); cancelled — терминальный. Из running/терминальных отмена запрещена. Миграция не нужна — status это string(16) без CHECK-констрейнта. - 2.3 Возврат брони (release), иначе резерв кошелька залипал (находка #1): • при отказе робота (finalize) — release после записи FAILED, отдельным tenantTx, чтобы сбой возврата не откатил статус; • при перманентном сбое джоба (failed) — тоже release; • новый endpoint POST /api/telegram/campaigns/{id}/cancel: draft/queued → cancelled с возвратом брони; из running/терминальных — 422. Возврат гейтится !sandbox (в песочнице заморозки не было). Ключи release совпадают с freeze в launch ('telegram','campaign',id). RLS: денежная операция в джобе идёт под SET LOCAL app.current_tenant_id (образец ChargeTgNameFeeJob). Возврат при rejected здесь НЕ трогаем — его выставляет опросчик модерации (Этап 3, задача 3.4). TDD, робот в тестах замокан, тесты на liderra_testing (7999… номера). Приёмка: Pest ClientTg RefundOnFail 5/5 + регрессия соседей (32/32 суммарно), deptrac 0, pint чисто, phpstan по боевым файлам (джоб/контроллер) 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b3a86e69ad |
feat(телеграм-реклама): Этап 1 — безопасность и защита входа модуля «по своей базе»
Закрыты 5 находок аудита (безопасность и валидация входа, деньги не задеты): - #11 ПДн-скрины робота: полноэкранный скриншот кабинета МТС (мог содержать телефоны базы, 152-ФЗ) больше не снимается и не уходит письмом по умолчанию — только по явному TG_DEBUG_SHOTS. Чистый хелпер src/shots.js. - #12 стоп-лист opt-out теперь нормализуется при сравнении (8XXXX / 10-значные формы вычищаются), сравнение по голому 7XXXXXXXXXX. - #13 верхние пределы входа: phones max:200000, phones.* max:32, audience_days max:365; store возвращает dropped_count (сколько номеров не распозналось). - #14 идемпотентность запуска: Фаза A джоба читает кампанию с lockForUpdate — два воркера сериализуются на блокировке строки (защита от гонки). - #2 предстартовый гейт аудитории: в боевом режиме launch НЕ бронирует деньги под кампанию с <367 / пустой аудиторией (иначе бронь залипала бы). Порог — config client_tg.auto_batch_threshold (367). В песочнице гейта нет. TDD, робот в тестах замокан, тесты на liderra_testing (7999… номера). Приёмка: Node 28/28, Pest ClientTg 105/105, phpstan 0, pint чисто, deptrac 0. Обновлён CampaignApiTest (тест «денег не хватает → 409» засеян ≥367 контактами, чтобы дойти до проверки денег после нового гейта). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
b23d380d69 |
feat(телеграм-модуль): мост Laravel → Node-робот — статус-машина, обёртка робота, джоб, песочница
Сессия 2 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Сердце модуля: по кампании запускаем браузер-робота кабинета МТС (у МТС нет API),
парсим его JSON и ведём статус-машину. В тестах робот замокан node-скриптами.
- Статус-машина Campaign: draft → queued → running → (draft_ready | launched |
failed | rejected); недопустимый переход — DomainException, статус не меняется
(глобальное исключение, а не доменный класс: deptrac Model: [], ADR-005).
Терминальные статусы без исходящих (resubmit — Сессия 5).
- TelegramRobotRunner: формирует task.json и запускает `node bin/run.js --task <file>`
через Symfony Process, разбирает {ok,matched,launched,campaignId}. Таймаут и
непарсабельный вывод → аккуратный RobotResult::failed, воркер не падает.
- RunTelegramCampaignJob: queued → running, собирает кандидатов, пишет временный
файл номеров (ПДн, чистится в finally), зовёт робота ВНЕ транзакции, итог пишет
под tenant-контекстом (SET LOCAL). ok+draft → draft_ready + matched; ошибка → failed
(причина в журнал — колонки нет, как у СМС-близнеца; показ клиенту — Сессия 5).
Идемпотентно: работает только со статусом queued. Денег в песочнице не трогает.
- Песочница config/client_tg.php + .env.example: TG_SANDBOX (по умолчанию ВКЛ) —
робот только draft, деньги/запуск выключены. Живой режим — осознанно в Сессии 6.
Реальная точка входа робота — bin/run.js --task (в плане текст «src/runner.js» —
вольная стенография). Живое списание отложено на Сессию 6: робот пока не возвращает
фактическую стоимость.
Проверки: 50 из 50 Pest зелёные (24 новых), phpstan 0 по своему коду, робот в тестах
замокан (кабинет МТС не тронут).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
298afb835a |
feat(телеграм-модуль): ядро клиентского модуля Telegram-рекламы — таблицы, модели, цена, аудитория, кошелёк
Сессия 1 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md. Бэкенд-ядро — зеркало готового СМС-модуля. Робота ещё нет — он в Сессии 2. - 8 таблиц client_tg_* с RLS tenant_isolation и GRANT crm_app_user; справочные tariffs/settings без RLS с guarded-GRANT. rls-reviewer PASS 8 из 8. - 7 моделей ClientTg + связи campaign->phones. - Ступенчатая цена TelegramTariffService — ₽ за показ по объёму; тариф = потолок, точную стоимость считает МТС. - Сборка аудитории TelegramAudienceService — сделки за период / своя база / свой список, нормализация телефона, стоп-лист и дедуп; отдаёт кандидатов, реальный охват узнаёт робот после загрузки в МТС. - Канал кошелька telegram: перенесён общий AdWallet со свежей версии портала — модели, сервис, миграции; списание и заморозка по каналу telegram под тестами, charge не бросает и не уходит в минус, нехватка средств ловится freeze до списания. Проверки: 26 из 26 Pest зелёные, larastan 0, gitleaks чисто. db/CHANGELOG_schema.md v8.86 — номер предварительный, ветка отстаёт от main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |