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

110 lines
5.2 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\Models\ClientTg\Tariff;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* SaaS-admin «Клиентская Telegram-реклама» — себестоимость показа (client_tg_tariffs).
* Гейт группы — saas-admin,admin-db (crm_admin_user); admin-db переключает соединение
* на pgsql_admin, а SharesAdminPdo (глобально в Pest.php) отдаёт ему ту же PDO-сессию,
* что у default (видимость засеянной схемы).
*
* 🔴 Форма таблицы изменилась 02.08.2026 (миграция client_tg_cena_po_media): было пять
* ступеней по объёму, стало три строки по видам медиа. У МТС скидки за объём НЕТ —
* цена плоская и зависит только от медиа (прайс кабинета, стр. 13).
*
* Здесь правится ЗАКУПОЧНАЯ цена (что платим мы), а не цена клиента: клиент платит
* её × наценку config client_tg.markup.
*
* Сидировано миграцией: none/0.480, image/0.720, video/0.816 — с НДС.
*/
uses(DatabaseTransactions::class);
test('GET /api/admin/telegram/tariffs возвращает три вида медиа', function () {
$r = $this->getJson('/api/admin/telegram/tariffs');
$r->assertStatus(200);
$tariffs = $r->json('tariffs');
expect($tariffs)->toHaveCount(3);
expect(collect($tariffs)->pluck('media_kind')->all())
->toBe([Campaign::MEDIA_NONE, Campaign::MEDIA_IMAGE, Campaign::MEDIA_VIDEO]);
expect($tariffs[0]['price_rub'])->toBe('0.480');
expect($tariffs[2]['price_rub'])->toBe('0.816');
});
test('PUT /api/admin/telegram/tariffs заменяет себестоимость всех трёх видов', function () {
$r = $this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['media_kind' => Campaign::MEDIA_NONE, 'price_rub' => '0.500'],
['media_kind' => Campaign::MEDIA_IMAGE, 'price_rub' => '0.750'],
['media_kind' => Campaign::MEDIA_VIDEO, 'price_rub' => '0.900'],
],
]);
$r->assertStatus(200);
expect(Tariff::count())->toBe(3)
->and(Tariff::where('media_kind', Campaign::MEDIA_NONE)->first()->price_rub)->toBe('0.500')
->and(Tariff::where('media_kind', Campaign::MEDIA_VIDEO)->first()->price_rub)->toBe('0.900');
expect(collect($r->json('tariffs'))->pluck('media_kind')->all())
->toBe([Campaign::MEDIA_NONE, Campaign::MEDIA_IMAGE, Campaign::MEDIA_VIDEO]);
});
test('PUT /api/admin/telegram/tariffs с нулевой ценой → 422', function () {
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['media_kind' => Campaign::MEDIA_NONE, 'price_rub' => '0.000'],
['media_kind' => Campaign::MEDIA_IMAGE, 'price_rub' => '0.750'],
['media_kind' => Campaign::MEDIA_VIDEO, 'price_rub' => '0.900'],
],
])->assertStatus(422);
// Исходные строки не тронуты.
expect(Tariff::count())->toBe(3)
->and(Tariff::where('media_kind', Campaign::MEDIA_NONE)->first()->price_rub)->toBe('0.480');
});
/**
* 🔴 Главное, что охраняет эта ручка. Прежняя принимала произвольный список ступеней:
* владелец мог сохранить сетку без нужной строки, и расчёт упал бы уже в бою — в момент
* списания денег с клиента. Теперь набор строго три вида, каждый ровно один раз.
*/
test('PUT /api/admin/telegram/tariffs без одного вида медиа → 422, старое сохранено', function () {
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['media_kind' => Campaign::MEDIA_NONE, 'price_rub' => '0.500'],
['media_kind' => Campaign::MEDIA_IMAGE, 'price_rub' => '0.750'],
],
])->assertStatus(422);
expect(Tariff::count())->toBe(3)
->and(Tariff::where('media_kind', Campaign::MEDIA_NONE)->first()->price_rub)->toBe('0.480');
});
test('PUT /api/admin/telegram/tariffs с задвоенным видом → 422', function () {
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['media_kind' => Campaign::MEDIA_NONE, 'price_rub' => '0.500'],
['media_kind' => Campaign::MEDIA_NONE, 'price_rub' => '0.600'],
['media_kind' => Campaign::MEDIA_VIDEO, 'price_rub' => '0.900'],
],
])->assertStatus(422);
expect(Tariff::count())->toBe(3);
});
test('PUT /api/admin/telegram/tariffs с выдуманным видом медиа → 422', function () {
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['media_kind' => 'gif', 'price_rub' => '0.500'],
['media_kind' => Campaign::MEDIA_IMAGE, 'price_rub' => '0.750'],
['media_kind' => Campaign::MEDIA_VIDEO, 'price_rub' => '0.900'],
],
])->assertStatus(422);
expect(Tariff::count())->toBe(3);
});