Колонка «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>
3.2 KiB
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
worldpricelist — the widest list available — plus the latest activeestimatepricelist as a safety net. - Deduplication is on
UPPER(lot_name). The estimate row wins a collision, so its category, description andprice_qualitystay 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 inweb/static/price-quality.js. This is the one place QF sets that field instead of only reading it (contrastbible-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 ownSelect/Count/Wherewithout 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.