package article import ( "errors" "fmt" "strings" "git.mchus.pro/mchus/quoteforge/internal/localdb" ) // ErrMissingCategoryForLot is returned when a lot has no category in local_pricelist_items.lot_category. var ErrMissingCategoryForLot = errors.New("missing_category_for_lot") type MissingCategoryForLotError struct { LotName string } func (e *MissingCategoryForLotError) Error() string { if e == nil || strings.TrimSpace(e.LotName) == "" { return ErrMissingCategoryForLot.Error() } return fmt.Sprintf("%s: %s", ErrMissingCategoryForLot.Error(), e.LotName) } func (e *MissingCategoryForLotError) Unwrap() error { return ErrMissingCategoryForLot } type Group string const ( GroupCPU Group = "CPU" GroupMEM Group = "MEM" GroupGPU Group = "GPU" GroupDISK Group = "DISK" GroupNET Group = "NET" GroupPSU Group = "PSU" ) // GroupForLotCategory maps pricelist lot_category codes into article groups. // Unknown/unrelated categories return ok=false. func GroupForLotCategory(cat string) (group Group, ok bool) { c := strings.ToUpper(strings.TrimSpace(cat)) switch c { case "CPU": return GroupCPU, true case "MEM": return GroupMEM, true case "GPU": return GroupGPU, true case "M2", "SSD", "HDD", "EDSFF", "HHHL": return GroupDISK, true case "NIC", "HCA", "DPU": return GroupNET, true case "HBA": return GroupNET, true case "PSU", "PS": return GroupPSU, true default: return "", false } } // ResolveLotCategoriesStrict resolves categories for lotNames using local_pricelist_items.lot_category // for a given server pricelist id. If any lot is missing or has empty category, returns an error. func ResolveLotCategoriesStrict(local *localdb.LocalDB, serverPricelistID uint, lotNames []string) (map[string]string, error) { if local == nil { return nil, fmt.Errorf("local db is nil") } cats, err := local.GetLocalLotCategoriesByServerPricelistID(serverPricelistID, lotNames) if err != nil { return nil, err } missing := make([]string, 0) for _, lot := range lotNames { cat := strings.TrimSpace(cats[lot]) if cat == "" { missing = append(missing, lot) continue } cats[lot] = cat } if len(missing) > 0 { fallback, err := local.GetLocalComponentCategoriesByLotNames(missing) if err != nil { return nil, err } for _, lot := range missing { if cat := strings.TrimSpace(fallback[lot]); cat != "" { cats[lot] = cat } } for _, lot := range missing { if strings.TrimSpace(cats[lot]) == "" { return nil, &MissingCategoryForLotError{LotName: lot} } } } return cats, nil } // NormalizeServerModel produces a stable article segment for the server model. func NormalizeServerModel(model string) string { trimmed := strings.TrimSpace(model) if trimmed == "" { return "" } upper := strings.ToUpper(trimmed) var b strings.Builder for _, r := range upper { if r >= 'A' && r <= 'Z' { b.WriteRune(r) continue } if r >= '0' && r <= '9' { b.WriteRune(r) continue } if r == '.' { b.WriteRune(r) } } return b.String() }