Commit Graph

638 Commits

Author SHA1 Message Date
Дмитрий a2bd2dbc8b fix(bot): гостю больше нельзя приписывать его ступень, баланс и заявки
Проверка на боевом 14.07.2026 (сразу после выката фикса цен): гость — человек не вошёл,
никаких заявок у него нет — показал скриншот с 50 ₽, и бот ответил: «Вы сейчас на второй
ступени (50 ₽), потому что в этом месяце уже получили заявки». Выдумка про человека.

Причина: сторож сверяет личные цифры только у ВОШЕДШЕГО (карточка фактов). У гостя
карточки нет — и модель фантазировала свободно.

Теперь AnswerGuard знает, посчитаны ли личные цифры собеседника. Если нет (гость) —
режет фразы, утверждающие его нынешнее состояние: «вы сейчас на… ступени», «у вас
на балансе N», «вы уже получили заявки». Общие объяснения не трогаются: «вы платите
только за полученные заявки», «когда наберёте объём — перейдёте на следующую ступень».

Тесты бота: 242/242 (+2 новых).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:49:53 +03:00
Дмитрий 7b7718d319 fix(bot): бот врал про цены живым гостям — брал ВСЕ версии сетки тарифов разом
Живой разговор на боевом 14.07.2026: гость спросил цену, бот назвал «500 ₽ за первую
заявку месяца» (такого тарифа нет — сетка 55→25 ₽), клиент поймал на вранье и ушёл.

Три поломки из одного разговора:

1. ЦЕНЫ. В pricing_tiers лежат ЧЕТЫРЕ версии сетки (старые хранятся с датой начала
   действия). Портал берёт свежую через PricingTierRepository, а LivePrices читал
   таблицу напрямую и склеивал все версии: «ступень 1 — 500 ₽; ступень 1 — 70 ₽;
   ступень 1 — 55 ₽…». Сторож вранья был бессилен — 500 ₽ и правда лежало в поданных
   модели статьях. Теперь сетку берём тем же способом, что и «Биллинг» в кабинете.

2. ССЫЛКИ МИМО ТЕМЫ. У статьи «Собрать источники — цена, очередь…» слово «цена» стоит
   в заголовке, а в синонимах «50 рублей» — она перебивала «Тарифы» на любом денежном
   вопросе. Теперь берём САМУЮ совпавшую статью и только при уверенном совпадении
   (совпавшие слова покрывают хотя бы половину вопроса); на коротком уточнении новую
   ссылку не подсовываем, если человек уже получил одну.

3. ОТВЕТ С СЕРЕДИНЫ ФРАЗЫ. «Но если вы хотите понять…» — клиент решил, что ему хамят.
   Висящий союз снимался ДО того, как выбрасывалась отговорка «в инструкции этого нет».
   Порядок исправлен (AnswerGuard::polishStart).

Плюс по решению владельца: на «сколько стоит заявка?» бот сразу называет вилку
«от 55 ₽ за заявку до 25 ₽ при большом объёме» (метка {{вилка}} из живой сетки).

Тесты бота: 240/240, из них 6 новых — воспроизводят тот разговор и падали на старом коде.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:32:15 +03:00
Дмитрий 27bf64a885 feat(bot): гостю 60 вопросов в первый час, дальше 30 + страница разборов через портал
Лимиты гостя по решению владельца: разговорился — пусть говорит (60 вопросов в
первый час), но если завис в чате на весь день — тормозим до 30 в час, иначе один
посетитель съест дневной бюджет. Потолок одного разговора поднят 40 → 150 (иначе
«60 в час» упиралось бы в обрыв разговора).

Страница разборов «Как это работает» отдаётся самим порталом по адресу
/kak-eto-rabotaet: на боевом nginx отдаёт лендинг только по «/», все остальные пути
уходят в портал — значит, конфиги сервера трогать не нужно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:46:15 +03:00
Дмитрий 9233bf9960 Merge remote-tracking branch 'gitea/main' into worktree-jivo-bot-core
# Conflicts:
#	app/routes/console.php
2026-07-14 13:27:46 +03:00
Дмитрий c0487ddaa5 fix(billing,ux): портал видит отмену платежа и перестаёт молчать в формах
Разбор живого клиента (стоматология, Красноярск, 14.07): 2 часа настраивал 26
проектов, получил 14 отказов в трёх формах и ушёл, не заплатив.

Деньги:
- отменённый шлюзом платёж больше не висит «ожидает» вечно: закрываем как failed
  с причиной (PaymentSettlementService — общий путь для webhook и крона);
- billing:reconcile-payments каждые 5 минут сам спрашивает шлюз про зависшие
  pending. Побочно страхует от ПОТЕРИ ДЕНЕГ: если webhook не дойдёт, оплаченный
  платёж всё равно зачислится;
- кабинет говорит правду: «Оплата не завершена» + «Оплатить снова» вместо
  «баланс обновится автоматически» (GET /api/billing/last-payment).

🔴 RLS-мина (поймана валидатором ДО выката): UPDATE при отмене шёл без
tenant-контекста → на проде тронул бы 0 строк, а портал рапортовал бы «отменено».
Тесты слепы (тестовая БД под postgres). Регресс-тест проверяет ПОРЯДОК:
SET LOCAL tenant ДО UPDATE. Тот же класс, что инциденты 07.07 и 12.07.

Формы (клиент бился и уходил):
- удаление проекта со сделками: причина показывается на месте + кнопка
  «Поставить на паузу» (раньше 422 улетал в никуда — 4 попытки впустую);
- создание проекта: ошибка по дням недели больше не молчит (у поля не было
  места для показа — 2 немых отказа);
- автоподбор «Добавить вручную»: показываем причину от сервера (был голый
  catch {}), длинные ссылки 2ГИС/Яндекс.Карт принимаются — трекинг-хвост срезаем
  сами. Воспроизведено тестом: именно длинная ссылка давала 3 отказа подряд.

Наблюдаемость: причины отказов пишутся в журнал (маршрут, tenant, ИМЕНА полей;
значений нет — 152-ФЗ). Уровень warning: на проде LOG_LEVEL=warning, info в
журнал не попадает вовсе. Робот-сверщик добавлен в реестр пульса.

Тесты: Pest 2475/2475, Vitest 1215/1215.
Выкачено на боевой 14.07.2026 ~13:00 МСК; сверка сразу закрыла 3 мёртвых платежа
(10 000 ₽, 5 000 ₽, 1 000 ₽).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:14:49 +03:00
Дмитрий d435101039 fix(supplier): в офлайн-режиме не дёргать поставщика при создании проекта
Прод-инцидент 14.07.2026. В batch-режиме портал при создании проекта слал поставщику
«каркас» с limit=0 и без регионов. Кабинет такой запрос ОТБИВАЕТ ВСЕГДА — снято живьём
с боевого 14.07:

    POST /admin/visit/rt-project-save
    {"status":"Error","message":"Введите limit!"}

Дальше портал считал отказ поломкой: дёргал запасной путь через браузер, тот тоже падал,
и проект уезжал в ручную очередь. Итог на бою: 114 неразобранных записей и 2 ложных
high-инцидента «похоже, кабинет поставщика упал» (08.07 и 14.07). Кабинет при этом жив —
проверено запросом с боевого: отдаёт 140 проектов, сессия рабочая.

Лиды и деньги при этом НЕ терялись: настоящие строки создаёт вечерний SyncSupplierProjectsJob
(18:00 МСК) — уже с посчитанными лимитами и регионами. Так доехали 19/19 (07.07), 25/26
(08.07), 1/1 (10.07); «недоехавший» проект №20 у поставщика на деле есть (3 строки,
включены, лимит 1+1+1 = заказ клиента) — пусты лишь поля-ссылки в карточке.

Что сделано: handleBatch больше не ходит к поставщику при создании — слать нечего, дневной
лимит считается на cut-off, а не в момент создания. Идемпотентная привязка уже существующих
строк сохранена. Слать limit>0, чтобы кабинет «принял», НЕЛЬЗЯ: у каркаса нет регионов, и
включённая строка потянет лиды со всей страны за деньги клиента.

Тесты: batch-путь переписан под новое правило (поставщик не зовётся, ручная очередь пуста);
разбор проекта на площадки (site/call → B1+B2+B3, sms+keyword → B2+B3, sms → B3) вынесен в
прямые проверки SupplierProjectGrouping — раньше он проверялся через вызовы createProject.

Прогон: 2453/2458 (единственное падение — ExampleTest/Vite manifest, окружение свежего
worktree, к правке отношения не имеет), phpstan 0, pint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:44:44 +03:00
Дмитрий 411f45ed2e Merge remote-tracking branch 'gitea/main' into worktree-jivo-bot-core 2026-07-14 10:18:16 +03:00
Дмитрий e517b7a256 Merge remote-tracking branch 'gitea/main' into worktree-jivo-bot-core
# Conflicts:
#	app/bootstrap/app.php
#	app/config/services.php
#	app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
#	db/CHANGELOG_schema.md
#	db/schema.sql
2026-07-14 09:44:56 +03:00
Дмитрий 992e62fd2c fix(visitors): карточка «Что смотрят в кабинете» падала — нет колонки t.name
Запрос активности портала просил у tenants колонку name, которой не существует:
в схеме она называется organization_name. База отвечала 42703 Undefined column,
фронт показывал красную плашку «Не удалось загрузить данные» поверх страницы
/admin/visitors. Остальные три запроса страницы работали, поэтому карточки
рисовались пустыми, а не сломанными.

Наружу поле по-прежнему отдаётся как name — контракт API и фронт не менялись.

Добавлен тест на portal-эндпоинт. Из четырёх запросов страницы тестами были
покрыты три; единственный непокрытый и оказался сломанным — тот же класс потери,
что CsvReconcileJobTest: код едет, тест нет.

Проверки: AdminVisitors 5/5, смежные Tracking 14/14, Pint чисто, Larastan 0 ошибок.
Прод не тронут — выката не было.

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-14 09:14:16 +03:00
Дмитрий 5b7bbbda50 fix(billing): письма о заморозке баланса не доходили — воркер не видел клиента под RLS
Прод-инцидент 14.07.2026. Пять писем (заморозка, напоминание, финальное,
разморозка, «проект остановлен — нет денег») уезжали в очередь с Eloquent-моделью
Tenant. SerializesModels заменяет модель на id, а воркер грузит её заново — под
ролью crm_app_user, где RLS-policy tenants_self_isolation без app.current_tenant_id
отдаёт 0 строк → ModelNotFoundException. Клиент №7 заморожен с 12.07 и не получил
ни одного письма; на проде это ломало письма о заморозке для ВСЕХ клиентов.

Письма больше не ходят в БД при отправке: несут снимок данных (без SerializesModels).

Заодно: сторож incidents:watch-failures плодил копию persistent-инцидента каждый час
(строка в failed_jobs живёт вечно, а дедуп был окном в 60 мин) — 2 залипшие ошибки
дали 31 запись за сутки и красную лампу «Очереди/джобы». Дедуп persistent теперь по
факту незакрытого инцидента, а не по возрасту последней копии.

Регрессия: BalanceMailsQueueRestoreTest (6 кейсов) + 2 теста сторожа.
Прогон: 165/165 billing+incidents, phpstan 0, pint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 08:55:37 +03:00
Дмитрий a649a41ce4 feat(visitors): короткие ссылки для рассылок — liderra.ru/s, /hh, /tg
В смс дорог каждый символ: вместо длинного хвоста с метками сервер сам
подставляет канал и дату рассылки (дд-мм), чтобы разные рассылки не слипались.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 06:53:35 +03:00
Дмитрий ff36e0c10e fix(visitors): партиции нарезает только штатный механизм + сторож схемы на 80 таблиц
Полный прогон вскрыл реальный баг: миграция создавала партицию site_events_2026_07
своими границами (в местном времени), а MonthlyPartitionManager — site_events_y2026_m07
в UTC. Партиции перекрывались (42P17) и роняли 19 ЧУЖИХ тестов. Теперь партиции
нарезает только менеджер (ensureRange), как у всех остальных таблиц.

Плюс сторож схемы обновлён под +2 таблицы учёта (80 таблиц, 141 индекс) и
записаны готовые ссылки с метками для смс/hh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:22:46 +03:00
Дмитрий 12e8ba0cac feat(visitors): чистка данных старше 180 дней (152-ФЗ)
Гостей чистит visitors:prune (воскресенье 03:15 МСК), партиции событий —
существующий partitions:drop-expired по новой настройке retention=6 месяцев.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:06:27 +03:00
Дмитрий dc335cc3d8 feat(demo): демо-проект выглядит живым — портал на кадрах не выглядит пустым 2026-07-14 05:01:04 +03:00
Дмитрий f2ab97f9f6 feat(visitors): админ-API воронки, каналов, визитов и активности в кабинете
Воронка — по уникальным живым гостям (is_datacenter=false); гости с хостингов
и VPN считаются отдельным числом, в конверсию не попадают.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:00:00 +03:00
Дмитрий cd6875edbb fix(autopodbor): откат склейки возвращает проекты и источники на свою карточку
Разбор докладной 13.07.2026: AutopodborController::restoreMergeEvent воскрешал
только карточку поглощённого конкурента (имя/сайт/справочники/телефоны/is_federal),
но не его autopodbor_sources — и если у источника был created_project_id (по нему
идут заявки/деньги), проект оставался приклеен к выжившей карточке под её именем.

Добавлен снимок источников поглощённых конкурентов: nullable-колонка
autopodbor_merge_events.absorbed_sources (миграция 2026_07_14_090000, идемпотентна),
заполняется AutopodborCompetitorMerger::snapshotAbsorbedSources ДО переноса/
переименования при слиянии (включая имя проекта на момент склейки). restoreMergeEvent
по этому снимку либо перевешивает исходную строку источника обратно на воскрешённую
карточку, либо пересоздаёт её (если строка была удалена при разрешении коллизии
dedup_key), и откатывает имя проекта.

Старые записи журнала (до миграции) снимка не имеют — восстанавливаются как раньше
(только карточка), ответ API помечается 'partial' => true с честным сообщением.

db/schema.sql v8.65 + db/CHANGELOG_schema.md — запись синхронизирована.
tests/Feature/Autopodbor/ + tests/Feature/Bot/ — 417/417 после migrate:fresh.
Живая проверка на локальном стенде через Playwright (создание проекта → слияние →
«Вернуть» → проект и источник вернулись на воскрешённую карточку) — подтверждено.

Ветка worktree-jivo-bot-core, НЕ на боевом проде — выкат отдельным решением владельца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 04:51:16 +03:00
Дмитрий 52b6a34eab fix(bot): ссылку на разбор берём у ближайшей подходящей статьи, а не только у первой 2026-07-13 20:57:46 +03:00
Дмитрий 9f1b2ccb8d fix(bot): гость получает ссылку на разбор по всем частым вопросам (канбан, баланс, заявки) 2026-07-13 20:38:12 +03:00
Дмитрий 202a49590a feat(visitors): события регистрации и входа пишет backend, гость связывается с клиентом
Три точки: обычный вход, вход через 2FA (оба пути — код и резервный),
подтверждение почты (именно там клиент реально создаётся, а не на /register).
Учёт в try/catch — ошибка учёта не может уронить вход. Регресс авторизации 114/114.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:56:54 +03:00
Дмитрий 705a2d18d4 feat(demo): конкуренты-стоматологии в демо-данных для съёмки разборов 2026-07-13 19:48:39 +03:00
Дмитрий 50daf92d6d feat(visitors): маячок POST /api/track + кука гостя на .liderra.ru
Task 4 плана 2026-07-13-visitors-analytics.md: VisitTracker (создание/обновление
гостя, запись событий, привязка к клиенту) + публичный TrackController@store,
маршрут POST /api/track (throttle:track 60/мин), лимитер в AppServiceProvider,
CSRF-исключение api/track в bootstrap/app.php.

Отклонения от плана (тест-харнесс, не прод-код):
- getCookie('lid_vid', false) в тесте убрал decrypt=false — с withCookie() это
  давало двойное шифрование (withCookie сам шифрует plain-значение).
- добавлен withCredentials() перед withCookie()+postJson/getJson — Laravel
  тест-клиент по умолчанию не шлёт cookie на JSON-запросы (как XHR
  credentials:'omit'); реальный маячок шлёт fetch с credentials:'include',
  так что прод не затронут.

Pest: 5/5 (TrackEndpointTest). Pint: app/Services/Tracking + TrackController.
2026-07-13 19:46:48 +03:00
Дмитрий 4546d39554 fix(autopodbor): разные конкуренты больше не считаются дублями (пустое поле — не совпадение) 2026-07-13 19:35:36 +03:00
Дмитрий bdd81ec8d5 feat(visitors): таблицы site_visitors + site_events (партиции, GRANT'ы, маскирование ПДн) 2026-07-13 19:21:29 +03:00
Дмитрий 1bac5d9a42 feat(bot): гостю - ссылка на пошаговый разбор по слагу статьи 2026-07-13 18:12:18 +03:00
Дмитрий b3982980f6 feat(demo): демо-клиента с красивыми данными для съёмки разборов 2026-07-13 17:08:21 +03:00
Дмитрий 292a8793cd feat(bot): гостю бот показывает пример на лендинге, а не молчит 2026-07-13 14:27:13 +03:00
Дмитрий bb40340ada fix(bot): сторож больше не режет правду «ограничений по нишам нет» 2026-07-13 13:44:15 +03:00
Дмитрий aa9537d43e fix(bot): сторож ловит жёсткий отказ в замене в любой форме (живой баг 13.07) 2026-07-13 13:37:37 +03:00
Дмитрий 1804f38724 test(bot): связка сторож↔карточка фактов — формат не разъедется молча 2026-07-13 12:49:33 +03:00
Дмитрий c3b5cfce5b fix(bot): сторож проверяет число↔факт — перепутанные цифры из карточки больше не проходят
Ревьюер прогнал AnswerGuard руками: пропускал числа словами («два проекта»),
разрыв «число...единица» («20 в день»), чужие единицы (сделок/клиент/сутки/...)
и самое опасное — число, которое в карточке ЕСТЬ, но относится к ДРУГОМУ факту
(модель переставила местами). Карточку (ClientFacts) генерирует код с
предсказуемым форматом, поэтому сторож теперь разбирает её на пары
«факт → правильное число» (баланс, цена ступени, получено/до след. ступени,
кол-во проектов, заявки сегодня/вчера/7дней/месяц, списано) и режет
несовпадение по смыслу, а не по буквальной формулировке.

TDD: 9 новых тестов (196→195 итого с учётом снятого дубля), 195/195 зелёных.
Стресс-прогон 20 предложений (10 враньё + 10 честных) вручную — 0 расхождений
после фикса дизамбигуации «этот/этом месяц» vs голое «за месяц».
2026-07-13 12:40:54 +03:00
Дмитрий 550486b61c fix(bot): переписка вошедшего клиента закрыта от посторонних (404) 2026-07-13 12:06:53 +03:00
Дмитрий 26eef67131 feat(bot): задача очереди отдаёт мозгу карточку фактов вошедшего 2026-07-13 12:00:42 +03:00
Дмитрий 3e2f27d1b4 fix(bot): карточка фактов — граница ступени, пустая сетка цен, честный докблок 2026-07-13 11:54:04 +03:00
Дмитрий d5ad30b3dc feat(bot): личный вопрос вошедшего отвечается по карточке, гостю — вход в кабинет 2026-07-13 11:48:39 +03:00
Дмитрий 8da2e8c219 fix(bot): сторож режет не только выдуманные цены, но и выдуманные количества 2026-07-13 11:36:30 +03:00
Дмитрий 7cfa998e88 feat(bot): карточка фактов — проекты, заявки, списания, пополнение 2026-07-13 11:30:47 +03:00
Дмитрий 831e8c399c feat(bot): карточка фактов — ступень; починены русские склонения 2026-07-13 11:23:01 +03:00
Дмитрий 673fddbe60 feat(bot): карточка фактов клиента — деньги 2026-07-13 11:09:27 +03:00
Дмитрий 6a3a9e5d3a fix(bot): хвосты — экскурсии по теме и только из кабинета, приветствие, доступы
1. Экскурсии «Показать на портале» включены обратно. Прежнее правило давало ссылку,
   если совпало ХОТЯ БЫ ОДНО слово, — бот вёл на уведомления в ответ на «первое
   сообщение подряд». Теперь два условия: реплика должна быть ВОПРОСОМ и слово из
   него должно попасть в заголовок статьи или её синонимы. Плюс экскурсия предлагается
   ТОЛЬКО клиенту из кабинета: гостю с лендинга ссылка внутрь кабинета бесполезна.
   Ссылки в окошке стали кликабельными (без innerHTML — текст ответа от модели).

2. На «hello» бот отвечал «в моей инструкции нет ответа» и просил телефон: заготовка
   знала только русские приветствия. Приветствие — не вопрос, эскалировать его глупо.

3. «Хочу, чтобы менеджер не видел биллинг» бот принимал за просьбу позвать НАШЕГО
   менеджера и уводил к специалисту, хотя ответ есть в статье, — и через реплику сам
   же отвечал, отчего выглядел противоречащим себе. Правило про доступ дополнено.
   Статья «Команда и доступ» дописана: сколько человек можно пустить, почему нельзя
   скрыть раздел, баланс общий на всех.

Тесты 161/161.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 10:14:52 +03:00
Дмитрий 8c3fa9a96b feat(bot): ответственность за источник — новая статья + сторож
Правило владельца 13.07.2026. Лидерра отдаёт ВСЕ номера, прошедшие через
выбранный клиентом источник (и входящие, и исходящие), и не сортирует людей
по темам. Источник конкурента может быть шире ниши клиента (клиника с 30
врачами против одной стоматологии); конкурент может расширить профиль,
ошибиться в рекламе или прозванивать купленную базу — это вне нашего контроля.

Заявка не по теме = не брак, замены за неё нет; но тон мягкий — объяснить
и развернуть клиента к работе с ИСТОЧНИКОМ (сузить, сменить, пауза).

Статья kachestvo-istochnika.md + мягкие напоминания в трёх местах, где речь
заходит об источниках. Сторож AnswerGuard::promisesNicheRelevance режет
обещания «только целевые / отсеиваем нецелевых / гарантируем трафик».
50 статей, тесты 153/153.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:49:31 +03:00
Дмитрий 7f8b245b0c fix(bot): сторож режет любой выдуманный срок связи, а не список глаголов
Прогон через свой чат 13.07.2026: бот сказал «специалист перезванивает
в течение рабочего дня». Поддержка круглосуточная, «рабочего дня» у нас нет —
это обещание, которое можно нарушить. Прежнее правило перечисляло глаголы
(«ответят», «разберут») и пропустило «перезванивает».

Правило теперь по смыслу: связь + срок = режем; «круглосуточно» — оставляем.
Тесты 151/151.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:17:20 +03:00
Дмитрий e969b7700a chore(chat): убрать настройки Jivo — чат теперь свой; экскурсии выключены
Config: jivo_bot -> bot (убраны неиспользуемые webhook_secret/outbound_url/token
- транспорт Jivo уже удалён), jivosite убран целиком (виджет больше не нужен).
Env: JIVO_BOT_* -> BOT_*; JIVO_WIDGET_ID/VITE_JIVO_WIDGET_ID удалены.
tours_enabled выключен по умолчанию (BOT_TOURS_ENABLED=false) — экскурсии
«Показать на портале» уходили не по теме вопроса (живая проверка 13.07.2026),
чинить релевантность отдельно; сторож-тест не даёт включить незаметно.

Заодно убраны обнаруженные хвосты того же виджета, оставшиеся от прежних
задач: JivoLivenessProbe (класс+тест+регистрация в мониторинге внешних
сервисов), плитка «JivoSite» в админ-дашборде, встроенный скрипт виджета
в welcome.blade.php, осиротевший тест resources/js/.../JivoWidget.vue
(компонент уже был удалён ранее), declaration VITE_JIVO_WIDGET_ID.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:32:21 +03:00
Дмитрий dddf7fbd83 feat(chat): в письме владельцу вся переписка и откуда клиент 2026-07-13 07:54:37 +03:00
Дмитрий 06ad38ecb7 test(chat): сторож хранилища счётчиков ловит и пропажу ключа
Прежний сторож проверял только «не file»: без ключа config вернул бы null,
и проверка прошла бы впустую. Теперь ключ обязан быть redis или array,
и сам класс обязан брать именно это хранилище, а не общий кэш приложения.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 07:49:20 +03:00
Дмитрий 9e0614c8a5 fix(chat): счётчики переезжают на атомарный Redis — файловый кэш терял прибавки
Файловый Cache::increment() под параллельной нагрузкой (10 x 30) досчитал 77 из 300 —
защита от накрутки счёта (пауза/час/IP/сутки) продавливалась параллельным скриптом.
ChatRateLimiter теперь берёт своё хранилище (services.jivo_bot.limiter_store, по
умолчанию redis, в тестах array) и считает через атомарные add()+increment(), а не
через фасад RateLimiter на общем кэше. Ручная проверка на живом Redis: 300 из 300.
2026-07-13 07:47:20 +03:00
Дмитрий 09b980f5ac feat(chat): три уровня счётчиков + письмо-тревога о суточном потолке
Защита денег для публичного чата (лендинг открыт всему интернету,
каждый ответ бота ~0,44 руб): пауза 2 сек между репликами, лимит
вопросов в час (гость 20 / вошедший 60), потолок реплик на разговор
(40), лимит по IP в час (60) и общий суточный потолок ответов (1500)
с одноразовым письмом-тревогой владельцу при пробитии. Отказ всегда
вежливый — клиент получает текст, а не молчание.
2026-07-13 07:29:47 +03:00
Дмитрий b4c4900635 feat(chat): опрос ответов и восстановление переписки 2026-07-13 07:17:38 +03:00
Дмитрий cfd26e38ef 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
2026-07-13 07:04:38 +03:00
Дмитрий 197e771a4f feat(chat): журнал разговоров переезжает с Jivo на свой чат
jivo_chat_id -> chat_id + добавлены source/user_id/ip (спека
2026-07-13-own-chat-widget-design §5). Историческая миграция создания
таблицы не тронута.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 06:45:54 +03:00
Дмитрий 9cd12f179f fix(bot): замены заявки НЕТ — недозвон не брак (правило владельца 13.07.2026)
В трёх статьях было написано «не берёт трубку → заменим». Это ошибка: замену
мы не делаем, и бот честно обещал её клиентам.

Тон тонкий, тему сглаживаем:
- не обещать замену/возврат («заменим», «вернём деньги») — AnswerGuard::promisesReplacement;
- но и не рубить дверью («замена не полагается», «деньги не возвращаются») — тоже режем;
- мягко: недозвон — обычное дело; совет СМС → звонок позже → мессенджер; спорный
  случай на support@liderra.ru, специалист разбирает индивидуально, без обещаний;
- не выдумывать срок ответа поддержки (она 24/7).

Статьи: zamena-zayavki.md переписана, поправлены kak-rabotat-s-nomerom.md и sdelki.md.
Два старых теста охраняли прежнее (неверное) правило — переписаны под новое.

Тесты 130/130. Бот НЕ на проде.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:59:11 +03:00