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
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Jobs\Bot\ProcessJivoMessageJob;
|
||||
use App\Jobs\Bot\ProcessChatMessageJob;
|
||||
use App\Mail\BotContactRequestMail;
|
||||
use App\Models\BotDialog;
|
||||
use App\Models\KnowledgeChunk;
|
||||
@@ -20,7 +20,6 @@ uses(RefreshDatabase::class);
|
||||
* что уже сказал, и правильно принять оставленный телефон.
|
||||
*/
|
||||
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',
|
||||
@@ -32,20 +31,34 @@ beforeEach(function () {
|
||||
'topics' => 'пополнить, баланс, минималка', 'chunk_index' => 0,
|
||||
'content' => 'Пополнить баланс можно картой или по счёту.',
|
||||
]);
|
||||
Http::fake(['bot.jivosite.com/*' => Http::response(['ok' => true])]);
|
||||
});
|
||||
|
||||
/** Ответ модели, который получит бот. */
|
||||
function llmSays(string $text): void
|
||||
{
|
||||
Http::fake([
|
||||
'bot.jivosite.com/*' => Http::response(['ok' => true]),
|
||||
'llm.api.cloud.yandex.net/*' => Http::response([
|
||||
'result' => ['alternatives' => [['message' => ['role' => 'assistant', 'text' => $text]]]],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Мимикрирует то, что делает ChatController: пишет реплику клиента в журнал и
|
||||
* запускает джобу с её номером.
|
||||
*/
|
||||
function sendChat(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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 Находка 1 (D28): клиент оставил телефон, почта не ответила — задача упала (tries=1),
|
||||
* и клиент не получил НИЧЕГО. Ответ клиенту важнее письма.
|
||||
@@ -53,8 +66,8 @@ function llmSays(string $text): void
|
||||
it('почта упала — клиент всё равно получил подтверждение, что номер принят', function () {
|
||||
Mail::shouldReceive('to')->once()->andThrow(new RuntimeException('SMTP недоступен'));
|
||||
|
||||
(new ProcessJivoMessageJob('chat-mailfail', 'c1', 'верните мне деньги'))->handle();
|
||||
(new ProcessJivoMessageJob('chat-mailfail', 'c1', '+7 999 123-45-67'))->handle();
|
||||
sendChat('chat-mailfail', 'верните мне деньги');
|
||||
sendChat('chat-mailfail', '+7 999 123-45-67');
|
||||
|
||||
$last = BotDialog::where('direction', 'out')->latest('id')->firstOrFail();
|
||||
expect($last->message)->toBe(BotAnswerService::CONTACT_TAKEN_TEXT);
|
||||
@@ -69,8 +82,8 @@ it('клиент оставил номер сам, без просьбы бот
|
||||
Mail::fake();
|
||||
llmSays('Пополнить баланс можно картой.');
|
||||
|
||||
(new ProcessJivoMessageJob('chat-selfphone', 'c2', 'как пополнить баланс'))->handle();
|
||||
(new ProcessJivoMessageJob('chat-selfphone', 'c2', 'вот мой номер +7 900 000-11-22'))->handle();
|
||||
sendChat('chat-selfphone', 'как пополнить баланс');
|
||||
sendChat('chat-selfphone', 'вот мой номер +7 900 000-11-22');
|
||||
|
||||
Mail::assertQueued(BotContactRequestMail::class, fn ($m) => $m->phone === '79000001122');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user