Runbook: Systeme — обогащение товарных карточек

Severity (default): P2 Owner: Ассортимент / Catalog Core Связанные логи: ingestion.bulk.card_details.fetched, ingestion.bulk.card_details.skipped, ingestion.bulk.card_details.freshness_failed

Симптом

У карточек Systeme есть только артикул или несколько базовых полей, хотя в публичной карточке https://api.systeme.ru/catalog/view/<артикул> видны ТН ВЭД, native-характеристики, ETIM, нетто-габариты и уровни брутто-упаковки. Либо карточки перестали обновляться после включения enrichment.

Диагностика

Сначала убедиться, что production-конфигурация включена через deploy-managed template, а не локальный override контейнера:

rg -n '^SUPPLIER_SYSTEME_CARD_DETAILS_(MAX_ITEMS_PER_TICK|REFRESH_TTL)=' deploy/prod-env-template.env

Проверить, что существует ровно одна уже заведённая общая характеристика для ТН ВЭД. До её ручной модерации не создавать новую с похожим именем.

SELECT c.id, c.code, c.name, c.data_type, c.status,
       count(m.id) AS mapping_count
  FROM characteristics c
  LEFT JOIN char_name_mappings m
    ON lower(m.canonical_name) = lower(c.code)
    OR lower(m.canonical_name) = lower(c.name)
 WHERE lower(c.code) IN ('tnved', 'tn_ved', 'код_тн_вэд', 'код_тнвэд')
    OR lower(c.name) IN ('код тн вэд', 'код тнвэд')
 GROUP BY c.id, c.code, c.name, c.data_type, c.status
 ORDER BY c.status DESC, c.code;

Проверить целевую mapping и данные двух контрольных SKU после прохождения supplier-sync → charnorm → canonical assignment:

SELECT m.supplier, m.supplier_code, m.canonical_name, m.display_name_ru,
       m.value_type, m.confidence, m.needs_review
  FROM char_name_mappings m
 WHERE m.supplier = 'systeme'
   AND m.supplier_code = 'systeme/tnved';
 
SELECT o.supplier_sku, r.source_ref, r.supplier_code, r.display_name,
       r.raw_value, r.observed_at
  FROM supplier_offers o
  JOIN offer_characteristic_raw r ON r.offer_id = o.id
 WHERE o.supplier = 'systeme'
   AND o.supplier_sku IN ('0N-1046SE', 'A9D49625')
 ORDER BY o.supplier_sku, r.source_ref, r.supplier_code;

Ожидаемое: у 0N-1046SE есть systeme/tnved = 8532220000; у A9D49625systeme/tnved = 8536201007, native/ETIM факты и source_ref = systeme_card. Полные брутто-упаковки и hierarchy находятся losslessly в supplier_offers.raw_attributes -> 'systeme_card'.

В live API поле systeme_card.product.brand приходит объектом с name, а не строкой. Маппер обязан принимать обе формы; иначе ошибка декодирования одного поля отбрасывает всю карточку, хотя lossless raw-снимок уже сохранён. Быстрая проверка формы ответа:

SELECT supplier_sku,
       jsonb_typeof(raw_attributes #> '{systeme_card,product,brand}') AS brand_shape,
       raw_attributes #>> '{systeme_card,product,brand,name}' AS brand_name
  FROM supplier_offers
 WHERE supplier = 'systeme'
   AND supplier_sku IN ('0N-1046SE', 'A9D49625');

Ожидаемое production-значение brand_shape = object. После исправления контракта повторный card tick должен создать rows с source_ref = systeme_card и поставить отдельную freshness-метку этого source.

SELECT supplier_sku,
       raw_attributes #> '{systeme_card,packingGross}' AS packing_gross,
       raw_attributes #> '{systeme_card,hierarchy}' AS hierarchy
  FROM supplier_offers
 WHERE supplier = 'systeme'
   AND supplier_sku IN ('0N-1046SE', 'A9D49625');

Смягчение

Если официальный API возвращает 429/5xx, временно остановить только дополнительный карточечный поток: установить в deploy-managed конфигурации SUPPLIER_SYSTEME_CARD_DETAILS_MAX_ITEMS_PER_TICK=0 и выполнить обычный deployment. Базовый getdata/цена/остатки Systeme продолжат работать.

После deployment проверить, что прекращаются новые события ingestion.bulk.card_details.fetched, а catalog job завершается без ошибок. Не добавлять proxy pool и не переключаться на HTML-scraping.

Устранение root cause

  1. Исправить доступность/лимит официального api.systeme.ru либо контракт endpoint’а.
  2. Вернуть ограниченный budget (начинать с 500) через deploy/prod-env-template.env; TTL 720h означает обновление только собственного systeme_card snapshot примерно раз в 30 дней.
  3. Если systeme/tnved остался unmapped, повторить read-only query выше. После того как он вернул ровно одну существующую характеристику, вручную подставить её точный code вместо <existing_tnved_code>:
INSERT INTO char_name_mappings
    (supplier, supplier_code, canonical_name, display_name_ru,
     unit_symbol, canonical_unit, value_type, confidence, needs_review,
     version, inputs_hash, source, llm_model, updated_at)
VALUES
    ('systeme', 'systeme/tnved', '<existing_tnved_code>', 'Код ТН ВЭД',
     NULL, NULL, 'string', 1.0, false,
     1, 'manual:systeme/tnved:v1', 'manual', NULL, now())
ON CONFLICT (supplier, supplier_code) DO UPDATE SET
    canonical_name = EXCLUDED.canonical_name,
    display_name_ru = EXCLUDED.display_name_ru,
    unit_symbol = NULL,
    canonical_unit = NULL,
    value_type = 'string',
    confidence = 1.0,
    needs_review = false,
    version = char_name_mappings.version + 1,
    inputs_hash = EXCLUDED.inputs_hash,
    source = 'manual',
    llm_model = NULL,
    updated_at = now();
  1. Run the normal charnorm/projector workload; do not write canonical_assignments directly. The category classifier receives the resulting shared facts on its next unassigned-category pass.

Эскалация

  • Если api.systeme.ru gives persistent 429/403/5xx for more than 30 minutes: Ассортимент + owner of supplier integration.
  • If the preflight query returns zero or more than one TН ВЭД characteristic: Catalog Core data steward; do not choose or create a duplicate automatically.
  • If raw rows exist but canonical assignments do not appear after charnorm/projector: Charnorm / Catalog projector on-call.

Связано

  • rate-limit-exhausted.md
  • backend/internal/core/ingestion/infra/systeme/card_details.go
  • backend/internal/core/normalization/infra/systeme/offer_mapper.go
  • backend/internal/core/categoryclass/infra/postgres/unassigned_reader.go