aa4b482d45
Закрывает Этап 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>
100 lines
4.2 KiB
PHP
100 lines
4.2 KiB
PHP
<?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
|
||
});
|