Add initial backend implementation
- Go module with Gin, GORM, JWT, excelize dependencies - Configuration loading from YAML with all settings - GORM models for users, categories, components, configurations, alerts - Repository layer for all entities - Services: auth (JWT), pricing (median/average/weighted), components, quotes, configurations, export (CSV/XLSX), alerts - Middleware: JWT auth, role-based access, CORS - HTTP handlers for all API endpoints - Main server with dependency injection and graceful shutdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
178
internal/services/pricing/service.go
Normal file
178
internal/services/pricing/service.go
Normal file
@@ -0,0 +1,178 @@
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user