refactor lot matching into shared module

This commit is contained in:
2026-02-07 06:22:56 +03:00
parent b629af9742
commit 95b5f8bf65
14 changed files with 1190 additions and 520 deletions

View File

@@ -3,12 +3,12 @@ package repository
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/warehouse"
"gorm.io/gorm"
)
@@ -288,60 +288,11 @@ func (r *PricelistRepository) enrichWarehouseItems(items []models.PricelistItem)
return nil
}
lotSet := make(map[string]struct{}, len(lots))
for _, lot := range lots {
lotSet[lot] = struct{}{}
}
resolver, err := r.newWarehouseLotResolver()
qtyByLot, partnumbersByLot, err := warehouse.LoadLotMetrics(r.db, lots, true)
if err != nil {
return err
}
var logs []struct {
Partnumber string `gorm:"column:partnumber"`
Qty *float64 `gorm:"column:qty"`
}
if err := r.db.Model(&models.StockLog{}).Select("partnumber, qty").Find(&logs).Error; err != nil {
return err
}
qtyByLot := make(map[string]float64, len(lots))
for _, row := range logs {
if row.Qty == nil {
continue
}
lot, err := resolver.resolve(row.Partnumber)
if err != nil {
continue
}
if _, ok := lotSet[lot]; !ok {
continue
}
qtyByLot[lot] += *row.Qty
}
var mappings []models.LotPartnumber
if err := r.db.Where("lot_name IN ? AND TRIM(lot_name) <> ''", lots).
Order("partnumber ASC").
Find(&mappings).Error; err != nil {
return err
}
partnumbersByLot := make(map[string][]string, len(lots))
seenPair := make(map[string]struct{}, len(mappings))
for _, m := range mappings {
lot := strings.TrimSpace(m.LotName)
pn := strings.TrimSpace(m.Partnumber)
if lot == "" || pn == "" {
continue
}
key := lot + "\x00" + strings.ToLower(pn)
if _, ok := seenPair[key]; ok {
continue
}
seenPair[key] = struct{}{}
partnumbersByLot[lot] = append(partnumbersByLot[lot], pn)
}
for i := range items {
if qty, ok := qtyByLot[items[i].LotName]; ok {
q := qty
@@ -352,131 +303,6 @@ func (r *PricelistRepository) enrichWarehouseItems(items []models.PricelistItem)
return nil
}
var (
errWarehouseResolveConflict = errors.New("multiple lot matches")
errWarehouseResolveNotFound = errors.New("lot not found")
)
type warehouseLotResolver struct {
partnumberToLots map[string][]string
exactLots map[string]string
allLots []string
}
func (r *PricelistRepository) newWarehouseLotResolver() (*warehouseLotResolver, error) {
var mappings []models.LotPartnumber
if err := r.db.Find(&mappings).Error; err != nil {
return nil, err
}
partnumberToLots := make(map[string][]string, len(mappings))
for _, m := range mappings {
pn := normalizeWarehouseResolverKey(m.Partnumber)
lot := strings.TrimSpace(m.LotName)
if pn == "" || lot == "" {
continue
}
partnumberToLots[pn] = append(partnumberToLots[pn], lot)
}
for key, vals := range partnumberToLots {
partnumberToLots[key] = uniqueWarehouseStrings(vals)
}
var allLotsRows []models.Lot
if err := r.db.Select("lot_name").Find(&allLotsRows).Error; err != nil {
return nil, err
}
exactLots := make(map[string]string, len(allLotsRows))
allLots := make([]string, 0, len(allLotsRows))
for _, row := range allLotsRows {
lot := strings.TrimSpace(row.LotName)
if lot == "" {
continue
}
exactLots[normalizeWarehouseResolverKey(lot)] = lot
allLots = append(allLots, lot)
}
sort.Slice(allLots, func(i, j int) bool {
li := len([]rune(allLots[i]))
lj := len([]rune(allLots[j]))
if li == lj {
return allLots[i] < allLots[j]
}
return li > lj
})
return &warehouseLotResolver{
partnumberToLots: partnumberToLots,
exactLots: exactLots,
allLots: allLots,
}, nil
}
func (r *warehouseLotResolver) resolve(partnumber string) (string, error) {
key := normalizeWarehouseResolverKey(partnumber)
if key == "" {
return "", errWarehouseResolveNotFound
}
if mapped := r.partnumberToLots[key]; len(mapped) > 0 {
if len(mapped) == 1 {
return mapped[0], nil
}
return "", errWarehouseResolveConflict
}
if exact, ok := r.exactLots[key]; ok {
return exact, nil
}
best := ""
bestLen := -1
tie := false
for _, lot := range r.allLots {
lotKey := normalizeWarehouseResolverKey(lot)
if lotKey == "" {
continue
}
if strings.HasPrefix(key, lotKey) {
l := len([]rune(lotKey))
if l > bestLen {
best = lot
bestLen = l
tie = false
} else if l == bestLen && !strings.EqualFold(best, lot) {
tie = true
}
}
}
if best == "" {
return "", errWarehouseResolveNotFound
}
if tie {
return "", errWarehouseResolveConflict
}
return best, nil
}
func normalizeWarehouseResolverKey(v string) string {
return strings.ToLower(strings.TrimSpace(v))
}
func uniqueWarehouseStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, v := range values {
n := strings.TrimSpace(v)
if n == "" {
continue
}
k := strings.ToLower(n)
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
out = append(out, n)
}
return out
}
// GetPriceForLot returns item price for a lot within a pricelist.
func (r *PricelistRepository) GetPriceForLot(pricelistID uint, lotName string) (float64, error) {
var item models.PricelistItem