Files
portal/app/tests/Feature/Bot/BotContactCaptureTest.php
T
Дмитрий 6f72367b0f feat(jivo-bot): эскалация = просьба телефона + письмо с контактом в поддержку
Правило владельца 12.07.2026: оператора в чате может не быть, поэтому бот НЕ
переводит диалог (INVITE_AGENT убран из авто-флоу). Не смог ответить (личный
вопрос, стоп-тема, незнание) → просит номер телефона → как только клиент его
оставил, контакт уходит письмом в поддержку, а клиенту — «Спасибо, передал».

- ContactCapture: распознаёт номер в любом формате (+7/8/скобки/дефисы) → 79161234567
- BotContactRequestMail + шаблон: телефон, вопрос-на-котором-спасовал, id чата,
  переписка целиком, время → services.support.email
- ProcessJivoMessageJob: ждём контакт, если прошлый ответ бота просил номер;
  номера нет (клиент передумал/спросил другое) — отвечаем как обычно
- тексты эскалации переписаны под сбор контакта

TDD: BotContactCaptureTest (4 новых) + обновлены 4 теста прежней механики. 46/46.
Живая проверка на стенде: вопрос → просьба → номер → письмо с полной перепиской.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:30:33 +03:00

85 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\Bot\ProcessJivoMessageJob;
use App\Mail\BotContactRequestMail;
use App\Models\BotDialog;
use App\Models\KnowledgeChunk;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
uses(RefreshDatabase::class);
/**
* Правило владельца 12.07.2026: бот не «передаёт диалог оператору» (оператор
* не сидит в чате круглосуточно), а ПРОСИТ телефон и присылает контакт письмом.
*/
beforeEach(function () {
config()->set('services.jivo_bot.outbound_url', 'https://bot.jivosite.com/webhooks/p/t');
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' => 'Проект — это заявка на поток клиентов.',
]);
Http::fake(['bot.jivosite.com/*' => Http::response(['ok' => true])]);
});
it('эскалация просит телефон, а не зовёт оператора в чат', function () {
Mail::fake();
(new ProcessJivoMessageJob('chat-c1', 'client-1', 'какой у меня баланс?'))->handle();
$out = BotDialog::where('direction', 'out')->firstOrFail();
expect($out->escalated)->toBeTrue()
->and($out->message)->toContain('номер телефона');
Http::assertNotSent(fn ($r) => ($r['event'] ?? '') === 'INVITE_AGENT');
Mail::assertNothingSent();
});
it('клиент прислал телефон после просьбы → письмо с контактом и вопросом уходит в поддержку', function () {
Mail::fake();
(new ProcessJivoMessageJob('chat-c2', 'client-2', 'какой у меня баланс?'))->handle();
(new ProcessJivoMessageJob('chat-c2', 'client-2', 'мой номер +7 916 123-45-67'))->handle();
Mail::assertSent(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 () {
Mail::fake();
(new ProcessJivoMessageJob('chat-c3', 'client-3', 'звоните мне на +79161234567 если что'))->handle();
Mail::assertNothingSent();
});
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;
(new ProcessJivoMessageJob($chat, 'c', 'какой у меня баланс?'))->handle();
(new ProcessJivoMessageJob($chat, 'c', $phone))->handle();
}
Mail::assertSent(BotContactRequestMail::class, 3);
Mail::assertSent(BotContactRequestMail::class, fn ($m) => $m->phone === '79161234567');
});