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>
119 lines
4.7 KiB
PHP
119 lines
4.7 KiB
PHP
<?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');
|
||
});
|