0020. API изменения цен и скидок (одиночное и массовое)
Копия ADR из репозитория:
backend/docs/adr/0020-pricing-api.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-06-25
- Связанные задачи: OP-59 (изменение цен), OP-60 (настройка скидок) — 5.2 · Управление ценами и скидками
Расширение OP-60 (скидки). WB-эндпоинт
POST /api/v2/upload/taskпринимает и цену, и скидку ({ nmID, price?, discount? }), поэтому настройка скидок переиспользует ту же инфраструктуру модуляpricingбез новой миграции/очереди: эндпоинтыPOST /pricing/discount(single) иPOST /pricing/discount/bulk(массово, общий прогон {@link PriceBulkJobEntity} и очередь). Скидка — целое 0–99,0= снятие скидки. ОбщиеsubmitSingle/enqueueItemsвPricingServiceпараметризованы элементом загрузки (цена шлёт{nmID,price,discount?}, скидка —{nmID,discount}) и действием аудита (pricing.discount.set/pricing.discount.bulk). Статус прогона — общийGET /pricing/bulk/:id.
Контекст и проблема
ТЗ 5.2: эндпоинты установки цен по артикулам (одиночное + массовое), применяемые на WB, с валидацией,
асинхронной обработкой массовых изменений и отчётом об успехах/ошибках, аудитом (F6). Блокеры OP-33
(WB-клиент), OP-38 (сущности), OP-41 (очереди) — Done. Новый модуль pricing.
WB-API цен двухшаговый и асинхронный: POST /api/v2/upload/task (scope prices, домен
discounts-prices-api) принимает батч { data: [{ nmID, price, discount? }] } (≤1000) и возвращает
taskId; реальное применение и пер-товарные ошибки — отдельными методами GET /api/v2/history/tasks
и GET /api/v2/history/goods/task. Лимит — 10 запросов / 6 сек на кабинет.
Решение
Модуль pricing:
- Single (
POST /pricing/price):PricingService.setPriceвалидирует DTO (цена — целое ≥ 1, скидка 0–99; невалидное → 400, AC3), берёт ключ кабинета (RBAC, 404 чужой), вызывает WBupload/taskс одним элементом, возвращаетtaskId, пишет аудит (F6). Синхронно. - Bulk (
POST /pricing/bulk): создаёт прогон {@link PriceBulkJobEntity} (price_bulk_jobs: входныеitems, счётчики,result), ставит джобу в очередьpricing-bulk(OP-41), возвращаетbulkJobId(202). ВоркерPricingProcessorчанкует (≤1000), отправляет каждый чанк в WB, агрегирует успехи/ошибки по чанкам вresult(taskId загрузок + ошибки), помечает прогонCompleted(частичные ошибки — в отчёте; сбой чанка не валит остальные).GET /pricing/bulk/:idотдаёт статус/отчёт (скоуп — по владельцу прогона; Admin — все). - Валидация — на DTO (class-validator): цена/скидка/массив (≤1000). Для OP-59 цена обязательна (изменение цен); скидки целиком — OP-60.
- Лимит WB — точечный override
prices:/api/v2/upload/task= 10/6с вWB_ENDPOINT_RATE_LIMITS.
Рассмотренные варианты (отчёт об ошибках bulk)
- Чанк-уровневый отчёт при отправке (выбран): фиксируем приём WB (taskId) / отклонение чанка (errorText). Не требует поллинга; покрывает AC2 «отчёт об успехах/ошибках» на уровне отправки.
- Пер-товарный поллинг
history/goods/task(подтверждение применения, карантин). Точнее, но добавляет двухшаговый асинхронный поллинг — отложено как follow-up (документировано ниже). - Поэлементная отправка (1 товар = 1 вызов WB) ради пер-товарных ошибок. Отвергнут: при лимите 10/6с массовая отправка деградирует; батч-загрузка — целевой режим WB.
Последствия
- Положительные: появились single/bulk изменения цен на WB с валидацией, аудитом и отчётом прогона — основа для репрайсера (5.3) и скидок (OP-60). Очередь даёт ретраи (BullMQ) и изоляцию чанков.
- Границы / вне scope (follow-up):
- Подтверждение применения на стороне WB (
history/tasks+history/goods/task) и пер-товарные ошибки/карантин — отдельный шаг (отчёт сейчас — на уровне приёма загрузки WB). - История изменений цен — OP-61 (не дублируем;
price_bulk_jobs— тех-статус прогона, не история). - Скидки целиком / discount-only — OP-60.
- Подтверждение применения на стороне WB (
- Цены — целые рубли (WB принимает integer); валюта кабинета берётся из методов получения цен.
- Аудит (F6) — на
pricing.set(single, после WB) иpricing.bulk(постановка прогона).