Files
portal/app/tests/Feature/Advertising/AdStopNoFundsChargesFirstTest.php
T

101 lines
5.0 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Events\AdvertisingStopped;
use App\Models\AdCampaign;
use App\Models\AdWallet;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Http;
// Откат после теста. Без него записи файла остаются в базе и роняют ЧУЖИЕ тесты.
uses(DatabaseTransactions::class);
/**
* ВЫХОД 3 — «Остановлено, нет денег».
*
* 🔴 Дыра, найденная 04.08.2026 рядом с той же дырой в паузе (§6и отчёта приёмки).
* Когда у клиента кончались деньги, портал глушил рекламу в Яндексе, ставил статус
* «Остановлено, нет денег» и БЕЗУСЛОВНО возвращал заморозку — не заплатив за показы,
* которые уже случились. Списание за показы делает часовая задача, и она берёт ТОЛЬКО
* кампании со статусом «крутится»: остановленную она пропускает навсегда.
*
* Итог: показы последнего часа уходили клиенту даром, а Яндексу за них платили мы.
* Ровно то же, что чинили в паузе, только через другую дверь.
*
* Правило то же: СПЕРВА заплати за показанное, ПОТОМ отпускай деньги. Не смог узнать
* число показов — деньги не отпускай вовсе: отпустить, не заплатив, значит подарить.
*/
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
config(['services.yandex_direct.enabled' => true]);
config(['services.yandex_direct.token' => 'DIRTOKEN']);
config(['services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com']);
});
it('остановка без денег сперва платит за показанное, потом отпускает заморозку', function () {
Http::fake([
'*/json/v5/campaigns' => Http::response(['result' => ['SuspendResults' => [['Id' => 777]]]]),
'*/json/v5/reports' => Http::response('2500', 200), // накопительно за всё время
]);
$svc = app(AdWalletService::class);
$svc->topup($this->tenant->id, '3000.00', 'yandex', 'тест');
$campaign = AdCampaign::create([
'tenant_id' => $this->tenant->id,
'name' => 'Кончились деньги на ходу',
'audience_days' => 10,
'status' => AdCampaign::STATUS_RUNNING,
'yandex_campaign_id' => 777,
'estimated_impressions' => 10000,
'paid_impressions' => 10000,
'charged_client_rub' => '0.00',
]);
$svc->freeze($this->tenant->id, 'yandex', 'campaign', $campaign->id, '1200.00');
event(new AdvertisingStopped($this->tenant->id));
$wallet = AdWallet::where('tenant_id', $this->tenant->id)->first();
// 2500 показов × 120 ₽ за тысячу = 300 ₽ — они обязаны уйти с баланса.
expect((string) $campaign->fresh()->charged_client_rub)->toBe('300.00')
->and((string) $wallet->balance_rub)->toBe('2700.00')
->and((string) $wallet->frozen_rub)->toBe('0.00')
->and($campaign->fresh()->status)->toBe(AdCampaign::STATUS_STOPPED_NO_FUNDS);
});
it('не узнали число показов — рекламу глушим, но деньги НЕ отпускаем', function () {
Http::fake([
'*/json/v5/campaigns' => Http::response(['result' => ['SuspendResults' => [['Id' => 778]]]]),
'*/json/v5/reports' => Http::response(['error' => ['error_detail' => 'нет доступа']], 400),
]);
$svc = app(AdWalletService::class);
$svc->topup($this->tenant->id, '3000.00', 'yandex', 'тест');
$campaign = AdCampaign::create([
'tenant_id' => $this->tenant->id,
'name' => 'Отчёт не отдался',
'audience_days' => 10,
'status' => AdCampaign::STATUS_RUNNING,
'yandex_campaign_id' => 778,
'estimated_impressions' => 10000,
'paid_impressions' => 10000,
'charged_client_rub' => '0.00',
]);
$svc->freeze($this->tenant->id, 'yandex', 'campaign', $campaign->id, '1200.00');
event(new AdvertisingStopped($this->tenant->id));
$wallet = AdWallet::where('tenant_id', $this->tenant->id)->first();
// Реклама заглушена — статус сменился.
expect($campaign->fresh()->status)->toBe(AdCampaign::STATUS_STOPPED_NO_FUNDS)
// Но заморозка на месте: отпустить, не заплатив, значит подарить показы.
->and((string) $wallet->frozen_rub)->toBe('1200.00')
->and((string) $campaign->fresh()->charged_client_rub)->toBe('0.00');
});