7a2b052f0d
Хвост «связка» из STATE. Раньше пересдача отклонённой кампании создавала в кабинете
МТС НОВУЮ кампанию (RunTelegramCampaignJob, очищала mts_campaign_id). Теперь портал
поручает роботу ЧИНИТЬ ту же кампанию: робот заходит в неё через «Исправить» по
mts_campaign_id, вносит правки и переотправляет на модерацию «без оплаты» (0 ₽).
Робот-режим mode:'resubmit' уже проверен живьём (коммит d50a93e5). Выбор поведения
(«чинить ту же», не «создавать новую») подтверждён владельцем.
Что сделано:
- RobotResult: поле resubmitted (bool) + разбор из JSON робота.
- TelegramRobotRunner::taskPayload: пробрасывает submitMode (draft|live для resubmit).
- ResubmitTelegramCampaignJob (новый): RLS через tenantTx (SET LOCAL current_tenant_id),
идемпотентность по queued, денежно-статусная логика F5 как в RunTelegramCampaignJob.
Успех: бой (resubmitted) → moderating, песочница → draft_ready. Отказ с mts_campaign_id
→ needs_review без возврата брони.
- Контроллер resubmit: mts_campaign_id СОХРАНЯЕТСЯ (не очищаем), guard на пустой id → 422,
dispatch ResubmitTelegramCampaignJob.
Код-ревью (субагент) поймало Important-баг, исправлено:
- I-1: failed() был скопирован из эталона, где «queued ⇒ нет mts_campaign_id». У пересдачи
queued ВСЕГДА с id → при перманентном сбое в queued срабатывала ветка hasMtsId →
queued→needs_review (перехода НЕТ в TRANSITIONS) → кампания зависала в queued с
замороженной бронью, уборщик её не метёт. Фикс: queued → failed + release (робот кабинет
не трогал — Фаза A не закоммитила running); running — прежняя осторожная логика.
- M-1: успех в бою с resubmitted=false (аномалия контракта) давал терминальный draft_ready
с зависшей бронью → needs_review (бронь под ручную сверку).
Приёмка: ResubmitJobTest + ResubmitTest вместе 15/15 (51 проверка); pint/phpstan(0)/
deptrac(0) чисто; схема БД не менялась. Детали и приёмочный лист:
docs/superpowers/2026-07-28-resubmit-svyazka-ACCEPT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>