Files
descrybe/apps/api/internal/processing/category.go
T
2026-08-16 16:57:36 +02:00

185 lines
4.8 KiB
Go

package processing
import (
"strconv"
"strings"
"unicode"
)
// categoryDisplayLabel returns the human category name for meta / synthesize /
// {{category}}. Prefers CategoryName; never falls back to a digits-only unique_id.
func categoryDisplayLabel(result StepResult) string {
if n := strings.TrimSpace(result.CategoryName); n != "" {
return n
}
cat := strings.TrimSpace(result.Category)
if cat == "" || isDigitsOnlyCategoryID(cat) {
return ""
}
return cat
}
// resolveCategoryDisplayName looks up unique_id → name; returns "" when unknown
// so callers do not treat "50" as a human label.
func resolveCategoryDisplayName(uid string, namesByUID map[string]string) string {
uid = strings.TrimSpace(uid)
if uid == "" || len(namesByUID) == 0 {
return ""
}
return strings.TrimSpace(namesByUID[uid])
}
func syncCategoryName(out *StepResult, namesByUID map[string]string) {
if out == nil {
return
}
if n := resolveCategoryDisplayName(out.Category, namesByUID); n != "" {
out.CategoryName = n
}
}
func isDigitsOnlyCategoryID(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if !unicode.IsDigit(r) {
return false
}
}
return true
}
// categoryUniqueIDFromAny extracts a company category unique_id from feed/mapped
// shapes: plain string/number, nested {unique_id|category_unique_id|#text|…}, or
// a one-element array of those. Returns "" for empty/"none"/unusable values.
func categoryUniqueIDFromAny(v any) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return normalizeCategoryUniqueID(t)
case float64:
if t == float64(int64(t)) {
return normalizeCategoryUniqueID(strconv.FormatInt(int64(t), 10))
}
return normalizeCategoryUniqueID(strconv.FormatFloat(t, 'f', -1, 64))
case float32:
return categoryUniqueIDFromAny(float64(t))
case int:
return normalizeCategoryUniqueID(strconv.Itoa(t))
case int64:
return normalizeCategoryUniqueID(strconv.FormatInt(t, 10))
case int32:
return normalizeCategoryUniqueID(strconv.FormatInt(int64(t), 10))
case jsonNumberStringer:
return normalizeCategoryUniqueID(t.String())
case map[string]any:
for _, k := range []string{
"unique_id", "category_unique_id", "categoryUniqueId",
"#text", "text", "id", "value", "code",
} {
if s := categoryUniqueIDFromAny(t[k]); s != "" {
return s
}
}
return ""
case []any:
for _, item := range t {
if s := categoryUniqueIDFromAny(item); s != "" {
return s
}
}
return ""
default:
return ""
}
}
// jsonNumberStringer matches encoding/json.Number without importing encoding/json here.
type jsonNumberStringer interface {
String() string
}
func normalizeCategoryUniqueID(s string) string {
s = strings.TrimSpace(s)
if s == "" || s == "<nil>" || strings.EqualFold(s, "none") {
return ""
}
return s
}
// categoryUniqueIDFromMaps returns the first usable category unique_id from maps
// (normalized/mapped/raw). Prefer canonical keys; mapped should win by call order.
func categoryUniqueIDFromMaps(maps ...map[string]any) string {
for _, m := range maps {
if m == nil {
continue
}
for _, k := range []string{
"category", "category_unique_id", "categoryUniqueId",
"Category", "product_type", "productType",
} {
if s := categoryUniqueIDFromAny(m[k]); s != "" {
return s
}
}
}
return ""
}
// coerceMappedCategoryUniqueID writes a string unique_id onto m["category"] when
// category (or category_unique_id) can be resolved — so RunSteps string paths stay deterministic.
func coerceMappedCategoryUniqueID(m map[string]any) string {
if m == nil {
return ""
}
cat := categoryUniqueIDFromMaps(m)
if cat == "" {
return ""
}
m["category"] = cat
if _, ok := m["category_unique_id"]; !ok {
m["category_unique_id"] = cat
}
return cat
}
// applyCategoryFromMapped sets StepResult.Category from feed/mapped unique_id when present.
// Does not clear an existing Category when maps lack a usable value (caller may preserve prior).
func applyCategoryFromMapped(out *StepResult, maps ...map[string]any) {
if out == nil {
return
}
cat := categoryUniqueIDFromMaps(maps...)
if cat == "" {
return
}
out.Category = SanitizeText(cat)
if out.FieldSources == nil {
out.FieldSources = map[string]any{}
}
out.FieldSources["category"] = "mapped"
}
// filterCategoryIfInvalid clears Category when the company has a known unique_id set
// and the value is not in that set. Empty valid set means "skip validation" (tests / no taxonomy).
func filterCategoryIfInvalid(out *StepResult, valid map[string]struct{}) {
if out == nil || len(valid) == 0 {
return
}
cat := strings.TrimSpace(out.Category)
if cat == "" {
return
}
if _, ok := valid[cat]; ok {
return
}
out.Category = ""
if out.FieldSources != nil {
delete(out.FieldSources, "category")
}
out.Notes = append(out.Notes, "category: ignored (unknown unique_id for company)")
}