Commit Graph

2 Commits

Author SHA1 Message Date
Дмитрий 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>
2026-07-28 07:28:16 +03:00
Дмитрий 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>
2026-07-27 16:51:12 +03:00