Files
QuoteForge/bible-local/11-lot-suggestions.md
Mikhail Chusavitin d204e337b5 feat: сохранять ручные PN→LOT маппинги как lot_suggestion в qt_vendor_partnumber_seen
При сохранении 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>
2026-06-16 15:39:53 +03:00

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:

  • 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:

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

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.