Сценарий: полный путь сметы (Acts 1–8)
NOTE
Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см.
backend/internal/core/) или пока не начат. Правила маркировки — в50-processes/documentation-standard.md.
Главный пользовательский сценарий системы. Связывает почти все BC.
Триггер
Customer (сметчик / закупщик) загружает смету.
Участники
| BC | Роль |
|---|---|
| Estimate | Owner. Orchestrator всего процесса. Версионирование. |
| Search | Поиск кандидатов. |
| Matching | Determines confidence уровень. |
| Pricing | Расчёт per строка. |
| Visibility | Filter offers. |
| Customer | Subject + features. |
| Ingestion | High-priority refresh для линий сметы. |
Acts (high-level)
| Act | Что происходит |
|---|---|
| 1 | Загрузка (CSV/XLSX/text/API) |
| 2 | Парсинг + нормализация позиций |
| 3 | Поиск кандидатов |
| 4 | Обработка ошибок и подтверждение |
| 5 | Сбор предложений (offers + pricing) |
| 6 | Оптимизация |
| 7 | Результат (с альтернативами + breakdown) |
| 8 | Экспорт / переход в заказ (Phase 3+) |
Sequence diagram (high-level)
sequenceDiagram autonumber participant U as 🟫 Customer participant EST as Estimate participant ING as Ingestion participant S as Search participant M as Matching participant P as Pricing participant V as Visibility U->>EST: 🟦 UploadEstimate (Act 1) EST-->>EST: 🟧 EstimateUploaded EST->>EST: 🟦 ParseLines (Act 2, LLM-assisted) loop for each line EST-->>EST: 🟧 EstimateLineParsed end EST->>ING: publish 🟧 CustomerEstimateRequested Note over ING: high-priority refresh для линий EST->>S: 🟦 FindCandidates (Act 3) S->>V: filter S-->>EST: 🟧 SearchCandidatesReturned (per line) loop per line alt single match_confidence ≥ strong EST-->>EST: 🟧 EstimateLineCandidateSelected (auto) else иначе EST-->>U: 🟧 EstimateLineErrorPresented (Act 4) U-->>EST: 🟦 SelectCandidate EST-->>EST: 🟧 EstimateLineCandidateSelected end end EST->>P: 🟦 RequestPricing (Act 5) loop per (line, candidate) P-->>EST: 🟧 PricingResolved end EST->>EST: 🟦 Optimize (Act 6, ILP / greedy) EST-->>EST: 🟧 EstimateOptimized EST-->>EST: 🟧 AlternativeGenerated (Act 7) EST-->>U: response (UI / API) U->>EST: 🟦 Finalize EST-->>EST: 🟧 EstimateFinalized U->>EST: 🟦 Export (Act 8) EST-->>EST: 🟧 EstimateExported
Act 1 — Загрузка
Форматы:
- CSV / XLSX (столбцы: артикул / описание / количество / ожидаемая цена).
- Текстовый список.
- API JSON.
Смета может быть «грязной»: опечатки в артикулах, смешанные единицы, позиции без артикула, частичные характеристики.
UploadEstimate → Estimate{version=1, status=parsing}.
Act 2 — Парсинг и нормализация
Per строку:
- Извлечь артикул (
mpn/supplier_sku). - Извлечь наименование и характеристики из описания.
- Извлечь количество и единицу.
- (Опционально) извлечь ожидаемую цену для cross-check.
LLM-assistance для сложных случаев. Результат — EstimateLineDraft с полями + confidence per field.
После парсинга → EstimateLineParsed per строка.
Act 3 — Поиск кандидатов
Per строка → Search:
- Если есть mpn/sku — артикульный поиск (exact / fuzzy).
- Иначе — textual + semantic (hybrid BM25 + embedding).
- Возвращает top-N кандидатов с score.
SearchCandidatesReturned.
Параллельно — Estimate публикует CustomerEstimateRequested для Ingestion: high-priority refresh observations линий (чтобы Pricing на Act 5 имел свежие данные).
Act 4 — Обработка ошибок (UX)
Per строка:
match_confidence ≥ strong+ единственный кандидат → auto-select.- Multiple kandidates или
match_confidence < strong→EstimateLineErrorPresented. UI показывает «что система поняла из строки» + список кандидатов с объяснением. - Customer выбирает или корректирует строку (новая версия Estimate).
Типовые сценарии ошибок:
- Артикул с опечаткой → fuzzy предложил похожий.
- Неправильная единица («500 мм» vs «500 м») → системе пометить + просит подтвердить.
- Двусмысленное описание → multiple identity_profile → выбрать один.
Act 5 — Сбор предложений
Per (line, canonical_id):
- Pricing.Compute(customer, canonical, quantity, context) →
PricingResolvedс breakdown + observation_source. - См. подробно pricing-calculation.md.
Все offers, прошедшие Visibility, доступны для оптимизатора.
Act 6 — Оптимизация
Целевая функция (выбирает customer):
min_price— минимизировать сумму.min_suppliers— балансировать цену и количество контрагентов.min_lead_time— минимизироватьmax(delivery_time).single_manufacturer— ограничение «только один производитель».
Ограничения:
- Остатки на складах (из observation.stock_by_warehouse).
- Минимальные партии (поставщиковые кратности).
- Географические ограничения поставки.
- Whitelisting/blacklisting customer’а.
Алгоритм:
- ≤ 200 позиций → ILP / MIP оптимизатор.
-
200 → greedy с эвристиками + балансировка.
EstimateOptimized с выбранным offer per строка.
Act 7 — Результат
Per строка:
- Выбранный offer + breakdown цены + остаток + срок.
- 2-3 альтернативы с trade-off (дешевле но дольше / быстрее но дороже / другой производитель).
- Объяснение «выбрано потому что …».
Total:
- Сумма с группировкой по поставщикам.
- Список позиций
pricing_mode=on_requestотдельно (не суммируются). - Список discontinued / weak match (если допущены).
Act 8 — Дальше
- Export: XLSX / PDF (текущая Phase).
- Phase 3+: ручное оформление заказа на основе сметы.
- Phase 6+: прямая передача в исполнение через API поставщиков (используя customer credentials с
place_ordersfeature).
Decision points
- EstimateLine с
match_confidence=weakв авто-смете — запрещено по умолчанию (invariant). Customer может явно разрешить (override). - На-request позиция — не суммируется в total; UX «запросить цену» (Phase 3+).
- B2B
features.allow_estimate_approval=true— Finalize требует согласования внутри организации. - Все observations stale в Act 5 — strategy: подождать N сек или вернуть stale с пометкой (config).
Edge cases
| Случай | Поведение |
|---|---|
| 5000 позиций в смете | Greedy + ограничение на размер batch; Pricing.Compute параллелится. |
| Customer изменил строку после Act 6 | Новая EstimateVersion; старые версии immutable. |
| Discontinued canonical в смете | Показывается с пометкой + suggest replaced_by. |
| Позиция без кандидатов вообще | EstimateLineErrorPresented — customer может вручную ввести supplier_sku. |
| Customer revoke credential во время Act 5 | Pricing fallback на system + fallback_reason в breakdown. |
| LLM-парсер выдал низкий confidence для критичных полей | Помечаем строку, требуем confirm. |
EstimateBecameStale (price/observation обновился) | Customer видит бэдж «нужен пересчёт»; breakdown сохранён, но recalculate by request. |
Инварианты сценария
EstimateLineссылается на существующий canonical (если matched).- В оптимизированной смете все позиции имеют
match_confidence ≥ strong, если customer явно не разрешил иное. - Breakdown pricing сохраняется вместе с estimate (для аудита и replay).
- Versioning: любое изменение → новая
EstimateVersion. Старые immutable. - Customer не видит чужие смет(ы) (Visibility + RBAC).
- Pricing внутри estimate использует customer credentials (если есть и валидны), fallback — system.
- Каждая позиция объяснима (UX critical).
Метрики и observability
estimates_uploaded_total,estimate_lines_parsed_total.estimate_parse_latency_seconds,estimate_optimize_latency_seconds.estimate_match_confidence_distribution{level}.estimate_lines_required_user_confirm_ratio— для UX оптимизации.estimate_finalized_total,estimate_exported_total{format}.estimate_optimization_objective_value{mode}.estimate_stale_marked_total— послеOfferObservationRecorded.
Связанные файлы
- Контексты:
../contexts/estimate.md,../contexts/search.md,../contexts/pricing.md,../contexts/matching.md,../contexts/visibility.md,../contexts/customer.md,../contexts/ingestion.md. - Сценарии:
pricing-calculation.md,analog-search.md,update-and-diff.md. - ADR-0014 (graceful degradation).