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:
187
internal/services/component.go
Normal file
187
internal/services/component.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mchus/quoteforge/internal/models"
|
||||
"github.com/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, vendor, model from lot_name
|
||||
// "CPU_AMD_9654" → category="CPU", vendor="AMD", model="9654"
|
||||
// "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", vendor="INTEL", model="4.Sapphire_2S_32xDDR5"
|
||||
func ParsePartNumber(lotName string) (category, vendor, model string) {
|
||||
parts := strings.SplitN(lotName, "_", 3)
|
||||
if len(parts) >= 1 {
|
||||
category = parts[0]
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
vendor = parts[1]
|
||||
}
|
||||
if len(parts) >= 3 {
|
||||
model = parts[2]
|
||||
}
|
||||
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"`
|
||||
Vendor string `json:"vendor"`
|
||||
Model string `json:"model"`
|
||||
CurrentPrice *float64 `json:"current_price"`
|
||||
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 page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 || perPage > 100 {
|
||||
perPage = 20
|
||||
}
|
||||
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,
|
||||
Vendor: c.Vendor,
|
||||
Model: c.Model,
|
||||
CurrentPrice: c.CurrentPrice,
|
||||
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) {
|
||||
c, err := s.componentRepo.GetByLotName(lotName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Track usage
|
||||
_ = s.componentRepo.IncrementRequestCount(lotName)
|
||||
|
||||
view := &ComponentView{
|
||||
LotName: c.LotName,
|
||||
Vendor: c.Vendor,
|
||||
Model: c.Model,
|
||||
CurrentPrice: c.CurrentPrice,
|
||||
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) {
|
||||
return s.categoryRepo.GetAll()
|
||||
}
|
||||
|
||||
func (s *ComponentService) GetVendors(category string) ([]string, error) {
|
||||
return s.componentRepo.GetVendors(category)
|
||||
}
|
||||
|
||||
// ImportFromLot creates metadata entries for lots that don't have them
|
||||
func (s *ComponentService) ImportFromLot() (int, error) {
|
||||
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[cat.Code] = cat.ID
|
||||
}
|
||||
|
||||
imported := 0
|
||||
for _, lot := range lots {
|
||||
category, vendor, model := ParsePartNumber(lot.LotName)
|
||||
|
||||
metadata := &models.LotMetadata{
|
||||
LotName: lot.LotName,
|
||||
Vendor: vendor,
|
||||
Model: model,
|
||||
Specs: make(models.Specs),
|
||||
}
|
||||
|
||||
if catID, ok := categoryMap[category]; ok {
|
||||
metadata.CategoryID = &catID
|
||||
}
|
||||
|
||||
if err := s.componentRepo.Create(metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
imported++
|
||||
}
|
||||
|
||||
return imported, nil
|
||||
}
|
||||
Reference in New Issue
Block a user