- 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>
123 lines
6.2 KiB
Go
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"
|
|
}
|