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>
110 lines
5.2 KiB
PHP
110 lines
5.2 KiB
PHP
<?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);
|
||
});
|