0031. Метод учёта товарных остатков и интеграция со складом селлера
Копия ADR из репозитория:
backend/docs/adr/0031-inventory-stock-accounting.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted (рекомендация спайка согласована 2026-07-07 — журнал операций + сверка с WB; «свой склад» через адаптер импорта)
- Дата: 2026-07-07
- Связанные задачи: OP-133 (Spike · 5.11 · Товарный учёт); потребители — OP-128 (модель номенклатуры/остатков), OP-129 (синхронизация с WB), OP-130 (модель движения), OP-131 (учёт операций), OP-132 (API остатков/истории)
- Связанные ADR: ADR-0008 (хранение временных рядов и истории), ADR-0026 (стратегия доступа к ЛК — вся WB-часть по официальному API), ADR-0006 (rate-limit/retry для WB)
Контекст и проблема
ТЗ 5.11 помечает «Метод учёта остатков — уточняется», а интеграцию со складом селлера/фулфилментом — «API + доступ к веб-порталу для менеджера». Нужно решить не «откуда брать данные» (WB их отдаёт), а как считать остаток и движение по цепочке из ТЗ (поставщик → склад селлера/фулфилмент → доставка до WB → склад WB → в пути до/от клиента → продажа → возврат → иные статусы).
Что закрывает официальный WB API (проверено по локальному снимку OpenAPI)
Чтение остатков WB покрыто полностью — ЛК не нужен:
| Данные | Эндпоинт | Домен |
|---|---|---|
| Остатки FBW (склады WB), полный снимок | GET /api/v1/supplier/stocks | statistics |
| Детальные остатки по складам/размерам (async: task → status → download) | GET /api/v1/warehouse_remains | analytics |
| Остатки на складах селлера (FBS) | POST /api/v3/stocks/{warehouseId} | marketplace |
| Агрегаты остатков по группам товаров | POST /api/v2/stocks-report/products/groups | analytics |
| Список складов селлера / складов WB | GET /api/v3/warehouses, GET /api/v3/offices | marketplace |
Пробел ровно один: участок цепочки «поставщик → свой склад селлера / фулфилмент» у WB API нет — эти движения существуют вне WB (в WMS селлера, в 1С, в Excel менеджера).
Рассмотренные варианты (метод учёта)
| Вар. | Суть | Плюсы | Минусы |
|---|---|---|---|
| A. Зеркало WB | Только читаем снимок остатков WB и показываем | Тривиально, всегда сходится с WB | Нет «своей» цепочки движения и истории из ТЗ (учёт поступлений/списаний/перемещений) |
| B. Журнал операций (event-sourced) + сверка с WB | Каждое движение — событие (поступление/списание/перемещение/возврат/продажа); остаток = проекция; периодическая сверка проекции с WB-снимком | Полная цепочка, история движения, аудитируемость (ТЗ) | Нужен источник событий для «своей» части (ручной ввод/импорт) |
| C. Периодические снимки без событий | Храним снимки остатков во времени | Проще B | Теряется причинность движения; «история» = ряд снимков, а не операции |
Решение
Принят вариант B — журнал операций + сверка с WB:
- Движения — append-only fact-таблица (поверх ADR-0008, партиции помесячно): тип операции,
nm_id, склад, статус-звено цепочки, количество, дельта, источник, время. Остаток — проекция per(wb_account_id, nm_id, warehouse, status), пересобираемая идемпотентно. - WB-часть — только официальный API (согласуется с ADR-0026): синк остатков 1/час (OP-129) пишет WB-снимок в отдельную таблицу; сверка сопоставляет проекцию с WB-снимком и помечает расхождения (не «чинит молча»).
- «Свой склад / фулфилмент» — адаптер источника событий: ручной ввод + CSV-импорт в MVP; опциональный коннектор к WMS селлера — за интерфейсом, вне MVP. Веб-портал менеджера из ТЗ — на стороне склада, мы принимаем от него события импортом/API, а не ходим в его UI.
- Оценка себестоимости: для остатков в штуках метод оценки (FIFO/средневзвешенная) не требуется. Если понадобится денежная оценка — средневзвешенная (детерминированно, проще), отдельным решением; в scope 5.11 не входит.
Последствия
- Положительные: реализуется цепочка движения и история из ТЗ; WB-данные — целиком по официальному API (никакого ЛК/парсинга, согласуется с ADR-0026); переиспользуются ADR-0008 (партиции/история) и ADR-0006 (rate-limit). Сверка делает расхождения видимыми, а не скрытыми.
- Границы (осознанно): участок «свой склад/фулфилмент» без готового API в MVP — ручной ввод/CSV-импорт; денежная оценка остатков — вне scope до отдельного ADR; диаризация статусов «в пути до/от клиента» ограничена тем, что отдаёт WB (order/sale события).
- Разблокирует: OP-131 (учёт операций финализируется как запись событий в журнал), OP-130 (модель движения = звенья цепочки как статусы), OP-132 (API остатков/истории читает проекцию и журнал), OP-129 (синк пишет WB-снимок для сверки).