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:
139
internal/services/quote.go
Normal file
139
internal/services/quote.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mchus/quoteforge/internal/models"
|
||||
"github.com/mchus/quoteforge/internal/repository"
|
||||
"github.com/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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user