Контекст: каталог

NOTE

Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см. backend/internal/core/) или пока не начат. Правила маркировки — в 50-processes/documentation-standard.md.

Назначение

Хранит единую модель товара — CanonicalProduct — независимую от того, как товар представлен у конкретного поставщика. Catalog — источник истины «что считается одним и тем же товаром» и «какие у этого товара характеристики».

Главный смысл

identity_signature уникален: два разных canonical product не могут иметь одинаковую сигнатуру; одинаковые offers разных поставщиков сходятся в один canonical.

Агрегаты / сущности / value objects

ИмяТипНазначение
CanonicalProduct🟨 AggregateЕдиная единица товара. Корень: identity_signature. Содержит media: [MediaAsset], certificates: [MediaAsset], videos: [MediaAsset], external_identities: [ExternalIdentity] (набор ID из federated taxonomies: ETIM class, GTIN, MPN, OKPD2, eCl@ss — ADR-0028).
IdentityProfileEШаблон критических характеристик. Deep-dive: ../product-identity.md.
ManufacturerEПроизводитель + aliases[].
CharacteristicEАтрибут: key, value_type, default_unit. Deep-dive: ../characteristics.md.
UnitEЕдиница измерения с to_canonical.
EquivalenceClassEГруппа взаимозаменяемых canonical products.
IdentitySignatureVOstable_hash(manufacturer, mpn?, profile, critical_attrs).
LifecycleStatusVO enumnew / active / deprecated / discontinued.
ClassificationTagVOСвободный тег (НЕ иерархия).
MediaAssetVO(kind, url_small?, url_large?, mime, format?, watermarked, rights_source_credential_ref?). Kind: image / video / certificate / datasheet.
SupplierCharacteristicMapping🟨 AggregateACL маппинг: (supplier_ref, supplier_char_code) → MappingTarget. MappingTarget = Direct(tracium_key) | ViaTaxonomy(standard_ref, external_code). Event-sourced. ADR-0028.
SupplierClassificationMapping🟨 AggregateACL маппинг: (supplier_ref, supplier_class_code) → MappingTarget (Direct [tags] или ViaTaxonomy). Event-sourced.
SupplierManufacturerMapping🟨 AggregateACL маппинг: (supplier_ref, supplier_manufacturer_code) → Manufacturer.id. Event-sourced.
IdentityStandard🟨 AggregateСправочник federated taxonomies: (code, version?, domain, kind, status). Стартовый seed: etim v6/v7, gs1_gtin, mpn, okpd2. Расширяемый (eCl@ss, UNSPSC, ISO 13584) через admin UI. ADR-0028.
TaxonomyCharacteristicMapping🟨 Aggregate(standard_ref, external_feature_code) → Characteristic.key + unit_override?. Один маппинг переиспользуется N поставщиками.
TaxonomyClassMapping🟨 Aggregate(standard_ref, external_class_code) → [ClassificationTag] (multi-tag).
TaxonomyValueMappingVOДля enum-значений: (standard_ref, external_value_code) → tracium_value.
ExternalIdentityVOMarker на CanonicalProduct: (standard_ref, external_code). Несколько одновременно.
MappingTargetVODiscriminated union: Direct / ViaTaxonomy.
MappingSourceVO enumdeclared / imported / inferred / operator / moderation_accepted.
MappingConfidenceVO(score: float, method: string) — для inferred / imported записей.

Полный реестр — ../data-model.md.

Доменные события

Событие (рус → en)ПричинаИсточник
КаноническийТоварСоздан (CanonicalCreated)Появился новый identity_signaturePolicy Matching: MatchDecided=unmatched
ХарактеристикаДобавлена (CharacteristicAdded)Добавлен normalized атрибутКоманда ОбновитьХарактеристикиТовара
ХарактеристикаИзменена (CharacteristicChanged)Изменено значение / unitТо же
ХарактеристикаУдалена (CharacteristicRemoved)Удалён атрибутManual / Matching rebuild
МедиаДобавлены (MediaAssetAdded)Добавлено image / video / certificate / datasheetEnrichment / manual / ProposedAction
МедиаУдалены (MediaAssetRemoved)Удалены устаревшие assetsOperator
КонтентОбновлён (ContentUpdated)Описание / SEO / агрегированные media измененыEnrichment / manual
СтатусЖизненногоЦиклаИзменён (LifecycleStatusChanged)new → active → deprecated → discontinuedPolicy / manual
ПреемникУстановлен (ReplacedBySet)discontinued получил преемникаManual / supplier feed
ПрофильИдентичностиСоздан (IdentityProfileCreated)Новый профильManual / ML proposal
КритическийАтрибутДобавлен (CriticalAttributeAdded)Profile.critical_attribute_keys расширенADR-required
КлассЭквивалентностиСформирован (EquivalenceClassFormed)Группа аналогов созданаManual / rule-driven
МаппингХарактеристикиДобавлен (SupplierCharacteristicMappingAdded)Новая запись в SupplierCharacteristicMappingDictionary import / operator / moderation
МаппингКлассификацииДобавлен (SupplierClassificationMappingAdded)Новая запись в SupplierClassificationMappingImport / operator / moderation
МаппингПроизводителяДобавлен (SupplierManufacturerMappingAdded)Новая запись в SupplierManufacturerMappingImport / operator / moderation
МаппингПодтверждён (MappingConfirmedByModeration)AI-агент или операторвинд подтвердил inferred-маппингModeration BC
СтандартИдентичностиЗарегистрирован (IdentityStandardRegistered)Новый federated standard (ETIM/eCl@ss/GS1/…)Admin UI
МаппингТаксономииХарактеристикДобавлен (TaxonomyCharacteristicMappingAdded)Import / operator / moderationCanonical Catalog
МаппингТаксономииКлассаДобавлен (TaxonomyClassMappingAdded)То же
ВнешнийИдентификаторПрисвоен (ExternalIdentityAssigned)CanonicalProduct получил marker (ETIM class / GTIN / MPN)Matching / operator
ВнешнийИдентификаторУдалён (ExternalIdentityRevoked)Маркер оказался ошибочнымModeration

Команды

КомандаАкторЦелевой агрегатРезультат
СоздатьКаноническийТовар (CreateCanonical)MatchingCanonicalProductКаноническийТоварСоздан
ОбновитьХарактеристикиТовара (UpdateCharacteristics)Enrichment / MatchingCanonicalProductХарактеристика*
ИзменитьСтатусТовара (ChangeLifecycleStatus)PolicyCanonicalProductСтатусЖизненногоЦиклаИзменён
УстановитьПреемника (SetReplacedBy)Moderation BC consumer (через ProposedAction)CanonicalProductПреемникУстановлен
ПрименитьРешениеМодерации (ApplyModerationDecision)Moderation BC consumerCanonicalProduct / Manufacturerсоответствующее domain event + ack
СоздатьIdentityProfile (CreateIdentityProfile)Operator (RFC) — структурное решениеIdentityProfileПрофильИдентичностиСоздан
ДобавитьКритическийАтрибут (AddCriticalAttribute)Operator (ADR) — структурное breakingIdentityProfileКритическийАтрибутДобавлен (breaking)
СоздатьКлассЭквивалентности (FormEquivalenceClass)Policy / rule / Moderation suggestEquivalenceClassКлассЭквивалентностиСформирован
ДобавитьAliasПроизводителя (AddManufacturerAlias)Moderation BC consumer (через ProposedAction)ManufacturerManufacturerAliasAdded

Политики

ТриггерРеакция
MatchDecided=unmatched (Matching) + есть identity_signature→ команда СоздатьКаноническийТовар
OfferCharacteristicsUpdated (Offers) + match=exact/strong→ команда ОбновитьХарактеристикиТовара. Lookup цепочка: SupplierCharacteristicMapping(supplier_ref, char_code) → MappingTarget. Если Direct — использовать tracium_key напрямую. Если ViaTaxonomy — второй lookup TaxonomyCharacteristicMapping(standard_ref, external_code) → tracium_key. Если ни один mapping не найден → CharacteristicMappingProposalOpened → Moderation.
OfferObservationRecorded + connector Capabilities.Taxonomies.NativeCoding=true + offer несёт external codes (ETIM class, GTIN, MPN)→ команда AssignExternalIdentity для связанного Canonical (если matched) → ExternalIdentityAssigned. Этот marker дальше используется Matching как high-confidence сигнал.
OfferMediaUpdated (Offers) + match=exact/strong→ команда ОбновитьМедиаТовара (merge с учётом watermark priority: watermarked=false перетирает watermarked=true для того же logical_id)
CharacteristicEnrichedByAI (Enrichment, non-critical + confidence ≥ threshold)→ команда ОбновитьХарактеристикиТовара напрямую
ProposedAction(AcceptEnrichment) от Moderation (critical attr)→ команда ОбновитьХарактеристикиТовара + ack
ProposedAction(SetReplacedBy|AddManufacturerAlias|AcceptMapping) от Moderation→ соответствующая команда + ack
Все active offers стали discontinued (Offers)→ команда ИзменитьСтатусТовара(deprecated)
CriticalAttributeAdded (Catalog)→ команда RematchAll для затронутых canonicals
Кластер orphans с похожими characteristics обнаружен (Matching)→ publish IdentityProfileProposalCreated → AI-агент identity_profile_proposal (всегда defer на operator RFC)
EnrichmentJobCompleted{kind=characteristic_dictionary_refresh} (Ingestion)→ merge новых mappings в SupplierCharacteristicMapping; неизвестные коды → Moderation
EnrichmentJobCompleted{kind=manufacturer_dictionary_refresh}→ merge в SupplierManufacturerMapping; для новых Manufacturer’ов — predict alias через ML, fallback на Moderation
EnrichmentJobCompleted{kind=classification_dictionary_refresh}→ merge в SupplierClassificationMapping; multi-tag preserved (1 supplier_class_code → N ClassificationTag)

Read-модели

  • 🟩 canonical_index (Elasticsearch) — для Search.
  • 🟩 canonical_with_active_offers (PG materialized) — для админки и Pricing pre-fetch.
  • 🟩 equivalence_graph — для подбора аналогов.

Инварианты

  1. identity_signature уникален в системе.
  2. manufacturer_ref всегда канонизирован (не алиас).
  3. lifecycle_status = discontinuedreplaced_by опционально, но если задан — тоже canonical, не offer.
  4. Все characteristics[].unit соответствуют value_type своей Characteristic.
  5. Critical characteristics не могут быть null у CanonicalProduct. Если все источники молчат — canonical не создаётся, offer становится orphan.
  6. IdentityProfile.critical_attribute_keys — break-change при изменении: только через ADR.
  7. EquivalenceClass.members[] — только canonical products одного IdentityProfile (или явно многопрофильный flag).
  8. MediaAsset.watermarked = false на canonical допустим только если есть хотя бы один offer-источник с credential, имеющей media_unwatermarked feature (ADR-0024).
  9. SupplierCharacteristicMapping уникален по (supplier_ref, supplier_char_code). MappingTarget — ровно один: либо Direct, либо ViaTaxonomy. Не оба одновременно.
  10. SupplierClassificationMapping допускает multi-tag: 1 supplier-код → N ClassificationTag. Но Tracium ClassificationTag остаётся плоским (ADR), иерархия поставщика распаковывается в tag-set.
  11. Любое изменение mapping публикует интеграционное событие — downstream (Matching, Enrichment) должны инвалидировать кеши по supplier_ref.
  12. TaxonomyCharacteristicMapping уникален по (standard_ref, external_feature_code). Один external_code → один tracium_key. Разные external_code разных стандартов могут указывать на один и тот же tracium_key (это ожидаемо: ETIM EF001234 и eCl@ss AD-VEV-00123 оба = nominal_current_A).
  13. CanonicalProduct.external_identities[] — set по (standard_ref, external_code). Канонический не может иметь два разных GTIN (если такое замечено — Moderation review).
  14. IdentityStandard.domain — справочный; НЕ блокирует использование (электрический товар может иметь и ETIM, и GS1).

Интеграционные события (публикуем)

Топик: catalog.events.v1. Partition key: aggregate_id (canonical_id).

ИмяКогда
CanonicalCreatedСоздание canonical
CanonicalCharacteristicsChangedЛюбые изменения characteristics (для Search reindex, Enrichment)
CanonicalMediaChangedMedia update (для Search / UI)
CanonicalLifecycleStatusChangedДля Pricing / Search фильтров
IdentityProfileChangedДля Matching invalidation
EquivalenceClassChangedДля Search analog index

Топик: catalog.mappings.v1. Partition key: supplier_ref.

ИмяКогда
SupplierCharacteristicMappingAdded/Changed/RemovedИзменения ACL справочника характеристик
SupplierClassificationMappingAdded/Changed/RemovedИзменения ACL справочника классификации
SupplierManufacturerMappingAdded/Changed/RemovedИзменения ACL справочника производителей

Топик: catalog.taxonomies.v1. Partition key: standard_ref.

ИмяКогда
IdentityStandardRegistered/Changed/DeprecatedИзменения в справочнике federated standards
TaxonomyCharacteristicMappingAdded/Changed/RemovedИзменения ETIM/eCl@ss/… характеристик маппинга
TaxonomyClassMappingAdded/Changed/RemovedТо же для классификации
TaxonomyValueMappingAdded/Changed/RemovedДля enum-значений

Топик: catalog.external_identities.v1. Partition key: canonical_id.

ИмяКогда
ExternalIdentityAssignedCanonical получил marker (ETIM class, GTIN, MPN, …)
ExternalIdentityRevokedMarker оказался ошибочным

Подписанные интеграционные события

ИсточникСобытиеЗачем
MatchingMatchDecidedТриггер СоздатьКаноническийТовар или линковка
EnrichmentCharacteristicEnrichedByAI (non-critical)Применение к canonical
ModerationProposedAction(AcceptEnrichment|SetReplacedBy|AddManufacturerAlias)ApplyModerationDecision
OffersOfferLifecycleStatusChangedВозможный downgrade canonical

Связи в context map

BCПаттернНазначение
MatchingACL (upstream от Catalog)Защищает canonical от шумных supplier-данных. Catalog не работает с raw supplier payload.
OffersCustomer/Supplier (Catalog — supplier)Catalog не зависит от Offers; Offers — downstream consumer canonical_id.
EnrichmentPublished LanguageEnrichment слушает CanonicalCharacteristicsChanged, публикует CharacteristicEnrichedByAI.
SearchOpen Host ServiceCatalog публикует событиями; Search строит read-проекцию.
ModerationCustomer/SupplierНе-структурные ручные действия (replaced_by, alias, accept critical enrichment) делегированы AI-агентам. Только IdentityProfile / CriticalAttribute остаются за человеком (RFC/ADR).

Мини event storming

flowchart LR
    subgraph M["Matching (ACL)"]
        MD["🟧 MatchDecided<br/>=unmatched"]
    end
    subgraph C["Catalog"]
        P1["🟪 Policy: новый identity_signature"]
        CMD1["🟦 СоздатьКаноническийТовар"]
        CP["🟨 CanonicalProduct"]
        E1["🟧 КаноническийТоварСоздан"]
        E2["🟧 ХарактеристикаДобавлена"]
    end
    subgraph E["Enrichment"]
        EE["🟧 CharacteristicEnrichedByAI"]
    end
    subgraph S["Search"]
        SR["🟩 canonical_index"]
    end

    MD --> P1 --> CMD1 --> CP --> E1
    EE -.subscribed.-> CP
    CP --> E2
    E1 --> SR
    E2 --> SR

Связанные файлы