withHeaders(['Accept' => 'application/json']); $this->tenant = Tenant::factory()->create(['balance_rub' => '5000.00']); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); config(['client_tg.markup' => '1.40']); }); /** Черновик кампании «по своей базе». */ function chernovikCena(mixed $test, int $planned = 0): Campaign { return Campaign::create([ 'tenant_id' => $test->tenant->id, 'status' => Campaign::STATUS_DRAFT, 'ad_text' => 'Приходите к нам', 'ad_link' => 'https://t.me/example', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '5000.00', 'audience_kind' => Campaign::AUDIENCE_BASE, 'planned_count' => $planned, 'estimated_cost_rub' => '0.00', 'created_by' => $test->user->id, ]); } /** Набивает базу контактов, чтобы кампания прошла порог площадки. */ function kontaktyCena(mixed $test, int $skolko): void { for ($i = 0; $i < $skolko; $i++) { Contact::create([ 'tenant_id' => $test->tenant->id, 'phone' => sprintf('7999%07d', $i), 'name' => null, 'operator' => null, ]); } } // ── Вид медиа запоминается при загрузке ───────────────────────────────────── it('картинка помечается как image', function () { $campaign = chernovikCena($this); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => MtsMediaFixture::kartinka(), ])->assertOk(); expect($campaign->fresh()->media_kind)->toBe(Campaign::MEDIA_IMAGE); }); it('видео помечается как video', function () { $campaign = chernovikCena($this); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => MtsMediaFixture::video(), ])->assertOk(); expect($campaign->fresh()->media_kind)->toBe(Campaign::MEDIA_VIDEO); }); it('черновик без медиа считается как «без медиа»', function () { expect(chernovikCena($this)->media_kind)->toBeNull(); }); // ── Смета на экране пересчитывается сразу после загрузки ──────────────────── it('после загрузки видео смета кампании растёт с 268.80 до 456.96', function () { $campaign = chernovikCena($this, 400); $campaign->update(['estimated_cost_rub' => '268.80']); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => MtsMediaFixture::video(), ])->assertOk(); expect((string) $campaign->fresh()->estimated_cost_rub)->toBe('456.96'); }); it('замена видео на картинку удешевляет смету обратно', function () { $campaign = chernovikCena($this, 400); $this->post("/api/telegram/campaigns/{$campaign->id}/media", ['media' => MtsMediaFixture::video()])->assertOk(); expect((string) $campaign->fresh()->estimated_cost_rub)->toBe('456.96'); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => MtsMediaFixture::kartinka(), ])->assertOk(); expect($campaign->fresh()->media_kind)->toBe(Campaign::MEDIA_IMAGE) ->and((string) $campaign->fresh()->estimated_cost_rub)->toBe('403.20'); }); // ── Живой счётчик на экране знает про медиа ───────────────────────────────── it('оценка охвата считает цену по заявленному медиа', function () { kontaktyCena($this, 400); $bez = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ]); $kartinka = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, 'media_kind' => Campaign::MEDIA_IMAGE, ]); $video = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, 'media_kind' => Campaign::MEDIA_VIDEO, ]); expect($bez->json('estimated_cost_rub'))->toBe('268.80') ->and($kartinka->json('estimated_cost_rub'))->toBe('403.20') ->and($video->json('estimated_cost_rub'))->toBe('456.96'); }); it('выдуманный вид медиа в оценке не проходит', function () { kontaktyCena($this, 400); $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, 'media_kind' => 'gif', ])->assertStatus(422); }); // ── Списание при запуске идёт по медиа, а не по «просто показам» ──────────── it('запуск кампании с видео списывает 456.96, а не 268.80', function () { config(['client_tg.sandbox' => false]); kontaktyCena($this, 400); Queue::fake(); $campaign = chernovikCena($this, 400); $this->post("/api/telegram/campaigns/{$campaign->id}/media", ['media' => MtsMediaFixture::video()])->assertOk(); $this->postJson("/api/telegram/campaigns/{$campaign->id}/launch")->assertOk(); DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenant->id); $spisano = (string) BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->value('amount_rub'); expect(bccomp(ltrim($spisano, '-'), '456.96', 2))->toBe(0) ->and((string) $campaign->fresh()->estimated_cost_rub)->toBe('456.96'); }); it('запуск кампании без медиа списывает 268.80', function () { config(['client_tg.sandbox' => false]); kontaktyCena($this, 400); Queue::fake(); $campaign = chernovikCena($this, 400); $this->postJson("/api/telegram/campaigns/{$campaign->id}/launch")->assertOk(); DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenant->id); $spisano = (string) BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->value('amount_rub'); expect(bccomp(ltrim($spisano, '-'), '268.80', 2))->toBe(0); });