Стадию меняют два разных места — результат разговора менеджера и автопереезд
по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день»
было не из чего построить. Запись движения перенесена в событие модели: один
шов на всех, включая любой будущий третий источник.
- sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям);
- prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»);
- stage += 'trash' — место под колонку «Корзина».
Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе
тест джобы и тест API.
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.
Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов gitleaks отработал по-настоящему.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
В спеке §3 уточнено по факту кода: нормализатор отдаёт номер с плюсом,
а хранится он без плюса — плюс срезаем.
Сторожа орфографии и разметки пропущены: они не находят проблемы, а падают —
их библиотеки в корне обрезаны той же поломкой, что статанализ и сторож
журналов. Сторож секретов gitleaks — живой, работает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Проверка ссылок падала на девяноста двух ошибках, поэтому любая отправка в main
шла через обход. Смысл починки не в красоте: пока ошибок девяносто две, новая
битая ссылка тонет среди них и никто её не замечает. Теперь ноль, и пуш проходит
обычным порядком.
Что было внутри девяноста двух:
68 — ссылка написана от корня хранилища, а документ лежит на две-три папки
вглубь. Приписаны шаги вверх. Файлы всё это время лежали на месте.
8 — файл существует, но переехал: Jobs/Billing, Jobs/Supplier, Services.
Адреса переписаны на нынешние места.
8 — файла нет и не будет: ProcessWebhookJob снят при уходе от старого
биллинга, SupplierCsvParser удалён 09.07 как мёртвый, каталог memory
уехал в claude-brain, а HANDOFF прогрева от 25.07 не существует ни
в одном коммите. Ссылки сняты, текст оставлен.
3 — неверное число шагов вверх: две точки вместо трёх.
3 — это вообще не ссылки, а примеры в тексте: http:// как образец мусорного
ввода, https://ваш-сайт.ru из подсказки формы. Обёрнуты в кавычки-код.
1 — адрес с приписанными номерами строк, которых проверятель не понимает.
1 — Россвязь. Сайт мёртв по-настоящему: сервер 194.226.91.2 отдаёт nginx-ову
404 на любой адрес, включая корень, а сертификат выписан не на это имя;
ведомство расформировано. Замену проверить не удалось — преемник
с этой машины не отвечает вовсе. Ссылка снята, адрес читается текстом.
Вопрос «где реестр нумерации живёт теперь» остаётся открытым.
Исключения проверятеля не тронуты ни на строку. Ноль получен починкой, а не
глушением сторожа — иначе вся работа теряет смысл.
Отдельным прибором проверено, что видимый читателю текст не изменился ни в одной
из 81 правленой строки: правились только адреса ссылок. Разметка линтером чистая.
Вместе с починкой ложится промт смены, по которому работа делалась.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.
Сверх самого слияния:
- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.
Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса»
не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой:
в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета.
Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу
ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при
необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов.
Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом
не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт.
Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице
«Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления
и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов,
их клиент разбирает сам.
Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены.
1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту
под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам,
500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание
в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17:
право на запись плюс нумератор. В плане про это было написано прямым текстом,
я прошёл мимо. Тесты поймать не могли: ходят суперпользователем.
2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана
как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана
убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» —
это норма, так и есть живьём.
3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже
когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы
ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит
теперь сам джоб: показываем один раз, пока сказать нечего.
4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите.
В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её
больше не берёт. Сценарий существует только при частичном отказе — тест переписан
на два объявления и теперь вырезание защиты его роняет.
Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74,
мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое
объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране
кабинета, и добыть её может лишь тот, у кого есть глаза.
Как теперь работает:
опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению;
робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу,
раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой
вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту
письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо,
в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора».
Четыре ловушки, пойманные по дороге и проверенные вырезанием:
1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит
по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы
в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16.
2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок
креативов не нужен, значит при выключенном рубильнике она получила бы задание,
и робот пошёл бы в живой кабинет.
3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота
обход снова клал бы «причину выясняем» поверх настоящей причины.
4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ
и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это
в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя
транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен
на сам механизм: обычным тестом это не ловится, они ходят суперпользователем.
Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс
внутри открытой транзакции.
Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.
Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.
Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.
Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.
Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбор клиентского модуля «Реклама в Телеграме по своей базе» на дыры
(4 параллельных код-ревью + личная перепроверка) и план их закрытия.
- findings: 14 находок, сгруппированы по серьёзности; главное — вся денежная
часть латентна под песочницей, но капканы (залипшая бронь, минимум 367 после
freeze, потерянный внешний id, зависшие статусы) сработают при go-live.
- spec: решения владельца (бронь→возврат/списание по факту; «своё имя» и
авторассылка доделываем; начинаем с безопасности), границы, приёмка по областям.
- plan: 5 этапов в порядке 1→2→5→3→4, каждая задача в TDD; этап 3 ждёт живого
отказа МТС (Part B).
Ревизия 27.07 (разбор самих спеки/плана на дыры):
- билинг-модель МТС («от X ₽» = нижняя граница, трата по показам) —
выяснить ДО реализации списания (предусловие этапа 2, задача 2.0);
- внешний id кампании МТС сохранять РАНО (робот плодит реальные черновики уже
на шаге аудитории) — иначе осиротевшие черновики и слепой рефанд;
- уборка брошенных черновиков (сегодня чистили руками) — узаконить (задача 3.1b);
- уборщик зависших консервативен: при известном mts_campaign_id не рефандит вслепую;
- оговорка: тест идемпотентности слабый (dispatchSync последователен).
Только документы (docs/superpowers/*). Кода не трогает.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Модуль-близнец СМС-рассылки, но через робота-в-браузере (у МТС нет API).
Спека: цель, что берём из СМС, ключевое отличие (нет провода → робот, пачки ≥367,
реальные деньги, модерация), клиентский экран, два режима (ручной + авто с лимитом
на объявление), обратная связь по отказу, что требует живой разведки, текст для клиента.
План: 6 сессий по ≤250–300k токенов с логичными остановками под /compact, каждая
оставляет код рабочим; Сессия 5 (отказ) гейтится живой разведкой Сессии 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Копия клиентского Яндекс-показы (ветка feat/reklama-yandex-pokazy) для канала ВК:
общий рекламный кошелёк, зеркало guard аудитории с числом ВК (2000), программная
загрузка креатива (проверено живым API content/static.json → операторский шаг для ВК
убираем). Доступ к VK Ads API получен и проверен вживую, ключи на бою, канал под
рубильником services.vk_ads.enabled до go-live. Код не пишем — решение владельца.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Живая проверка боевого кабинета опровергла допущение, на котором стояла Часть C и
часть уже написанного ядра. Конструктор креативов Яндекса закрыт 01.06.2026 —
Яндекс пишет об этом сам плашкой в редакторе объявления, показы его креативов
остановлены с 30.06. Адаптивного креатива из одной картинки не существует: медийная
кампания устроена как объявление на каждый размер блока со своим креативом, что
подтверждает наша живая кампания прогрева из 15 объявлений.
Отдельно проверен read-only запрос creatives.get с боевым токеном: метод отдаёт наши
баннеры, тип у них HTML5_CREATIVE, имя у всех одинаковое «Новый креатив» — опознавать
по имени нельзя, — и приходит PreviewUrl, годный для показа клиенту.
Отсюда дизайн: робот становится грузчиком готовых файлов, а не оформителем креатива.
Номера портал берёт сам через creatives.get слепком «до и после» с сопоставлением по
ширине и высоте. Ядро переходит с одного креатива на кампанию на креатив у каждого
баннера. Решения владельца: файлы готовит клиент, робот сам опрашивает портал, живёт
на боевом сервере, вход заводится через экран и удалённый рабочий стол на localhost.
Дописаны три вещи, не проговорённые в обсуждении: правило частичной модерации —
кампания работает, если принято хотя бы одно объявление, разморозка только когда
отклонены все; минимум один файл и не более одного на размер; раздел честно
неизвестного вместо выдуманных ответов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спек по итогам brainstorming с владельцем. Бот-RPA на Playwright автоматизирует
полный цикл создания Telegram-кампании по своей базе телефонов в кабинете МТС
Маркетолог живой сессией браузера, т.к. публичного API для этого нет ни у кого
на рынке РФ. Запуск руками, алярм на почту, два режима черновик/боевой.
Модуль-витрина для клиентов — вне scope, отдельный будущий спек.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ф0-разведка (находки F0-9…F0-24 + 9 скриншотов), дизайн-спека
и пошаговый план (21 задача, 3 очереди) по UX-правкам рекламного
Яндекс-блока портала. Реализация — в этой ветке от main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Убран 'class' из образца реестра (сборка через match в makeSmsProvider,
config:cache-safe), цена канала по ключу '*', и честно расписана стоимость
подключения следующего оператора (файл-провайдер + запись + арm + синоним).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дизайн схемы «МТС-номер → МТС, остальные → СМС-центр» с минимумом работы
при подключении новых операторов: реестр каналов в конфиге, справочник
операторов (OperatorNormalizer), имя отправителя на каждый канал, резерв
выключателем (по умолчанию OFF). Схему БД не трогаем. Утверждено владельцем.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Каждый из 3 рекламных каналов (Яндекс/ВК/Телеграм) получает свои 11 сроков + days;
движок считает решение по каждому каналу отдельно (decideForPlatform, чистая функция);
сроки переезжают на sales_ad_audience_platforms; recalc сводит на фирму для backward-compat
читателей (СМС/карточки); sync-джобы включают номера по срокам своей площадки; заодно
чинится рассинхрон ВК-джоба (читал singleton вместо строки площадки). Состав/СМС/заезд
из поиска — Фаза 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Портальная часть (пункты 1,2,3,5,6,7): общий движок таблицы на v-data-table,
ниша+дата колонки и фильтры, Отметить все+счётчик, показ менеджера +
назначение на СМС, пагинация 10/25/50/100, пометки греётся/прогрет + значки
каналов на карточках. Пункт 4 (поиск клиентов) — следующим заходом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кусок A (к реализации): фундамент «фирма на площадке», таблица настроек
по площадкам (свой рубильник/пороги/синхронизация), три пункта меню
Яндекс/ВК/Телеграм; поведение прогрева не меняется, 177 номеров → «Яндекс».
Куски B (витрина: значки на карточках, колонки «Греются/Прогреты», история
эпизодов, СМС) и C (раздельные сроки) — дорожная карта. Плюс справочник 11 сроков.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Начальник отдела продаж отмечает фирмы прогрева галочками, пишет текст,
видит цену ДО отправки и журнал после. Отправки СМС в проекте не было
вообще — ни у СМС-центра (только баланс и HLR), ни у МТС (только файл).
Что сделано:
- разъём провайдера SmsProvider: новый оператор подключается одним файлом
- заглушка FakeSmsProvider — модуль работает и проверяется ДО согласования
имени отправителя у операторов (это недели), иначе разработку не закончить
- маршрутизация по оператору: билайновский номер уходит через Билайн за
4,75 ₽, прочие через МТС — без ручного выбора канала
- стоп-лист: кто отписался, тому не шлём никогда, проверка перед списанием
- отбор получателей с шестью причинами пропуска, все ДО траты денег
- списание скопировано с AutopodborChargeService; пока клиента нет
(tenant_id пуст) с баланса не берём — платим оператору напрямую
- оператор номера доезжает из «Поиска клиентов» в прогрев (был известен
и оплачен ДаДате, но терялся при передаче)
Мультиклиентность в костях: колонка tenant_id во всех четырёх таблицах
СМС с первого дня, NULL = «Лидерра сама». Клиент добавляется строкой,
а не переделкой модуля.
Найдено и закрыто при исполнении:
- замок от двойного списания стоял не на том соединении: кампания на
pgsql_supplier, деньги на pgsql, lockForUpdate по кампании отпускался
сразу. На бою два запуска списали бы дважды, обрыв — оставил бы пометку
«оплачено» при неушедших деньгах. Источник правды перенесён в
balance_transactions под замок по тенанту. Доказано тестом: старый код
списывал 700 вместо 850
- приём в портал требовал phones строкой по regex — словарь с оператором
получал 422, в базу не доезжало ничего. Тесты были зелёные, потому что
звали сервис МИМО контроллера. Проверка теперь принимает оба формата,
тест идёт через HTTP
- телефоны директоров в contacts остаются строками (договор
SalesProspectController), словари — только в верхнем phones
Заодно вылечена мигающая поломка 48 тестов доставки лидов: помощник
createRoutingSnapshotFromProject клал снимок на сегодня, а LeadRouter
после 21:00 МСК ищет завтрашний (вечерний переворот заливки) — вечерние
прогоны падали, дневные проходили. Помощник теперь зеркалит активную дату
роутера в любой час. Регрессия SnapshotHelperTimeOfDayTest замораживает
22:00 МСК и пинит инвариант. Боевой LeadRouter не тронут.
Тесты: 84 бэкенд + фронт по экрану + 397 поисковика, весь набор 3226
зелёный, статанализ чист. Все защиты проверены вырезанием.
План: docs/superpowers/plans/2026-07-20-sms-progrev-modul.md
Спека: docs/superpowers/specs/2026-07-20-sms-progrev-modul-design.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Постановка владельца 19.07.2026: на экране «Последние списки» нужен фильтр под
выбранную нишу, галочки на списках и групповые действия (удалить / собрать телефоны /
прогреть / отдать менеджеру) — все только по горячим фирмам.
Решения владельца зафиксированы: горячие = 70+, уже собранных и отданных пропускаем
и сообщаем сколько, перед платным действием и удалением спрашиваем с числом,
Телеграм и СМС — заглушки до готовности модулей.
Найдено при проектировании: прогрев берёт только фирмы с собранным мобильным, значит
порядок работы — телефоны → прогрев → менеджер; после прогрева на нашей стороне не
оставалось следа (добавляем пометку площадки); показ 1000 списков требует переноса
счётчиков в колонки, иначе главная разбирает всю базу на каждое открытие.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Признак channels у фирмы, три кнопки и колонка «Где греем» на экране
начальника, заливка в ВК с тремя статусами (нет доступа / ждёт объёма /
работает). Порог автомата у ВК — 2000 номеров, это его же документация.
Зафиксированы находки 19.07: охват списка в ВК меньше сотни при 111
загруженных номерах, и что «111 из 111» у Яндекса — проверка формата,
а не совпадений. Открытый вопрос про перезапись списка в API ВК помечен
явно — уточняется при получении доступа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Коммит d2c2ec43 от 19.07 оторвался от main (dangling, ни в одной ветке):
чистка ПДн и YandexAudienceClient в основную ветку так и не попали.
Номера заменены на фиктивные 7999000000X, клиент и его тесты внесены заново.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец переформулировал: сначала ГРЕЕМ рекламой, потом отдаём менеджеру.
Прогрев — отдельный этап ДО воронки, карточка рождается при назначении менеджера.
Правила рекламы теперь следуют за стадией карточки, все сроки настраиваются:
прогрев 3 дня, новые 3, взят в работу 14, переговоры до даты созвона
(просрочена → +3 → стоп), недозвон 7, отказ 3, регистрация/тестирование 30,
пополнил баланс и пользователь — стоп.
Проверено на боевых данных: у «Взят в работу» и «Регистрация/Тестирование»
даты созвона нет и не будет — им обязателен свой срок, иначе реклама вечная.
У «Переговоров» дата обязательна в коде, логика владельца закрывает полностью.
HANDOFF содержит промпт для следующей сессии, все технические грабли,
состояние Яндекса (сегмент 58029600, письмо в поддержку отправлено)
и запись об инциденте с ПДн в истории коммитов.
Все телефоны в документах замаскированы.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(sudya): проект сводного судьи диалогов Александры
Собран из трёх существующих семейств судей: голосовые (NISQA/дуэли/glitch),
чатовые (AnswerGuard + слепые судьи-агенты), нормативные (llm-judge/reviewer-agent/
judge-evaluator). Ни одно из них не судило ГЛАВНОЕ — продала ли она; для номера
менеджера по продажам на лендинге это ключевой спектр.
Устройство: приборы (приговор) → коллегия узких судей-ролей (находки) →
судья над судьями (доля промахов). 36 признаков в шести спектрах.
Протокол честности, каждое правило оплачено ошибкой: слепота, нейтральный вопрос,
живой якорь в партии (забраковал якорь → партия недействительна), роли вместо
одной большой рубрики, находка без цитаты источника не считается.
Границы названы явно: звук живых клиентов не пишем (ПДн — отдельная работа),
судья не правит Александру сам, владелец остаётся высшей инстанцией.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
fix(sales): строгое разделение двух ролей начальника — экраны «мои» больше не показывают отдел
Начальник продаёт сам и руководит отделом. Правило «начальник видит всё»
применялось к ЧЕЛОВЕКУ, а не к экрану, поэтому разделы меню врали:
- «Потенциальные клиенты» показывали все карточки отдела — точную копию
«Воронки отдела» (это владелец и заметил);
- «Мои клиенты» и «Сводка» — всех клиентов отдела;
- «Привязать клиента» — очередь заявок отдела, причём в ЧУЖОМ формате
({pending, history} вместо {data}), форма получала не те данные.
Новое правило: роли разделяются по ЭКРАНУ. Любой запрос по умолчанию отдаёт
только личное — включая начальника. Весь отдел выдаётся только по явному
?scope=department, и просят его только экраны раздела НАЧАЛЬНИК. Менеджеру
параметр ничего не даёт: проверка по роли, не по параметру.
ownedTenantIds теперь ВСЕГДА личные привязки (тип сузился с ?array до array),
добавлен visibleTenantIds для области видимости запроса.
Правом начальника осталось открыть ЛЮБУЮ карточку — кандидата и клиента:
иначе из «Воронки отдела» не открылась бы карточка чужого менеджера.
Ограничены только списки, не доступ к записи.
Следствие: у начальника сейчас 0 своих кандидатов и клиентов, поэтому его
личные экраны станут пустыми — это правильно, а не поломка.
Pest 272/272, Vitest зелёный, vue-tsc чист, Larastan без своих ошибок.
Спека — §23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): кабинет начальника — единый период (без 500) + пополнения баланса
Экраны начальника отстали от вчерашних правок кабинета менеджера (§16–§21).
1. Живой баг: «Произвольный» период до выбора дат ронял запрос в 500 на пяти
точках из шести (сводка отдела, доход, результативность, выплаты, тарифы).
Разбор периода вынесен в трейт ResolvesSalesPeriod: 422 вместо падения,
период по умолчанию d30 вместо this — как показывает сам PeriodPicker.
2. Пополнения баланса (topup_rub) добавлены в dashboard/overview (kpi +
строки менеджеров) и managers/performance. На экранах: плашка «Пополнили
баланс» в сводке отдела и колонка «Пополнили ₽» в обеих таблицах
результативности. Из подписей убрано «(мес)» — период больше не месяц.
3. «Воронка отдела» правок не потребовала: она рендерит те же доску и карточку,
что экран менеджера, а карточка сама грузит журнал и сохраняет контакты.
Тесты: SalesPeriodRequestTest проходит по всем шести точкам сразу.
Pest 263/263, Vitest 1420/1420, vue-tsc чист.
Спека — §22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): периоды сегодня/вчера/7/30 дней календарём, пополнения за период, скролл доски наверх
Замечания владельца по сводке и доске.
ПЕРИОДЫ:
- Резолвер понимает today/yesterday/d7/d30; «7 дней» = сегодня и 6 предыдущих.
Месячные this/prev/prev2 сервер принимает по-прежнему.
- По умолчанию 30 дней.
- ПОЧИНЕНО: выбор «Произвольный» без дат ронял запрос (500). Теперь понятный 422,
а на фронте период применяется только когда отмечены ОБЕ даты.
- Произвольный выбирается календарём-диапазоном, не руками; порядок дат неважен.
ПОПОЛНЕНИЯ ЗА ПЕРИОД (переиспользован готовый topupsRub):
- Сводка: плашка «Пополнили баланс» рядом с «Σ баланс».
- Мои клиенты: колонка «Пополнил».
Заодно даёт число, которое видимо меняется при смене периода.
ДОСКА: горизонтальная полоса прокрутки поднята НАД колонками (колонки высокие,
системная полоса уезжала за экран); синхронизация в обе стороны, ResizeObserver
на приезжающие карточки.
Спека §21 (включая §21.4 — как по коду заполняется «Требуют внимания»).
Гейты: Pest 257/257 sales+unit, Vitest 1417, vue-tsc чисто, Larastan 0 в своих. TDD.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): результат разговора и его содержание — одной записью в журнале
Владелец: «объедини в одну запись: договорились… сверху, внизу текст».
Было две записи подряд — в ленте читалось как два события, хотя разговор один.
- БД v8.75: sales_prospect_notes.title VARCHAR(500) NULL — что решили.
- Одна запись kind=note: title = «Договорились на созвон 20.07.2026 17:35»,
body = краткое содержание. Есть результат без содержания — как раньше,
одна запись kind=stage без заголовка. Ручная заметка — без заголовка.
- В ленте заголовок строкой сверху, под ним текст.
- Старые парные записи задним числом не сливаем — историю не переписываем.
Спека §20.2. Гейты: Pest 240/240 sales, Vitest 64/64 воронка, vue-tsc чисто,
Larastan 0 в своих файлах. TDD RED→GREEN.
🪤 Тесты DOM для v-dialog: контент уезжает телепортом в body — искать через
document.body.querySelector, а не wrapper.find.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
fix(sales): одна кнопка «Сохранить» в карточке — контакты больше не отдельно
Владелец: «исправь сохранения контакта убери, все на кнопку Сохранить заведи».
Две кнопки в одном окне путали: можно было записать результат и уйти, потеряв
только что набранные телефоны.
- Кнопка «Сохранить контакты» и подпись «сохранено» убраны.
- «Сохранить» теперь: сохраняет контакты → если выбран результат, проверяет
и отправляет его → если результат не выбран, просто закрывает окно.
- Контакты не сохранились (нет сети) — результат НЕ отправляем и показываем
ошибку, иначе стадия уехала бы, а телефоны потерялись.
- submit() стал асинхронным, кнопка показывает загрузку.
Спека §20.1. Гейты: Vitest 62/62 воронка (4 новых теста), vue-tsc чисто. TDD.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): содержание разговора — вместе с результатом; недозвону дата перезвона
Владелец после первого показа карточки: «куда звонить убери; договорились на
созвон — ниже краткое содержание, и оно копится в Историю разговоров, свежие
в начало; недозвон — поставь дату следующего перезвона».
- Блок «Куда звонить» убран: дублировал строку «Телефон общий» из данных фирмы.
- «Краткое содержание разговора» переехало последним полем в блок «Результат
разговора» (было отдельное поле с кнопкой слева — два места для одного
действия). Уходит параметром summary вместе с результатом, ложится в журнал
отдельной записью kind=note. Пустое — не пишем.
- Порядок внутри одного сохранения: сначала автозапись про этап, затем
содержание → у него больший id и в ленте оно оказывается НАД этапом.
- Недозвон получил необязательное поле «Когда перезвонить»: пишется в
next_call_at (видно на плитке) и дописывается в автозапись журнала.
Спека §20. Гейты: Pest 236/236 sales, Vitest 59/59 воронка, vue-tsc чисто,
Larastan 0 в своих файлах. TDD RED→GREEN.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): журнал разговоров по кандидату + переделка обоих окон воронки
Владелец: «краткое содержание разговора писать, чтобы не потерять историю»
и «переделай обоих дизайн, очень не практично, спроси у перплексити».
ЖУРНАЛ (БД v8.74, таблица sales_prospect_notes, append-only):
- kind=note — менеджер написал руками; kind=stage — автозапись о переезде
по стадии (взял в работу, созвон, недозвон, отказ, регистрация).
Повторное открытие карточки журнал не засоряет.
- GET/POST /api/sales/prospects/{id}/notes; лента свежими сверху, грузится
при открытии карточки, а не вместе с доской.
- PATCH /prospects/{id} БОЛЬШЕ НЕ принимает notes: он молча затирал прошлую
запись — ровно та потеря истории, ради которой журнал и появился.
Нашёл rls-reviewer, закрыто тестом. Старые notes перенесены в журнал.
ДИЗАЙН (по разбору Pipedrive/HubSpot/Salesforce через Perplexity):
- Карточка 1100px, две колонки. Слева «что за фирма» + история разговоров
с полем «о чём поговорили». Справа зона действия: «Куда звонить» (номер
крупно, ссылкой tel:), «Результат разговора» своим фоном, контактные лица.
Кнопки внизу окна. Пустая история объясняет, что делать.
- Форма создания разбита на разделы: Компания / Контактные лица / Заметка;
Юрлицо и Город в одну строку; обязательных полей по-прежнему два.
Спека §19. Гейты: Pest 231/231 sales, Vitest 60/60 воронка, vue-tsc чисто,
Larastan 0 в своих файлах, rls-reviewer PASS. TDD RED→GREEN.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): карточка кандидата — девять полей владельца + правка контактных лиц
Владелец задал список полей карточки и попросил дать менеджеру записывать
контактных лиц прямо в карточке: поиск отдаёт 5-7 ПРЕДПОЛАГАЕМЫХ номеров
директора, живой из них выясняется обзвоном; а если директор перенаправил
к маркетологу — менеджер фиксирует и его, людей может быть 2-3.
- Карточка показывает РОВНО девять полей в его порядке: Юрлицо, Сайт, ИНН фирмы,
Адрес, Телефон общий, Каналы, Бюджет, Директор, Тел. директора (предполагаемый).
Убраны Оценка, Достоверность, Реклама, Запросы в Директе, ОГРН, Статус юрлица,
Личный ИНН директора, Почта директора — данные остаются в payload.
- PATCH /api/sales/prospects/{id}/contacts — список заменяется целиком, пустой
стирает всех; права как у update() (менеджер свои, начальник любые); главный
телефон карточки из поиска не трогается.
- ProspectContactsEditor.vue + utils/prospectContacts.ts — один редактор контактов
на диалог создания и карточку; диалог создания переведён на него.
- Закрывает хвост §16: у 24 живых карточек из поиска контактных лиц не было,
теперь дозаполняются руками.
Спека §18. Гейты: Pest 223/223 sales, Vitest 196 файлов / 1397 тестов,
vue-tsc чисто, Larastan 0 в своих файлах. TDD RED→GREEN на каждом шаге.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
fix(sales): «Зарегистрировался» подаёт ЗАЯВКУ начальнику, а не привязывает клиента сразу
Владелец заметил: в отделе есть штатный порядок «через начальника» (экран «Заявки
на привязку»), а кнопка в карточке кандидата создавала SalesClientAssignment напрямую
и в истории заявок ничего не появлялось.
- registerProspect() теперь зовёт SalesAttachmentService::submit() от имени ВЛАДЕЛЬЦА
карточки. Свободен → заявка pending + письма менеджеру и начальникам; занят другим →
заявка с пометкой конфликта, чужая привязка не трогается; уже свой → заявки нет.
Привязка со снимком тарифа создаётся только при одобрении (handleApprove).
- Стадия карточки едет в «Зарегистрировался» СРАЗУ (решение владельца): регистрация —
факт, воронка показывает правду; «чей клиент и кому деньги» решает начальник.
Автожизнь не зависит от одобрения — джоб смотрит linked_tenant_id.
- Отменён прежний 422 «клиент уже закреплён за другим менеджером» — теперь это заявка.
- Подсказка под полем e-mail говорит про одобрение начальником.
Спека §17 (отменяет правило §6 «привязываем сразу»). Гейты: Pest 218/218 sales,
Vitest 38/38, Larastan 0 в своих файлах. TDD RED→GREEN.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
feat(sales): кандидат — ИНН обязателен, юрлицо отдельно от бренда, контактные лица
Замечание владельца по форме «Добавить кандидата»: телефонов бывает много, нужны
ФИО и должность человека, ИНН обязателен, бренд и юрлицо — разные вещи, юрлицо
подтягивать по ИНН через ДаData.
- БД v8.73: sales_prospects.legal_name VARCHAR(500) + contacts JSONB DEFAULT [].
phone остаётся «главным телефоном»; inn в БД по-прежнему nullable — карточки
из поиска и 24 живые строки могут быть без него.
- store(): ИНН обязателен + контрольная сумма ФНС; повтор ИНН у того же
менеджера — понятный 422 вместо 500 от уникального индекса; пустые контакты
и телефоны отсекаются; phone = первый телефон первого контакта.
- POST /api/sales/prospects/lookup-inn — юрлицо/город по ИНН через готовый шов
PartyLookup (тот же, что в «Реквизитах»). Ничего не сохраняет; ДаData молчит
или упала — заводим руками.
- Диалог создания: ИНН* с кнопкой «Найти», бренд*, юрлицо, блок контактных лиц
(+человек / +телефон). Карточка показывает юрлицо и контакты со ссылками tel:.
Спека §16, CHANGELOG_schema v8.73. Гейты: Pest 28/28, Vitest 38/38,
Larastan 0 в своих файлах, rls-reviewer PASS 4/4.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
docs(spec): дизайн — прокси автоподбора (Proxy.Market) на плашке «Внешние сервисы» Командного центра
Вариант А: живость проверяет виртуалка-рендер (у неё логин/пароль прокси),
портал показывает срок «до 04.08.2026» из конфига (владелец обновляет руками
при продлении). Схему БД не трогаем. Новая ручка /proxy-check на рендере +
ProxyMarketProbe + строка proxy_market в реестре/плашке.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
Фиксирую после выката 16.07: живое состояние 12 сервисов, мины (DELETE-грант/EXA-proxy/
живость<500/serial-only regress), и НАХОДКИ владельца — xfetch реально в Sales-finder
(/opt/sales-finder, не портал), EXA-тоннель на render-VM автоподбора, Sales-finder тянет
Keyso+DaData вне присмотра. §6 — готовый промпт для следующей сессии.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>