Files
portal/app/tests/Feature/ClientTg/AutoRuleApiTest.php
T
Дмитрий aa4b482d45 feat(телеграм-реклама): Этап 5 — «Своё имя» + «Авторассылка» из кабинета + предохранители трат
Закрывает Этап 5 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
Клиент управляет именем отправителя и авторассылкой сам из кабинета; авто не тратит
без денег и сверх дневного лимита. TDD, весь бэкенд+фронт зелёный.

## 5.1 — предохранители авторассылки
Накопитель в БОЮ перед постановкой пачки проверяет: смета умещается И в свободный
остаток кошелька (balance−frozen), И в дневной лимит правила за вычетом трат за
сегодня. Не прошла — держим черновиком + Log::info('client_tg.auto_skipped', reason).
Дефолт daily_limit_rub=0 → авто выключено (само денег не потратит). В песочнице гейта
нет (деньги не трогаются, как ручной launch). Новые колонки client_tg_auto_rule:
daily_limit_rub, spent_today_rub, spent_date (счётчик за день, сброс при смене даты).

## 5.2 — клиентское API имени (SenderController)
GET /sender (статус + остаток грейса), POST /sender (завести), /sender/disable,
/sender/enable (suspended→active, идемпотентно по периоду — без двойной оплаты в месяц;
проверка средств ДО списания). Логика в TelegramSenderService (enableSender+snapshot),
контроллер тонкий. Всё скоуп тенантом.

## 5.3 — экран имени (TelegramSenderPanel.vue)
Самодостаточная панель: статус имени человеческими словами, дата оплаты, остаток грейса
при долге; suspended → «Отключено за долг» + «Включить»; нет имени → форма «Завести имя».
telegram.ts: fetchSender/createSender/disableSender/enableSender.

## 5.4 — экран авторассылки + API (AutoRuleController)
GET/PUT /api/telegram/auto-rule (вкл/выкл, объявление, порог, бюджет, дневной лимит).
Порог клиентский — новая колонка client_tg_auto_rule.batch_threshold (NULL → дефолт
конфига 367; ниже 367 API не даёт — минимум МТС). TelegramAutoRulePanel.vue: тумблер +
поля порога/бюджета/лимита. В списке кампаний авто-кампании (created_by=null) помечены
чипом «авто».

Обе панели встроены в AdvertisingTelegramView.

## Схема
Две аддитивные миграции на существующую таблицу client_tg_auto_rule (000014 daily_limit
+ счётчик за день; 000015 batch_threshold). RLS/GRANT не тронуты (табличный GRANT
покрывает новые колонки). Записи db/CHANGELOG_schema.md v8.91/v8.92; обе прогнаны через
rls-reviewer — CLEAN.

## Приёмка
TDD. Бэкенд ClientTg 177/177; phpstan/pint/deptrac чисто. Фронт полный набор
212 файлов/1538 тестов зелёные; vue-tsc+ESLint по нашим файлам чисто. Проверено вживую
в браузере (клиент demo, песочница): обе панели читают и пишут — правило сохраняется
точь-в-точь (порог/бюджет/лимит), имя заводится (pending, деньги не тронуты).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:02:56 +03:00

100 lines
4.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\AutoRule;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
/**
* Клиентское HTTP-API правила авторассылки Telegram (план §Этап 5, задача 5.4).
* Клиент из кабинета включает/выключает авто, задаёт порог пачки, бюджет на кампанию
* и дневной лимит трат. Одно правило на тенанта; scope из $request->user().
*
* Auth/tenant как у CampaignApiTest: Tenant::factory + User(tenant_id) + actingAs.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
});
/** @return array<string, mixed> валидный payload сохранения правила. */
function autoRulePayload(array $overrides = []): array
{
return array_merge([
'enabled' => true,
'ad_text' => 'Заходите в наш канал',
'ad_link' => 'https://t.me/example_channel',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '2500.00',
'daily_limit_rub' => '1000.00',
'batch_threshold' => 500,
], $overrides);
}
it('GET /auto-rule без правила отдаёт безопасные значения по умолчанию (авто выключено)', function () {
$this->getJson('/api/telegram/auto-rule')
->assertOk()
->assertJsonPath('enabled', false)
->assertJsonPath('daily_limit_rub', '0.00');
});
it('PUT /auto-rule создаёт правило тенанта', function () {
$this->putJson('/api/telegram/auto-rule', autoRulePayload())
->assertOk()
->assertJsonPath('enabled', true)
->assertJsonPath('daily_limit_rub', '1000.00')
->assertJsonPath('batch_threshold', 500);
$rule = AutoRule::where('tenant_id', $this->tenant->id)->first();
expect($rule)->not->toBeNull();
expect((string) $rule->daily_limit_rub)->toBe('1000.00');
expect($rule->batch_threshold)->toBe(500);
expect($rule->enabled)->toBeTrue();
});
it('PUT /auto-rule обновляет существующее правило, не плодит второе', function () {
$this->putJson('/api/telegram/auto-rule', autoRulePayload())->assertOk();
$this->putJson('/api/telegram/auto-rule', autoRulePayload(['enabled' => false, 'daily_limit_rub' => '0.00']))
->assertOk()
->assertJsonPath('enabled', false);
expect(AutoRule::where('tenant_id', $this->tenant->id)->count())->toBe(1);
});
it('PUT /auto-rule: порог ниже минимума МТС (367) — 422', function () {
$this->putJson('/api/telegram/auto-rule', autoRulePayload(['batch_threshold' => 100]))
->assertStatus(422)
->assertJsonValidationErrors(['batch_threshold']);
});
it('PUT /auto-rule: отрицательный дневной лимит — 422', function () {
$this->putJson('/api/telegram/auto-rule', autoRulePayload(['daily_limit_rub' => '-5']))
->assertStatus(422)
->assertJsonValidationErrors(['daily_limit_rub']);
});
it('PUT /auto-rule: при включении без текста объявления — 422', function () {
$this->putJson('/api/telegram/auto-rule', autoRulePayload(['ad_text' => '']))
->assertStatus(422)
->assertJsonValidationErrors(['ad_text']);
});
it('GET /auto-rule не видит правило чужого тенанта', function () {
$other = Tenant::factory()->create();
AutoRule::create([
'tenant_id' => $other->id, 'enabled' => true, 'ad_text' => 'Чужой',
'ad_link' => 'https://t.me/x', 'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '2500.00', 'daily_limit_rub' => '999.00',
]);
$this->getJson('/api/telegram/auto-rule')
->assertOk()
->assertJsonPath('enabled', false)
->assertJsonPath('daily_limit_rub', '0.00'); // видит СВОИ дефолты, не чужие 999
});