Files
PriceForge/bible/history.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

9.4 KiB

Change History

Architectural decisions and significant refactoring are recorded here. Every architectural decision MUST be documented in this file.


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

Hardened pricelist formation rules to remove ambiguity and prevent silent data loss in UI/API.

What changed

  • estimate snapshot now explicitly excludes hidden components (qt_lot_metadata.is_hidden = 1).
  • Category snapshot in qt_pricelist_items.lot_category now always has a deterministic fallback: PART_ is assigned even when a LOT row is missing in lot.
  • PricelistItem JSON contract was normalized to a single category field (LotCategory -> json:"category"), removing duplicate JSON-tag ambiguity.
  • Meta-price source expansion now always includes the base LOT, then merges meta_prices sources with deduplication (exact + wildcard overlaps).

Rationale

  • Prevent hidden/ignored components from leaking into estimate pricelists.
  • Keep frontend category rendering stable for all items.
  • Avoid non-deterministic JSON serialization when duplicate tags are present.
  • Ensure meta-article pricing includes self-history and does not overcount duplicate sources.

Constraints

  • estimate pricelist invariant: current_price > 0 AND is_hidden = 0.
  • category in API must map from persisted qt_pricelist_items.lot_category.
  • Missing category source must default to PART_.
  • Meta source list must contain each LOT at most once.

Files

  • Model: internal/models/pricelist.go
  • Estimate/Warehouse snapshot service: internal/services/pricelist/service.go
  • Pricing handler/meta expansion: internal/handlers/pricing.go
  • Pricing service/meta expansion: internal/services/pricing/service.go
  • Tests:
    • internal/services/pricelist/service_estimate_test.go
    • internal/services/pricelist/service_warehouse_test.go
    • internal/handlers/pricing_meta_expand_test.go
    • internal/services/pricing/service_meta_test.go
    • internal/services/stock_import_test.go

2026-02-20: Seen Registry Deduplication by Partnumber

Decision

Changed qt_vendor_partnumber_seen semantics to one row per partnumber (vendor/source are no longer part of uniqueness).

Rationale

  • Eliminates duplicate seen rows when the same partnumber appears both with vendor and without vendor.
  • Keeps ignore behavior consistent regardless of vendor presence.
  • Simplifies operational cleanup and prevents re-creation of vendor/no-vendor duplicates.

Constraints

  • partnumber is now the unique key in seen registry.
  • Ignore checks are resolved by partnumber only.
  • Stock provenance must be preserved (source_type='stock') when stock data exists for the partnumber.

Files

  • Migration: migrations/025_dedup_vendor_seen_by_partnumber.sql
  • Service: internal/services/stock_import.go
  • Service: internal/services/vendor_mapping.go
  • Model: internal/models/lot.go

2026-02-18: Global Vendor Partnumber Mapping

Decision

Implemented global vendor partnumber → LOT mapping with bundle support and seen-based ignore logic.

Key rules

  • lot_partnumbers is the canonical mapping contract (1:1 per key).
  • Composite mappings use internal bundle tables (qt_lot_bundles, qt_lot_bundle_items).
  • Ignore logic moved from stock_ignore_rules to qt_vendor_partnumber_seen.is_ignored.
  • Resolver order: exact (vendor, partnumber) → fallback (vendor='', partnumber).

Rationale

  • Preserves external/client contracts (lot_partnumbers, LOT-based pricelists).
  • Avoids multi-row ambiguity in lot_partnumbers.
  • Supports complex assembled vendor SKUs without client-side changes.
  • Centralizes ignore behavior across all sources via seen-registry.

Constraints

  • Bundle LOT is internal and must stay hidden in regular LOT list by default.
  • Resolver order is mandatory and fixed.
  • Bundle allocation for missing estimate: fallback from previous active warehouse pricelist; if absent → 0.

Files

  • Migration: migrations/023_vendor_partnumber_global_mapping.sql
  • Backfill: migrations/024_backfill_vendor_seen_from_stock_and_ignore.sql
  • Resolver: internal/lotmatch/matcher.go
  • Service: internal/services/vendor_mapping.go
  • Warehouse calc: internal/warehouse/snapshot.go
  • Stock import: internal/services/stock_import.go
  • API: internal/handlers/pricing.go, cmd/pfs/main.go

2026-02-10: LOT Page Refactoring

Decision

Moved LOT management to a dedicated /lot page. Removed LOT tab from Pricing Admin.

What changed

  • Created web/templates/lot.html — two tabs: LOT (component management) and Mappings (partnumber ↔ LOT).
  • Removed LOT tab from admin_pricing.html; default tab changed to estimate.
  • Removed "Сопоставление partnumber → LOT" section from Warehouse tab.
  • Updated navigation: LOT → /lot, Pricing Admin → /admin/pricing.
  • Added Lot() handler in internal/handlers/web.go.
  • Added /lot route in cmd/pfs/main.go.

Rationale

Separation of concerns: LOT/component management is distinct from pricing administration.


Warehouse Pricing: Weighted Average

Decision

Warehouse pricelist uses weighted_avg (quantity-weighted average) as the sole price calculation method. Commit edff712 switched from weighted_median to weighted_avg.

  • price_method field always contains "weighted_avg".
  • No price_period_days, price_coefficient, manual_price, meta_prices in warehouse pricelists.

Architecture Conventions

All future architectural decisions must be documented here with:

  • Date
  • Decision summary
  • Rationale
  • Constraints / invariants
  • Affected files