Files
portal/app/tests/Feature/ClientTg/AutoMediaCenaTest.php
T
Дмитрий 3864f6f11a feat,телеграм-реклама: цена показа по виду медиа вместо ступеней по объёму
Мы продавали дешевле, чем покупали. Тариф давал скидку за объём — 0.45 → 0.36 ₽
за показ, — а у МТС такой скидки нет: прайс кабинета, стр. 13, берёт 0,48 ₽ за
показ плоско. Скидку давали мы, а нам её не давал никто: на объёме от 50 000
наценка 1.40 превращалась в 5%.

Второе. Кабинет показывает CPM БЕЗ НДС — колонка списка так и названа. Счёт
кампании 2231134 сошёлся: 420 × 400 ₽/1000 × 1,2 = 201,60 ₽. Мы ИП на УСН, НДС
не возмещается, это расход. Себестоимость с НДС: 0.480 без медиа, 0.720 с
картинкой, 0.816 с видео.

Третье. Вид медиа до расчёта не доходил вовсе — объявление с видео продавалось
по цене объявления без картинки. На видео уходили в минус до 176 ₽ с тысячи, и
портал нигде свою цену с ценой МТС не сравнивал. Песочница выключена с 02.08 —
деньги живые.

Что сделано:
- миграция client_tg_cena_po_media: min_qty → media_kind, точность цены 6,2 →
  6,3, три строки вместо пяти ступеней, media_kind на кампании и авто-правиле;
- вид медиа определяется по СОДЕРЖИМОМУ файла, а не по расширению имени;
- экран админки переделан: три фиксированные строки по видам медиа, рядом цена
  за тысячу для сверки с кабинетом, добавлять и удалять нечего;
- запись v9.65 в журнале схемы.

Замеры этой смены: телеграм-модуль 313 тестов 0 падений, экраны 242 файла
1841 тест 0 падений, статанализ 0, форматтер чисто. Проверка типов — 6 ошибок,
все в чужих файлах, были до нас.

Тест экрана тарифов проверен вырезанием, а не только зелёным: сломал передачу
media_kind — красный; сломал расчёт цены за тысячу — красный.

Осталось незакрытым: числа 0.720 и 0.816 — вывод по правилу «кабинет пишет без
НДС», а не замер. Счёт кампании с картинкой живьём не снимали, шаг «Стоимость»
недостижим без загрузки живых номеров. Замерите — правится одной строкой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:15:46 +03:00

119 lines
4.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\ClientTg\AutoRule;
use App\Models\ClientTg\Campaign;
use App\Models\Tenant;
use App\Models\User;
use App\Services\ClientTg\TelegramAutoAccumulator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\Support\MtsMediaFixture;
/**
* Авто-реклама тоже считает деньги по виду медиа (решение владельца 02.08.2026).
*
* 🔴 Почему отдельным файлом. Накопитель авто-режима — ТРЕТЬЕ место, где считается
* клиентская цена (первые два — создание кампании и её запуск). Разъедутся — экран
* покажет одно, спишется другое. У авто-правила своё медиа, и оно переносится в
* кампанию-пачку: если вид не перенести, пачка с видео будет продана как пачка
* без картинки.
*
* Числа: 400 показов × 0.816 ₽ × 1.40 = 456.96 ₽ с видео против 268.80 ₽ без медиа.
*
* Синтетические файлы и номера 7999… — реальных ПДн нет.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
Storage::fake('local');
$this->withHeaders(['Accept' => 'application/json']);
$this->tenant = Tenant::factory()->create(['balance_rub' => '50000.00']);
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
config(['client_tg.markup' => '1.40']);
});
/** Правило авто-рекламы с порогом заведомо выше, чем мы наберём (копим, не запускаем). */
function praviloAvtoCena(mixed $test, int $porog = 100000): AutoRule
{
return AutoRule::create([
'tenant_id' => $test->tenant->id,
'enabled' => true,
'ad_text' => 'Приходите к нам',
'ad_link' => 'https://t.me/example',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '5000.00',
'daily_limit_rub' => '5000.00',
'batch_threshold' => $porog,
'updated_by' => $test->user->id,
]);
}
it('видео в авто-правиле помечается как video', function () {
praviloAvtoCena($this);
$this->post('/api/telegram/auto-rule/media', [
'media' => MtsMediaFixture::video(),
])->assertOk();
expect(AutoRule::where('tenant_id', $this->tenant->id)->first()->media_kind)
->toBe(Campaign::MEDIA_VIDEO);
});
it('картинка в авто-правиле помечается как image', function () {
praviloAvtoCena($this);
$this->post('/api/telegram/auto-rule/media', [
'media' => MtsMediaFixture::kartinka(),
])->assertOk();
expect(AutoRule::where('tenant_id', $this->tenant->id)->first()->media_kind)
->toBe(Campaign::MEDIA_IMAGE);
});
it('вид медиа переносится из правила в кампанию-пачку', function () {
praviloAvtoCena($this);
$this->post('/api/telegram/auto-rule/media', [
'media' => MtsMediaFixture::video(),
])->assertOk();
app(TelegramAutoAccumulator::class)->accumulate($this->tenant->id, '79990000001');
$pachka = Campaign::where('tenant_id', $this->tenant->id)->whereNull('created_by')->first();
expect($pachka)->not->toBeNull()
->and($pachka->media_kind)->toBe(Campaign::MEDIA_VIDEO);
});
it('смета пачки с видео считается по видео, а не по «просто показам»', function () {
praviloAvtoCena($this);
$this->post('/api/telegram/auto-rule/media', [
'media' => MtsMediaFixture::video(),
])->assertOk();
$nakopitel = app(TelegramAutoAccumulator::class);
for ($i = 0; $i < 400; $i++) {
$nakopitel->accumulate($this->tenant->id, sprintf('7999%07d', $i));
}
$pachka = Campaign::where('tenant_id', $this->tenant->id)->whereNull('created_by')->first();
expect($pachka->planned_count)->toBe(400)
->and((string) $pachka->estimated_cost_rub)->toBe('456.96');
});
it('пачка без медиа считается по цене без медиа', function () {
praviloAvtoCena($this);
$nakopitel = app(TelegramAutoAccumulator::class);
for ($i = 0; $i < 400; $i++) {
$nakopitel->accumulate($this->tenant->id, sprintf('7999%07d', $i));
}
$pachka = Campaign::where('tenant_id', $this->tenant->id)->whereNull('created_by')->first();
expect((string) $pachka->estimated_cost_rub)->toBe('268.80');
});