Контекст: смета

NOTE

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

Назначение

Estimate обращается к proposal pipeline через POST /v1/search/proposals с QueryItemID для каждой строки сметы. Response несёт proposals_by_item / items_outcome — связь 1:1 EstimateLinequery_item_id. Non-fixed PricingMode (on_request, unavailable) не суммируется в total — Estimate должен обрабатывать эти режимы отдельно.

Главный пользовательский сценарий: клиент загружает грязную смету → система парсит, находит кандидаты, считает цены, оптимизирует, объясняет. Estimate — orchestrator: вызывает Search, Pricing, Matching через интеграционные события / use-case API. Не владеет товарными или ценовыми данными.

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

Каждая позиция объяснима: «выбрано потому что …». Это критическая черта продукта — система не чёрный ящик, а прозрачный ассистент. Estimate версионируется: любое изменение пользователем создаёт новую версию.

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

ИмяТипНазначение
Estimate🟨 AggregateСмета. Версионируется. Owner — customer_ref.
EstimateVersionEКонкретная версия (immutable).
EstimateLineEОдна позиция сметы (после парсинга).
EstimateLineDraftVOСырая строка с полями + confidence.
EstimateOptimizationVOMode + constraints.
OptimizationModeVO enummin_price / min_suppliers / min_lead_time / single_manufacturer.
EstimateAlternativeVOАльтернативный offer для строки (2-3 на позицию).
EstimateExplanationVOСтруктурированное «почему именно этот offer».
EstimateExportFormatVO enumxlsx / pdf.

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

СобытиеПричина
СметаЗагружена (EstimateUploaded)Customer → upload (CSV / XLSX / текст)
СтрокаСметыРаспознана (EstimateLineParsed)Parser распознал поля строки
КандидатыНайдены (SearchCandidatesReturned)Search вернул результаты
КандидатВыбран (EstimateLineCandidateSelected)Customer / auto при match_confidence ≥ strong
ОшибкаПоказанаПользователю (EstimateLineErrorPresented)Низкая confidence / ambiguous → UX
СметаОптимизирована (EstimateOptimized)ILP / greedy завершился
АльтернативаСгенерирована (AlternativeGenerated)2-3 варианта на позицию
СметаЗафиксирована (EstimateFinalized)Customer подтвердил версию
СметаЭкспортирована (EstimateExported)XLSX / PDF / API
СметаУстарела (EstimateBecameStale)Истёк TTL pricing breakdown — нужно пересчитать

Команды

КомандаАкторЦелевой агрегатРезультат
ЗагрузитьСмету (UploadEstimate)CustomerEstimateEstimateUploaded
РаспознатьСтроки (ParseLines)EstimateBuilderEstimateEstimateLineParsed*
НайтиКандидатов (FindCandidates)EstimateBuilder— (вызов Search)SearchCandidatesReturned*
ВыбратьКандидата (SelectCandidate)Customer / autoEstimateLineEstimateLineCandidateSelected
ЗапроситьЦены (RequestPricing)EstimateBuilder— (вызов Pricing)PricingResolved*
ОптимизироватьСмету (Optimize)EstimateBuilderEstimateEstimateOptimized
Зафиксировать (Finalize)CustomerEstimateEstimateFinalized
Экспортировать (Export)CustomerEstimateExported
СоздатьВерсию (CreateVersion)Customer (любое изменение)Estimateновая EstimateVersion

Политики

ТриггерРеакция
EstimateUploadedParseLines
EstimateLineParsed + есть mpn/skuFindCandidates (артикульный поиск)
EstimateLineParsed без mpn/skuFindCandidates (textual + semantic)
SearchCandidatesReturned + один кандидат с match_confidence ≥ strong→ auto SelectCandidate
SearchCandidatesReturned иначеEstimateLineErrorPresented (UX confirm)
EstimateLineCandidateSelected (все позиции)RequestPricing для всех
PricingResolved (для всех)Optimize
OfferObservationRecorded (Offers) для canonical в смете→ invalidate cache → флаг EstimateBecameStale
Finalize + features.allow_estimate_approval→ ожидание согласования

Read-модели

  • 🟩 customer_estimates (PG) — список смет клиента.
  • 🟩 estimate_explainability_log (CH) — для каждой позиции: что выбрано, почему.

Инварианты

  1. EstimateLine.canonical_product_ref существует (если matched).
  2. В оптимизированной смете все позиции имеют match_confidence ≥ strong, если иное не разрешено пользователем явно.
  3. Breakdown pricing сохраняется вместе с estimate (для аудита и повторного расчёта). При истечении TTL — флаг stale, не удаляется.
  4. Estimate версионируется: любое изменение клиента → новая EstimateVersion. Старые версии immutable.
  5. Позиции с pricing_mode = on_request не суммируются в total; маркируются явно.
  6. Customer владеет смет(ами); другой customer не может прочитать чужую смету через любой API (RBAC + Visibility).

Целевая функция оптимизации

  • min_price — минимизировать сумму.
  • min_suppliers — балансировать цену и количество контрагентов.
  • min_lead_time — минимизировать max(delivery_time).
  • single_manufacturer — ограничение «только один производитель».

Ограничения:

  • Остатки на складах.
  • Минимальные партии (поставщиковые кратности).
  • Географические ограничения поставки.
  • Пользовательские whitelisting/blacklisting поставщиков.

Алгоритм:

  • ≤ 200 позиций → ILP / MIP.
  • 200 → greedy с эвристиками + балансировка.

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

Топик: estimate.events.v1. Partition key: estimate_id.

ИмяКогда
EstimateUploaded, EstimateLineParsed, EstimateOptimized, EstimateFinalized, EstimateExportedLifecycle
CustomerEstimateRequestedСигнал для Ingestion: refresh observations для линий

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

ИсточникСобытиеРеакция
SearchSearchCandidatesReturnedЗаполнение кандидатов в линию
PricingPricingResolvedЗаполнение breakdown
OffersOfferObservationRecordedMark stale для затронутых смет
OffersOfferLifecycleStatusChanged=discontinuedAlert клиенту: позиция X стала discontinued

Связи в context map

BCПаттернНазначение
SearchOHS (Search → Estimate)Estimate orchestrator вызывает Search use-case
PricingOHS (Pricing → Estimate)То же для Pricing
MatchingPL (Matching → Estimate)confidence уровни и orphans влияют на UX
VisibilitySKFilter offers, показываемых в estimate
CustomerSK (SessionContext)Auth + ownership
IngestionPL (Estimate → Ingestion)Публикует CustomerEstimateRequested для high-priority refresh

Мини event storming

flowchart LR
    U["🟫 Customer"]
    subgraph EST["Estimate"]
        UP["🟦 UploadEstimate"]
        EU["🟧 EstimateUploaded"]
        PARSE["🟦 ParseLines"]
        EP["🟧 EstimateLineParsed"]
        FC["🟦 FindCandidates"]
        ECS["🟧 SearchCandidatesReturned"]
        SEL["🟦 SelectCandidate"]
        ECC["🟧 EstimateLineCandidateSelected"]
        RP["🟦 RequestPricing"]
        PR["🟧 PricingResolved"]
        OPT["🟦 Optimize"]
        EOPT["🟧 EstimateOptimized"]
        FIN["🟧 EstimateFinalized"]
    end
    subgraph S["Search"]
        SR["🟩 SearchResult"]
    end
    subgraph PG["Pricing"]
        PE["🟦 Compute"]
    end

    U --> UP --> EU --> PARSE --> EP --> FC
    FC -.OHS.-> SR --> ECS --> SEL --> ECC --> RP
    RP -.OHS.-> PE --> PR --> OPT --> EOPT --> FIN

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