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>
117 lines
3.8 KiB
PHP
117 lines
3.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ClientTg\Campaign;
|
|
use App\Models\ClientTg\CampaignPhone;
|
|
use App\Models\ClientTg\Contact;
|
|
use App\Models\ClientTg\Tariff;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('creates a campaign with related phones and reads them back via hasMany', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
$campaign = Campaign::create([
|
|
'tenant_id' => $tenant->id,
|
|
'status' => Campaign::STATUS_DRAFT,
|
|
'ad_text' => 'Подключите наш телеграм-бот и получайте заявки быстрее',
|
|
'ad_link' => 'https://liderra.ru',
|
|
'media_path' => null,
|
|
'ord_category' => 'Размещение рекламы',
|
|
'budget_cap_rub' => '1000.00',
|
|
'audience_kind' => Campaign::AUDIENCE_DEALS,
|
|
'audience_params' => ['days' => 30],
|
|
'planned_count' => 420,
|
|
'matched_count' => null,
|
|
'estimated_cost_rub' => '168.00',
|
|
'actual_cost_rub' => null,
|
|
'created_by' => null,
|
|
]);
|
|
|
|
$phoneA = CampaignPhone::create([
|
|
'tenant_id' => $tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'phone' => '79990000001',
|
|
'expires_at' => null,
|
|
]);
|
|
CampaignPhone::create([
|
|
'tenant_id' => $tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'phone' => '79990000002',
|
|
'expires_at' => null,
|
|
]);
|
|
|
|
expect($campaign->phones)->toHaveCount(2)
|
|
->and($phoneA->campaign->id)->toBe($campaign->id);
|
|
});
|
|
|
|
it('round-trips the audience_params array cast', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
$campaign = Campaign::create([
|
|
'tenant_id' => $tenant->id,
|
|
'status' => Campaign::STATUS_DRAFT,
|
|
'ad_text' => 'text',
|
|
'ad_link' => null,
|
|
'media_path' => null,
|
|
'ord_category' => 'Размещение рекламы',
|
|
'budget_cap_rub' => '0.00',
|
|
'audience_kind' => Campaign::AUDIENCE_LIST,
|
|
'audience_params' => ['phones_count' => 500, 'note' => 'файл'],
|
|
'created_by' => null,
|
|
]);
|
|
|
|
$campaign->refresh();
|
|
|
|
// jsonb не сохраняет порядок ключей — сверяем по ключам, не по порядку.
|
|
expect($campaign->audience_params)->toBeArray()
|
|
->and($campaign->audience_params['phones_count'])->toBe(500)
|
|
->and($campaign->audience_params['note'])->toBe('файл');
|
|
});
|
|
|
|
it('creates and reads back a contact (своя база)', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
$contact = Contact::create([
|
|
'tenant_id' => $tenant->id,
|
|
'phone' => '79990000003',
|
|
'name' => 'Иван Иванов',
|
|
'operator' => 'Билайн',
|
|
]);
|
|
|
|
$contact->refresh();
|
|
|
|
expect($contact->phone)->toBe('79990000003')
|
|
->and($contact->name)->toBe('Иван Иванов');
|
|
});
|
|
|
|
it('reads seeded tariffs', function () {
|
|
// Три строки — по одной на вид медиа (было пять ступеней по объёму до 02.08.2026,
|
|
// см. миграцию client_tg_cena_po_media: скидки за объём у МТС нет).
|
|
expect(Tariff::count())->toBe(3);
|
|
});
|
|
|
|
it('round-trips the decimal cast on budget_cap_rub', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
$campaign = Campaign::create([
|
|
'tenant_id' => $tenant->id,
|
|
'status' => Campaign::STATUS_DRAFT,
|
|
'ad_text' => 'decimal test',
|
|
'ad_link' => null,
|
|
'media_path' => null,
|
|
'ord_category' => 'Размещение рекламы',
|
|
'budget_cap_rub' => '1000.5',
|
|
'audience_kind' => Campaign::AUDIENCE_DEALS,
|
|
'audience_params' => null,
|
|
'created_by' => null,
|
|
]);
|
|
|
|
$campaign->refresh();
|
|
|
|
expect((string) $campaign->budget_cap_rub)->toBe('1000.50');
|
|
});
|