Files
portal/app/tests/Feature/ClientTg/ManualFlowTest.php
T
Дмитрий 0c9642f3d0 feat(телеграм-модуль): клиентский экран + ручной запуск в песочнице — API, telegram.ts, экран, сквозной сценарий
Сессия 3 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Клиент сам собирает объявление + аудиторию и жмёт «Запустить»; робот кабинета МТС
(в тестах замокан) доводит до черновика. Всё в песочнице: деньги/живой запуск выключены.

- Клиентский API (Api\ClientTg\CampaignController + маршруты /api/telegram/*):
  создание (store) и запуск (launch) РАЗДЕЛЕНЫ. store создаёт ЧЕРНОВИК со сметой и
  числом кандидатов (деньги/робот не трогаются); launch draft→queued, в бою бронирует
  лимит на объявление (budget_cap_rub) и ставит RunTelegramCampaignJob. Нехватка денег
  → 409 (кампания остаётся черновиком); повторный запуск не-черновика → 422; изоляция
  тенантов (чужой show → 404). Валидация текст/ссылка/аудитория/бюджет; deals без срока → 422.
- Фронт-API telegram.ts (зеркало client-sms.ts, уже): fetch/create/get/launch, cookie-сессия.
- Экран AdvertisingTelegramView.vue (/advertising/telegram): форма (объявление, ссылка,
  3 способа аудитории, лимит), кнопка «Запустить» (create→launch), баннер песочницы,
  список кампаний с человеческим ярлыком статуса, сообщение при нехватке денег.
- Канал «Реклама Телеграм» АКТИВИРОВАН: advertisingChannels.ts route, провод в AppSidebar
  и AppMoreDrawer (route → реальный экран, остальные каналы — прежняя заглушка), маршрут
  в router. Тесты каналов обновлены (телеграм больше не заглушка).
- Ручной сквозной сценарий (песочница): создать → запустить → джоб (робот замокан, draft)
  → draft_ready + matched виден в API.

Отличие от СМС-близнеца: отдельного эндпоинта «предпросчёт до создания» нет — смету и
кандидатов клиент видит в самом черновике (создание черновик денег не тратит).

Проверки: 60/60 Pest ClientTg зелёные (+10 новых), 1512 Vitest зелёные (0 регрессий),
phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений. Робот в тестах замокан
(кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 17:39:43 +03:00

72 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\ClientTg\RunTelegramCampaignJob;
use App\Models\ClientTg\Campaign;
use App\Models\Tenant;
use App\Models\User;
use App\Services\ClientTg\RobotResult;
use App\Services\ClientTg\TelegramRobotRunner;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
/**
* Ручной сквозной сценарий в песочнице (план §Сессия 3, задача 3.4):
* создать (API) → запустить (API) → джоб (робот ЗАМОКАН, mode:draft) →
* статус draft_ready + matched виден клиенту в API.
*
* Робот кабинета МТС не трогаем (замокан). Синтетические номера 7999… — реальные НИКОГДА.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
config(['client_tg.sandbox' => true]);
});
it('песочница: создать → запустить → робот-черновик → draft_ready + matched виден в API', function () {
Queue::fake();
// Робот замокан: черновик готов, нашлось 2 совпадения в кабинете МТС.
$mock = Mockery::mock(TelegramRobotRunner::class);
$mock->shouldReceive('run')->once()->andReturn(new RobotResult(ok: true, matched: 2));
app()->instance(TelegramRobotRunner::class, $mock);
// 1. Клиент создаёт черновик из списка своих номеров.
$create = $this->postJson('/api/telegram/campaigns', [
'ad_text' => 'Заходите в наш канал',
'ad_link' => 'https://t.me/example_channel',
'audience_kind' => 'list',
'budget_cap_rub' => '1000.00',
'phones' => ['79990000001', '79990000002'],
]);
$create->assertCreated()->assertJsonPath('status', Campaign::STATUS_DRAFT);
$id = $create->json('id');
expect($create->json('planned_count'))->toBe(2);
// 2. Клиент жмёт «Запустить» — кампания в очереди, джоб поставлен.
$this->postJson("/api/telegram/campaigns/{$id}/launch")
->assertOk()
->assertJsonPath('status', Campaign::STATUS_QUEUED);
Queue::assertPushed(RunTelegramCampaignJob::class);
// 3. Отрабатывает воркер очереди (робот замокан) — доводит до черновика.
// Зовём handle через контейнер: под Queue::fake() dispatchSync не выполняется,
// а нам нужен реальный прогон джоба с замоканным роботом.
app()->call([new RunTelegramCampaignJob($id, $this->tenant->id), 'handle']);
// 4. Клиент видит в кабинете: черновик готов, совпадений — 2.
$this->getJson("/api/telegram/campaigns/{$id}")
->assertOk()
->assertJsonPath('campaign.status', Campaign::STATUS_DRAFT_READY)
->assertJsonPath('campaign.matched_count', 2);
// Список тоже отражает финальный статус.
$index = $this->getJson('/api/telegram/campaigns')->assertOk();
expect(collect($index->json('campaigns'))->firstWhere('id', $id)['status'])
->toBe(Campaign::STATUS_DRAFT_READY);
});