Implement warehouse/lot pricing updates and configurator performance fixes
This commit is contained in:
@@ -2,6 +2,9 @@ package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
@@ -21,6 +24,9 @@ type QuoteService struct {
|
||||
pricelistRepo *repository.PricelistRepository
|
||||
localDB *localdb.LocalDB
|
||||
pricingService *pricing.Service
|
||||
cacheMu sync.RWMutex
|
||||
priceCache map[string]cachedLotPrice
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
func NewQuoteService(
|
||||
@@ -36,9 +42,16 @@ func NewQuoteService(
|
||||
pricelistRepo: pricelistRepo,
|
||||
localDB: localDB,
|
||||
pricingService: pricingService,
|
||||
priceCache: make(map[string]cachedLotPrice, 4096),
|
||||
cacheTTL: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
type cachedLotPrice struct {
|
||||
price *float64
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type QuoteItem struct {
|
||||
LotName string `json:"lot_name"`
|
||||
Quantity int `json:"quantity"`
|
||||
@@ -70,6 +83,7 @@ type PriceLevelsRequest struct {
|
||||
Quantity int `json:"quantity"`
|
||||
} `json:"items"`
|
||||
PricelistIDs map[string]uint `json:"pricelist_ids,omitempty"`
|
||||
NoCache bool `json:"no_cache,omitempty"`
|
||||
}
|
||||
|
||||
type PriceLevelsItem struct {
|
||||
@@ -170,11 +184,55 @@ func (s *QuoteService) CalculatePriceLevels(req *PriceLevelsRequest) (*PriceLeve
|
||||
return nil, ErrEmptyQuote
|
||||
}
|
||||
|
||||
lotNames := make([]string, 0, len(req.Items))
|
||||
seenLots := make(map[string]struct{}, len(req.Items))
|
||||
for _, reqItem := range req.Items {
|
||||
if _, ok := seenLots[reqItem.LotName]; ok {
|
||||
continue
|
||||
}
|
||||
seenLots[reqItem.LotName] = struct{}{}
|
||||
lotNames = append(lotNames, reqItem.LotName)
|
||||
}
|
||||
|
||||
result := &PriceLevelsResult{
|
||||
Items: make([]PriceLevelsItem, 0, len(req.Items)),
|
||||
ResolvedPricelistIDs: map[string]uint{},
|
||||
}
|
||||
|
||||
type levelState struct {
|
||||
id uint
|
||||
prices map[string]float64
|
||||
}
|
||||
levelBySource := map[models.PricelistSource]*levelState{
|
||||
models.PricelistSourceEstimate: {prices: map[string]float64{}},
|
||||
models.PricelistSourceWarehouse: {prices: map[string]float64{}},
|
||||
models.PricelistSourceCompetitor: {prices: map[string]float64{}},
|
||||
}
|
||||
|
||||
for source, st := range levelBySource {
|
||||
sourceKey := string(source)
|
||||
if req.PricelistIDs != nil {
|
||||
if explicitID, ok := req.PricelistIDs[sourceKey]; ok && explicitID > 0 {
|
||||
st.id = explicitID
|
||||
result.ResolvedPricelistIDs[sourceKey] = explicitID
|
||||
}
|
||||
}
|
||||
if st.id == 0 && s.pricelistRepo != nil {
|
||||
latest, err := s.pricelistRepo.GetLatestActiveBySource(sourceKey)
|
||||
if err == nil {
|
||||
st.id = latest.ID
|
||||
result.ResolvedPricelistIDs[sourceKey] = latest.ID
|
||||
}
|
||||
}
|
||||
if st.id == 0 {
|
||||
continue
|
||||
}
|
||||
prices, err := s.lookupPricesByPricelistID(st.id, lotNames, req.NoCache)
|
||||
if err == nil {
|
||||
st.prices = prices
|
||||
}
|
||||
}
|
||||
|
||||
for _, reqItem := range req.Items {
|
||||
item := PriceLevelsItem{
|
||||
LotName: reqItem.LotName,
|
||||
@@ -182,22 +240,17 @@ func (s *QuoteService) CalculatePriceLevels(req *PriceLevelsRequest) (*PriceLeve
|
||||
PriceMissing: make([]string, 0, 3),
|
||||
}
|
||||
|
||||
estimatePrice, estimateID := s.lookupLevelPrice(models.PricelistSourceEstimate, reqItem.LotName, req.PricelistIDs)
|
||||
warehousePrice, warehouseID := s.lookupLevelPrice(models.PricelistSourceWarehouse, reqItem.LotName, req.PricelistIDs)
|
||||
competitorPrice, competitorID := s.lookupLevelPrice(models.PricelistSourceCompetitor, reqItem.LotName, req.PricelistIDs)
|
||||
|
||||
item.EstimatePrice = estimatePrice
|
||||
item.WarehousePrice = warehousePrice
|
||||
item.CompetitorPrice = competitorPrice
|
||||
|
||||
if estimateID != 0 {
|
||||
result.ResolvedPricelistIDs[string(models.PricelistSourceEstimate)] = estimateID
|
||||
if p, ok := levelBySource[models.PricelistSourceEstimate].prices[reqItem.LotName]; ok && p > 0 {
|
||||
price := p
|
||||
item.EstimatePrice = &price
|
||||
}
|
||||
if warehouseID != 0 {
|
||||
result.ResolvedPricelistIDs[string(models.PricelistSourceWarehouse)] = warehouseID
|
||||
if p, ok := levelBySource[models.PricelistSourceWarehouse].prices[reqItem.LotName]; ok && p > 0 {
|
||||
price := p
|
||||
item.WarehousePrice = &price
|
||||
}
|
||||
if competitorID != 0 {
|
||||
result.ResolvedPricelistIDs[string(models.PricelistSourceCompetitor)] = competitorID
|
||||
if p, ok := levelBySource[models.PricelistSourceCompetitor].prices[reqItem.LotName]; ok && p > 0 {
|
||||
price := p
|
||||
item.CompetitorPrice = &price
|
||||
}
|
||||
|
||||
if item.EstimatePrice == nil {
|
||||
@@ -220,6 +273,93 @@ func (s *QuoteService) CalculatePriceLevels(req *PriceLevelsRequest) (*PriceLeve
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *QuoteService) lookupPricesByPricelistID(pricelistID uint, lotNames []string, noCache bool) (map[string]float64, error) {
|
||||
result := make(map[string]float64, len(lotNames))
|
||||
if pricelistID == 0 || len(lotNames) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
missing := make([]string, 0, len(lotNames))
|
||||
if noCache {
|
||||
missing = append(missing, lotNames...)
|
||||
} else {
|
||||
now := time.Now()
|
||||
s.cacheMu.RLock()
|
||||
for _, lotName := range lotNames {
|
||||
if entry, ok := s.priceCache[s.cacheKey(pricelistID, lotName)]; ok && entry.expiresAt.After(now) {
|
||||
if entry.price != nil && *entry.price > 0 {
|
||||
result[lotName] = *entry.price
|
||||
}
|
||||
continue
|
||||
}
|
||||
missing = append(missing, lotName)
|
||||
}
|
||||
s.cacheMu.RUnlock()
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
loaded := make(map[string]float64, len(missing))
|
||||
if s.pricelistRepo != nil {
|
||||
prices, err := s.pricelistRepo.GetPricesForLots(pricelistID, missing)
|
||||
if err == nil {
|
||||
for lotName, price := range prices {
|
||||
if price > 0 {
|
||||
result[lotName] = price
|
||||
loaded[lotName] = price
|
||||
}
|
||||
}
|
||||
s.updateCache(pricelistID, missing, loaded)
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback path (usually offline): local per-lot lookup.
|
||||
if s.localDB != nil {
|
||||
for _, lotName := range missing {
|
||||
price, found := s.lookupPriceByPricelistID(pricelistID, lotName)
|
||||
if found && price > 0 {
|
||||
result[lotName] = price
|
||||
loaded[lotName] = price
|
||||
}
|
||||
}
|
||||
s.updateCache(pricelistID, missing, loaded)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("price lookup unavailable for pricelist %d", pricelistID)
|
||||
}
|
||||
|
||||
func (s *QuoteService) updateCache(pricelistID uint, requested []string, loaded map[string]float64) {
|
||||
if len(requested) == 0 {
|
||||
return
|
||||
}
|
||||
expiresAt := time.Now().Add(s.cacheTTL)
|
||||
s.cacheMu.Lock()
|
||||
defer s.cacheMu.Unlock()
|
||||
|
||||
for _, lotName := range requested {
|
||||
if price, ok := loaded[lotName]; ok && price > 0 {
|
||||
priceCopy := price
|
||||
s.priceCache[s.cacheKey(pricelistID, lotName)] = cachedLotPrice{
|
||||
price: &priceCopy,
|
||||
expiresAt: expiresAt,
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.priceCache[s.cacheKey(pricelistID, lotName)] = cachedLotPrice{
|
||||
price: nil,
|
||||
expiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *QuoteService) cacheKey(pricelistID uint, lotName string) string {
|
||||
return fmt.Sprintf("%d|%s", pricelistID, lotName)
|
||||
}
|
||||
|
||||
func calculateDelta(target, base *float64) (*float64, *float64) {
|
||||
if target == nil || base == nil {
|
||||
return nil, nil
|
||||
|
||||
Reference in New Issue
Block a user