При сохранении 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>
162 lines
5.8 KiB
Markdown
162 lines
5.8 KiB
Markdown
# 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 '<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):
|
|
|
|
```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.
|