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>
This commit is contained in:
2026-02-21 22:16:16 +03:00
parent 225e1beda9
commit a4457a0a28
13 changed files with 751 additions and 32 deletions

View File

@@ -5,6 +5,56 @@
---
## 2026-02-21: Partnumber Book Snapshots for QuoteForge
### Decision
Implemented versioned snapshots of the `lot_partnumbers` → LOT mapping in `qt_partnumber_books` / `qt_partnumber_book_items`. PriceForge writes; QuoteForge reads (SELECT only).
### What changed
- Migration `026`: added `is_primary_pn TINYINT(1) DEFAULT 1` to `lot_partnumbers`; created `qt_partnumber_books` and `qt_partnumber_book_items` tables (version `VARCHAR(20)`, later corrected).
- Migration `027`: corrected `version VARCHAR(20) → VARCHAR(30)``PNBOOK-YYYY-MM-DD-NNN` is 21 chars and overflowed the original column.
- Migration `028`: added `description VARCHAR(10000) NULL` to `qt_partnumber_book_items` — required by QuoteForge sync (`SELECT partnumber, lot_name, is_primary_pn, description`).
- Models `PartnumberBook`, `PartnumberBookItem` (with `Description *string`) added to `internal/models/lot.go`; `IsPrimaryPN bool` added to `LotPartnumber`.
- Service `internal/services/partnumber_book.go`:
- `CreateSnapshot`: expands bundles (QuoteForge is bundle-unaware), copies `description` from `lot_partnumbers` to every expanded row, generates version `PNBOOK-YYYY-MM-DD-NNN`, deactivates previous books and activates new one atomically, then runs GFS retention cleanup.
- `expandMappings`: filters out rows where `lot_name` is empty/whitespace; filters out partnumbers marked `is_ignored = true` in `qt_vendor_partnumber_seen`. Only valid PN→LOT pairs enter the snapshot.
- `applyRetention`: deletes items explicitly (`DELETE … WHERE book_id IN (…)`) before deleting books — does not rely on FK CASCADE which GORM does not trigger on batch deletes.
- `ListBooks`: returns all snapshots ordered newest-first with item counts.
- GFS retention policy: 7 daily · 5 weekly · 12 monthly · 10 yearly; applied automatically after each snapshot; active book is never deleted.
- Task type `TaskTypePartnumberBookCreate` added to `internal/tasks/task.go`.
- Handlers `ListPartnumberBooks` and `CreatePartnumberBook` added to `internal/handlers/pricing.go`; `PartnumberBookService` injected via constructor.
- Routes `GET /api/admin/pricing/partnumber-books` and `POST /api/admin/pricing/partnumber-books` registered in `cmd/pfs/main.go`.
- UI: "Снимки сопоставлений (Partnumber Books)" section with snapshot table, progress bar, and "Создать снапшот сопоставлений" button added to `web/templates/vendor_mappings.html`.
### Rationale
QuoteForge needs a stable, versioned copy of the PN→LOT mapping to resolve BOM line items without live dependency on PriceForge. Snapshots decouple the two systems.
### Constraints
- Bundles MUST be expanded: QuoteForge does not know about `qt_lot_bundles`.
- Snapshot rows with empty `lot_name` or `is_ignored = true` partnumbers MUST be excluded.
- `description` in book items comes from `lot_partnumbers.description`; for expanded bundle rows the description of the parent partnumber mapping is used.
- `is_primary_pn` is copied from `lot_partnumbers` into each book item; drives qty logic in QuoteForge.
- New snapshot is immediately `is_active=1`; all previous books are deactivated in the same transaction.
- Version format: `PNBOOK-YYYY-MM-DD-NNN` (`VARCHAR(30)`), sequential within the same day.
- Item deletion during retention MUST be explicit (items first, then books) — FK CASCADE is unreliable with GORM batch deletes.
- QuoteForge has `SELECT` permission only on `qt_partnumber_books` and `qt_partnumber_book_items`.
### Files
- Migrations: `026_add_partnumber_books.sql`, `027_fix_partnumber_books_version_length.sql`, `028_add_description_to_partnumber_book_items.sql`
- Models: `internal/models/lot.go`
- Service: `internal/services/partnumber_book.go`
- Handler: `internal/handlers/pricing.go`
- Tasks: `internal/tasks/task.go`
- Routes: `cmd/pfs/main.go`
- UI: `web/templates/vendor_mappings.html`
---
## 2026-02-20: Pricelist Formation Hardening (Estimate/Warehouse/Meta)
### Decision