Files
portal/app/tests/Feature/Bot/BotContactCaptureTest.php
T
Дмитрий cfd26e38ef feat(chat): свой приём сообщений вместо вебхука Jivo
Заменяет JivoBotController+JivoBotClient на ChatController (POST /api/chat/message)
и ProcessJivoMessageJob на ProcessChatMessageJob. Реплику клиента теперь пишет
контроллер и возвращает её номер (message_id); джоба получает этот номер и берёт
историю строго до него, не отправляя ответ во внешний Jivo — он ложится в bot_dialogs,
откуда его заберёт своё окошко (Задача 3). Мозг бота не менялся.

Спека: docs/superpowers/specs/2026-07-13-own-chat-widget-design.md §5
2026-07-13 07:04:38 +03:00

104 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\Bot\ProcessChatMessageJob;
use App\Mail\BotContactRequestMail;
use App\Models\BotDialog;
use App\Models\KnowledgeChunk;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
uses(RefreshDatabase::class);
/**
* Правило владельца 12.07.2026: бот не «передаёт диалог оператору» (оператор
* не сидит в чате круглосуточно), а ПРОСИТ телефон и присылает контакт письмом.
*/
beforeEach(function () {
config()->set('services.support.email', 'support@liderra.ru');
config()->set('services.yandexgpt', [
'api_key' => 'k', 'folder_id' => 'f', 'model' => 'yandexgpt-lite/latest',
'endpoint' => 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion',
'timeout_seconds' => 8,
]);
KnowledgeChunk::create([
'source_path' => 'help/p.md', 'title' => 'Что такое проект', 'tour' => null,
'topics' => 'создать проект', 'chunk_index' => 0,
'content' => 'Проект — это заявка на поток клиентов.',
]);
});
/**
* Мимикрирует то, что делает ChatController: пишет реплику клиента в журнал и
* запускает джобу с её номером.
*/
function chatTurn(string $chatId, string $text): void
{
$row = BotDialog::create([
'chat_id' => $chatId,
'direction' => 'in',
'message' => $text,
'created_at' => now(),
]);
(new ProcessChatMessageJob($chatId, (int) $row->id, $text))->handle();
}
it('эскалация просит телефон', function () {
Mail::fake();
chatTurn('chat-c1', 'какой у меня баланс?');
$out = BotDialog::where('direction', 'out')->firstOrFail();
expect($out->escalated)->toBeTrue()
->and($out->message)->toContain('номер телефона');
Mail::assertNothingSent();
});
it('клиент прислал телефон после просьбы → письмо с контактом и вопросом уходит в поддержку', function () {
Mail::fake();
chatTurn('chat-c2', 'какой у меня баланс?');
chatTurn('chat-c2', 'мой номер +7 916 123-45-67');
// Письмо уходит ЧЕРЕЗ ОЧЕРЕДЬ: почта не должна ронять ответ клиенту (прогон диалогами
// 12.07.2026 — SMTP не ответил, задача упала, клиент с номером остался без ответа).
Mail::assertQueued(BotContactRequestMail::class, function (BotContactRequestMail $mail) {
return $mail->hasTo('support@liderra.ru')
&& $mail->phone === '79161234567'
&& str_contains($mail->question, 'баланс');
});
$last = BotDialog::where('direction', 'out')->latest('id')->firstOrFail();
expect($last->message)->toContain('Спасибо');
});
it('телефон без просьбы бота — тоже принимаем: человек хочет, чтобы перезвонили', function () {
// Правило изменено 12.07.2026 по итогам прогона диалогами: раньше номер «без просьбы»
// игнорировался, и бот отвечал «не могу работать с персональными данными в чате».
// Номер в чате = заявка на звонок. Не теряем его никогда.
Mail::fake();
chatTurn('chat-c3', 'звоните мне на +79161234567 если что');
Mail::assertQueued(BotContactRequestMail::class, fn ($m) => $m->phone === '79161234567');
$last = BotDialog::where('direction', 'out')->latest('id')->firstOrFail();
expect($last->message)->toContain('Спасибо');
});
it('номер распознаётся в разных форматах', function () {
Mail::fake();
$variants = ['8 (916) 123-45-67', '+7-916-123-45-67', '79161234567'];
foreach ($variants as $i => $phone) {
$chat = 'chat-fmt-'.$i;
chatTurn($chat, 'какой у меня баланс?');
chatTurn($chat, $phone);
}
Mail::assertQueued(BotContactRequestMail::class, 3);
Mail::assertQueued(BotContactRequestMail::class, fn ($m) => $m->phone === '79161234567');
});