0017. Учёт комиссий/логистики/штрафов из отчёта реализации WB
Копия ADR из репозитория:
backend/docs/adr/0017-wb-financial-charges.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-06-24
- Связанные задачи: OP-55 (5.1 · Оцифровка продаж / финансовый учёт)
Контекст и проблема
ТЗ 5.1: для корректного расчёта прибыли (OP-56) нужны финансовые составляющие — комиссии, логистика,
штрафы. Источник — отчёт WB о реализации. Нужна загрузка, привязка к продаже/артикулу/периоду,
нормализация типов начислений и агрегация по артикулу. Блокер OP-54 (импорт WB) — Done. Расширяем
модуль digitization.
Рассмотренные варианты
Источник WB
/api/v5/supplier/reportDetailByPeriod(выбран, scopestatistics). Детализация отчёта о реализации: по строке — комиссия (ppvz_sales_commission), логистика (delivery_rub), штраф (penalty), эквайринг, хранение, удержания, к перечислению (ppvz_for_pay). Пагинация поrrdid. Это канонический финотчёт WB на уровне строки./api/v1/supplier/sales(OP-54) — там только упрощённыеforPay/finishedPrice, без разбивки комиссий/логистики/штрафов. Недостаточно для учёта затрат.
Модель хранения
- Одна таблица
wb_report_lines(выбран): строка отчёта со всеми финполями +raw. Нормализация начислений в типы — на лету чистойextractCharges(line); агрегаты считаются чистойaggregateByArticle.- Плюсы: повторяет источник 1:1, идемпотентность по
rrd_id,rawfuture-proof, нет дублирования данных в отдельную charge-таблицу.
- Плюсы: повторяет источник 1:1, идемпотентность по
- Нормализованная
wb_charges(строка на каждое начисление, enum type). Отвергнут для базового учёта: взрывает каждую строку отчёта в N charge-строк, усложняет идемпотентность и не нужен для агрегатов (их даётextractCharges+aggregateByArticleнадwb_report_lines). Можно ввести позже, если понадобятся per-charge запросы.
Расчёт агрегатов
- Чистая
aggregateByArticle(lines)(выбран): группировка поnm_id, сумма по типам — без БД, детерминированно, легко покрыть тестами на эталонных данных (AC3). Сервис читает строки за период и зовёт функцию. - SQL
GROUP BY nm_id, SUM(...). Отвергнут как основной: расчёт «размазан» по SQL, сложнее юнит-тестировать нормализацию начислений; SQL-агрегация — возможная оптимизация позже.
Решение
В модуле digitization:
WbReportLineEntity(wb_report_lines): финполя строки отчёта, FKwb_account_id→wb_accounts(CASCADE), unique(wb_account_id, rrd_id), индексы по(wb_account_id, nm_id)и периоду,rawjsonb.ChargeType(commission/logistics/fine/acquiring/storage/deduction) + чистыйextractCharges(line)(ненулевые затраты) — нормализация.additional_payment(доплата/корректировка вознаграждения ВВ) — НЕ начисление-затрата (знаковая выплата продавцу), поэтому не входит вextractCharges/total; значение хранится в колонке иrawдля P&L (OP-56).- Фильтр периода в
summaryByArticle— по дню операции в московском времени ((rr_dt AT TIME ZONE 'Europe/Moscow')::date), с fallback наdate_fromдля строк безrr_dt; иначе session-TZ (UTC) сдвинул бы день на границах и null-rr_dtстроки выпадали бы из агрегата. aggregateByArticle(lines)— суммы по артикулу × тип + итого.WbFinancialReportService:importReport(actor, dto)(пагинация поrrdid, idempotent upsert, per-record resilience как OP-54) +summaryByArticle(actor, dto)(scope кабинета черезgetById(actor,id), чтение строк, агрегатор).- Контроллер:
POST /digitization/financial-report/import,GET /digitization/financial-report/summary. - Общие хелперы
parseWbDate(MSK)/wbMoneyвынесены вwb-mapping.util.tsи переиспользованы OP-54-маппером (DRY, единый источник MSK-логики).
Последствия
- Положительные: появился учёт затрат — вход для расчёта прибыли (OP-56). Нормализация и агрегаты —
чистые функции, полностью покрыты тестами.
sridсшивает строки отчёта с продажами/заказами OP-54. - Границы: себестоимость (закупочные цены) — не здесь (товарный учёт 5.11); P&L — OP-56; ежедневная авто-синхронизация — OP-58. Per-charge нормализованная таблица — отложена.
- Деньги —
numeric(14,2); агрегатор оперирует числами и возвращает строки фиксированной точности. - MSK-обработка дат — общая с OP-54 (отчёт тоже в московском времени).