package services import ( "errors" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/repository" "git.mchus.pro/mchus/quoteforge/internal/services/pricing" ) var ( ErrEmptyQuote = errors.New("quote cannot be empty") ErrComponentNotFound = errors.New("component not found") ErrNoPriceAvailable = errors.New("no price available for component") ) type QuoteService struct { componentRepo *repository.ComponentRepository statsRepo *repository.StatsRepository pricingService *pricing.Service } func NewQuoteService( componentRepo *repository.ComponentRepository, statsRepo *repository.StatsRepository, pricingService *pricing.Service, ) *QuoteService { return &QuoteService{ componentRepo: componentRepo, statsRepo: statsRepo, pricingService: pricingService, } } type QuoteItem struct { LotName string `json:"lot_name"` Quantity int `json:"quantity"` UnitPrice float64 `json:"unit_price"` TotalPrice float64 `json:"total_price"` Description string `json:"description"` Category string `json:"category"` HasPrice bool `json:"has_price"` } type QuoteValidationResult struct { Valid bool `json:"valid"` Items []QuoteItem `json:"items"` Errors []string `json:"errors"` Warnings []string `json:"warnings"` Total float64 `json:"total"` } type QuoteRequest struct { Items []struct { LotName string `json:"lot_name"` Quantity int `json:"quantity"` } `json:"items"` } func (s *QuoteService) ValidateAndCalculate(req *QuoteRequest) (*QuoteValidationResult, error) { if len(req.Items) == 0 { return nil, ErrEmptyQuote } if s.componentRepo == nil || s.pricingService == nil { return nil, errors.New("offline mode: quote calculation not available") } result := &QuoteValidationResult{ Valid: true, Items: make([]QuoteItem, 0, len(req.Items)), Errors: make([]string, 0), Warnings: make([]string, 0), } lotNames := make([]string, len(req.Items)) quantities := make(map[string]int) for i, item := range req.Items { lotNames[i] = item.LotName quantities[item.LotName] = item.Quantity } components, err := s.componentRepo.GetMultiple(lotNames) if err != nil { return nil, err } componentMap := make(map[string]*models.LotMetadata) for i := range components { componentMap[components[i].LotName] = &components[i] } var total float64 for _, reqItem := range req.Items { comp, exists := componentMap[reqItem.LotName] if !exists { result.Valid = false result.Errors = append(result.Errors, "Component not found: "+reqItem.LotName) continue } item := QuoteItem{ LotName: reqItem.LotName, Quantity: reqItem.Quantity, HasPrice: false, } if comp.Lot != nil { item.Description = comp.Lot.LotDescription } if comp.Category != nil { item.Category = comp.Category.Code } // Get effective price (override or calculated) price, err := s.pricingService.GetEffectivePrice(reqItem.LotName) if err == nil && price != nil && *price > 0 { item.UnitPrice = *price item.TotalPrice = *price * float64(reqItem.Quantity) item.HasPrice = true total += item.TotalPrice } else { result.Warnings = append(result.Warnings, "No price available for: "+reqItem.LotName) } result.Items = append(result.Items, item) } result.Total = total return result, nil } // RecordUsage records that components were used in a quote func (s *QuoteService) RecordUsage(items []models.ConfigItem) error { if s.statsRepo == nil { // Offline mode: usage stats are unavailable and should not block config saves. return nil } for _, item := range items { revenue := item.UnitPrice * float64(item.Quantity) if err := s.statsRepo.IncrementUsage(item.LotName, item.Quantity, revenue); err != nil { return err } } return nil }