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>
This commit is contained in:
Дмитрий
2026-07-12 11:30:33 +03:00
parent 9b83b2e035
commit f33d1a1638
8 changed files with 276 additions and 16 deletions
@@ -27,10 +27,12 @@ it('стоп-тема (мой баланс) → эскалация без пох
$answer = app(BotAnswerService::class)->answer('какой у меня баланс?');
// Текст именно стоп-эскалации («передаю ему диалог»), а не «в инструкции нет ответа»:
// до фикса 12.07.2026 вопрос проскакивал стоп-гейт и эскалировал лишь пустым поиском.
// Текст именно стоп-эскалации («лучше разберёт живой специалист»), а не «в инструкции
// нет ответа»: до фикса 12.07.2026 вопрос проскакивал стоп-гейт и эскалировал лишь
// пустым поиском. Эскалация просит телефон (правило владельца), а не зовёт оператора.
expect($answer->escalate)->toBeTrue()
->and($answer->text)->toContain('передаю ему диалог');
->and($answer->text)->toContain('лучше разберёт живой специалист')
->and($answer->text)->toContain('номер телефона');
Http::assertNothingSent();
});
@@ -48,7 +50,7 @@ it('стоп-тема со словами между «у меня» и «бал
$answer = app(BotAnswerService::class)->answer('какой у меня сейчас баланс?');
expect($answer->escalate)->toBeTrue()
->and($answer->text)->toContain('передаю ему диалог');
->and($answer->text)->toContain('лучше разберёт живой специалист');
Http::assertNothingSent();
});
@@ -97,7 +99,8 @@ it('LLM ответил «в инструкции этого нет» → чес
expect($answer->escalate)->toBeTrue()
->and($answer->text)->not->toContain('Показать на портале')
->and($answer->text)->toContain('живому специалисту');
->and($answer->text)->toContain('в моей инструкции нет ответа')
->and($answer->text)->toContain('номер телефона');
});
it('вопрос про эксклюзивность заявок → эскалация без похода в LLM', function () {