При сохранении vendor-spec строки с заполненным lot_mappings автоматически
отправляются на сервер и пишутся в новый столбец lot_suggestion. Столбец
хранит JSON-массив [{lot_name, qty}] — тот же формат, что qt_partnumber_book_items.lots_json.
Если миграция ещё не прошла (столбец отсутствует), приложение логирует WARN
и записывает строку без столбца; сбоя нет.
Контракт для инструмента создания partnumber-books описан в bible-local/11-lot-suggestions.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.8 KiB
11 - Lot Suggestions (qt_vendor_partnumber_seen)
Purpose
qt_vendor_partnumber_seen records vendor partnumbers encountered during import
that have no mapping in the active partnumber book. When a user manually maps
such a partnumber to one or more LOT names in the QuoteForge UI, those mappings
are written back to the server as lot suggestions — hints for the team that
maintains qt_partnumber_book_items.
Schema Extension
Add one nullable column to qt_vendor_partnumber_seen:
ALTER TABLE `qt_vendor_partnumber_seen`
ADD COLUMN `lot_suggestion` longtext DEFAULT NULL
COMMENT 'JSON array [{lot_name, qty}] — user-entered LOT mappings from the UI';
Updated table contract (relevant columns only)
| Column | Type | Notes |
|---|---|---|
partnumber |
varchar(255) UNIQUE NOT NULL | natural key |
lot_suggestion |
longtext (JSON) | nullable; set when user maps the PN manually |
lot_suggestion contains the same JSON shape as qt_partnumber_book_items.lots_json:
[
{ "lot_name": "LOT_A", "qty": 1 },
{ "lot_name": "LOT_B", "qty": 2 }
]
Rules:
nullor absent means no suggestion has been entered yet;- an empty array
[]is not a valid value — usenullinstead; - a single PN may map to multiple lots (
lot_nameentries), each with its ownqty; - the array is ordered — the order reflects the order of
lot_mappings[]in the vendor spec row at the time of last user save; qtymust be a positive integer (≥ 1).
Write Contract (QuoteForge → MariaDB)
QuoteForge writes lot_suggestion when all of the following are true:
- The user saves a vendor BOM via
PUT /api/configs/:uuid/vendor-spec. - At least one
vendor_specrow has a non-emptylot_mappings[]array (manually entered or confirmed by the user — not auto-resolved from a partnumber book). - The MariaDB connection is available at the time of save.
For each such row:
INSERT INTO qt_vendor_partnumber_seen
(source_type, vendor, partnumber, description, is_ignored, last_seen_at, lot_suggestion)
VALUES
('manual', '', ?, ?, 0, NOW(3), ?)
ON DUPLICATE KEY UPDATE
lot_suggestion = VALUES(lot_suggestion),
last_seen_at = IF(lot_suggestion IS NULL, last_seen_at, NOW(3))
lot_suggestionvalue = JSON-marshalledlot_mappings[]from the vendor spec item, reusing the same{lot_name, qty}shape.- If the PN row already exists and
lot_suggestionis already set, it is overwritten with the latest user input (the user is assumed to have corrected it). - If the user clears all lot_mappings for a PN (sets to empty), no update is sent —
the existing
lot_suggestionon the server is left untouched. - Rows where
lot_mappings[]is empty or nil are skipped entirely (no insert, no update). - Writes are best-effort: a MariaDB error for one row is logged and skipped; remaining rows continue. A write failure does not fail the vendor-spec save.
Read Contract (Partnumber-Book Creation Tool → MariaDB)
The tool that maintains qt_partnumber_book_items reads qt_vendor_partnumber_seen
to discover new partnumbers and their suggested mappings.
Discovery query
SELECT
s.id,
s.partnumber,
s.description,
s.vendor,
s.lot_suggestion,
s.last_seen_at,
b.lots_json AS book_lots_json
FROM qt_vendor_partnumber_seen s
LEFT JOIN qt_partnumber_book_items b ON b.partnumber = s.partnumber
WHERE s.is_ignored = 0
AND s.lot_suggestion IS NOT NULL
ORDER BY s.last_seen_at DESC;
Interpretation rules
| Condition | Meaning | Suggested action |
|---|---|---|
book_lots_json IS NULL AND lot_suggestion IS NOT NULL |
No book entry yet; user suggested mapping | Create new qt_partnumber_book_items row with lots_json = lot_suggestion |
book_lots_json IS NOT NULL AND lot_suggestion IS NOT NULL AND they differ |
User corrected or extended the existing mapping | Review diff and decide whether to update qt_partnumber_book_items |
book_lots_json IS NOT NULL AND lot_suggestion IS NOT NULL AND they match |
Suggestion already applied | No action needed |
Suggestion format
lot_suggestion is valid JSON (or null). Parse it as an array of objects:
[
{ "lot_name": "LOT_A", "qty": 1 },
{ "lot_name": "LOT_B", "qty": 2 }
]
Map directly to qt_partnumber_book_items.lots_json — the formats are identical.
Multiple lots per PN
One PN may have multiple suggestion entries (e.g., a bundle). The array carries
all of them. The book-creation tool must preserve the full array when writing
lots_json, not just the first element.
Qty semantics
qty in a lot suggestion means "how many of this LOT per one occurrence of the
vendor PN". This matches qt_partnumber_book_items.lots_json exactly. Example:
a server platform that comes with 4 PSUs would produce
[{"lot_name": "PS_1300W_Titanium", "qty": 4}].
Permissions
The existing qfs_user grant covers this column — no new permission is required:
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_vendor_partnumber_seen TO 'qfs_user'@'%';
The book-creation tool connects with its own credentials and needs at minimum:
GRANT SELECT ON RFQ_LOG.qt_vendor_partnumber_seen TO '<book_tool_user>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_partnumber_book_items TO '<book_tool_user>'@'%';
Migration
Migration is applied outside this repo (server-side DDL):
ALTER TABLE `qt_vendor_partnumber_seen`
ADD COLUMN IF NOT EXISTS `lot_suggestion` longtext DEFAULT NULL
COMMENT 'JSON [{lot_name, qty}] — user LOT suggestions from QuoteForge UI';
QuoteForge handles a missing column gracefully: if the migration has not run yet,
the write with lot_suggestion fails with "Unknown column" (MariaDB 1054), a warning
is logged, and the row is re-inserted without the column. The app never crashes on
migration lag.