Commit Graph

610 Commits

Author SHA1 Message Date
Дмитрий 5b7d6e1bab fix(смс-клиент): «не определён оператор» вместо неправды «номер не из МТС»
Строка приёмочного листа 2.3. Раньше номер с НЕИЗВЕСТНЫМ оператором и номер
чужого оператора давали одну причину, и клиент читал про свой номер неправду:
«не из МТС» — хотя чей он, мы не знаем. Теперь причины две, и обе правдивы.

Так приходит большинство таких номеров: вписанные руками — без оператора
всегда, номера «своей базы» — до того, как ДаДата его проставит.

Причина живёт в четырёх местах, а не в трёх, как считал план: отборщик,
читатель снимка (журнал рассылки) и ДВА словаря подписей на экране. Счётчик в
контроллере складывает любые причины — править нечего.

Два капкана, оба доказаны вырезанием, а не рассуждением:

Порядок проверок. Отсеивать номер сразу, как только оператор не распознан (так
велел план), нельзя: универсальный канал берёт и номер без оператора, и такие
номера перестали бы уходить — рассылка молча уменьшилась бы при зелёных тестах.
Сперва спрашиваем маршрутизатор, причину называем только после отказа.
Поведение не изменилось ни на волос — изменилась надпись.

Деление. Словарь операторов отдаёт пустоту и когда оператора нет, и когда имя
есть, но словарь его не знает («Тинькофф Мобайл»). Делим по сырому значению,
иначе получилось бы новое враньё в другую сторону.

Тесты: +5 (три причины врозь, сторож универсального канала, журнал рассылки —
чтобы предпросмотр и журнал не расходились в словах). ClientSms 165/165, приём
лидов 17/17, фронт 1656 зелёных, phpstan 0, pint и eslint чисто.

Живой прогон: одна сводка сразу показывает и правду, и контроль — «Уйдёт 1 СМС
— 9.00 ₽ · Не уйдёт: не определён оператор — не знаем, куда слать — 1 · номер
не из МТС (пока шлём только по МТС) — 1». В журнале рассылки та же правда.

Экран пока НЕ подсказывает, что оператор ещё выясняется — отдельная работа,
записана в «Чего эта работа НЕ делает» п.16.
2026-07-28 10:58:45 +03:00
Дмитрий 9fc2f030cf feat(смс-клиент): срок «за последние N дней» не больше года
Строка приёмочного листа 2.8. Больше 365 дней задать нельзя, и человек читает
почему: «Больше 365 дней нельзя — возьмите срок покороче».

Правило max:365 стоит в общей проверке полей — значит и на предпросмотре, и на
создании рассылки, мимо экрана его не обойти. Заодно человеческий текст у нижней
границы: стандартный называл поле программистским именем audience_days.

На экране проверка настоящая, а не только атрибут max у поля: напечатать 400
руками браузер спокойно даёт, и атрибут при этом молчит. При запрещённом сроке
экран показывает сообщение, запрос на сервер не уходит и смета гасится — иначе
кнопка «Отправить» осталась бы живой со старым числом получателей.

Тесты: 2 новых серверных (отказ с человеческим текстом на обеих дорогах +
контрольная граница ровно 365) и 2 на экране. ClientSms 158/158, приём лидов
17/17, фронт 1654 зелёных. Проверено вырезанием четырежды.

Живой прогон: 30 дней — «Уйдёт 5 СМС — 45.00 ₽»; напечатал 400 — сообщение,
смета исчезла, кнопка заперлась, на сервер не ушло ни одного запроса; вернул
365 — смета снова на месте. Прямыми запросами мимо экрана сервер отказывает тем
же текстом и рассылку не создаёт.
2026-07-28 08:42:14 +03:00
Дмитрий a5451bef76 feat(смс-клиент): галочки статусов воронки в рассылке по сделкам
Строки приёмочного листа 2.6 и 2.7. Клиент выбирает, каким сделкам слать:
пять галочек воронки (Новая сделка, Просмотрено, В работе, Сделка,
Не реализовано), по умолчанию отмечены все.

Отбор применяется в ClientSmsAudienceBuilder::fromDeals() — через него идут
и смета предпросмотра, и снимок получателей при запуске, поэтому число на
экране и факт отправки остаются одним списком.

Пустой список = «все статусы», отдельного значения «никому» нет (решение
В-42): экран не даёт снять последнюю галочку и объясняет почему. Новая
колонка audience_statuses (jsonb, NULL = «все») — уже созданные рассылки
поведения не меняют. Права не нужны: колонка наследует права таблицы.

Живой прогон поймал дефект, которого не видели тесты: запрет снять
последнюю галочку не работал вообще — галочка Vuetify правит список на
месте, и обычное наблюдение этого не видело, а тест присваивал новый
список. Лечение: наблюдение вглубь + возврат после отрисовки; тест
переписан на правку списка на месте.

Тесты: 5 новых серверных + 4 на экране, ClientSms 156/156, приём лидов
17/17, фронт 1652 зелёных. Живой прогон: 5 → 4 получателя после снятия
«Не реализовано»; рассылка только со статусом «Сделка» ушла ровно на номер
этой сделки. Журнал схемы: v9.09 (эта работа) и v9.08 — пропущенная запись
о снимке получателей, дописана задним числом.
2026-07-28 08:09:04 +03:00
Дмитрий 3a2f260334 feat(смс-клиент): кончились деньги — рассылка встаёт честно, а не молча
Перед каждым списанием джоб проверяет, что деньги есть. Без этого
AdWalletService::charge при недоборе не бросает исключение, а прижимает
баланс к нулю: часть рассылки уходила бы бесплатно, а клиент видел бы
пустой кошелёк без объяснения.

Доступное этой рассылке = баланс − заморожено + СВОЯ активная заморозка.
Наивное «баланс − заморожено» остановило бы рассылку на первом же номере
при полном кошельке — вся смета заморожена при создании (ловушка В-41,
доказана вырезанием: тест-страховка краснеет).

Кончились деньги — номер пишется в журнал как «не хватило денег», рассылка
получает статус «остановлена» с причиной no_funds, заморозка снимается.
Экран разбирает причину: «Остановлено: закончились деньги. Ушло N из M,
списано X ₽».

Живой прогон нашёл то, чего не видели тесты (В-48): статус в колонке рядом
писал «остановлена вами» и для денежной остановки. Починено, тест расширен
на всю ячейку.

Строка приёмочного листа 2.4. Тесты: ClientSms 147/147, приём лидов 17/17,
фронт 1648, phpstan 0, pint чисто.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 06:36:11 +03:00
Дмитрий 49c75d17f4 fix(смс-клиент): права на счётчики всего семейства client_sms_* — блокер выката
Ни одна из 14 миграций модуля не выдала GRANT USAGE, SELECT на sequence.
На боевом кластере роль crm_app_user не владеет таблицами и не имеет
BYPASSRLS, поэтому первый же INSERT упал бы с «permission denied for
sequence». Тесты и локальная база этот класс поломки не видят: там
суперпользователь. Тот же случай уже был на бою — v8.84/v8.85.

Починено аддитивной миграцией: старые миграции не переписываем, на
кластере они не перезапускаются. Проверено вырезанием — откат снимает
право, накат возвращает.

Там же: три GRANT в create_sms_global_optouts обёрнуты в проверку
существования роли — без неё migrate падал на чистой базе.

Заодно приведены к правде два теста меню: «Рассылка СМС» давно не
заглушка, а настоящий раздел, тесты этого не знали и были красные.

Нашёл rls-reviewer при приёмке Этапа 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:17:16 +03:00
Дмитрий 37493b7d02 feat(смс-клиент): экраны Этапа 1 — «Не писать этим», «Остановить», ключ заказа, общий стоп-лист в админке
Вкладка «Не писать этим» отдельной панелью SmsOptoutsPanel.vue: номер руками,
пачкой из файла, удаление; непонятые строки показаны образцами, а не молча.

Кнопка «Остановить» видна, пока рассылка в очереди, идёт или ждёт утра; после
остановки строка показывает честный итог «Ушло N из M, списано X ₽» — деньги
фактические, а не смета.

Ключ заказа crypto.randomUUID() уходит с каждой отправкой и меняется после
успеха. На ответ сервера «похоже, это повтор» экран задаёт вопрос словами
сервера и повторяет только по согласию человека.

В админке раздел «Общий стоп-лист»: список, внесение с причиной, удаление.

Отказ получателя (страница по ссылке и приписка в тексте) в экранах
отсутствует — отменён владельцем, см. В-30 приёмочного листа.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:45:47 +03:00
Дмитрий b29a4ca4c4 revert(смс-клиент): отказ получателя убран целиком — решение владельца
Владелец: «нет такой функции и задачи нет, забудь о ней! пришла и пришла смс».
Причина — приписка «Отказ: liderra.ru/s/…» ставила НАШ адрес в рекламное СМС, которое
клиент шлёт своим покупателям: он рекламирует себя, а не нас.

Убрано: страница отказа /s/{token}, таблица коротких ссылок, сервис токенов, приписка
в тексте рассылки, колонка with_optout_link, ограничение частоты sms-unsubscribe,
три файла тестов, три миграции (на прод не выкатывались).

Осталось нетронутым: стоп-лист самого клиента «Не писать этим» и общий стоп-лист
портала — это другое, их владелец не отменял.

Строки приёмочного листа 1.6-1.13 срезаны, записано в «Чего эта работа НЕ делает» п.14
и в журнал вопросов В-30. Возражение про 38-ФЗ высказано владельцу и им отклонено.
Всё удалённое лежит в истории: коммиты 7aa30833 и 1dece3a2.

ClientSms 140/140, приём лидов 17/17.
2026-07-27 18:58:48 +03:00
Дмитрий 7aa3083399 feat(смс-клиент): страница отказа по ссылке из СМС — без входа, номер маской, с лимитом
Человек, получивший СМС, теперь может отказаться сам: короткая ссылка
liderra.ru/s/<токен> открывает простую страницу с одной кнопкой. Нажал — номер
в стоп-листе именно той компании, от которой пришло сообщение.

- таблица client_sms_unsubscribe_links (RLS + tenant_isolation), одна ссылка
  на пару «клиент + номер» навсегда — при каждой рассылке новая не плодится
- сервис токенов: 12 символов, без 0/O/o и 1/l/I (ссылку диктуют вслух)
- публичная страница: blade без Vue-сборки, номер только маской +7 *** *** ** 67
- ограничение частоты 20/мин с IP — перебор ссылок иначе = утечка номеров;
  защита проверена вырезанием, тест на 429 краснеет
- неизвестная ссылка отвечает 404 без подсказок
- служебное соединение только в этом контроллере (тенант-контекста у страницы
  нет) + GRANT для crm_supplier_worker на client_sms_optouts
- CHANGELOG схемы v9.02, с напоминанием ПЕРЕзапустить 03_service_bypass_policies.sql

Строки приёмочного листа 1.6, 1.7, 1.8, 1.9 закрыты тестами; живой прогон —
после выката, страницу надо открыть телефоном.

Ветка feat/client-sms-broadcast, песочница, в main не влито.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:58:56 +03:00
Дмитрий a39694e11b feat(смс-клиент): общий стоп-лист портала — номер закрыт у всех клиентов сразу
Этап 1 «Нельзя обжечься», задачи 1–3 приёмочного листа
(docs/superpowers/2026-07-27-PRIEMKA-client-sms-fixes.md, строки 1.4, 1.4а, 1.5).

- новая таблица sms_global_optouts (SaaS-уровень, RLS намеренно нет: номер
  закрывается у всех тенантов сразу — защита договора с МТС при жалобе);
- ClientSmsRecipientSelector отсеивает такой номер ПЕРВЫМ, раньше тенантского
  стоп-листа: наше обязательство перед оператором сильнее настроек клиента;
- клиенту причина видна словами — «номер закрыт администрацией», а не молчаливое
  «не отправлено» (решение владельца, вопрос В-2 листа);
- админ-адреса /api/admin/sms/global-optouts: внести (номер в любом виде),
  список, убрать; непонятый номер отклоняется внятно;
- запись v9.00 в db/CHANGELOG_schema.md.

Проверено: ClientSms 122/122, приём лидов 17/17, phpstan по своим файлам 0,
gitleaks чисто. Защита доказана вырезанием — без отсева падают ровно 3 теста.

Живого прогона строк 1.4/1.5 ещё нет: админ-экран идёт задачей 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:17:20 +03:00
Дмитрий bc573a99eb feat(смс-клиент): своя база — загрузка Excel, пример файла, оператор через ДаДату, отброшенные не молча
Доработки экрана «Моя база» клиентской СМС-рассылки по просьбе владельца:

- Кривые номера больше не выбрасываются молча: uploadContacts/uploadContactsFile
  возвращают rejected + rejected_samples, экран показывает «Добавлено: N,
  не распознали: M (например: …) — проверьте формат».
- Таблица базы — только «Номер» и «Оператор» (колонку «Имя» убрал).
- Загрузка базы Excel-файлом: сервис ClientSmsPhoneFileReader (phpspreadsheet)
  читает первую колонку, пропускает заголовок, номера-как-числа не уходят в
  научную нотацию; endpoint POST /api/sms/contacts/file; на экране — выбор файла
  и кнопка «Загрузить файл».
- «Скачать пример файла»: ClientSmsBaseExampleWriter + GET /api/sms/contacts/example
  — готовый .xlsx с колонкой «Телефон» и образцами номеров.
- Оператор через ДаДату: EnrichClientSmsContactsOperatorJob обогащает добавленные
  номера (DaDataPhoneClient.provider) в фоне, под tenant-контекстом (RLS), с
  защитой дневного бюджета (DaDataBudgetGuard); сбой ДаДаты не роняет прогон,
  оператор остаётся пустым; уже известного оператора не перезапрашиваем.

Тесты: бэкенд ClientSms 130/130, фронт СМС-спеки 63/63 — зелёные; pint чист;
портал пересобран. Синтетические номера 7999… в тестах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 21:11:13 +03:00
Дмитрий 3ad2ffb771 feat(смс-клиент): бланк письма-разрешения — Word на все 8 случаев + выбор правообладателя
Имя отправителя Лидерра регистрирует у МТС от лица клиента, поэтому письмо-разрешение
нужно всегда. Собрал генератор бланка так, чтобы он покрывал все случаи и всегда
отдавал целый документ — заполненный, где есть данные, и с прочерками, где их нет.

- ConsentLetterBuilder — единый источник текста письма на все 8 комбинаций
  «объект (юр.наименование/название ИП/товарный знак/домен) × правообладатель
  (юрлицо/ИП/физлицо)» строго по образцам МТС. Паспорт физлица не храним — прочерк.
- ConsentDocxWriter — рендер в РЕДАКТИРУЕМЫЙ Word (.docx) вместо PDF, чтобы клиент
  дописал недостающее; акцент про паспорт («должны совпадать с документом на право»).
- consentForm(): убрана жёсткая блокировка (бланк отдаётся всегда), добавлен выбор
  правообладателя owner_type/owner_name — домен/знак могут быть на физлице даже
  у клиента-юрлица/ИП, тогда письмо от этого физлица.
- Экран: подсказки «как назвать имя» под каждый вид, общее правило (до 11 знаков,
  латиницей), радио «на кого зарегистрирован домен/знак» + поле ФИО, кнопка бланка
  активна после ввода имени, ярлык «Скачать бланк разрешения (Word)».
- consent-form.blade.php удалён (заменён docx); phpoffice/phpword ^1.4 в composer.

Тесты: бэкенд ClientSms 116, фронт SMS-спеки 44 — зелёные; pint чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 18:33:45 +03:00
Дмитрий cfc143ddd6 fix(смс-клиент): вид имени по типу лица клиента + формулировка письма строго по объекту
Устранена нестыковка: физлицу давали выбрать «Название юрлица» → письмо-бессмыслица.

- Экран показывает только допустимые виды имени по типу лица из реквизитов:
  физлицо — домен и товарный знак; ИП — плюс наименование ИП; юрлицо — плюс
  название юрлица. Радио-группа рендерится из allowedNameTypes.
- sender() отдаёт subject_type клиента.
- consentLetter: тип правообладателя в письме привязан к объекту — юр.наименование
  всегда юрлицо, наименование ИП всегда ИП, домен/товарный знак по типу клиента.

Проверено в браузере: физлицо видит только домен/ТЗ; юрлицо → письмо «ООО … в лице
гендиректора … на использование юридического наименования организации», без «ИП».
Backend SenderTest 19, фронт СМС 45, pint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 16:29:30 +03:00
Дмитрий 13e5472259 fix(смс-клиент): письмо-разрешение точно по образцам МТС — одностороннее, объект × правообладатель
Бланк разрешения переписан по официальной странице МТС support.mts.ru
«Письмо-разрешение от правообладателя»:

- Одностороннее письмо от правообладателя клиента в наш адрес; получатель —
  наш ИП по ИНН из legal_entities is_default. Не двусторонний договор.
- Текст по матрице: объект товарный знак/домен/юр.наименование/ИП × тип
  правообладателя юрлицо в лице гендиректора на основании Устава / ИП / физлицо
  с паспортными данными от руки.
- Срок, подпись и печать правообладателя, можно ЭП; приписка МТС что рукописные
  и без подписи не принимаются.

Метод consentLetter вместо consentSubject; Blade переписан под письмо.
Тексты сверены со всеми комбинациями образцов МТС. Backend 97, фронт 43, pint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 16:04:57 +03:00
Дмитрий c52fedae9d feat(смс-клиент): имя отправителя регистрируем мы — разрешение по образцу + документ-основание
Имя у оператора регистрирует Лидерра от лица клиента, поэтому от клиента два
скана: подписанное разрешение и документ-основание. Оба обязательны, галочка
согласия убрана.

- Бланк разрешения — PDF по нашему образцу «Разрешение-домен УНИВЕРСАЛЬНОЕ»:
  Правообладатель = клиент, Пользователь = наш ИП из legal_entities is_default.
  4 вида имени домен/юрлицо/ИП/товарный знак — у каждого своё основание права.
- Физлицо: домен часто на физлицо — гейт по ФИО, паспорт вписывается от руки,
  паспорт не храним 152-ФЗ.
- Две колонки client_sms_senders: doc_* документ-основание, consent_doc_*
  подписанное разрешение.
- Админка: скачивание обоих сканов, вид .../document/basis и .../document/consent.
- Гейт реквизитов requisites_ready; эндпоинт GET /api/sms/sender/consent-form.

Тесты: ClientSms backend 95, фронт СМС 43, pint чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 14:38:21 +03:00
Дмитрий c47c3fb4d5 fix(смс-клиент): недосписание при повторе, срок для рассылки по сделкам, заслонка баланса + понятный интерфейс
Корректность/деньги:
- джоб рассылки: списание ПОШТУЧНО и атомарно с записью в журнал, ключ идемпотентности на номер, факт из БД — нет недосписания при падении посреди рассылки и повторе
- валидация: срок обязателен при source=deals, иначе выборка по всей истории сделок
- «Отправить» гаснет при нехватке свободного баланса; index отдаёт баланс/заморозку
- помесячный джоб имени: устойчив к пустым настройкам, сигнал в лог при автоотключении за долг
- цена одного авто-СМС + подтверждение при включении авто-рассылки
- загрузка контактов сообщает число нераспознанных номеров

Понятность для клиента:
- «СМС» вместо «сегментов», «пробный режим» вместо «песочницы», человеческие причины пропуска, шаги 1-2-3, выгода своего имени, факт вместо оценки в таблице, имя и текст в подтверждении

Тесты: бэкенд ClientSms 83/83, фронт СМС 50/50, приём лидов 10/10; pint/stan чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 10:27:14 +03:00
Дмитрий f2e6783ae8 feat(смс-клиент): фронт Этапа 2 — карточка имени, авто-СМС, админ-экран
Клиент: карточка имени отправителя (статусы pending/active/suspended/rejected,
диалог заказа с согласием и платой, отключение) + вкладка «Авто-СМС» (тумблер+текст).
Админ: экран «СМС: тарифы и имена» — правка ступеней, платы за имя/грейса, очередь
подтверждения имён (approve/reject/disable); роут /admin/sms + пункт AdminLayout.
API-обёртки в client-sms.ts и admin.ts (cookie-сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 23:21:17 +03:00
Дмитрий 9d6c3372b6 feat(смс-клиент): экран рассылки + пункт меню + маршрут /advertising/sms
AdvertisingSmsView: кошелёк-шапка, вкладки (мои рассылки/новая/база/шаблоны),
счётчик символов·сегментов·цены, предпросмотр с задержкой и расшифровкой отсева,
диалог подтверждения, баннеры песочницы и имени отправителя (liderra.ru). Роут
advertising-sms; активирован пункт-заглушка «Рассылка СМС» в advertisingChannels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:25:49 +03:00
Дмитрий c47dc8ff23 feat(смс-клиент): фронт-API client-sms.ts
Обёртки к /api/sms/* (cookie-сессия, как advertising.ts): рассылки, предпросмотр,
база контактов, шаблоны. Типы кампании/сообщения/контакта/шаблона/предпросмотра.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:25:47 +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
Дмитрий 94c6fdd7d3 chore(прогрев): вернуть в git WarmingChannelIcons.vue (был только на проде, пропущен git add)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 17:47:31 +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
Дмитрий ea01e50a7a feat(реклама): письмо и флаг баннера при нехватке рекламного кошелька 2026-07-24 21:11:51 +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
Дмитрий 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
Дмитрий 5958ff81ba feat(смс): подпись причины пропуска «нет имени для канала» на экране 2026-07-23 12:48:20 +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
Дмитрий 14ff80a05e feat(прогрев): типы firmRow/prospect.warming + обёртка assignAdAudienceFirm 2026-07-21 16:21:00 +03:00
Дмитрий 1032b34ec3 feat(прогрев): «?»-подсказки к каждому из 11 сроков на страницах площадок
По каждому сроку — иконка mdi-help-circle-outline с всплывающей подсказкой
(наведение + тап на мобильном), кратко по-человечески «что этот срок значит».
Экран общий для Яндекс/ВК/Телеграм — видно на всех трёх вкладках.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:59:03 +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