Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.
Сверх самого слияния:
- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.
Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.4. Живая отправка кампании
ставит статус `moderating` (задача 3.2), но одобрение/отказ приходит от МТС
позже и без API. Опросчик раз в цикл робот-читалкой заходит в кабинет по
`mts_campaign_id` и применяет вердикт — КОНСЕРВАТИВНО с деньгами.
- PollTelegramModerationJob: раз в 15 минут перечисляет `moderating`-кампании
с `mts_campaign_id` (без id на модерацию уйти не могли — пропускаем) и читает
вердикт роботом РОВНО ОДИН РАЗ на кампанию (каждый вызов = заход в браузер):
· `rejected` («Отклонена») → статус `rejected` + причина из кабинета; возврат
брони кошелька (в бою, ключ совпадает с freeze); уведомление «отклонена»;
· `approved` («Одобрена») → `launched`; уведомление «одобрена». Деньги НЕ
трогаем — бронь под потолок держится, фактическую стоимость спишет билинг
по факту показов (2.4 → позже);
· `moderating` / робот не прочитал вердикт (null) → НЕ трогаем, ждём цикла.
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; возврат брони и уведомление —
ПОСЛЕ транзакции статуса, каждый в своём try/catch. Повторная проверка
status===moderating под lockForUpdate (гонка). Зеркалит SweepStuckTelegramCampaignsJob.
- RobotResult: поле `moderationStatus` (approved|rejected|moderating|null) + разбор
в fromRobotJson.
- TelegramRobotRunner: метод `readModeration($mtsCampaignId)` (режим read-status,
Node-сторона — задача 3.5); `campaignId` проброшен в task-payload.
- NotificationService: `notifyTelegramCampaignApproved` (зеркало Rejected, in-app
без pref-гейта) + событие EVENT_TG_CAMPAIGN_APPROVED.
- console.php: расписание опросчика everyFifteenMinutes с heartbeat-трекингом.
Селекторы экрана отказа — из живой разведки Part B (bots/mts-telegram-ads/
FLOW-FINDINGS.md, «Разведка Сессии 6»); повторного захода в кабинет не потребовалось.
Миграция не нужна — новых колонок/статусов нет (moderating/rejected уже были).
TDD. Приёмка (моя область, чистый прогон): PollModerationTest 6/6 (отклонена/
одобрена/ещё-на-модерации/не-прочиталось/без-id + деньги: при отказе бронь
возвращена, при одобрении не тронута) + весь набор ClientTg 143/143.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.3. Робот в браузере может
оборваться на полпути (воркер убит, сеть отвалилась) — кампания навсегда
виснет в `running`, а замороженные деньги залипают. Уборщик добивает статус,
но КОНСЕРВАТИВНО с деньгами.
- SweepStuckTelegramCampaignsJob: раз в 10 минут ищет `running` старше 15 минут
(штатный робот ≤ 420с). Развилка по факту создания черновика в кабинете:
· нет mts_campaign_id (черновик не создан) → кампания заведомо не ушла →
`failed` + возврат брони кошелька;
· есть mts_campaign_id (черновик создан) → могла уйти на модерацию МТС →
деньги вслепую НЕ возвращаем, `needs_review` до ручной сверки (задача 3.4).
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; release гейтится на !sandbox,
ключ совпадает с freeze. Повторная проверка status===running под lockForUpdate
(гонка с finalize). Зеркалит связку ChargeTgNameFeeJob + RunTelegramCampaignJob.
- Campaign.php: константа STATUS_NEEDS_REVIEW; переходы running→needs_review
(терминальный) и queued→failed (для failed() джоба при раннем сбое). Миграция
не нужна — на колонке status нет CHECK, машина статусов в модели.
- RunTelegramCampaignJob: public int $timeout = 420 (робот ~300с + навигации);
failed() теперь умеет queued→failed (переход добавлен в модель).
- console.php: расписание уборщика everyTenMinutes с heartbeat-трекингом.
TDD. Приёмка (моя область, чистый прогон): SweepStuckTest 7/7 (в т.ч. деньги —
с черновиком не тронуты, без черновика возвращены) + StatusMachine/Moderating/
ExternalId/RunCampaignJob/LaunchIdempotency/RobotResult/RobotRunner 49/49.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Р6 — задание, брошенное «в работе», держало очередь для всех клиентов навсегда.
Причина сбоя обрезается до 900 знаков, иначе портал отвергал отчёт о самом частом
виде поломки. Провал отчёта больше не глотается молча — едет в письмо человеку.
Приём отчёта ловит любую ошибку и закрывает задание сбойным вместо 500 роботу.
Новый сторож creative-jobs:reap каждые 10 минут разбирает пробку.
Р7 — обрыв на докладе «готово» объявлял провалом уже сделанную работу. Доклад
вынесен из-под обработчика сбоя, повторяется трижды, при неудаче только письмо.
Осечка ухода со страницы после «Создать» больше не считается провалом загрузки.
Р8 — пустой список размеров считался успехом, и робот заливал одни и те же файлы
по кругу, оставляя мусор в живом кабинете. Теперь громкий отказ до похода в Яндекс.
Портал 274/274, робот 41/41. Все защиты проверены вырезанием.
Выходов снятия заморозки денег по-прежнему четыре.
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.
4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
(audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).
4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.
4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).
4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).
Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 6: SyncAdAudienceJob считает состав пофирменно решением
PlatformWarmingDecider по срокам Яндекса вместо сводного phone.state.
Фирма, погасшая по Яндексу, уходит из сегмента, даже если сводка
active из-за другой площадки (ВК/МТС). Джоб читает sales_prospects,
поэтому планировщик передаёт pgsql_supplier в конструктор (защита от
permission denied на роли crm_app_user, инцидент 19.07).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
no_access / waiting_volume / working. Ниже порога 2000 номеров и без
ключа доступа джоб не делает к ВК ни одного обращения (Http::assertNothingSent
в тестах) — защита от случайной траты денег владельца, тот же приём, что
у рубильника enabled в Яндексе.
VkAudienceClient::replaceList намеренно не реализован (throw): техническая
документация API ВК закрыта до получения доступа (заявка подана 19.07).
Найдено по ходу: свойство конструктора джоба нельзя называть $connection —
конфликтует с публичным $connection из Illuminate\Bus\Queueable (фатальная
ошибка "incompatible property composition"). Переименовано в $dbConnection,
как $prospectConnection у RecalcAdAudienceJob.
Task 8 плана docs/superpowers/plans/2026-07-19-vybor-ploshadki-progreva.md.
На бою джоб бежит вне веб-запроса, то есть на дефолтной роли crm_app_user,
у которой нет прав на sales_prospects — пересчёт падал с permission denied
и не работал вообще. Планировщик теперь передаёт pgsql_supplier, как это
делает SalesProspectsAdvanceJob. Регресс-тест закрывает.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SyncAdAudienceJob использует готовый YandexAudienceClient (Http::fake
в тестах) и режим modify_data replace: заливает весь активный список
целиком раз в сутки (03:20 МСК, сразу после RecalcAdAudienceJob).
Рубильник sales_ad_audience_state.enabled=false — джоб не делает ни
одного обращения к Яндексу. Ошибку Яндекса кладём в last_error,
не проглатываем молча.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 5 плана v2: RecalcAdAudienceJob (routes/console.php, дневное 03:10 МСК) ночью
сверяет стадию связанной карточки sales_prospects с последней запомненной у фирмы
(sales_ad_audience_firms.stage_seen/stage_changed_at) и зовёт AdAudienceScheduler,
чтобы проставить каждому номеру состояние active/paused/stopped.
Найдена и закрыта дыра в исходном плане: ни prospect->updated_at (двигается ЛЮБОЙ
правкой карточки — заметка менеджера ложно продлевала бы рекламу), ни firm->assigned_at
(момент назначения менеджера, не смены стадии) не годились источником даты «сколько
фирма сидит в текущей стадии». Решение: фирма сама хранит эту дату — миграция
2026_07_20_110000 (+2 nullable-колонки, db/CHANGELOG_schema.md v8.78). Новый тест
«правка карточки не перезапускает отсчёт рекламы» закрывает ровно эту дыру.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Действие «Зарегистрировался» (PATCH action=registered): email→tenant, привязка
SalesClientAssignment со снимком тарифа, linked_tenant_id+stage=registered; клиент
занят другим → 422. Диалог карточки: пункт «Зарегистрировался» + поле e-mail.
SalesProspectsAdvanceJob (каждые 15 мин, pgsql_admin): по balance_transactions
считает стадию — Σtopup≥30000→user, >0→topped_up, есть расход при 0 topup→testing,
иначе registered; ручные/отказные стадии не трогает. Схема НЕ меняется.
Гейты: бэк 19/19, фронт 9/9, Larastan 0. 🪤 property $connection конфликтовал с
трейтом Queueable → переименовал в $dbConnection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Разбор живого клиента (стоматология, Красноярск, 14.07): 2 часа настраивал 26
проектов, получил 14 отказов в трёх формах и ушёл, не заплатив.
Деньги:
- отменённый шлюзом платёж больше не висит «ожидает» вечно: закрываем как failed
с причиной (PaymentSettlementService — общий путь для webhook и крона);
- billing:reconcile-payments каждые 5 минут сам спрашивает шлюз про зависшие
pending. Побочно страхует от ПОТЕРИ ДЕНЕГ: если webhook не дойдёт, оплаченный
платёж всё равно зачислится;
- кабинет говорит правду: «Оплата не завершена» + «Оплатить снова» вместо
«баланс обновится автоматически» (GET /api/billing/last-payment).
🔴 RLS-мина (поймана валидатором ДО выката): UPDATE при отмене шёл без
tenant-контекста → на проде тронул бы 0 строк, а портал рапортовал бы «отменено».
Тесты слепы (тестовая БД под postgres). Регресс-тест проверяет ПОРЯДОК:
SET LOCAL tenant ДО UPDATE. Тот же класс, что инциденты 07.07 и 12.07.
Формы (клиент бился и уходил):
- удаление проекта со сделками: причина показывается на месте + кнопка
«Поставить на паузу» (раньше 422 улетал в никуда — 4 попытки впустую);
- создание проекта: ошибка по дням недели больше не молчит (у поля не было
места для показа — 2 немых отказа);
- автоподбор «Добавить вручную»: показываем причину от сервера (был голый
catch {}), длинные ссылки 2ГИС/Яндекс.Карт принимаются — трекинг-хвост срезаем
сами. Воспроизведено тестом: именно длинная ссылка давала 3 отказа подряд.
Наблюдаемость: причины отказов пишутся в журнал (маршрут, tenant, ИМЕНА полей;
значений нет — 152-ФЗ). Уровень warning: на проде LOG_LEVEL=warning, info в
журнал не попадает вовсе. Робот-сверщик добавлен в реестр пульса.
Тесты: Pest 2475/2475, Vitest 1215/1215.
Выкачено на боевой 14.07.2026 ~13:00 МСК; сверка сразу закрыла 3 мёртвых платежа
(10 000 ₽, 5 000 ₽, 1 000 ₽).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гостей чистит visitors:prune (воскресенье 03:15 МСК), партиции событий —
существующий partitions:drop-expired по новой настройке retention=6 месяцев.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-инцидент 11-12.07.2026: робот итоговой проверки слал письма «нет заказа у
поставщика» на строки, которые в кабинете ЕСТЬ, включены и с верным лимитом.
Корень: кабинет дописывает метку канала к имени строки только при СОЗДАНИИ, а при
обновлении сохраняет имя ровно как прислали. Наш ежедневный updateProject слал голый
uniqueKey и каждый прогон стирал метку. Последствия:
- итоговая проверка выводила площадку из префикса имени и переставала узнавать
строку -> ложное missing 11.07 и 12.07;
- лид от такой строки приходил с project без метки -> webhook не мог определить
канал и писал platform=DIRECT вместо B1/B2/B3, то есть терялась атрибуция канала.
Что сделано:
- SupplierPortalClient::toPayload — на update имя уходит с меткой канала; на create
остаётся голым, там метку ставит сам кабинет и один save с тремя флагами рождает
три строки, общего префикса у них нет.
- VerifySupplierOrderJob::normalizeLive — площадка берётся из служебного поля src
rt/bl/mt, а не из префикса имени; сверка больше не зависит от имени вообще.
- Новая разовая команда supplier:repair-project-names — возвращает метку строкам,
у которых её уже стёрли. Payload собирается ИЗ ЖИВОЙ строки кабинета, меняется
ровно одно поле name; по умолчанию сухой прогон, запись только с --apply.
Ветка пересобрана на gitea/main — закрывает follow-up «фича итоговой проверки заказа
не сведена в main». Попутно возвращён CsvReconcileJobTest, отставший от кода после
сведения main 09.07: он не фейкал fetchDeliveredLeads и падал 9 из 11.
Боевой liderra.ru: выкачено, починена 81 строка, робот показывает 0 расхождений
138 наших строк вместо 57. Двум лидам восстановлен канал по журналу выдач поставщика.
Тесты: Pest supplier 277/277, Pint clean, Larastan 0 новых ошибок.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 5: RunAutopodborStudyJob::handle() оборачивает существующее тело в
try/finally — после ЛЮБОГО завершения прогона (done/empty/failed, включая
проброшенное исключение) распорядитель AutopodborStudyScheduler::tick()
берёт следующего кандидата с освободившейся дорожки шага 2.
Задача 6: artisan autopodbor:study-tick — крон-страховка раз в минуту на
случай, если дорожка освободилась не через завершение джобы. Без
withoutOverlapping() — требует таблицу cache_locks, которой нет в схеме
(см. комментарий в routes/console.php); overlap уже защищён внутренним
Cache::lock в AutopodborStudyScheduler::tick().
Закрывает рассинхрон онлайна со слепком поставщика 21:00.
- 4.1: SyncSupplierProjectJob в окне 18:00→00:00 МСК кладёт проект в новую очередь
supplier_deferred_sync вместо немедленной отправки (перезаписала бы зафиксированный
слепок). Вне окна — как раньше.
- 4.2: FlushDeferredOnlineSyncJob в 00:05 МСК досылает отложенное вне окна и чистит очередь.
- Схема: +1 таблица supplier_deferred_sync (project_id PK, без RLS — системная очередь как
supplier_manual_sync_queue), миграция 2026_06_25_120000, schema.sql v8.54 + CHANGELOG.
RLS-ревью пройдено (no-RLS консистентно прецеденту; формулировки GRANT/метрик уточнены).
Тесты 6/6 + регрессия онлайн-синка 33/33 зелёные. Под LEFTHOOK=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
152-ФЗ блокер B1/F-P1: телефоны и имена контактов soft-deleted сделок не
вычищались и хранились бессрочно. Добавлена плановая команда-ретеншен.
Команда pd:scrub-soft-deleted-deals анонимизирует phone/contact_name/phones
сделок с deleted_at старше N дней; N из system_settings
pd_scrub_soft_deleted_deals_days, по умолчанию no-op — юр.срок не зашит в код.
Значения затирания идентичны PdErasureService. Cross-tenant через
pgsql_supplier BYPASSRLS, идемпотентно, summary-запись в pd_processing_log
системным актором. Планировщик ежедневно 03:30 МСК с heartbeat.
Схема v8.41: partial index deals_deleted_at_index ON deals deleted_at WHERE
deleted_at IS NOT NULL для дешёвой выборки; счётчик индексов 120 на 121.
F-T2 проверен: /api/admin за middleware saas-admin fail-closed 503 — кодовой
правки не требует.
TDD: 4 Pest ScrubSoftDeletedDealsCommandTest GREEN. Escape-per-write — печать
церемонии не опечатывала план.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 файла одной пачкой. Исключены чужие зоны: CLAUDE.md, .claude/settings.json, docs/observer/.pii-counters.json.
gitleaks staged: no leaks found. Не верифицировано тестами - сохранение труда в историю.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1.6 плана 2026-05-24-billing-v2-spec-c-preflight-vtb.
BalanceFrozenReminderJob — окна 24-48ч (reminder) и 72-96ч (final).
Throttle через balance_freeze_log markers (event_type 'reminder_sent' /
'final_sent') на 5 дней — повторов в окне не будет.
Re-evaluate PreflightResult для актуального дефицита в письме
(клиент мог частично пополнить — reminder покажет обновлённое число).
Schedule @18:30 MSK (после основного sweep @18:00) — если sweep
только что заморозил тенанта, reminder в тот же день не сработает
(окно 24h+ ещё не открыто).
TDD: 4 теста GREEN (reminder/final/skip-fresh/throttle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 1.4+1.5 Спека C. BalancePreflightSweepJob (chunkById всех тенантов,
переход active->frozen / frozen->active, идемпотентность, журнал balance_freeze_log
через pgsql_supplier) + BillingPreflightSweepCommand + cron billing:preflight-sweep
@18:00 MSK (SyncSupplierProjectsJob сдвинут 18:00->18:05). 4 Mailable
(Frozen/Reminder/Final/Unfrozen) + blade. Job шлёт Frozen/Unfrozen при переходах;
Reminder/Final (T+24h/T+72h) — классы готовы, рассылка по дате — следующий шаг.
11 Phase 1 billing-тестов GREEN. Адаптации под факт схемы: contact_email (не email),
organization_name (не name), is_active+daily_limit_target (не status+daily_limit).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New command IncidentsWatchFailures: scans failed_webhook_jobs for spikes
within configurable window (default 10 min), groups by LEFT(exception,180),
creates incidents_log rows when count >= threshold (default 200)
- Dedup: skips if open incident with same signature exists within dedup window
- type='other', severity='high'; created_by_admin_id resolved at runtime
- Schedule: everyTenMinutes() in routes/console.php
- 4 Pest tests: below-threshold / spike-detected / dedup / multi-signature
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Запас ~3 часа до портального дедлайна 21:00 — эскалация на ярус 2/3
(медленный браузер / ручной оператор) происходит в рабочее время.
RefreshSupplierSessionJob daily — на 15 мин раньше sync (17:45).
Hourly RefreshSupplierSessionJob — без изменений.
Spec §4.7. Task 9 of 12. Tests 2/2 (cron expression + timezone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit #2 Phase 14 P2: partition tables were not auto-created.
Without this entry the scheduler never called partitions:create-months,
causing partition exhaustion on the first day of each new month.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Месячный cron-сброс tenants.delivered_in_month + projects.delivered_in_month
1-го числа каждого месяца в 00:00 МСК. Идёт через pgsql_supplier BYPASSRLS
connection (паттерн ResetDeliveredTodayCommand). Идемпотентный
(WHERE delivered_in_month <> 0 → повторный запуск 0 affected rows).
4 теста: reset multi-tenant + idempotency + Schedule registration +
BYPASSRLS without SET LOCAL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec §6.1: ежедневный сброс projects.delivered_today=0 после midnight МСК.
delivered_in_month НЕ трогаем (это месячный счётчик, Plan 4 cron).
Реализация: Artisan-команда `projects:reset-delivered-today` (idempotent
UPDATE без транзакции/локов — отрабатывает за <1 сек), Schedule в
routes/console.php с dailyAt('00:00')->timezone('Europe/Moscow').
NB: `withoutOverlapping()` пропущен — требует таблицу cache_locks, которой
нет в schema.sql (Laravel-default-миграции удалены в фазе 1). Идемпотентность
UPDATE делает overlap-защиту избыточной.
Tests: 2/2 pass, phpstan 0 errors (1 baseline для $this->artisan, как у
прочих Pest-тестов с artisan-helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>