Files
portal/app/tests/Feature/ClientTg/AdminTgApiTest.php
T
Дмитрий 5b04d751c0 feat(телеграм-модуль): авто-режим, своё имя + помесячная оплата, админ-тарифы
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.

4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
  (audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
  по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
  и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
  Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).

4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
  🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
  очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
  в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.

4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
  disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
  free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
  кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
  Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
  в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).

4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
  ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).

Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:41:33 +03:00

79 lines
3.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\Setting;
use App\Models\ClientTg\Tariff;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* SaaS-admin «Клиентская Telegram-реклама» — тарифная сетка (client_tg_tariffs) и
* общие настройки (client_tg_settings, singleton). План §Сессия 4, задача 4.4.
* Зеркало AdminTariffTest (СМС). Гейт группы — saas-admin,admin-db (crm_admin_user);
* admin-db переключает соединение на pgsql_admin, а SharesAdminPdo (глобально в
* Pest.php) отдаёт ему ту же PDO-сессию, что у default (видимость засеянной схемы).
*
* Сидированы миграциями: 5 ступеней (1/0.45, 1000/0.42, 5000/0.40, 10000/0.38,
* 50000/0.36) + settings (fee 2500.00/мес, grace 29 дней).
*/
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(5);
expect(collect($tariffs)->pluck('min_qty')->all())->toBe([1, 1000, 5000, 10000, 50000]);
expect($tariffs[0]['price_rub'])->toBe('0.45');
expect($r->json('settings.name_fee_rub_per_month'))->toBe('2500.00');
expect($r->json('settings.name_debt_grace_days'))->toBe(29);
});
test('PUT /api/admin/telegram/tariffs заменяет весь набор ступеней', function () {
$r = $this->putJson('/api/admin/telegram/tariffs', [
'rows' => [
['min_qty' => 1, 'price_rub' => '0.50'],
['min_qty' => 5000, 'price_rub' => '0.35'],
],
]);
$r->assertStatus(200);
expect(Tariff::count())->toBe(2);
$rows = Tariff::orderBy('min_qty')->get();
expect($rows[0]->min_qty)->toBe(1);
expect($rows[0]->price_rub)->toBe('0.50');
expect($rows[1]->min_qty)->toBe(5000);
expect($rows[1]->price_rub)->toBe('0.35');
expect(collect($r->json('tariffs'))->pluck('min_qty')->all())->toBe([1, 5000]);
});
test('PUT /api/admin/telegram/settings обновляет singleton настроек', function () {
$r = $this->putJson('/api/admin/telegram/settings', [
'name_fee_rub_per_month' => '3000.00',
'name_debt_grace_days' => 14,
]);
$r->assertStatus(200);
expect($r->json('name_fee_rub_per_month'))->toBe('3000.00');
expect($r->json('name_debt_grace_days'))->toBe(14);
$settings = Setting::first();
expect($settings->name_fee_rub_per_month)->toBe('3000.00');
expect($settings->name_debt_grace_days)->toBe(14);
});
test('PUT /api/admin/telegram/tariffs с некорректной ступенью → 422', function () {
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [['min_qty' => 0, 'price_rub' => '0.50']],
])->assertStatus(422);
$this->putJson('/api/admin/telegram/tariffs', [
'rows' => [['min_qty' => 1, 'price_rub' => '0.00']],
])->assertStatus(422);
// Исходные 5 ступеней не тронуты.
expect(Tariff::count())->toBe(5);
});