Commit Graph

632 Commits

Author SHA1 Message Date
Дмитрий 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
Дмитрий c320e095e1 fix(bot): ювелирная доводка после второго круга судей
- «скидка ЗА ОБЪЁМ» больше не считается просьбой скидки: бот отвечает про
  ступени тарифа, а не уводит к специалисту.
- «где посмотреть мою ступень / мой лимит» — вопрос про портал, а не про
  личные данные клиента: эскалация снята.
- Цена ↔ ниша/регион: правило по СМЫСЛУ (AnswerGuard::priceTiedToNiche).
  Враньё «цена зависит от ниши» возвращалось ПЯТЬЮ формулировками —
  регулярка на каждую не работает. Честное «цена НЕ зависит от региона»
  сохраняется.
- Вырезано: «на тысячу выйдет от 2 до 20 заявок» (реально максимум 4),
  «работаем не со всеми нишами» / «есть направления, с которыми мы не
  сотрудничаем» (ограничений по нишам НЕТ), «не берёт трубку — забей»
  (такую заявку МЕНЯЮТ), «баланс на паузе не потратится», выдуманные сроки
  зачисления по счёту.
- Промпт: цена только от ступени объёма; запретных ниш нет.
- Статьи: тронута ТОЛЬКО строка topics (синонимы поиска), тексты не менялись.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:35:11 +03:00
Дмитрий bd918c1dce fix(bot): держим нить разговора и не теряем телефон клиента
Прогон 46 диалогами (250 реплик подряд в одном чате) вскрыл то, чего не видел
прогон отдельными вопросами:

- Клиент оставил телефон → бот не отвечал ВООБЩЕ: письмо в поддержку слалось
  синхронно внутри ответа, SMTP не отозвался → упала вся задача (tries=1).
  Теперь номер в чате = заявка на звонок: принимаем всегда (даже без просьбы
  бота), письмо уходит через очередь и не может уронить ответ.
- Бот «забывал» себя: сторож резал цену, взятую из истории чата, ответ пустел
  → эскалация. Числа из своих прошлых ответов теперь разрешены; память чата
  3 → 5 пар реплик; в эскалационную ветку тоже передаётся история.
- AnswerGuard: + телефон поддержки (его нет), + юр-гарантии в новых оборотах
  («Да, законно», «Штраф не прилетит»), + враньё «на паузе списаний нет»,
  + выдумки про 18:00/ночь, + «цена зависит от ниши»; снимает висящее «Но…»
  и голое «Правильно, не будут» после вырезанной лжи.
- Промпт: не повторять сказанное, не поддакивать клиенту, без нумерованных списков.
- pravilo-18-00.md: 18:00 — граница для ИЗМЕНЕНИЙ, а не конец приёма заявок.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 04:52:09 +03:00
Дмитрий c5125e981e feat(bot): выбор модели + три замка безопасности + инструкция 51 статья
Прогнал 250 вопросов через настоящий вебхук шесть раз и сравнил четыре модели
вслепую силами судей-агентов. Итоги — в docs findings.

Модель. Появился переключатель services.jivo_bot.llm: yandex (данные не покидают
РФ) либо aitunnel (зарубежные модели через российский шлюз). По слепому сравнению
Claude Haiku 4.5 выиграл финал у Gemini 157:24 и у обеих моделей Яндекса: он
полнее, точнее по кнопкам и цифрам и говорит как человек, а не справочник.

Три замка, потому что живая модель = самоуверенная модель:
- PersonalDataMask — телефоны и почта вырезаются ДО отправки в любую модель
  (клиент оставляет номер в чате; за границу ПДн уходить не должны).
- ChatTextCleaner — markdown и смайлики (Jivo их не рисует): было 191 ответ
  с разметкой, стало 0.
- AnswerGuard — режет юридические гарантии («вас не оштрафуют», «мы гарантируем»,
  «не нарушаем закон») и ЛЮБУЮ цену, которой нет в выданных статьях. Пусто после
  чистки → зовём живого специалиста.

Поиск по инструкции переписан: выбрасываются вопросительные слова (из-за них
«какие у вас тарифы» находило выгрузку в Excel), ранжирование сперва по заголовку
и синонимам, выдача — три РАЗНЫЕ статьи и каждая целиком, плюс починка опечаток
(«каг сазадь праэкт») и учёт прошлой реплики в коротких «а это платно?».

Инструкция: 40 → 51 статья. Новые — по фактам владельца: бесплатный тест и
подарок 1000 ₽, «мы канал, а не реклама», СМС → через час звонок, средние
показатели (8–12 из 100 разговаривают, ещё 5–7 по СМС), номер не вернётся
30 дней, замена брака, вывод остатка, поддержка 24/7, удаление данных, ниши.
Вычищено враньё: минус баланса, «списали без заявки», пауза вечером, выходные,
«Вся РФ», статус «Отказ» (его нет — есть «Не реализовано»), amoCRM, платный
повторный сбор, нарастающий итог тарифных ступеней.

Тесты: 106 из 106. Каждая найденная тупость закреплена тестом.

НЕ НА ПРОДЕ. Выкат — только по прямой команде владельца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:44:48 +03:00
Дмитрий 7d6813e47d fix(tests): партиция за прошлый месяц + возврат аудит-тестов, отставших от ADR-021
Разбор хвостов полного прогона: было 2407/2418, стало 2417/2418 — единственное
оставшееся падение ExampleTest воспроизводится только там, где не собран фронт
Vite manifest not found, к коду отношения не имеет.

1. partitions:create-months — новый флаг --behind, по умолчанию 1.
   Нарезалось только «текущий месяц и вперёд», поэтому на свежей базе партиции за
   прошлый месяц не было НИКОГДА. Между тем строки с датой в прошлом месяце — штатное
   явление: тридцатидневное окно списаний метрика runway и вставки на стыке месяцев.
   На боевом не всплывало — там партиции копятся с самого начала, проверено: нарезаны
   до января 2027, крон жив. А на свежей базе и в CI INSERT падал «no partition found»
   в первой половине КАЖДОГО месяца. Ловилось двумя тестами AdminTenantShowTest.runway
   и роутингом лида.

2. Возвращены три аудит-теста, потерянных при сведении main 09.07.
   ADR-021 сделал цепочки журналов общими и AuditChainConfig в main это уже отражает,
   а тесты остались на старом per-tenant поведении ADR-018 и падали. Правильные версии
   лежали на ветке fix/audit-chain-global-scope, но в main не доехали — тот же класс
   потери, что и CsvReconcileJobTest.

Тесты: полный прогон 2417/2418 зелёный, Pint чисто.

Известное и НЕ тронутое, вынесено в отдельный список:
  - Larastan на main даёт 30 замечаний уровня типов, ни одного живого бага:
    мёртвые ветки в AuditRebuildChain, оставшиеся от ADR-021; ресурсы автоподбора без
    аннотации модели; ProjectController читает виртуальные пометки gate_payload и
    launch_deferred, которых нет в таблице.
  - Тест лимита входов флакует только в длинном прогоне, в одиночку зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:02:21 +03:00
Дмитрий b28f858419 fix(supplier): метка канала B1_/B2_/B3_ не стирается при обновлении заказа
Прод-инцидент 11-12.07.2026: робот итоговой проверки слал письма «нет заказа у
поставщика» на строки, которые в кабинете ЕСТЬ, включены и с верным лимитом.

Корень: кабинет дописывает метку канала к имени строки только при СОЗДАНИИ, а при
обновлении сохраняет имя ровно как прислали. Наш ежедневный updateProject слал голый
uniqueKey и каждый прогон стирал метку. Последствия:
  - итоговая проверка выводила площадку из префикса имени и переставала узнавать
    строку -> ложное missing 11.07 и 12.07;
  - лид от такой строки приходил с project без метки -> webhook не мог определить
    канал и писал platform=DIRECT вместо B1/B2/B3, то есть терялась атрибуция канала.

Что сделано:
  - SupplierPortalClient::toPayload — на update имя уходит с меткой канала; на create
    остаётся голым, там метку ставит сам кабинет и один save с тремя флагами рождает
    три строки, общего префикса у них нет.
  - VerifySupplierOrderJob::normalizeLive — площадка берётся из служебного поля src
    rt/bl/mt, а не из префикса имени; сверка больше не зависит от имени вообще.
  - Новая разовая команда supplier:repair-project-names — возвращает метку строкам,
    у которых её уже стёрли. Payload собирается ИЗ ЖИВОЙ строки кабинета, меняется
    ровно одно поле name; по умолчанию сухой прогон, запись только с --apply.

Ветка пересобрана на gitea/main — закрывает follow-up «фича итоговой проверки заказа
не сведена в main». Попутно возвращён CsvReconcileJobTest, отставший от кода после
сведения main 09.07: он не фейкал fetchDeliveredLeads и падал 9 из 11.

Боевой liderra.ru: выкачено, починена 81 строка, робот показывает 0 расхождений
138 наших строк вместо 57. Двум лидам восстановлен канал по журналу выдач поставщика.

Тесты: Pest supplier 277/277, Pint clean, Larastan 0 новых ошибок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:14:22 +03:00
Дмитрий beb0528612 fix(bot): продукт, 50% номеров и договор-оферта — по живым замечаниям владельца
1. 🔴 МОЙ КОСЯК: на «как получить номер человека, заходящего на сайт» бот отвечал
   «нельзя» — то есть отговаривал клиента от того, ЧТО МЫ И ПРОДАЁМ. Владелец:
   «как нельзя, а мы чем занимаемся?!». Теперь: «это ровно то, что делает Лидерра —
   создайте проект, укажите сайт, контакты пойдут в Сделки».
2. «Все ли номера с источника цепляет система?» — нет, ПРИМЕРНО ПОЛОВИНУ: передавать
   можно только абонентов с согласием (требование закона). Формулировка владельца.
3. «Как подписать договор с юрлицом?» — у нас публичная ОФЕРТА, подписывать нечего.
   Слово «договор» убрано из стоп-тем; новая статья: оферта (/legal/offer), политика,
   возврат + закрывающие документы (счёт → оплата → акт, УСН без НДС).
   Деньги/жалобы/претензии — по-прежнему живой специалист.

Все три проверены ЖИВЬЁМ через настоящий вебхук чата (а не в обход очереди).
Инструкция: 41 статья. Pest бот 58/58.

🪤 Урок: после правки кода бота ОБЯЗАТЕЛЬНО перезапускать queue:work — воркер
кэширует бут. Владелец видел старые ответы, пока я радовался зелёным тестам.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:13:34 +03:00