Files
portal/app/tests/Feature/Advertising/CampaignImpressionChargerTest.php
T
Дмитрий 5a4c0e0235 feat(реклама): показы — баннеры клиента по размерам, два режима аудитории, клиентская цена и наценка
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.

Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.

Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.

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

161 lines
6.5 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;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
function makePokazyCampaign(int $tenantId, ?int $paidImpressions, ?string $clientCpmRub = null): 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,
'client_cpm_rub' => $clientCpmRub,
]);
}
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 by the campaign own CPM when client_cpm_rub is set, not the global default', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makePokazyCampaign($tenant->id, 10000, '90.00');
app(CampaignImpressionCharger::class)->charge($campaign, 2500);
$campaign->refresh();
// 2500 показов × 90.00₽/1000 = 225.00 (не 300.00, как было бы по глобальной цене 120.00).
expect($campaign->charged_client_rub)->toBe('225.00');
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect($wallet->balance_rub)->toBe('9775.00');
});
it('records our margin-adjusted yandex cost alongside the client charge (40% margin default)', 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();
// charged_client_rub = 300.00 (дефолт 120.00₽/1000), margin 40% → yandex_cost_rub = 60% от 300.00 = 180.00.
expect($campaign->charged_client_rub)->toBe('300.00')
->and(DB::table('ad_campaigns')->where('id', $campaign->id)->value('yandex_cost_rub'))->toBe('180.00');
});
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);
});