215 lines
9.7 KiB
Markdown
215 lines
9.7 KiB
Markdown
# 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.
|
|
|
|
### Update (2026-02-25)
|
|
|
|
- `DELETE /api/admin/pricing/vendor-mappings` now removes both:
|
|
- mapping rows from `lot_partnumbers`
|
|
- and matching entries from `qt_vendor_partnumber_seen` (so "seen/unmapped" rows disappear from the global UI immediately after delete).
|
|
|
|
### 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
|