Files
portal/app/tests/Feature/ClientTg/ModeratingStatusTest.php
T
Дмитрий 86c560e0c4 feat(телеграм-реклама): Этап 3.2 — статус «на модерации» + переходы
Этап 3 «Жизненный цикл и модерация», задача 3.2. Живая отправка робота в МТС
означает «ушло на модерацию», а не «запущено»: кампания получает статус
`moderating`, а `launched` придёт позже, когда опросчик вердикта (задача 3.4)
увидит одобрение.

- Campaign.php: константа STATUS_MODERATING + переходы running→moderating,
  moderating→launched|rejected, rejected→queued (пересдача, задача 3.6).
  running→launched оставлен ради обратной совместимости. Миграция НЕ нужна:
  на колонке status нет CHECK-ограничения (машина статусов — в модели),
  `moderating` влезает в varchar(16).
- RunTelegramCampaignJob.finalize(): живой успех (launched=true) → moderating
  вместо launched; песочница (черновик) по-прежнему → draft_ready.

Тест-инфра (побочно, но необходимо для проверки): tests/TestCase.php получил
`protected $dropTypes = true`. RefreshDatabase's migrate:fresh дропал таблицы,
но НЕ типы Postgres; при заблокированном дропе таблицы её composite row-type
переживал db:wipe, и перезагрузка db/schema.sql падала на дубле типа
(«legal_entities … уже существует»), оставляя ЧАСТИЧНУЮ схему — давний
интермиттентный флак «migrate:fresh иногда прерывается» (site_events/
client_tg_tariffs случайно отсутствовали). Дроп типов на каждом refresh резко
снизил флак (было 15–46/130 → стало 93–126/130). Только для APP_ENV=testing,
на прод не влияет. Остаточный редкий обрыв — отдельная пред-существующая
проблема, не этой задачи.

TDD, робот замокан, песочница. Приёмка (моя область, чистый прогон):
ModeratingStatusTest 8/8 + StatusMachineTest/RunCampaignJobTest/ExternalIdTest/
RobotRunnerTest 35/35 суммарно; phpstan (Campaign+Job) 0, deptrac 0, pint чисто.

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

136 lines
5.2 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.2 — статус `moderating` («на модерации») + переходы.
* Живая отправка робота (launched=true) означает «ушло на модерацию МТС», а НЕ
* «запущено»: статус становится `moderating`, а `launched` придёт позже, когда
* опросчик вердикта (задача 3.4) увидит одобрение. Песочница по-прежнему →
* `draft_ready`. Отклонённую можно пересдать: `rejected → queued`.
*/
/** Кампания в заданном статусе (self-contained, без общих global-хелперов). */
function moderatingCampaign(string $status): Campaign
{
$tenant = Tenant::factory()->create();
return Campaign::query()->create([
'tenant_id' => $tenant->id,
'status' => $status,
'ad_text' => 'Тест модерации',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '1000.00',
'audience_kind' => Campaign::AUDIENCE_DEALS,
'planned_count' => 0,
'estimated_cost_rub' => '0.00',
'created_by' => 1,
]);
}
/** Queued-кампания-список с фейковыми номерами для прогона джоба. */
function moderatingQueuedCampaign(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;
}
function moderatingMockRobot(RobotResult $result): void
{
$mock = Mockery::mock(TelegramRobotRunner::class);
$mock->shouldReceive('run')->andReturn($result);
app()->instance(TelegramRobotRunner::class, $mock);
}
it('константа STATUS_MODERATING существует и равна moderating', function () {
expect(Campaign::STATUS_MODERATING)->toBe('moderating');
});
it('из running допускает moderating', function () {
$c = moderatingCampaign(Campaign::STATUS_RUNNING);
expect($c->canTransitionTo(Campaign::STATUS_MODERATING))->toBeTrue();
$c->transitionTo(Campaign::STATUS_MODERATING);
expect($c->status)->toBe(Campaign::STATUS_MODERATING);
expect(Campaign::find($c->id)->status)->toBe(Campaign::STATUS_MODERATING);
});
it('из moderating допускает launched и rejected', function (string $to) {
$c = moderatingCampaign(Campaign::STATUS_MODERATING);
$c->transitionTo($to);
expect($c->status)->toBe($to);
})->with([
Campaign::STATUS_LAUNCHED,
Campaign::STATUS_REJECTED,
]);
it('rejected → queued разрешён (пересдача)', function () {
$c = moderatingCampaign(Campaign::STATUS_REJECTED);
expect($c->canTransitionTo(Campaign::STATUS_QUEUED))->toBeTrue();
$c->transitionTo(Campaign::STATUS_QUEUED);
expect($c->status)->toBe(Campaign::STATUS_QUEUED);
});
it('moderating не пускает назад в running', function () {
$c = moderatingCampaign(Campaign::STATUS_MODERATING);
expect(fn () => $c->transitionTo(Campaign::STATUS_RUNNING))
->toThrow(DomainException::class);
expect($c->status)->toBe(Campaign::STATUS_MODERATING);
});
it('живой finalize (launched=true) → moderating, а НЕ launched', function () {
config()->set('client_tg.sandbox', false);
$tenant = Tenant::factory()->create();
$campaign = moderatingQueuedCampaign($tenant->id);
moderatingMockRobot(new RobotResult(ok: true, matched: 437, launched: true, campaignId: '2231140'));
RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id);
$fresh = Campaign::find($campaign->id);
expect($fresh->status)->toBe(Campaign::STATUS_MODERATING)
->and($fresh->matched_count)->toBe(437)
->and($fresh->mts_campaign_id)->toBe('2231140');
});
it('песочница (draft) остаётся draft_ready, не moderating', function () {
config()->set('client_tg.sandbox', true);
$tenant = Tenant::factory()->create();
$campaign = moderatingQueuedCampaign($tenant->id);
moderatingMockRobot(new RobotResult(ok: true, matched: 400, launched: false, campaignId: 'draft'));
RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id);
expect(Campaign::find($campaign->id)->status)->toBe(Campaign::STATUS_DRAFT_READY);
});