2dab5f2f8d7500c8ffa0bd6fac92541aa812ca49
1006 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ea706e531 |
merge: телеграм-кошелёк сведён в рабочую ветку — она снова равна боевому
Кошелёк и заморозка телеграм-рекламы выкачены на боевой из ветки
fix/tg-zagolovok-obyavleniya. Пока работа жила только там, рабочая ветка
расходилась с боевым на 29 файлов, и следующая сверка приняла бы это за
чужое затирание. Теперь в рабочей ветке есть всё, что стоит на бою.
Сведение без склеек. Проверки на объединённом дереве: статанализ 0,
телеграм плюс СМС 732 из 732, экраны 251 файл и 2034 теста, робот 160 из 160.
🪤 Статанализ сперва упал одной ошибкой на новой колонке mts_cost_rub, и
причина была не в коде: файл подсказок типов _ide_helper_models.php лежит вне
git, у каждой рабочей папки свой, и в этой он про колонку не знал, потому что
местная база разработки отставала на три миграции. Лечение: догнать местную
базу и пересобрать подсказки ключом -M -n. Собственный сторож проекта поймал,
что я пересобрал их неверным ключом, — без него анализатор упал бы молча.
|
||
|
|
e13130e3d7 |
fix(админка): снят сторож xfetch — он врал зелёным, а сам сервис нам больше не нужен
Сторож пинговал https://xf4.ru/fetch и считал сервис живым по любому ответу меньше 500. А GET туда ВСЕГДА отдаёт 405 (ручка только на POST). Поэтому 03.08.2026, когда у xf4 отвалился браузерный движок и «Поиск клиентов» весь день собирал по нулям, админка всё это время показывала «xfetch жив». Сторож проверял, что дом стоит, а не что внутри кто-то есть. Сам xf4 из поиска убран (перешли на свою рендер-виртуалку), сторожить нечего. - реестр ExternalBalanceRefresher: 15 ключей -> 14, класс сторожа удалён; - KNOWN_SERVICE_KEYS и ссылка «пополнить» для xfetch убраны из дашборда; - карточка «Поиск клиентов» теперь служба + Keyso (рендерщик виден отдельной плиткой «Свой рендер»). XfetchClient автоподбора НЕ тронут — это другой слой, и он уже деградирует в пустоту без ключа (XFETCH_* убраны из боевого .env ещё в июле). Тесты: Pest 35 зелёных, Vitest 22 зелёных, Larastan 0 ошибок. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7327483c61 | Merge branch 'feat/prospects-manual-testing-kp' into fix/tg-zagolovok-obyavleniya | ||
|
|
f70527c3df |
fix,деньги: телеграм-реклама переведена на рекламный кошелёк с заморозкой — робот наконец платит МТС
🔴 Найдено чтением кода, а не по памяти: робот НИКОГДА не оплачивал кампанию. finalize доводил боевой запуск до кассы кабинета и уходил (launched:false, stoppedAt:'payment'), денежные кнопки ему были запрещены наглухо, а «реальная оплата» отложена на «Сессию 6», которой не случилось. Песочницу при этом выключили 02.08 в 07:40. Итог на бою: клиент жал «Запустить» → у него списывалась ВСЯ смета по размеру списка → в кабинете оставался неоплаченный черновик → на модерацию он не уходил → реклама не показывалась ни разу → деньги не возвращались никогда (статус draft_ready терминальный, возврата не имеет). То есть беда была не «клиент переплачивает разницу», как записали накануне, а «клиент платит сто процентов ни за что». Слепки установленного у владельца робота совпали с веткой до буквы — на боевой машине тот же код. Владелец решил: боевой не трогать (стоит как стоит), роботу денежную кнопку разрешить, но с потолком. ── Робот теперь платит ──────────────────────────────────────────────────────── submitWithPayment на шаге /payment: сперва ЧИТАЕТ сумму к оплате, потом гонит её через гейт против меньшего из двух потолков (лимит кампании и общий потолок робота), и только потом ищет кнопку и жмёт. Сумма не прочиталась — не платим: не знаем, что списываем. Не ушли со /payment после клика — падаем громко, портал по отказу отпустит заморозку. Общий чёрный список денежных кнопок НЕ ослаблен: та же кнопка остаётся запретной для всех прочих путей, включая пересдачу. Разрешение точечное. Прочитанная сумма — это НАШИ расходы у МТС; она едет в портал полем actualCostRub, которое до сих пор было пустой заготовкой. ── Кошелёк вместо общего баланса ────────────────────────────────────────────── Порядок зеркалит сам МТС (билинг снят живьём 27–28.07, FLOW-FINDINGS «Задача 2.0»: кабинет резервирует сумму, окончательно списывает по факту показов, остаток возвращает): запуск → морозим смету на ad_wallets (канал telegram) робот заплатил → фактическая сумма легла в кампанию (mts_cost_rub, v9.66) модерация «да» → списываем по факту × наценка, остаток отпускаем модерация «нет» → отпускаем всё, ни рубля не списано сбой до кабинета → отпускаем всё Заморозка была убрана 29.07 намеренно — тогда рассуждали «сумма известна в момент запуска, морозить нечего». Рассуждение верно ровно до вопроса владельца: сумма известна, а сколько человек из списка вообще есть в телеграме — нет. Факта нет, а модерация одобрила — НЕ списываем ничего и кричим в журнал. Списать «по оценке» значило бы вернуть ровно ту беду, ради которой всё и делалось. Идемпотентность больше не самодельная: бронь уникальна по кампании, списание — по ключу события. Прежнее «сальдо проводок» стало не нужно, CampaignChargeServiceTest удалён — его предмет (charge/refund по общему балансу) больше не существует, замена KoshelekTelegramaTest. ── Экран ────────────────────────────────────────────────────────────────────── Карточка денег показывала общий баланс портала. После переезда это стало прямым враньём: клиент видел бы «денег хватает» там, где запуск отвечает 409. Теперь ручка отдаёт СВОБОДНЫЕ деньги кошелька (баланс минус заморозка) и заморозку отдельной графой, а кнопка пополнения ведёт в рекламный кошелёк — прежняя клала бы деньги в общий баланс, и запустить рекламу всё равно было бы нельзя. ── Проверено вырезанием, а не только зелёным ───────────────────────────────── - убрал запрет «сумма не прочитана» — покраснели 2 датчика оплаты; - вернул списание по смете вместо факта — датчик поймал 315 ₽ там, где должно быть 210 ₽. Замеры: телеграм-модуль 330/330, вместе с рекламой и СМС 1078/1078, экраны 251 файл / 2033 теста / 0 падений, робот 160/160, статанализ 0, типы 0, формат чисто. Сторож денег под боевой ролью переписан: класс «тихий ноль» закрылся сам — AdWalletService ставит контекст клиента сам, а не надеется на вызывающего. 🔴 На боевой НЕ выкачено. Осталось открытым: показать клиенту строкой «заморожено / списано по факту / возвращено» на карточке кампании; пересдача по-прежнему шлёт «без оплаты» (кампания вернётся на модерацию неоплаченной); числа 0,720 и 0,816 за показ с медиа так и не замерены живьём. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ec026f1b7b |
fix,смс: час по названию региона — Москва и Питер записаны в справочнике парой
Найдено приёмкой на живом справочнике боевого сервера. Оператор определялся верно, а часовой пояс у Москвы и Санкт-Петербурга — нет: у 24 063 диапазонов из 453 080 кода субъекта нет вовсе, потому что регион там записан ПАРОЙ — «г. Москва и Московская область», «г. Санкт-Петербург и Ленинградская область», «Архангельская область * Ненецкий автономный округ». Однозначного субъекта у такой строки действительно не существует. Это самые крупные по числу абонентов диапазоны страны. Без разбора пары оператор проставлялся, а пояс нет — и сообщение всё равно не уходило, потому что без пояса номер ждёт уточнения региона, решение владельца В-85. Нам, в отличие от приёма лидов, точный субъект не нужен — нужен ЧАС, а у обеих половин каждой такой пары он общий. Поэтому пара разбирается на половины по разделителям « и » и « * », и час берётся ТОЛЬКО если все узнанные половины сошлись. Не сошлись или ни одна не узнана — честное «не знаю»: молча выбрать одну из двух значит однажды отправить человеку СМС ночью. Заодно поправлено утверждение в шапке класса: ключ ДаДаты на боевом сервере ЕСТЬ, это проверено командой. Почему обращение к ней не дало оператора, установить не удалось — падений в журнале нет ни одного. Модуль СМС 402 из 402, статанализ 0. |
||
|
|
d4ccb5ac95 |
fix,смс: оператор и часовой пояс номера — из бесплатного справочника Россвязи
Модуль спрашивал и оператора, и регион ТОЛЬКО у платной ДаДаты, а без её полезного ответа выходил молча. На бою это убило две трети модуля: рассылки «по своей базе» и «списком руками» не отправляли ни одного сообщения — причина skipped_unknown_operator, живой прогон 03.08.2026. Теперь порядок такой: сперва ДаДата — только она знает про перенос номера к другому оператору, — затем справочник phone_ranges, который дозаполняет оставшееся пустым и никогда не затирает уже известное. Тем же справочником страхуется приём лидов от поставщика. Починено в трёх местах: обогащение своей базы номеров, догон снимка рассылки и предпросмотр. Предпросмотр врал клиенту «ждут уточнения региона» про номера, которые справочник знает наизусть, и смету на них не показывал. Исчерпанный дневной лимит ДаДаты больше не обрывает догон целиком: бесплатный справочник спрашивается всё равно, и остаток списка не ждёт следующего часа. Сторож считает ЗАПРОСЫ: предпросмотр списка на 20 000 номеров, спрашивая справочник по одному номеру, давал 20 044 запроса и 22 секунды на одно нажатие кнопки. Пачками — 64 запроса и 4,6 секунды. Тесты писались первыми и проверены красными; сторож пачечного запроса проверен вырезанием. Модуль СМС 399 из 399, статанализ 0. |
||
|
|
4a439ebb80 |
merge: правка телеграм-смены от 01:59 сведена — на бою снова обе работы
Вторая смена выложила на боевой свою свежую правку в 01:55–02:00, поверх выката
СМС-модуля. Их выкладка заменила сборку экранов целиком, а СМС-модуля в их ветке
нет — в результате экраны клиентских СМС на бою пропали из сборки, хотя код,
маршруты и таблицы остались целы. Их телеграм-правка при этом работала.
Сверка байт в байт показала: расходились ровно 5 файлов, все из их записи
|
||
|
|
f2ac3f4c7f |
fix,телеграм: подсказка обещала цену вдвое ниже настоящей + образец файла для базы номеров
Приёмка владельца на боевом, два замечания из трёх (третье — про рекламный кошелёк и заморозку — отложено, кусок большой). 1. Подсказка «?» у поля медиа обещала «600 ₽ за тысячу с картинкой, 680 ₽ с видео». Клиент платит 1008 и 1142,40 ₽. Числа были вбиты в текст руками и протухли в ту минуту, когда миграция client_tg_cena_po_media поменяла тариф. Лечение в корень, а не подстановкой верных чисел: цену называет тот, кто её считает. Ручка оценки отдаёт ceny_za_tysyachu по каждому виду медиа (себестоимость × наценка), подсказка собирается из них функцией podskazkaProMedia. Пока сервер не ответил — текст без единой цифры: подставить «примерные» числа значило бы вернуть ровно эту беду. Прайс держим отдельно от охвата: охват на ошибке гасим (устаревший хуже никакого), а цены от настроек аудитории не зависят — иначе подсказка мигала бы на каждой опечатке в поле. 2. Замечание дословно: «нету скачать файл с примером как надо заполнить для нас файл». Кнопка «Скачать образец» рядом с полем загрузки — подпись клиент читает уже ПОСЛЕ того, как файл отклонили. Пять строк, написания разные (с плюсом, с восьмёркой, со скобками), номера синтетические 7999. Заголовка-строки в образце намеренно нет: разборщик нормализует первый столбец КАЖДОЙ строки, и слово «Телефон» попало бы в «не похоже на номер» — наш собственный образец показал бы клиенту ошибку. Проверено вырезанием, а не только зелёным: - вернул в подсказку вбитые 600/680 — покраснели 4 датчика, включая тот, что прямо запрещает эти два числа; - вставил в образец строку-заголовок — покраснели 3. Полный прогон поймал две мои же поломки, обе настоящие: - значок mdi-file-download-outline на новой кнопке ОТСУТСТВОВАЛ в карте Lucide — на экране стал бы вопросом в кружке. Поймал сторож значков, у которого вчера опустошили список поблажек. Добавлен (Download — точного «файла со стрелкой» в Lucide нет); - датчик подсказок ждал PODSKAZKI.media строкой, а её больше нет. Замеры: экраны 245 файлов / 1870 тестов / 0 падений; телеграм-модуль 327 / 0; статанализ 0; формат чисто; типы 6 — все в чужих файлах, столько же было до; сборка 3,80 с. Счётчик в phpstan-baseline сдвинут 13→15 (новые тесты на Pest), diff проверен глазами: изменилась ровно эта строка. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7ad9e0a66 |
merge: телеграм-реклама второй смены сведена в рабочую ветку перед выкатом СМС
Замер боевого перед выкатом СМС-модуля показал мину: на liderra.ru сейчас работает код ветки fix/tg-zagolovok-obyavleniya — 33 записи телеграм-рекламы, которых не было ни в main, ни в рабочей ветке. Сверено слепками файлов: config/client_tg.php и TelegramTariffService.php на бою совпадают с их версией и расходятся с нашей. Экраны портала собираются одним куском, поэтому выкат СМС в прежнем виде откатил бы их живую рекламу назад. Решение владельца — сперва забрать их работу к себе. Разрешено пять склеек, все — сохранением обеих сторон: - Tariff.php: их пояснение про закупочную цену плюс наша строка для подсказчика типов; - phpstan-baseline.neon: взята наша вычищенная версия. Их 260 строк заметания не возвращены — статанализ после сведения дал 0 без них, потому что чинили мы не baseline, а сам механизм, и он вылечил их новые тесты тоже; - advertising-telegram-view.spec.ts: наш типизированный мок оставлен; - CHANGELOG_schema.md: столкновения номеров НЕ было — у нас v9.33/v9.34, у них v9.64/v9.65. Обе пары сохранены, ничего не двигали; в файл дописано, что дыра v9.35–v9.63 это след их завышенного замера, а не потерянные записи; - STATUS.md: служебный файл наблюдателя, взят свежий. Что сведение вскрыло дополнительно: - их сторож значков поймал НАШИ три имени с экранов СМС — mdi-file-sign, mdi-account-badge-outline, mdi-file-download-outline в карте Lucide отсутствовали и на бою рисовались бы вопросом в кружке. Добавлены по смыслу; - проверка типов фронта: 1 ошибка стала 0. Образец имени отправителя в тесте админки не задавал два поля, и Partial подмешивал в них undefined. Проверено после сведения: PHP 4614 тестов, 4610 зелёных, 4 пропущено, 0 падений; экраны 250 файлов, 2018 зелёных, 3 пропущено, 0 падений; статанализ 0 через composer stan; проверка типов 0. Унаследованное, не мной внесённое и не тронутое: линтер фронта показывает 1 замечание на неразрывный пробел в advertising-sms-view.spec.ts — знак там нужен по смыслу проверки, было до сведения. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
701e973251 |
fix: тест карточки клиента падал каждый вечер после 21:00 по Москве
Тест показателей карточки клиента в портале отдела продаж клал время сделки московской строкой без пояса, а столбцы времени — timestamptz, и сеанс базы живёт в UTC. Строка «02.08 23:05» читалась как 23:05 UTC, то есть как 03.08 02:05 по Москве — уже за верхней границей периода, которая равна 03.08 00:00 МСК, то есть 02.08 21:00 UTC. Отсюда ноль сделок вместо одной. Днём эта ошибка не видна вовсе: тест зелёный с утра до девяти вечера и красный после. Утренний прогон 02.08 был честно зелёным — просто было утро. Ровно этот класс чинился 02.08 в самом отчёте (App\Support\MskBoundary), но в приборе, который его проверяет, беда осталась. Здесь чиним прибор, а не показания: мгновение кладётся в UTC. Приёмка вырезанием: до правки тест падал и в полном наборе, и в одиночку; после — 8 из 8. Полный набор целиком: 4533 теста, 4529 зелёных, 4 пропущено, ни одного падения. Та же схема замечена ещё в нескольких тестах отдела продаж, но сейчас они зелёные — значит период у них устроен иначе. Вслепую чинить то, чего не видел красным, не стал; список передан следующей смене. |
||
|
|
36f5093692 |
merge: общая ветка с клиентским СМС-модулем сведена в рабочую
Общая ветка переведена вперёд на ветку клиентских СМС — перемоткой, без слияния, поэтому конфликтов там быть не могло. Затем общая сведена в рабочую ветку, которая отставала на 79 записей. Столкновение было одно и знакомое — словарь орфографии cspell-words.txt. Разрешено правилом «обе стороны настоящие»: слова обеих веток сохранены, ничего не выброшено. Журнал схемы БД на этот раз свёлся сам, столкновения номеров не было. СТОРОЖ ПДн ОСТАНОВИЛ ЗАПИСЬ И БЫЛ ПРАВ. В образце базы номеров, который клиент скачивает перед своей первой рассылкой, стоял рабочий телефон владельца — в двух видах. Это не утечка чужих данных: тот же номер публично опубликован в реквизитах ИП по требованию ЮKassa. Но клиент заполняет этот файл своими номерами и запускает рассылку — забытая строка означала бы СМС владельцу за деньги клиента. Заменён на выдуманный из тестового диапазона. Подсказки на экране рассылок («+7 999 123-45-67») выдуманы изначально; разрешены в .gitleaks.toml ПО ЗНАЧЕНИЮ, а не по файлу, чтобы сами файлы остались под охраной. Сторож проверен вырезанием: подложенный номер, не подпадающий ни под одно разрешение, он поймал; после снятия подложки — чисто. Служебный счётчик наблюдателя убран в тайник на время сведения и возвращён после — он машинный и пересоздаётся хуками. Файлы второй смены, которая работает в этой же папке, не тронуты и в слияние не попали. Проверки сведённой ветки: тесты 4533, зелёных 4529, падений нет; экраны 238 файлов и 1906 зелёных, сторож сети не сработал ни разу; статанализ 0; формат чист. |
||
|
|
00cc072e2f |
feat,телеграм-реклама: «Моя база номеров» заработала — пункт больше не ведёт в никуда
Это снимает запрет на выкат телеграм-рекламы. Пункт «Моя база номеров» стоял в выборе аудитории с 27.07, а писать в таблицу client_tg_contacts не умела НИ ОДНА строка кода: ни экрана загрузки, ни серверной ручки, ни переноса из сделок. У любого клиента пункт всегда показывал ноль. Клиент выбрал бы «свою базу», увидел пустоту и решил, что портал потерял его клиентов. Песочница выключена, деньги живые — катить в таком виде было нельзя. Читающая половина при этом была готова с самого начала: TelegramAudienceService умеет и нормализацию, и схлопывание дублей, и вычитание стоп-листа. Не хватало только записи, поэтому работа вышла куда меньше, чем казалось. Сервер: - TelegramBazaService — разбор файла, замена базы, очистка. Номер берётся по одному в строке либо первым столбцом таблицы, разделители запятая, точка с запятой, табуляция. Мусорные строки не роняют разбор, а считаются отдельно. - три ручки: GET, POST и DELETE /api/telegram/contacts. - потолок 200 000 номеров и 10 МБ на файл; обрыв по потолку не молчит, а возвращается признаком. Экран, в шаге «Кому показываем»: - сколько номеров в базе, заливка файла, очистка; - итог заливки целиком: принято, повторов, не похоже на номер. «Принято 1200» без остального читалось бы как «файл зашёл полностью»; - после заливки счётчик охвата пересчитывается сам. Решения, которые стоит знать: - заливка ЗАМЕНЯЕТ базу, а не добавляет. Так предсказуемее: клиент держит базу у себя и заливает заново. Подмешивание копило бы номера, от которых он не смог бы избавиться — построчного удаления в интерфейсе нет. На экране это написано до нажатия, молчаливой потери нет. - файл без единого годного номера отклоняется, старая база остаётся цела. Иначе клиент залил бы файл не того формата и потерял всё. - замена сделана удалением и вставкой, а не upsert: право UPDATE на таблице не выдано, upsert упёрся бы в это на бою и молча правил бы ноль строк. - наружу отдаём только счётчики. Номера — персональные данные, экрану они не нужны и в ответах не появляются. Приёмка: 12 тестов сервера и 9 тестов экрана, все до кода и все красные по верной причине — сервер отвечал 405, блока на экране не было. Замеры: телеграм-модуль 325 тестов 0 падений, экраны 244 файла 1855 тестов 0 падений, статанализ 0, форматтер чисто. Проверка типов 6 ошибок, все чужие, столько же было до работы. Для выката, проверить на бою: право USAGE на счётчике client_tg_contacts_id_seq. Замерил в тестовой базе — счётчик без права, но ровно так же выглядит и счётчик client_tg_campaigns_id_seq, в который портал на бою пишет. То есть новых прав эта работа не требует, но проверка дешёвая, а пропущенный grant на бою даёт отказ, которого на dev не видно. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3864f6f11a |
feat,телеграм-реклама: цена показа по виду медиа вместо ступеней по объёму
Мы продавали дешевле, чем покупали. Тариф давал скидку за объём — 0.45 → 0.36 ₽ за показ, — а у МТС такой скидки нет: прайс кабинета, стр. 13, берёт 0,48 ₽ за показ плоско. Скидку давали мы, а нам её не давал никто: на объёме от 50 000 наценка 1.40 превращалась в 5%. Второе. Кабинет показывает CPM БЕЗ НДС — колонка списка так и названа. Счёт кампании 2231134 сошёлся: 420 × 400 ₽/1000 × 1,2 = 201,60 ₽. Мы ИП на УСН, НДС не возмещается, это расход. Себестоимость с НДС: 0.480 без медиа, 0.720 с картинкой, 0.816 с видео. Третье. Вид медиа до расчёта не доходил вовсе — объявление с видео продавалось по цене объявления без картинки. На видео уходили в минус до 176 ₽ с тысячи, и портал нигде свою цену с ценой МТС не сравнивал. Песочница выключена с 02.08 — деньги живые. Что сделано: - миграция client_tg_cena_po_media: min_qty → media_kind, точность цены 6,2 → 6,3, три строки вместо пяти ступеней, media_kind на кампании и авто-правиле; - вид медиа определяется по СОДЕРЖИМОМУ файла, а не по расширению имени; - экран админки переделан: три фиксированные строки по видам медиа, рядом цена за тысячу для сверки с кабинетом, добавлять и удалять нечего; - запись v9.65 в журнале схемы. Замеры этой смены: телеграм-модуль 313 тестов 0 падений, экраны 242 файла 1841 тест 0 падений, статанализ 0, форматтер чисто. Проверка типов — 6 ошибок, все в чужих файлах, были до нас. Тест экрана тарифов проверен вырезанием, а не только зелёным: сломал передачу media_kind — красный; сломал расчёт цены за тысячу — красный. Осталось незакрытым: числа 0.720 и 0.816 — вывод по правилу «кабинет пишет без НДС», а не замер. Счёт кампании с картинкой живьём не снимали, шаг «Стоимость» недостижим без загрузки живых номеров. Замерите — правится одной строкой. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
15eadfde63 |
merge: подтянул общую ветку в клиентские СМС — 4 записи отставания закрыты
Общая ветка принесла правки по разведке Яндекса: список рисует только видимые строки, упавшее задание больше не сгорает, у придержанного объявления не бывает окна причины, плюс четыре промта смен 01-02.08. СМС-кода эти правки не касаются. Столкновение одно — словарь орфографии: обе стороны дописали слова в конец. Обе стороны настоящие, склеены обе, ни одно слово не выброшено. Проверки после сведения: статанализ 0, форматирование СМС и рекламы passed, прогон 423 теста и все зелёные — тесты рекламы, которые принесла общая ветка, плюс весь модуль клиентских СМС. Ветка по-прежнему в main НЕ влита, не пушена, на боевой не выкачена. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
76cb44b91d |
feat(телеграм-реклама): пачка 4б — мастер шагов, живой счётчик охвата и серверная ручка оценки
Замечание владельца: «мастер шагов вместо простыни» и «живой счётчик охвата вместо кнопки Рассчитать». Форма была одна длинная, охват узнавался только по нажатию кнопки. Экран: - мастер из четырёх шагов: Кому показываем → Объявление → Деньги → Проверка; - живой счётчик охвата на первом шаге, пересчитывается сам при смене источника людей, периода или списка номеров (с задержкой, чтобы не дёргать сервер на каждую букву); - кнопки «Рассчитать» больше нет: «Запустить» создаёт кампанию, прикладывает картинку и отправляет в кабинет одним нажатием; - на шаге «Деньги» рядом смета и остаток баланса — нехватка денег видна ДО запуска, а не отказом после; - сводка на последнем шаге повторяет всё выбранное. Сервер — новая ручка POST /api/telegram/campaigns/estimate: 🔴 Живой счётчик обязан считать на каждое изменение поля, а охват до сих пор возвращала только POST /campaigns — и она СОЗДАЁТ черновик в базе. Через неё счётчик наплодил бы десятки кампаний-призраков в списке клиента. Новая ручка только читает: ни записи, ни денег, ни робота. На это стоит отдельный тест. Ручка считает ТЕМ ЖЕ кодом, что и создание кампании (общее тело выборки из сделок вынесено на оба входа TelegramAudienceService). Отдельный тест сверяет два ответа: разойдись они — клиент видел бы на экране одно число, а платил по другому. 🔴 Запуск запрещён, когда людей меньше 367 — правило площадки, не наше. Тот же предохранитель стоит на сервере (AudienceGateTest); на экране он объясняет заранее, вместо отказа после оплаты. Новых прав в базе ручка не требует: читает строго то же, что уже читает создание кампании, и не пишет никуда. Тесты: +EstimateApiTest (11 на PHP), +telegram-master-shagov (27 на экранах). 14 прежних проверок формы переехали в набор мастера вместе с кодом — сперва переписаны на новом месте и там проверены, потом убраны со старого. Устарели по смыслу только две («Рассчитать создаёт черновик», «правка сбрасывает смету»): считать вручную больше нечего, устареть расчёту негде. Замер: экраны 240 файлов, 1829 тестов, 0 падений (было 239/1813); телеграм- модуль на PHP 292 теста, 0 падений (было 281); статанализ 0 (базовая линия пересобрана — добавлен только новый Pest-файл, удалений нет); типы 6 чужих ошибок вместо 7; pint чист. На боевой НЕ выкачено. Глазами не принято — приёмка в пачке 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a2496dfe61 |
feat(реклама): честные статусы кампаний — экран больше не зовёт «Крутится» рекламу с нулём показов
Замечание владельца З-6: «почему статус крутится, когда она отклонена?» Живой замер кампании #6 на бою: статус running, зелёное «Крутится», показов доставлено НОЛЬ, потрачено 0 ₽, заморожено 3 333,36 ₽ — третьи сутки. Главное, что вскрылось: экран физически не мог показать правду. Список кампаний отдавал estimated_impressions («сколько обещали») и не отдавал delivered_impressions («сколько было»), хотя колонка есть. Чисел принятых/отклонённых объявлений тоже не было. Поэтому чинили с сервера, а не с подписей. - сервер отдаёт факты: delivered_impressions + счётчики объявлений (всего/принято/ отклонено), ОДНИМ запросом на весь список — на N+1 поставлен отдельный датчик; те же счётчики доезжают и в отчёт по кампании; - считаются только включённые в показ: снятое галочкой в Яндекс не уезжает и в знаменателе «2 из 15» ему не место; - ярлык по фактам: running при нуле показов → «Принято, показов пока нет» нейтральным цветом; пошли показы → зелёное «Крутится»; часть отклонена → приписка «часть объявлений отклонена — 2 из 15»; все → красное «Отклонено»; - правило перехода статусов НЕ тронуто: на rejected висит возврат заморозки (AdWalletService::release, «ВЫХОД 2»). Чинили то, что видит человек; - подписи собраны в один файл (composables/campaignStatusMeta.ts). Их было ДВЕ копии — в списке и в отчёте — и они уже разъехались; разъехавшиеся копии и есть та разница между экранами, на которую жалуется владелец; - телеграм-экран: подписи вынесены отдельно и приведены к тем же словам («На модерации в МТС» ↔ «На модерации в Яндексе», «Отклонено» на обоих); голое «Запущена» → «Запущена в кабинете МТС». Граница честности: у телеграм-модуля нет счётчика показов ВООБЩЕ — actual_cost_rub в таблице есть, но её не пишет ни одна строка кода. Поэтому там нельзя сказать ни «крутится», ни «показов пока нет»: это была бы выдумка того же сорта. Записано открытым вопросом владельцу в файле замечаний. Проверено: 435 тестов рекламы Яндекса, 281 телеграма, 1771 тест экранов (235 файлов, 3 пропущено), статанализ 0, формат чист. Новых тестов 18. Глазами НЕ принято (приёмка — пачка 5), на боевой НЕ выкачено. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7922bffb6d |
feat(телеграм-реклама): заголовок и картинка в авто-режиме + честная проверка медиа по требованиям МТС
Пачка 1 замечаний владельца о едином виде рекламных экранов. Песочница выключена 02.08.2026 — каждая дыра ниже стоила живых денег. 1. Заголовок объявления в АВТО-правиле. Кабинет МТС требует его для рекламы сайта; утром 02.08 поле довезли до разовой формы, а в авто его не было вовсе — накопитель создавал кампанию без ad_headline, и она сгорела бы в кабинете уже после списания. Колонка client_tg_auto_rule.ad_headline, приём в контроллере (обязателен только при включённом авто и не-телеграмной ссылке), перенос в кампанию, поле на экране. 2. Картинка/видео в авто-правиле. Колонка media_path, отдельная загрузка POST /api/telegram/auto-rule/media, перенос в кампанию, поле на экране. 3. Проверка медиа переписана по настоящим требованиям кабинета, снятым глазами 02.08. Было mimes:png,jpg,jpeg,gif,mp4|max:51200 — врало по пяти пунктам: принимало GIF, пропускало вдвое больший вес, не смотрело пиксели и длительность, зря отказывало в mov/webm. Стало правило App\Rules\ClientTg\MtsMedia: картинка JPEG/PNG до 25 МБ и 640x360...5120x2880, видео до 20 МБ, 3-55 секунд, от 640x360. Формат картинки определяется по содержимому, длительность и кадр видео читает App\Support\Mp4Probe из контейнера — без внешних программ. Отказ человеческий: «Картинка слишком маленькая: 300x200. Нужна не меньше 640x360». 4. Цена показа с медиа — 600 руб. с картинкой, 680 с видео — теперь видна ДО загрузки файла, на обоих экранах. Сверх плана, найдено по дороге: 5. Предохранитель накопителя: правило, сохранённое до 02.08 со ссылкой на сайт и пустым заголовком, всё равно ушло бы в кабинет. Теперь такая пачка держится черновиком, в журнал пишется причина no_headline. 6. Отказ сервера доходит до клиента его словами. Оба экрана глушили ответ общей фразой «Не удалось рассчитать кампанию», и человек не понимал, что не так с файлом. Границы честности: длительность и размер кадра читаются только у mp4/mov/m4v; у webm/mkv/mpeg/wmv проверяются формат и вес — так и записано в Mp4Probe. Проверено: 281 тест телеграм-модуля, 1753 фронтенд-теста, статанализ 0, Pint чист. Глазами НЕ принимали — приёмка живьём в пачке 5. На боевой не выкачено. Журнал схемы: v9.64 — номер взят как максимум по всем веткам плюс один, чтобы не повторить столкновения 29.07 и 01.08. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bffca85399 |
docs+fix: канон схемы догнал миграции — 39 таблиц описаны, сверка считает обе стороны
Канон знал 82 таблицы и 995 столбцов, а база — 121 и 1470: не описаны были рекламный модуль, телеграм-рассылки и весь отдел продаж. Главное, что выяснилось по дороге: db/schema.sql не документ, а исполняемый файл — миграция 0001_01_01_000000_load_initial_schema заливает его целиком первым шагом сборки. Перенести туда DDL модулей нельзя: те же таблицы создают дельта-миграции, их 77 и 49 из них без стражей. Сборка с нуля падала дважды. Поэтому канон разбит на два файла: schema.sql исполняется, новый schema_modules.sql только описывает. Проверено не глазами: - сборка с нуля 148 из 148 DONE; - столбцы 1470 против 1470, ноль расхождений по имени, типу, длине и NULL; - все четыре счётчика расхождений инструмента — ноль; - schema.sql отдельно исполняется без единой ошибки, ограничения 495=495, политики 64=64; - статанализ 0. Инструмент сверки усилен: считал только одну сторону и потому не видел переименования jivo_chat_id в chat_id — старое имя жило в каноне месяц. Теперь обе стороны, канон из нескольких файлов, понимание RENAME/DROP COLUMN и DROP TABLE. Починен разбор переносов строки внутри определения столбца. Сторож SchemaDeltaTest дополнен: файл модулей обязан быть на месте и описывать 39 таблиц, а тело schema.sql их не содержать. Принят вырезанием. Найдено и НЕ исправлено, нужно решение владельца: задвоенный указатель на deals и пять политик без NULLIF в миграциях. Оба помечены в тексте канона. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1ed82b6b36 |
fix: сутки по всему порталу считаются с московской полуночи, а не с трёх ночи
Продолжение правки отдела продаж от 02.08: тот же класс ошибки найден ещё
в тринадцати местах. Граница периода, посчитанная по Москве, уходит в базу
надписью без смещения и читается как гринвичская — сутки съезжают на три часа.
У ошибки оказалось ДВА вкуса, и поиск из промта находил только первый:
1. граница по Москве, отданная как есть (дашборд клиента, списания,
расход на рекламу, карточка клиента у отдела продаж, сводка админа);
2. граница ВООБЩЕ без Москвы - now()->startOfDay() и Carbon::today();
пояс приложения гринвичский, значит день начинался в 03:00 МСК
(список лидов и выгрузка, биллинг, карточка клиента у админа,
посетители, ответ бота клиенту, сверка CSV, снимок рекламной кампании).
Правило названо в одном месте - App\Support\MskBoundary: instant() отдаёт
границу мгновением, dayAfter() - первое мгновение после дня (полуинтервал,
чтобы не терять последнюю секунду). Календарные даты и счёт дней НЕ трогали:
там нужен именно московский календарь.
Заодно в списаниях клиента убрана вторая копия условий периода - выгрузка
CSV фильтровала по своей копии, и та уже разъехалась с общей.
Проверка: tests/Feature/NightBoundaryMskTest.php - девять проверок с часами,
замороженными на 00:30 МСК. До правки девять из девяти красные, после - зелёные.
Две из них поначалу проходили и на сломанном коде: в 00:30 МСК гринвичский день
это ещё вчерашний, и ночное событие случайно попадало в окно; добавлено второе
событие "вчера днём", которое обязано остаться за бортом.
Три чужие проверки закрепляли старую ошибку и поправлены:
- DealIndexTest "конец дня" клал заявку в 23:30 по Гринвичу - по московскому
календарю это уже 02:30 следующего дня;
- ClientFactsTest и GuardCardContractTest строят заявки от now() и ночью
краснеют сами - часы заморожены на 12:00 МСК с возвратом в afterEach.
Статанализ: 0. Форматтер: чисто.
|
||
|
|
3c72560139 |
fix(телеграм): ложное «вход слетел» больше не хоронит кампанию клиента
Приёмка Г1 02.08.2026 прошла: задание прошло насквозь, робот создал кампанию 2234762 в кабинете МТС, заголовок объявления и ссылка проверены ГЛАЗАМИ на шаге «Объявление». Песочница, 445 номеров из 450 опознано, деньги не двигались. Следы пробы убраны, копия — app/storage/app/proba-priemki-2026-08-02.json. Приёмка вскрыла мину. Первое из двух заданий упало с «Вход слетел — нужен повторный логин», хотя вход был ЖИВ: тем же кодом робота минутой позже проверка ответила «да». Кабинет МТС завис на пустой странице, робот принял это за разлогин. Цена — одна осечка сразу хоронила кампанию: задание в failed, кампания в failed, второй попытки нет. На приёмке это 1 прогон из 2. Робот (bots/mts-telegram-ads): - ensureLoggedIn в src/session.js — три попытки с паузой 5с; сбой самой проверки считается попыткой, а не падением; - SessionLostError несёт признак retryable; - runner.js во всех трёх режимах (запуск, чтение вердикта, пересдача) зовёт ensureLoggedIn и протаскивает retryable в отчёт; - bin/poll.js протаскивает retryable из своей обёртки. Портал (app): - RobotResult читает retryable (только при ok:false); - TgRobotController::done на временный отказ возвращает задание в очередь (queued, taken_at=null) и кампанию НЕ трогает, пока attempts < max_attempts; - client_tg.robot.max_attempts, по умолчанию 3, env TG_ROBOT_MAX_ATTEMPTS. Считаются выдачи задания, а не отказы: тем же счётчиком attempts пользуется возврат по сроку аренды, поэтому бесконечного круга не выйдет. Тесты писались красными: портал 254/254 (720 проверок, +4 новых), робот 134/134 (+4 новых). Pint чист, статанализ 0 ошибок. В phpstan-baseline.neon дописаны 27 записей про $this в Pest-замыканиях — тем же порядком, что у соседнего TgRobotDoneTest. На боевой НЕ выкачено. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c8de85701 |
fix отдел продаж: сутки в отчётах снова считаются с полуночи, а не с трёх ночи
Живая ошибка на боевом. Пополнение клиента в 01:00 попадало во вчерашний день, а первые три часа первого числа месяца — в прошлый месяц. Руководитель видел деньги не за тот день, и ни ошибки, ни следа в журнале при этом не было. Причина. Границу суток портал считает по Москве, а в базу отправляет БЕЗ пояса — просто «02.08 00:00». Сеанс базы живёт по Гринвичу и читает эту надпись как гринвичскую, то есть как 03:00 по Москве. Замер прямым запросом: граница «полночь по Москве» превращается в базе в «три часа ночи по Москве». Лечение. Границы периода уходят в запрос МГНОВЕНИЯМИ, а не надписью на часах. Правило названо в одном месте — в описании периода (SalesPeriodRange) — двумя методами startInstant / endExclusiveInstant, и там же сказано, где их брать НЕЛЬЗЯ: для сравнения с календарной датой (paid_on) и для счёта дней нужен именно московский календарь, там перевод в UTC всё сломает. Почему это не ловилось раньше. Ошибка видна ТОЛЬКО ночью: тот же код в 23:55 давал зелёный прогон, в 00:20 — красный. Попалась случайно, прогон пересёк полночь. Оба теста теперь морозят часы на 00:30 МСК — то есть проверяют дыру в любое время суток, а не когда повезёт. Часы возвращаются в afterEach. Отдельная находка того же захода: ЧЕТЫРЕ проверки границ ЗАКРЕПЛЯЛИ ошибку. Они клали данные голой строкой (то есть по Гринвичу), а период строили по Москве — две ошибки гасили друг друга. Починил границу — они честно покраснели. Данные в них теперь тоже московские, и пояс пишется прямо в строке: объект даты по дороге в базу пояс теряет, а строка доходит как есть. Приёмка вырезанием в обе стороны: убрал перевод в мгновения — покраснели три проверки, включая обе денежные; вернул — папка продаж 481 из 481 зелёная. Полный прогон: 4111 тестов, 4107 зелёных, 4 пропущено, 0 падений. Статанализ 0. После полного прогона база чиста — сторож чистоты подтверждает. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a5c278adaf |
test: рекламная папка и журнал вебхука перестали оставлять записи — остаток 44 → 0
Прогон рекламной папки оставлял в базе 44 записи. Теперь ноль, папка целиком
зелёная — 351 из 351. Полная tests/Feature: 3282 теста, 0 падений, и после неё
в базе не остаётся НИЧЕГО, кроме справочников, которые кладутся при сборке.
Два места прошлая смена считала неизлечимыми — оба вылечились, потому что
диагноз был не перепроверен:
1. CampaignBannerEndpointsTest оставлял 24 записи, больше половины всей грязи.
Он нарочно ловит отказ базы, а после отказа внутри транзакции Postgres
глушит все следующие команды. Лечение — ловить отказ в ОТДЕЛЬНОЙ точке
сохранения: DB::transaction внутри уже открытой ставит savepoint и
откатывает только его. Приёмка вырезанием: убрал уникальный ключ в
миграции — тест покраснел ровно там, где должен; вернул — зелёный.
2. AdWalletUnderRealRoleTest. В промте: «откат противопоказан, он ходит
настоящей ролью базы». Оказалось — не противопоказан, смена роли идёт по
ТОМУ ЖЕ соединению и незакоммиченные записи видны. Тут была реальная
опасность, что откат обезвредит самого сторожа, поэтому вырезание делал
отдельно: убрал у службы установку контекста клиента — оба теста
покраснели. Значит сторож сторожит по-прежнему.
Журнал вебхука оставлял 4 строки: он пишется ВТОРЫМ соединением к базе, куда
обычный откат не дотягивается. Двум файлам добавлена общая связка соединений
(SharesSupplierPdo) — папка вебхука теперь оставляет ноль.
Прежняя оценка исправлена. В промте стояло «~20 файлов оставляют по одной-две
записи». На деле после уборки восьми главных грязь оставляли ДВА файла из всей
папки: 2 и 3 записи, сумма сошлась с наблюдаемыми 5 ровно. «Нет отката» и
«гадит в базу» — разные вещи: папка Autopodbor, где без отката 16 файлов, не
оставляет ни одной записи. Мерить надо остатком, а не поиском по тексту.
Боевой код не тронут: git diff по app/app/ пуст после каждой правки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2590c01477 |
test: половина грязи рекламных тестов убрана — остаток в базе со 116 до 44
Восьми файлам рекламной папки поставлен откат после теста. Проверено остатком в базе снаружи: папка оставляла 116 записей арендаторов, теперь 44. Папка целиком зелёная - 351 из 351. Полный прогон после уборки: 4111 тестов, 4107 зелёных, 4 пропущено, ноль падений - значит на эту грязь никто не опирался. Побочно прогон стал БЫСТРЕЕ: 12,9 минуты против 16,5. Один файл намеренно оставлен как был, причина записана прямо в нём: CampaignBannerEndpointsTest нарочно ловит отказ базы, а Postgres после отказа внутри транзакции глушит все следующие команды. С откатом файл падает - чинить надо сам тест, а не обёртку. Датчик пришлось расшифровывать, и это стоит помнить. Числа сперва выходили с минусами: файл, который базы не касается вовсе, «уносил» девять записей. Прямым замером выяснилось, что КАЖДЫЙ прогон начинается с пересборки базы - было 6 записей, стало 0 после теста про расписание. Значит разница «до/после» при переборе файлов = оставил этот минус оставил предыдущий. Расшифровал цепочкой, сумма сошлась со 116 ровно - на этом расшифровка и проверяется. Поимённый расклад по файлам перенесён в промт. Грабля на будущее, тоже в промте: uses() обязан стоять НИЖЕ подключений классов. Подключение действует с места объявления и ниже, поэтому вызов выше падает «класс не найден» - и падает сразу вся папка. Полное имя с ведущей чертой не спасает: pint САМ превращает его в подключение внизу и ломает работавший файл. Остаток долга измерен и НЕ закрыт: ~20 файлов оставляют по одной-две записи, среди них миграционные, которым откат может быть противопоказан по сути. По всей папке Feature без отката 96 файлов из 580 - корень общий, в app/tests/Pest.php строка RefreshDatabase закомментирована. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
82ffa17a24 |
test: денежная проверка приёмника МТС считает проводки своего клиента, а не всей базы
Тест «денег НЕ двигает» падал только в полном прогоне: ждал ноль проводок по всей таблице, а видел 19. Поодиночке был зелёный - значит виноват не он и не код приёмника. Виновник найден и назван: тридцать файлов рекламной папки идут БЕЗ отката. Откат им не выдаётся и оптом - в tests/Pest.php строка RefreshDatabase закомментирована. Их проводки остаются в базе и достаются соседям. По всей папке Feature таких файлов 96 из 580. Датчик, которым нашёл: после прогона папки смотреть остаток в базе снаружи, через psql. База пересобирается в начале прогона, поэтому всё, что осталось после - вина этого прогона. Три папки, четыре минуты, ровно те же 19 строк, и они видны поимённо: пополнения и заморозки рекламных кампаний. Правка узкая: считаем проводки СВОЕГО клиента. Смысл проверки не изменился - приёмник не имеет права двигать деньги клиента, чьё сообщение он тронул. Приёмка вырезанием: заставил приёмник вернуть деньги этому же клиенту - тест покраснел; убрал - позеленел. Боевой код не тронут, git diff по app/app пуст. Прогон трёх папок: 989 из 989 зелёных, при том что 19 чужих строк в базе так и лежат - тест к соседям больше не чувствителен. Сама грязь НЕ убрана: файлы без отката - отдельная работа, делать её вслепую нельзя, части тестов записанные данные нужны по существу. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b67467dffe |
test: два вечерних мигания тестов поставщика — часы заморожены, причина названа
Оба падения одной болезни: помощник кладёт слепок маршрутизации на дату,
активную СЕЙЧАС, а роутер спрашивает дату, активную ПОТОМ. С 21:00 МСК
активной становится завтрашняя - вечерний переворот заливки, - и даты
расходятся.
- AutoPauseFlowTest, тест «второе письмо через 65 минут»: прыжок на
65 минут переносил проверку через границу 21:00. Падал каждый вечер
в окне 19:55-21:00 МСК, в остальное время был зелёный. Пойман в 20:38.
- CsvWebhookRaceTest: файл кладёт ДВА слепка - на активную дату и
отдельно на завтра. После 21:00 это одна и та же дата, все четыре
теста файла падали на нарушении уникальности. Пойман в 21:02.
Проверено отдельно: падает и БЕЗ моих правок, беда была до меня.
Лечение одинаковое и уже применённое в SnapshotHelperTimeOfDayTest:
часы замораживаются на 09:00 МСК СЕГОДНЯ. Дата именно сегодняшняя,
не выдуманная в прошлом - ради партиций supplier_leads.
Обоим файлам добавлен возврат часов в afterEach: AutoPauseFlowTest
раньше оставлял часы сдвинутыми на +65 минут для всех соседей своего
процесса.
Сплошной замер по всем файлам, которые кладут слепок: явную вторую дату
передаёт ровно один - тот самый CsvWebhookRaceTest. Класс закрыт.
Приёмка вырезанием: подложил в боевой код неверное хранилище отсрочки
письма - тест покраснел; вернул - позеленел. Боевой код не тронут,
git diff по app/app пуст. Полный прогон ветки в вечернем режиме:
4111 тестов, 4107 зелёных, 4 пропущено, НОЛЬ падений.
Первая подложка приёмку прошла молча и была отброшена: файловое
хранилище само протухает по сдвинутым часам, поэтому прыжок времени
её обезвреживал. Годной оказалась только вторая.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b6a15c5bbf |
merge: подтянул общую ветку в клиентские СМС — 227 записей отставания закрыты
Ветка шла отдельно почти неделю и отставала на 227 записей, отставание росло
каждый день. Направление сведения — общая В ветку: перевод main владелец
отклонил, значит вливать в него нечего.
Девять столкновений, каждое разобрано по существу.
Журнал схемы: столкнулись НЕ три номера, как ожидалось, а ВСЕ - обе ветки
независимо заняли v8.96-v9.25 и v9.32 разным содержимым. Обе стороны
настоящие, выбросить нельзя ни одну, поэтому перенумерована ветка, а не
общая: 31 запись уехала в свободный диапазон v9.33-v9.63. Содержание не
тронуто - доказано сверкой с исходной версией через git, посимвольно.
Соответствие старых номеров новым вписано в сам журнал, чтобы старые
документы ветки оставались читаемыми. Прежняя пометка про "запас v9.32"
заменена: запас не спас, v9.32 в общей ветке тоже был занят.
Сборка тестовой базы: взята версия общей ветки. Она позже и доказана
замером - двумя шагами вместо migrate:fresh, который спотыкался на
типе-призраке и оставлял схему неполной.
Список слов орфографии сведён объединением: 2106 наших + 2169 общих дали
2173, ни одно слово ни с одной стороны не потеряно - проверено сравнением.
Расписание работ, маршруты экранов и админский слой: обе стороны добавляли
своё в одно место, оставлены обе.
Витрина рекламных каналов: каждая ветка сделала настоящим СВОЙ канал -
ветка СМС свой, общая Телеграм. После сведения настоящих три, заглушки
исключают все три. Сторож витрины принят вырезанием: убрал СМС из списка
настоящих - покраснел, вернул - позеленел.
СТОЛКНОВЕНИЕ ИМЁН, созданное самим сведением. Оба набора тестов объявляли
глобального помощника pollCampaign - свой в СМС (один довод) и свой в
Телеграме (от двух до четырёх). Две функции с одним именем в одном языке
не живут: пока ветки шли врозь, этого не видел никто. Помощник СМС
переименован в pollSmsCampaign. Проверено, что других таких пар в PHP-тестах
нет ни одной.
Статанализ ветки доведён с 674 замечаний до НУЛЯ, уровень не понижен и в
baseline не заметено ничего.
- 616 из 674 - ложный класс Pest, закрытый тремя узкими правилами; правила
перенесены из рабочей ветки, где владелец их уже принял;
- остальные 42 - свои, в новом коде ветки, и починены по существу:
задвоенный ключ массива в трёх тестах (след копирования - комментарий
оторвался от своей строки), врущие описания двух помощников (PHP сам
делает из ключа-номера число), сужение типа возврата, прятавшее от
анализатора свойства подставного отправителя, лишний знак вопроса и
четыре бесполезных перенумерования списка.
- Приёмка вырезанием: подложил несуществующий метод - анализатор назвал его
поимённо и покраснел; убрал - ноль.
Шапки 59 моделей обновлены пересборкой подсказчика и ОСТАВЛЕНЫ намеренно
(правка только в комментариях, проверено): без них анализатор не связывает
модель с описанием и не знает, что дата - это дата, а не строка. Откатил их
сперва по привычке - получил 15 замечаний про даты, вернул - ноль.
Орфография: 15 файлов проверено, 0 замечаний (смотрел и на число
проверенных файлов, не только на число ошибок). Добавлены три слова из имён
миграций ветки.
Заодно: алиас ruflo-core в списке имён сторожа реестра - плагин описан в
реестре групповым именем, сторож видел только машинное. Мостится ТОЛЬКО имя;
🔴 содержательный долг остаётся - реестр до сих пор зовёт ruflo изолированным,
хотя его разморозили 28.07. Это чинить отдельно, через claude-md-management.
Проверено: три фронтовых сторожа рекламы 18/18, статанализ 0, разметка 0,
орфография 0, синтаксис PHP чист. Полный прогон тестов ветки - отдельным
шагом, он ещё ни разу не делался.
NB: в журнале схемы есть задвоенные номера v8.26 (пять раз) и v8.64 (два) -
это досталось по наследству из общей ветки, ровно столько же их там и было.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
39f1d77895 |
fix разведка Яндекса: список рисует только видимые строки + упавшее задание больше не сгорает
Две мины, обе вскрыты замерами, обе роняли разведку молча.
1. СПИСОК ОБЪЯВЛЕНИЙ ВИРТУАЛИЗОВАН — «объявления нет» бывало ложью.
Вчерашний разбор §7.10 утверждал, что семь объявлений кампании 713175197 пропали
из кабинета. Это неверно. Три замера подряд:
- ads.get боевым ключом вернул ВСЕ 15, одна кампания, одна группа, State=OFF;
- разметка страницы держит 8 строк;
- сам кабинет отвечает своему списку "adsCount": 15, и в этом же ответе лежат
«пропавшие» номера.
Список рисует только видимые строки, и у него СВОЯ полоса прокрутки: колесо мыши
по странице его не двигает — двенадцать оборотов не сдвинули ничего, и это ровно
та проверка, что породила слова «прокрутка их число не меняет».
Лечение — dolistatDoObyavleniya: не дождались строки, крутим ящик списка шагами
по 600 точек, после каждого смотрим снова. Останов — нашли, упёрлись в дно или
кончились 25 шагов. Только после этого доклад «объявления в списке нет» — правда.
Приёмка глазами настоящим readRejection по живому кабинету:
17787641406 и 17787641505 из «пропавших» — принесли настоящую причину;
17787641506 — прежние 760 знаков через окно, старая дорога цела.
2. УПАВШЕЕ ЗАДАНИЕ РАЗВЕДКИ СГОРАЛО НАВСЕГДА.
Защита от дублей смотрела все задания по номеру объявления, не глядя на состояние,
и находила своё же упавшее. Одна осечка навсегда лишала клиента причины отказа,
а поднять разведку можно было только правкой боевой базы руками — так 01.08
и пришлось делать со всеми тринадцатью.
Теперь сбойное дублем не считается: поднимаем ТУ ЖЕ строку обратно в очередь,
счётчик попыток копится, потолок — три попытки. Строки в работе и успешно
закрытые не трогаем.
Сторожа: три новых на постановку заданий, два на долистывание. Третий сторож проверен
вырезанием — покраснел ровно на убранной защите. Статанализ ноль, тесты рекламы 354/354,
робот 88/88, орфография и разметка чисты.
Урок дороже самой починки: доклад робота — не замер. Ложная фраза робота за один шаг
стала ложным выводом о продукте и уехала в разбор и в промт следующей смене.
На боевой НЕ выкатывалось.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6e7c99f5ab |
merge: свёл ветку робота телеграма со своей — обе закрыты, готовятся в main
Владелец объявил закрытыми обе телеграм-ветки и велел свести их вместе, ювелирно и ничего не потеряв. Ветка робота несла 9 коммитов, которых у меня не было, и трогала 79 файлов. Конфликта три: - app/tests/Frontend/advertising-channels.spec.ts — настоящий, в коде. Обе стороны сторожили одно: у кого есть настоящий экран, а кто заглушка. Моя перечисляла живые площадки прямо в тесте, версия робота берёт их из общего списка REAL_ROUTES наверху файла и вдобавок проверяет, что заглушки вообще остались. Проверил список: там и Яндекс, и Телеграм — покрытие то же, проверка сильнее. Взял версию робота, файл вышел байт в байт как у него. Сторож прогнан отдельно, зелёный; - app/phpstan-baseline.neon — машинный, пересобран заново по проектной процедуре в два шага, phpstan.neon возвращён на место, итог 0 ошибок; - docs/observer/STATUS.md — машинный файл наблюдателя, взята своя версия, его всё равно переписывает хук. Доказательство, что ничьё не пропало — сверкой, а не на слово: - файлов, где результат отличается от версии робота, а я их НЕ трогал: ноль. То есть ни одна его правка не откатилась молча; - файлов, отличающихся от main: 79, и все 79 объясняются нашей работой. Необъяснённых ноль — значит чужая работа из main не затёрта. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
62d81e33f7 |
Merge remote-tracking branch 'gitea/main' into fix/tg-zagolovok-obyavleniya
# Conflicts: # docs/observer/STATUS.md |
||
|
|
9ef688fa8d |
merge: подтянул main после выката починок модерации Яндекса
Мой коммит уехал в main отдельно — пересадкой на актуальную вершину и оттуда на боевой. Возвращаю main к себе, чтобы ветка не разъезжалась. Пришло 9 коммитов, из них главное — работа соседней сессии по воронке продаж, уже влитая в main и запушенная. Конфликта два, оба в общих машинных файлах: - cspell-words.txt — сторону не выбирал, объединил. Все слова обеих сторон на месте; убрана ровно одна строка — мой же дубль «админский», который прошлый коммит добавил дважды. Проверено сравнением с версией до слияния: другого отличия нет; - docs/observer/STATUS.md — машинный файл наблюдателя со столбиком часов процессов, взята своя версия, его всё равно переписывает хук. Замечание на будущее: патч «минимум площадки считается по длине периода» живёт в двух коммитах — |
||
|
|
947cb3403a |
fix(воронка-продаж): сроки фильтра по датам — «Просроченные» отдельным пунктом, планы смотрят вперёд
Просьба владельца 01.08.2026: «убери вчера, наверх создай просроченные,
сегодня, завтра и т.д. и проверь что они правильно привязаны и реально
работают»; во втором режиме «убери завтра — завтра у тебя не может быть».
У каждого режима теперь СВОЙ список сроков, они не пересекаются:
Что надо сделать (вперёд): Просроченные / Сегодня / Завтра /
Ближайшие 7 дней / Ближайшие 30 дней / Произвольный
Что менялось (назад): Сегодня / Вчера / 7 дней / 30 дней / Произвольный
Каждый пункт показывает ровно то, что на нём написано. Раньше просроченное
подмешивалось в ЛЮБОЙ выбранный период, и «Сегодня» показывало не только
сегодняшнее — теперь это отдельный первый пункт (period=overdue), а подпись
«плюс всё просроченное» убрана за ненадобностью.
Вторая, невидимая глазом поломка: «7/30 дней» в режиме планов считались
НАЗАД (d7/d30) — «что надо сделать за прошедшую неделю». Добавлены зеркала
next7/next30 в SalesPeriodResolver.
Срок из чужого набора («что менялось завтра») сервер отвергает с 422, а не
подменяет молча текущим месяцем, как делал прежний резолвер по умолчанию.
Проверено:
- сервер: 30/30 (фильтр + резолвер), весь отдел продаж 498/498;
- фронт: 1739/1739 весь набор;
- приёмка вырезанием — подложил поломку в оба места, оба набора покраснели;
- глазами в браузере 1920×1080: списки сроков в обоих режимах, «Просроченные»
дают ровно забытые карточки, «Завтра» — ровно завтрашнюю (скрины 08–12).
Попутно починен чужой протухший тест advertising-channels: Телеграм давно
стал живым роутом, а тест продолжал считать его заглушкой и был красным.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
9552efc820 |
fix реклама Яндекса: три поломки модерации, вскрытые заходом в живой кабинет
Кампания 713175197 третьи сутки не показывалась при замороженных у клиента деньгах, а программный интерфейс на всех уровнях рапортовал «Идут показы». Правду отдала только подпись на экране кабинета: «Для показа в заданных регионах предоставьте документы» — 13 объявлений придержаны, 2 отклонены. 1. Яндекс отвечает машине ПО-АНГЛИЙСКИ, если не попросить русский. Замер боевым ключом, два одинаковых запроса подряд: без заголовка «Rejected at moderation.», с Accept-Language ru «Отклонено на модерации.». Английская строка уезжала клиенту в переписку и письмом как пояснение модератора, честная заглушка «причину выясняем» не срабатывала никогда, а на следующем обходе английская строка ложилась ПОВЕРХ доклада разведчика — проверено по боевой ленте: 30.07 в 15:02 робот принёс полную причину, в 17:00 её накрыло. Лечение: спрашиваем язык явно плюс второй заслон — английские отписки узнаются как отписки. 2. Отказ включения Яндекс кладёт в Warnings, а код смотрел только в Errors. Ответ целиком: ResumeResults с Warnings 10201 «Объявление не остановлено» при пустом Errors. Исключения нет, портал считал, что справился: двое суток обход каждые два часа поднимал 13 объявлений, ноль записей в журнале. Лечение: resumeAds возвращает, кого включить не дали. 3. Новый исход «принято, но придержано» порталу не был известен вовсе. Вердикт ACCEPTED, ярлыка «Отклонено» нет, разведчик не ходил — клиент видел «Идут показы» при нулевых показах. Теперь по отказу включения клиенту идёт сообщение «Яндекс принял объявление, но пока не показывает его. Причину выясняем» — текст выбран владельцем — и туда же едет разведка. Ловушка, обойденная по дороге: включение спрашиваем ДО разбора объявлений. Наоборот — сообщение о придержке легло бы поверх доклада разведчика, и обход начал бы чередовать их по кругу: защита от дублей смотрит на последнее сообщение. Замеры. Восемь новых сторожей, все написаны ДО починки и падали именно на живых данных. Отдельный сторож на противоположный случай — включённому объявлению разведку не заводим — был зелёным с самого начала. Портал 4083 теста, 4043 прошло, 16 упало: те же шесть давних классов и ровно те же числа, что до работы, было 4069/4029/16. Плюс 14 тестов — ровно новые. Pint и Larastan по изменённым файлам чистые. Главный урок записан в cabinet-flow.md §7.9: в §7.7 лежит правдивый РУССКИЙ ответ Яндекса, снятый ДРУГИМ инструментом, который язык просил. Разбор отписок построили по показаниям прибора, которым продукт не пользуется. Мерить надо той же дорогой, по которой ходит боевой код. На боевой НЕ выкатывалось. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7bfe5a46f |
fix реклама Яндекса: три поломки модерации, вскрытые заходом в живой кабинет
Кампания 713175197 третьи сутки не показывалась при замороженных у клиента деньгах, а программный интерфейс на всех уровнях рапортовал «Идут показы». Правду отдала только подпись на экране кабинета: «Для показа в заданных регионах предоставьте документы» — 13 объявлений придержаны, 2 отклонены. 1. Яндекс отвечает машине ПО-АНГЛИЙСКИ, если не попросить русский. Замер боевым ключом, два одинаковых запроса подряд: без заголовка «Rejected at moderation.», с Accept-Language ru «Отклонено на модерации.». Английская строка уезжала клиенту в переписку и письмом как пояснение модератора, честная заглушка «причину выясняем» не срабатывала никогда, а на следующем обходе английская строка ложилась ПОВЕРХ доклада разведчика — проверено по боевой ленте: 30.07 в 15:02 робот принёс полную причину, в 17:00 её накрыло. Лечение: спрашиваем язык явно плюс второй заслон — английские отписки узнаются как отписки. 2. Отказ включения Яндекс кладёт в Warnings, а код смотрел только в Errors. Ответ целиком: ResumeResults с Warnings 10201 «Объявление не остановлено» при пустом Errors. Исключения нет, портал считал, что справился: двое суток обход каждые два часа поднимал 13 объявлений, ноль записей в журнале. Лечение: resumeAds возвращает, кого включить не дали. 3. Новый исход «принято, но придержано» порталу не был известен вовсе. Вердикт ACCEPTED, ярлыка «Отклонено» нет, разведчик не ходил — клиент видел «Идут показы» при нулевых показах. Теперь по отказу включения клиенту идёт сообщение «Яндекс принял объявление, но пока не показывает его. Причину выясняем» — текст выбран владельцем — и туда же едет разведка. Ловушка, обойденная по дороге: включение спрашиваем ДО разбора объявлений. Наоборот — сообщение о придержке легло бы поверх доклада разведчика, и обход начал бы чередовать их по кругу: защита от дублей смотрит на последнее сообщение. Замеры. Восемь новых сторожей, все написаны ДО починки и падали именно на живых данных. Отдельный сторож на противоположный случай — включённому объявлению разведку не заводим — был зелёным с самого начала. Портал 4083 теста, 4043 прошло, 16 упало: те же шесть давних классов и ровно те же числа, что до работы, было 4069/4029/16. Плюс 14 тестов — ровно новые. Pint и Larastan по изменённым файлам чистые. Главный урок записан в cabinet-flow.md §7.9: в §7.7 лежит правдивый РУССКИЙ ответ Яндекса, снятый ДРУГИМ инструментом, который язык просил. Разбор отписок построили по показаниям прибора, которым продукт не пользуется. Мерить надо той же дорогой, по которой ходит боевой код. На боевой НЕ выкатывалось. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4efd3d3329 |
merge: подтянул воронку продаж с основной - иначе выкат откатил бы блок менеджеров
Владелец спросил, не потрём ли мы соседнюю смену. Проверил - потёрли бы. Наша ветка отстала от основной на 7 коммитов, и это ровно сегодняшняя работа по воронке продаж: корзина, журнал движений карточки, фильтр по датам, дробь у Отказа. Выкат по плану был ПОЛНЫЙ заливкой файлов - боевой сайт получил бы портал без всего этого. Соседи выкатывали точечно, шестью файлами, поэтому у них и не рвануло. СТОЛКНОВЕНИЕ НОМЕРОВ ЖУРНАЛА СХЕМЫ, ТРЕТИЙ РАЗ ЗА СУТКИ. Час назад я записал v9.31 за заголовок объявления. В это же время соседи записали v9.31 за корзину воронки и влили в основную. Обе записи настоящие. Git конфликта НЕ ПОКАЗАЛ - просто склеил две записи с одинаковым номером в один файл, молча. Наша подвинута на v9.32, боевая осталась на своём номере. Пересобран список исключений статанализа: после сведения он врал счётчиком 90 против 95 и не знал новых файлов соседей. Проверено, что пересборка ничего настоящего не спрятала - все 16 замечаний были одного вида, ложная тревога PendingCalls, плюс одно про форму данных внутри задания робота, тоже в тесте. ПОЧИНЕН ВРУЩИЙ СТОРОЖ ВИТРИНЫ РЕКЛАМНЫХ КАНАЛОВ. Тест утверждал, что настоящий экран есть только у Яндекса, а Телеграм получил свой ещё 27.07 - сторож пять дней держал фронтовый набор красным, и этого никто не видел, потому что фронт целиком не гоняли. Список настоящих каналов ведётся руками намеренно: смысл сторожа - поймать случайно прописанный маршрут у канала-заглушки. Приёмка сторожа вырезанием: на честном коде проходит, на подложенном маршруте для ВК падает, после возврата снова проходит. Файл каналов вернулся байт в байт. Замеры после сведения: - портал 4091 тест, 4087 прошло, 0 падений, 0 ошибок - фронт 232 файла, 1739 тестов, 0 падений - статанализ 0 замечаний - робот 130 из 130 На бой ничего не выкачено. Боевой сайт не тронут. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19d9eda368 |
fix сверка: не кричим про расхождение, пока оператор просто ещё не ответил
Находка приёмки Этапа 5. Сверка считала счёт оператора «известным», если отчиталось хотя бы ОДНО сообщение из всей рассылки, а разницу брала против нашего расхода по ВСЕМ. Живой замер на стенде: оператор отчитался по 10 сообщениям из 120 — экран написал «наш расход 360.00 руб, счёт оператора 60.00 руб, разница минус 300.00 руб». Человек прочитает это как «МТС недосчитал 300 рублей». Правда другая: МТС ещё не отчитался по 110 сообщениям. Отчёты приходят постепенно, опрос ходит раз в десять минут — значит такое состояние было бы у КАЖДОЙ свежей рассылки. Это тот же класс, что уже ловили на вебхуке поставщика: сторож, не умеющий отличить «потеряно» от «ещё в пути», штампует ложные тревоги и заставляет чинить несломанное. Что сделано: - разница считается от нашего расхода ПО ОТЧИТАННОМУ, то есть сравнимое со сравнимым. Для этого запрос отдельно складывает наши части только по тем сообщениям, за которые оператор назвал цену; - «наш расход» слева остался прежним — это сколько мы должны за всю рассылку, и число полезное. Рядом с разницей экран пишет охват: «оператор отчитался по 10 из 120». Без охвата разница непонятна: не видно, по всей ли рассылке она; - охват пишется ТОЛЬКО когда отчитались не по всем, иначе строка шумела бы всегда; - настоящее расхождение по деньгам видно и при неполном отчёте — ждать полного отчёта, чтобы заметить, что оператор считает дороже, было бы хуже исходной беды. Порядок работы соблюдён. Четыре теста сервера и два теста экрана написаны ДО правки и покраснели на отсутствующих полях. Каждый доказан вырезом, вырезов четыре: вернул разницу к расходу по всей рассылке - покраснели 2; убрал охват из ответа - покраснели 3; убрал строку охвата с экрана - покраснел 1; показал охват всегда - покраснел другой 1. Это пара: один тест стережёт «видно», второй «не шумит». Прогоны: модуль 387 из 387, фронт 222 файла и 1722 зелёных при 3 намеренно пропущенных, формат чист, типы - 5 ошибок и до правки, и после, все в чужих файлах. Статанализ добавил 4 замечания одного ложного класса: анализатор не понимает $this внутри тестов Pest и не видит getJson. Доказательство ложности прямое - эти самые тесты проходят, не будь метода, они бы падали. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1bd33c09cf |
fix: возврат чужой работы, которую откатило моё сведение
Перед слиянием с main я откатил 4 файла к своей старой версии, чтобы
сдвинуть с места застрявшее слияние — и слияние закрепило этот откат.
Пострадала починка от 29.07: чтение связок поставщика из pivot (без неё
5 из 9 работающих проектов показывали жёлтое «Готовим к запуску» при
живом заказе) и два набора тестов вебхука/сверки CSV.
Файлы возвращены к состоянию main
|
||
|
|
2fc844756c |
fix деньги: заморозка под боевой ролью больше не зависит от памяти человека
Находка приёмки Этапа 5. В Этапе 4 уже находили, что рабочей роли не выдано право на счётчик номеров таблицы заморозок ad_wallet_holds_id_seq, из-за чего заморозка денег под боевой ролью не проходила ВОВСЕ. Тогда стенд починили руками и переписали памятку выката. Приёмка проверила это на базе, собранной ТОЛЬКО миграциями — то есть на точной модели того, что получит боевой после выката. Права там снова нет: починка жила лишь в памятке. Ни одна миграция её не несла. Цена промаха денежная и широкая: заморозка делается при КАЖДОМ заказе рассылки, при заказе имени отправителя и при запуске рекламной кампании Яндекса. Выкат, на котором забудут перезапустить db/02_grants.sql, ломает всё это разом и молча. Что сделано: - миграция 2026_08_01_101400 выдаёт USAGE, SELECT на три счётчика кошелька рабочей и админской ролям. Роли не на глаз, а спрошены у базы: INSERT на сами таблицы кошелька есть ровно у этих двух, служебному работнику кошелёк не выдавался вовсе; - сторож tests/Feature/Advertising/AdWalletGrantsTest.php спрашивает у базы, а не читает текст миграции: опечатка в имени роли внутри миграции ошибки не даёт, права просто молча не выдаются; - запись v9.32 в журнале схемы. Структуру она не меняет — только права. Порядок работы соблюдён: сторож написан ДО миграции и покраснел ровно на том, на чём должен — «не выдано право на счётчик ad_wallet_holds_id_seq». После миграции зелёный. Живой прогон под ролью crm_app_user на базе из одних миграций: до — «нет доступа к последовательности», после — заморозка проходит и возвращает номер строки. Номер записи журнала взят v9.32, а не следующий свободный v9.26: номера v9.26-v9.31 уже заняты в других, ещё не сведённых ветках, и v9.26 повторил бы столкновение номеров, которое разбирали 01.08. Разрыв закроется при сведении с main. Памятку выката это НЕ отменяет: на уже накатанных базах миграция доносит право, но перезапуск db/02_grants.sql остаётся правильным шагом для всего остального. Статанализ добавил одно замечание — ложный класс Pest: анализатор не понимает $this внутри тестов Pest и не видит markTestSkipped. Тот же класс сидит в давно закоммиченном MigrationGrantsTest. Доказательство ложности прямое: тест проходит, не будь метода — упал бы. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a93cc12086 |
merge: свёл заголовок объявления с веткой робота - обе половины телеграма в одной ветке
Ветки разошлись: у соседней не было трёх правок тестов от 01.08, у нашей не
было заголовка объявления. Ни одна не содержала другую. Сведены в нашу.
Конфликт был ровно один и в автогенерённом файле docs/observer/STATUS.md -
время последнего обновления и список процессов. Взята наша сторона, хук всё
равно перегенерирует. В коде столкновений не было ни одного, в списке
исключений статанализа тоже.
Дописана запись журнала схемы v9.31 на их миграцию ad_headline - в исходном
коммите
|
||
|
|
1acbaf9383 |
Merge remote-tracking branch 'gitea/main' into feat/prospects-manual-testing-kp
# Conflicts: # docs/observer/STATUS.md |
||
|
|
9ee4e9a79f |
feat(воронка продаж): корзина, дробь у «Отказа», фильтр по датам — экраны
- 12-я колонка «Корзина», результат «В корзину» просит только причину; - в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного тестирования»; при нуле дробь не рисуется — «69/0» это шум; - у «Выслано КП» поле даты подписано «если договорились» и необязательно; - орган фильтра по датам на ОБОИХ экранах, два режима + период. Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела» (снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/). Браузер поймал то, чего не видели тесты: при смене режима оставался прежний период, и «что менялось за завтра» давало пустую доску — теперь период возвращается к «Сегодня», на это заведён отдельный тест. Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31. |
||
|
|
d9f22d1353 |
fix тесты: убрана грязь между тестами и закрыт выход в интернет из прогона
Полный набор стал зелёным целиком: 4063 теста, 0 падений и 0 ошибок против 36 непроходящих до правок. Рабочий код не тронут - изменены только тесты. Корень у пяти правок один: тест зелёный, а после себя оставляет мусор в базе, и падают соседи. Три файла рекламы писали набело, без отката. Их записи - задание роботу и проекты без источника лидов - переживали тест, и дальше в том же прогоне их подбирали чужие проверки. Отсюда 16 падений в файле про робота креативов и 13 в файлах про ночной слепок. Тестам запрещён выход в интернет - Http::preventStrayRequests в TestCase. До этого прогон физически ходил в живой кабинет Яндекс Директа, а поломка маскировалась под "Invalid OAuth token", хотя ключ в тесте подставной. Сторож принят вырезанием: со снятой правкой та же поломка называет себя честно, с адресом запроса. Он же вскрыл, что ProjectRuleNotificationTest слал живой запрос на удаление проекта в кабинет поставщика crm.bp-gr.ru, оставаясь зелёным - ответ кабинета тест не проверяет. Поставлена заглушка. ExternalServiceDownAlertTest избавлен от зависимости от сети - закрыт хвост, тянувшийся с 30.07: падал в общем прогоне, проходил в одиночку. InAppNotificationTest брал первую попавшуюся запись во всей таблице вместо своей. SalesOverviewTest попадал в топ-4 клиентов по удаче: при запросе всего отдела отбор не ограничен ничем, все тенанты базы с нулём лидов равны. Клиенту даны настоящие лиды - место в четвёрке заслуженное. Проверено: полный прогон 4063/4059 зелёный, статанализ 0, код-стиль моих файлов чисто. На бой ничего не выкачено. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc56c95e13 |
fix тесты: тестовая база перестала рваться посреди прогона
Починка не моя и к Этапу 5 отношения не имеет — она взята из ветки fix/robot-yandex-zamok, где написана и доказана 31.07. Сюда перенесена потому, что без неё прогон СМС-модуля в этой ветке недостоверен. Корень в самом Laravel: в конце каждого теста он проверяет, осталось ли соединение в своей черновой транзакции, и если тест её закрыл сам — сбрасывает признак «база собрана». Следующий тест пересобирает базу целиком прямо посреди прогона, снося её под всеми остальными. В проекте полно кода со своими транзакциями, поэтому срабатывало через раз. Что сделано: - пересборка базы теперь один раз и в самом начале прогона, а не лениво в середине по первому файлу, который её закажет; - признак «собрано» держится взведённым — пересборка посреди прогона стала невозможна; - отказ заливки схемы стал громким: раньше отказ мог вернуться без ошибки, и прогон ехал дальше по неполной схеме; - добавлена сверка полноты сборки: сколько шагов миграции записано против того, сколько их лежит файлами. Не сошлось — прогон умирает сразу и внятно, а не через сотни «таблицы X не существует»; - тест-ловушка TestDbRebuildGuardTest: первый тест выходит из транзакции и ставит метку, второй метку проверяет. Без защиты второй падает. Замер в этой ветке, СМС-модуль, 44 файла: - общая тестовая база и без починки — 267 зелёных, 4 красных пачки из 15 даже с шестью попытками на пачку; - своя база liderra_testing_sms и с починкой — 383 из 383 одним прогоном. Заодно вскрылась вторая, более грубая причина: общая база liderra_testing держала 143 записи о применённых миграциях при 137 файлах в этой ветке. Записей больше файлов — доказательство, что в базу пишет чужая рабочая папка. Датчик простой: сравнить число записей с числом файлов; любое неравенство значит «база не твоя, верить прогону нельзя». Лечение — своя база на рабочую папку, как уже сделано у соседних веток. Прогон всех 4000 тестов ветки с этой починкой я НЕ делал — мерил только область СМС-модуля. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7aa54773c1 |
merge: свёл заголовок объявления с веткой робота — воронка продаж и опрос в одной ветке
Влил fix/robot-yandex-zamok (26 коммитов: сведение с основной, стадии воронки «Тестирование ручное» и «Выслано КП», чтение вердикта и пересдача по опросу) в ветку заголовка объявления. Конфликт был один — docs/observer/STATUS.md, машинный файл наблюдателя со столбиком часов процессов. Взята своя, более свежая версия; файл всё равно перезаписывается хуком. Замер после слияния (своя тестовая база liderra_testing_zag, прогон в тишине): портал 4069 тестов, 4029 прошло, 16 упало; робот 130/130. До слияния было 4063/4018/17. Тестов больше, падений меньше — стык чистый. Падают шесть давних классов, не связанных с этой работой: CreativeRobotEndpoint (11 — лезет в живой Директ и получает «недействительный ключ»), CreativeJobService, InAppNotification, PhoneRegionSmoke, ProjectExtensions, SalesOverview. Проверено отдельно: таблица заданий роботу не ограничивает список режимов (обычная строка), поэтому новые «read-status» и «resubmit» миграции не требуют. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
016416502a |
feat(воронка продаж): корзина, КП без обязательной даты, фильтр по датам — сервер
- результат «В корзину»: одна причина, дата созвона стирается, платящего не выбросить; - реклама на корзине встаёт СРАЗУ — явной веткой, а не случайно через unknown_stage; - у «Выслано КП» дата созвона стала необязательной: бывает «пришлите на почту, если интересно — перезвоню». Врущий старый тест на 422 без даты поправлен; - фильтр доски date_mode=todo|changed + период (добавлен вид «завтра»). «Надо сделать» — созвон в периоде ИЛИ просрочен, кроме отказа и корзины. «Менялось» — есть движение стадии ИЛИ запись разговора за период. Прогон: 1245/1245 (отдел продаж + все юнит-тесты). |
||
|
|
53ebcd9fe0 |
feat(воронка продаж): журнал движений карточки + место под корзину
Стадию меняют два разных места — результат разговора менеджера и автопереезд по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день» было не из чего построить. Запись движения перенесена в событие модели: один шов на всех, включая любой будущий третий источник. - sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям); - prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»); - stage += 'trash' — место под колонку «Корзина». Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе тест джобы и тест API. |
||
|
|
32df332901 |
fix(телеграм-реклама): заголовок объявления для рекламы сайта — кампания больше не встаёт
Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 40 знаков), когда в объявлении ссылка на САЙТ, а не на телеграм-канал. Робот про это поле не знал, «Продолжить» молча не срабатывало, кампания вставала на шаге «Объявление» — в бою уже ПОСЛЕ списания денег. Проверено живьём: 2234454 (сайт — встала) против 2234462 (канал — дошла до подтверждения) и 2234490 (сайт с заголовком — дошла). Портал спрашивает заголовок заранее, на создании черновика: обязателен только для не-телеграмной ссылки (App\Support\TelegramLink), колонка ad_headline varchar(40), поле на экране появляется по той же развилке. Робот заполняет его в кабинете. Три ловушки, добытые живыми прогонами (описаны в коде): - поле дорисовывается в ОТВЕТ на ссылку, с задержкой — надо ждать, а не спрашивать; - под описание подходит несколько элементов — нужен .first(); - серая надпись внутри поля НЕ placeholder, а нарисованная подпись: поиск по атрибуту давал ноль совпадений при видимом на снимке поле. Опознаём по видимой надписи. Тесты: робот 130/130, ClientTg 250/250, экран 23/23. Полный прогон бэкенда — те же 13 падающих классов до и после правки (ни одного в телеграм-части). В baseline статанализа добавлен известный ложный класс Pest для нового файла тестов. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
22ac6e4f13 |
feat(смс-клиент): журнал рассылки открывается страницами по 50, а не одним куском
Решение владельца В-203 — «делай». Это не строка приёмочного листа, а мина, найденная разведкой: журнал отдавал ВСЕ сообщения рассылки одним ответом. На рассылке в двадцать тысяч номеров это двадцать тысяч строк за раз и подвисший экран — ровно то, что Этап 4 уже вынул из базы номеров. Этап 5 сделал мину горячее: в журнал добавилась судьба каждого номера, и человек стал открывать его чаще. Теперь по 50 строк, внизу подпись «Всего строк: 120 · страница 2 из 3» и переключатель. На рассылке в одну страницу переключателя нет — не шуметь там, где листать нечего. Размер страницы адресом не задерёшь: потолок 200, иначе страницы обходятся одним параметром и мы возвращаемся туда, откуда ушли. Номер страницы передаётся серверу явно. Сам по себе постраничный вывод берёт его из общего запроса приложения — и вторая страница выходит неотличимой от первой; эту дыру мы уже ловили живьём 30 июля на базе номеров. Главное решение здесь про доверие к числам: итог по судьбам и предложение досыла считаются по ВСЕЙ рассылке, а не по видимой странице. Иначе человек, листнув, увидел бы другой итог и не понял, какому верить. А досыл — это ещё и деньги: считать не дошедших по видимой странице значило бы называть заниженное число и брать не ту сумму. Стерегут это отдельные тесты — и на сервере, и на экране. Восемь тестов на сервере и пять на экране, все доказаны вырезом; вырезов вышло одиннадцать. Но главным прибором тут был браузер: вырез «убрать номер страницы» проверка запросом не видит вовсе — это записано в самом уроке. Живьём пройдены все три страницы: пятьдесят номеров, потом другие пятьдесят, потом остаток в двадцать; подпись менялась, а итог «доставлено 80, не доставлено 40» и кнопка «Дослать не дошедшим (40)» на всех трёх остались прежними. Стенд возвращён. Попутно поймана болезнь измерителя, уже второй раз за смену: он считал только «не сошлось» и не видел «рухнуло», отчего доложил два красных вместо семи. Прибор обязан читать весь отчёт, а не то поле, которое ты ждал. Один существующий тест пришлось поправить — он закреплял прежний вызов без номера страницы. Поправлен так, чтобы стеречь новое поведение, и к нему добавлен второй: страницу не назвали — просим первую. |
||
|
|
72db586a12 |
merge: свёл ветку телеграм-робота с основной, разрулил столкновение номеров журнала схемы
Основная ветка ушла вперёд на 23 коммита - приехала чужая работа по воронке продаж, вебхуку поставщика и сверке CSV. Конфликтов было два. 1. Журнал схемы db/CHANGELOG_schema.md. В обеих ветках лежала запись v9.28 от 31.07 про разное: у нас очередь заданий роботу, у них стадии воронки "Тестирование ручное" и "Выслано КП". Обе записи настоящие, поэтому ни одна не выброшена: боевая v9.28 осталась на своём номере, наши две подвинуты на v9.29 очередь заданий и v9.30 грант админ-роли. Поправлены ссылки на номер в шапках двух миграций, перекрёстные ссылки внутри самих записей и врезка "Перенумерация" вверху журнала - там теперь описан и этот случай. 2. docs/observer/STATUS.md - файл авто-генерируемый, взята версия основной ветки, хук перепишет его сам. Заголовок db/schema.sql не трогали: там своя нумерация v8.85, ни одна из веток её не двигала. Плюс одна настоящая находка статанализа, приехавшая с чужой работой: у метода SupplierPortalClient::fetchDeliveredLeads в описании возвращаемого набора не было поля tag, хотя код его уже возвращает и чужой же тест его ждёт. Дописал одно поле в описание, логику не трогал. Список исключений статанализа пересобран - разъехались счётчики ложного класса Pest от новых строк в чужих тестах. Проверено на ОТДЕЛЬНОЙ тестовой базе liderra_testing_tgmerge: телеграм на портале 244 из 244 робот 124 из 124 статанализ 0 замечаний код-стиль чисто полный прогон 4063 теста, 4018 прошло, 17 упало До сведения было 4039 тестов и 20 падений - тестов стало больше, падений меньше. Ни одно падение не касается телеграма или журнала схемы: 13 из 17 в рекламном модуле Яндекса, из них 11 - живой отказ авторизации Яндекс Директа, остальные счётные, от накопленных за прогон данных. Отдельно вскрылось при проверке: общая тестовая база liderra_testing испорчена - в ней 180 записей о применённых миграциях при 146 файлах, то есть в неё пишет не только эта рабочая папка. Из-за этого сборка базы срывалась на первом же шаге. Отдельная база всё вылечила. Это хвост Х2б, лечение в репозиторий не вносил - решение владельца. На бой ничего не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd83f440eb |
feat(смс-клиент): приёмник отчётов о доставке от МТС — ускоряет опрос, не заменяет его
Строка листа 5.2. Появился адрес, на который МТС может присылать судьбу сообщения сам,
не дожидаясь нашего вопроса. В ответ отдаём код 204 — этого он требует, иначе считает
доставку неудачной и шлёт повторы.
Несущее решение здесь одно, и из него растёт всё остальное: приёмник — НАДСТРОЙКА, а не
замена. Опрос каждые десять минут остаётся на месте. Значит любой отказ приёмника
безобиден: не понял письмо, не узнал номер сообщения, вовсе выключен — всё доберёт опрос.
Поэтому везде выбран ноль вместо догадки, и это позволило честно работать при незнании.
А незнание крупное: формы письма, которое шлёт МТС, живьём не видел никто. Записано только,
что письма приходят и что отвечать надо кодом 204. Документации тут веры нет — она уже
соврала про опрос, описав одну форму вместо другой. Выдумывать я не стал (В-243): приёмник
разбирает ровно ту форму, которую видел живой ответ на опрос, а незнакомое письмо кладёт в
журнал сервера своей ФОРМОЙ — перечнем полей, без содержимого, потому что внутри телефоны, а
это персональные данные. Первый же живой отчёт покажет свою форму сам, и читатель дописается
одной правкой. Проверено живьём: телефон в журнал не утёк.
Правило «как отчёт ложится в журнал» переехало из команды опроса в общий дом на два входа.
Разъехавшись, они писали бы по-разному, а по журналу считаются деньги. Форма письма при этом
живёт в канале, приёмник про устройство МТС не знает ничего — новый оператор с кабинетом
вставляется, не трогая ни приёмника, ни журнала.
Деньги приёмник не двигает вовсе. Возврат за недоставленное остаётся в опросе, где у него
своя двойная защита от повтора; вторая дорога к кошельку означала бы вторую возможность
вернуть дважды. Отдельный тест это стережёт.
Адрес публичный, поэтому защита тройная: секрет в адресе (не задан — приёмник закрыт наглухо,
а не «пускать всех»), необязательный список адресов отправителя и счётчик обращений. Чужому —
404, существование приёмника не подтверждаем. Список адресов пока пуст: адреса МТС нам
неизвестны, и придумать их нельзя.
Десять тестов, все доказаны вырезом — вырезов вышло двенадцать. Два вырезали и не покрасили
ничего, и опять ошибалось моё ожидание, а не код (шестой раз): место оказалось защищено
несколькими независимыми строгостями, каждой хватало поодиночке. Снял по две и по три разом —
покраснели ровно те тесты. Заодно попался прибор: прогон один раз показал девять тестов
вместо десяти при нуле красных, то есть пропавшая работа выглядела как отсутствие проблем.
Живьём: верный отчёт правит строку и отвечает 204; чужой секрет — 404 и строка не тронута;
непонятное письмо — 204 и ни одной записи; отчёт про неизвестный номер — 204 и предупреждение;
адрес вне списка — 404. Кошелёк за весь прогон не шелохнулся. Стенд возвращён.
Включение — сторона владельца: адрес указывается в кабинете МТС. До этого всё работает опросом.
🔴 И честно: пока канал МТС на бою не отправляет ничего, проверить приёмник живым письмом
нечем — отчётам просто неоткуда взяться.
|
||
|
|
65f3785bf3 |
feat(смс-клиент): сверка нашего расчёта с расчётом оператора — в админке у владельца
Строка листа 5.7. В админке «СМС» появился блок «Сверка расчётов с оператором»: по каждой отправленной рассылке видно, сколько частей насчитали мы и сколько оператор, сколько сообщений разошлись, наш расход, счёт оператора и разница. Клиенту это не показывается вовсе (решение В-201) — он платит по своему тарифу, наш расход перед оператором не его дело. Главное решение здесь — отсутствие числа и ноль это РАЗНЫЕ состояния. Экран пишет «цена канала не задана», «оператор цену не сообщил», «не спрашивали», «сверять нечем» и никогда не рисует 0 ₽ вместо неизвестного. Иначе владелец видел бы идеальную сходимость там, где сверять нечем — ровно тот молчаливый сбой, что стоил четырёх поломок 20.07. Разница считается только когда известны оба числа. Порядок работы задала живая проба, а не код. Шесть живых сообщений с боевого (разрешение и номера дал владелец) с перебором по одному признаку: МТС-номер и Т2-номер, одна часть и две, три разных имени отправителя, смешанный и чисто русский текст. Все шесть — «не отправлено», цена 0, отказ в ту же секунду, что и приём. Владелец проверил кабинет: баланс 5010 ₽, имя живое. Значит вывод из памяти проекта «пустой счёт» опровергнут, причина на стороне оператора (В-235, В-237) и требует разбора с их поддержкой. Попутно вторично и жёстче подтвердилось В-212: оператор принял даже номер Теле2, которого наш канал не обслуживает. «Принял» не значит ничего — по-старому портал записал бы «отправлено» и списал бы деньги за сообщения, которых нет. Что проба дала положительного: оператор считает ЧАСТИ ровно как мы — 1 на короткое, 2 на длинное в 118 знаков. На этом сверка по частям и построена, деньги ей не нужны. Строка 5.7 сужена честно и не молча (В-236): денежная половина построена, но живьём не доказана — цены нет с обеих сторон. У оператора 0, а у нас цена канала на бою не задана вовсе (В-234). Дозакрыть можно двумя вещами: числом из договора и одним реально отправленным сообщением с ненулевой ценой. План велел править AdminSmsController — такого файла нет вовсе (В-231, четвёртый раз за проект). Сверка живёт отдельным распорядителем: она ни ценам, ни отказам, ни именам отправителя не родня. Тесты: 9 на сервере, 6 на экране. Все проверены вырезом — 15 вырезов, каждый покрасил именно свои тесты. Один вырез не сработал, и опять ошибалось моё ожидание, а не код (В-239): база сама считает сравнение с пустотой «неизвестным», поэтому страж оказался лишним; заменён на вырез с настоящей ошибкой этого места. Живой прогон в браузере парный: без заданной цены канала — «цена канала не задана» и «сверять нечем»; с ценой 3 ₽ за часть — 9.00 ₽ против 12.00 ₽, разница 3.00 ₽, а расхождение по частям (3 против 4) выделено красным. Стенд возвращён. Статанализ поймал настоящую ошибку: сводные числа читались как поля модели сообщения, которых у неё нет. Починено — итоги берутся голыми строками, а не моделями. |