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

127 lines
5.2 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Events\AdvertisingStopped;
use App\Jobs\ChargeCampaignSpendJob;
use App\Models\AdCampaign;
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Tests\Concerns\SharesSupplierPdo;
// Джоб перечисляет кампании через pgsql_supplier (BYPASSRLS) — без share PDO
// вставленные в тест-транзакции кампании (default pgsql-соединение) не видны
// второму соединению до commit'а. RefreshDatabase раньше не был подключён в
// этом файле — подключаем оба вместе (см. общее правило других Advertising-тестов).
uses(RefreshDatabase::class);
uses(SharesSupplierPdo::class);
function configureYandexDirectForSpend(): void
{
config(['services.yandex_direct.enabled' => true]);
config(['services.yandex_direct.token' => 'DIRTOKEN']);
config(['services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com']);
}
/** TSV: накопительное число показов за всё время кампании (ALL_TIME). */
function fakeYandexImpressionsReport(string $tsv = '2500'): void
{
Http::fake(['*/json/v5/reports' => Http::response($tsv, 200)]);
}
function makeRunningCampaign(int $tenantId): AdCampaign
{
return AdCampaign::create([
'tenant_id' => $tenantId,
'name' => 'Кампания списания',
'status' => AdCampaign::STATUS_RUNNING,
'yandex_campaign_id' => 222,
'audience_days' => 10,
'use_uploaded_list' => true,
'frequency' => 15,
'estimated_impressions' => 10000,
'paid_impressions' => 10000,
]);
}
it('charges the client for delivered impressions at the flat CPM (client_cpm_rub not set → default 120.00 ₽/1000)', function () {
configureYandexDirectForSpend();
fakeYandexImpressionsReport();
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '1000.00', 'yandex', 'тест');
$campaign = makeRunningCampaign($tenant->id);
app(ChargeCampaignSpendJob::class)->handle();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
// 2500 показов × 120.00 ₽/1000 = 300.00 ₽ клиенту.
expect($wallet->balance_rub)->toBe('700.00'); // 1000.00 300.00
$tx = AdWalletTransaction::where('tenant_id', $tenant->id)
->where('external_key', "yandex-imp:{$campaign->id}:2500")
->first();
expect($tx)->not->toBeNull()
->and($tx->type)->toBe(AdWalletTransaction::TYPE_CHARGE)
->and($tx->amount_rub)->toBe('-300.00');
});
it('does not double-charge on a second run (idempotent by external_key)', function () {
configureYandexDirectForSpend();
fakeYandexImpressionsReport();
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '1000.00', 'yandex', 'тест');
makeRunningCampaign($tenant->id);
app(ChargeCampaignSpendJob::class)->handle();
$balanceAfterFirst = AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub;
app(ChargeCampaignSpendJob::class)->handle();
$balanceAfterSecond = AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub;
expect($balanceAfterSecond)->toBe($balanceAfterFirst)
->and($balanceAfterSecond)->toBe('700.00');
});
it('stops all advertising when the charge leaves the tenant insolvent', function () {
Event::fake([AdvertisingStopped::class]);
configureYandexDirectForSpend();
fakeYandexImpressionsReport();
$tenant = Tenant::factory()->create();
// Заморозка ТАЕТ вместе со списанием по своей кампании, поэтому нехватку денег
// создаёт ЧУЖОЙ резерв: у клиента запущена вторая кампания с холдом 200.00 ₽,
// а первая съедает 300.00 ₽ из 400.00 ₽ → на вторую денег уже нет (100 < 200)
// → стоп всей рекламы тенанта.
app(AdWalletService::class)->topup($tenant->id, '400.00', 'yandex', 'тест');
$campaign = makeRunningCampaign($tenant->id);
app(AdWalletService::class)->freeze($tenant->id, 'yandex', 'campaign', $campaign->id + 1, '200.00');
app(ChargeCampaignSpendJob::class)->handle();
Event::assertDispatched(AdvertisingStopped::class, fn ($e) => $e->tenantId === $tenant->id);
});
it('does nothing and sends no HTTP when the yandex_direct switch is off', function () {
config(['services.yandex_direct.enabled' => false]);
fakeYandexImpressionsReport();
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '1000.00', 'yandex', 'тест');
makeRunningCampaign($tenant->id);
app(ChargeCampaignSpendJob::class)->handle();
Http::assertNothingSent();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('1000.00');
});