Закрыты денежно-независимые куски Этапа 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>