Files
PriceForge/internal/config/config.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

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)
}