package pricing import ( "time" "git.mchus.pro/mchus/quoteforge/internal/config" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/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"` } // RecalculateAllPrices recalculates prices for all components func (s *Service) RecalculateAllPrices() (updated int, errors int) { // Get all components filter := repository.ComponentFilter{} offset := 0 limit := 100 for { components, _, err := s.componentRepo.List(filter, offset, limit) if err != nil || len(components) == 0 { break } for _, comp := range components { if err := s.UpdateComponentPrice(comp.LotName); err != nil { errors++ } else { updated++ } } offset += limit } return updated, errors }