Сценарий: полный путь сметы (Acts 1–8)

NOTE

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

Главный пользовательский сценарий системы. Связывает почти все BC.

Триггер

Customer (сметчик / закупщик) загружает смету.

Участники

BCРоль
EstimateOwner. Orchestrator всего процесса. Версионирование.
SearchПоиск кандидатов.
MatchingDetermines confidence уровень.
PricingРасчёт per строка.
VisibilityFilter offers.
CustomerSubject + features.
IngestionHigh-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.

Смета может быть «грязной»: опечатки в артикулах, смешанные единицы, позиции без артикула, частичные характеристики.

UploadEstimateEstimate{version=1, status=parsing}.

Act 2 — Парсинг и нормализация

Per строку:

  1. Извлечь артикул (mpn / supplier_sku).
  2. Извлечь наименование и характеристики из описания.
  3. Извлечь количество и единицу.
  4. (Опционально) извлечь ожидаемую цену для 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 < strongEstimateLineErrorPresented. 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_orders feature).

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 5Pricing fallback на system + fallback_reason в breakdown.
LLM-парсер выдал низкий confidence для критичных полейПомечаем строку, требуем confirm.
EstimateBecameStale (price/observation обновился)Customer видит бэдж «нужен пересчёт»; breakdown сохранён, но recalculate by request.

Инварианты сценария

  1. EstimateLine ссылается на существующий canonical (если matched).
  2. В оптимизированной смете все позиции имеют match_confidence ≥ strong, если customer явно не разрешил иное.
  3. Breakdown pricing сохраняется вместе с estimate (для аудита и replay).
  4. Versioning: любое изменение → новая EstimateVersion. Старые immutable.
  5. Customer не видит чужие смет(ы) (Visibility + RBAC).
  6. Pricing внутри estimate использует customer credentials (если есть и валидны), fallback — system.
  7. Каждая позиция объяснима (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.

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