Files
portal/app/tests/Feature/Advertising/CampaignImpressionChargerTest.php
T
Дмитрий 7d3ed2f6eb feat(реклама): Часть 6 — списание по факту показов + клиентский отчёт по показам
CampaignImpressionCharger: списание с рекламного кошелька за фактически
показанные показы (показано×120/1000, дельта от уже списанного, идемпотентно
по external_key yandex-imp:{id}:{billable}), режет по оплаченному, статус
completed при достижении оплаченного; bcmath, 6 boundary-тестов. Клиентский
отчёт CampaignReportDialog переведён с недельного бюджета на показы
(оплачено/показано/частота/потрачено); статусы queued/completed в списке и
отчёте. Маржа/yandex_cost клиенту не видны. Бэкенд 105/105, фронт 104/104.

Отложено до Части 4 (нужен Директ): джоб, тянущий фактические показы из отчёта
Директа и зовущий CampaignImpressionCharger; campaigns.suspend при стопе;
переделка админ-маржи с наценки-% на реальный yandex_cost_rub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 16:07:19 +03:00

131 lines
5.0 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\Models\AdCampaign;
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use App\Services\Advertising\CampaignImpressionCharger;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
function makePokazyCampaign(int $tenantId, ?int $paidImpressions): AdCampaign
{
return AdCampaign::create([
'tenant_id' => $tenantId,
'name' => 'Кампания за показы',
'status' => AdCampaign::STATUS_RUNNING,
'audience_days' => 30,
'use_uploaded_list' => true,
'frequency' => 15,
'estimated_impressions' => $paidImpressions,
'paid_impressions' => $paidImpressions,
]);
}
it('charges the client for actually delivered impressions', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 10000);
app(CampaignImpressionCharger::class)->charge($campaign, 2500);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('300.00')
->and($campaign->delivered_impressions)->toBe(2500)
->and($campaign->status)->toBe(AdCampaign::STATUS_RUNNING);
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('9700.00');
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(1);
});
it('does not double-charge when called again with the same delivered count', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 10000);
$charger = app(CampaignImpressionCharger::class);
$charger->charge($campaign, 2500);
$charger->charge($campaign->refresh(), 2500);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('300.00');
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(1);
});
it('charges only the delta on a later top-up call', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 10000);
$charger = app(CampaignImpressionCharger::class);
$charger->charge($campaign, 2500);
$charger->charge($campaign->refresh(), 6000);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('720.00')
->and($campaign->delivered_impressions)->toBe(6000);
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('9280.00'); // 10000 720
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(2);
});
it('marks the campaign completed once delivered reaches the paid amount', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 5000);
app(CampaignImpressionCharger::class)->charge($campaign, 5000);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('600.00')
->and($campaign->delivered_impressions)->toBe(5000)
->and($campaign->status)->toBe(AdCampaign::STATUS_COMPLETED);
});
it('caps billable impressions at the paid amount when delivered overshoots', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 5000);
app(CampaignImpressionCharger::class)->charge($campaign, 8000);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('600.00')
->and($campaign->delivered_impressions)->toBe(8000)
->and($campaign->status)->toBe(AdCampaign::STATUS_COMPLETED);
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('9400.00'); // 10000 600
});
it('charges nothing for zero delivered impressions', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 5000);
app(CampaignImpressionCharger::class)->charge($campaign, 0);
$campaign->refresh();
expect($campaign->charged_client_rub)->toBe('0.00')
->and($campaign->delivered_impressions)->toBe(0)
->and($campaign->status)->toBe(AdCampaign::STATUS_RUNNING);
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('10000.00');
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(0);
});