Files
PriceForge/internal/models/parts_log.go
Mikhail Chusavitin 5f8aec456b 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>
2026-03-21 17:25:54 +03:00

123 lines
6.2 KiB
Go

package models
import "time"
// OfferType classifies whether a quote is public (market) or private (negotiated).
type OfferType string
const (
OfferTypePublic OfferType = "public"
OfferTypePrivate OfferType = "private"
)
// SupplierType classifies suppliers by their role in the supply chain.
type SupplierType string
const (
SupplierTypeTrader SupplierType = "trader"
SupplierTypeSelf SupplierType = "self"
SupplierTypeCompetitor SupplierType = "competitor"
)
// PricelistTypeBySupplierType maps supplier_type to pricelist source.
// world includes all supplier types.
var PricelistTypeBySupplierType = map[SupplierType][]PricelistSource{
SupplierTypeTrader: {PricelistSourceEstimate},
SupplierTypeSelf: {PricelistSourceWarehouse},
SupplierTypeCompetitor: {PricelistSourceCompetitor},
}
// SupplierTypesForPricelist returns the supplier_type values to include for a given pricelist source.
func SupplierTypesForPricelist(src PricelistSource) []SupplierType {
switch src {
case PricelistSourceEstimate:
return []SupplierType{SupplierTypeTrader}
case PricelistSourceWarehouse:
return []SupplierType{SupplierTypeSelf}
case PricelistSourceCompetitor:
return []SupplierType{SupplierTypeCompetitor}
case PricelistSourceWorld:
return []SupplierType{SupplierTypeTrader, SupplierTypeSelf, SupplierTypeCompetitor}
default:
return nil
}
}
// PartsLog is the unified append-only quote journal.
// All price quotes from all sources (warehouse, competitors, traders) are stored here.
// The only permitted mutation after insert is filling lot_name/lot_category by the backfill job.
type PartsLog struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
SupplierCode string `gorm:"column:supplier_code;size:100;not null;default:''" json:"supplier_code"`
Partnumber string `gorm:"column:partnumber;size:255;not null;default:''" json:"partnumber"`
Vendor string `gorm:"column:vendor;size:255;not null;default:''" json:"vendor"`
Description *string `gorm:"column:description;size:1000" json:"description,omitempty"`
LotName *string `gorm:"column:lot_name;size:255" json:"lot_name,omitempty"`
LotCategory *string `gorm:"column:lot_category;size:50" json:"lot_category,omitempty"`
Price float64 `gorm:"column:price;type:decimal(12,4);not null" json:"price"`
Qty *float64 `gorm:"column:qty;type:decimal(12,4)" json:"qty,omitempty"`
OfferType OfferType `gorm:"column:offer_type;type:enum('private','public');not null;default:'public'" json:"offer_type"`
LeadTimeWeeks *int `gorm:"column:lead_time_weeks" json:"lead_time_weeks,omitempty"`
CreatedBy string `gorm:"column:created_by;size:100;not null;default:''" json:"created_by"`
QuoteDate time.Time `gorm:"column:quote_date;type:date;not null" json:"quote_date"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
}
func (PartsLog) TableName() string {
return "parts_log"
}
// ImportFormat stores file format definitions used by the unified import pipeline.
// column_mapping JSON uses the same structure as CompetitorColumnMapping.
type ImportFormat struct {
FormatCode string `gorm:"column:format_code;primaryKey;size:100" json:"format_code"`
Name string `gorm:"column:name;size:255;not null" json:"name"`
FileType string `gorm:"column:file_type;type:enum('csv','xlsx','mxl','json');not null" json:"file_type"`
HeaderRow int `gorm:"column:header_row;not null;default:1" json:"header_row"`
DataStartRow int `gorm:"column:data_start_row;not null;default:2" json:"data_start_row"`
Delimiter *string `gorm:"column:delimiter;size:5" json:"delimiter,omitempty"`
Encoding string `gorm:"column:encoding;size:20;not null;default:'utf-8'" json:"encoding"`
ColumnMapping []byte `gorm:"column:column_mapping;type:json" json:"column_mapping,omitempty"`
SampleFilename *string `gorm:"column:sample_filename;size:500" json:"sample_filename,omitempty"`
Notes *string `gorm:"column:notes;type:text" json:"notes,omitempty"`
IsActive bool `gorm:"column:is_active;not null;default:true" json:"is_active"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
}
func (ImportFormat) TableName() string {
return "qt_import_formats"
}
// IgnoreRule stores structured ignore patterns for the unified import pipeline.
// Applied in order: ignore → mapping → insert.
type IgnoreRule struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Field string `gorm:"column:field;type:enum('partnumber','vendor','description');not null" json:"field"`
MatchType string `gorm:"column:match_type;type:enum('exact','glob','contains');not null;default:'exact'" json:"match_type"`
Pattern string `gorm:"column:pattern;size:500;not null" json:"pattern"`
Note *string `gorm:"column:note;size:500" json:"note,omitempty"`
CreatedBy string `gorm:"column:created_by;size:100;not null;default:''" json:"created_by"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
}
func (IgnoreRule) TableName() string {
return "qt_ignore_rules"
}
// LotMetadataConfig represents per-(lot, pricelist_type) configuration.
// This is the extended view of qt_lot_metadata with the new composite primary key.
// Used for world/competitor/warehouse pricelist generation; estimate uses LotMetadata.
type LotMetadataConfig struct {
LotName string `gorm:"column:lot_name;primaryKey;size:255" json:"lot_name"`
PricelistType string `gorm:"column:pricelist_type;primaryKey;size:20;not null;default:'estimate'" json:"pricelist_type"`
PriceMethod string `gorm:"column:price_method;type:enum('manual','median','average','weighted_median');default:'median'" json:"price_method"`
PeriodDays int `gorm:"column:period_days;not null;default:90" json:"period_days"`
OnMissingQuotes string `gorm:"column:on_missing_quotes;type:enum('keep','drop');not null;default:'drop'" json:"on_missing_quotes"`
IsHidden bool `gorm:"column:is_hidden;default:false" json:"is_hidden"`
}
func (LotMetadataConfig) TableName() string {
return "qt_lot_metadata"
}