Files
PriceForge/bible-local/vendor-mapping.md
Mikhail Chusavitin f73e3d144d Vendor mapping: wildcard ignore patterns, bulk CSV import, multi-lot qty
- Add glob pattern support (* and ?) for ignore rules stored in
  qt_vendor_partnumber_seen (is_pattern flag, migration 041)
- Pattern matching applied in stock/competitor import, partnumber book
  snapshot, and vendor mappings list (Go-side via NormalizeKey)
- BulkUpsertMappings: replace N+1 loop with two batch SQL upserts,
  validating all lots in a single query (~1500 queries → 3-4)
- CSV import: multi-lot per PN via repeated rows, optional qty column
- CSV export: updated column format vendor;partnumber;lot_name;qty;description;ignore;notes
- UI: ignore patterns section with add/delete, import progress feedback
- Update bible-local/vendor-mapping.md with new CSV format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 09:41:48 +03:00

142 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Vendor Mapping (Сопоставление partnumber → LOT)
> Решение зафиксировано: 2026-02-18
## Концепция
`qt_partnumber_book_items` — канонический контракт сопоставления для внешнего конфигуратора.
Одна строка на `partnumber`; состав хранится в `lots_json` как список `{lot_name, qty}`.
---
## Правила
### 1. Единственная запись на PN
Запрещено создавать несколько строк для одного `partnumber` в `qt_partnumber_book_items`.
### 2. Резолвинг
- Сначала точное совпадение по `partnumber` в `qt_partnumber_book_items`.
- Если состав PN содержит несколько LOT, резолвер возвращает весь список `{lot_name, qty}`.
- Vendor хранится как metadata в `qt_vendor_partnumber_seen`; канонический vendor-aware mapping в БД отсутствует.
### 4. Ignore-логика
- **Не использовать** `stock_ignore_rules` для новой логики.
- Использовать `qt_vendor_partnumber_seen.is_ignored`.
- `qt_vendor_partnumber_seen` хранится в формате **1 строка на partnumber** (vendor/source не участвуют в уникальности).
- Ignore применяется по `partnumber` (одинаково для записей с vendor и без vendor).
- Если внешняя система ошибочно записала в `qt_vendor_partnumber_seen.partnumber` значение, равное `lot.lot_name`,
такая строка считается мусором и не должна попадать в Global Vendor Mappings UI.
Такие seen-строки подлежат очистке, если для этого значения нет явной строки в `qt_partnumber_book_items`.
---
## Таблицы БД
| Таблица | Назначение |
|---------|------------|
| `qt_vendor_partnumber_seen` | Реестр seen-записей (уникально по `partnumber`) + флаг `is_ignored` |
| `qt_partnumber_books` | Версионированные книги PN; каждая книга хранит `partnumbers_json` — список PN, входящих в книгу |
| `qt_partnumber_book_items` | Глобальный source-of-truth каталог `partnumber -> lots_json`; без дубликатов по `partnumber`; `lots_json` хранит список `{lot_name, qty}` |
Миграции:
- `migrations/023_vendor_partnumber_global_mapping.sql` (historical)
- `migrations/025_dedup_vendor_seen_by_partnumber.sql`
- `migrations/026_add_partnumber_books.sql`
- `migrations/027_fix_partnumber_books_version_length.sql`
- `migrations/028_add_description_to_partnumber_book_items.sql`
- `migrations/031_drop_is_primary_pn.sql`
- `migrations/032_drop_legacy_vendor_mapping_tables.sql`
---
## Partnumber Book — инварианты снимка
При формировании partnumber book обязательно:
1. **`qt_partnumber_book_items` содержит одну строку на `partnumber`** — дубликаты по PN не допускаются.
2. **`lots_json`** содержит полный состав PN как JSON-массив объектов `{lot_name, qty}`.
3. **Ignored PN исключаются** — партномера с `qt_vendor_partnumber_seen.is_ignored = true` не попадают ни в каталог, ни в `partnumbers_json` книги.
4. **Многокомпонентные PN не разворачиваются в отдельные строки** — PN сохраняется одной записью, а состав компонентов уходит в `lots_json`.
5. **`description`** берётся из `qt_partnumber_book_items.description`.
6. **`qt_partnumber_books.partnumbers_json`** хранит отсортированный список PN, входящих в конкретную книгу.
7. **Конфликт разных составов для одного PN недопустим** — если при построении книги один и тот же PN даёт разные `lots_json`, создание snapshot должно завершиться ошибкой.
## QuoteForge Read Contract
QuoteForge must read the active book header first:
```sql
SELECT id, version, created_at, created_by, partnumbers_json
FROM qt_partnumber_books
WHERE is_active = 1
ORDER BY created_at DESC, id DESC
LIMIT 1;
```
Then load item payloads from the catalog by PN list:
```sql
SELECT partnumber, lots_json, description
FROM qt_partnumber_book_items
WHERE partnumber IN (...PNs from partnumbers_json...);
```
Contract notes:
- `partnumbers_json` is the membership snapshot of the selected book.
- `qt_partnumber_book_items` is the current canonical catalog of PN compositions.
- `lots_json` format is JSON array: `[{"lot_name":"CPU_X","qty":2},{"lot_name":"RAM_X","qty":4}]`
- QuoteForge must not expect `lot_name` or `is_primary_pn` columns in `qt_partnumber_book_items`.
---
## Связанные модули
| Роль | Файл |
|------|------|
| Миграция | `migrations/025_dedup_vendor_seen_by_partnumber.sql` |
| Миграции снимков | `migrations/026032` |
| Резолвер | `internal/lotmatch/matcher.go` |
| Сервис маппингов | `internal/services/vendor_mapping.go` |
| Сервис снимков | `internal/services/partnumber_book.go` |
| API | `internal/handlers/pricing.go` |
| Warehouse calc | `internal/warehouse/snapshot.go` |
| Stock import seen/ignore | `internal/services/stock_import.go` |
| Модели | `internal/models/lot.go`, `internal/models/configuration.go` |
| Роутинг | `cmd/pfs/main.go` |
---
## CSV Import / Export (Global Vendor Mappings UI)
- Формат CSV для Excel (RU locale): разделитель `;`
- Кодировка: `UTF-8` (BOM допускается и поддерживается)
- Колонки: `vendor;partnumber;lot_name;qty;description;ignore`
- Допустим импорт как с заголовком, так и без заголовка (в фиксированном порядке колонок выше)
- Пустые строки пропускаются
- Для строки обязательны `partnumber` и (`lot_name` или заполненный `ignore`)
- Если `ignore` заполнен и `lot_name` пустой, строка помечается как ignored в `qt_vendor_partnumber_seen`
### Маппинг один p/n → несколько lot
Один p/n можно привязать к нескольким lot через несколько строк с одинаковым `partnumber`.
Строки аккумулируются, затем сохраняются как один `lots_json` (upsert).
```
vendor;partnumber;lot_name;qty;description;ignore
Intel;E-2336;Server_Intel;1;Процессор Intel;
Intel;E-2336;Server_AMD;2;;
Samsung;MZ7L3480HCHQ-00A07;SSD_480GB;;;
```
- `qty` необязательно; если пусто или 0 — считается `1`
- `description` берётся из первой строки p/n
- Каждый повторный импорт того же p/n **перезаписывает** весь `lots_json`
### Экспорт незамапленных p/n
Кнопка «Выгрузить незамапленные» генерирует CSV в том же формате с пустыми `lot_name` и `qty` — для заполнения пользователем и последующего импорта.