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) | Схемы |
|---|---|---|---|
| WB | 1 | {1, 2} | 1=FBO, 2=FBS |
| Ozon | 3 | {3, 4, 5} | 3=FBS, 4=rFBS, 5=FBO |
| YM | 6 | {6, 8} | 6=FBS, 8=FBY (mp_id=7 YM-Express в kernel ОТСУТСТВУЕТ) |
| Lamoda | 9 | {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-returns | build_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) — «ПРОДАНО»:
| Суб-mp | realized-статус | Источник |
|---|---|---|
| 1, 2 (WB) | sale | wb_sales, for_pay после комиссии |
| 3, 4, 5 (Ozon) | delivered | postings delivered |
| 6, 8 (YM) | DELIVERED | ym 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'ы, которые ломают наивные допущения
-
⚠️ mp2 (WB FBS) не имеет
saleвовсе → realized для mp2 = 0 (проверено в БД 2026-07-03:fbs_fbs188 строк 2026-02-04…05-18,ordered59 строк с 2026-06-05,sale= 0). Канон маппит2→'sale'формально; данных нет. Не пугаться «пустого» realized mp2. -
⚠️ WB пишет заказ и выкуп РАЗНЫМИ строками под разными
order_id:wb_orders(ordered, srid) иwb_sales(sale, sale_id), плюс legacy-backfillorder/return. Realized-фильтрstatus='sale'берёт выкуп один раз — это и снимает двойной счёт WB. ⛔ Не суммировать все WB-статусы «чтобы не терять» — задвоишь. -
⚠️ Lamoda
Deliveredфлипается на возврате, у OzondeliveredНЕ флипается (возврат — отдельная строка), WB возврат — отдельная строка. Поэтому net-of-returns вычитается изfact_returnsотдельно (DNR-005), а не «ловится статусом». -
⚠️ Легаси-хвост от старого коллектора (cutover ~2026-05-20) — в ОБЕИХ факт-таблицах, проверено posting-level в БД 2026-07-03:
fact_orders: ~2000 non-realized строк со старым словарём статусов — WBorder(mp1,719)/return(mp1,169)/fbs_fbs(mp2,188), LamodaNot bought(465)/Refunded(166)/Arrived|Left LM Express(90)/… Все обрываются ≤ 2026-05-20. Ozon/YM хвоста НЕ имеют.fact_returns: до-майские возвраты кривые — WBamountОТРИЦАТЕЛЬНЫЙ +quantityNULL (мар −822k, апр −330k), OzonquantityNULL до мая, 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 помесячно (июнь=июньские данные), turnoversales_28dк середине июня целиком в июне, а отрицательные суммы (мар-апр) отсекает guardamount>=0.Вердикт: пере-деривация НЕ нужна для корректности. «Baseline/golden только с ИЮНЯ 2026» (memory
83f0d3c2) достаточно и правильно. Re-derive = косметика (чистые до-июньские графики), низкий приоритет, owner-gated (трогает историю — нельзя тихо перезаписывать). НЕ скрытый баг — осознанно исключённый до-cutover период. -
⚠️ Ozon
analytics— псевдо-статус аналитического коллектора, это НЕ заказы (memory27d812c0). На 2026-07-03 изfact_ordersвычищен (0 строк; ранее 773 стр/899 qty искажали май). Аналитика живёт вgwptd_intake.ozon_analytics/fact_analytics— ⛔ не возвращать её строки вfact_ordersи не считать как продажи/спрос. Эти же псевдо-строки дали ложный сигнал «недосбор Ozon 0.40×»: legacymp_orders_dailyих всё ещё содержит (1427 строк за июнь, idanalytics_<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.