Маппинг ETM → Canonical

NOTE

Статус: Target service boundary. Документ описывает целевую сервисную границу. Код либо полностью отсутствует, либо существует только как scaffold — смотрите секцию «Статус документа» ниже для точного указания на код. Правила маркировки — в 50-processes/documentation-standard.md.

Как поля ETM API ложатся на нашу внутреннюю модель.

Важно: модель разделяет SupplierOffer (товарная канва) и SupplierOfferObservation (наблюдения per credential). Цены и остатки от ETM ложатся в Observation, а характеристики/MPN/упаковка — в Offer. См. ../../../../10-business/contexts/credentials.md.

Высокоуровнево — что куда

В SupplierOffer (товарная канва, общая для всех credentials)

ETMНаша модель
gdscodesupplier_offer.supplier_sku
mnf_code + mnf_namemanufacturer_alias (supplier_id="etm", external_code=mnf_code) → manufacturer_id
artsupplier_offer.manufacturer_sku_parsed
gdsClassTree[]supplier_offer.classification_tags[]
gdsChars[]supplier_offer.characteristics_from_supplier[]
certificates[]supplier_offer.certificates[]
gdsPacks[]supplier_offer.packaging[] + производные характеристики (вес, габариты)
gdsImages[]supplier_offer.media[type=image]
gdsVideos[]supplier_offer.media[type=video]

В SupplierOfferObservation (per credential, append-only)

ETMНаша модель
price, pricewnds, price_tarif, price_retailobservation.price_* (см. ниже)
Все цены = 0observation.pricing_mode = on_request
InfoStores[]observation.stock_by_warehouse[]
InforDeliveryTimeobservation.delivery_terms[]

Ключевое: одна и та же позиция (gdscode) даёт один SupplierOffer, но N observations по числу credentials, через которые она была опрошена.

Ценообразование

ETM ценаПоле в observation
price (без НДС)price_base, currency=RUB, vat_included=false
pricewnds (с НДС)price_with_vat
price_tarif (тариф производителя)price_tariff
price_retail (розничная)price_retail
Все = 0pricing_mode = on_request, все цены = null

Структура observation по цене:

supplier_offer_observation
├── id
├── supplier_offer_ref
├── credential_ref            # КАКОЙ credential дал эту цену
├── credential_scope          # system | customer
├── customer_ref              # денорм. для запросов
├── pricing_mode              # fixed | on_request
├── currency                  # "RUB"
├── price_base                # = ETM price
├── price_with_vat            # = ETM pricewnds
├── price_tariff              # = ETM price_tarif
├── price_retail              # = ETM price_retail
├── observed_at
└── raw_payload_ref

Важно: одна позиция через 5 разных credentials (1 system + 4 customer) даёт 5 разных observations с потенциально разными ценами — и это нормально, это и есть смысл договорных цен ETM.

Остатки

Также пишутся в supplier_offer_observation (вместе с ценами или отдельным observation, в зависимости от того, через какой endpoint получены).

Из /goods/{id}/remains:

Поле ETMНаше поле
InfoStores[].StoreCodeobservation.stock_by_warehouse[].warehouse_code
InfoStores[].StoreTypeobservation.stock_by_warehouse[].warehouse_type (rc/crs/op)
InfoStores[].StoreNameobservation.stock_by_warehouse[].warehouse_name (кэшируется)
InfoStores[].StoreQuantRemobservation.stock_by_warehouse[].quantity
InfoStores[].StoreDateobservation.stock_by_warehouse[].observed_at
InfoSuppStores[]observation.stock_by_warehouse[warehouse_type=manufacturer]
InforDeliveryTime.DeliveryTimeInPresobservation.delivery_terms.days_in_stock
InforDeliveryTime.DeliveryProductionTermobservation.delivery_terms.days_production

Из /goods/remains?store=X:

Поле ETMНаше поле
StoreCodeobservation.stock_by_warehouse[].warehouse_code
GdsCodesupplier_sku
Article— (сверка)
RemInfoobservation.stock_by_warehouse[].quantity (в минимальных единицах упаковки!)

Важно: warehouse_scope credential может ограничивать список складов, к которым у этой credential есть доступ. Соответственно у разных credentials наблюдаемые stock_by_warehouse[] могут отличаться.

Внимание: RemInfo в минимальных единицах упаковки (см. gdsPacks.minPack), а не в штуках. Нужна нормализация в штуки для унификации.

Характеристики (gdsChars)

Самая богатая часть. Маппинг ведётся через дополнительную таблицу supplier_attribute_alias:

supplier_attribute_alias
├── supplier_id        : "etm"
├── external_key       : ConfigCharCode (например, "132")
├── external_name      : gdsCharName (например, "Напряжение, В")
├── our_characteristic : characteristic.key (например, "voltage")
├── value_transform    : правило трансформации значения (см. ниже)
├── source             : manual / ml_inferred / from_catalog
├── confidence
└── approved_by

Трансформация значений

Примеры:

  • voltage: gdsCharVal=“660” → распарсить как число → 660 V (единица из gdsCharName «В»).
  • protection_class: gdsCharVal=“IP54” → нормализовать «IP54» → enum-значение «IP54».
  • current_type: gdsCharVal=“Переменный (AC)” → нормализовать → enum-значение ac.
  • weight: gdsCharVal=“0.71” → value=0.71, unit=kg (kg из gdsCharName «Масса, кг»).

Значения с ConfigCharIdVal

ETM даёт ConfigCharIdVal — числовой id значения в их классификаторе. Это полезно для enum-характеристик: мы можем хранить соответствие (etm_config_char_code, etm_config_char_id_val) → our_characteristic_value_id и использовать для быстрого матчинга значений «Серый» = «Gray» = «Graphite» между поставщиками.

Пример характеристик (из реального ответа)

Вход:

{"gdsCharName": "Напряжение, В", "gdsCharVal": "660", "ConfigCharCode": "132", "ConfigCharIdVal": 135}

Маппинг (после approval):

{
  "supplier_id": "etm",
  "external_key": "132",
  "external_name": "Напряжение, В",
  "our_characteristic": "voltage",
  "value_transform": {"type": "number_with_unit", "unit": "V"},
  "source": "manual",
  "approved_by": "moderator-ivan"
}

На выходе supplier_offer.characteristics содержит:

{"key": "voltage", "value": 660, "unit": "V", "original": "660"}

Производители (mnf_code → manufacturer)

Маппинг через таблицу manufacturer_alias:

manufacturer_alias
├── manufacturer_id    : uuid нашего canonical manufacturer
├── supplier_id        : "etm"
├── external_code      : mnf_code (например, "48")
├── external_name      : mnf_name (например, "Электротехник")
├── source             : from_dict / from_offer / manual
└── confidence

Инициализация dictionary: ручной прогон /info/search/r-manuf/ и занесение всех производителей в manufacturer_alias (со статусом from_dict, высокий confidence). Новые производители, появляющиеся в offer’ах, но отсутствующие в dictionary, попадают в очередь модерации.

Классификация (gdsClassTree)

Не используется как иерархия категорий. Сохраняется как набор tags:

[
  {"source": "etm", "key": "class_50", "value": "Оборудование низковольтное"},
  {"source": "etm", "key": "class_5030", "value": "Кнопки, кнопочные посты, ..."},
  {"source": "etm", "key": "class_503025", "value": "Кнопочные посты"}
]

Используется для:

  • UX-фильтров в админке («покажи все с class_50»).
  • Подсказок для matcher’а Identity Profile (object_type == "пост кнопочный" может подтверждаться наличием class_503025).

НЕ используется для identity_signature.

Упаковка и весогабариты (gdsPacks)

packaging
├── supplier_id     : "etm"
├── supplier_sku
├── pack_code       : "1"
├── pack_name       : "шт"
├── quantity        : 1
├── weight_kg       : 0.694
├── length_m        : 0.16
├── width_m         : 0.1
├── height_m        : 0.1
└── volume_l        : 1.6

Плюс производные характеристики:

  • weight = 0.694 kg.
  • dimensions = {L:0.16, W:0.1, H:0.1, unit: m}.

Медиа (gdsImages, gdsVideos)

Для каждого изображения / видео:

  1. Берём URL (префикс https://cdn.etm.ru + gdsImgRef / gdsVidSrc).
  2. Скачиваем (по расписанию, не при каждом просмотре).
  3. Сохраняем в наш S3 media/etm/<yyyy>/<mm>/<dd>/<hash>.<ext>.
  4. Получаем наш URL, прокидываем в CDN.

Изображения с водяными знаками ETM использовать нельзя → запросить у менеджера ETM доступ к изображениям без водяных знаков (см. spec.md, раздел Goods).

Сертификаты

Для каждого certificates[]:

  • Скачиваем PDF.
  • Сохраняем в S3 certificates/etm/<hash>.pdf.
  • Сохраняем метаданные (name, type) в таблицу certificate.
  • Связываем с canonical product (если там нет более канонического).

Единицы измерения (edizm)

ETM edizm:

  • "p"unit = "pcs" (штука).
  • "m"unit = "m".
  • "kg"unit = "kg".
  • … (полный список — запросить у менеджера / собрать эмпирически).

Хранится как единица продажи в supplier_offer.unit_of_sale.

Стратегия «тип кода»

  • Работаем по type=etm (стабильный, числовой).
  • type=cli использовать только если заведены соответствия в системе ETM.
  • type=mnf — для быстрой проверки артикула производителя, но не для регулярных fetch’ей.

Нормализация артикулов

ETM art (артикул) часто имеет префикс «ET», «К» и т. п. При матчинге:

  • Сохраняем оригинал в supplier_offer.manufacturer_sku_parsed.
  • Нормализуем для сравнения (удаляем внутренние ET-префиксы вида «ET054487»), более каноничным считаем mnf_code + art без префикса поставщика.