Files
PriceForge/bible-local/vendor-mapping.md
2026-03-07 22:10:05 +03:00

154 lines
7.9 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
## Концепция
`lot_partnumbers` — канонический контракт сопоставления для внешнего конфигуратора.
Маппинг строго 1:1 по ключу `(vendor, partnumber)``lot_name`.
---
## Правила
### 1. Единственная запись на ключ
Запрещено создавать несколько строк для одного ключа `(vendor, partnumber)`.
### 2. Порядок резолвинга (фиксированный)
```
1. Точное совпадение: vendor + partnumber → lot_name
2. Fallback: vendor='' + partnumber → lot_name
```
Резолвер: `internal/lotmatch/matcher.go`
### 3. Составные маппинги — через бандлы
Если один внешний partnumber соответствует нескольким LOT — использовать внутренние бандл-таблицы:
```
lot_partnumbers: (vendor, partnumber) → bundle_lot_name
qt_lot_bundles: bundle_lot_name → [item1, item2, ...]
qt_lot_bundle_items: bundle_lot_name, lot_name, qty
```
- Bundle LOT — внутренний, скрыт в обычном UI LOT по умолчанию.
- Bundle expansion происходит в PriceForge: при расчёте warehouse-прайслиста и при создании partnumber book snapshot.
### 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-строки подлежат очистке, если для этого значения нет явного PN→LOT маппинга в `lot_partnumbers`.
### 5. Клиентская совместимость
- Клиент потребляет LOT-based прайслисты (как обычно).
- Bundle expansion/allocation происходит только внутри PriceForge.
---
## Allocation при отсутствии estimate
Если у bundle-компонента нет estimate-цены:
1. Fallback: взять из предыдущего активного warehouse-прайслиста.
2. Если нет предыдущего — цена `0`.
---
## Таблицы БД
| Таблица | Назначение |
|---------|------------|
| `lot_partnumbers` | Канонические маппинги `(vendor, partnumber)``lot_name`; колонка `is_primary_pn` — ведущий PN для qty в BOM |
| `qt_lot_bundles` | Определения бандлов (bundle LOT → описание) |
| `qt_lot_bundle_items` | Состав бандла: `(bundle_lot_name, lot_name, qty)` |
| `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`
- `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/029_change_partnumber_book_items_to_lots_json.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. **Бандлы не разворачиваются в отдельные строки** — bundle PN сохраняется одной записью, а состав компонентов уходит в `lots_json`.
5. **`description`** берётся из `lot_partnumbers.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, is_primary_pn, 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` column in `qt_partnumber_book_items` anymore.
---
## Связанные модули
| Роль | Файл |
|------|------|
| Миграция | `migrations/023_vendor_partnumber_global_mapping.sql` |
| Миграция | `migrations/025_dedup_vendor_seen_by_partnumber.sql` |
| Миграции снимков | `migrations/026029` |
| Резолвер | `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 (Global Vendor Mappings UI)
- Формат CSV для Excel (RU locale): разделитель `;`
- Кодировка: `UTF-8` (BOM допускается и поддерживается)
- Рекомендуемые колонки: `vendor;partnumber;lot_name;description;ignore`
- Допустим импорт как с заголовком, так и без заголовка (в фиксированном порядке колонок выше)
- Пустые строки пропускаются
- Для строки обязательны `partnumber` и (`lot_name` или заполненный `ignore`)
- Если `ignore` заполнен и `lot_name` пустой, строка помечается как ignored в `qt_vendor_partnumber_seen`