2026-07-26 16:07:19 +03:00
|
|
|
|
<?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;
|
2026-07-26 21:13:20 +03:00
|
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-07-26 16:07:19 +03:00
|
|
|
|
|
|
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
function makePokazyCampaign(int $tenantId, ?int $paidImpressions, ?string $clientCpmRub = null): AdCampaign
|
2026-07-26 16:07:19 +03:00
|
|
|
|
{
|
|
|
|
|
|
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,
|
2026-07-26 21:13:20 +03:00
|
|
|
|
'client_cpm_rub' => $clientCpmRub,
|
2026-07-26 16:07:19 +03:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 16:07:19 +03: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);
|
|
|
|
|
|
});
|