set('client_tg.sandbox', false); }); function crCampaign(int $tenantId, string $status, ?string $mtsId = null): Campaign { $campaign = Campaign::query()->create([ 'tenant_id' => $tenantId, 'status' => $status, 'mts_campaign_id' => $mtsId, 'ad_text' => 'Приходите к нам за услугой', 'ad_link' => 'https://example.test/promo', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '500.00', 'audience_kind' => Campaign::AUDIENCE_LIST, 'planned_count' => 2, 'estimated_cost_rub' => '500.00', 'created_by' => 1, ]); foreach (['79990000001', '79990000002'] as $phone) { CampaignPhone::query()->create(['tenant_id' => $tenantId, 'campaign_id' => $campaign->id, 'phone' => $phone]); } return $campaign; } function crMockRobot(RobotResult $result): void { $mock = Mockery::mock(TelegramRobotRunner::class); $mock->shouldReceive('run')->andReturn($result); app()->instance(TelegramRobotRunner::class, $mock); } /** Держится ли ещё заморозка под кампанию (1 — держится, 0 — отпущена). */ function crHoldActive(int $tenantId, int $campaignId): int { return AdWalletHold::where('tenant_id', $tenantId) ->where('channel', 'telegram') ->where('source_type', 'client_tg_campaign') ->where('source_id', $campaignId) ->where('status', AdWalletHold::STATUS_ACTIVE) ->count(); } it('finalize: провал робота с id кабинета → needs_review, списание держится, refund НЕ произошёл', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = crCampaign($tenant->id, Campaign::STATUS_QUEUED); tgKoshelekTopup($tenant->id, '1000.00'); app(TelegramCampaignChargeService::class)->freeze($campaign, (string) $campaign->estimated_cost_rub); crMockRobot(RobotResult::failed('Оборвалось на подтверждении', 'confirm', '2231200')); RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id); $fresh = Campaign::find($campaign->id); expect($fresh->status)->toBe(Campaign::STATUS_NEEDS_REVIEW) ->and($fresh->mts_campaign_id)->toBe('2231200') ->and(crHoldActive($tenant->id, $campaign->id))->toBe(1) // заморозку не отпускали ->and(tgKoshelekFrozen($tenant->id))->toBe('500.00'); // смета держится занятой }); it('finalize: провал робота БЕЗ id кабинета → failed, refund произошёл, баланс восстановлен', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = crCampaign($tenant->id, Campaign::STATUS_QUEUED); tgKoshelekTopup($tenant->id, '1000.00'); app(TelegramCampaignChargeService::class)->freeze($campaign, (string) $campaign->estimated_cost_rub); crMockRobot(RobotResult::failed('Мало номеров «не МТС»', 'upload')); RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id); $fresh = Campaign::find($campaign->id); expect($fresh->status)->toBe(Campaign::STATUS_FAILED) ->and(crHoldActive($tenant->id, $campaign->id))->toBe(0) // заморозка отпущена ->and(tgKoshelekFrozen($tenant->id))->toBe('0.00') // ничего не занято ->and(tgKoshelekBalance($tenant->id))->toBe('1000.00'); // и ни рубля не списано }); it('failed(): running с id кабинета → needs_review, списание держится, refund НЕ произошёл', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = crCampaign($tenant->id, Campaign::STATUS_RUNNING, mtsId: '2231201'); tgKoshelekTopup($tenant->id, '1000.00'); app(TelegramCampaignChargeService::class)->freeze($campaign, (string) $campaign->estimated_cost_rub); (new RunTelegramCampaignJob($campaign->id, $tenant->id))->failed(new RuntimeException('boom')); $fresh = Campaign::find($campaign->id); expect($fresh->status)->toBe(Campaign::STATUS_NEEDS_REVIEW) ->and(crHoldActive($tenant->id, $campaign->id))->toBe(1) ->and(tgKoshelekFrozen($tenant->id))->toBe('500.00'); }); it('failed(): running без id кабинета → failed, refund произошёл, баланс восстановлен', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = crCampaign($tenant->id, Campaign::STATUS_RUNNING); tgKoshelekTopup($tenant->id, '1000.00'); app(TelegramCampaignChargeService::class)->freeze($campaign, (string) $campaign->estimated_cost_rub); (new RunTelegramCampaignJob($campaign->id, $tenant->id))->failed(new RuntimeException('boom')); $fresh = Campaign::find($campaign->id); expect($fresh->status)->toBe(Campaign::STATUS_FAILED) ->and(crHoldActive($tenant->id, $campaign->id))->toBe(0) ->and(tgKoshelekBalance($tenant->id))->toBe('1000.00'); }); it('failed(): уже moderating → статус не трогаем, refund НЕ произошёл', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = crCampaign($tenant->id, Campaign::STATUS_MODERATING, mtsId: '2231202'); tgKoshelekTopup($tenant->id, '1000.00'); app(TelegramCampaignChargeService::class)->freeze($campaign, (string) $campaign->estimated_cost_rub); (new RunTelegramCampaignJob($campaign->id, $tenant->id))->failed(new RuntimeException('boom')); $fresh = Campaign::find($campaign->id); expect($fresh->status)->toBe(Campaign::STATUS_MODERATING) ->and(crHoldActive($tenant->id, $campaign->id))->toBe(1) ->and(tgKoshelekFrozen($tenant->id))->toBe('500.00'); });