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

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 замена legacy ozon_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 (ISO T00: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: legacy mp_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.
  • 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_units deprecated на /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 нет).