0016. Импорт данных WB (продажи/заказы/возвраты): модуль digitization
Копия ADR из репозитория:
backend/docs/adr/0016-wb-data-import-digitization.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-06-24
- Связанные задачи: OP-54 (5.1 · Оцифровка продаж / финансовый учёт)
Контекст и проблема
ТЗ 5.1: загрузка продаж, заказов и возвратов из Wildberries — основа финансового учёта и источник для РНП/репрайсера. Первая модульная фича. Нужен импорт за период, идемпотентный, с привязкой к артикулу/бренду/кабинету, устойчивый к ошибкам отдельных записей. Источник — WB API через F3 (OP-33). Блокеры OP-33/38/41 — Done.
Рассмотренные варианты
Источники WB
/api/v1/supplier/orders+/api/v1/supplier/sales(выбран, scopestatistics). Заказы — из orders; продажи И возвраты — из sales (один фид:saleIDS…=продажа,R…=возврат). Пагинация инкрементальная поdateFrom=lastChangeDate.- Плюсы: канонические seller-эндпоинты, одна модель для продаж+возвратов (как отдаёт WB).
reportDetailByPeriod(v5, finance). Отвергнут для базового импорта: это детализация реализации (комиссии/логистика) — относится к OP-55, не к сырому импорту продаж/заказов.
Модель хранения
- Две таблицы
wb_orders+wb_sales(выбран),wb_sales.type= enumsale|return. Денорм.nm_id/supplier_article/brandиз фида +rawjsonb. FK наwb_accounts(CASCADE).- Плюсы: повторяет структуру источника, привязка self-contained,
rawfuture-proof.
- Плюсы: повторяет структуру источника, привязка self-contained,
- Единая таблица
wb_documentsсdocument_type. Отвергнут: orders и sales имеют разный набор полей (orders —isCancel/cancelDate/discountPercent; sales —forPay/saleID), смешение в одну таблицу = много nullable-колонок и слабая типизация.
Идемпотентность
- Unique-ключ +
upsert(update on conflict) (выбран):wb_orders (wb_account_id, srid),wb_sales (wb_account_id, sale_id). On-conflict = update, т.к. WB актуализируетfinishedPrice/priceWithDisc/forPayв течение 24 часов — повторный импорт обновляет суммы. INSERT … ON CONFLICT DO NOTHING. Отвергнут: теряет 24-часовую актуализацию сумм.
Триггер
- Синхронный эндпоинт
POST /digitization/import(выбран) под аутентифицированным пользователем —getDecryptedApiKey(actor, id)требует actor для RBAC-скоупинга. - Фоновый BullMQ-джоб с системным актором. Отвергнут для OP-54: фоновый актор/RBAC-обход и ежедневный fan-out — это OP-58 (ADR-0009 называет его первым потребителем планировщика).
Решение
Модуль src/modules/digitization/:
- Сущности
WbOrderEntity(wb_orders),WbSaleEntity(wb_sales,typeenum) — extendsBaseEntity, FKwb_account_id→wb_accounts(CASCADE), unique-индексы для идемпотентности, индексы поnm_id/date,rawjsonb. WbImportService.importForAccount(actor, {wbAccountId, dateFrom}): получает ключ, постранично тянет orders и sales черезWbApiClient(scopestatistics), маппит чистымwb-import.mapper, батч-upsert. Per-record ошибки маппинга — лог + счётчик, прогон продолжается. Возвращает summary{orders, sales, returns: {fetched, saved, failed}}.- Контроллер
POST /digitization/import—JwtAuthGuard, Swagger,@CurrentUseractor. - Миграция
CreateWbImportTables; модуль подключён вAppModule; путь добавлен вopenapi.e2e-spec.
Последствия
- Положительные: появился базовый импорт WB — фундамент для учёта комиссий (OP-55), P&L (OP-56), отчётов (OP-57). Продажи+возвраты в одной модели (как у WB). Идемпотентный повторный импорт с актуализацией сумм. Полностью покрыт unit-тестами на моках (живой WB-токен не нужен для Gate 1).
- Отступления/границы: ежедневная авто-синхронизация и фоновый fan-out по кабинетам — OP-58
(здесь только синхронный per-account импорт). Кросс-линковка на
catalog.products/brands(FK поnm_id) — будущая работа; пока артикул/бренд хранятся денормализованно из фида. Детализация реализации (комиссии/логистика/штрафы изreportDetailByPeriod) — OP-55. - Деньги хранятся как
numeric(точность), в TS — строка (конвенция TypeORM numeric). - Сырой объект WB в
rawjsonb — трассируемость и устойчивость к новым полям без миграций.