85 lines
3.6 KiB
PHP
85 lines
3.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Jobs\ClientTg\SweepStuckTelegramCampaignsJob;
|
||
|
|
use App\Models\AdWallet;
|
||
|
|
use App\Models\AdWalletHold;
|
||
|
|
use App\Models\ClientTg\Campaign;
|
||
|
|
use App\Models\Tenant;
|
||
|
|
use App\Services\Advertising\AdWalletService;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Tests\Concerns\SharesSupplierPdo;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ревью-фикс F4 (Мост #1): кампания не должна вечно висеть в `moderating`, если робот
|
||
|
|
* не может прочитать вердикт (сломались селекторы кабинета / кампанию удалили / id
|
||
|
|
* протух) — иначе бронь кошелька заперта бессрочно. Предохранитель по возрасту: старше
|
||
|
|
* окна `client_tg.moderation_stuck_hours` → `needs_review` для ручного разбора, БЕЗ
|
||
|
|
* возврата брони (кампания могла реально показываться → деньги вслепую не трогаем).
|
||
|
|
*/
|
||
|
|
uses(RefreshDatabase::class, SharesSupplierPdo::class);
|
||
|
|
|
||
|
|
/** Кампания на модерации с состаренным updated_at (в часах). */
|
||
|
|
function modStuckCampaign(int $tenantId, ?string $mtsId, int $ageHours): Campaign
|
||
|
|
{
|
||
|
|
$c = Campaign::query()->create([
|
||
|
|
'tenant_id' => $tenantId,
|
||
|
|
'status' => Campaign::STATUS_MODERATING,
|
||
|
|
'mts_campaign_id' => $mtsId,
|
||
|
|
'ad_text' => 'Мод-стак-тест',
|
||
|
|
'ord_category' => 'Размещение рекламы',
|
||
|
|
'budget_cap_rub' => '1000.00',
|
||
|
|
'audience_kind' => Campaign::AUDIENCE_LIST,
|
||
|
|
'planned_count' => 0,
|
||
|
|
'estimated_cost_rub' => '0.00',
|
||
|
|
'created_by' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
if ($ageHours > 0) {
|
||
|
|
DB::table('client_tg_campaigns')->where('id', $c->id)
|
||
|
|
->update(['updated_at' => now()->subHours($ageHours)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $c;
|
||
|
|
}
|
||
|
|
|
||
|
|
function modStuckWallet(int $tenantId, int $campaignId, string $balance, string $freeze): void
|
||
|
|
{
|
||
|
|
AdWallet::query()->create(['tenant_id' => $tenantId, 'balance_rub' => $balance, 'frozen_rub' => '0.00']);
|
||
|
|
app(AdWalletService::class)->freeze($tenantId, 'telegram', 'campaign', $campaignId, $freeze);
|
||
|
|
}
|
||
|
|
|
||
|
|
it('moderating допускает переход в needs_review', function () {
|
||
|
|
$t = Tenant::factory()->create();
|
||
|
|
$c = modStuckCampaign($t->id, '2231160', 0);
|
||
|
|
|
||
|
|
expect($c->canTransitionTo(Campaign::STATUS_NEEDS_REVIEW))->toBeTrue();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('moderating дольше окна → needs_review, бронь НЕ возвращается', function () {
|
||
|
|
config()->set('client_tg.sandbox', false);
|
||
|
|
config()->set('client_tg.moderation_stuck_hours', 24);
|
||
|
|
$t = Tenant::factory()->create();
|
||
|
|
$c = modStuckCampaign($t->id, '2231161', ageHours: 25);
|
||
|
|
modStuckWallet($t->id, $c->id, '5000.00', '1000.00');
|
||
|
|
|
||
|
|
SweepStuckTelegramCampaignsJob::dispatchSync();
|
||
|
|
|
||
|
|
expect(Campaign::find($c->id)->status)->toBe(Campaign::STATUS_NEEDS_REVIEW)
|
||
|
|
->and((string) AdWallet::where('tenant_id', $t->id)->value('frozen_rub'))->toBe('1000.00')
|
||
|
|
->and(AdWalletHold::where('source_id', $c->id)->value('status'))->toBe(AdWalletHold::STATUS_ACTIVE);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('свежая moderating (в пределах окна) не трогается', function () {
|
||
|
|
config()->set('client_tg.sandbox', false);
|
||
|
|
config()->set('client_tg.moderation_stuck_hours', 24);
|
||
|
|
$t = Tenant::factory()->create();
|
||
|
|
$c = modStuckCampaign($t->id, '2231162', ageHours: 2); // 2ч < 24ч
|
||
|
|
|
||
|
|
SweepStuckTelegramCampaignsJob::dispatchSync();
|
||
|
|
|
||
|
|
expect(Campaign::find($c->id)->status)->toBe(Campaign::STATUS_MODERATING);
|
||
|
|
});
|