Files
portal/app/tests/Feature/ClientTg/ExternalIdTest.php
T
Дмитрий 68fe632cca fix(телеграм-реклама): правки по сводному код-ревью ветки — деньги, статус-машина, робот, RLS
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное.
Поведение в песочнице не меняется; правки готовят ветку к боевому включению.

F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по
  4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа
  23505 в боевом режиме.
F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200;
  длинное значение даёт ошибку поля, а не замаскированный 422 от БД.
F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и
  считает дневной лимит под lockForUpdate строки правила.
F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс
  предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем.
F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id
  значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони.
F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке
  domClickButton, finalize целит только кнопку отправки на модерацию.
F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment;
  не дошли до /payment → падаем громко.
F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком.
F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе
  кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в
  000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql.

Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали.

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

88 lines
3.7 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 сохранён, статус needs_review (могла уйти на модерацию — ревью-фикс F5)', 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);
// Черновик в кабинете реально создан (есть id) → кампания МОГЛА уйти на модерацию →
// needs_review, деньги вслепую не возвращаем (F5). Раньше было failed (риск оплаты показов).
expect($fresh->mts_campaign_id)->toBe('2231134')
->and($fresh->status)->toBe(Campaign::STATUS_NEEDS_REVIEW);
});