diff --git a/app/app/Services/Bot/AnswerGuard.php b/app/app/Services/Bot/AnswerGuard.php index f1d76fbb..de8cd5ae 100644 --- a/app/app/Services/Bot/AnswerGuard.php +++ b/app/app/Services/Bot/AnswerGuard.php @@ -108,10 +108,12 @@ class AnswerGuard // статьях этого нет вовсе) и делил одно на два — клиент слышит отказ там, где отказа нет. '/не\s+прода[её]м[^.!?]{0,40}(поштучн\w*|по\s+одн\w+|отдельн\w+\s+номер\w*)/iu', '/(номер\w*|лид\w*)[^.!?]{0,30}(поштучн\w*|по\s+штук\w*)[^.!?]{0,20}не\s+прода/iu', - // NB: «минимального заказа НЕТ» — правда, её резать нельзя. Поэтому ловим только - // УТВЕРЖДЕНИЕ о минимуме: «минимальный заказ — 100 заявок», «пакет от 500 штук». - '/минимальн\w+\s+(объ[её]м|заказ|партия|пакет)\w*[^.!?]{0,20}(—|-|:|от|составля\w+|\d)/iu', - '/(пакет\w*|партия\w*)\s+от\s+\d+/iu', + // NB: «минимального заказа НЕТ» и «минимальный заказ — одна заявка» — ПРАВДА, резать + // нельзя (живая проверка на боевом 14.07.2026: сторож съел верный ответ, остался + // обрубок «Платите только за то, что получите»). Ловим только выдуманный минимум + // БОЛЬШЕ ОДНОГО: «минимальный заказ — 100 заявок», «пакет от 500 штук». + '/минимальн\w+\s+(объ[её]м|заказ|партия|пакет)\w*[^.!?]{0,20}(?!1\b)([2-9]|\d{2,})/iu', + '/(пакет\w*|партия\w*)\s+от\s+(?!1\b)([2-9]|\d{2,})/iu', // 🔴 Владелец 14.07.2026: сколько заявок выйдет на подарочную 1000 ₽ — НЕ ОБЕЩАЕМ и // числа не называем (besplatnyy-test.md). Сколько придёт, зависит от источника, а на // тесте по своему сайту их обычно единицы. «Несколько десятков» — враньё в лицо. @@ -129,11 +131,14 @@ class AnswerGuard * Числа словами. Модель их не всегда переводит в цифры («у вас два проекта» вместо * «2 проекта»), а клиенту от формы всё равно — считаем так же, как цифру. */ + // NB: падежные формы обязательны. Живая проверка 14.07.2026: в статье написано «продаём + // любой объём, хоть ОДНУ штуку», а бот отвечал «от 1 номера» — сторож не знал формы + // «одну», числа 1 в разрешённых не увидел и вырезал ПРАВДУ, оставив обрубок. private const WORD_NUMBERS = [ 'ноль' => '0', - 'один' => '1', 'одна' => '1', 'одно' => '1', - 'два' => '2', 'две' => '2', - 'три' => '3', + 'один' => '1', 'одна' => '1', 'одно' => '1', 'одну' => '1', 'одного' => '1', 'одной' => '1', + 'два' => '2', 'две' => '2', 'двух' => '2', + 'три' => '3', 'трёх' => '3', 'трех' => '3', 'четыре' => '4', 'пять' => '5', 'шесть' => '6', @@ -770,9 +775,21 @@ class AnswerGuard { preg_match_all('/\d[\d\s]{0,9}\d|\d/u', $context, $matches); - return array_values(array_unique(array_map( + $numbers = array_map( fn (string $n) => (string) preg_replace('/\s+/u', '', $n), $matches[0] ?? [] - ))); + ); + + // 🔴 Живая проверка на боевом 14.07.2026: в статье написано словом («продаём любой + // объём, хоть ОДНУ штуку»), бот ответил цифрой («от 1 номера») — и сторож вырезал + // ПРАВДУ, потому что видел в материалах только цифры. Слово-число в материалах — + // такое же разрешение, как цифра. + $wordAlt = implode('|', array_keys(self::WORD_NUMBERS)); + preg_match_all('/\b('.$wordAlt.')\b/iu', $context, $words); + foreach ($words[1] ?? [] as $word) { + $numbers[] = $this->normalizeNumberToken($word); + } + + return array_values(array_unique($numbers)); } } diff --git a/app/tests/Feature/Bot/Run250FixesTest.php b/app/tests/Feature/Bot/Run250FixesTest.php index 0be4e738..461e8e07 100644 --- a/app/tests/Feature/Bot/Run250FixesTest.php +++ b/app/tests/Feature/Bot/Run250FixesTest.php @@ -127,6 +127,19 @@ it('число, названное самим клиентом, сторож н expect($guard->clean($text, $context, personalFactsKnown: false))->toContain('5 номеров'); }); +it('правду «минимальный заказ — одна заявка» сторож не режет, а выдуманный минимум режет', function () { + // Живая проверка на боевом 14.07.2026: сторож съел верный ответ про 5 номеров, остался + // обрубок «Платите только за то, что получите». Виновата была моя же грубая регулярка. + $guard = app(AnswerGuard::class); + $ctx = 'Минимального заказа нет: продаём любой объём, хоть одну штуку.'; + + $true = 'Да, конечно. Минимальный заказ — от 1 номера, берите сколько нужно.'; + expect($guard->clean($true, $ctx, personalFactsKnown: false))->toContain('от 1 номера'); + + $lie = 'Минимальный заказ — 100 заявок в месяц.'; + expect($guard->clean($lie, $ctx, personalFactsKnown: false))->not->toContain('100 заявок'); +}); + it('сколько заявок выйдет на подарочную тысячу — бот не обещает', function () { $guard = app(AnswerGuard::class);