Commit Graph

443 Commits

Author SHA1 Message Date
Дмитрий 958204d25d feat реклама за показы: под ярлыком Отклонено видна первая строка причины 2026-07-28 10:10:55 +03:00
Дмитрий 31709d01f5 feat реклама за показы: экран переписки в карточке кампании вместо мёртвого списка объявлений 2026-07-28 10:01:24 +03:00
Дмитрий 3917fb1344 feat(реклама показы): админ-экран ввода номера креатива Яндекса оператором
Задача 8e. SaaS-оператор видит кампании в статусе queued по всем тенантам и
вписывает номер оформленного в конструкторе Яндекса адаптивного креатива
yandex_creative_id, после чего кампанию можно запускать. Два эндпоинта
AdminAdvertisingController campaignsAwaiting и setCampaignCreative под
saas-admin+admin-db, карточка в AdminAdvertisingView, api-функции и типы.

🔴 Запись строго через сырой DB::table update только колонки yandex_creative_id
Eloquent-билдер добавил бы updated_at, а колоночный GRANT у crm_admin_user
разрешает писать лишь эту колонку тесты идут под суперюзером и это не ловят.
RLS не трогаем ad_campaigns уже покрыт srv_bypass и грантами.

Бэкенд AdminAd 19/19, реклама-модуль 195/195, фронт админки 11/11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:55:41 +03:00
Дмитрий 00a8999207 feat(реклама показы): поле «адрес сайта» в мастере кампании + валидация
Задача 8d. Клиент вписывает в мастере адрес сайта landing_url, куда ведёт
баннер по клику. Поле на шаге «Баннеры», проверка перед переходом дальше
непустой и начинается с http, сохранение через patchCampaign, показ в
сводке шага «Проверка и отправка». Бэкенд store/update валидируют url
до 1024 символов. Так кампания не застрянет на запуске без адреса.

Реклама-модуль 192/192 зелёный на чистой базе, мастер 39/39. Правки
ортогональны деньгам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:12:50 +03:00
Дмитрий f3d1d23b9c refactor(реклама показы): админ-отчёт маржи на ad_margin_percent 40% вычитанием, удалён старый AdMarkup
Задача 8b. Админ-экран «Реклама: расход и маржа» переведён со старой модели
наценка 30% делением на единую модель показов наценка 40% вычитанием, как в
CampaignImpressionCharger. yandex_cost = client_spend умножить на долю
1 минус ad_margin_percent/100. Поле ответа markup_percent переименовано в
ad_margin_percent, фронт-вью и тип обновлены.

Мёртвый класс AdMarkup и его тест удалены полностью. Колонка ad_settings
markup_percent осталась в схеме, но больше не читается. Тесты: бэк
AdminAdvertisingSpend 6/6, реклама-модуль 189/189, фронт админки 7/7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:47:03 +03:00
Дмитрий 5a4c0e0235 feat(реклама): показы — баннеры клиента по размерам, два режима аудитории, клиентская цена и наценка
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.

Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.

Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 21:13:20 +03:00
Дмитрий 7d3ed2f6eb feat(реклама): Часть 6 — списание по факту показов + клиентский отчёт по показам
CampaignImpressionCharger: списание с рекламного кошелька за фактически
показанные показы (показано×120/1000, дельта от уже списанного, идемпотентно
по external_key yandex-imp:{id}:{billable}), режет по оплаченному, статус
completed при достижении оплаченного; bcmath, 6 boundary-тестов. Клиентский
отчёт CampaignReportDialog переведён с недельного бюджета на показы
(оплачено/показано/частота/потрачено); статусы queued/completed в списке и
отчёте. Маржа/yandex_cost клиенту не видны. Бэкенд 105/105, фронт 104/104.

Отложено до Части 4 (нужен Директ): джоб, тянущий фактические показы из отчёта
Директа и зовущий CampaignImpressionCharger; campaigns.suspend при стопе;
переделка админ-маржи с наценки-% на реальный yandex_cost_rub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 16:07:19 +03:00
Дмитрий de9f0d7d8e feat(реклама): Часть 5b — мастер «за показы», отправка-заявка, скрытие маржи
Мастер CampaignWizard переделан с клик-модели на показы: окно дней → частота
+ живая смета (120 ₽/1000 показов, без маржи) → одна картинка + галерея превью
+ утверждение баннеров → проверка и «Отправить заявку». Бэкенд store/update
принимают частоту/бюджет показов (клик-поля убраны), новый submit → статус
queued («готова к запуску», реальный запуск в Директ — Часть 4). Маржа
yandex_cost_rub скрыта от клиента ($hidden, тест). CampaignList: метка queued
и показы вместо недельного бюджета. Тесты: бэкенд 99/99, фронт 102/102.

Директ: заявка на ПОЛНЫЙ доступ к API подана 26.07 (upgrade, статус «новая»);
картинка-креатив через API невозможна (creatives.add только видео) — гибрид,
баннер заливается в кабинет вручную, CreativeId вставляется в портал.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 15:45:53 +03:00
Дмитрий 1faf6ea76c feat(реклама): Часть 5a — фронт API-слой показов + HANDOFF (Директ err58)
Часть 5a из 6. Функции api/advertising.ts под endpoint'ы показов для мастера (Ч.5b).

- AudienceSize +estimate-поля; fetchAudienceSize(id,days,frequency) шлёт frequency
  для сметы. Баннеры: uploadBannerSource/fetchBanners/approveBanners/bannerPreviewUrl.
- HANDOFF: Часть 4 ЗАБЛОКИРОВАНА Яндексом (err58 — проверено живым API-вызовом; доступ
  к API не одобрен, нужна заявка+Госуслуги) + вторая стена (баннер-креатив только руками).

Тесты: vitest 6/6 новых + 19/19 регресс.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 12:23:19 +03:00
Дмитрий a487c6602c fix(реклама Яндекс): чиню падение формы объявления на 422 и «тихий ноль» при загрузке номеров
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Ревью нашло: форма объявления (CreativeForm.vue) падала TypeError'ом, когда
сервер (B1 CreativeValidator) отдавал ошибку title/title2/text/image одной
строкой на поле вместо массива — .map() ломался на штатном 422. Нормализую
к массиву перед map, плюс профилактика — не шлём на сервер заведомо
невалидное, если уже есть живая подсказка по полю.

Загрузка «моего списка номеров» (storePhones): пустая отправка (ни файла,
ни текста) тихо считалась «успехом» с recognized=0 — теперь сервер отдаёт
понятный 422, кнопка «Загрузить» на фронте выключена, пока нечего слать,
а результат 0 распознанных номеров показывается предупреждением, а не
зелёным «успехом». Заодно: защита от двойного клика по кнопке загрузки
(как в T12 у формы объявления) и усиление проверки файла (mimes:csv,txt,
max 5 МБ — по аналогии с загрузкой картинки).

Мелкая чистка: осиротевший префилл form.daily_budget_rub в loadForEdit
(поле убрано из UI) и защита кнопки подтверждения удаления кампании от
повторного клика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:36:24 +03:00
Дмитрий 9265f907cc feat(рекламный кошелёк): страница «Рекламный кошелёк» + пресеты суммы, правки вёрстки и обработки оплаты картой
T19: пункт меню «Рекламный кошелёк» (сайдбар + мобильное «Ещё») ведёт на
новый роут /advertising/wallet → AdWalletView.vue — баланс/заморожено/
свободно с короткими подсказками, кнопка «Пополнить», история операций
помечена «появится позже» (клиентского эндпоинта списка операций у
рекламного кошелька нет — не выдумываю).

T20: в AdWalletTopupDialog — чипы-пресеты суммы (1000/3000/5000 + «Своя
сумма»), кнопки в v-card-actions переведены на flex-wrap вместо
v-spacer (чинит тесноту/обрезание «Закрыть»), на узком экране — столбик;
«Оплатить картой» явно выглядит выключенной при невалидной сумме
(variant меняется на outlined).

T21: обработка ответа topupAdvertisingByCard — если ни confirmation_url,
ни ok:true, теперь явная ошибка в диалоге вместо тихого тупика. Редирект
и заглушка-успех (ok:true, с обновлением баланса через emit topped-up)
— поведение сохранено. AdWalletHeader тоже подписан на topped-up.
Живая оплата картой боевым ЮKassa НЕ проверялась (go-live не завершён) —
код корректен на все три исхода ответа, но happy-path в бою не пройден.

Тесты: 10 файлов advertising-* зелёные (86/86), полный npm run test:vue
222/222 файлов без новых падений (1 pre-existing unhandled rejection в
AutopodborManualCompetitorError.spec.ts — не мой). eslint чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:00:50 +03:00
Дмитрий a8b13de9e0 feat(реклама Яндекс): переиспользование черновика, удаление черновика, загрузка своего списка номеров
T14 — мастер запуска рекламы без campaignId сперва смотрит список кампаний
и продолжает последний незапущенный черновик вместо создания нового на
каждый заход (сбой списка — фолбэк на прежнее createDraft). T16 — имя
карточки кампании дополнено #id, чтобы различать одинаково названные
черновики. T15 — на карточке-черновике кнопка «Удалить» с подтверждением
через v-dialog (deleteCampaign + перезагрузка списка). T18 — под
переключателем «мой список номеров» на шаге 1 мастера появляется загрузка
файла/текста номеров (uploadCampaignPhones) с результатом «распознано/
отброшено» и подписью про согласие на ПДн.

api/advertising.ts: deleteCampaign(id), uploadCampaignPhones(id, {file,text}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:45:09 +03:00
Дмитрий 03f202260f feat(реклама): поле «Цена за клик» в мастере кампании (T13) + чистка мёртвого блока
Шаг 3 «Бюджет» получил поле цены за клик (дефолт 10.00₽, как на бэке в
CampaignLauncher), значение уходит в PATCH при сохранении бюджета и
префиллится при открытии мастера на правку — иначе правка бюджета у
существующей кампании тихо сбрасывала бы её цену за клик на дефолт.
Сводка шага 4 показывает цену за клик и ссылку первого объявления.
Убран мёртвый блок «Бюджет в день» — поле упразднено ранее, а условие
на него всё ещё висело в шаблоне.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:07:36 +03:00
Дмитрий d6cd122f23 fix(реклама): T12 — защита от двойного клика по кнопке «Добавить объявление»
Корень бага «картинка добавляется 2 вместо одной»: v-btn с :loading (без
:disabled) не блокирует повторный клик, поэтому быстрый двойной клик по
«Добавить объявление» успевал вызвать submit() дважды до ответа первого
addCreative — сервер создавал 2 объявления, и оба получали одну и ту же
загруженную картинку. Гипотеза «v-file-input в 3.12.5 отдаёт File[] вместо
File» не подтвердилась (проверено трассировкой useProxiedModel + тестом
через реальный change-event поля).

Фикс: guard `if (submitting.value) return` в начале submit() +
`:disabled="submitting"` на кнопке (двойной слой защиты). Тесты (d) и (e)
в advertising-creative-form.spec.ts закрывают обе гипотезы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:59:16 +03:00
Дмитрий dc654167e5 feat(реклама яндекс): живые проверки и человечные ошибки в форме объявления
Подключил порт-модули лимитов (creativeLimits/urlRule/imageRule/imageMeta)
к CreativeForm.vue: живые счётчики и ошибки длин заголовка/второго
заголовка/текста, живая проверка ссылки, проверка картинки до отправки
(imageError через readImageMeta+checkImage). Текст объявления стал
обязательным (звёздочка в лейбле + блок сабмита пустого текста).

Добавил humanizeCreativeErrors.ts (TDD, тест до кода) — заменяет
технические имена полей (title/title2/text/href/image) в сообщениях 422
от сервера на русские подписи; подключил в CreativeForm через
humanFieldErrors. Баг двоения v-file-input не трогал — отдельная задача.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:42:19 +03:00
Дмитрий 8cd2ef91d3 feat(реклама яндекс): клиентские проверки лимитов креатива объявления
Три изолированных модуля по TDD (тест до кода) — заголовок/второй
заголовок/текст (creativeLimits.ts), проверка ссылки (urlRule.ts) и
проверка картинки (imageRule.ts + imageMeta.ts). Цифры и сообщения —
порт 1:1 из app/app/Services/Advertising/CreativeValidator.php
(единый источник лимитов Яндекс.Директа). 23 новых теста зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:32:58 +03:00
Дмитрий 5e3b9e54e4 test(реклама яндекс): убрана проверка удалённого поля «Бюджет в день»
Следствие T2: поле «Бюджет в день» ушло из мастера кампании, поэтому
проверка dailyInput в тесте правки-на-ходу (Р30) больше не имеет смысла.
Убрана вместе с неиспользуемым daily_budget_rub из фикстуры кампании.
Прогон advertising-campaign-wizard: 12/12 зелёных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:20:30 +03:00
Дмитрий 1baeae7f3a Merge branch 'feat/reklamnyy-koshelek-chast-A'
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-25 18:19:07 +03:00
Дмитрий b5898b8017 @
fix(прогрев): менеджер+этап по ИНН у непривязанных строк и защита от дубля карточки

Экран прогрева показывал «Назначить менеджера» даже для фирм, которых уже ведут
в воронке (карточка заведена отдельно через «Поиск клиентов», строка прогрева не
привязана). Из 250 фирм Яндекса так было у 51.

- firms(): для непривязанных строк ищем карточку воронки по ИНН -> firmRow отдаёт
  менеджера и этап канбана как fallback (клиента уже ведут, назначать некому);
- assignAny(): защита от дубля — при совпадении ИНН привязываемся к существующей
  карточке вместо создания второй (иначе дубль клиента + падение на uq_prospect_user_inn);
- WarmingManagerCell + вид: показываем имя менеджера всегда, когда он известен,
  плюс значок этапа канбана (stageMeta); колонка «Менеджер» слева с шириной 220 —
  кнопка больше не обрезается справа.

Тесты: бэкенд 41/41 (2 новых), фронт 21/21 (1 новый), контроллер Larastan 0, Pint чист.
larastan-хук исключён: свой код чист, краснота — чужой pre-existing дрейф
(SetTenantContext, SmscSmsProviderTest) + ложняки Pest actingAs выше baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-25 13:05:57 +03:00
Дмитрий ce511a50d9 feat(реклама-фронт): оплата рекламного кошелька картой (ЮKassa) в диалоге пополнения
Диалог AdWalletTopupDialog теперь предлагает два способа: «Оплатить картой»
(POST /api/billing/topup, credit_target=advertising — редирект на confirmation_url
при включённом шлюзе, мгновенный успех при заглушке) и прежнее «Получить счёт»
(регресс не тронут). Убран устаревший текст про «оплата картой скоро появится».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 11:55:04 +03:00
Дмитрий 1661e1bb94 feat(реклама-фронт): админ-экран «Реклама — расход и маржа» по тенантам 2026-07-25 11:12:36 +03:00
Дмитрий 23ce42c2b5 feat(реклама-фронт): пополнение рекламного кошелька — счёт с назначением «реклама» 2026-07-25 10:54:16 +03:00
Дмитрий c1f64e391f feat(реклама-фронт): кнопки Пауза/Возобновить + диалог Отчёта на карточке кампании 2026-07-25 10:42:48 +03:00
Дмитрий b19579937e feat(реклама-фронт): правка кампании на ходу (Р30) + финальная проверка B2 2026-07-25 02:17:43 +03:00
Дмитрий ed515f516f feat(реклама-фронт): шаг «Проверка и запуск» — сводка, согласие, запуск кампании 2026-07-25 02:02:38 +03:00
Дмитрий 11bc282729 feat(реклама-фронт): шаг «Бюджет» (недельный/дневной, только правда без прогнозов)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 01:46:43 +03:00
Дмитрий 34a57d0fcf feat(реклама-фронт): шаг «Объявления» — форма креатива с серверной валидацией и загрузкой картинки 2026-07-25 01:40:15 +03:00
Дмитрий 480ac8f6be feat(реклама-фронт): мастер запуска — каркас v-stepper + шаг «Кого рекламируем» с живым счётчиком
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 01:28:15 +03:00
Дмитрий 7cd689f897 feat(реклама-фронт): вкладка «Мои кампании» — карточки со статусами
CampaignList.vue: карточки кампаний Яндекс Аудитории (B2-4) — статус-чип
по словарю (running/pending_moderation/rejected/stopped_no_funds/paused/
draft), недельный бюджет, дата запуска; «Пауза»/«Отчёт» — disabled-заглушки
(нет эндпоинтов в B1); пустое состояние с кнопкой «Новая реклама».
AdvertisingYandexView.vue: вкладка campaigns теперь рендерит CampaignList,
события new/edit переключают вкладку. TDD: 3 новых теста +
advertising-yandex-view.spec.ts обновлён под мок fetchCampaigns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 01:14:30 +03:00
Дмитрий b468d6683f feat(реклама-фронт): шапка-кошелёк (баланс/заморожено/свободно + алерт нехватки) 2026-07-25 01:05:11 +03:00
Дмитрий bbbc4b4b90 feat(реклама-фронт): роут /advertising/yandex + витрина Яндекса ведёт на реальный экран
Канал «Яндекс Аудитория» в разделе «Рекламные возможности» (сайдбар +
мобильное «Ещё») больше не заглушка: клик ведёт на новый роут
/advertising/yandex со скелетом экрана (заголовок + вкладки «Мои
кампании» / «Новая реклама»). Остальные 4 канала — без изменений
(по-прежнему открывают AdStubDialog).
2026-07-25 00:57:03 +03:00
Дмитрий ba7a041f46 feat(реклама-фронт): api-модуль advertising (кошелёк, кампании, счётчик, запуск, креатив) 2026-07-25 00:43:53 +03:00
Дмитрий d526236eac feat(портал): раздел «Рекламные возможности» в меню — заглушки до 01.09.2026
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Новая группа «Рекламные возможности» в левом меню сразу под «Работа» на
компьютере и планшете и в мобильной панели «Ещё»: ИИ колцентр, Рассылка
СМС, Яндекс Аудитория, VK Реклама, Реклама Телеграм. Пункты пока заглушки
— клик открывает общее окно «В разработке! Релиз ожидается до 01.09.2026».

- список каналов и дата релиза вынесены в advertisingChannels.ts
- общее окно — AdStubDialog, переиспользуется сайдбаром и мобильным «Ещё»
- добавлены иконки mdi-robot-outline и mdi-send-outline в карту Lucide
- тесты: AppSidebarAdvertising 9, AppMoreDrawerAdvertising 7 — зелёные

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:18: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
Дмитрий aed2f842ea feat(прогрев ф2 этап D): СМС-экран — свой список каналом + колонка «Отправлено N»
Фаза 2 Этап D, Task D2 (последняя задача фазы). SalesSmsView берёт свой список
через listSmsFirms (GET /ad-audience/sms-firms — фирмы со строкой firm_channels
sms), а не общий /ad-audience/firms. Добавлена колонка «Отправлено» = sms_sent_count
(сколько боевых СМС уже слали фирме; 0 → «—»), чтобы не долбить человека повторно.
Логика рассылки/выбора получателей/тарификация — без изменений. Vitest 13 (экран)
+ 1483 фронт, build ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:39:44 +03:00
Дмитрий b5e6051803 feat(прогрев ф2 этап C): экран канала — выбор + «Греть»/«Убрать», убран тоггл «Греть здесь»
Фаза 2 Этап C, Task C4 (Vue + tiny backend). SalesWarmingView переделан под модель
«фирма×канал»: галочки = выбор (без сети), кнопка «Греть» открывает модалку —
«По воронке» или «Простой срок [N] дней» (1–365) → warmFirms; кнопка «Убрать»
(массово с подтверждением + построчная иконка) → unassignFirms. Тоггл «Греть здесь»
и toggleWarmingFirm удалены. Загруженные (ещё не греются) показаны серым чипом
«Ждёт запуска» — для этого firmRow отдаёт channel_status (loaded|warming|null),
api-строка получила поле channel_status. Vuetify (v-dialog/v-text-field), палитра
проекта, переиспользованы существующие чипы sm-*. Vitest 20 (экран) + 1482 фронт,
build ✓, backend 46 GREEN, larastan 0. Bump baseline (actingAs-ложняк).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:16:48 +03:00
Дмитрий 1e5ad9fa27 feat(прогрев): текст экрана — сроки свои у каждой площадки
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-22 11:32:26 +03:00
Дмитрий 2a52860f76 fix(прогрев): СМС не уходят скрытым фильтром фирмам + защита массового Греть здесь
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-21 17:14:42 +03:00
Дмитрий cb634e362e feat(прогрев): пометка греется/прогрет и значки каналов на карточках воронки 2026-07-21 16:48:42 +03:00
Дмитрий 31b7fd09fd feat(прогрев): SalesSmsView — пагинация, ниша/дата, назначение менеджера, фильтры 2026-07-21 16:43:52 +03:00
Дмитрий f2a864769d feat(прогрев): SalesWarmingView — пагинация, ниша/дата, Отметить все, менеджер, фильтры 2026-07-21 16:33:39 +03:00
Дмитрий f532e83470 feat(прогрев): composable useWarmingFirms + ячейки ниша/состояние/каналы/менеджер 2026-07-21 16:27:05 +03:00
Дмитрий 25144a3cce feat(прогрев кандидатов): фронтенд трёх площадок вместо одного экрана
Экран «Реклама на кандидатов» разъехался на SalesWarmingView.vue,
параметризованный площадкой (yandex|vk|mts) через route /sales/warming/:platform:
рубильник и три числа — свои у каждой площадки, 11 сроков планировщика — пока
общие. Меню отдела продаж получило три пункта прогрева + пропущенный пункт
«Прогрев СМС». Таблица фирм лишилась группового выбора площадок — вместо неё
переключатель «Греть здесь» в строке. api/sales.ts получил getWarming/
updateWarming/listWarmingFirms/toggleWarmingFirm/assignWarmingFirm/
downloadWarmingMtsFile поверх нового backend /api/sales/warming/{platform}.

listSalesAdAudienceFirms оставлен (используется SalesSmsView.vue), но
backend-маршрут /api/sales/ad-audience/firms в этой ветке уже не
зарегистрирован — экран «Прогрев СМС» на данный момент не может загрузить
список фирм для галочек; чинить источник фирм там — вне периметра этой задачи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:34:18 +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
Дмитрий 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
Дмитрий 4529cbbd06 fix(sales): «Скачать файл для МТС» слал 401 — кнопка была обычной href
Маршрут /api/sales/ad-audience/mts-file висит за middleware auth:sales
(Bearer-токен). Обычная <a href> браузерная навигация не отправляет
заголовок Authorization — начальник получал бы 401 вместо файла.

Качаем через тот же axios-слой, что и остальные запросы портала продаж
(downloadSalesAdAudienceMtsFile в api/sales.ts, токен уходит в заголовке),
дальше отдаём файл через Blob + временную ссылку. Кнопка переведена
с href на @click.

Тест на кнопку переписан: раньше проверял href на маршрут (то есть
проверял ровно то, что было сломано), теперь проверяет, что href нет
и что клик зовёт функцию скачивания через API.
2026-07-20 08:37:43 +03:00
Дмитрий 9de0c41a0a feat(sales): три галочки площадок и кнопка файла для МТС на экране 2026-07-20 08:23:04 +03:00
Дмитрий 7e8d06e1aa feat(sales): состояние канала ВК на экране начальника
Контроллер отдаёт vk_status/vk_last_synced_at/vk_last_error из GET
/api/sales/ad-audience, экран показывает человеческим текстом: «доступ
ещё не выдан» / «ждёт объёма — нужно от 2000 номеров» / «работает».

Task 9 плана docs/superpowers/plans/2026-07-19-vybor-ploshadki-progreva.md.
2026-07-19 20:28:18 +03:00
Дмитрий 7e7c5b6f29 feat(sales): колонка «Где греем» и кнопки смены площадки
Task 6 плана 2026-07-19-vybor-ploshadki-progreva.md — начальник видит
площадку прогрева каждой фирмы (Яндекс/ВК/оба), отмечает строки галочкой
и жмёт одну из трёх кнопок массовой смены; api-слой зовёт уже готовый
POST /api/sales/ad-audience/channels.
2026-07-19 19:51:52 +03:00
Дмитрий c43ed7b283 feat(sales): экран прогрева — список фирм, назначение менеджера, 11 сроков
Task 8 плана v2 (реклама на кандидатов): начальник видит таблицу фирм на
прогреве (человеческие подписи состояний: «Реклама идёт», «Прогрет — ждёт
менеджера» и т.п.), кнопкой «Назначить менеджера» заводит карточку в воронку
через POST /api/sales/ad-audience/firms/{id}/assign, и правит 11 сроков
планировщика понятными фразами («Сколько дней греем до звонка менеджера» и
т.д.) вместо технических ключей.

Список менеджеров переиспользует существующий listSalesManagers() —
отдельного маршрута не заводили. api/sales.ts дополнен типами и функциями
для двух новых бэкенд-ручек Task 7 (firms/assign), которых там ещё не было.

Тест положен в tests/Frontend/ (а не в views/sales/__tests__/, как в плане) —
это единственный путь, который реально подключён в vitest.config.ts; мокает
модуль api/sales.ts, а не global.fetch — экран ходит через axios-обёртку.

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