142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
package services
|
|
|
|
import (
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
|
"math"
|
|
)
|
|
|
|
// ResolvedBOMRow is the result of resolving a single vendor BOM row.
|
|
type ResolvedBOMRow struct {
|
|
localdb.VendorSpecItem
|
|
// ResolutionSource already on VendorSpecItem: "book", "manual_suggestion", "unresolved"
|
|
}
|
|
|
|
// AggregatedLOT represents a LOT with its aggregated quantity from the BOM.
|
|
type AggregatedLOT struct {
|
|
LotName string
|
|
Quantity int
|
|
}
|
|
|
|
// VendorSpecResolver resolves vendor BOM rows to LOT names using the active partnumber book.
|
|
type VendorSpecResolver struct {
|
|
bookRepo *repository.PartnumberBookRepository
|
|
}
|
|
|
|
func NewVendorSpecResolver(bookRepo *repository.PartnumberBookRepository) *VendorSpecResolver {
|
|
return &VendorSpecResolver{bookRepo: bookRepo}
|
|
}
|
|
|
|
// Resolve resolves each vendor spec item's lot name using the 3-step algorithm.
|
|
// It returns the resolved items. Manual lot suggestions from the input are preserved as pre-fill.
|
|
func (r *VendorSpecResolver) Resolve(items []localdb.VendorSpecItem) ([]localdb.VendorSpecItem, error) {
|
|
// Step 1: Get the active book
|
|
book, err := r.bookRepo.GetActiveBook()
|
|
if err != nil {
|
|
// No book available — mark all as unresolved
|
|
for i := range items {
|
|
if items[i].ResolvedLotName == "" {
|
|
items[i].ResolutionSource = "unresolved"
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
for i, item := range items {
|
|
pn := item.VendorPartnumber
|
|
|
|
// Step 1: Look up in active book
|
|
matches, err := r.bookRepo.FindLotByPartnumber(book.ID, pn)
|
|
if err == nil && len(matches) > 0 {
|
|
items[i].LotMappings = make([]localdb.VendorSpecLotMapping, 0, len(matches[0].LotsJSON))
|
|
for _, lot := range matches[0].LotsJSON {
|
|
if lot.LotName == "" {
|
|
continue
|
|
}
|
|
items[i].LotMappings = append(items[i].LotMappings, localdb.VendorSpecLotMapping{
|
|
LotName: lot.LotName,
|
|
QuantityPerPN: lotQtyToInt(lot.Qty),
|
|
})
|
|
}
|
|
if len(items[i].LotMappings) > 0 {
|
|
items[i].ResolvedLotName = items[i].LotMappings[0].LotName
|
|
}
|
|
items[i].ResolutionSource = "book"
|
|
continue
|
|
}
|
|
|
|
// Step 2: Pre-fill from manual_lot_suggestion if provided
|
|
if item.ManualLotSuggestion != "" {
|
|
items[i].ResolvedLotName = item.ManualLotSuggestion
|
|
items[i].ResolutionSource = "manual_suggestion"
|
|
continue
|
|
}
|
|
|
|
// Step 3: Unresolved
|
|
items[i].ResolvedLotName = ""
|
|
items[i].ResolutionSource = "unresolved"
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
// AggregateLOTs applies qty from the resolved PN composition stored in lots_json.
|
|
func AggregateLOTs(items []localdb.VendorSpecItem, book *localdb.LocalPartnumberBook, bookRepo *repository.PartnumberBookRepository) ([]AggregatedLOT, error) {
|
|
lotTotals := make(map[string]int)
|
|
|
|
if book != nil {
|
|
for _, item := range items {
|
|
if item.ResolvedLotName == "" {
|
|
continue
|
|
}
|
|
lot := item.ResolvedLotName
|
|
pn := item.VendorPartnumber
|
|
|
|
matches, err := bookRepo.FindLotByPartnumber(book.ID, pn)
|
|
if err != nil || len(matches) == 0 {
|
|
lotTotals[lot] += item.Quantity
|
|
continue
|
|
}
|
|
for _, m := range matches {
|
|
for _, mappedLot := range m.LotsJSON {
|
|
if mappedLot.LotName != lot {
|
|
continue
|
|
}
|
|
lotTotals[lot] += item.Quantity * lotQtyToInt(mappedLot.Qty)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// No book: all resolved rows contribute qty=1 per lot
|
|
for _, item := range items {
|
|
if item.ResolvedLotName != "" {
|
|
lotTotals[item.ResolvedLotName] += item.Quantity
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build aggregated list
|
|
seen := make(map[string]bool)
|
|
var result []AggregatedLOT
|
|
for _, item := range items {
|
|
lot := item.ResolvedLotName
|
|
if lot == "" || seen[lot] {
|
|
continue
|
|
}
|
|
seen[lot] = true
|
|
qty := lotTotals[lot]
|
|
if qty < 1 {
|
|
qty = 1
|
|
}
|
|
result = append(result, AggregatedLOT{LotName: lot, Quantity: qty})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func lotQtyToInt(qty float64) int {
|
|
if qty < 1 {
|
|
return 1
|
|
}
|
|
return int(math.Round(qty))
|
|
}
|