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

STATUS-SEMANTICS — семантика статусов и групп маркетплейсов

Статус: активный · Владелец: Anton / business · Сверено с кодом: 2026-07-03 @ d7aaeab (логика kernel не менялась с e346dd5: только комментарий 225fd65 + golden-тест 97012d3)

Зачем. «Статус заказа» значит разное для разных метрик и разных МП. Единый наивный realized-statuses.yaml (как в первой версии governance-плана) опасно упрощает и приводит к тому, что агент «приводит рейтинг к канону статусов» и ломает сигнал. Здесь — семантика по метрике и по МП, с caveat'ами. Единый источник значений в коде — canon.py (REALIZED_STATUS, PRIMARY_GROUPS). Машинное зеркало — metric-status-map.yaml.


1. Группы маркетплейсов (canon.PRIMARY_GROUPS)

Первичный mp_id → суб-mp группы. Вся группа схлопывается в одну строку под первичным mp.

МППервичныйГруппа (суб-mp)Схемы
WB1{1, 2}1=FBO, 2=FBS
Ozon3{3, 4, 5}3=FBS, 4=rFBS, 5=FBO
YM6{6, 8}6=FBS, 8=FBY (mp_id=7 YM-Express в kernel ОТСУТСТВУЕТ)
Lamoda9{9}

Все суб-mp, встречающиеся как источники: (1,2,3,4,5,6,8,9).

⚠️ mp_id=7 (YM Express) нет в kernel — не добавлять в фильтры/группы как «забытый». ⚠️ Нумерация mp_id аддитивна — новые МП добавляются новыми id, историю не ремапим (решение владельца, epic-02/00). Это правило истории — см. также правило иммутабельности ниже.


2. Семантика статусов ПО МЕТРИКЕ (главное)

Один статус-фильтр НЕ подходит всем метрикам. Раскладка:

МетрикаЧто считаетСемантика статусовКод
Оборачиваемость (fact_turnover)скорость ухода стокаПРОДАНО (realized) + net-of-returnsbuild_fact_turnover.py:24-34
Рейтинг (fact_rating)доходность вложенияПРОДАНО (realized) + net-of-returns (решение владельца 2026-07-03)build_fact_rating.py:137-142 (_RATING_MP_SPEC) — см. DO-NOT-REGRESS DNR-001
Прогноз/отгрузка (shipment_distribution)дефицит vs ожидаемые продажиПРОДАНО (realized из canon)data_api/routers/reports/shipment_distribution.py:24,56-74
Продажи-эндпоинты (Data API)выкупы за периодПРОДАНО (realized per-mp из canon)data_api/routers/reports/{sales,bestsellers,dashboard,structure}.py

Realized-набор (canon.REALIZED_STATUS) — «ПРОДАНО»:

Суб-mprealized-статусИсточник
1, 2 (WB)salewb_sales, for_pay после комиссии
3, 4, 5 (Ozon)deliveredpostings delivered
6, 8 (YM)DELIVEREDym stats
9 (Lamoda)Deliveredстатус флипается на возврате

Золотое Яблоко (mp10)

Золотое Яблоко (mp10) используется только как источник текущей отгрузки: analytics/sales/table/count даёт SKU-агрегат скользящего 30-дневного окна.

В рейтинге и оборачиваемости не участвует. Агрегат не содержит ни дат событий, ни денежных сумм — включить его в эти метрики нечем. Исключение закреплено составом _RATING_MP_SPEC в build_fact_rating.py и _PRIMARY_CASE в build_fact_turnover.py: mp10 там просто нет. Семантика количества задана единственным местом — metric-status-map.yaml, здесь она намеренно не повторяется.

«Спрос» (demand) = все статусы заказа (ordered/sale/…), только Lamoda=Delivered. ⛔ В расчётах НЕ используется (решение владельца 2026-07-03: оценка только по реальным продажам, не по заказам — см. DNR-001). Термин оставлен как справочный, чтобы будущий агент не спутал его с realized и не вернул demand в рейтинг.


3. Caveat'ы, которые ломают наивные допущения

  1. ⚠️ mp2 (WB FBS) не имеет sale вовсе → realized для mp2 = 0 (проверено в БД 2026-07-03: fbs_fbs 188 строк 2026-02-04…05-18, ordered 59 строк с 2026-06-05, sale = 0). Канон маппит 2→'sale' формально; данных нет. Не пугаться «пустого» realized mp2.

  2. ⚠️ WB пишет заказ и выкуп РАЗНЫМИ строками под разными order_id: wb_orders (ordered, srid) и wb_sales (sale, sale_id), плюс legacy-backfill order/return. Realized-фильтр status='sale' берёт выкуп один раз — это и снимает двойной счёт WB. ⛔ Не суммировать все WB-статусы «чтобы не терять» — задвоишь.

  3. ⚠️ Lamoda Delivered флипается на возврате, у Ozon delivered НЕ флипается (возврат — отдельная строка), WB возврат — отдельная строка. Поэтому net-of-returns вычитается из fact_returns отдельно (DNR-005), а не «ловится статусом».

  4. ⚠️ Легаси-хвост от старого коллектора (cutover ~2026-05-20) — в ОБЕИХ факт-таблицах, проверено posting-level в БД 2026-07-03:

    • fact_orders: ~2000 non-realized строк со старым словарём статусов — WB order(mp1,719)/ return(mp1,169)/fbs_fbs(mp2,188), Lamoda Not bought(465)/Refunded(166)/Arrived|Left LM Express(90)/… Все обрываются ≤ 2026-05-20. Ozon/YM хвоста НЕ имеют.
    • fact_returns: до-майские возвраты кривые — WB amount ОТРИЦАТЕЛЬНЫЙ + quantity NULL (мар −822k, апр −330k), Ozon quantity NULL до мая, Lamoda возвратов до июня НЕТ вовсе (сбор начался в июне: 959 строк).

    Почему июнь+ ЧИСТ (хвост инертен для рейтинга/оборачиваемости/продаж): (а) realized-таймлайн гладкий через cutover (WB 1417→907→648→526; Lamoda 962→1072→828→932) — старый коллектор писал realized тем же словарём canon (sale/Delivered), продажи консистентны; (б) UNIQUE-ключ fact_orders(mp_id,order_id,model_id) физически исключает двойной счёт на стыке; (в) rolling-окна не достают до грязи — рейтинг accrual помесячно (июнь=июньские данные), turnover sales_28d к середине июня целиком в июне, а отрицательные суммы (мар-апр) отсекает guard amount>=0.

    Вердикт: пере-деривация НЕ нужна для корректности. «Baseline/golden только с ИЮНЯ 2026» (memory 83f0d3c2) достаточно и правильно. Re-derive = косметика (чистые до-июньские графики), низкий приоритет, owner-gated (трогает историю — нельзя тихо перезаписывать). НЕ скрытый баг — осознанно исключённый до-cutover период.

  5. ⚠️ Ozon analytics — псевдо-статус аналитического коллектора, это НЕ заказы (memory 27d812c0). На 2026-07-03 из fact_orders вычищен (0 строк; ранее 773 стр/899 qty искажали май). Аналитика живёт в gwptd_intake.ozon_analytics/fact_analytics — ⛔ не возвращать её строки в fact_orders и не считать как продажи/спрос. Эти же псевдо-строки дали ложный сигнал «недосбор Ozon 0.40×»: legacy mp_orders_daily их всё ещё содержит (1427 строк за июнь, id analytics_<sku>_<date>), поэтому наивное сравнение с legacy завышало «истину». Posting-level сверка (исключив analytics) = 100% покрытие, см. COVERAGE-MATRIX.md §2. При сверке Ozon с legacy ВСЕГДА фильтруй status<>'analytics'.


4. data_status (контракт Data API) — ортогонально статусам заказа

Не путать со статусами заказа. Это контракт достоверности ответа (epic-01/04,/06):

Значения — 1:1 с кодом (data_api/envelope.py:14-27; PHP-сторона app/Support/DataStatus.php обязана совпадать):

data_statusЗначение (по envelope.py)
okзапрос успешен, есть строки
no_dataзапрос успешен, 0 строк — легитимная пустота, НЕ ошибка
staleисточник/зеркало НЕДОСТУПНО (нет таблицы/гранта) → деградированный ответ вместо 5xx
errorсбой обработки (баг развёртки бизнес-источника) → 5xx + лог

Правило по типу фетча (memory c520b226): business-запрос → error при сбое; meta/mirror/ preview → stale + degraded_fields (не blanket-5xx). См. data_api/envelope.py, app/Support/DataStatus.php. Связь с DNR-007 («ok/0 строк = баг»).


5. Иммутабельность истории (forward-only) + санкционированное исключение

Правило: исторические строки снапшотов (fact_rating, rating_history) не переписываются задним числом — курс/себестоимость меняются, поэтому месяц хранится снимком (DNR-006).

Опциональная косметика (owner-gated): пере-деривация легаси-хвоста fact_orders (хвост 2025-11…2026-05-20, см. §3.4) как обязательный шаг НЕ планируется — вердикт §3.4: для корректности не нужна, re-derive = косметика, низкий приоритет. Если делать — только явно, с отмашки владельца, не «мимоходом». Любая другая перезапись истории требует owner-gate + ADR.