true]); config(['services.yandex_direct.token' => 'DIRTOKEN']); config(['services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com']); } /** TSV: Clicks=7, Cost=200.00 ₽ (яндексовы рубли, до наценки). */ function fakeYandexReportsSpend(string $tsv = "7\t200.00"): 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, 'weekly_budget_rub' => '500.00', ]); } it('charges the client the marked-up cost of yesterday\'s Direct spend', function () { configureYandexDirectForSpend(); fakeYandexReportsSpend(); $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(); expect($wallet->balance_rub)->toBe('740.00'); // 1000.00 − 260.00 $date = Carbon::now()->subDay()->toDateString(); $tx = AdWalletTransaction::where('tenant_id', $tenant->id) ->where('external_key', "yandex:{$campaign->id}:{$date}") ->first(); expect($tx)->not->toBeNull() ->and($tx->type)->toBe(AdWalletTransaction::TYPE_CHARGE) ->and($tx->amount_rub)->toBe('-260.00'); }); it('does not double-charge on a second run (idempotent by external_key)', function () { configureYandexDirectForSpend(); fakeYandexReportsSpend(); $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('740.00'); }); it('stops all advertising when the charge leaves the tenant insolvent', function () { Event::fake([AdvertisingStopped::class]); configureYandexDirectForSpend(); fakeYandexReportsSpend(); $tenant = Tenant::factory()->create(); // Малый баланс, крупная заморозка → после списания 260.00 баланс уйдёт ниже frozen. app(AdWalletService::class)->topup($tenant->id, '300.00', 'yandex', 'тест'); $campaign = makeRunningCampaign($tenant->id); app(AdWalletService::class)->freeze($tenant->id, 'yandex', 'campaign', $campaign->id, '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]); fakeYandexReportsSpend(); $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'); });