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

0023. Движок пересчёта цен репрайсера

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

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

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

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

  1. Как доставить «текущие показатели» в движок?
  2. Как разрешать конфликт нескольких подходящих правил для одного артикула?
  3. Как интегрироваться с историей цен (модуль 5.2)?

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

Вопрос 1: источник показателей​

ВариантОписаниеОценка
Push API (выбрано)Клиент передаёт показатели в запросеDecoupled; движок не зависит от хранилища метрик
Pull от сервисаДвижок сам запрашивает показатели из оцифровкиСильная связность; требует реализации агрегации в движке
Периодический cronДвижок сам читает БД по расписаниюНеуправляемость; latency неизвестна

Вопрос 2: разрешение конфликтов​

ВариантОписаниеОценка
Oldest-first (выбрано)Побеждает правило с наименьшим createdAtДетерминировано, предсказуемо, прозрачно для пользователя
Приоритет явным полемЧисловой priority в таблице правилНужна дополнительная ручная настройка
Самое строгое правилоВыбирать максимальное/минимальное действиеНепредсказуемо при разных типах действий

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

ВариантОписаниеОценка
PriceHistoryService.recordBulk (выбрано)Движок вызывает уже готовый bulk-методПовторно использует существующий путь; source=repricer
Новая запись через PricingServiceДобавить метод applyRepricerPricesЛишний слой; PricingModule уже экспортирует PriceHistoryService

Принятые решения​

1. Push API для показателей​

POST /repricer/run принимает RepricerRunDto — список { nmId, currentPrice, brand?, metrics }. Движок не знает, откуда взялись метрики. Клиент (или будущий cron-сервис 5.3) отвечает за их сборку и передачу.

2. Oldest-first для разрешения конфликтов​

При нескольких подходящих правилах для одного артикула побеждает правило с наименьшим createdAt (старейшее = наивысший приоритет). Правила загружаются из БД c ORDER BY created_at ASC; первое совпадение выбирается и цикл прерывается.

Обоснование: «старые правила = намеренно заданные» — это соответствует ожиданиям пользователя, настраивающего правила последовательно. Детерминизм гарантируется монотонностью created_at + id (UUID v4, не сортируется, но created_at уникален в пределах одного кабинета на практике).

3. Интеграция через PriceHistoryService​

Движок напрямую вызывает WbApiClient.request (глобальный сервис, не требует дополнительных импортов) и PriceHistoryService.recordBulk (экспортируется из PricingModule) с source = PriceChangeSource.Repricer. oldValue в bulk-записях равен null — это поведение идентично массовому изменению через PricingProcessor и задокументировано в ADR-0021.

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

  • Движок stateless — легко тестируется и масштабируется.
  • Conflict resolution прозрачен и воспроизводим без знания priority-полей.
  • POST /repricer/run можно вызывать как вручную, так и из будущего cron-планировщика.
  • oldValue в истории = null для репрайсерных изменений (аналогично bulk). Если нужно отслеживать «до», используйте последнюю запись истории перед отметкой repricer.