tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); /** Столько контактов в своей базе тенанта. */ function zavestiKontakty(int $tenantId, int $skolko, int $ot = 0): void { for ($i = $ot; $i < $ot + $skolko; $i++) { Contact::create([ 'tenant_id' => $tenantId, 'phone' => sprintf('7999%07d', $i), 'name' => null, 'operator' => null, ]); } } it('🔴 считает охват и смету, НЕ создавая кампанию в базе', function () { zavestiKontakty($this->tenant->id, 5); $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ]); $otvet->assertOk()->assertJsonStructure(['planned_count', 'estimated_cost_rub', 'min_count', 'enough']); expect($otvet->json('planned_count'))->toBe(5); // Главное: в базе не осталось ни одного черновика. Иначе живой счётчик за минуту // работы клиента наплодил бы десятки кампаний-призраков. expect(Campaign::count())->toBe(0); }); it('смета совпадает с той, что даёт создание черновика — иначе счётчик врёт перед оплатой', function () { zavestiKontakty($this->tenant->id, 7); $ocenka = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); $chernovik = $this->postJson('/api/telegram/campaigns', [ 'ad_text' => 'Заходите', 'ad_link' => 'https://t.me/example', 'audience_kind' => Campaign::AUDIENCE_BASE, 'budget_cap_rub' => '1000.00', ])->assertCreated(); expect($ocenka->json('planned_count'))->toBe($chernovik->json('planned_count')); expect($ocenka->json('estimated_cost_rub'))->toBe($chernovik->json('estimated_cost_rub')); }); it('сделки за период: то, что старше окна, в охват не идёт', function () { Deal::factory()->create([ 'tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(2), ]); Deal::factory()->create([ 'tenant_id' => $this->tenant->id, 'phone' => '79990000002', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(30), ]); $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_DEALS, 'audience_days' => 5, ])->assertOk(); expect($otvet->json('planned_count'))->toBe(1); }); it('свой список: считает по присланным номерам и схлопывает дубли', function () { $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_LIST, 'phones' => ['79990000001', '8 999 000 00 01', '79990000002', 'ерунда'], ])->assertOk(); // Первые два — один и тот же человек в разных записях, третий отдельный, четвёртый не номер. expect($otvet->json('planned_count'))->toBe(2); // И тут тоже ничего не записалось — ни кампании, ни номеров. expect(Campaign::count())->toBe(0); }); it('отписавшихся в охват не считает — как и создание черновика', function () { zavestiKontakty($this->tenant->id, 3); Optout::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000000']); $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); expect($otvet->json('planned_count'))->toBe(2); }); it('чужих контактов не видит', function () { $chuzhoy = Tenant::factory()->create(); zavestiKontakty($chuzhoy->id, 40, 500); zavestiKontakty($this->tenant->id, 3); $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); expect($otvet->json('planned_count'))->toBe(3); }); it('говорит, хватает ли людей для площадки — 367 это её правило, не наше', function () { zavestiKontakty($this->tenant->id, 5); $malo = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); expect($malo->json('min_count'))->toBe(367); expect($malo->json('enough'))->toBeFalse(); zavestiKontakty($this->tenant->id, 400, 1000); $hvatit = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); expect($hvatit->json('enough'))->toBeTrue(); }); it('пустая своя база — честный ноль, а не отказ', function () { $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); expect($otvet->json('planned_count'))->toBe(0); expect($otvet->json('enough'))->toBeFalse(); }); it('незнакомый вид аудитории отвергается, а не считается нулём молча', function () { $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => 'chto-to-svoyo', ])->assertStatus(422); }); it('сделки без числа дней отвергаются — иначе молча посчитаем за всё время', function () { $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_DEALS, ])->assertStatus(422); }); it('без входа в кабинет ручка недоступна', function () { app('auth')->forgetGuards(); $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertStatus(401); }); /** * Цена за тысячу показов — ОТ СЕРВЕРА, а не вбитая в текст экрана. * * 🔴 Откуда взялось (приёмка владельца 03.08.2026). Подсказка «?» у поля медиа обещала * «600 ₽ за тысячу с картинкой, 680 ₽ с видео». Настоящие клиентские цены на тот момент — * 1008 и 1142,40 ₽: числа были вбиты в текст руками и протухли в ту же минуту, когда * миграция client_tg_cena_po_media поменяла тариф. Обещать клиенту цену вдвое ниже * настоящей — это не опечатка, это неверная цена в кабинете. * * Лечение: цену называет тот же, кто её считает. Экран не имеет права знать числа. */ it('оценка отдаёт клиентскую цену за тысячу по каждому виду медиа', function () { $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); // Себестоимость 0.480 / 0.720 / 0.816 ₽ за показ × наценка 1.40 × 1000 показов. expect($otvet->json('ceny_za_tysyachu.none'))->toBe('672.00'); expect($otvet->json('ceny_za_tysyachu.image'))->toBe('1008.00'); expect($otvet->json('ceny_za_tysyachu.video'))->toBe('1142.40'); }); it('цена за тысячу едет из таблицы тарифа — поменяли тариф, поменялся ответ', function () { Tariff::where('media_kind', Campaign::MEDIA_IMAGE)->update(['price_rub' => '1.000']); $otvet = $this->postJson('/api/telegram/campaigns/estimate', [ 'audience_kind' => Campaign::AUDIENCE_BASE, ])->assertOk(); // 1.000 × 1.40 × 1000 = 1400. Если бы число было вбито в код, ответ не сдвинулся бы. expect($otvet->json('ceny_za_tysyachu.image'))->toBe('1400.00'); // Соседние виды медиа при этом не поехали. expect($otvet->json('ceny_za_tysyachu.none'))->toBe('672.00'); });