5211048bce6d362ce2a94c9bcab8f741663999db
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |