Files
QuoteForge/bible-local/decisions/2026-07-24-component-universe-world-union.md
Mikhail ChusavitinandClaude Opus 4.8 905f9a4952 feat: PN code в импорте BOM, объединённый список LOT (world ∪ estimate)
Колонка «PN code» в таблице импорта BOM: HPE-спецификации хранят партномер
в двух столбцах, канонический вид — P52534-B21#B19. Склейка попадает
в vendor_partnumber и сравнивается с книгой партномеров как есть; при
загрузке разбирается обратно на две колонки. Схему БД менять не пришлось.

Список допустимых LOT собирается из world ∪ estimate (componentUniverse),
а не только из estimate. Прежняя область видимости молча теряла
lot_mappings: резолвер сопоставлял PN по книге, которая про прайслисты
не знает, а фронт отбрасывал LOT, которого нет в estimate — при этом
в корзину он всё равно попадал. World-only позиции отдают price_quality 0.

Кнопка «Пересопоставить» — перерешать BOM по актуальной книге, так как
при открытии конфигурации сопоставления остаются замороженными.

Итоги на вкладке «Ценообразование»: звёздочки убраны (переносились
на новую строку), все три суммы красные, при наведении — попап с долей
цен из прайслиста WORLD. Колонка «PN вендора» больше не переносится.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:01:31 +03:00

3.2 KiB
Raw Permalink Blame History

Decision: the component universe is world estimate, not estimate alone

Date: 2026-07-24 Status: active

Context

Every component read path in internal/localdb/components.go was scoped to the latest active estimate pricelist. That pricelist is therefore what defined the set of LOTs the configurator considers to exist, and the frontend used it as its validation list (_bomLotValid in index.html, fed by /api/components).

That silently broke BOM mappings. The resolver matches a vendor partnumber against the partnumber book, which knows nothing about pricelists, so it legitimately returns LOTs the estimate pricelist does not carry. The frontend then displayed such a row as mapped — _getRowBaseLot does not validate resolved_lot — and applyBOMToEstimate pushed the LOT into the cart, but _getRowCanonicalLotMappings filtered it out through _bomLotValid, so the mapping vanished on save. Result: the LOT sat in the cart while its BOM row showed "н/д", and the pricing tab listed it at the bottom as an orphan with an empty vendor PN.

Prices for such LOTs were already available — see 2026-07-10-world-pricelist-fallback.md — only membership was missing.

Decision

componentUniverse() in internal/localdb/components.go is the single source of the LOT set, and every component read path goes through it: ListComponents, SearchLocalComponents, SearchLocalComponentsByCategory, GetLocalComponent, GetLocalComponentCategories, GetLocalComponentCategoriesByLotNames, CountComponents.

  • The universe is the latest active world pricelist — the widest list available — plus the latest active estimate pricelist as a safety net.
  • Deduplication is on UPPER(lot_name). The estimate row wins a collision, so its category, description and price_quality stay authoritative and nothing changes for LOTs that were already visible.
  • A world-only row reports price_quality = 0, rendering at the red end of the shared scale in web/static/price-quality.js. This is the one place QF sets that field instead of only reading it (contrast bible-local/03-database.md), and it deliberately overrides whatever the external pricing tool wrote on the world row.
  • The helper always returns a derived table ((?) AS c), so callers apply their own Select/Count/Where without knowing which pricelists exist.
  • Either pricelist may be absent; only both missing is an error.

On the frontend, _bomLotPersistable additionally lets a LOT with resolution_source === 'book' be persisted into lot_mappings[] even when it is in neither pricelist. Hand-typed LOTs stay validated against the universe.

Consequences

  • The configurator's pickers, category list and search now surface world-only LOTs. They are selectable and appear in the reddest quality colour.
  • CountComponents — the "is there data" check — counts the union.
  • A LOT present in neither pricelist (nothing to price it with) still resolves from the book and now survives a save, instead of being dropped without a message.
  • Covered by internal/localdb/component_universe_test.go: union and dedup, estimate winning a collision, forced quality 0, and each single-source / empty case.