a709efdee46fda0ce43e9d5b5bcc883caef0ce0d
209 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a709efdee4 |
feat(кошелёк): две колонки, период, разрез по кампаниям, расшифровка заморозки
Владелец 06.08: "кошелёк убогий и неинформативный". Отметил пять слабых мест из шести. Не отметил только прогноз "надолго ли хватит" - он пополняет по факту, а не по остатку. Что теперь на экране: - слева липкая колонка денег: свободно крупно, и под ней РАСШИФРОВКА заморозки - какая кампания, сколько заперто, когда вернётся. Раньше стояло голое "Заморожено 2 507 руб" без единого слова, под что; - период: сегодня / вчера / 7 дней / 30 дней / свои даты, живёт в адресе страницы; - столбики расхода по дням, цвет по каналу, своим CSS без библиотеки; - таблица "Куда ушли" - строка на кампанию с суммой и долей, клик отбирает ленту; - закладки по каналам сохранены, но теперь считаются за выбранный период; - лента разбита по дням с итогом за день и постраничной догрузкой. Сервер: две новые сборки RaskryitieZamorozki и OtchyotKoshelka, новая ручка /api/advertising/wallet/report, лента получила отбор по датам, каналу и кампании и листание по ключу вместо смещения. Расчёт списаний, заморозки и возврата по сроку НЕ тронут - правка только про показ. Найдено по дороге и закрыто: - чтение отчёта обёрнуто контекстом построчной защиты. Без этого на боевом запрос вернул бы НОЛЬ строк молча: местная база под суперпользователем защиту обходит, и мы бы увидели это только у клиента; - сторож на число запросов мог зеленеть БЕЗ самой ручки - на любой неизвестный путь портал отвечает страницей с кодом 200. Усилен и проверен вырезанием маршрута; - общий форматтер срезал хвостовой ноль: "833,50" превращалось в "833,5". На это независимо наступили три правки подряд, каждая завела свою копию. Сведено в formatExact. Границы суток и группировка по дням считаются ПО МОСКВЕ, а не по Гринвичу. Сторож взят с живого боевого: запись 69, 2026-08-05 21:00:24 UTC - для человека это 6 августа. Наивные версии всех трёх мест были написаны нарочно и увидены красными, прежде чем чинились. Старые сторожа не выброшены, а перенесены: закладки по каналам переехали в отчёт вместе с проверкой на 120 строк, отбор ленты по каналу - в проверку ленты. Каждый переезд назван поимённо. Проверено: 461 сторож сервера, 2135 сторожей экрана, контроль типов ноль, статанализ ноль, форматтер чисто, сборка фронта проходит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2f0ef8e868 |
feat,обзвон: тариф звонка переехал в базу — админ правит цену без программиста
Беда: правило цены звонка уже написано и работает, а самих двух цифр — за снятую трубку и за начатую минуту — держать было негде и менять некому. Чтобы поправить цену, нужен был программист и новая сборка. Заведена таблица obzvon_tariffs: строка ровно одна на весь портал, замок CHECK id = 1, деньги целыми копейками — те же единицы, что у строки звонка. Ручка админки GET и PUT /api/admin/obzvon/tariff правит обе цифры разом: половина новой пары с половиной старой давала бы тариф, которого никто не назначал. 🔴 Обе цифры назначены владельцем ВСЛЕПУЮ: сколько нам самим стоит звонок, ни разу не измерено. Оговорка написана в четырёх местах вплотную к самим числам и уходит в ответ ручки, чтобы её видел человек на экране, а не только программист в коде. 🔴 Смена тарифа НЕ пересчитывает вчерашние звонки: цена и снимок тарифа лежат в самой строке звонка. Здесь только «сколько будет стоить следующий». Мусор в цене не принимается: отрицательная, пустая, нечисловая и с третьим знаком после точки — третий знак молча пропал бы копейкой. Правка оставляет след в saas_admin_audit_log — кто, когда, с чего на что. Канон схемы — db/schema_modules.sql раздел 23, журнал — запись v9.69. Сторожа — app/tests/Feature/Obzvon/ObzvonTariffTest.php, все показаны красными. План: docs/superpowers/plans/2026-08-04-obzvon-pod-klienta.md §З-1.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b634d20dc |
fix(сторож секретов): приёмочные документы в корне docs/superpowers/ вставали насмерть у всех
Полная проверка истории находила 5 «незамаскированных телефонов» в
docs/superpowers/2026-07-27-PRIEMKA-client-sms-fixes.md и не пускала push НИКОГО.
Замерено: все пять с префиксом 7999 — синтетические по правилу проекта
(«реальные НИКОГДА»). Настоящих персональных данных нет.
Причина: список исключений покрывает подпапки docs/superpowers/{plans,runbooks,
specs,audits,findings,prototypes}, а этот документ лежит ПРЯМО в docs/superpowers/
и под них не подпадал. Файл из истории не убрать, не переписав общую ветку, —
значит, чинить надо список.
Разрешение узкое: только docs/superpowers/ГГГГ-ММ-ДД-PRIEMKA-*.md. Прочие файлы
корня docs/superpowers/ проверяются по-прежнему.
Проверено: до правки — 5 находок, после — «no leaks found» на 7589 записях.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
458f206ce9 |
feat(смс): канал Теле2 — отправка, судьба сообщений и приёмник отчётов
Канал «SMS-Таргет» (target.t2.ru, HTTP API v2, Basic Auth) обеими половинами:
- отправка через POST /send_message (msisdn числом, shortcode, text);
- опрос судьбы GET /send_message/message-id-{id} — по одному сообщению,
пакетного запроса у t2 нет;
- приёмник обратных звонков GET|POST /api/webhook/t2-delivery/{secret}
со строками ?id=&status=&parts= — форма другая, чем у МТС.
Ловушки, закрытые тестами:
- слово status в ответе встречается дважды: снаружи «запрос удался»,
внутри судьба сообщения. Спутать = объявить доставленным всё подряд;
- отказ приезжает с HTTP 200 — судим по телу, не по коду;
- незнакомое слово статуса судьбу НЕ меняет, сохраняется в delivery_raw.
За «не доставлено» клиенту возвращаются деньги (В-198) — выдумка двигала
бы рубли;
- t2 наш ответ не проверяет и повторов не делает: пропущенный звонок потерян,
поэтому опрос client-sms:poll-delivery остаётся обязательным.
Деньги приёмник не двигает — возврат живёт в команде опроса (В-146).
Защита приёмника как у МТС: секрет не короче 32 знаков (пустой = закрыт
наглухо), список адресов отправителя, счётчик обращений; чужому 404, не 403.
Канал ВЫКЛЮЧЕН и живьём не пробован: имя отправителя Liderra.ru (заявка
№ 31121) на согласовании, без него t2 не выдаёт логин с паролём.
Порядок включения — docs/superpowers/runbooks/2026-08-04-vklyuchenie-kanala-t2.md
Миграций нет: графы судьбы появились ещё при МТС. Теле2 уже был в списке
разрешённых операторов и в тарифах админки — новых экранов не нужно.
Проверено: 78 тестов зелёные, Pint чист, статанализ по ВСЕМУ проекту 0 ошибок,
маршрут зарегистрирован. Против main — только вставки, удалений нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6353c5d223 | feat(воронка): сопоставление и служебная ручка разовой проставки ниш | ||
|
|
bc76978c82 | feat(воронка): список ниш для подсказок и правка ниши в карточке | ||
|
|
447ad6af49 |
fix(рекламный кошелёк): клиент видит, за что заперты его деньги
Приёмка 03.08.2026 на боевом нашла три дыры вокруг одной и той же суммы: у клиента заморожены 3 333,36 ₽, и узнать за что было негде. Деньги не терялись — терялось объяснение, а для клиента, у которого заперта треть кошелька, это неотличимо от пропажи. 1) Денежная плашка врала после паузы и возобновления (Я-Ф1/Я-Ф4 FAIL). Она грузилась ОДИН раз, при открытии страницы, и о переменах не узнавала. После «Возобновить» экран обещал 9 991 ₽ свободных, когда на деле оставалось 6 657,64 ₽: клиент считал новую кампанию на несуществующие деньги и получал отказ, которого не ждал. Правда появлялась только после перезагрузки. Стало: список кампаний сообщает «деньги подвинулись», экран перечитывает плашку. Сигнал шлют ровно те три действия, что двигают деньги: запуск, пауза, возобновление. Сторож держит не содержимое плашки (оно зелено и при поломке), а сам факт повторного обращения за кошельком после денежного действия. 2) Летопись не фиксировала ни постановку заморозки, ни снятие. Заморозка писалась суммой 0,00 ₽ — жёстко зашитой, снятие не писалось вовсе. Стало: заморозка пишется настоящей суммой со знаком минус (деньги ушли из свободных, баланс не двигается), снятие — отдельной строкой типа release. Подпись человеческая и с номером: «Заморозили под кампанию №6». 3) На экране кошелька вместо истории стояло «появится позже». Стало: живая история движений — новая ручка GET /api/advertising/wallet/transactions (последние 100, свежие сверху, чужие не отдаёт) и лента на экране: подпись человеческими словами, сумма со знаком, дата по Москве. У заморозки и её снятия отдельная пометка «баланс не изменился», чтобы клиент не принял резерв за списание. 4) Карточка кампании называла не ту сумму. Показывалось поле budget_rub — число, которое клиент когда-то вписал сам и которое в расчёте денег не участвует вообще: «Бюджет показов 200 ₽» при заморозке 3 333,36 ₽. Стало: «Смета показов» — считанная сервером (показы × цена за 1000, та же арифметика, что и у заморозки), плюс строка «Зарезервировано» с живой бронёй по этой кампании. Схема не менялась — все поля летописи уже были. Проверено: 375 сторожей рекламы, 748 по смежным кошелькам (телеграм и СМС ходят тем же сервисом), 2042 на фронте — все зелёные. Статанализ 0 ошибок, типы Vue чисты, стиль в изменённых файлах чист. Все четыре сторожа приняты вырезанием: на старом коде красные. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7ad9e0a66 |
merge: телеграм-реклама второй смены сведена в рабочую ветку перед выкатом СМС
Замер боевого перед выкатом СМС-модуля показал мину: на liderra.ru сейчас работает код ветки fix/tg-zagolovok-obyavleniya — 33 записи телеграм-рекламы, которых не было ни в main, ни в рабочей ветке. Сверено слепками файлов: config/client_tg.php и TelegramTariffService.php на бою совпадают с их версией и расходятся с нашей. Экраны портала собираются одним куском, поэтому выкат СМС в прежнем виде откатил бы их живую рекламу назад. Решение владельца — сперва забрать их работу к себе. Разрешено пять склеек, все — сохранением обеих сторон: - Tariff.php: их пояснение про закупочную цену плюс наша строка для подсказчика типов; - phpstan-baseline.neon: взята наша вычищенная версия. Их 260 строк заметания не возвращены — статанализ после сведения дал 0 без них, потому что чинили мы не baseline, а сам механизм, и он вылечил их новые тесты тоже; - advertising-telegram-view.spec.ts: наш типизированный мок оставлен; - CHANGELOG_schema.md: столкновения номеров НЕ было — у нас v9.33/v9.34, у них v9.64/v9.65. Обе пары сохранены, ничего не двигали; в файл дописано, что дыра v9.35–v9.63 это след их завышенного замера, а не потерянные записи; - STATUS.md: служебный файл наблюдателя, взят свежий. Что сведение вскрыло дополнительно: - их сторож значков поймал НАШИ три имени с экранов СМС — mdi-file-sign, mdi-account-badge-outline, mdi-file-download-outline в карте Lucide отсутствовали и на бою рисовались бы вопросом в кружке. Добавлены по смыслу; - проверка типов фронта: 1 ошибка стала 0. Образец имени отправителя в тесте админки не задавал два поля, и Partial подмешивал в них undefined. Проверено после сведения: PHP 4614 тестов, 4610 зелёных, 4 пропущено, 0 падений; экраны 250 файлов, 2018 зелёных, 3 пропущено, 0 падений; статанализ 0 через composer stan; проверка типов 0. Унаследованное, не мной внесённое и не тронутое: линтер фронта показывает 1 замечание на неразрывный пробел в advertising-sms-view.spec.ts — знак там нужен по смыслу проверки, было до сведения. 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> |
||
|
|
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> |
||
|
|
b6a15c5bbf |
merge: подтянул общую ветку в клиентские СМС — 227 записей отставания закрыты
Ветка шла отдельно почти неделю и отставала на 227 записей, отставание росло
каждый день. Направление сведения — общая В ветку: перевод main владелец
отклонил, значит вливать в него нечего.
Девять столкновений, каждое разобрано по существу.
Журнал схемы: столкнулись НЕ три номера, как ожидалось, а ВСЕ - обе ветки
независимо заняли v8.96-v9.25 и v9.32 разным содержимым. Обе стороны
настоящие, выбросить нельзя ни одну, поэтому перенумерована ветка, а не
общая: 31 запись уехала в свободный диапазон v9.33-v9.63. Содержание не
тронуто - доказано сверкой с исходной версией через git, посимвольно.
Соответствие старых номеров новым вписано в сам журнал, чтобы старые
документы ветки оставались читаемыми. Прежняя пометка про "запас v9.32"
заменена: запас не спас, v9.32 в общей ветке тоже был занят.
Сборка тестовой базы: взята версия общей ветки. Она позже и доказана
замером - двумя шагами вместо migrate:fresh, который спотыкался на
типе-призраке и оставлял схему неполной.
Список слов орфографии сведён объединением: 2106 наших + 2169 общих дали
2173, ни одно слово ни с одной стороны не потеряно - проверено сравнением.
Расписание работ, маршруты экранов и админский слой: обе стороны добавляли
своё в одно место, оставлены обе.
Витрина рекламных каналов: каждая ветка сделала настоящим СВОЙ канал -
ветка СМС свой, общая Телеграм. После сведения настоящих три, заглушки
исключают все три. Сторож витрины принят вырезанием: убрал СМС из списка
настоящих - покраснел, вернул - позеленел.
СТОЛКНОВЕНИЕ ИМЁН, созданное самим сведением. Оба набора тестов объявляли
глобального помощника pollCampaign - свой в СМС (один довод) и свой в
Телеграме (от двух до четырёх). Две функции с одним именем в одном языке
не живут: пока ветки шли врозь, этого не видел никто. Помощник СМС
переименован в pollSmsCampaign. Проверено, что других таких пар в PHP-тестах
нет ни одной.
Статанализ ветки доведён с 674 замечаний до НУЛЯ, уровень не понижен и в
baseline не заметено ничего.
- 616 из 674 - ложный класс Pest, закрытый тремя узкими правилами; правила
перенесены из рабочей ветки, где владелец их уже принял;
- остальные 42 - свои, в новом коде ветки, и починены по существу:
задвоенный ключ массива в трёх тестах (след копирования - комментарий
оторвался от своей строки), врущие описания двух помощников (PHP сам
делает из ключа-номера число), сужение типа возврата, прятавшее от
анализатора свойства подставного отправителя, лишний знак вопроса и
четыре бесполезных перенумерования списка.
- Приёмка вырезанием: подложил несуществующий метод - анализатор назвал его
поимённо и покраснел; убрал - ноль.
Шапки 59 моделей обновлены пересборкой подсказчика и ОСТАВЛЕНЫ намеренно
(правка только в комментариях, проверено): без них анализатор не связывает
модель с описанием и не знает, что дата - это дата, а не строка. Откатил их
сперва по привычке - получил 15 замечаний про даты, вернул - ноль.
Орфография: 15 файлов проверено, 0 замечаний (смотрел и на число
проверенных файлов, не только на число ошибок). Добавлены три слова из имён
миграций ветки.
Заодно: алиас ruflo-core в списке имён сторожа реестра - плагин описан в
реестре групповым именем, сторож видел только машинное. Мостится ТОЛЬКО имя;
🔴 содержательный долг остаётся - реестр до сих пор зовёт ruflo изолированным,
хотя его разморозили 28.07. Это чинить отдельно, через claude-md-management.
Проверено: три фронтовых сторожа рекламы 18/18, статанализ 0, разметка 0,
орфография 0, синтаксис PHP чист. Полный прогон тестов ветки - отдельным
шагом, он ещё ни разу не делался.
NB: в журнале схемы есть задвоенные номера v8.26 (пять раз) и v8.64 (два) -
это досталось по наследству из общей ветки, ровно столько же их там и было.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
dd83f440eb |
feat(смс-клиент): приёмник отчётов о доставке от МТС — ускоряет опрос, не заменяет его
Строка листа 5.2. Появился адрес, на который МТС может присылать судьбу сообщения сам,
не дожидаясь нашего вопроса. В ответ отдаём код 204 — этого он требует, иначе считает
доставку неудачной и шлёт повторы.
Несущее решение здесь одно, и из него растёт всё остальное: приёмник — НАДСТРОЙКА, а не
замена. Опрос каждые десять минут остаётся на месте. Значит любой отказ приёмника
безобиден: не понял письмо, не узнал номер сообщения, вовсе выключен — всё доберёт опрос.
Поэтому везде выбран ноль вместо догадки, и это позволило честно работать при незнании.
А незнание крупное: формы письма, которое шлёт МТС, живьём не видел никто. Записано только,
что письма приходят и что отвечать надо кодом 204. Документации тут веры нет — она уже
соврала про опрос, описав одну форму вместо другой. Выдумывать я не стал (В-243): приёмник
разбирает ровно ту форму, которую видел живой ответ на опрос, а незнакомое письмо кладёт в
журнал сервера своей ФОРМОЙ — перечнем полей, без содержимого, потому что внутри телефоны, а
это персональные данные. Первый же живой отчёт покажет свою форму сам, и читатель дописается
одной правкой. Проверено живьём: телефон в журнал не утёк.
Правило «как отчёт ложится в журнал» переехало из команды опроса в общий дом на два входа.
Разъехавшись, они писали бы по-разному, а по журналу считаются деньги. Форма письма при этом
живёт в канале, приёмник про устройство МТС не знает ничего — новый оператор с кабинетом
вставляется, не трогая ни приёмника, ни журнала.
Деньги приёмник не двигает вовсе. Возврат за недоставленное остаётся в опросе, где у него
своя двойная защита от повтора; вторая дорога к кошельку означала бы вторую возможность
вернуть дважды. Отдельный тест это стережёт.
Адрес публичный, поэтому защита тройная: секрет в адресе (не задан — приёмник закрыт наглухо,
а не «пускать всех»), необязательный список адресов отправителя и счётчик обращений. Чужому —
404, существование приёмника не подтверждаем. Список адресов пока пуст: адреса МТС нам
неизвестны, и придумать их нельзя.
Десять тестов, все доказаны вырезом — вырезов вышло двенадцать. Два вырезали и не покрасили
ничего, и опять ошибалось моё ожидание, а не код (шестой раз): место оказалось защищено
несколькими независимыми строгостями, каждой хватало поодиночке. Снял по две и по три разом —
покраснели ровно те тесты. Заодно попался прибор: прогон один раз показал девять тестов
вместо десяти при нуле красных, то есть пропавшая работа выглядела как отсутствие проблем.
Живьём: верный отчёт правит строку и отвечает 204; чужой секрет — 404 и строка не тронута;
непонятное письмо — 204 и ни одной записи; отчёт про неизвестный номер — 204 и предупреждение;
адрес вне списка — 404. Кошелёк за весь прогон не шелохнулся. Стенд возвращён.
Включение — сторона владельца: адрес указывается в кабинете МТС. До этого всё работает опросом.
🔴 И честно: пока канал МТС на бою не отправляет ничего, проверить приёмник живым письмом
нечем — отчётам просто неоткуда взяться.
|
||
|
|
65f3785bf3 |
feat(смс-клиент): сверка нашего расчёта с расчётом оператора — в админке у владельца
Строка листа 5.7. В админке «СМС» появился блок «Сверка расчётов с оператором»: по каждой отправленной рассылке видно, сколько частей насчитали мы и сколько оператор, сколько сообщений разошлись, наш расход, счёт оператора и разница. Клиенту это не показывается вовсе (решение В-201) — он платит по своему тарифу, наш расход перед оператором не его дело. Главное решение здесь — отсутствие числа и ноль это РАЗНЫЕ состояния. Экран пишет «цена канала не задана», «оператор цену не сообщил», «не спрашивали», «сверять нечем» и никогда не рисует 0 ₽ вместо неизвестного. Иначе владелец видел бы идеальную сходимость там, где сверять нечем — ровно тот молчаливый сбой, что стоил четырёх поломок 20.07. Разница считается только когда известны оба числа. Порядок работы задала живая проба, а не код. Шесть живых сообщений с боевого (разрешение и номера дал владелец) с перебором по одному признаку: МТС-номер и Т2-номер, одна часть и две, три разных имени отправителя, смешанный и чисто русский текст. Все шесть — «не отправлено», цена 0, отказ в ту же секунду, что и приём. Владелец проверил кабинет: баланс 5010 ₽, имя живое. Значит вывод из памяти проекта «пустой счёт» опровергнут, причина на стороне оператора (В-235, В-237) и требует разбора с их поддержкой. Попутно вторично и жёстче подтвердилось В-212: оператор принял даже номер Теле2, которого наш канал не обслуживает. «Принял» не значит ничего — по-старому портал записал бы «отправлено» и списал бы деньги за сообщения, которых нет. Что проба дала положительного: оператор считает ЧАСТИ ровно как мы — 1 на короткое, 2 на длинное в 118 знаков. На этом сверка по частям и построена, деньги ей не нужны. Строка 5.7 сужена честно и не молча (В-236): денежная половина построена, но живьём не доказана — цены нет с обеих сторон. У оператора 0, а у нас цена канала на бою не задана вовсе (В-234). Дозакрыть можно двумя вещами: числом из договора и одним реально отправленным сообщением с ненулевой ценой. План велел править AdminSmsController — такого файла нет вовсе (В-231, четвёртый раз за проект). Сверка живёт отдельным распорядителем: она ни ценам, ни отказам, ни именам отправителя не родня. Тесты: 9 на сервере, 6 на экране. Все проверены вырезом — 15 вырезов, каждый покрасил именно свои тесты. Один вырез не сработал, и опять ошибалось моё ожидание, а не код (В-239): база сама считает сравнение с пустотой «неизвестным», поэтому страж оказался лишним; заменён на вырез с настоящей ошибкой этого места. Живой прогон в браузере парный: без заданной цены канала — «цена канала не задана» и «сверять нечем»; с ценой 3 ₽ за часть — 9.00 ₽ против 12.00 ₽, разница 3.00 ₽, а расхождение по частям (3 против 4) выделено красным. Стенд возвращён. Статанализ поймал настоящую ошибку: сводные числа читались как поля модели сообщения, которых у неё нет. Починено — итоги берутся голыми строками, а не моделями. |
||
|
|
c65856ec73 |
feat(смс-клиент): досыл не дошедшим — отдельной рассылкой «Досыл к рассылке №N»
Строка листа 5.6. В журнале рассылки появилась кнопка «Дослать не дошедшим (N)».
Нажатие спрашивает отдельным окном: сколько человек получат сообщение ещё раз и что
это отдельная рассылка с отдельной оплатой. По подтверждению заводится рассылка
«Досыл к рассылке №N» — у неё свои деньги и свой итог, а первая остаётся целой.
Почему отдельной рассылкой, а не второй попыткой в старой (решение владельца В-200):
переписав старые строки, мы стёрли бы «не доставлено», и человек больше не увидел бы,
что первая попытка не дошла. Да и база второй попытки не даст: на журнале сообщений
уникальный ключ по клиенту, рассылке и номеру.
Досылаем только по ОКОНЧАТЕЛЬНОМУ итогу (моё решение В-216): пока кто-то «ещё в пути»
или пока сама рассылка идёт — кнопки нет, а сервер отказывает человеческими словами.
Правило живёт в одном месте на сервере, экран получает готовый признак и условие не
пересчитывает: разойдись они — человек нажимал бы кнопку и получал отказ.
Деньги. Досыл списывается как обычная отправка (решение В-198) и своей денежной ветки
не имеет — считает, замораживает и списывает тот же общий код заказа. Повторное
нажатие второй рассылки НЕ заводит: ключ заказа постоянный, «resend:{номер}». Второй
клик означал бы вторую оплаченную рассылку тем же людям.
Живой прогон нашёл то, чего не видели тесты (В-227): досыл забывал оператора, которого
портал уже знал. Номер уходил как «вписанный руками», получал пометку «оператора не
знаем», а такую строку обогащение регионом уже не чинит — на бою, где универсального
канала может не быть, досыл не ушёл бы никому при созданной и оплаченной рассылке.
Теперь досыл вспоминает оператора из журнала сообщений первой рассылки: журнал хранится
без срока, в отличие от снимка получателей, который чистится через 90 дней.
Ещё две дыры, которых не было в плане. Первая: «в пути» считается только по
отправленному, поэтому у рассылки в работе он равен нулю — досыл разрешался бы посреди
работы (В-229). Вторая: защита от случайного повтора отбивала бы досыл молча, ведь текст
у него тот же, что у первой рассылки (В-228).
Схема v9.25: колонка resend_of_campaign_id и индекс у client_sms_campaigns. Прав не
требует — это колонка существующей таблицы, а не новая таблица.
Тесты: 8 на сервере, 4 на экране. Все проверены вырезом — двенадцать вырезов, каждый
покрасил именно свои тесты. Живой прогон в браузере: кнопка с числом, окно про отдельную
оплату, рассылка на два нужных номера с сохранённым оператором, первая рассылка цела,
второе нажатие ничего не завело. Стенд возвращён.
Движение денег живьём НЕ проверялось: на стенде включена песочница, а выключить её —
значит слать настоящие СМС за настоящие деньги.
|
||
|
|
c6bd19cede |
feat телеграм-робот: канал выдачи задания роботу
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
97a93cb763 |
feat(смс-клиент): кнопка «Продолжить рассылку» — доводим до конца тех, на кого не хватило денег
Строка листа 4.13, решение владельца В-49 «надо обязательно». Рассылка встала из-за денег → клиент пополнил кошелёк → нажал «Продолжить», и она идёт дальше с того места, где остановилась. 🔴 Главная мина (В-151, подтверждена чтением кода): джоб отправки считает обработанным ЛЮБОЙ номер, у которого есть запись в журнале этой рассылки, без разбора статуса. А номер, на котором кончились деньги, записан как «не хватило денег». Значит простой повторный запуск молча пропустил бы его: человек заплатил бы за продолжение, а получатель не получил бы ничего. 🔴 И вторая находка, которой в плане не было (В-184): «оставить строку и просто не считать номер обработанным» НЕВОЗМОЖНО физически — на журнале сообщений уникальный ключ по (клиент, рассылка, номер), вторая запись не вставится. Поэтому продолжение эти строки УДАЛЯЕТ, и по смыслу это верно: «не отправили из-за денег» фиксирует НЕслучившееся и после пополнения перестаёт быть правдой. Заодно замерен масштаб: при остановке пишется РОВНО ОДНА такая строка, у остальных номеров записей нет вовсе. Что сделано: · POST /api/sms/campaigns/{id}/resume — убирает строки «не хватило денег», ставит рассылку в очередь, гасит причину остановки (иначе экран продолжал бы объяснять человеку прошлую остановку у работающей рассылки); · продолжаем ТОЛЬКО остановленную ИЗ-ЗА ДЕНЕГ (В-187). Остановленную человеком продолжать — значит отменить его решение; сорванную сторожем — наступить на ту же поломку. У каждой причины свой человеческий отказ, и текст под тестом; · правило живёт в ОДНОМ месте на сервере (как enableDecision в Task 4): список рассылок отдаёт готовый признак can_resume и число «сколько ещё уйдёт». Счёт остатка — в читателе снимка, где уже живёт «кто ждёт» (одно место, В-96); · кнопка «Продолжить» в таблице рассылок + подтверждение: сколько осталось отправить, что повторно никому не пойдёт, что цена прежняя; · цену НЕ пересчитываем, снимок НЕ пересобираем, заново НЕ замораживаем (В-39, В-188): списание идёт поштучно с проверкой перед каждым номером, в минус рассылка не уйдёт — кончатся деньги, встанет снова с той же причиной. 🔴 Мой же тест поймал расхождение с моим же решением (В-189): проверку денег я впихнул в правило «можно ли продолжить» — и кнопка исчезла бы именно у того, кому она нужна. Разделил: состояние — признак для экрана, деньги — проверка по нажатию с числами («свободно 0.00 ₽, одно сообщение 8.50 ₽»). Право DELETE на журнал сообщений рабочей роли — миграция 2026_08_01_101000, схема v9.22 (В-186, вторая половина мины В-152): без него продолжение на бою падало бы «нет доступа». Сторож MigrationGrantsTest расширен. Проверено: ClientSms 320/320 (13 пачек, все с первой попытки), приём лидов 17/17, фронт 1700 + 3 пропущенных (одна чужая давняя ошибка, В-57), phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Шесть вырезов, каждый покраснел ровно там, где вырезан. 🔴 Живой прогон ДЕНЕЖНЫЙ и парный под боевой ролью crm_app_user: рассылка на 5 номеров при деньгах на 2 встала (ушло 2, списано 17.00 ₽, одна строка «не хватило денег»); продолжение без пополнения — отказ с числами; БЕЗ пометки клиента рассылка не видна вовсе (изоляция держит); после пополнения с пометкой — ушло 5 из 5, списано 42.50 ₽, в журнале ровно 5 записей по одной на номер, «не хватило денег» не осталось. В браузере: кнопка видна глазами, после нажатия и прогона очереди рассылка «завершена, 5 из 5», кнопка исчезла, у нетронутой рассылки осталась. Заодно поправлена речь: было «отправим ещё 3 сообщений». Стенд возвращён и сверен; намеренное изменение одно — миграция накатана на локальную dev-базу. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4784382d49 |
feat(смс-клиент): журнал автоматических СМС за 30 дней на экране клиента
Строка листа 4.11, решение владельца В-102 «обязательно нужен». Причины по авто-СМС писались в базу, но человеку их не показывал НИ ОДИН адрес портала: у авто-СМС нет кампании, а единственный список сообщений отбирает их по номеру кампании. Это и была суть задачи, а не мелочь на потом. Что сделано: · GET /api/sms/auto-log отбирает ровно авто-СМС (кампания пуста) за 30 дней: «ушло», «не ушло» и разбивка по причинам — АГРЕГАТАМИ по всему окну, из базы приезжают числа, а не тысячи строк; · поимённый список — последние 100 событий, потолок жёсткий, параметра у клиента нет вовсе; если событий больше, экран честно говорит «Показаны последние 100 событий из N. Числа выше — за все 30 дней» (В-170, класс В-166: обязательство держится запретом на сервере, а не вежливостью экрана); · блок на вкладке «Авто-СМС»: числа, причины человеческими словами по уже заведённому словарю подписей, таблица событий, а на пустом журнале — фраза «За 30 дней автоматических СМС не было» вместо пустой таблицы. Попутно починены две неправды экрана — обе внутри обязательства «человеческими словами», а не сверх него: · подпись «отписались раньше» переписана на «номер в вашем стоп-листе» (В-169): возможности отказаться у получателя НЕТ вовсе (решение В-30), номер в этот список вносит сам клиент. Подпись пережила отмену целой функции и объясняла человеку то, чего в портале нет — класс В-154; · пробное сообщение считается НЕ ушедшим (В-172). План велел обратное, но денежный счётчик модуля считает только настоящее «отправлено», и подпись экрана говорит «пробный режим — не отправлено». Это четырнадцатая ошибка плана, найденная тем же приёмом: открыть код и посмотреть, как это же состояние трактуют соседи. Статанализ поймал то, чего не увидели ни четыре зелёных теста, ни живой прогон, ни браузер (В-174): разбивка читала у модели поля, которых у неё нет. Переписано на пару «причина → сколько». У каждого прибора своя слепая зона. Проверено: ClientSms 309/309, приём лидов 17/17, фронт 1697 + 3 пропущенных, phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Восемь вырезов, каждый покраснел там, где вырезан. Живой прогон под боевой ролью crm_app_user, парно: события писал НАСТОЯЩИЙ джоб авто-СМС — с пометкой клиента «ушло 1 (списано 9.00 ₽), не ушло 2: пробный режим 1, стоп-лист 1», без пометки тот же вызов дал ноль. На 363 событиях — ровно 100 строк в таблице при полных числах. ДаДата не тронута: клиент подменён заглушкой. Стенд возвращён и сверен. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
77f61fb1d1 |
merge: сведение ветки «Реклама Телеграм» с боевым main
Слияние feat/client-telegram-ads с main
|
||
|
|
6d52f390a0 |
feat(смс-клиент): кнопка «включить имя обратно» — списывает и включает сразу
Строка листа 4.5, Этап 4 Task 4. Раньше имя, которое клиент выключил сам или которое отключили за долг, можно было вернуть только одним способом: заказать заново, снова приложив документы и снова дождавшись согласования у МТС. Теперь у клиента есть кнопка: она берёт плату за месяц вперёд и включает имя сразу — решение владельца. Денег не хватает — портал говорит, СКОЛЬКО именно не хватает, и не трогает ни имя, ни кошелёк. Документы заново не запрашиваются: заявка уже согласована, сканы на месте. Ключ списания у кнопки тот же, что у ночного работника, поэтому за один календарный месяц имя оплачивается один раз — кто бы его ни включал. Сумма нигде не зашита, берётся из карточки имени и вырастет сама, когда подключим остальных операторов. Плана оказалось мало ЧЕТЫРЕЖДЫ, и все четыре раза — про деньги. В-143: план не смотрел, КТО отключил имя. Имя отключает и владелец руками, и оно у МТС погашено; клиент нажал бы кнопку, портал взял бы с него плату, а имя не работало бы. Это ровно то, что работнику запретили в прошлой задаче. Кнопка теперь смотрит в ту же графу «почему отключено» и при включении её гасит. В-144: слово «отменено» стоит и у имени, которое когда-то работало, и у заявки, которую клиент отозвал ещё на согласовании. Вторую план включил бы как рабочее имя и списал за неё деньги — за имя, которого у оператора нет. Отличаю по отметке согласования: её ставит ровно одно место, кнопка одобрения у владельца. В-145: имя, оплаченное вперёд, план списал бы второй раз — а при пустом кошельке ещё и отказался бы включить имя, за которое уже заплачено. Смотрю на «оплачено до»: срок в будущем — включаю без списания и срок не двигаю. В-146: план отдавал экрану решение «показывать ли кнопку». После первых двух правок правило перестало быть про один признак, и оно переехало на сервер: адрес имени отдаёт готовые «можно ли включить» и «сколько спишется», экран не считает. Отдельно В-148 — его поймал только браузер, мимо прошли 12 серверных и 66 фронтовых тестов. Экран писал «плата за текущий срок уже внесена» у имени, отключённого за долг с апреля. Ноль в сумме приходил не от оплаты, а от песочницы — одно значение с двумя разными причинами. Развёл: сумма считается только по сроку оплаты, песочница гасит сам денежный шаг. Проверено вырезанием, восемь вырезов, все вернуты, каждый покраснел там, где вырезан: проверка денег, отбор по причине отключения, отметка согласования, ветка «оплачено вперёд», ключ месяца, песочница в решении о сумме, кнопка на экране, учёт «можно ли» на экране. Живой прогон под боевой ролью crm_app_user, семь нажатий: 50 ₽ при плате 100 — отказ «Не хватает 50.00 ₽», не тронуто ничего; пополнил до 550 — имя работает, списано ровно 100 ₽, оплачено до 29.08; имя владельца при 5000 ₽ — отказ, деньги целы; неодобренная заявка при 5000 ₽ — отказ, деньги целы; второе нажатие — «имя уже работает», в движениях одно списание. Пара на пометку клиента: без неё то же нажатие не делает ничего, с ней — списывает и включает. Живой прогон в браузере: у имени с долгом видна кнопка и строка «Спишется 2500.00 ₽ — плата за имя за месяц»; нажал — карточка стала «Ваше имя: MYSHOP (активно)». У имени, отключённого владельцем, кнопки нет вовсе. Прогоны: клиентские СМС 275/275 (11 пачек, все с первой попытки), приём лидов 17/17, фронт 1683 + 3 пропущенных, phpstan ровно 2 чужие давние, pint чисто. Проверка типов фронта: 5 ошибок в 5 файлах вместо прежних 8 в 6 — задал явный тип ответа в спеке, который и так правил, и три давние ошибки ушли вместе с моими. Схема базы не менялась, миграций нет. |
||
|
|
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>
|
||
|
|
0ba7890bf9 |
feat реклама за показы: робот-разведчик приносит клиенту причину отказа с экрана кабинета
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране кабинета, и добыть её может лишь тот, у кого есть глаза. Как теперь работает: опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению; робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу, раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо, в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора». Четыре ловушки, пойманные по дороге и проверенные вырезанием: 1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16. 2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок креативов не нужен, значит при выключенном рубильнике она получила бы задание, и робот пошёл бы в живой кабинет. 3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота обход снова клал бы «причину выясняем» поверх настоящей причины. 4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен на сам механизм: обычным тестом это не ловится, они ходят суперпользователем. Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс внутри открытой транзакции. Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре. На боевой не выкатывалось, рубильник Директа выключен. Co-Authored-By: Claude Opus 5 <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>
|
||
|
|
83f685bbac |
refactor(смс-клиент): контроллер рассылки разрезан на три по смыслу (строка Н.1)
Перекладка, а не правка: поведение прежнее, тесты не тронуты ни в одной строке. Был один файл на 914 строк — в голову и в контекст он уже не помещался. Стало: - ClientSmsController (333) — сами рассылки: смета, запуск, остановка, журнал; - ClientSmsBaseController (280) — база номеров, стоп-лист «Не писать этим», шаблоны; - ClientSmsSenderController (354) — своё имя отправителя, бланк согласия, авто-СМС. Эффективное имя отправителя нужно двум файлам сразу (рассылке и разделу имени), поэтому вынесено в трейт ResolvesClientSmsSenderName. Разойтись эти два места права не имеют: расхождение означало бы, что клиент видит на экране одно имя, а МТС получает другое. Адреса маршрутов не изменились ни одним символом — в routes/web.php заменено только имя класса, иначе сломались бы фронт и старые тесты. Сверх таблицы плана (журнал В-74): приватные помощники addContacts/addOptouts уехали вместе со своими публичными методами, сервис цен внедрён в два контроллера — рассылке для сметы, имени отправителя для цены авто-СМС. Доказательства (В-75; вырезание защиты здесь неприменимо — новой защиты нет): ClientSms 169/169 — ровно столько же, сколько до разреза, без правок тестов; приём лидов 17/17; phpstan 1 ошибка, и та чужая давняя (nullsafe в consentForm), просто переехала вместе с кодом; pint чисто. Контрольный вырез: нарочно вернул два маршрута (/optouts и /sender) на старый класс, где этих методов уже нет — покраснели 5 тестов в двух файлах. Значит прогон действительно ходит через маршруты и заметил бы ошибку разреза. Живой прогон: все три области экрана ответили (рассылки, шаблоны, имя отправителя, база, стоп-лист, авто-СМС); предпросмотр по сделкам — «Уйдёт 5 СМС — 45.00 ₽»; шаблон заведён кнопкой и убран кнопкой. Стенд возвращён как был. Схему не трогали — записи в журнал схемы не требуется. |
||
|
|
d29f45da11 | feat реклама за показы: ручка Исправить и узкое исключение в замке правки | ||
|
|
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>
|
||
|
|
2736724511 | feat реклама за показы: ответ клиента с документом и отдача файла только своему тенанту | ||
|
|
5e5a9c7f7a | feat реклама за показы: ручка списка сообщений кампании — только своя лента | ||
|
|
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> |
||
|
|
af12b1ceb4 |
fix реклама за показы: предел веса картинки проверен по-настоящему, обход модерации не срывается целиком, робот не несёт токен на чужой адрес
Мелочи приёмочного листа v12 §8. Каждая правка с тестом; где защита уже стояла в коде — тест проверен вырезанием этой защиты. Предел веса картинки. Две прежние проверки были пустышками: сравнивали константу саму с собой и с тем же числом в ответе сервера. Вырезание правила max: оставляло обе зелёными. Настоящий тест грузит перевес и ждёт отказа — это четвёртая найденная пустышка за ветку. Обход модерации. Объявление без статуса и причина отказа длиннее колонки роняли запись в базу ВНЕ защиты, и обход обрывался на середине: остальные клиенты не узнавали, приняли их рекламу или отклонили, а деньги за отклонённый набор не возвращались. Запись ответа теперь под той же защитой, что и сеть; пустой статус не пишем вовсе, причину храним обрезанной. Робот. Адрес файла приходил в ответе сервера, а шли по нему со своим токеном без всякой сверки. Теперь адрес обязан вести на портал. Папка снимков экрана росла бесконечно, а на снимках видны логин и остаток счёта — старше двух недель убираются. Ещё: порядок посредников служебного канала — токен раньше служебного соединения; BannerGenerator больше не отдаёт молча файл тяжелее предела и берёт предел из общей константы; нулевой номер креатива ловится намеренно, а не случайно нестрогим сравнением. Портал 300/300, робот 60/60. Мест снятия заморозки денег по-прежнему четыре. Не тронуто намеренно: цена за 1000 показов и бюджет приходят от клиента — но это видимое поле мастера и принятое продуктовое решение, а не недосмотр. Решает владелец. |
||
|
|
b8f75b2a0c |
fix реклама за показы: файл робота только по своему заданию, замок на баннерах, защита от двойного запуска
Р3 хвост. Адрес файла баннера стал /api/creative-robot/jobs/{jobId}/banners/{bannerId}/file.
Раньше портал подставлял «какое-нибудь задание в работе» — защита выдачи чужих картинок
держалась на внешнем условии, а не на самом запросе. Робота править не пришлось: он берёт
адрес из ответа портала как есть. Запись v9.08 в журнал схемы про частичный уникальный
индекс; rls-reviewer по миграции — GO.
Р4. Замок на наборе баннеров после заведения кампании в Яндексе: перезаливка, включение
и удаление отдают 409 по тому же признаку yandex_campaign_id, что и замок на параметрах.
Без него в портале была новая картинка, а в Яндексе крутилась старая, а удаление строки
с номером объявления заставляло возобновление завести второе объявление того же размера —
старое продолжало крутиться за деньги клиента. Баннер с номером объявления не удаляется
никогда.
Р5. Захват кампании под запуск: новый промежуточный статус launching, перевод под замком
строки в транзакции. Два одновременных нажатия «запустить» проходили проверку статуса оба
и заводили две CPM-кампании при одной заморозке денег. На любой ошибке прежний статус
возвращается, номера созданных в Яндексе сущностей уцелевают — возобновляемый запуск
не тронут. Брошенный захват старше пятнадцати минут перехватывается.
Тесты: портал 266/266, робот 35/35. Все новые защиты проверены вырезанием.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b29a4ca4c4 |
revert(смс-клиент): отказ получателя убран целиком — решение владельца
Владелец: «нет такой функции и задачи нет, забудь о ней! пришла и пришла смс».
Причина — приписка «Отказ: liderra.ru/s/…» ставила НАШ адрес в рекламное СМС, которое
клиент шлёт своим покупателям: он рекламирует себя, а не нас.
Убрано: страница отказа /s/{token}, таблица коротких ссылок, сервис токенов, приписка
в тексте рассылки, колонка with_optout_link, ограничение частоты sms-unsubscribe,
три файла тестов, три миграции (на прод не выкатывались).
Осталось нетронутым: стоп-лист самого клиента «Не писать этим» и общий стоп-лист
портала — это другое, их владелец не отменял.
Строки приёмочного листа 1.6-1.13 срезаны, записано в «Чего эта работа НЕ делает» п.14
и в журнал вопросов В-30. Возражение про 38-ФЗ высказано владельцу и им отклонено.
Всё удалённое лежит в истории: коммиты
|
||
|
|
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> |
||
|
|
f5483332e9 |
feat(смс-клиент): кнопка «Остановить» — после нажатия ни одного нового СМС
Клиент может остановить рассылку в очереди, на отправке и в ожидании утреннего окна. Нажатие — это отметка «попросил остановить», а не мгновенный обрыв: сообщение, начатое в этот момент, доводится до конца, сеть на полпути не рвём. Джоб читает отметку свежим запросом перед каждым следующим номером. Итог честный: статус «остановлена», причина «клиент», ушло столько, сколько в журнале, списано ровно за это, заморозка снята полностью. Номера, до которых не дошли, в журнал не пишутся — с ними ничего не произошло (В-27). Строки приёмочного листа 1.14-1.17 (серверная часть; кнопка на экране — Task 10). Защита проверена вырезанием: убрать выход из цикла — 2 красных теста. ClientSms 151/151, приём лидов 17/17, phpstan по своим файлам чисто. |
||
|
|
7aa3083399 |
feat(смс-клиент): страница отказа по ссылке из СМС — без входа, номер маской, с лимитом
Человек, получивший СМС, теперь может отказаться сам: короткая ссылка liderra.ru/s/<токен> открывает простую страницу с одной кнопкой. Нажал — номер в стоп-листе именно той компании, от которой пришло сообщение. - таблица client_sms_unsubscribe_links (RLS + tenant_isolation), одна ссылка на пару «клиент + номер» навсегда — при каждой рассылке новая не плодится - сервис токенов: 12 символов, без 0/O/o и 1/l/I (ссылку диктуют вслух) - публичная страница: blade без Vue-сборки, номер только маской +7 *** *** ** 67 - ограничение частоты 20/мин с IP — перебор ссылок иначе = утечка номеров; защита проверена вырезанием, тест на 429 краснеет - неизвестная ссылка отвечает 404 без подсказок - служебное соединение только в этом контроллере (тенант-контекста у страницы нет) + GRANT для crm_supplier_worker на client_sms_optouts - CHANGELOG схемы v9.02, с напоминанием ПЕРЕзапустить 03_service_bypass_policies.sql Строки приёмочного листа 1.6, 1.7, 1.8, 1.9 закрыты тестами; живой прогон — после выката, страницу надо открыть телефоном. Ветка feat/client-sms-broadcast, песочница, в main не влито. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3531b3a0cc |
feat(смс-клиент): раздел «Не писать этим» самого клиента — руками, файлом, с источником
Стоп-лист тенанта получил всё, чего ему не хватало: откуда взялся отказ (клиент / получатель / портал), комментарий зачем, загрузку файлом и внятный ответ про непонятые строки — образцами, а не молча. - миграция: client_sms_optouts += source (default client), note - модель: константы источников + fillable - API /api/sms/optouts: список, внесение руками, загрузка Excel, удаление - изоляция тенанта явным where поверх RLS, проверена вырезанием защиты - CHANGELOG схемы v9.01 Строки приёмочного листа 1.1-1.3 закрыты со стороны бэкенда; экран «Не писать этим» — Task 10, до него живого прогона по этим строкам быть не может. Открытый вопрос В-15 владельцу: вправе ли клиент снять отказ, который пришёл от самого получателя. Пока снимается любой — строго по строке 1.2. Ветка feat/client-sms-broadcast, песочница, в main не влито. Co-Authored-By: Claude Opus 5 (1M context) <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> |
||
|
|
a39694e11b |
feat(смс-клиент): общий стоп-лист портала — номер закрыт у всех клиентов сразу
Этап 1 «Нельзя обжечься», задачи 1–3 приёмочного листа (docs/superpowers/2026-07-27-PRIEMKA-client-sms-fixes.md, строки 1.4, 1.4а, 1.5). - новая таблица sms_global_optouts (SaaS-уровень, RLS намеренно нет: номер закрывается у всех тенантов сразу — защита договора с МТС при жалобе); - ClientSmsRecipientSelector отсеивает такой номер ПЕРВЫМ, раньше тенантского стоп-листа: наше обязательство перед оператором сильнее настроек клиента; - клиенту причина видна словами — «номер закрыт администрацией», а не молчаливое «не отправлено» (решение владельца, вопрос В-2 листа); - админ-адреса /api/admin/sms/global-optouts: внести (номер в любом виде), список, убрать; непонятый номер отклоняется внятно; - запись v9.00 в db/CHANGELOG_schema.md. Проверено: ClientSms 122/122, приём лидов 17/17, phpstan по своим файлам 0, gitleaks чисто. Защита доказана вырезанием — без отсева падают ровно 3 теста. Живого прогона строк 1.4/1.5 ещё нет: админ-экран идёт задачей 10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9234a9c2bc |
feat реклама показы: очередь заданий робота-грузчика, служебный канал и постановка при запуске
Портал ставит роботу задание, когда у баннеров ещё нет номеров креативов: вместо ошибки клиент видит «готовим картинки», кампания остаётся черновиком, деньги не морозятся. Робот берёт задания строго по одному — иначе слепки креативов до и после перемешаются, и опознать их будет нельзя. Канал робота закрыт своим сервис-токеном, внесён в исключения проверки CSRF и отдаёт файл только того задания, которое сейчас в работе. Постановка задания стоит внутри проверки рубильника Директа — при выключенном рубильнике портал в Яндекс не ходит. Права на новую таблицу выданы роли crm_admin_user: канал идёт через посредник admin-db, подменяющий подключение. Нумератор выдан crm_app_user — он единственный вставляет строки. Журнал схемы — запись v9.06. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
bc573a99eb |
feat(смс-клиент): своя база — загрузка Excel, пример файла, оператор через ДаДату, отброшенные не молча
Доработки экрана «Моя база» клиентской СМС-рассылки по просьбе владельца: - Кривые номера больше не выбрасываются молча: uploadContacts/uploadContactsFile возвращают rejected + rejected_samples, экран показывает «Добавлено: N, не распознали: M (например: …) — проверьте формат». - Таблица базы — только «Номер» и «Оператор» (колонку «Имя» убрал). - Загрузка базы Excel-файлом: сервис ClientSmsPhoneFileReader (phpspreadsheet) читает первую колонку, пропускает заголовок, номера-как-числа не уходят в научную нотацию; endpoint POST /api/sms/contacts/file; на экране — выбор файла и кнопка «Загрузить файл». - «Скачать пример файла»: ClientSmsBaseExampleWriter + GET /api/sms/contacts/example — готовый .xlsx с колонкой «Телефон» и образцами номеров. - Оператор через ДаДату: EnrichClientSmsContactsOperatorJob обогащает добавленные номера (DaDataPhoneClient.provider) в фоне, под tenant-контекстом (RLS), с защитой дневного бюджета (DaDataBudgetGuard); сбой ДаДаты не роняет прогон, оператор остаётся пустым; уже известного оператора не перезапрашиваем. Тесты: бэкенд ClientSms 130/130, фронт СМС-спеки 63/63 — зелёные; pint чист; портал пересобран. Синтетические номера 7999… в тестах. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c52fedae9d |
feat(смс-клиент): имя отправителя регистрируем мы — разрешение по образцу + документ-основание
Имя у оператора регистрирует Лидерра от лица клиента, поэтому от клиента два скана: подписанное разрешение и документ-основание. Оба обязательны, галочка согласия убрана. - Бланк разрешения — PDF по нашему образцу «Разрешение-домен УНИВЕРСАЛЬНОЕ»: Правообладатель = клиент, Пользователь = наш ИП из legal_entities is_default. 4 вида имени домен/юрлицо/ИП/товарный знак — у каждого своё основание права. - Физлицо: домен часто на физлицо — гейт по ФИО, паспорт вписывается от руки, паспорт не храним 152-ФЗ. - Две колонки client_sms_senders: doc_* документ-основание, consent_doc_* подписанное разрешение. - Админка: скачивание обоих сканов, вид .../document/basis и .../document/consent. - Гейт реквизитов requisites_ready; эндпоинт GET /api/sms/sender/consent-form. Тесты: ClientSms backend 95, фронт СМС 43, pint чисто. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2c8c876d51 |
feat(реклама): Часть 3b-2 — endpoint'ы баннеров (загрузка/превью/утверждение)
Часть 3b-2 из 6 (Часть 3 «баннеры» закрыта целиком).
- ad_campaigns.banners_approved_at (nullable) — момент утверждения набора; новая
загрузка сбрасывает в NULL.
- Endpoint'ы tenant-scoped: banner-source (1 картинка→15 баннеров), banners (список превью),
banners/{id}/preview (стрим приватного файла), banners/approve (флаг; пусто→422). Чужой→404.
Тесты: 22/22 зелёные (вкл. регресс). CHANGELOG v8.98.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
51b8f501ae |
feat(смс-клиент): бэкенд имени и авто-рассылки
Жизненный цикл имени: клиент requestSender (freeze первого месяца) / disableSender; админ approve (charge+release, active, paid_until+1мес) / reject / disable. Помесячная оплата ChargeSmsNameFeeJob (проверка средств ДО списания, долг>29 дней→suspended, идемпотентно по external_key), расписание в console.php. Авто-рассылка: DealSmsObserver (защитный, freshness-guard, НИКОГДА не роняет приём лида) → SendAutoSmsForDealJob (best-effort, идемпотентно по deal_id, песочница/деньги/маршрут). Действующее имя кампании = active-имя тенанта иначе liderra.ru. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cd77d32fc3 |
feat(реклама): удаление черновика кампании (T15) и загрузка своего списка номеров (T17)
DELETE /api/advertising/campaigns/{id} — удаляет кампанию только в статусе
draft своего тенанта (409 для запущенных/на паузе/др., 404 для чужого
тенанта); дочерние объявления и телефоны уходят каскадом на уровне схемы.
POST /api/advertising/campaigns/{id}/phones — принимает текстовое поле
или csv/txt-файл, номера построчно/через запятую нормализует через
PhoneNormalizer, невалидные отбрасывает, дубли схлопывает, пишет через
insertOrIgnore (unique tenant_id+campaign_id+phone), отвечает
{recognized, skipped}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d1a4c28dc1 |
feat(смс-клиент): API рассылок/базы/шаблонов + админ-тарифы + маршруты
ClientSmsController (index/preview/store/show, база контактов, шаблоны; tenant по $request->user()->tenant_id + явный where; 409 при нехватке денег, freeze при создании). AdminSmsTariffController (правка ступеней и настроек, зона saas-admin/admin-db, crm_admin_user). Маршруты /api/sms и /api/admin/sms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
566028ed9d | feat(реклама): админ-API расход/маржа по тенантам (ad_wallet_transactions charge) | ||
|
|
bdc1d09475 | feat(реклама): пауза/возобновление кампании клиента (Директ suspend/resume) |