package services import ( "fmt" "strings" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/repository" ) type ComponentService struct { componentRepo *repository.ComponentRepository categoryRepo *repository.CategoryRepository statsRepo *repository.StatsRepository } func NewComponentService( componentRepo *repository.ComponentRepository, categoryRepo *repository.CategoryRepository, statsRepo *repository.StatsRepository, ) *ComponentService { return &ComponentService{ componentRepo: componentRepo, categoryRepo: categoryRepo, statsRepo: statsRepo, } } // ParsePartNumber extracts category and model from lot_name // "CPU_AMD_9654" → category="CPU", model="AMD_9654" // "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", model="INTEL_4.Sapphire_2S_32xDDR5" func ParsePartNumber(lotName string) (category, model string) { parts := strings.SplitN(lotName, "_", 2) if len(parts) >= 1 { category = parts[0] } if len(parts) >= 2 { model = parts[1] } return } type ComponentListResult struct { Components []ComponentView `json:"components"` Total int64 `json:"total"` Page int `json:"page"` PerPage int `json:"per_page"` } type ComponentView struct { LotName string `json:"lot_name"` Description string `json:"description"` Category string `json:"category"` CategoryName string `json:"category_name"` Model string `json:"model"` PriceFreshness models.PriceFreshness `json:"price_freshness"` PopularityScore float64 `json:"popularity_score"` Specs models.Specs `json:"specs,omitempty"` } func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage int) (*ComponentListResult, error) { // If no database connection (offline mode), return empty list // Components should be loaded via /api/sync/components first if s.componentRepo == nil { return &ComponentListResult{ Components: []ComponentView{}, Total: 0, Page: page, PerPage: perPage, }, nil } if page < 1 { page = 1 } if perPage < 1 { perPage = 20 } if perPage > 5000 { perPage = 5000 } offset := (page - 1) * perPage components, total, err := s.componentRepo.List(filter, offset, perPage) if err != nil { return nil, err } views := make([]ComponentView, len(components)) for i, c := range components { view := ComponentView{ LotName: c.LotName, Model: c.Model, PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3), PopularityScore: c.PopularityScore, Specs: c.Specs, } if c.Lot != nil { view.Description = c.Lot.LotDescription } if c.Category != nil { view.Category = c.Category.Code view.CategoryName = c.Category.Name } views[i] = view } return &ComponentListResult{ Components: views, Total: total, Page: page, PerPage: perPage, }, nil } func (s *ComponentService) GetByLotName(lotName string) (*ComponentView, error) { // If no database connection (offline mode), return error if s.componentRepo == nil { return nil, fmt.Errorf("offline mode: component data not available") } c, err := s.componentRepo.GetByLotName(lotName) if err != nil { return nil, err } // Track usage _ = s.componentRepo.IncrementRequestCount(lotName) view := &ComponentView{ LotName: c.LotName, Model: c.Model, PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3), PopularityScore: c.PopularityScore, Specs: c.Specs, } if c.Lot != nil { view.Description = c.Lot.LotDescription } if c.Category != nil { view.Category = c.Category.Code view.CategoryName = c.Category.Name } return view, nil } func (s *ComponentService) GetCategories() ([]models.Category, error) { // If no database connection (offline mode), return default categories if s.categoryRepo == nil { return models.DefaultCategories, nil } return s.categoryRepo.GetAll() } // ImportFromLot creates metadata entries for lots that don't have them func (s *ComponentService) ImportFromLot() (int, error) { // If no database connection (offline mode), return error if s.componentRepo == nil || s.categoryRepo == nil { return 0, fmt.Errorf("offline mode: import not available") } lots, err := s.componentRepo.GetLotsWithoutMetadata() if err != nil { return 0, err } categories, err := s.categoryRepo.GetAll() if err != nil { return 0, err } categoryMap := make(map[string]uint) for _, cat := range categories { categoryMap[strings.ToUpper(cat.Code)] = cat.ID } imported := 0 for _, lot := range lots { // Use lot_category from database if available, otherwise parse from lot_name var category string if lot.LotCategory != nil && *lot.LotCategory != "" { category = strings.ToUpper(*lot.LotCategory) } else { category, _ = ParsePartNumber(lot.LotName) category = strings.ToUpper(category) } _, model := ParsePartNumber(lot.LotName) metadata := &models.LotMetadata{ LotName: lot.LotName, Model: model, Specs: make(models.Specs), } if catID, ok := categoryMap[category]; ok { metadata.CategoryID = &catID } else { // Create new category if it doesn't exist newCat, err := s.categoryRepo.CreateIfNotExists(category) if err == nil && newCat != nil { metadata.CategoryID = &newCat.ID } } if err := s.componentRepo.Create(metadata); err != nil { continue } imported++ } return imported, nil }