0024. Периодический пересчёт репрайсера
Копия ADR из репозитория:
backend/docs/adr/0024-repricer-schedule.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-06-25
- Связанные задачи: OP-64 (5.3 · Репрайсер)
Контекст и проблема
ТЗ 5.3: пересчёт цен по правилам должен запускаться автоматически по расписанию
(scheduleIntervalHours в правиле) без участия оператора. Ключевые вопросы:
- Как регистрировать расписания при старте и не порождать дубликаты?
- Откуда брать «текущие цены» артикулов для периодического запуска?
- Как инициировать пересчёт без 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 actor | run(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не проверяет статус кабинета — если кабинет деактивирован, тики будут падать в аудит до ручного отключения правил.