3864f6f11a
Мы продавали дешевле, чем покупали. Тариф давал скидку за объём — 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>
112 lines
5.9 KiB
PHP
112 lines
5.9 KiB
PHP
<?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);
|
||
});
|