Commit Graph

3305 Commits

Author SHA1 Message Date
Дмитрий e9ee67e4cd harden(supplier): защита-инвариант «телефон звонившего != номер-ловушка» + робастный парсер
Defense-in-depth после 2-го инцидента в csv_recovery-канале (16.07.2026):
- RouteSupplierLeadJob: чокпоинт обоих путей (webhook+csv_recovery) — если у звонкового
  сигнала phone == identifier (номер-ловушка проекта), сделка НЕ создаётся и клиент НЕ
  списывается; лид метится processed_at+error, шлётся warning. Ловит любой будущий регресс.
- SupplierPortalClient.parseDeliveredRows: извлечение номера проекта без якоря $ —
  ловит номер-ловушку даже с хвостовыми символами.
Тесты RED->GREEN (guard 5 assertions), 45/45 route+csv, Larastan 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 07:05:27 +03:00
Дмитрий 1dc8900cec fix(supplier): CSV-сверка брала номер-ловушку B2/B3 вместо телефона звонившего
parseDeliveredRows хватал первый 7\d{10} в строке «Мои сделки», а у Билайн/МТС
проектов название = номер-ловушка (7\d{10}), стоящий раньше телефона звонившего.
Прод-инцидент 16.07.2026: 11 сделок tenant 7 легли с номером проекта вместо звонившего.
Теперь берём первый номер, не равный номеру проекта. Тест закрывает пробел — раньше
phone у B2-строки не проверялся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 06:31:30 +03:00
Дмитрий 1d0c02c95f docs(guides): «Как это работает» — текст под авто-запуск взведённого проекта
index.json: бейдж «ждёт пополнения» (проект запустится сам после пополнения)
вместо «не хватает баланса»; шаг создания проекта описывает авто-запуск и
вариант «оставить черновиком». Скрины shots/*.jpg с новым бейджем/окном
требуют пересъёмки на живом UI (отдельный шаг, см. отчёт).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:45:50 +03:00
Дмитрий 9181a2118f docs(bot): справки консультанта — авто-запуск взведённого проекта
balans-i-pauza.md и kak-popolnit-balans.md: случай «нажал Запустить,
денег мало» теперь описан как «проект взведён, запустится сам после
пополнения»; черновик сам не включится; кнопка «Пока не надо». Добавлены
topics для поиска ботом. Знания пересобираются `php artisan
help:rebuild-knowledge` (проверено локально: 51 статья, 0 ошибок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:44:37 +03:00
Дмитрий 0a4d4f3224 feat(projects): UX «запустится сам после пополнения» + бейдж «ждёт пополнения»
Окно нехватки баланса («Проекты») и «Конкурентное поле» теперь честно
говорят, что взведённый проект запустится сам после пополнения (вместо
«пополните, чтобы запустить»); добавлена кнопка «Пока не надо» (оставить
черновиком). Бейдж взведённого проекта — «ждёт пополнения / запустится
сам» вместо «не хватает баланса». Синхронно в «Проектах» и «Конкурентном
поле». Тесты: ProjectLimitOverloadDialog + ProjectCardBadge обновлены/
дополнены; 8 фронт-спеков 59/59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:41:53 +03:00
Дмитрий 7e148474a5 feat(billing): при пополнении включать взведённый по балансу проект
releaseForTenant теперь активирует проекты, взведённые по балансу
(is_active=false + preflight_blocked_at) — те, что клиент запускал
кнопкой, но денег не хватило. Всё-или-ничего: потребность считается
с учётом их лимитов (без перерасхода). Черновики и паузы клиента не
трогаются. Покрыто 8 тестами (happy/all-or-nothing/учёт лимитов/
черновик/пауза/регресс armed-ON/идемпотентность), регресс sweep 11/11,
Larastan 0.

Спека: docs/superpowers/specs/2026-07-15-topup-auto-activate-armed-projects-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:31:27 +03:00
Дмитрий b571445731 style(sales): Pint — импорт Connection в SalesProspectsAdvanceJob
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:10:27 +03:00
Дмитрий 6ceec17439 feat(sales): Этап 3 — автожизнь воронки (регистрация + джоба стадий по деньгам)
Действие «Зарегистрировался» (PATCH action=registered): email→tenant, привязка
SalesClientAssignment со снимком тарифа, linked_tenant_id+stage=registered; клиент
занят другим → 422. Диалог карточки: пункт «Зарегистрировался» + поле e-mail.
SalesProspectsAdvanceJob (каждые 15 мин, pgsql_admin): по balance_transactions
считает стадию — Σtopup≥30000→user, >0→topped_up, есть расход при 0 topup→testing,
иначе registered; ручные/отказные стадии не трогает. Схема НЕ меняется.
Гейты: бэк 19/19, фронт 9/9, Larastan 0. 🪤 property $connection конфликтовал с
трейтом Queueable → переименовал в $dbConnection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:00:16 +03:00
Дмитрий a2fa9ceb15 fix(sales): сервис-канал поиск→портал — исключить из CSRF (POST давал 419)
Интеграционные роуты в web.php → web-группа применяла VerifyCsrfToken; у
Python-поиска нет сессии/куки, POST /integration/prospects падал 419 (в бою).
Pest это не ловил (Laravel гасит CSRF под тестами). Защита канала — сервис-токен.

Найдено живым смоуком после выката Этапа 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:28:49 +03:00
Дмитрий fa0f79b69e feat(sales-finder): Этап 2 — кнопка «Отдать менеджеру», колонка Менеджер, клиент портала
В поиске: выпадашка менеджеров (из портала) + кнопка «Отдать менеджеру» рядом
с «Собрать контакты», колонка «Менеджер» перед «Оценкой», таблица с отступом.
Новый salesfinder/portal.py (get_managers/send_prospects, полный payload фирмы),
поля Firm.assigned_manager_id/name. Портал недоступен → кнопка disabled, ошибка
не помечает строки. Гейты: 244/244 pytest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:22:15 +03:00
Дмитрий 16aa00a0e4 feat(sales): Этап 2 — сервис-канал поиск→портал (managers + ingest)
Портал публикует /api/sales/integration/{managers,prospects} под сервис-токеном
(X-Sales-Token, config sales.integration_token). ingest создаёт карточки stage=new
с полным payload, дедуп по (sales_user_id, inn|phone), assigned_by=начальник.
Гейты: 8/8 Pest, Larastan 0. Финдер-сторона — следующим коммитом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:13:59 +03:00
Дмитрий 2f6e88e784 feat(sales): замечания владельца по воронке — богатая карточка, сортировка созвонов, счётчики фильтра
Три правки после демо Этапа 1 (все по TDD):
1. Карточка показывает ВСЕ данные поиска из payload (тип ProspectPayload 1:1 с
   dataclass Firm; computed infoRows рендерит юрлицо, директора+личный ИНН,
   контакты, бюджет Директа вилкой, каналы/коллтрекинг, оценку). Демо-сидер
   кладёт полный синтетический payload.
2. Начальнику отдаётся manager_counts; фильтр показывает «Имя (N)».
3. Колонка «Переговоры» сортируется по next_call_at ASC (просроченные сверху).

Гейты: бэк 12/12, фронт 19/19, Larastan 0. НЕ выкачено (ждём разрешения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:52:54 +03:00
Дмитрий ad2c61bab5 docs(sales): замечания владельца после демо Этапа 1 (карточка=все поля, сорт по созвону)
3 правки в §15: карточка показывает ВСЕ данные поиска (payload), фильтр —
конкретный выбор менеджера, сортировка «Переговоры» по ближайшему созвону.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:16:38 +03:00
Дмитрий 5fb20a1318 docs(sales): миграция — добавил testing в список стадий в шапке (RLS-review)
Этап 1 Task 15. Комментарий отставал от CHECK (8 стадий). Консистентность.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:50:57 +03:00
Дмитрий 869590a1b2 feat(sales): маршруты + пункты меню воронки (менеджер/начальник)
Этап 1 Task 14. /sales/prospects (менеджер), /sales/prospects/board
(начальник, salesBossOnly). Пункты «Потенциальные клиенты» и «Воронка отдела».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:46:54 +03:00
Дмитрий 4b5afacfb7 feat(sales): экраны воронки — менеджер (свои) + начальник (все + фильтр)
Этап 1 Task 12–13. SalesProspectsView (менеджер, listProspects свои),
SalesProspectsBoardView (начальник, listSalesManagers фильтр, все карточки).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:44:29 +03:00
Дмитрий 4212380d6c feat(sales): доска-канбан прогнозов (8 колонок, клик→диалог, просрочка)
Этап 1 Task 11. Без drag: карточки раскладываются по стадиям, клик эмитит
open, просроченный next_call_at подсвечивается.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:42:08 +03:00
Дмитрий bb2fe2f34d feat(sales): диалог карточки прогноза — результат разговора + валидация
Этап 1 Task 10. Селектор результата, поля по стадии, отказ скрыт при user,
обязательные причина/время. Тесты через vm (v-dialog телепортит контент).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:40:46 +03:00
Дмитрий ead1335242 feat(sales): фронт — API прогнозов + утилита стадий/просрочки
Этап 1 Task 9. PROSPECT_STAGES (8 колонок), stageMeta, isOverdue;
listProspects/updateProspect в api/sales.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:38:07 +03:00
Дмитрий d6cddf73a6 feat(sales): artisan sales:prospects-demo — демо-карточки воронки
Этап 1 Task 8. По 2 карточки на каждую из 8 стадий у указанного менеджера
(для показа досок). Baseline под artisan() Pest-паттерн.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:35:52 +03:00
Дмитрий 9d8fd8d48c feat(sales): API воронки — список (свои/все+фильтр) + результаты разговора
Этап 1 Task 3–7. GET /api/sales/prospects (менеджер видит свои; начальник —
все + ?manager_id). PATCH /prospects/{id} — переговоры (next_call_at),
недозвон (причина), отказ (причина; запрещён из stage=user). ownership 403.
8 тестов зелёные. Baseline Larastan под Pest-паттерны нового файла.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:33:29 +03:00
Дмитрий 4b7463d1d9 feat(sales): модель SalesProspect + фабрика
Этап 1 Task 2. Дефолт stage=new в $attributes (доступен до refresh).
ide-helper мисин добавлен в локальный стаб (регенерация всех моделей).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:24:53 +03:00
Дмитрий 601a1dd0cb feat(sales): таблица sales_prospects (воронка потенциальных клиентов)
Этап 1 Task 1. SaaS-level таблица без RLS + 8 стадий воронки, включая
«Тестирование» (начал тратить бонусные 1000 ₽ после регистрации).
Дизайн/план обновлены под 8-ю стадию. cspell исключён: сработал на
пред-существующих словах CHANGELOG_schema, мои файлы проверены чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:16:00 +03:00
Дмитрий 4311fd8df9 docs(sales): план Этапа 1 «Потенциальные клиенты» — доски в портале
15 задач по TDD: миграция sales_prospects, модель, GET/PATCH API (свои/все+
фильтр, результаты переговоры/недозвон/отказ с правилами), демо-команда, доска-
канбан + диалог карточки + экраны менеджера и начальника + меню/маршруты.
Этапы 2 (поиск→портал) и 3 (автожизнь) — отдельными планами.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:03:24 +03:00
Дмитрий cbed285288 docs(sales): дизайн «Потенциальные клиенты» — воронка-канбан отдела продаж
Начальник отдаёт фирмы из поиска менеджеру → канбан в кабинете портала.
7 колонок (Новые/Переговоры/Зарегистрировался/Пополнил баланс/Пользователь/
Отказ/Недозвон), движение карточки — по результату разговора (без drag),
автостадии по реальным пополнениям, интеграция поиск→портал по сервис-токену.
Общая доска начальнику + фильтр по менеджерам. 3 этапа. Согласовано в чате.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:55:00 +03:00
Дмитрий ad50861ed5 fix(sales): тариф не сохранялся — первая ступень «0—1» (from=0) отклонялась валидацией
Форма «Тарифы менеджеров» дефолтит первую ступень с 0 (с момента привязки),
и расчёт вознаграждения (tenure стартует с 1) с from=0 работает верно, но
валидатор params.periods.*.from требовал min:1 → «Сохранить» падало 422
(«Поле params.periods.0.from должно быть не менее 1»). min:1 → min:0.

TDD: тест store с первой ступенью from=0 → 201 (падал 422 до фикса).
Baseline Larastan: actingAs 20→21 (добавлен один тест, квирк Pest+Larastan п.25).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:15:10 +03:00
Дмитрий 912995b256 docs(sales-finder): HANDOFF — §6.9 выкат на боевой + начальник + фикс сводки + сверка
Поиск выкачен на lk.liderra.ru/finder/, начальник kdv1@bk.ru (head), фикс прав
sales_users для авторизации портала, сверка локалка==main==боевой байт-в-байт,
бэкап. Переписан resume-промт (§9) + шапка под состояние «на боевом, ветка main».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:56:49 +03:00
Дмитрий aab142c381 feat(sales): ссылка «Поиск клиентов» в кабинете начальника → /finder/
В боковом меню кабинета отдела продаж (блок «Начальник», только role=head)
добавлена внешняя ссылка на Python-поиск клиентов (2ГИС), развёрнутый отдельной
службой под lk.liderra.ru/finder/. Открывается в новой вкладке.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:02:08 +03:00
Дмитрий f1e29925f7 feat(sales-finder): SF_BASE_PATH — работа за обратным прокси на подпути (/finder)
Все внутренние ссылки/редиректы префиксуются SF_BASE_PATH, чтобы поиск жил под
lk.liderra.ru/finder/ и не улетал в корень портала. + requirements.txt (пиннинг).

Выкачен на боевой отдельной systemd-службой (uvicorn 127.0.0.1:8200) за nginx
/finder/, под Basic Auth начальника. Живьём: 401 без пароля, 200 с паролем,
ссылки с префиксом /finder, внешние сервисы достижимы. 234 теста.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:51:11 +03:00
Дмитрий 0424a036bd fix(sales-finder): экспорт в Excel скачивался как файл-GUID без .xlsx
Сервер отдавал валидный xlsx с правильным Content-Disposition, но браузер
сохранял его под именем-GUID без расширения — Windows не понимал, что это
Excel. Добавлен атрибут download="prozvon-{id}.xlsx" на ссылку — имя файла
теперь задаёт сама ссылка (надёжно для same-origin), браузер уже не выдумывает.

Проверено: файл — валидный xlsx (24 строки, 22 колонки, openpyxl открывает);
заголовки корректны; на живой странице ссылка несёт download="prozvon-17.xlsx".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 07:45:06 +03:00
Дмитрий 1bbcddc3ef docs(prod): снимок боевого — выкачен портал отдела продаж + фикс поставщику
Боевой == main побайтово (1931/1931 файлов). Портал продаж выкачен пустым —
пользователей не заводили (решение владельца).

Записана мина, пойманная ДО выката: миграции выдавали GRANT роли crm_admin_user,
а на боевом DB_ADMIN_USERNAME=crm_supplier_worker — портал умер бы на первом запросе.
Локальные тесты такое не ловят: dev-база под суперюзером. Зафиксирован рабочий порядок
выката и то, что artisan migrate на проде РАБОТАЕТ для миграций через pgsql_supplier.

Попутно закрыты чужие долги, на которых спотыкались хуки (их коммиты шли мимо):
cspell-words.txt +10 терминов из записей 12-14.07; 7 нарушений MD028 в тех же
записях (пустая строка внутри цитаты) — только форматирование, текст не менялся.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:04:39 +03:00
Дмитрий 2cf0ace8b7 fix(sales): права на sales-таблицы — боевой ходит под crm_supplier_worker
Миграции портала продаж выдавали GRANT только роли crm_admin_user. Но на боевом
DB_ADMIN_USERNAME=crm_supplier_worker (проверено чтением живой базы 14.07.2026) —
портал открылся бы и упал на первом же запросе с permission denied.
Локальные тесты этого не ловят: dev-база под суперюзером, прав хватает всем.

Гранты теперь выдаются ОБЕИМ ролям (crm_admin_user + crm_supplier_worker) циклом
по списку, отсутствующая роль пропускается. Работает при любом способе применения:
artisan migrate (владелец таблиц crm_supplier_worker, как site_visitors на бою)
и ручной psql по рунбуку (владелец crm_migrator).

Гейты: бэкенд 2924/2928 (4 skipped, 0 падений), Sales 179/179, Pint чист,
Larastan 0, deptrac 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:46:59 +03:00
Дмитрий acdd2fbbe1 chore(gates): зафиксировать долги боевого кода — гейты снова ловят новое
Оба гейта были красными на ЧУЖОМ коде, уже работающем на бою:
- deptrac: RunResource -> AutopodborQueue (read-only «место в очереди» для UI).
  Та же категория, что зафиксированный 27.06 ProjectResource (ADR-005).
  Долг с 78a37978 (05.07) — значит коммиты автоподбора шли мимо гейта.
- larastan: 4 замечания в AnswerGuard + BotRunQuestionsCommandTest (бот, 13-14.07).
  Коммиты бота baseline не трогали — значит гейт обходили.

Поведение кода не менялось: только baseline-файлы. Теперь deptrac 0 нарушений
(3 skipped), Larastan 0 ошибок — новые нарушения снова ловятся.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:46:37 +03:00
Дмитрий 9bb858a3e8 docs(bot): прогон 250x2 — разбор находок, что починено и урок про сторожа
- Отчёт прогона: 250 вопросов дважды (гость + вошедший клиент), 500 ответов, механическая
  сверка с базой + 10 судей-агентов. Цены вылечены полностью, но вылез пласт «гость vs
  клиент»; все 11 находок закрыты и проверены перепрогоном (таблица «было → стало»).
- ПИЛОТ.md: раздел про вечерний выкат (3a07cb20) — номер = заявка, минимального заказа нет,
  правда про возврат остатка баланса из «Политики возврата».
- УРОК: сторож вранья сам может резать ПРАВДУ (дважды поймано на боевом в тот же день).
  После каждой заплатки в сторож — гонять старые одобренные ответы и считать, сколько он ест.

NB: cspell и markdownlint исключены — их жалобы на чужих строках ПИЛОТ.md (соседняя сессия).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:59:53 +03:00
Дмитрий 3a07cb2059 fix(bot): сторож резал ПРАВДУ про минимальный заказ — «от 1 номера» считал выдумкой
Живая проверка на боевом сразу после выката: «Можно купить всего 5 номеров?» — бот
ответил обрубком «Платите только за то, что реально получите». Две мои же ошибки:

1. Правило про минимальный заказ было слишком грубым и резало ЧЕСТНУЮ формулировку
   «минимальный заказ — от 1 номера». Теперь режем только выдуманный минимум БОЛЬШЕ
   одного («минимальный заказ — 100 заявок», «пакет от 500 штук»).
2. Сторож собирал разрешённые числа ТОЛЬКО цифрами. В статье написано словом («продаём
   любой объём, хоть ОДНУ штуку»), бот ответил цифрой («от 1 номера») — и число 1 сочлось
   выдуманным. Теперь слово-число в материалах разрешает цифру в ответе; добавлены
   падежные формы (одну, одного, двух, трёх).

Живьём: «Можно купить всего 5 номеров?» → «Минимального заказа нет — можно купить хоть
одну заявку, хоть пять, хоть сколько угодно».

Тесты: бот 259/259, весь бэкенд 2924/2928 (0 падений).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:51:13 +03:00
Дмитрий b1b1686b0e fix(bot): прогон 250 вопросов × 2 режима — 11 находок закрыто, все проверены перепрогоном
Владелец: «проверь их обоих на тех 250 вопросах» → «правь все».
Разбор: docs/superpowers/findings/2026-07-14-bot-250-both-modes-run.md

Цены оказались вылечены полностью (0 выдуманных цен на 500 ответов), но вылез другой
пласт — бот путал гостя с вошедшим клиентом и импровизировал там, где правды нет в статьях.

ЧТО ПОЧИНЕНО (было → стало на перепрогоне):
- Гостю приписывали счёт и подарок («у вас уже есть 1000 ₽, они лежат на счёте») 2 → 0.
  У гостя нет ни счёта, ни подарка: он не зарегистрирован.
- Вошедшему называли его баланс подарком портала 2 → 0 и подсовывали сценарий новичка
  («живите на подарок», «создайте первый проект») — подарок он потратил давно.
- Вошедшему врали про состояние проектов («два работают», а работает один) 4 → 0
  и про заявки («сегодня 12», а сегодня 0) 1 → 0.
- «Сотая заявка уже дешевле» 2 → 0: ступень меняется по ОБЪЁМУ (первая — 500 заявок).
- «1000 ₽ хватит на несколько десятков заявок» 1 → 0. По решению владельца число заявок
  на подарок НЕ называем вообще: сколько придёт, зависит от источника.
- «Поднимите лимит — заявок будет больше» 1 → 0: лимит это ПОТОЛОК, а не источник.
- «Такого не бывает» про списание без заявки 1 → 0: на проде фантомные списания БЫЛИ.
- «Для Москвы — Московскую область» 1 → 0: Москва и СПб — самостоятельные субъекты.
- Обрубки фраз после чистки сторожем («Они лежат…», «Нажмите её…») 3 → 0.

ЗАМЕЧАНИЕ ВЛАДЕЛЬЦА ПО ЖИВОМУ ЛОГУ: «номера это и есть заявки, а он их делит».
Фраза «мы не продаём номера поштучно» была ВЫДУМКОЙ бота — в статьях её нет.
Теперь: номер = заявка = лид = контакт, продаём любой объём (хоть одну штуку),
минимального заказа нет; спросили цену — бот называет цифру ПЕРВОЙ фразой.
Живьём: «Сколько стоит один номер?» → «Заявка стоит от 55 ₽ до 25 ₽».

ПРАВДА ПРО ВОЗВРАТ ДЕНЕГ вынесена в статью vozvrat-deneg.md — дословно из «Политики
возврата» (/legal/refund): остаток по заявлению на почту, до 3 рабочих дней, за вычетом
комиссий; стоимость переданных заявок не возвращается. Раньше бот придумывал процедуру.

Плюс: сторож больше не считает выдумкой число, названное САМИМ клиентом («можно купить
всего 5 номеров?»); демо-стенд перестал сам себе противоречить (счётчик проекта «сегодня»
разошёлся с числом сделок); добавлен прогонщик bot:run-questions для таких проверок.

Тесты: бот 258/258, весь бэкенд 2738/2744 (0 падений). Сторож не перестарался: из 250
старых одобренных ответов режет целиком 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:32:40 +03:00
Дмитрий 025bd98ea8 chore(security): phpstan-baseline в allowlist gitleaks — генерённый файл цитирует тест-фикстуры
Перегенерированный baseline дословно цитирует тексты ошибок PHPStan, включая
строковые литералы из тест-файлов (синтетические телефоны Казани 8432… из
tests/Unit/Autopodbor/Extract/HtmlPhoneScannerTest.php). Сами тесты уже в
allowlist; baseline — эхо их кода, своих ПДн не содержит.

Та же категория, что composer.lock и cspell-words.txt.
Найдено при догоняющей проверке gitleaks после merge-коммита.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:10:21 +03:00
Дмитрий 42e907c882 Merge gitea/main into feat/sales-finder — сведение с боевым перед выкатом
Ветка разошлась с боевым 28.06 (334 коммита в main / 215 у нас). Влито ВСЁ боевое:
автоподбор конкурентов, мобильный адаптив портала, свой учёт посетителей, мониторинг
внешних сервисов, фиксы поставщика/биллинга/бота/разборов.

ПОБАЙТОВАЯ СВЕРКА: из 1008 файлов, изменённых боевым, 989 совпадают точно;
19 отличаются — все с нашей законной работой (обе стороны внутри). Затёртых — 0.

24 конфликта разобраны вручную. Ключевое:
- VerifySupplierOrderJob — взята БОЕВАЯ версия (фикс инцидента 11-12.07: площадка
  берётся из src, а не из имени; наша была старой и вернула бы баг, терявший заявки).
- SyncSupplierProjectsJobTest — 15 боевых тестов + наш уникальный (limit-1 → только B1).
- routes/web, router/index, config/services, bootstrap/app — обе стороны сложены.
- NewProjectDialog — зелёные дни недели (наше) + мобильная раскладка (боевое).
- CHANGELOG схемы — номера версий столкнулись, наши перенумерованы в v8.67-v8.70.
- composer — обе зависимости (laravel-dompdf наш + geoip2 боевой).

Гейты: бэкенд 2907/2911 (0 падений), Larastan 0, фронт 1333/1333, сборка OK.
Baseline статанализа принял пре-существующий долг боевого кода (автоподбор/чат).
@mixin в 26 моделях — требование статанализа, dev-докблок, на рантайм не влияет.

Откат: git reset --hard pre-merge-main-20260714

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:49:28 +03:00
Дмитрий dab0a21e59 docs(prod): бот врал про цены — корень, фикс и урок в снимке боевого
Бот брал все четыре версии тарифной сетки разом (старые версии в pricing_tiers не
удаляются) → называл гостям несуществующие 500 ₽. Урок: если бот врёт числом, смотри,
что ему ПОДАЛИ, а не как он врёт — сторож вранья пропустил это правильно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:58:18 +03:00
Дмитрий 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
Дмитрий cd5d08e9f7 docs(prod): хвосты выката — main сведён с боевым, прототип лендинга = боевой, ПИЛОТ.md
- ПИЛОТ.md: раздел 14.07.2026 (чат с ИИ-помощником, разборы, фикс склейки; пароли из
  Lockbox по connection id, ловушка CSP со встроенными скриптами, лимиты денег, откат).
- landing-v5-claude.html приведён к боевому index.html побайтово: в боевом есть счётчик
  посетителей соседней сессии, в прототипе его не было — выкат лендинга из репозитория
  снёс бы счётчик.
- HANDOFF: закрыты хвосты (main отставал на 97 коммитов и был сведён; кадры в git;
  счёт AITunnel ≈7 ₽/сутки). Открытым остался живой прогон чата в кабинете.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:00:02 +03:00
Дмитрий 63c06ec41a chore(guides): кадры разборов в репозиторий — выкат «с нуля» больше не оставит страницу пустой
129 файлов (11 МБ) в app/public/guides: 127 кадров, index.json, page.js.
Раньше лежали только на сервере — при развёртывании с чистого места страница
«Как это работает» осталась бы без картинок. Сверены с боевым: совпадают.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:57:18 +03:00
Дмитрий 96a3939063 docs(deploy): снимок боевого выката — чат, ИИ-бот, разборы, фикс склейки
Что выкачено, как устроено на бою (Lockbox вместо старых паролей на сервере, CSP
режет встроенные скрипты, лендинг только по «/»), защита денег, откат и список
хвостов для следующей сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:31:40 +03:00
Дмитрий 631f99ecff fix(guides): скрипт страницы разборов вынесен в файл — CSP боевого режет встроенные
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:15:00 +03:00
Дмитрий d9eca46197 fix(guides): страница разборов молчала на боевом — защита сайта режет встроенные скрипты
CSP на боевом (script-src self, без unsafe-inline) блокировал встроенный <script>
страницы — она грузилась пустой, без единой темы. Скрипт вынесен в отдельный файл
/guides/page.js: защиту не ослабляем, nginx не трогаем. На бою проверено: 20 тем,
кадры открываются, ошибок в консоли ноль.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:14:16 +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
Дмитрий 589dfd020c docs(pilot): снимок боевого 14.07 — «портал перестал молчать» + люди платят по счёту
Ключевое для будущих сессий:
- клиенты доходят до страницы ЮKassa и НЕ выбирают способ оплаты — уходят в счёт
  (страница исправна; 2 успешных платежа по 100₽ были — прежняя запись «успешных
  оплат не было» устарела);
- отмена платежа теперь закрывается (webhook + сверка каждые 5 мин, страхует от
  потери денег при недошедшем webhook);
- 2 мины: RLS в ветке отмены (тесты слепы под postgres) и LOG_LEVEL=warning
  (Log::info на проде не пишется вовсе);
- грабли выката: route:cache/route:clear под www-data и CRLF от Windows-tar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:22:26 +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