Files
portal/app/tests/Feature/ClientTg/ExternalIdTest.php
T
Дмитрий 73bd36b6ce feat(телеграм-реклама): Этап 3.1 — id кампании МТС хранится рано и при отказе (+ разведка экрана отказа)
Этап 3 «Жизненный цикл и модерация», задача 3.1. Плюс закрыта задача 3.0
(живая разведка экрана отказа) — вердикт МТС по кампании «займ» (2231134)
пришёл: «Отклонена». Разведка read-only, деньги не тронуты.

Задача 3.1 — колонка mts_campaign_id + РАННЕЕ и надёжное сохранение:
- Миграция client_tg_campaigns.mts_campaign_id (varchar(32) NULL, после
  status_reason) + запись CHANGELOG_schema v8.89 (предварит., ветка). RLS не
  меняется; rls-reviewer не требуется (nullable-колонка данных).
- Робот (Node): чистый хелпер parseCampaignId(url) в cabinet.js (покрыт
  тестом); runner.js захватывает id СРАЗУ после создания черновика (шаг
  аудитории) и печатает маркер MTS_CAMPAIGN_ID=<id> в stderr; id теперь
  идёт и в ветке ОТКАЗА (раньше терялся).
- Обёртка (PHP): TelegramRobotRunner восстанавливает id из stderr-маркера во
  всех путях (таймаут/непарсабельный вывод/JSON без id); RobotResult::failed
  принимает id.
- Джоб: finalize сохраняет mts_campaign_id при ЛЮБОМ исходе (успех/отказ), не
  затирая ранее сохранённый id. Метод failed() не трогали — туда результат не
  доходит (осознанный residual, закроют уборщик 3.1b и sweeper 3.3).

Разведка отказа (3.0) записана в bots/mts-telegram-ads/FLOW-FINDINGS.md:
причина показана текстом в слайд-модалке «Причины отклонения кампании»
(кнопка «Причины»); поля загрузки файла на экране отказа нет — документ
грузится через «Исправить» → шаг «Сообщение» → «Комментарий для модератора»;
кнопка пересдачи — «Исправить».

TDD, робот замокан, тесты на liderra_testing (номера 7999…).
Приёмка: Node 48/48 (npm test), Pest ExternalIdTest 2/2 + регрессия ClientTg
122/122, phpstan (4 боевых файла) 0, deptrac 0 нарушений, pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:49:01 +03:00

86 lines
3.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\ClientTg\RunTelegramCampaignJob;
use App\Models\ClientTg\Campaign;
use App\Models\ClientTg\CampaignPhone;
use App\Models\Tenant;
use App\Services\ClientTg\RobotResult;
use App\Services\ClientTg\TelegramRobotRunner;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
/**
* Задача 3.1 — РАННЕЕ и НАДЁЖНОЕ сохранение id кампании МТС (mts_campaign_id).
* Робот отдаёт числовой id черновика кабинета даже при последующем падении;
* джоб сохраняет его при ЛЮБОМ исходе (успех/отказ), чтобы опросчик вердикта и
* уборщик денег (следующие задачи Этапа 3) нашли реально ушедшую кампанию.
*
* Робот ЗАМОКАН, песочница (mode=draft, деньги не трогаем). Синтетические
* номера 7999… — реальные НИКОГДА.
*/
beforeEach(function () {
config()->set('client_tg.sandbox', true);
});
/** Кампания-список заданного статуса с двумя фейковыми номерами. */
function externalIdCampaign(int $tenantId): Campaign
{
$campaign = Campaign::query()->create([
'tenant_id' => $tenantId,
'status' => Campaign::STATUS_QUEUED,
'ad_text' => 'Приходите к нам за услугой',
'ad_link' => 'https://example.test/promo',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '1500.00',
'audience_kind' => Campaign::AUDIENCE_LIST,
'planned_count' => 2,
'estimated_cost_rub' => '0.00',
'created_by' => 1,
]);
foreach (['79990000001', '79990000002'] as $phone) {
CampaignPhone::query()->create([
'tenant_id' => $tenantId,
'campaign_id' => $campaign->id,
'phone' => $phone,
]);
}
return $campaign;
}
/** Подменяет раннер моком, возвращающим $result. */
function externalIdMockRobot(RobotResult $result): void
{
$mock = Mockery::mock(TelegramRobotRunner::class);
$mock->shouldReceive('run')->andReturn($result);
app()->instance(TelegramRobotRunner::class, $mock);
}
it('успех: mts_campaign_id сохранён, статус draft_ready', function () {
$tenant = Tenant::factory()->create();
$campaign = externalIdCampaign($tenant->id);
externalIdMockRobot(new RobotResult(ok: true, matched: 437, launched: false, campaignId: '2231134'));
RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id);
$fresh = Campaign::find($campaign->id);
expect($fresh->mts_campaign_id)->toBe('2231134')
->and($fresh->status)->toBe(Campaign::STATUS_DRAFT_READY);
});
it('отказ с id: mts_campaign_id сохранён, статус failed', function () {
$tenant = Tenant::factory()->create();
$campaign = externalIdCampaign($tenant->id);
externalIdMockRobot(RobotResult::failed('regulated-тема', 'ad', '2231134'));
RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id);
$fresh = Campaign::find($campaign->id);
expect($fresh->mts_campaign_id)->toBe('2231134')
->and($fresh->status)->toBe(Campaign::STATUS_FAILED);
});