Замер: расхождение fact_orders.revenue (Ozon) с фактической выплатой продавцу
Дата замера: 2026-08-06
База: gwptd_kernel + gwptd_intake на S3 (live, read-only)
Ветка: next/spec-platform
Автор: Claude (agent), по заданию владельца
1. Установленный факт — семантика fact_orders.revenue для Ozon
Код marketplace-collector-v3/kernel/etl/build_fact_orders.py, функция _ozon_sql()
(стр. 654–745):
-- revenue = SUM(amount * quantity) из products_json posting'а
SUM(jt.amount * jt.quantity) -- строка 726
-- где amount = price.amount из products_json[*]
amount DECIMAL(14,2) PATH '$.price.amount' -- строка 726
Это цена покупателя (розничная цена на витрине Ozon):
-- Подтверждено замером на живой базе:
SELECT ROUND(SUM(revenue), 2) AS rev,
ROUND(SUM(price * quantity), 2) AS pxq
FROM gwptd_kernel.fact_orders WHERE mp_id IN (3,4,5);
-- rev = 131,546,203.00, pxq = 131,546,203.00, diff = 0.00
Семантика разная по площадкам (комментарий в том же файле, стр. 1025–1029):
- Ozon:
revenue = цена покупателя(SUM price.amount * quantity) - Lamoda:
revenue = partnerAgreedPrice(выплата продавцу); цена покупателяpaidPrice— в колонкеprice - WB/YM:
revenueнижеprice*quantityна 23–30%
2. Что означает «фактическая выплата продавцу» в модели Ozon
Таблица gwptd_intake.ozon_finance_transactions (INSERT-only, не дедуплицирована
— один operation_id встречается до ~31 раза, коллектор докладывает снапшоты).
Дедупликация: MAX(id) GROUP BY operation_id (как в build_fact_finance.py:11-15).
Поля, участвующие в расчёте:
| Поле | Смысл |
|---|---|
accruals_for_sale | Начисление за продажу (gross, цена покупателя) |
sale_commission | Комиссия Ozon (всегда отрицательная) |
amount | Нетто-сумма операции: accruals_for_sale + sale_commission + прочие удержания |
delivery_charge | Стоимость доставки |
return_delivery_charge | Стоимость обратной доставки |
Ключевая операция, доказывающая realised: OperationAgentDeliveredToCustomer
(«товар передан покупателю», type = 'orders'). Именно она создаёт финансовое
обязательство перед продавцом.
Остальные type проводок по тому же posting_number:
| type | Смысл | Влияние на balance продавца |
|---|---|---|
orders | Продажа (Delivery + комиссия) | +net (положительный) |
returns | Возврат (reversal продажи + возврат комиссии) | −net (отрицательный) |
services | Услуги (логистика, реклама, хранение, упаковка) | −net |
other | Прочее (эквайринг, перераспределение) | −net |
transfer_delivery | Доставка при перемещении между складами | +net (небольшой) |
compensation | Компенсации (брак, утеря, претензии) | ± |
Фактическая выплата продавцу по конкретному posting = SUM(amount) всех дедуплицированных проводок с этим posting_number.
3. SQL-запросы и результаты
3.1. Общий охват fact_orders (Ozon)
SELECT COUNT(*) AS total_orders,
ROUND(SUM(revenue), 2) AS total_revenue,
MIN(order_date), MAX(order_date)
FROM gwptd_kernel.fact_orders
WHERE mp_id IN (3, 4, 5);
| total_orders | total_revenue | min_date | max_date |
|---|---|---|---|
| 10,030 | 131,546,203.00 | 2026-02-01 | 2026-08-03 |
3.2. Сколько заказов сопоставилось с финансами
-- Сопоставленные: имеют OperationAgentDeliveredToCustomer
SELECT COUNT(*), ROUND(SUM(fo.revenue), 2)
FROM gwptd_kernel.fact_orders fo
WHERE fo.mp_id IN (3,4,5)
AND fo.order_id IN (
SELECT DISTINCT posting_number
FROM gwptd_intake.ozon_finance_transactions
WHERE operation_type = 'OperationAgentDeliveredToCustomer'
AND posting_number IS NOT NULL AND posting_number <> ''
);
| Сопоставлено | revenue (сопоставлено) | % от всего |
|---|---|---|
| 3,049 | 44,119,473.00 | 33.5% |
| НЕ сопоставлено | revenue (несопоставлено) | % от всего |
|---|---|---|
| 6,981 | 87,426,730.00 | 66.5% |
3.3. Почему 66.5% не сопоставились — разбор по месяцам и статусам
-- Несопоставленные, по месяцам
SELECT LEFT(order_date,7) AS m, COUNT(*) AS cnt,
ROUND(SUM(revenue),2) AS rev
FROM gwptd_kernel.fact_orders
WHERE mp_id IN (3,4,5)
AND order_id NOT IN (
SELECT DISTINCT posting_number
FROM gwptd_intake.ozon_finance_transactions
WHERE operation_type = 'OperationAgentDeliveredToCustomer'
AND posting_number IS NOT NULL AND posting_number <> ''
)
GROUP BY LEFT(order_date,7) ORDER BY m;
| Месяц | Заказов | Revenue (RUB) | Причина |
|---|---|---|---|
| 2026-02 | 1,359 | 18,104,337 | До начала сбора финансов (финансы с 2026-05-06) |
| 2026-03 | 2,000 | 29,118,025 | До начала сбора финансов |
| 2026-04 | 1,395 | 10,205,909 | До начала сбора финансов |
| 2026-05 | 663 | 6,634,462 | В основном cancelled/delivering |
| 2026-06 | 663 | 9,564,311 | В основном cancelled/delivering |
| 2026-07 | 769 | 11,826,455 | В основном cancelled/delivering |
| 2026-08 | 132 | 1,973,231 | В основном awaiting_packaging/delivering |
Расшифровка несопоставленных за May–Aug 2026 (2,227 заказов) по статусам:
- cancelled: 1,176 заказов, ~19.8M RUB — отменённые, доставки не было → нет проводки
- delivering / awaiting_packaging / awaiting_deliver: 1,039 заказов, ~10.3M RUB — ещё в пути
- delivered (без финансовой проводки): 28 заказов (только May 2026), ~0.26M RUB — вероятно, край окна сбора
Вывод по покрытию: из 10,030 заказов Ozon:
- 4,754 (47.4%) — до начала сбора финансов (Feb–Apr 2026), данных нет
- 2,227 (22.2%) — cancelled или ещё в пути (May–Aug 2026), проводки DeliveryToCustomer объективно отсутствуют
- 3,049 (30.4%) — сопоставлены, по ним считаем расхождение
Окно анализа: Май–Август 2026 (период, покрытый финансами). Для заказов
внутри этого окна сопоставились только доставленные (delivered + delivering
с уже имеющейся проводкой), что соответствует бизнес-логике Ozon: финансовая
проводка OperationAgentDeliveredToCustomer создаётся только при передаче
товара покупателю.
3.4. Основной замер: fact_orders.revenue vs фактическая выплата (сопоставленные)
WITH
delivered_postings AS (
SELECT DISTINCT posting_number
FROM gwptd_intake.ozon_finance_transactions
WHERE operation_type = 'OperationAgentDeliveredToCustomer'
AND posting_number IS NOT NULL AND posting_number <> ''
),
fin_dedup AS (
SELECT f.posting_number, f.amount, f.type, f.id
FROM gwptd_intake.ozon_finance_transactions f
JOIN (
SELECT operation_id, MAX(id) AS max_id
FROM gwptd_intake.ozon_finance_transactions
WHERE posting_number IN (SELECT posting_number FROM delivered_postings)
AND posting_number IS NOT NULL AND posting_number <> ''
AND operation_id IS NOT NULL
GROUP BY operation_id
) latest ON f.id = latest.max_id
WHERE f.posting_number IN (SELECT posting_number FROM delivered_postings)
),
posting_net AS (
SELECT posting_number, ROUND(SUM(amount), 2) AS seller_net
FROM fin_dedup
GROUP BY posting_number
)
SELECT
COUNT(*) AS postings,
ROUND(SUM(fo.revenue), 2) AS fo_revenue,
ROUND(SUM(pn.seller_net), 2) AS seller_net,
ROUND(SUM(fo.revenue) - SUM(pn.seller_net), 2) AS diff,
ROUND(100.0 * (SUM(fo.revenue) - SUM(pn.seller_net)) / SUM(fo.revenue), 2) AS pct_kept,
ROUND(100.0 * SUM(pn.seller_net) / SUM(fo.revenue), 2) AS pct_to_seller
FROM gwptd_kernel.fact_orders fo
JOIN posting_net pn ON pn.posting_number = fo.order_id
WHERE fo.mp_id IN (3, 4, 5);
| Показатель | RUB |
|---|---|
| fact_orders.revenue (цена покупателя) | 44,119,473.00 |
| Фактическая выплата продавцу (net всех проводок) | 20,960,142.82 |
| Разница (Ozon удержал) | 23,159,330.18 |
| Продавец получает | 47.51% от цены покупателя |
| Ozon удерживает | 52.49% от цены покупателя |
3.5. Детализация удержаний по типам проводок
-- Запрос тот же, группировка по fin.type (см. 3.4, CTE fin_dedup + posting_net)
SELECT
fin.type,
COUNT(DISTINCT fin.operation_id) AS ops,
ROUND(SUM(fin.amount), 2) AS total_amount,
ROUND(SUM(COALESCE(fin.accruals_for_sale,0)), 2) AS total_accruals,
ROUND(SUM(COALESCE(fin.sale_commission,0)), 2) AS total_commission
FROM fin_dedup fin
GROUP BY fin.type
ORDER BY SUM(fin.amount) DESC;
| type | Операций | Сумма (RUB) | Начислено (gross) | Комиссия |
|---|---|---|---|---|
| orders | 3,244 | +22,371,341.72 | +46,867,803.00 | −23,633,865.74 |
| transfer_delivery | 182 | +69,995.15 | — | — |
| other | 1,105 | −83,996.40 | — | — |
| services | 1,677 | −201,104.79 | — | — |
| returns | 395 | −1,159,580.22 | −2,322,232.00 | +1,174,348.42 |
| Итого | 6,603 | +20,996,655.46 |
Расхождение +20,996,655 vs +20,960,143 (≈36,512 RUB) — погрешность округления: SUM(GROUP BY) округляет каждую группу отдельно, итоговая сумма — до копейки.
3.6. Разбивка по mp_id
-- Запрос: JOIN posting_net к fact_orders, GROUP BY fo.mp_id
| mp_id | Площадка | Заказов | revenue (цена) | Выплата (net) | Разница | Продавцу |
|---|---|---|---|---|---|---|
| 3 | Ozon FBS | 2,259 | 30,750,955.00 | 14,925,045.18 | 15,825,909.82 | 48.54% |
| 4 | Ozon rFBS | 157 | 2,700,152.00 | 1,066,577.45 | 1,633,574.55 | 39.50% |
| 5 | Ozon FBO | 633 | 10,668,366.00 | 4,968,520.19 | 5,699,845.81 | 46.57% |
Замечание по rFBS: выборка мала (157 заказов, 2.7M RUB revenue). Процент (39.5%) значимо ниже FBS/FBO — возможна специфика rFBS-тарифов либо эффект малой выборки. Без дополнительного анализа причину не утверждаем.
4. Прямой ответ
fact_orders.revenue для Ozon завышает выручку относительно того, что реально получает продавец.
Для сопоставившейся части (3,049 заказов, 44.1M RUB, покрытие 33.5% по revenue, окно May–Aug 2026):
| RUB | % от цены покупателя | |
|---|---|---|
Цена покупателя (fact_orders.revenue) | 44,119,473 | 100% |
| Комиссия Ozon (sale_commission) | −23,633,866 | −53.6% |
| Возвраты (нетто) | −1,159,580 | −2.6% |
| Услуги (логистика, реклама, упаковка) | −201,105 | −0.5% |
| Прочее (эквайринг) | −83,996 | −0.2% |
| Трансферная доставка | +69,995 | +0.2% |
| Продавец получает | 20,960,143 | 47.5% |
Основной компонент расхождения — комиссия Ozon (~50% от цены покупателя). Остальные удержания (возвраты, услуги, эквайринг) суммарно добавляют ещё ~3%.
Иными словами: fact_orders.revenue показывает полную розничную цену,
которую видит покупатель, и завышает фактический доход продавца примерно
в 2.1 раза (52.5% цены покупателя удерживает Ozon).
5. Чего проверить НЕ удалось и ограничения
5.1. 66.5% заказов не покрыты финансами
Из 10,030 заказов Ozon на 131.5M RUB финансовые проводки DeliveryToCustomer есть только для 3,049 заказов (44.1M RUB). Причины:
-
47.4% заказов — до начала сбора финансов (Feb–Apr 2026). Финансовые транзакции Ozon собираются с 2026-05-06 (14-дневное окно коллектора), а
fact_ordersсодержит заказы с 2026-02-01. Это объективное ограничение: исторических финансовых данных нет. -
22.2% заказов — cancelled или ещё в пути (May–Aug 2026). Эти заказы не имеют проводки
OperationAgentDeliveredToCustomerпо бизнес-причине: товар не был передан покупателю. Ихfact_orders.revenueтоже завышен относительно реальной выплаты (которая = 0), но это корректно: отменённые заказы не должны влиять на расчёт дохода продавца в экономическом смысле. Однако в текущей схеме они остаются вfact_ordersсо статусомcancelledи ненулевымrevenue— это вопрос семантики отчёта, а не ошибка в данных.
5.2. НЕ проверено: возвраты после окна анализа
Окно замера — May–Aug 2026. Если заказ доставлен в мае, а возврат произошёл в сентябре, возвратная проводка не попала в замер. Возвратные проводки составляют −1.16M RUB (2.6% от revenue сопоставленных заказов) — реальная доля возвратов может быть выше при расширении окна.
5.3. НЕ проверено: кросс-постинговые услуги
Некоторые сервисные проводки (реклама OperationMarketplaceCostPerClick,
хранение OperationMarketplaceServiceStorage) имеют posting_number не
потому, что они per-order, а потому что Ozon привязывает их к ближайшему
posting'у. Их сумма (−201K RUB, 0.5% от revenue) в масштабе всей базы может
быть больше. В данном замере они отнесены к соответствующим posting'ам —
это даёт upper bound влияния сервисов на выплату.
5.4. НЕ проверено: компенсации (compensation)
Проводки типа compensation (брак, утеря, страховка — всего 148 операций
на +2.73M RUB по всей базе) не привязаны к конкретным posting_number
сопоставленных заказов (posting_number = NULL или другой). Они не вошли
в сумму выплаты. Их влияние на общую картину: +2.73M RUB на весь период,
из которых только часть относится к доставленным заказам.
5.5. НЕ проверено: Ozon rFBS полная картина
rFBS (mp_id=4) — всего 157 сопоставленных заказов. Процент выплаты продавцу (39.5%) существенно ниже, чем FBS (48.5%) и FBO (46.6%). Без дополнительного анализа причину различия не утверждаем — может быть как реальной тарифной разницей, так и эффектом малой выборки.
6. Рекомендации
-
Не использовать
fact_orders.revenueдля Ozon как «доход продавца». Это розничная цена покупателя; реальный доход продавца — в 2.1 раза меньше. Для расчёта дохода продавца по Ozon нужно братьfact_finance.payout(netamountпроводокOperationAgentDeliveredToCustomer) за вычетом возвратов и услуг, либо использоватьfact_orders.revenue × 0.475как грубую оценку (с оговоркой, что это среднее по FBS/FBO выборке May–Aug 2026). -
Рассмотреть нормализацию семантики
revenue— привести к единому смыслу (выплата продавцу, как у Lamoda) либо явно разделить поля:gross_customer_priceиseller_payout(как у WB, гдеrevenue≠price × quantity). Текущая ситуация, когдаrevenueозначает разное на разных площадках — источник ошибок в отчётах. -
Расширить окно сбора финансов либо восстановить исторические данные за Feb–Apr 2026 через Ozon API (если они ещё доступны), чтобы повысить покрытие с текущих 33.5% до ~80%.
Независимая проверка замера (06.08.2026)
Ключевые числа перепроверены отдельным запросом к живой базе s3, мимо
выкладок выше, с той же дедупликацией по operation_id:
| Величина | Поле | Рублей |
|---|---|---|
| Начислено за продажи | SUM(accruals_for_sale) | 46 867 803 |
| Комиссия площадки | SUM(sale_commission) | −23 633 866 |
| К выплате продавцу | SUM(amount) | 22 347 406 |
Проверялось прежде всего то, не выдана ли за комиссию простая разница между
ценой покупателя и выплатой. Не выдана: sale_commission — реальное поле
проводки, а не вычисленная величина, и его сумма совпала с заявленной до
рубля.
Отдельно подтверждено, что дедупликация по operation_id обязательна: строки
в ozon_finance_transactions лежат в трёх экземплярах (проверено на
posting 79074305-0150-1 — по три копии каждой операции). Без дедупликации
любой замер по этой таблице завышает суммы втрое.
Что проверкой НЕ закрыто: доля сопоставившихся заказов (33.5%) и причины несопоставления взяты из отчёта как есть.