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'); });