Files
portal/app/tests/Feature/ClientTg/MediaCenaTest.php
T

184 lines
7.5 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\ClientTg\Campaign;
use App\Models\ClientTg\Contact;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\Support\MtsMediaFixture;
/**
* Цена зависит от медиа — на всём пути от экрана до списания (решение владельца
* 02.08.2026 после замера кабинета МТС).
*
* 🔴 Что охраняем. Кабинет берёт за тысячу показов 480 ₽ без медиа, 720 ₽ с картинкой
* и 816 ₽ с видео (с НДС). До этой правки расчёт принимал ТОЛЬКО число показов, и
* объявление с видео продавалось по цене объявления без картинки — убыток молчал,
* потому что портал нигде не сравнивает свою цену с ценой МТС. Песочница выключена
* 02.08.2026, деньги живые.
*
* Клиентская цена = себестоимость × 1.40 ⇒ за 400 показов:
* без медиа 400 × 0.480 × 1.40 = 268.80 ₽
* картинка 400 × 0.720 × 1.40 = 403.20 ₽
* видео 400 × 0.816 × 1.40 = 456.96 ₽
*
* Синтетические файлы (Tests\Support\MtsMediaFixture) и номера 7999… — реальных ПДн нет.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
Storage::fake('local');
$this->withHeaders(['Accept' => 'application/json']);
$this->tenant = Tenant::factory()->create(['balance_rub' => '0.00']);
tgKoshelekTopup($this->tenant->id, '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();
expect(tgKoshelekFrozen($this->tenant->id))->toBe('456.96')
->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();
expect(tgKoshelekFrozen($this->tenant->id))->toBe('268.80');
});