$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('keeps charged_client_rub monotonic when a later report returns fewer impressions', 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, 6000); // 6000 × 120/1000 = 720.00 $charger->charge($campaign->refresh(), 2500); // отчёт «просел» до 2500 — база НЕ опускается $campaign->refresh(); // charged_client_rub держится на high-water 720.00, а не откатывается к 300.00. expect($campaign->charged_client_rub)->toBe('720.00') ->and($campaign->delivered_impressions)->toBe(2500); // метрика показов отражает факт $wallet = AdWallet::where('tenant_id', $tenant->id)->first(); expect($wallet->balance_rub)->toBe('9280.00'); // 10000 − 720, повторно не списали // Повторный рост до 6000 не даёт двойного списания (external_key уже был). $charger->charge($campaign->refresh(), 6000); $campaign->refresh(); expect($campaign->charged_client_rub)->toBe('720.00'); expect(AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub)->toBe('9280.00'); }); /** * ВЫХОД 1, второе условие — срок показа истёк. * * Медийка по списку телефонов почти всегда НЕ добирает смету: аудитория ограничена, * частота показов ограничена. Без этого условия статус вечно оставался бы `running`, * а заморозка — ACTIVE навсегда: деньги клиента заперты в кампании, которая уже * ничего не показывает (EndDate в Директе прошёл, delivered замер). */ it('completes an under-delivered campaign once its show period has ended and releases the hold', function () { $tenant = Tenant::factory()->create(); $svc = app(AdWalletService::class); $svc->topup($tenant->id, '10000.00', 'yandex', 'тест'); $campaign = makePokazyCampaign($tenant->id, 10000); $campaign->update(['shows_until' => now()->subDay()->toDateString()]); $svc->freeze($tenant->id, 'yandex', 'campaign', $campaign->id, '1200.00'); app(CampaignImpressionCharger::class)->charge($campaign, 2500); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_COMPLETED) ->and($campaign->charged_client_rub)->toBe('300.00'); $wallet = AdWallet::where('tenant_id', $tenant->id)->first(); expect($wallet->frozen_rub)->toBe('0.00') // остаток сметы разморожен ->and($wallet->balance_rub)->toBe('9700.00'); // списано только за фактические показы }); it('keeps the campaign running while its show period is still open', function () { $tenant = Tenant::factory()->create(); app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест'); $campaign = makePokazyCampaign($tenant->id, 10000); $campaign->update(['shows_until' => now()->addDay()->toDateString()]); app(CampaignImpressionCharger::class)->charge($campaign, 2500); expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_RUNNING); }); it('keeps the campaign running on the last day of the show period', function () { $tenant = Tenant::factory()->create(); app(AdWalletService::class)->topup($tenant->id, '10000.00', 'yandex', 'тест'); $campaign = makePokazyCampaign($tenant->id, 10000); $campaign->update(['shows_until' => now()->toDateString()]); // последний день — ещё крутится app(CampaignImpressionCharger::class)->charge($campaign, 2500); expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_RUNNING); }); 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); });