Unified Quote Journal (parts_log) v3

- New unified append-only quote log table parts_log replaces three
  separate log tables (stock_log, partnumber_log_competitors, lot_log)
- Migrations 042-049: extend supplier, create parts_log/import_formats/
  ignore_rules, rework qt_lot_metadata composite PK, add lead_time_weeks
  to pricelist_items, backfill data, migrate ignore rules
- New services: PartsLogBackfillService, ImportFormatService,
  UnifiedImportService; new world pricelist type (all supplier types)
- qt_lot_metadata PK changed to (lot_name, pricelist_type); all queries
  now filter WHERE pricelist_type='estimate'
- Fix pre-existing bug: qt_component_usage_stats column names
  quotes_last30d/quotes_last7d (no underscore) — added explicit gorm tags
- Bible: full table inventory, baseline schema snapshot, updated pricelist/
  data-rules/api/history/architecture docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-03-21 17:25:54 +03:00
parent f73e3d144d
commit 5f8aec456b
40 changed files with 2294 additions and 124 deletions

View File

@@ -11,9 +11,11 @@ Categories are **always** taken from `lot.lot_category` (table `lot`).
### When creating pricelists
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`.
1. **Estimate**: include only components with `current_price > 0` and `is_hidden = 0` (`pricelist_type='estimate'`), then load `lot.lot_category`.
2. **Warehouse**: load `lot.lot_category` for all positions (`pricelist_type='warehouse'`).
3. **Competitor**: load `lot.lot_category` for all positions (`pricelist_type='competitor'`).
4. **World**: load `lot.lot_category` for all positions (`pricelist_type='world'`).
5. Persist into `lot_category` column of `qt_pricelist_items`.
### Model
@@ -34,14 +36,57 @@ type PricelistItem struct {
## Price Method
- Warehouse pricelist: `price_method` = `"weighted_avg"` (quantity-weighted average).
- Estimate pricelist: method determined by per-component settings.
- **Estimate**: method determined by per-component settings in `qt_lot_metadata` (`pricelist_type='estimate'`). Default: `median`.
- **Warehouse**: `weighted_avg` (quantity-weighted average). Default `period_days=7`.
- **Competitor**: `weighted_median`. `qty NULL` → weight = 1.
- **World**: `weighted_median` across all supplier types. `qty NULL` → weight = 1.
---
## Warehouse: Filtering by qt_partnumber_book_items
## supplier_type
**Rule (CRITICAL)**: only positions with a matching record in `qt_partnumber_book_items` are included in the warehouse pricelist. Unmapped partnumbers are completely excluded.
The `supplier` table `supplier_type` column determines which pricelist(s) a supplier's quotes feed:
| supplier_type | Feeds into |
|---------------|-----------|
| `trader` | estimate, world |
| `self` | warehouse, world |
| `competitor` | competitor, world |
**price_uplift**: divisor applied at import. `effective_price = raw_price / price_uplift`. All prices in `parts_log` are always in USD.
**Defaults by type**:
| supplier_type | price_uplift | default_lead_time_weeks |
|---------------|-------------|------------------------|
| trader | 1.0 | NULL |
| self | 1.0 | 0 |
| competitor | 1.3 | 2 |
---
## offer_type
`parts_log.offer_type` ENUM: `'private'` or `'public'` (default `'public'`).
Selected at import time. Both types are currently included in all pricelist calculations.
---
## qt_ignore_rules
Replaces the `is_ignored` mechanism in `qt_vendor_partnumber_seen`. Checked at import before mapping.
| field | match_type | behavior |
|-------|-----------|---------|
| partnumber / vendor / description | exact / glob / contains | DROP row if matched |
Order of checks at import: **ignore rules → partnumber book → write to parts_log**.
---
## Filtering by qt_partnumber_book_items
**Rule (CRITICAL)**: only `parts_log` rows with a non-NULL `lot_name` are included in any pricelist. `lot_name` is populated by the daily backfill job (`PartsLogBackfillService`) which resolves partnumbers via `qt_partnumber_book_items`. Rows that cannot be resolved remain with `lot_name=NULL` and are excluded from all pricelists.
---