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>
69 lines
3.7 KiB
PHP
69 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Support;
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
/**
|
|
* Синтетические файлы медиа под требования кабинета МТС.
|
|
*
|
|
* 🪤 Почему классом, а не функциями в тесте: в Pest помощники из разных файлов живут
|
|
* в ОДНОМ пространстве имён, и одноимённые функции в двух наборах роняют прогон
|
|
* «Cannot redeclare» — на этом уже спотыкались при сведении веток 01.08.2026.
|
|
* Класс автозагружается (Tests\ → tests/) и столкнуться не может.
|
|
*
|
|
* Настоящих пикселей внутри нет — только те поля заголовка, которые читает
|
|
* App\Support\Mp4Probe: длительность (mvhd) и размер кадра (tkhd).
|
|
*/
|
|
final class MtsMediaFixture
|
|
{
|
|
/** Один бокс контейнера ISO-BMFF: длина (4 байта) + тип (4 байта) + содержимое. */
|
|
private static function box(string $type, string $payload): string
|
|
{
|
|
return pack('N', 8 + strlen($payload)).$type.$payload;
|
|
}
|
|
|
|
/** Байты mp4 с настоящими mvhd (длительность) и tkhd (размер кадра). */
|
|
public static function mp4Bytes(float $seconds = 10.0, int $width = 1280, int $height = 720): string
|
|
{
|
|
$timescale = 1000;
|
|
$duration = (int) round($seconds * $timescale);
|
|
|
|
$mvhd = "\x00\x00\x00\x00" // version 0 + flags
|
|
.pack('N', 0).pack('N', 0) // создан / изменён
|
|
.pack('N', $timescale).pack('N', $duration) // масштаб времени и длительность
|
|
.pack('N', 0x00010000) // rate
|
|
.pack('n', 0x0100) // volume
|
|
.str_repeat("\x00", 2 + 8 + 36 + 24) // reserved + matrix + pre_defined
|
|
.pack('N', 2); // next_track_ID
|
|
|
|
$tkhd = "\x00\x00\x00\x07" // version 0 + флаги «дорожка включена»
|
|
.pack('N', 0).pack('N', 0) // создан / изменён
|
|
.pack('N', 1) // track_ID
|
|
.pack('N', 0) // reserved
|
|
.pack('N', $duration) // длительность дорожки
|
|
.str_repeat("\x00", 8) // reserved
|
|
.pack('n', 0).pack('n', 0) // layer, alternate_group
|
|
.pack('n', 0).pack('n', 0) // volume, reserved
|
|
.str_repeat("\x00", 36) // matrix
|
|
.pack('N', $width << 16).pack('N', $height << 16); // ширина/высота 16.16
|
|
|
|
return self::box('ftyp', 'isom'.pack('N', 512).'isomiso2mp41')
|
|
.self::box('moov', self::box('mvhd', $mvhd).self::box('trak', self::box('tkhd', $tkhd)));
|
|
}
|
|
|
|
/** Годное видео файлом — ровно то, что кабинет принимает. */
|
|
public static function video(float $seconds = 10.0, int $width = 1280, int $height = 720): UploadedFile
|
|
{
|
|
return UploadedFile::fake()->createWithContent('ad.mp4', self::mp4Bytes($seconds, $width, $height));
|
|
}
|
|
|
|
/** Годная картинка файлом. */
|
|
public static function kartinka(int $width = 1280, int $height = 720): UploadedFile
|
|
{
|
|
return UploadedFile::fake()->image('ad.jpg', $width, $height);
|
|
}
|
|
}
|