- 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>
212 lines
6.0 KiB
Go
212 lines
6.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
mysqlDriver "github.com/go-sql-driver/mysql"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Database DatabaseConfig `yaml:"database"`
|
|
Pricing PricingConfig `yaml:"pricing"`
|
|
Export ExportConfig `yaml:"export"`
|
|
Alerts AlertsConfig `yaml:"alerts"`
|
|
Scheduler SchedulerConfig `yaml:"scheduler"`
|
|
Notifications NotificationsConfig `yaml:"notifications"`
|
|
Logging LoggingConfig `yaml:"logging"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
Mode string `yaml:"mode"`
|
|
ReadTimeout time.Duration `yaml:"read_timeout"`
|
|
WriteTimeout time.Duration `yaml:"write_timeout"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
Name string `yaml:"name"`
|
|
User string `yaml:"user"`
|
|
Password string `yaml:"password"`
|
|
MaxOpenConns int `yaml:"max_open_conns"`
|
|
MaxIdleConns int `yaml:"max_idle_conns"`
|
|
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
|
|
}
|
|
|
|
func (d *DatabaseConfig) DSN() string {
|
|
cfg := mysqlDriver.NewConfig()
|
|
cfg.User = d.User
|
|
cfg.Passwd = d.Password
|
|
cfg.Net = "tcp"
|
|
cfg.Addr = net.JoinHostPort(d.Host, strconv.Itoa(d.Port))
|
|
cfg.DBName = d.Name
|
|
cfg.ParseTime = true
|
|
cfg.Loc = time.Local
|
|
cfg.Params = map[string]string{
|
|
"charset": "utf8mb4",
|
|
}
|
|
return cfg.FormatDSN()
|
|
}
|
|
|
|
type PricingConfig struct {
|
|
DefaultMethod string `yaml:"default_method"`
|
|
DefaultPeriodDays int `yaml:"default_period_days"`
|
|
FreshnessGreenDays int `yaml:"freshness_green_days"`
|
|
FreshnessYellowDays int `yaml:"freshness_yellow_days"`
|
|
FreshnessRedDays int `yaml:"freshness_red_days"`
|
|
MinQuotesForMedian int `yaml:"min_quotes_for_median"`
|
|
PopularityDecayDays int `yaml:"popularity_decay_days"`
|
|
}
|
|
|
|
type ExportConfig struct {
|
|
TempDir string `yaml:"temp_dir"`
|
|
MaxFileAge time.Duration `yaml:"max_file_age"`
|
|
CompanyName string `yaml:"company_name"`
|
|
}
|
|
|
|
type AlertsConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
CheckInterval time.Duration `yaml:"check_interval"`
|
|
HighDemandThreshold int `yaml:"high_demand_threshold"`
|
|
TrendingThresholdPercent int `yaml:"trending_threshold_percent"`
|
|
}
|
|
|
|
type SchedulerConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
PollInterval time.Duration `yaml:"poll_interval"`
|
|
AlertsInterval time.Duration `yaml:"alerts_interval"`
|
|
UpdatePricesInterval time.Duration `yaml:"update_prices_interval"`
|
|
UpdatePopularityInterval time.Duration `yaml:"update_popularity_interval"`
|
|
ResetWeeklyCountersInterval time.Duration `yaml:"reset_weekly_counters_interval"`
|
|
ResetMonthlyCountersInterval time.Duration `yaml:"reset_monthly_counters_interval"`
|
|
PartsLogBackfillInterval time.Duration `yaml:"parts_log_backfill_interval"`
|
|
}
|
|
|
|
type NotificationsConfig struct {
|
|
EmailEnabled bool `yaml:"email_enabled"`
|
|
SMTPHost string `yaml:"smtp_host"`
|
|
SMTPPort int `yaml:"smtp_port"`
|
|
SMTPUser string `yaml:"smtp_user"`
|
|
SMTPPassword string `yaml:"smtp_password"`
|
|
FromAddress string `yaml:"from_address"`
|
|
}
|
|
|
|
type LoggingConfig struct {
|
|
Level string `yaml:"level"`
|
|
Format string `yaml:"format"`
|
|
Output string `yaml:"output"`
|
|
FilePath string `yaml:"file_path"`
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config file: %w", err)
|
|
}
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing config file: %w", err)
|
|
}
|
|
|
|
cfg.setDefaults()
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func (c *Config) setDefaults() {
|
|
if c.Server.Host == "" {
|
|
c.Server.Host = "127.0.0.1"
|
|
}
|
|
if c.Server.Port == 0 {
|
|
c.Server.Port = 8084
|
|
}
|
|
if c.Server.Mode == "" {
|
|
c.Server.Mode = "release"
|
|
}
|
|
if c.Server.ReadTimeout == 0 {
|
|
c.Server.ReadTimeout = 30 * time.Second
|
|
}
|
|
if c.Server.WriteTimeout == 0 {
|
|
c.Server.WriteTimeout = 30 * time.Second
|
|
}
|
|
|
|
if c.Database.Port == 0 {
|
|
c.Database.Port = 3306
|
|
}
|
|
if c.Database.MaxOpenConns == 0 {
|
|
c.Database.MaxOpenConns = 25
|
|
}
|
|
if c.Database.MaxIdleConns == 0 {
|
|
c.Database.MaxIdleConns = 5
|
|
}
|
|
if c.Database.ConnMaxLifetime == 0 {
|
|
c.Database.ConnMaxLifetime = 5 * time.Minute
|
|
}
|
|
|
|
if c.Pricing.DefaultMethod == "" {
|
|
c.Pricing.DefaultMethod = "weighted_median"
|
|
}
|
|
if c.Pricing.DefaultPeriodDays == 0 {
|
|
c.Pricing.DefaultPeriodDays = 90
|
|
}
|
|
if c.Pricing.FreshnessGreenDays == 0 {
|
|
c.Pricing.FreshnessGreenDays = 30
|
|
}
|
|
if c.Pricing.FreshnessYellowDays == 0 {
|
|
c.Pricing.FreshnessYellowDays = 60
|
|
}
|
|
if c.Pricing.FreshnessRedDays == 0 {
|
|
c.Pricing.FreshnessRedDays = 90
|
|
}
|
|
if c.Pricing.MinQuotesForMedian == 0 {
|
|
c.Pricing.MinQuotesForMedian = 3
|
|
}
|
|
|
|
if c.Logging.Level == "" {
|
|
c.Logging.Level = "info"
|
|
}
|
|
if c.Logging.Format == "" {
|
|
c.Logging.Format = "json"
|
|
}
|
|
if c.Logging.Output == "" {
|
|
c.Logging.Output = "stdout"
|
|
}
|
|
if c.Scheduler.PollInterval == 0 {
|
|
c.Scheduler.PollInterval = 1 * time.Minute
|
|
}
|
|
if c.Alerts.CheckInterval == 0 {
|
|
c.Alerts.CheckInterval = 1 * time.Hour
|
|
}
|
|
if c.Scheduler.AlertsInterval == 0 {
|
|
c.Scheduler.AlertsInterval = c.Alerts.CheckInterval
|
|
}
|
|
if c.Scheduler.UpdatePricesInterval == 0 {
|
|
c.Scheduler.UpdatePricesInterval = 24 * time.Hour
|
|
}
|
|
if c.Scheduler.UpdatePopularityInterval == 0 {
|
|
c.Scheduler.UpdatePopularityInterval = 24 * time.Hour
|
|
}
|
|
if c.Scheduler.ResetWeeklyCountersInterval == 0 {
|
|
c.Scheduler.ResetWeeklyCountersInterval = 7 * 24 * time.Hour
|
|
}
|
|
if c.Scheduler.ResetMonthlyCountersInterval == 0 {
|
|
c.Scheduler.ResetMonthlyCountersInterval = 30 * 24 * time.Hour
|
|
}
|
|
if c.Scheduler.PartsLogBackfillInterval == 0 {
|
|
c.Scheduler.PartsLogBackfillInterval = 24 * time.Hour
|
|
}
|
|
}
|
|
|
|
func (c *Config) Address() string {
|
|
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
|
}
|