Контекст: сеть поставщиков

NOTE

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

Назначение

Моделирует поставщиков как граф ролей и связей. Производитель → дистрибьютор → агрегатор → агент → склад. Вычисляет SupplyChainTrace для каждого offer (snapshot цепочки). Используется Pricing, Visibility, Offers, Matching для understanding origin данных.

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

Поставщик в реальности — не атомарная единица, а узел в графе. Один и тот же бизнес-субъект может быть manufacturer, distributor, api_provider и warehouse_operator одновременно. Граф изменяется отдельным flow, trace для offers пересчитывается event-driven.

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

ИмяТипНазначение
Supplier🟨 AggregateУзел графа. Корень: supplier_id.
SupplierKindVO enummanufacturer / distributor / aggregator / marketplace / marketplace_seller / agent / warehouse_operator / logistics / mixed.
SupplierRoleEОдна из ролей supplier’а. Расширяемый словарь.
RoleKindCodeVOapi_provider / warehouse_operator / sales_agent / manufacturer / distributor / aggregator / logistics.
RoleScopeVOfull / classification_tag:{tag} / manufacturer_group:{id} / warehouse:{code}.
SupplierRelationshipEНаправленное ребро.
RelationshipKindCodeVOaggregates / resells / is_agent_of / uses_api_of / uses_warehouse_of / subsidiary_of / exclusive_for / listed_on / GENERIC.
TrustLevelVO enumorigin / tier_1 / tier_2 / unverified.
SupplyChainTraceVOShared Kernel. Snapshot цепочки для offer/observation. Содержит nodes[] + warehouse_chain[].
WarehouseChainLinkVOЭлемент warehouse_chain: (warehouse_ref, warehouse_kind, lead_time_days?).
WarehouseEСклад, привязан к warehouse_operator. Обязательное поле kind: WarehouseKind.
WarehouseKindVO enumregional_center / logistics_center / pickup_office / manufacturer_warehouse / third_party / transit. Фиксированный enum (как TrustLevel) — расширяется только через ADR. См. ADR-0025.
SellerELightweight узел внутри агрегатора (SupplierKind=aggregator|marketplace). Корень: (supplier_ref, external_seller_id). Не имеет своей credential/session. ADR-0026.
SellerStatusVO enumactive / inactive / banned.
SellerRatingVO(score 0..5, sample_size?, observed_at, source). Snapshot на Seller + history в CH.
RelationshipSourceVO enumdeclared / observed / inferred.
ConfidenceVOДля inferred отношений.

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

СобытиеПричина
ПоставщикДобавлен (SupplierCreated)Operator
РольПоставщикаДобавлена (SupplierRoleAdded)Operator / observation
РольПоставщикаУдалена (SupplierRoleRemoved)Operator
СвязьУстановлена (SupplierRelationshipEstablished)Operator / observation / inferred
СвязьИзменена (SupplierRelationshipUpdated)Operator
СвязьУдалена (SupplierRelationshipRemoved)Operator
СловарьРолейРасширен (RoleKindDictionaryExtended)Operator (admin UI)
СловарьСвязейРасширен (RelationshipKindDictionaryExtended)Operator
ЦепочкаПоставокПересчитана (SupplyChainTraceRecomputed)Policy: при изменении графа / создании offer
ЦикловОбнаружено (SupplierGraphCycleDetected)Validator (alert, не block)
ВыводСвязиЗапрошен (InferredRelationshipProposed)ML/heuristics → moderation
ПродавецЗарегистрирован (SellerRegistered)Marketplace connector первый раз увидел seller’а
РейтингПродавцаОбновлён (SellerRatingUpdated)Новый snapshot rating из fetch’а
СтатусПродавцаИзменён (SellerStatusChanged)active ↔ inactive ↔ banned (observed исчезновение seller’а N раз подряд → inactive)
ПродавецПовышенДоПоставщика (SellerPromotedToSupplier)Operator решил оформить seller’а как полноценный Supplier aggregate

Команды

КомандаАкторЦелевой агрегатРезультат
СоздатьПоставщика (CreateSupplier)OperatorSupplierSupplierCreated
ДобавитьРоль (AddRole)OperatorSupplierRoleSupplierRoleAdded
УстановитьСвязь (EstablishRelationship)OperatorSupplierRelationshipSupplierRelationshipEstablished
ПересчитатьTrace (RecomputeTrace)Engine— (per offer/observation)SupplyChainTraceRecomputed
ПодтвердитьInferredСвязь (ConfirmInferredRelationship)OperatorSupplierRelationshipupgrade source declared

Политики

ТриггерРеакция
SupplierOfferSeen (Offers)RecomputeTrace (initial)
SupplierRelationshipEstablished/Updated/RemovedRecomputeTrace для всех offers с participating suppliers
SupplierRoleAdded/RemovedRecomputeTrace затронутых offers
Schedule (daily)RecomputeTrace всех offers (защита от пропущенных событий)
Inferred relationship с confidence ≥ threshold→ publish InferredRelationshipProposed → AI-агент inferred_relationship_review в Moderation BC
ProposedAction(ConfirmInferredRelationship) от Moderation→ upgrade source declared + ack DecisionApplied
Cycle detected→ alert, не block
Marketplace payload: новый external_seller_idSellerRegistered + SupplierRelationshipEstablished{kind=listed_on, source=observed}
Marketplace payload: обновлённый ratingSellerRatingUpdated (только если diff ≥ min_delta, избегаем шума)
Seller отсутствует в fetch N раз подряд (default 5)SellerStatusChanged{status=inactive}. Через M дней inactive → banned без operator-override → не используется в Pricing.
Operator promotes sellerSellerPromotedToSupplier + миграция observations на новый supplier_ref + закрытие listed_on ребра

Read-модели

  • 🟩 supplier_graph_view (PG materialized + ES) — для визуализации в админке.
  • 🟩 current_supply_chain_traces (PG materialized) — (offer_id) → trace.
  • 🟩 relationships_pending_review (PG) — inferred → moderation.

Инварианты

  1. kind отражает основную роль; roles[] — полный набор.
  2. trust_level влияет на conflict resolution и автоматический матчинг. 2a. Seller lightweight invariant (ADR-0026): Seller entity существует только в контексте listed_on связи с одним Supplier{kind=aggregator|marketplace}. Seller без aggregator’а — invalid state. 2b. Seller default trust_level = unverified; upgrade возможен только через SellerPromotedToSupplier (становится полным Supplier) или через moderation decision. 2c. SellerRating.score в диапазоне [0, 5]; score=null допустимо (marketplace не отдал). При отсутствии rating Pricing tie-break использует только trust_level + price + freshness.
  3. SupplyChainTrace — value object, не aggregate; пересчитывается отдельным consumer’ом.
  4. Цикл в графе детектируется и помечается алертом, но не запрещён (могут быть валидные cross-роли).
  5. inferred отношения не попадают напрямую в trace — только после подтверждения модератором.
  6. Manufacturer (Catalog) и Supplier с roles=[manufacturer] — связаны через manufacturer.supplier_ref (оптionally).
  7. Warehouse.kind обязательно и присваивается при создании. Изменение kind — через отдельный command ReclassifyWarehouse + audit.
  8. kind = manufacturer_warehouse автоматически присваивается при observed_in_payload с trust_level=origin, если payload пришёл напрямую из API производителя или из InfoSuppStores-подобного поля поставщика-дистрибьютора. Линк Warehouse.supplier_ref в этом случае указывает на Supplier с ролью manufacturer.
  9. SupplyChainTrace.warehouse_chain пересчитывается синхронно с SupplyChainTraceRecomputed; отражает путь товара: manufacturer_warehouse → logistics_center → pickup_office (в типичном для ЭТМ случае).

Источники отношений

  • declared — заведено вручную (договоры, бизнес-знание).
  • observed — извлечено из payload (e.g., ETM в mnf_code указывает manufacturer → implicit resells).
  • inferred — выведено ML (overlap прайсов → вероятно aggregates).

inferred → AI-агент Moderation BC (inferred_relationship_review) перед попаданием в граф; человек только при escalation.

Использование SupplyChainTrace

ГдеКак
PricingDiscovery альтернативных источников (тот же canonical через разные chains, разная цена)
PricingBreakdown с структурой маржи (origin / dist / aggregator / our markup)
MatchingConflict resolution через trust_level (origin > tier_1 > tier_2)
VisibilityPredicate real_manufacturer_in, chain_includes_supplier, chain_depth_gt, warehouse_kind_includes
Estimatemin_lead_time optimization использует warehouse_chain + StockForecast для эффективного lead time
Audit / complianceПроисхождение для маркируемой / медицинской продукции
Phase 6+ — execution routingМаршрутизация заказа по цепочке

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

Топик: supplier.graph.events.v1 (см. ADR-0017). Partition key: supplier_id для графовых; offer_id для trace.

ИмяКогда
SupplierCreated, SupplierRoleAdded, SupplierRoleRemovedИзменения узлов
SupplierRelationshipEstablished/Updated/RemovedИзменения рёбер
RoleKindDictionaryExtended, RelationshipKindDictionaryExtendedИзменения словарей (ADR-0016)
SupplyChainTraceRecomputedPer offer — для Offers / Pricing / Visibility
SupplierGraphCycleDetectedДля алерта

Топик: supplier.sellers.v1. Partition key: aggregator_supplier_id.

ИмяКогда
SellerRegistered, SellerStatusChanged, SellerRatingUpdated, SellerPromotedToSupplierADR-0026. Consumers: Pricing (invalidate cache), admin UI, CH sink.

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

ИсточникСобытиеРеакция
OffersSupplierOfferSeenInitial RecomputeTrace
CatalogManufacturerCreatedВозможный auto-link к Supplier с manufacturer role

Связи в context map

BCПаттернНазначение
OffersSK (SupplyChainTrace VO)Snapshot trace вкладывается в SupplierOffer
PricingSK (SupplyChainTrace, TrustLevel)Conflict resolution, breakdown
VisibilitySKChain-предикаты
MatchingSK (TrustLevel)Влияет на confidence priority
CatalogCustomer/Supplier (Catalog ↔ Supplier Network через Manufacturer.supplier_ref)
ModerationCustomer/Supplierinferred отношения проверяет AI-агент, не человек

Мини event storming

flowchart LR
    OP["🟫 Operator"]
    subgraph SN["Supplier Network"]
        ADD["🟦 EstablishRelationship"]
        SR["🟨 SupplierRelationship"]
        E1["🟧 SupplierRelationshipEstablished"]
        P1["🟪 Policy: пересчитать traces"]
        REC["🟦 RecomputeTrace"]
        E2["🟧 SupplyChainTraceRecomputed"]
    end
    subgraph OFF["Offers"]
        OS["🟧 SupplierOfferSeen"]
        TR["🟦 SupplyChainTrace VO"]
    end
    subgraph PR["Pricing"]
        SUBP["🟪 Subscribe SupplyChainTraceRecomputed"]
    end
    subgraph V["Visibility"]
        SUBV["🟪 chain-predicates"]
    end

    OP --> ADD --> SR --> E1 --> P1 --> REC --> E2
    OS -.PL.-> REC
    E2 -.PL.-> TR
    E2 -.SK.-> SUBP
    E2 -.SK.-> SUBV

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