# 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`: ```sql 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`: ```json [ { "lot_name": "LOT_A", "qty": 1 }, { "lot_name": "LOT_B", "qty": 2 } ] ``` Rules: - `null` or absent means no suggestion has been entered yet; - an empty array `[]` is not a valid value — use `null` instead; - a single PN may map to multiple lots (`lot_name` entries), each with its own `qty`; - the array is ordered — the order reflects the order of `lot_mappings[]` in the vendor spec row at the time of last user save; - `qty` must be a positive integer (≥ 1). ## Write Contract (QuoteForge → MariaDB) QuoteForge writes `lot_suggestion` when all of the following are true: 1. The user saves a vendor BOM via `PUT /api/configs/:uuid/vendor-spec`. 2. At least one `vendor_spec` row has a non-empty `lot_mappings[]` array (manually entered or confirmed by the user — not auto-resolved from a partnumber book). 3. The MariaDB connection is available at the time of save. For each such row: ```sql 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_suggestion` value = JSON-marshalled `lot_mappings[]` from the vendor spec item, reusing the same `{lot_name, qty}` shape. - If the PN row already exists and `lot_suggestion` is 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_suggestion` on 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 ```sql 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: ```json [ { "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: ```sql 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: ```sql GRANT SELECT ON RFQ_LOG.qt_vendor_partnumber_seen TO ''@'%'; GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_partnumber_book_items TO ''@'%'; ``` ## Migration Migration is applied outside this repo (server-side DDL): ```sql 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.