Files
portal/app/tests/Feature/ClientTg/TariffServiceTest.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

112 lines
5.9 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\Campaign;
use App\Services\ClientTg\TelegramTariffService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
/**
* 🔴 Замер живьём 02.08.2026 (кабинет МТС + прайс «Скачать прайс», страница 13).
*
* Прежний тариф был ступенчатым (0.45 → 0.36 ₽ за показ по объёму) и строился на
* числе «CPM 400 ₽», которое кабинет показывает БЕЗ НДС. Обе посылки оказались
* неверными:
*
* 1. Скидки за объём у МТС НЕТ. Прайс, раздел «Реклама в Telegram по своей базе»:
* «1 показ в Telegram абонентам других операторов РФ — 0,48 ₽», плоско.
* 2. 0,48 ₽ — цена С НДС («Бюджет рекламы и CPM включает НДС»). Сходится с кабинетом:
* аудитория 420 × 400 ₽/1000 × 1,2 = 201,60 ₽ — ровно счёт кампании 2231134.
*
* Из-за этого наценка 1.40 существовала только на бумаге: на объёме от 50 000 клиент
* платил 504 ₽ за тысячу при себестоимости 480 ₽ — наценка 5%, а не 40%.
*
* Медиа кабинет тарифицирует отдельно (шаг «Объявление», блок «Медиа»):
* «CPM с изображением — 600 ₽, с видео — 680 ₽» — это ПОЛНАЯ ставка, а не надбавка.
* Числа показаны по тому же правилу без НДС ⇒ с НДС 720 и 816 ₽ за тысячу.
* 🪤 Последнее — ВЫВОД по правилу, а не замер: доказать можно только кампанией с
* картинкой, доведённой до шага «Стоимость» (туда не попасть без загрузки живых номеров).
*/
it('себестоимость показа плоская и зависит только от медиа', function () {
$p = app(TelegramTariffService::class);
expect($p->costPerImpressionRub(Campaign::MEDIA_NONE))->toBe('0.480')
->and($p->costPerImpressionRub(Campaign::MEDIA_IMAGE))->toBe('0.720')
->and($p->costPerImpressionRub(Campaign::MEDIA_VIDEO))->toBe('0.816');
});
it('объём на себестоимость не влияет — скидки за объём у МТС нет', function () {
$p = app(TelegramTariffService::class);
// Прежний тариф на этих же объёмах давал 0.45 / 0.42 / 0.40 / 0.38 / 0.36.
foreach ([1, 999, 1000, 5000, 10000, 50000, 500000] as $volume) {
expect($p->estimateRub($volume, Campaign::MEDIA_NONE))
->toBe(bcmul((string) $volume, '0.480', 2));
}
});
it('смета = показы × себестоимость вида медиа', function () {
$p = app(TelegramTariffService::class);
expect($p->estimateRub(1000, Campaign::MEDIA_NONE))->toBe('480.00')
->and($p->estimateRub(1000, Campaign::MEDIA_IMAGE))->toBe('720.00')
->and($p->estimateRub(1000, Campaign::MEDIA_VIDEO))->toBe('816.00')
->and($p->estimateRub(0, Campaign::MEDIA_NONE))->toBe('0.00');
});
it('клиентская цена = себестоимость × наценка: 672 / 1008 / 1142 ₽ за тысячу', function () {
config()->set('client_tg.markup', '1.40');
$p = app(TelegramTariffService::class);
expect($p->clientEstimateRub(1000, Campaign::MEDIA_NONE))->toBe('672.00')
->and($p->clientEstimateRub(1000, Campaign::MEDIA_IMAGE))->toBe('1008.00')
// 816.00 × 1.40 = 1142.40
->and($p->clientEstimateRub(1000, Campaign::MEDIA_VIDEO))->toBe('1142.40')
->and($p->clientEstimateRub(0, Campaign::MEDIA_NONE))->toBe('0.00');
});
it('без медиа — вид по умолчанию (старые вызовы не начинают считать по видео)', function () {
config()->set('client_tg.markup', '1.40');
$p = app(TelegramTariffService::class);
expect($p->clientEstimateRub(1000))->toBe('672.00');
});
it('наценка берётся из настроек', function () {
config()->set('client_tg.markup', '2.00');
$p = app(TelegramTariffService::class);
expect($p->clientEstimateRub(1000, Campaign::MEDIA_NONE))->toBe('960.00');
});
/**
* Сторож от молчаливого убытка: клиент обязан платить БОЛЬШЕ, чем платим мы.
* Именно этой проверки не было, и потому ступенчатый тариф годами уводил бы
* крупные кампании с медиа в минус, ничего не сказав.
*/
it('клиентская цена всегда выше себестоимости — на любом объёме и любом медиа', function () {
config()->set('client_tg.markup', '1.40');
$p = app(TelegramTariffService::class);
foreach ([Campaign::MEDIA_NONE, Campaign::MEDIA_IMAGE, Campaign::MEDIA_VIDEO] as $kind) {
foreach ([1, 367, 1000, 5000, 50000, 500000] as $volume) {
$nasha = $p->estimateRub($volume, $kind);
$klienta = $p->clientEstimateRub($volume, $kind);
expect(bccomp($klienta, $nasha, 2))->toBe(
1,
"объём {$volume}, медиа {$kind}: клиент платит {$klienta}, мы платим {$nasha}"
);
}
}
});
it('неизвестный вид медиа не проходит молча', function () {
$p = app(TelegramTariffService::class);
expect(fn () => $p->costPerImpressionRub('gif'))
->toThrow(InvalidArgumentException::class);
});