Перейти к основному содержимому

0024. Периодический пересчёт репрайсера

Копия ADR из репозитория: backend/docs/adr/0024-repricer-schedule.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-06-25
  • Связанные задачи: OP-64 (5.3 · Репрайсер)

Контекст и проблема​

ТЗ 5.3: пересчёт цен по правилам должен запускаться автоматически по расписанию (scheduleIntervalHours в правиле) без участия оператора. Ключевые вопросы:

  1. Как регистрировать расписания при старте и не порождать дубликаты?
  2. Откуда брать «текущие цены» артикулов для периодического запуска?
  3. Как инициировать пересчёт без RBAC-контекста пользователя?

Рассмотренные варианты​

Вопрос 1: регистрация расписаний​

ВариантОписаниеОценка
Startup scan (выбрано)RepricerScheduleService.onModuleInit читает активные правила и регистрирует слоты через ScheduleRegistryServiceПростота, детерминированный jobId, anti-overlap через реестр
Hot reload при изменении правилаПри создании/обновлении правила сразу регистрировать/отменять расписаниеСложнее, требует отслеживания removed-slots; нет OP в scope
Cron + динамическая конфигурацияЕдиный cron, читающий правила перед каждым тикомПовторное обращение к БД каждый тик; масштабируется хуже

Ограничение startup-подхода: изменение scheduleIntervalHours или добавление нового правила вступают в силу только после перезапуска сервиса. Это приемлемо для текущего масштаба (один сервис, редкие правки расписания).

Вопрос 2: источник текущих цен​

ВариантОписаниеОценка
findLastKnownPrices (выбрано)Читать последнюю запись price_changes (DISTINCT ON nmId)Не требует WB API-вызова, данные актуальны после каждого ручного изменения
Запрос к WB APIПолучать цены из WB перед каждым тикомДополнительный API-вызов; rate-limit риски; лишний IO
Кэш в RedisХранить текущую цену в RedisДополнительная инфраструктура; нет OP в scope

Ограничение: если цена изменялась напрямую в WB минуя Optima, findLastKnownPrices вернёт устаревшее значение. Это приемлемо — репрайсер работает с ценами, установленными через Optima.

Вопрос 3: системный actor​

ВариантОписаниеОценка
runSystem() (выбрано)Отдельный метод с actorType=System, actorId=nullЯвно разделяет user/system пути; uuid-колонки принимают null
Фиктивный user-объектПередавать служебный объект через run()Смешивает аудит-контекст; вводит специальный case в RBAC
Единый метод с optional actorrun(actor?) с проверкой actor ?? systemНеявно; сложнее тестировать

actorId: null передаётся в price_changes.actor_id (uuid nullable) и audit_logs.actor_id (varchar nullable) — оба принимают null без нарушения схемы.

Решение​

  • RepricerScheduleService.onModuleInit — startup scan, регистрирует слоты через ScheduleRegistryService.register() с защитой от ошибки на каждый слот (try/catch с log.warn, остальные слоты продолжают регистрацию).
  • RepricerScheduleProcessor — читает findLastKnownPrices, вызывает runSystem.
  • RepricerEngineService.runSystem — actorType=System, actorId=null.
  • Аудит успеха обёрнут в try/catch: ошибка записи аудита не отменяет уже применённые изменения цен и не вызывает BullMQ retry.

Последствия​

  • Изменения расписания применяются после перезапуска сервиса (Known Limitation).
  • Репрайсер работает только с ценами, прошедшими через Optima.
  • runSystem не проверяет статус кабинета — если кабинет деактивирован, тики будут падать в аудит до ручного отключения правил.