Files
portal/bots/mts-telegram-ads/test/resubmit-task.test.js
T
Дмитрий d50a93e55c feat(телеграм-робот): боевой режим пересдачи отклонённой кампании (mode:resubmit) — проверено живьём
Хвост №1 из STATE ревью-правок: оформили доказанный в A2 путь пересдачи в
постоянный режим робота mode:'resubmit'. Робот берёт ОТКЛОНЁННУЮ кампанию, входит
в её редактор через «Исправить», вносит исправления и повторно отправляет на
модерацию БЕЗ ОПЛАТЫ (0 ₽ — деньги не списываются).

Что нового:
- parseResubmitTask (task.js): своё задание пересдачи — campaignId + submitMode
  (draft|live, без дефолта, чтобы live не случился сам) + правки на выбор
  (moderatorFile и/или adText/buttonUrl/ordCategory). Нужна хоть одна правка —
  иначе тот же контент снова отклонят. Номера/бюджет не нужны (уже у кампании).
- openResubmitEditor (cabinet.js): нативный клик «Исправить» в строке кампании по
  id → ждём редактор /telegram-a2p/{id}/message (в чужую кампанию не лезем).
- submitWithoutPayment (cabinet.js): на /payment жмём ТОЛЬКО «Отправить на
  модерацию без оплаты»; денежные кнопки («Списать…»/«Оплатить») — двойная защита
  через isForbiddenButtonText, не жмём никогда.
- editResubmitFields + вынос fillAd в хелперы (fillAdText/fillAdLink/
  selectOrdCategory/fillAdMedia): пересдача правит только заданные поля тем же
  проверенным кодом (DRY, поведение fillAd не изменилось).
- runResubmit (runner.js) + ветка mode:'resubmit' в bin/run.js (до parseTask, как
  read-status). draft — предохранитель (до /confirmation, не шлём); live — отправка
  без оплаты.

Живая проверка на реальных отклонённых кампаниях (28.07.2026):
- DRAFT (2231132, правка текста + документ): вошёл «Исправить» → правка → загрузка
  документа → /confirmation → НЕ отправил (resubmitted:false, stoppedAt:confirmation).
- LIVE (2231134, правка текст+ссылка+ОРД, без документа): полный цикл → «без оплаты»
  (0 ₽) → resubmitted:true; read-status подтвердил moderating. Баланс не тронут.

Робот npm test 110/110 (было 94, +16). Приёмочный лист и живые результаты:
docs/superpowers/2026-07-28-robot-resubmit-mode-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:43:58 +03:00

83 lines
3.5 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseResubmitTask } from '../src/task.js';
// Пересдача (mode:'resubmit', задача 3.6 → robot-mode): чинит ОТКЛОНЁННУЮ кампанию
// по её id и повторно шлёт на модерацию без оплаты (0 ₽). Что чиним — на выбор:
// документ модератору (moderatorFile) и/или правка текста/ссылки/категории ОРД.
const base = {
mode: 'resubmit',
campaignId: '2231134',
submitMode: 'draft',
moderatorFile: 'C:/tmp/licenziya.pdf',
};
test('parseResubmitTask принимает задание с документом модератору', () => {
const t = parseResubmitTask(base);
assert.equal(t.campaignId, '2231134');
assert.equal(t.moderatorFile, 'C:/tmp/licenziya.pdf');
assert.equal(t.submitMode, 'draft');
});
test('parseResubmitTask требует campaignId (какую кампанию чинить)', () => {
const { campaignId, ...rest } = base;
assert.throws(() => parseResubmitTask(rest), /campaignId/);
});
test('parseResubmitTask требует submitMode draft|live (без дефолта — чтобы live не случился сам)', () => {
const { submitMode, ...rest } = base;
assert.throws(() => parseResubmitTask(rest), /submitMode/);
});
test('parseResubmitTask отвергает чужой submitMode', () => {
assert.throws(() => parseResubmitTask({ ...base, submitMode: 'xxx' }), /submitMode/);
});
test('parseResubmitTask принимает submitMode live', () => {
const t = parseResubmitTask({ ...base, submitMode: 'live' });
assert.equal(t.submitMode, 'live');
});
test('parseResubmitTask пробрасывает правки текста/ссылки/ОРД', () => {
const t = parseResubmitTask({
mode: 'resubmit',
campaignId: '2231134',
submitMode: 'draft',
adText: 'Исправленный текст',
buttonUrl: 'https://t.me/liderra',
ordCategory: 'Другая категория',
});
assert.equal(t.adText, 'Исправленный текст');
assert.equal(t.buttonUrl, 'https://t.me/liderra');
assert.equal(t.ordCategory, 'Другая категория');
});
test('parseResubmitTask принимает правку ТОЛЬКО текста, без документа', () => {
const t = parseResubmitTask({
mode: 'resubmit', campaignId: '2231134', submitMode: 'draft', adText: 'Новый текст',
});
assert.equal(t.adText, 'Новый текст');
assert.equal(t.moderatorFile, null);
});
test('parseResubmitTask требует ХОТЬ ОДНО исправление (документ или правку поля)', () => {
// Пересдача без единой правки = тот же контент → снова отказ. Бессмысленно.
assert.throws(
() => parseResubmitTask({ mode: 'resubmit', campaignId: '2231134', submitMode: 'draft' }),
/исправлен|документ|текст/i,
);
});
test('parseResubmitTask: незаданные поля правок → null', () => {
const t = parseResubmitTask(base); // только moderatorFile
assert.equal(t.adText, null);
assert.equal(t.buttonUrl, null);
assert.equal(t.ordCategory, null);
});
test('parseResubmitTask НЕ требует phonesFile/budgetRub (аудитория и стоимость уже у кампании)', () => {
const t = parseResubmitTask(base);
assert.equal(t.phonesFile, undefined);
assert.equal(t.budgetRub, undefined);
});