Контекст: смета
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:1EstimateLine↔query_item_id. Non-fixedPricingMode(on_request,unavailable) не суммируется в total — Estimate должен обрабатывать эти режимы отдельно.
Главный пользовательский сценарий: клиент загружает грязную смету → система парсит, находит кандидаты, считает цены, оптимизирует, объясняет. Estimate — orchestrator: вызывает Search, Pricing, Matching через интеграционные события / use-case API. Не владеет товарными или ценовыми данными.
Главный смысл
Каждая позиция объяснима: «выбрано потому что …». Это критическая черта продукта — система не чёрный ящик, а прозрачный ассистент. Estimate версионируется: любое изменение пользователем создаёт новую версию.
Агрегаты / сущности / value objects
| Имя | Тип | Назначение |
|---|---|---|
Estimate | 🟨 Aggregate | Смета. Версионируется. Owner — customer_ref. |
EstimateVersion | E | Конкретная версия (immutable). |
EstimateLine | E | Одна позиция сметы (после парсинга). |
EstimateLineDraft | VO | Сырая строка с полями + confidence. |
EstimateOptimization | VO | Mode + constraints. |
OptimizationMode | VO enum | min_price / min_suppliers / min_lead_time / single_manufacturer. |
EstimateAlternative | VO | Альтернативный offer для строки (2-3 на позицию). |
EstimateExplanation | VO | Структурированное «почему именно этот offer». |
EstimateExportFormat | VO enum | xlsx / 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) | Customer | Estimate | EstimateUploaded |
РаспознатьСтроки (ParseLines) | EstimateBuilder | Estimate | EstimateLineParsed* |
НайтиКандидатов (FindCandidates) | EstimateBuilder | — (вызов Search) | SearchCandidatesReturned* |
ВыбратьКандидата (SelectCandidate) | Customer / auto | EstimateLine | EstimateLineCandidateSelected |
ЗапроситьЦены (RequestPricing) | EstimateBuilder | — (вызов Pricing) | PricingResolved* |
ОптимизироватьСмету (Optimize) | EstimateBuilder | Estimate | EstimateOptimized |
Зафиксировать (Finalize) | Customer | Estimate | EstimateFinalized |
Экспортировать (Export) | Customer | — | EstimateExported |
СоздатьВерсию (CreateVersion) | Customer (любое изменение) | Estimate | новая EstimateVersion |
Политики
| Триггер | Реакция |
|---|---|
EstimateUploaded | → ParseLines |
EstimateLineParsed + есть mpn/sku | → FindCandidates (артикульный поиск) |
EstimateLineParsed без mpn/sku | → FindCandidates (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) — для каждой позиции: что выбрано, почему.
Инварианты
EstimateLine.canonical_product_refсуществует (если matched).- В оптимизированной смете все позиции имеют
match_confidence ≥ strong, если иное не разрешено пользователем явно. - Breakdown pricing сохраняется вместе с estimate (для аудита и повторного расчёта). При истечении TTL — флаг
stale, не удаляется. Estimateверсионируется: любое изменение клиента → новаяEstimateVersion. Старые версии immutable.- Позиции с
pricing_mode = on_requestне суммируются в total; маркируются явно. - 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, EstimateExported | Lifecycle |
CustomerEstimateRequested | Сигнал для Ingestion: refresh observations для линий |
Подписанные интеграционные события
| Источник | Событие | Реакция |
|---|---|---|
| Search | SearchCandidatesReturned | Заполнение кандидатов в линию |
| Pricing | PricingResolved | Заполнение breakdown |
| Offers | OfferObservationRecorded | Mark stale для затронутых смет |
| Offers | OfferLifecycleStatusChanged=discontinued | Alert клиенту: позиция X стала discontinued |
Связи в context map
| BC | Паттерн | Назначение |
|---|---|---|
| Search | OHS (Search → Estimate) | Estimate orchestrator вызывает Search use-case |
| Pricing | OHS (Pricing → Estimate) | То же для Pricing |
| Matching | PL (Matching → Estimate) | confidence уровни и orphans влияют на UX |
| Visibility | SK | Filter offers, показываемых в estimate |
| Customer | SK (SessionContext) | Auth + ownership |
| Ingestion | PL (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
Связанные файлы
- Сценарий:
../scenarios/estimate-end-to-end.md. search.md,pricing.md,matching.md,visibility.md,customer.md,ingestion.md.- ADR-0014 (graceful degradation), ADR (TBD: оптимизатор).