package pricing import ( "time" "github.com/mchus/quoteforge/internal/config" "github.com/mchus/quoteforge/internal/models" "github.com/mchus/quoteforge/internal/repository" ) type Service struct { componentRepo *repository.ComponentRepository priceRepo *repository.PriceRepository config config.PricingConfig } func NewService( componentRepo *repository.ComponentRepository, priceRepo *repository.PriceRepository, cfg config.PricingConfig, ) *Service { return &Service{ componentRepo: componentRepo, priceRepo: priceRepo, config: cfg, } } // GetEffectivePrice returns the current effective price for a component // Priority: active override > calculated price > nil func (s *Service) GetEffectivePrice(lotName string) (*float64, error) { // Check for active override first override, err := s.priceRepo.GetPriceOverride(lotName) if err == nil && override != nil { return &override.Price, nil } // Get component metadata component, err := s.componentRepo.GetByLotName(lotName) if err != nil { return nil, err } return component.CurrentPrice, nil } // CalculatePrice calculates price using the specified method func (s *Service) CalculatePrice(lotName string, method models.PriceMethod, periodDays int) (float64, error) { if periodDays == 0 { periodDays = s.config.DefaultPeriodDays } points, err := s.priceRepo.GetPriceHistory(lotName, periodDays) if err != nil { return 0, err } if len(points) == 0 { return 0, nil } prices := make([]float64, len(points)) for i, p := range points { prices[i] = p.Price } switch method { case models.PriceMethodAverage: return CalculateAverage(prices), nil case models.PriceMethodWeightedMedian: return CalculateWeightedMedian(points, s.config.DefaultPeriodDays), nil case models.PriceMethodMedian: fallthrough default: return CalculateMedian(prices), nil } } // UpdateComponentPrice recalculates and updates the price for a component func (s *Service) UpdateComponentPrice(lotName string) error { component, err := s.componentRepo.GetByLotName(lotName) if err != nil { return err } price, err := s.CalculatePrice(lotName, component.PriceMethod, component.PricePeriodDays) if err != nil { return err } now := time.Now() if price > 0 { component.CurrentPrice = &price component.PriceUpdatedAt = &now } return s.componentRepo.Update(component) } // SetManualPrice sets a manual price override func (s *Service) SetManualPrice(lotName string, price float64, reason string, userID uint) error { override := &models.PriceOverride{ LotName: lotName, Price: price, ValidFrom: time.Now(), Reason: reason, CreatedBy: userID, } return s.priceRepo.CreatePriceOverride(override) } // UpdatePriceMethod changes the pricing method for a component func (s *Service) UpdatePriceMethod(lotName string, method models.PriceMethod, periodDays int) error { component, err := s.componentRepo.GetByLotName(lotName) if err != nil { return err } component.PriceMethod = method if periodDays > 0 { component.PricePeriodDays = periodDays } if err := s.componentRepo.Update(component); err != nil { return err } return s.UpdateComponentPrice(lotName) } // GetPriceStats returns statistics for a component's price history func (s *Service) GetPriceStats(lotName string, periodDays int) (*PriceStats, error) { if periodDays == 0 { periodDays = s.config.DefaultPeriodDays } points, err := s.priceRepo.GetPriceHistory(lotName, periodDays) if err != nil { return nil, err } if len(points) == 0 { return &PriceStats{QuoteCount: 0}, nil } prices := make([]float64, len(points)) for i, p := range points { prices[i] = p.Price } return &PriceStats{ QuoteCount: len(points), MinPrice: CalculatePercentile(prices, 0), MaxPrice: CalculatePercentile(prices, 100), MedianPrice: CalculateMedian(prices), AveragePrice: CalculateAverage(prices), StdDeviation: CalculateStdDev(prices), LatestPrice: points[0].Price, LatestDate: points[0].Date, OldestDate: points[len(points)-1].Date, Percentile25: CalculatePercentile(prices, 25), Percentile75: CalculatePercentile(prices, 75), }, nil } type PriceStats struct { QuoteCount int `json:"quote_count"` MinPrice float64 `json:"min_price"` MaxPrice float64 `json:"max_price"` MedianPrice float64 `json:"median_price"` AveragePrice float64 `json:"average_price"` StdDeviation float64 `json:"std_deviation"` LatestPrice float64 `json:"latest_price"` LatestDate time.Time `json:"latest_date"` OldestDate time.Time `json:"oldest_date"` Percentile25 float64 `json:"percentile_25"` Percentile75 float64 `json:"percentile_75"` }