COLLECTORS-SPEC — Ozon (12 коллекторов)
Детали механики вызова Ozon Seller API. Общие паттерны — в COLLECTORS-SPEC.md.
Field-mapping'и — в data-model/intake-ozon.md и
data-model/intake-ozon-2.md. Пути file:line — от
marketplace-collector-v3/collectors/.
Общее: base = OZON["base_url"] = https://api-seller.ozon.ru; авторизация — заголовки
Client-Id + Api-Key в __init__ каждого коллектора. mp_id: FBS=3, rFBS=4, FBO=5 (config.py:35-37).
Все bounded-циклы помечены «epic-10/04».
1. ozon.product_info_list — каталог товаров
-
OzonProductInfoListCollector, mp_id=3 (ozon_product_info_list.py:21-23). Dual-write вручнуюmappings/ozon/product_info_list.yaml(:16, :104) →ozon_product_meta. -
POST
/v3/product/list(:40), body{"filter":{"visibility":"ALL"}, "last_id", "limit":1000}(:43-47). -
Снапшот, окна нет. Пагинация last_id,
_MAX_PAGES=500(:18):new_last_id = data.get("result", {}).get("last_id", "") # :61-64
if not new_last_id or new_last_id == last_id or len(items) < 1000:
break
last_id = new_last_id -
Таблица:
gwptd_intake.ozon_product_info_list(:89).
2. ozon.product_sku_map — карта sku↔offer_id
OzonProductSkuMapCollector, mp_id=3 (ozon_product_sku_map.py:26-28). Intake-only (:12).- Два шага: POST
/v3/product/list(last_id-пагинация, стоп :52-55,_MAX_PAGES=500:23) → POST/v3/product/info/list, body{"product_id": batch}батчами_BATCH=1000(:22, :63-66) — из ответа берутсяsources[].sku. - Дедуп пар
(sku, source_type)черезseen(:82-92). Таблица:gwptd_intake.ozon_product_sku_map(:97, :111). Назначение — резолв аналитики/финансов в kernel; V3-native замена legacyozon_product_meta(:2-12).
3. ozon.postings_fbs — постинги FBS/rFBS
-
OzonPostingsFbsCollector, mp_id класса = 3 (ozon_postings_fbs.py:39-41). Dual-write вручнуюmappings/ozon/postings_fbs.yaml(:19, :149) →mp_orders_daily. -
POST
/v4/posting/fbs/list(:58; миграция с /v3 deprecated 2026, :3). Body (:66-71):{"filter":{"since","to"}, "limit":100, "sort_dir":"ASC", "with":{"analytics_data":true,"financial_data":true}}. -
Окно:
since = today−7,to = today+1(ISOT00:00:00.000Z, :59-60). -
Ловушки v4 (:61-64): limit max 100 (не 1000); НЕ передавать пустой
cursorв первом запросе — пустая строка → HTTP 400; cursor добавляется в body только если непуст (:72-73). -
Пагинация cursor+has_next,
_MAX_PAGES=500(:21):has_next = data.get("has_next", False) # :89-93
new_cursor = data.get("cursor", "")
if not has_next or not new_cursor or new_cursor == cursor:
break
cursor = new_cursor -
Развод FBS/rFBS по mp_id:
_posting_mp_id()(:31-36) —tpl_integration_type in ("aggregator","hybrid")→ mp_id=4 (rFBS), иначе 3.delivery_schema='sds'API больше не отдаёт — не использовать (:32-34). -
Таблица:
gwptd_intake.ozon_postings_fbs(:100).
4. ozon.postings_fbo — постинги FBO
OzonPostingsFboCollector, mp_id=5 (ozon_postings_fbo.py:31-33). Dual-write вручнуюmappings/ozon/postings_fbo.yaml(:19, :131) →mp_orders_daily.- POST
/v3/posting/fbo/list(:50; миграция с /v2 deprecated, :3). Body/окно/пагинация — как у FBS: since=today−7, to=today+1 (:51-52), limit 100 (:53), cursor только при наличии (:62-63), стоп по has_next/cursor (:78-82),_MAX_PAGES=500(:21). - mp_id жёстко 5 при записи (:117), без rFBS-развилки. Таблица:
gwptd_intake.ozon_postings_fbo(:89).
5. ozon.stocks — остатки (агрегат по схемам)
-
OzonStocksCollector, mp_id=3 default (ozon_stocks.py:20-22, фактический тип стока вstocks_jsonпо fbo/fbs/rfbs, :3-4). Dual-write вручнуюmappings/ozon/stocks.yaml(:17, :103) →mp_stocks_daily. -
POST
/v4/product/info/stocks(:39), body{"cursor": last_id, "filter":{...,"visibility":"ALL"}, "limit":1000}(:44-48). -
Пагинация cursor с жёстким бондом
for _ in range(10)— максимум 10 страниц, «cursor бывает кривой» (:43); дедуп items черезseenпоoffer_id|product_id(:41, :59-64); стоп:if cursor == last_id or new_count == 0 or len(items) < 1000: # :70-72
break
last_id = cursor -
Таблица:
gwptd_intake.ozon_stocks(:78).
6. ozon.stock_on_warehouses — остатки FBO по складам
OzonStockOnWarehousesCollector, mp_id=5 (ozon_stock_on_warehouses.py:31-33). Без dual-write: legacymp_stocks_dailyнаполняет Laravel-коллекторOzonStocks.php(:14-15).- POST
/v2/analytics/stock_on_warehouses, body{"limit":1000, "offset":offset}(:44, :48). - Пагинация offset, бонд
range(50)= максимум 50 000 строк (:47); стоп — неполная страница (:65-67). - Даёт реальные названия складов/РФЦ (в отличие от агрегата ozon.stocks) — нужен для кластерной
отгрузки FBO (:3-7). Таблица:
gwptd_intake.ozon_stock_on_warehouses(:75).
7. ozon.prices — цены каталога (F-03)
-
OzonPricesCollector, mp_id=3 (ozon_prices.py:21-23). Dual-write вручнуюmappings/ozon/prices.yaml(:16, :134). -
POST
/v5/product/info/prices(:40), body{"filter":{...,"visibility":"ALL"}, "limit":1000}- cursor только при наличии (:45-50). Пагинация cursor,
_MAX_PAGES=500(:18), стоп :67-70.
- cursor только при наличии (:45-50). Пагинация cursor,
-
F-03 — страховка от битого cursor: v5-cursor ломался → молча собиралась только первая страница. Из ответа запоминается
total(:44, :58-62), после цикла сверка:# ozon_prices.py:74-84
if total is not None and collected < total:
self.logger.warning(
f"Ozon prices undercollected: {collected}/{total} items — possible broken cursor")
if self.run is not None:
self.run.rows_skipped += total - collectedНедобор → WARNING + rows_skipped → base.py помечает прогон
partialи шлёт алерт. -
Таблица:
gwptd_intake.ozon_prices(:88).
8. ozon.finance_transactions — финансовые транзакции
OzonFinanceTransactionsCollector, mp_id=3 (ozon_finance_transactions.py:29-31). Dual-write вручнуюmappings/ozon/finance_transactions.yaml(:17, :133-134).- POST
/v3/finance/transaction/list(:48), body{"filter":{"date":{"from","to"}, "operation_type":[], "posting_number":"", "transaction_type":"all"}, "page", "page_size":1000}(:52-61). - Окно:
today−14 … today(T00:00:00Z/T23:59:59Z, :49-50). - Пагинация page-нумерованная,
_MAX_PAGES=500(:19); стоп:page >= result.page_count(:74-75). - Таблица:
gwptd_intake.ozon_finance_transactions(:83).
9. ozon.returns — возвраты (FBS+FBO unified)
-
OzonReturnsCollector, mp_id=3 (ozon_returns.py:28-31). Dual-write вручнуюmappings/ozon/returns.yaml(:16, :117-118). -
POST
/v1/returns/list(:47), body{"filter":{"logistic_return_date":{"time_from","time_to"}}, "last_id", "limit":500}(:50-59). Окно:today−14 … today(:54-55). -
Пагинация числовой last_id + has_next,
_MAX_PAGES=500(:18):new_last_id = data.get("last_id") or 0 # :71-73
if not new_last_id or new_last_id == last_id or not data.get("has_next"):
break -
Схема FBS/FBO размечается из
r["schema"]илиplace.place_type(:99). Таблица:gwptd_intake.ozon_returns(:81).
10. ozon.analytics — продажи по SKU (F-04)
-
OzonAnalyticsCollector, mp_id=3 (ozon_analytics.py:22-24, агрегат идёт как FBS). Dual-write вручнуюmappings/ozon/analytics.yaml(:17, :121-122). -
POST
/v1/analytics/data(:41), body{"date_from": today-7, "date_to": today, "dimension":["sku","day"], "metrics":["ordered_units","revenue"], "limit":1000, "offset"}(:42-60). -
F-04 (0d90d26) — offset-пагинация с бондом:
_MAX_PAGES=100«страховка от бесконечного offset-цикла, не while True» (:19); стоп/инкремент:if len(rows) < limit: # :74-76
break
offset += limit -
Ловушка метрик (:51-56):
returns/returns_unitsdeprecated на /v1/analytics/data —returnsв одиночку → HTTP 400 «deprecated metrics used», в комбинации молча дропается (проверено на s3 2026-07-03); выживают толькоordered_units+revenue. Полеreturns_unitsпишется честнымNone(:116); реальные возвраты — ozon.returns. -
Таблица:
gwptd_intake.ozon_analytics(:83).
11. ozon.actions — акции/промо
OzonActionsCollector, mp_id=3 (ozon_actions.py:35-37);legacy_mapping_path = mappings/ozon/actions.yaml(:17, :39) → dual-write вmp_promotionsчерез BaseCollector.- GET
/v1/actions(:49-50) — одиночный запрос, без body, без пагинации;result→ все акции (:55-59). Snapshot-семантика: каждый прогон пишет полный список (:4-5). - Таблица:
gwptd_intake.ozon_actions(:64).
12. ozon.storage — расходы на хранение FBO
OzonStorageCollector, mp_id=5 (ozon_storage.py:30-33);legacy_mapping_path = mappings/ozon/storage.yaml(:24, :34) →mp_storage_costsчерез BaseCollector (:124).- У Ozon НЕТ отдельного storage-API: POST
/v3/finance/transaction/listс фильтромoperation_type=["OperationMarketplaceServiceStorage"](:3-6, :26, :44, :48-56). - Окно:
today−27 … today— API-лимит «only one month allowed», 27 дн = safety margin для 30-дневных месяцев (:8-9, :45-46). - Пагинация page,
_MAX_PAGES=100(:27); стопpage >= page_count(:79-81). - Дневной агрегат без разбивки по SKU (
items=[]):offer_id="_ozon_storage_total"(:71);amountчерезabs()— в API расход отрицательный (:9, :73). Таблица:gwptd_intake.ozon_storage(:89).
Сводка по осям
- Пагинация: last_id — №1, №2(шаг 1), №9; cursor — №3, №4, №5, №7; page/page_count — №8, №12; offset — №6, №10; без пагинации — №11; батчи — №2(шаг 2).
- mp_id: 3 (FBS) — №1, 2, 3(default), 5, 7, 8, 9, 10, 11; 4 (rFBS) — только строки №3 через
_posting_mp_id; 5 (FBO) — №4, 6, 12. - Legacy dual-write через BaseCollector — только №11, №12; вручную
Mapper.write_item— №1, 3, 4, 5, 7, 8, 9, 10; intake-only — №2, №6. use_cacheнигде не отключён (async-паттернов у Ozon нет).