Files
PriceForge/bible/vendor-mapping.md
Michael Chus a4457a0a28 Add partnumber book snapshots for QuoteForge integration
- Migrations 026-028: qt_partnumber_books + qt_partnumber_book_items
  tables; is_primary_pn on lot_partnumbers; version VARCHAR(30);
  description VARCHAR(10000) on items (required by QuoteForge sync)
- Service: CreateSnapshot expands bundles, filters empty lot_name and
  ignored PNs, copies description, activates new book atomically,
  applies GFS retention (7d/5w/12m/10y) with explicit item deletion
- Task type TaskTypePartnumberBookCreate; handlers ListPartnumberBooks
  and CreatePartnumberBook; routes GET/POST /api/admin/pricing/partnumber-books
- UI: snapshot list + "Создать снапшот сопоставлений" button with
  progress polling on /vendor-mappings page
- Bible: history, api, background-tasks, vendor-mapping updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:16:16 +03:00

110 lines
5.3 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).
### 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` | Версионированные снимки маппинга для QuoteForge (пишет PriceForge) |
| `qt_partnumber_book_items` | Строки снимка: `(book_id, partnumber, lot_name, is_primary_pn, description)`; бандлы развёрнуты; пустые lot_name и ignored PN исключены |
Миграции:
- `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`
---
## Partnumber Book — инварианты снимка
При формировании `qt_partnumber_book_items` обязательно:
1. **Пустой `lot_name` исключается**`TRIM(lot_name) = ''` или NULL не попадают в снимок.
2. **Ignored PN исключаются** — партномера с `qt_vendor_partnumber_seen.is_ignored = true` не попадают в снимок.
3. **Бандлы разворачиваются** — каждая строка содержит конечный `lot_name` компонента, не имя бандла.
4. **`description`** берётся из `lot_partnumbers.description`; для развёрнутых бандлов — из родительской записи partnumber.
5. **Удаление items при retention** — явное (`DELETE WHERE book_id IN (...)`), не через FK CASCADE.
---
## Связанные модули
| Роль | Файл |
|------|------|
| Миграция | `migrations/023_vendor_partnumber_global_mapping.sql` |
| Миграция | `migrations/025_dedup_vendor_seen_by_partnumber.sql` |
| Миграции снимков | `migrations/026028` |
| Резолвер | `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` |