Harden pricelist formation and document architecture decisions

This commit is contained in:
Mikhail Chusavitin
2026-02-20 19:01:07 +03:00
parent d381a5833d
commit c96a8806c4
14 changed files with 335 additions and 29 deletions

View File

@@ -115,7 +115,7 @@ HTTP Request
### internal/warehouse/
`snapshot.go` — warehouse pricelist creation: weighted median, bundle expansion, allocation.
`snapshot.go` — warehouse pricelist creation: weighted average, bundle expansion, allocation.
### internal/lotmatch/

View File

@@ -11,7 +11,7 @@ Categories are **always** taken from `lot.lot_category` (table `lot`).
### When creating pricelists
1. **Estimate**: load `lot.lot_category` for all components.
1. **Estimate**: include only components with `current_price > 0` and `is_hidden = 0`, then load `lot.lot_category`.
2. **Warehouse**: load `lot.lot_category` for all positions.
3. Persist into `lot_category` column of `qt_pricelist_items`.
@@ -28,7 +28,7 @@ type PricelistItem struct {
- Category is **not** a virtual field — it is persisted to DB when the pricelist is created.
- JOIN with `lot` is only needed for `lot_description`; category is already in `qt_pricelist_items`.
- Default value when category is absent in source: `PART_`.
- Default value when category is absent in source or LOT row is missing: `PART_`.
---

View File

@@ -5,6 +5,51 @@
---
## 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

View File

@@ -169,9 +169,10 @@ type PricelistItem struct {
LotName string `gorm:"size:255"`
Price float64 `gorm:"type:decimal(12,2)"`
// Stored category snapshot
LotCategory *string `gorm:"column:lot_category;size:50" json:"category,omitempty"`
// Virtual: populated via JOIN
LotDescription string `gorm:"-:migration" json:"lot_description,omitempty"`
Category string `gorm:"-:migration" json:"category,omitempty"`
// Virtual: populated programmatically
AvailableQty *float64 `gorm:"-" json:"available_qty,omitempty"`
Partnumbers []string `gorm:"-" json:"partnumbers,omitempty"`

View File

@@ -16,6 +16,10 @@ PriceForge supports three pricelist types (`source` field):
**Source**: snapshot of current component prices from `qt_lot_metadata`.
**Filter**:
- Include only rows with `current_price > 0`.
- Exclude hidden rows (`is_hidden = 1`).
**Stores**:
- `price_period_days` — price calculation window
- `price_coefficient` — markup coefficient