Commit Graph

1587 Commits

Author SHA1 Message Date
Дмитрий 977d54bc53 feat(воронка продаж): база под стадии Тестирование ручное и Выслано КП
Стадии manual_testing и kp_sent в CHECK + 4 nullable-колонки под их поля.
Откат и повторный накат проверены на своей тестовой базе измерением колонок.

Статанализ пропущен с разрешения владельца: 287 замечаний было и до правки,
все — известный ложный класс Pest, моих файлов среди них нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:45:10 +03:00
Дмитрий 075fdfef6f docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП 2026-07-31 10:17:13 +03:00
Дмитрий 6ddd2b6717 docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:17:03 +03:00
Дмитрий 6f469fb13d feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.

Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
  ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
  фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
  Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
  сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».

Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.

Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 06:26:07 +03:00
Дмитрий 7a2b052f0d feat(телеграм-реклама): связка Laravel → робот mode:resubmit — пересдача чинит ту же кампанию
Хвост «связка» из STATE. Раньше пересдача отклонённой кампании создавала в кабинете
МТС НОВУЮ кампанию (RunTelegramCampaignJob, очищала mts_campaign_id). Теперь портал
поручает роботу ЧИНИТЬ ту же кампанию: робот заходит в неё через «Исправить» по
mts_campaign_id, вносит правки и переотправляет на модерацию «без оплаты» (0 ₽).
Робот-режим mode:'resubmit' уже проверен живьём (коммит d50a93e5). Выбор поведения
(«чинить ту же», не «создавать новую») подтверждён владельцем.

Что сделано:
- RobotResult: поле resubmitted (bool) + разбор из JSON робота.
- TelegramRobotRunner::taskPayload: пробрасывает submitMode (draft|live для resubmit).
- ResubmitTelegramCampaignJob (новый): RLS через tenantTx (SET LOCAL current_tenant_id),
  идемпотентность по queued, денежно-статусная логика F5 как в RunTelegramCampaignJob.
  Успех: бой (resubmitted) → moderating, песочница → draft_ready. Отказ с mts_campaign_id
  → needs_review без возврата брони.
- Контроллер resubmit: mts_campaign_id СОХРАНЯЕТСЯ (не очищаем), guard на пустой id → 422,
  dispatch ResubmitTelegramCampaignJob.

Код-ревью (субагент) поймало Important-баг, исправлено:
- I-1: failed() был скопирован из эталона, где «queued ⇒ нет mts_campaign_id». У пересдачи
  queued ВСЕГДА с id → при перманентном сбое в queued срабатывала ветка hasMtsId →
  queued→needs_review (перехода НЕТ в TRANSITIONS) → кампания зависала в queued с
  замороженной бронью, уборщик её не метёт. Фикс: queued → failed + release (робот кабинет
  не трогал — Фаза A не закоммитила running); running — прежняя осторожная логика.
- M-1: успех в бою с resubmitted=false (аномалия контракта) давал терминальный draft_ready
  с зависшей бронью → needs_review (бронь под ручную сверку).

Приёмка: ResubmitJobTest + ResubmitTest вместе 15/15 (51 проверка); pint/phpstan(0)/
deptrac(0) чисто; схема БД не менялась. Детали и приёмочный лист:
docs/superpowers/2026-07-28-resubmit-svyazka-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:16:48 +03:00
Дмитрий d50a93e55c feat(телеграм-робот): боевой режим пересдачи отклонённой кампании (mode:resubmit) — проверено живьём
Хвост №1 из STATE ревью-правок: оформили доказанный в A2 путь пересдачи в
постоянный режим робота mode:'resubmit'. Робот берёт ОТКЛОНЁННУЮ кампанию, входит
в её редактор через «Исправить», вносит исправления и повторно отправляет на
модерацию БЕЗ ОПЛАТЫ (0 ₽ — деньги не списываются).

Что нового:
- parseResubmitTask (task.js): своё задание пересдачи — campaignId + submitMode
  (draft|live, без дефолта, чтобы live не случился сам) + правки на выбор
  (moderatorFile и/или adText/buttonUrl/ordCategory). Нужна хоть одна правка —
  иначе тот же контент снова отклонят. Номера/бюджет не нужны (уже у кампании).
- openResubmitEditor (cabinet.js): нативный клик «Исправить» в строке кампании по
  id → ждём редактор /telegram-a2p/{id}/message (в чужую кампанию не лезем).
- submitWithoutPayment (cabinet.js): на /payment жмём ТОЛЬКО «Отправить на
  модерацию без оплаты»; денежные кнопки («Списать…»/«Оплатить») — двойная защита
  через isForbiddenButtonText, не жмём никогда.
- editResubmitFields + вынос fillAd в хелперы (fillAdText/fillAdLink/
  selectOrdCategory/fillAdMedia): пересдача правит только заданные поля тем же
  проверенным кодом (DRY, поведение fillAd не изменилось).
- runResubmit (runner.js) + ветка mode:'resubmit' в bin/run.js (до parseTask, как
  read-status). draft — предохранитель (до /confirmation, не шлём); live — отправка
  без оплаты.

Живая проверка на реальных отклонённых кампаниях (28.07.2026):
- DRAFT (2231132, правка текста + документ): вошёл «Исправить» → правка → загрузка
  документа → /confirmation → НЕ отправил (resubmitted:false, stoppedAt:confirmation).
- LIVE (2231134, правка текст+ссылка+ОРД, без документа): полный цикл → «без оплаты»
  (0 ₽) → resubmitted:true; read-status подтвердил moderating. Баланс не тронут.

Робот npm test 110/110 (было 94, +16). Приёмочный лист и живые результаты:
docs/superpowers/2026-07-28-robot-resubmit-mode-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:43:58 +03:00
Дмитрий 05c22b3364 feat(инструменты): #90 grilling — допрос по готовому решению + нормативная синхронизация
Зарегистрирован вендоренный скил grilling из mattpocock/skills, MIT,
skills/productivity/grilling. Установлен user-level ~/.claude/skills/grilling/
— вне репозитория, единственная копия: проектная удалена во избежание
задвоения.

Роль — безжалостный допрос по УЖЕ имеющемуся плану или решению: обход дерева
развилок ветка за веткой, по одному вопросу за раз, к каждому вопросу свой
рекомендуемый ответ, факты ищутся самостоятельно, работа не начинается до
явного подтверждения заказчика.

Надстройка проекта поверх немодифицированного тела апстрима, отделена
заголовком:
- протокол docs/grilling/ГГГГ-ММ-ДД-тема.md — разделы Решили / Отрезали и
  почему / Осталось открытым; пишется по ходу, перечитывается после компакта
  контекста. Закрывает класс «договорённость сгорела при компакте»
- порядок обхода «сначала необратимое» — деньги, схема БД, что уходит клиенту
- критерий остановки: пустой фронт развилок с объявлением вслух
- «слушай, не защищай»

Граница ADR-021 GR1 с #55 discovery-interview — разрез по наличию решения:
grilling куёт решение, которое у заказчика уже есть, поэтому наводящий
рекомендуемый ответ обязателен; discovery-interview вскрывает проблему, когда
решения ещё нет, и там наводящие ответы запрещены. GR2 — граница с
brainstorming #19. GR3 — вендоринг без модификации апстрима.

Реестр: узел #90 + контракт, связка L1 между brainstorming и writing-plans,
классификация planning вес 0.8 — ниже первичных решателей. Автотаблицы
перегенерированы через registry-render, карта цепочек дополнена.

Нормативная синхронизация квинтета:
- Tooling Прил. Н v2.26 — новый §4.63, счётчик 87→88 и 107→108, off-phase
  +57→+58, футер
- Pravila v1.45 — §13.2 новый абзац, запись в истории версий
- PSR_v1 v3.25 — R10.1 Блок 1 note, запись в истории версий
- CLAUDE.md v2.49 — через плагин claude-md-management, §0 версии квинтета,
  §3.4 +#90, §9 запись

Попутно снят застарелый рассинхрон шапки Tooling: числа 84/104 остались от
майской версии, приведены к 88/108, дописаны research-tooling и узлы #87–#90.

Проверки: registry-render --check зелёный на обоих файлах,
observer-chain-map-checker OK 17 chains in sync, загрузчик видит 90 узлов,
markdownlint 0 ошибок, cspell 0 ошибок.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:16:25 +03:00
Дмитрий 39407df04d docs(телеграм-реклама): состояние ревью-правок + результаты живой проверки A1/A2
Фиксирует итог сессии 28.07: приёмочный лист 9 правок F1–F9, деплой-чеклист srv_bypass,
результаты живой проверки робота — A1 (чтение вердикта 2231134) и A2 (пересдача с документом,
0 руб), рецепт пересдачи для будущего robot-mode. Только документ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:57:15 +03:00
Дмитрий a0b7f4adfa fix(телеграм-робот): чтение вердикта модерации из списка кабинета — починка локатора по живому прогону
Живая проверка A1 (28.07.2026) на реальном отказе кампании 2231134 вскрыла два бага в
readModerationStatus, из-за которых боевой код не находил строку кампании — на юнит-тестах
было зелено, а кабинет показал обратное:

1. Паттерн ссылки: в списке кампаний ссылка = /cabinet/campaigns/telegram/{id}, а код искал
   /telegram-a2p/{id} — это адрес детальной/визард-страницы. Matcher расширен на оба варианта
   через campaignHrefRe/hrefMatchesCampaignId плюс юнит-тест.
2. Глубина подъёма по DOM: строка списка — грид из div, не tr/li; контейнер со статусом ряда
   на ~8 уровней выше ссылки. Предел подъёма поднят с 6 до 10; возврат на первом предке со
   статусом — выше склеиваются две кампании.

Итог живого прогона: робот вернул rejected плюс полный текст 5 пунктов модерации из слайд-модалки
Причины. Метка FLOW-CONFIRM в cabinet.js снята. Робот npm test 94/94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:46:00 +03:00
Дмитрий 68fe632cca fix(телеграм-реклама): правки по сводному код-ревью ветки — деньги, статус-машина, робот, RLS
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное.
Поведение в песочнице не меняется; правки готовят ветку к боевому включению.

F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по
  4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа
  23505 в боевом режиме.
F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200;
  длинное значение даёт ошибку поля, а не замаскированный 422 от БД.
F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и
  считает дневной лимит под lockForUpdate строки правила.
F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс
  предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем.
F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id
  значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони.
F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке
  domClickButton, finalize целит только кнопку отправки на модерацию.
F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment;
  не дошли до /payment → падаем громко.
F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком.
F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе
  кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в
  000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql.

Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:07:53 +03:00
Дмитрий 18135b47b4 feat(телеграм-реклама): Этап 3.5/3.6 + 4.3 — чтение вердикта, пересдача, уведомление об одобрении
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.

## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
  Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
  «На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
  Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
  read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
  в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
  причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
  {ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
  🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
  живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
  подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.

## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
  после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
  данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
  правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
  (.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
  status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
  rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
  + freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
  по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
  RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
  для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
  не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).

## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
  юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.

TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.

Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 11:29:23 +03:00
Дмитрий e9bec6d11e fix(портал продаж): ИНН при заведении кандидата больше не обязателен
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.

Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.

Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:39:19 +03:00
Дмитрий 5211048bce feat(телеграм-реклама): Этап 2 закрыт — робот несёт фактическую стоимость (2.2), билинг МТС подтверждён
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в
находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ
(резерв → списание по факту показов → возврат остатка, экран /payment).
Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно
сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения).
Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2.

- 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка,
  чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать
  её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле
  позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял.
- 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость
  кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая
  неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа →
  null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена
  (селектор строки стоимости подтвердим живьём в Этапе 3).
- Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план
  Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт).

TDD, робот замокан, тесты на liderra_testing.
Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest
(потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0,
pint чисто, deptrac 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 07:52:56 +03:00
Дмитрий 98388ccf26 docs(телеграм-реклама): аудит дыр модуля + спека и план закрытия (5 этапов)
Разбор клиентского модуля «Реклама в Телеграме по своей базе» на дыры
(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>
2026-07-27 20:56:29 +03:00
Дмитрий 5b04d751c0 feat(телеграм-модуль): авто-режим, своё имя + помесячная оплата, админ-тарифы
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.

4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
  (audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
  по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
  и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
  Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).

4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
  🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
  очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
  в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.

4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
  disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
  free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
  кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
  Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
  в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).

4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
  ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).

Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:41:33 +03:00
Дмитрий 8053c14665 docs(телеграм-модуль): дизайн-спека + план стройки клиентского модуля «Реклама в Телеграме по своей базе»
Модуль-близнец СМС-рассылки, но через робота-в-браузере (у МТС нет API).
Спека: цель, что берём из СМС, ключевое отличие (нет провода → робот, пачки ≥367,
реальные деньги, модерация), клиентский экран, два режима (ручной + авто с лимитом
на объявление), обратная связь по отказу, что требует живой разведки, текст для клиента.
План: 6 сессий по ≤250–300k токенов с логичными остановками под /compact, каждая
оставляет код рабочим; Сессия 5 (отказ) гейтится живой разведкой Сессии 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:26:57 +03:00
Дмитрий fafa15d8e1 docs(реклама вк): дизайн-спека — VK Реклама на клиентском портале как копия Яндекс-показы
Копия клиентского Яндекс-показы (ветка 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>
2026-07-27 12:32:59 +03:00
Дмитрий 741d1697c5 feat(телеграм-бот): бот-автоматизатор кампаний МТС Маркетолог — cabinet/runner/CLI/keepalive (задачи 10–16)
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
  подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
  верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
  (fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
  профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
  падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
  (разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
  снимаются на первом реальном черновике после разового логина владельца.

Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:43:20 +03:00
Дмитрий 51aac64c8a docs(телеграм-бот): разметка подтверждена живым прогоном до шага «Объявление»
Прогон в черновике на реальной базе (1740 номеров): Получатели 1709, МТС 713,
Не МТС 996, стоимость показов 478,08 ₽ — цепочка «загрузка базы → счётчик → цена»
работает. Шаг «Объявление» размечен полностью (текст/заголовок/ссылка/медиа +
обязательный ОРД-блок: категория downshift, название, «Я — посредник»). Добавлены
грабли автоматизации (coach-mark overlay, downshift, force-click). Шаги
«Стоимость/Подтверждение» — доразметить на первом реальном черновике бота.
Черновик удалён, баланс 5010 ₽ цел, временные ПДн стёрты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 23:34:48 +03:00
Дмитрий f747b2fc74 feat(телеграм-бот): персистентный браузер и проверка живости входа
Задачи 8–9 плана (код, проверка против кабинета — при логине владельца в
профиль бота):
- browser.js — launchPersistentContext(profileDir), humanPause по темпу
- session.js — isLoggedIn по стабильному пункту меню «Рассылки и звонки»
  (селектор подтверждён живьём при разведке)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:49:22 +03:00
Дмитрий a1492400a0 feat(телеграм-бот): тестируемое ядро — config, task, phones, budget, mailer
Задачи 3–7 плана (TDD, 14/14 зелёные):
- config.js — загрузка/валидация настроек окружения
- task.js — парсинг и валидация задания кампании (режимы draft|live)
- phones.js — нормализация и дедуп файла номеров (7XXXXXXXXXX)
- budget.js — предохранитель потолка бюджета
- mailer.js — письма алярма/отчёта через SMTP (nodemailer)

gitleaks: каталог test/ бота добавлен в allowlist (синтетические
телефоны-фикстуры 345-67-89 / 111-22-33, не реальные ПДн).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:47:15 +03:00
Дмитрий 22d1150e26 chore(телеграм-бот): скелет Node+Playwright проекта
Задача 2 плана: package.json (Node ESM, node --test), .env.example (профиль
браузера, SMTP Unisender Go, потолок бюджета, человеческий темп), .gitignore.
Зависимости playwright/nodemailer/dotenv + chromium установлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:44:52 +03:00
Дмитрий ea1991e926 docs(телеграм-бот): разметка визарда кампании по своей базе в кабинете МТС
Задача 1 плана: пройден визард Telegram Ads → «Своя база клиентов» в живом
кабинете в режиме только-чтение. Зафиксированы 7 шагов степпера, селекторы,
требования к файлу номеров (TXT/CSV/XLS/XLSX, формат 79XXXXXXXXX, минимум 367
не-МТС), счётчик «МТС/Не МТС», путь удаления черновика. Черновик 2229823 убран,
баланс 5010₽ не изменился. cspell-words: добавлены CPM/CPF/MTC/ЕРИР/Таргеты/алярм.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:33:03 +03:00
Дмитрий 97566406b5 docs(телеграм-бот): план реализации бота-автоматизатора Telegram Ads
16 задач в 5 блоков по TDD с частыми коммитами. Блок 0 — разведка визарда
кабинета в режиме черновик перед браузерными задачами. Ядро парсинг/номера/
потолок/письма покрыто node --test; браузерная часть на Playwright опирается
на cabinet-flow.md. Исполнение через subagent-driven-development.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:14:30 +03:00
Дмитрий 9edd1f78a0 docs(телеграм-бот): дизайн бота-автоматизатора Telegram Ads через кабинет МТС
Спек по итогам brainstorming с владельцем. Бот-RPA на Playwright автоматизирует
полный цикл создания Telegram-кампании по своей базе телефонов в кабинете МТС
Маркетолог живой сессией браузера, т.к. публичного API для этого нет ни у кого
на рынке РФ. Запуск руками, алярм на почту, два режима черновик/боевой.
Модуль-витрина для клиентов — вне scope, отдельный будущий спек.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:02:58 +03:00
Дмитрий db2bf0113c docs(телефония): снимок состояния Dasha-ботов и Mango + разбор ремонта исходящей
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Хендофф по голосовым «Лена»: маршруты Asterisk (вход→приёмщик, обзвон через
obzvonbot+endpoint obzvon-lena), состояние Dasha (2 агента, лимит 1 разговор),
Mango (8 номеров, этикетка «Омега», МАВ), открытые задачи. Плюс перенесён с
Рабочего стола разбор ремонта исходящей связи Mango (403→180).
Полные телефоны/секреты не кладём (ПДн) — только состояние и команды.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 23:07:52 +03:00
Дмитрий ddabe61558 Merge commit 'a90d60a3' into sms-multi-operator
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
# Conflicts:
#	app/app/Jobs/SendSmsCampaignJob.php
2026-07-23 14:34:52 +03:00
Дмитрий 80c9ca7e67 feat(витрина B1,B2,B4,B5): летопись эпизодов прогрева + значки в воронке; уборка мёртвых скоупов
Кусок B «витрина прогрева» (спека 2026-07-21 §6), решение владельца — полная летопись:
- B1: таблица sales_ad_audience_warming_episodes + модель + WarmingEpisodeRecorder
  (open/close/record идемпотентно). Схема v8.85, бэкфилл из firm_channels(warming)
  и боевых СМС, guard по источнику. rls-reviewer OK 8/8.
- B2: «Греть»/«Убрать» на площадке открывают/закрывают эпизоды канала.
- B4: warmingByProspect считает значки из летописи (live/count вместо массива каналов),
  тип WarmingBadgeState в sales.ts.
- B5: единый компонент WarmingBadges.vue (идёт/грели раньше/×N) в канбане;
  осиротевший WarmingChannelIcons удалён.
Уборка: убраны мёртвые скоупы forYandex/Vk/Mts + их импорт + тест (боевых вызовов нет).
cspell: +5 пре-существующих слов CHANGELOG в словарь (apk/cvtjpq/hgq/sar/sca).

Проверено: Sales 427/427, composer stan 0, pint/prettier чисто, весь Vue-набор зелёный.
B3 (СМС→эпизод) — отдельным коммитом (СМС-блок правит и параллельная сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:54:09 +03:00
Дмитрий 44c94da676 docs(смс): спека §3.2 приведена к реализации (match-сборка, честная стоимость нового оператора)
Убран 'class' из образца реестра (сборка через match в makeSmsProvider,
config:cache-safe), цена канала по ключу '*', и честно расписана стоимость
подключения следующего оператора (файл-провайдер + запись + арm + синоним).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:21:26 +03:00
Дмитрий c88e81e803 docs(смс): план реализации — маршрутизация СМС по операторам (МТС + СМС-центр)
9 задач TDD: справочник операторов, каналы МТС(Exolve)/СМС-центр(smsc.ru),
реестр каналов в конфиге + сборка роутера, нормализация оператора,
имя отправителя под канал, резерв выключателем (off). Схему БД не трогаем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:36:56 +03:00
Дмитрий c333746927 docs(смс): спека — маршрутизация СМС по операторам (МТС + СМС-центр, задел под остальных)
Дизайн схемы «МТС-номер → МТС, остальные → СМС-центр» с минимумом работы
при подключении новых операторов: реестр каналов в конфиге, справочник
операторов (OperatorNormalizer), имя отправителя на каждый канал, резерв
выключателем (по умолчанию OFF). Схему БД не трогаем. Утверждено владельцем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:20:10 +03:00
Дмитрий 48b88737e7 feat(прогрев ф2 этап D): backend СМС-канала — свой список sms-firms + счётчик «Отправлено N» (+ план этапа D)
Фаза 2 Этап D, Task D1. Новый эндпоинт GET /api/sales/ad-audience/sms-firms
(только head) отдаёт фирмы со строкой firm_channels(channel='sms'). Хелпер
smsSentCounts считает по каждой фирме число боевых СМС (sales_sms_messages
status='sent') по её номерам; fake_sent (песочница) не считается. firmRow получил
поле sms_sent_count (0 если не слали), проброшено и в firms/allFirms. СМС строкой
всегда loaded, сроками не греется — только чтение sales_sms_messages для счётчика,
тарификация/провайдер/запись кампаний не тронуты. Bump baseline (actingAs-ложняк).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:28:43 +03:00
Дмитрий 6775f58003 feat(прогрев ф2 этап C): показ по каналам — firms/бейджи/числа из firm_channels (+ план этапа C)
Фаза 2 Этап C, Task C1. Вкладка канала показывает ТОЛЬКО свой состав: firms({platform})
фильтрует по наличию строки firm_channels(channel) (loaded или warming). Бейджи
warming_channels строятся из строк firm_channels (yandex/vk/mts по строкам, sms по
факту рассылки), а не из ch_*. Числа вкладки (in_ads/expiring_week/waiting_sync)
считаются по фирмам со строкой firm_channels(channel, status='warming') — loaded в
рекламе не участвует. Это чинит поломку после этапа B (заезд не пишет ch_* ⇒ старые
ch_*-подсчёты давали 0/пусто у новых фирм). channelColumn удалён (сирота).
allFirms/toggle/mtsFile/движок/заливки не тронуты. ch_* не удаляем (откат).
Тесты мигрированы. Bump baseline (actingAs-ложняки).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:38:15 +03:00
Дмитрий ce48d911d6 feat(прогрев ф2 этап B): заезд = только загрузка во все 4 канала (loaded), без автостарта
Фаза 2 Этап B, Task B1 (+ план этапа B). AdAudienceIntake::ingest больше не
запускает прогрев: грузит фирму во все 4 канала (yandex/vk/mts/sms) строками
firm_channels status='loaded' (firstOrCreate — не понижает уже греющийся канал).
ch_* на заезде не пишутся (источник членства — строки firm_channels, resolveChannels
удалён). Повторный заезд обновляет только снимок-поля и НЕ сбрасывает
warmup_started_at/ready_at/stopped_at/stop_reason (заезд ≠ рестарт прогрева).
Фирма без warming-строк ни в одну заливку не идёт ⇒ автостарта нет по построению.
Запуск прогрева — кнопкой «Греть» на портале (этап C). Тесты intake мигрированы
на новую семантику. Bump baseline (+6 postJson-ложняков).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:39:45 +03:00
Дмитрий 199af4f906 docs(прогрев): план Фазы 2 этап A — ядро (firm_channels, 9 задач TDD) 2026-07-22 13:58:26 +03:00
Дмитрий 4568612af6 docs(прогрев): Фаза 2 — уточнения владельца (свободный срок, СМС без warming, бэкфилл СМС только слали) 2026-07-22 13:49:06 +03:00
Дмитрий 15f198c49c docs(прогрев): спека Фазы 2 — раздельные каналы + заезд-загрузка (вся фаза) 2026-07-22 13:39:20 +03:00
Дмитрий 072a646230 docs(прогрев): план Фазы 1 — раздельные сроки по площадкам (11 задач, TDD)
Миграция сроков на площадки + бэкфилл, модель durations(), хелпер decideForPlatform
(чистое ядро), контроллер per-platform, recalc сводка, sync Яндекс/ВК по своим срокам
(+ВК на строку площадки), mtsFile, firmRow per-platform, фронт-текст, регресс. Выкат —
отдельно по спеке §5. Реализация — следующей сессией.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 08:32:18 +03:00
Дмитрий 587ad4173b docs(прогрев): спека Фазы 1 — раздельные сроки по площадкам (кусок C)
Каждый из 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>
2026-07-22 08:27:28 +03:00
Дмитрий fbe4701399 feat(прогрев): firmRow отдаёт нишу, дату запуска, менеджера и состояние прогрева
Task 1 плана «кусок B». firmRow() в SalesAdAudienceController (firms()/allFirms())
теперь отдаёт rubric, warmup_started_at (ISO8601), manager_id/manager_name (через
prospect.sales_user_id) и агрегаты warming_active/warming_channels (yandex/vk/mts/sms).
Заглушка smsWarmedFirmIds() — Task 3 наполнит связью с СМС-рассылкой.

Тест AdAudienceWarmingFieldsTest — TDD (failing → green), плюс regression-прогон
72 тестов AdAudience* и composer stan (0 ошибок, phpstan-baseline.neon дополнен
записью под Pest actingAs() в новом тестовом файле — по конвенции остальных тестов).
2026-07-21 15:53:10 +03:00
Дмитрий 3197e24bbb docs(прогрев): план реализации — 4 страницы, ниша/менеджер/пагинация/дата/карточки
10 задач по TDD: бэкенд (firmRow +ниша/дата/менеджер/состояние/каналы, маршрут
назначения для СМС, sms-канал, блок warming в проспектах), фронт (composable
useWarmingFirms + 4 ячейки, v-data-table на 4 экранах, фильтры, select-all,
карточки воронки), полный регресс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:41:49 +03:00
Дмитрий d11778a32b docs(прогрев): спека — 4 страницы, ниша, менеджер, постраничность, дата запуска, пометки на карточках
Портальная часть (пункты 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>
2026-07-21 15:34:55 +03:00
Дмитрий 14a6b801d7 docs(deploy): runbook выката «три площадки прогрева» (кусок A)
Предполёт GO, пошаговый план: миграция+бэкенд (redeploy.sh) → фронт
(deploy-build.sh, маркер обновлён), smoke-проверки, откат. Ждёт «эскейп».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:22:50 +03:00
Дмитрий 1078566795 docs(прогрев): план куска A — фундамент площадок + три страницы
9 задач через TDD: миграция таблицы настроек площадок + бэкфилл из state,
модель SalesAdAudiencePlatform, джоб Яндекса на строку площадки,
контроллер по {platform}, маршруты /warming/{platform}, фронт (API +
общая обёртка трёх страниц + меню), уборка старого экрана + схема v8.81,
регресс «поведение не изменилось» + предполёт.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:15:19 +03:00
Дмитрий ecbcea8eed docs(прогрев): дизайн — три площадки прогрева + витрина по порталу
Кусок A (к реализации): фундамент «фирма на площадке», таблица настроек
по площадкам (свой рубильник/пороги/синхронизация), три пункта меню
Яндекс/ВК/Телеграм; поведение прогрева не меняется, 177 номеров → «Яндекс».
Куски B (витрина: значки на карточках, колонки «Греются/Прогреты», история
эпизодов, СМС) и C (раздельные сроки) — дорожная карта. Плюс справочник 11 сроков.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:01:46 +03:00
Дмитрий 6e771636ef feat(смс): модуль «Прогрев СМС» с заделом под мультиклиентность
Начальник отдела продаж отмечает фирмы прогрева галочками, пишет текст,
видит цену ДО отправки и журнал после. Отправки СМС в проекте не было
вообще — ни у СМС-центра (только баланс и 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>
2026-07-21 06:35:43 +03:00
Дмитрий 3fd0d81e09 @
docs(александра): передача по стройке — лёгкий оператор и точило

Приняли архитектуру по замыслу владельца: наставник не даёт подсказку
на каждый ход, а точит промпт оператора в фоне. Замер: лёгкий промпт
2.60с против 3.14с, наставник успевает лишь в 35% ходов — итого 0.77с.

Точило ВЫБИРАЕТ блоки из 13, а не пишет текст: промпт собирается кодом,
поэтому дрейфа нет. Опасные темы выбираются кодом мгновенно, оттенки —
моделью в фоне, её не ждём никогда.

В файле: где что лежит, замки лаборатории, эталон боевого с возвратом,
состав 13 блоков и утверждённый ответ про источник заявок, пять
провалившихся попыток ускорения с числами, карта задержек, ловушки
и открытые вопросы за владельцем.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-20 19:17:01 +03:00
Дмитрий 8e687f99c1 feat: сквозной путь клиента от рекламного клика до Вебвизора
Путь человека рвался пополам: лендинг и кабинет считали разные счётчики,
а вход и регистрация не писались вообще. Почему люди бросают регистрацию,
узнать было нельзя.

Счётчик лендинга 110476275 становится счётчиком полного пути и грузится
на всех страницах кабинета. Прежний счётчик кабинета 110494416 не тронут,
его история цела, на страницах кабинета работают оба.

Что сделано:
- загрузчик Метрики поднимает счётчики по области: public или app
- белый список раскладок: новая раскладка Метрику НЕ получает по умолчанию
- формы входа и регистрации закрыты от записи классом ym-hide-content
- метка utm не теряется при заходе сразу в кабинет минуя лендинг

Найдено при исполнении и закрыто:
- почта выводится в заголовке ДВУХ экранов, то есть вне формы: класс на форме
  её не накрывал, утекла бы в записи открытым текстом. Замаскирована точечно
- Метрика, единожды запустившись, пишет дальше сама и роутером не выключается.
  Админ входит через общий /login и идёт в админку к чужим телефонам.
  Корни админки и портала продаж закрыты ym-hide-content
- ключ metrika в config/services.php был объявлен ДВАЖДЫ: раздвоил его мой
  же merge 42e907c8 от 14.07. PHP молча берёт последний, правка первого блока
  не дала бы ничего и не выругалась. Дубль вычищен, прочие конфиги проверены

В кабинете Метрики включена галочка «включая поддомены»: счётчик принимал
данные только с liderra.ru и молча выбрасывал бы всё из кабинета.

Заодно погашен долг по статанализу: 63 ошибки держали коммит. Все до одной
в тестах, в боевом коде ноль. Природа ложная — анализатор не понимает
устройство Pest и ругается на обычный вызов внутри теста. Их гасят списком
игнора, а список пересобирали 18.07, тогда как тесты добавлялись 19-20.07,
в том числе мои по рекламной аудитории. Список пересобран, стало 0 ошибок.
Долг накопился в том числе потому, что вчера я обошёл эту проверку.

Тесты: 27 новых, все проверены вырезанием защиты. Полный набор 202 файла зелёный.

План: docs/superpowers/plans/2026-07-20-skvoznoy-put-do-vebvizora.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:47:19 +03:00
Дмитрий c503e93c8a docs(александра): передача сессии — ритм разговора и задержки 20.07
Два дня работы: звук (хрип от несовпадения A-law/µ-law, заворот высоких,
канал proxy.market) и механика разговора (сторож эха ел вопросы клиента,
глухота между её же предложениями, поток мозга по предложениям).

Ответ по существу 6.05с → 3.64с, пауз внутри реплики 2 → 0.

В файле: что читать следующей сессии, чем мерить, 8 ловушек
и незакрытый слой, который за владельцем.

Номер боевой линии не пишем — сторож ПДн прав, правило одно для всех
телефонов; номер берётся из журнала моста.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:09:38 +03:00
Дмитрий 8697f8b0a2 docs(finder): спецификация и план — фильтр по нише и групповые действия над списками
Постановка владельца 19.07.2026: на экране «Последние списки» нужен фильтр под
выбранную нишу, галочки на списках и групповые действия (удалить / собрать телефоны /
прогреть / отдать менеджеру) — все только по горячим фирмам.

Решения владельца зафиксированы: горячие = 70+, уже собранных и отданных пропускаем
и сообщаем сколько, перед платным действием и удалением спрашиваем с числом,
Телеграм и СМС — заглушки до готовности модулей.

Найдено при проектировании: прогрев берёт только фирмы с собранным мобильным, значит
порядок работы — телефоны → прогрев → менеджер; после прогрева на нашей стороне не
оставалось следа (добавляем пометку площадки); показ 1000 списков требует переноса
счётчиков в колонки, иначе главная разбирает всю базу на каждое открытие.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 08:00:27 +03:00