"""Map messy PO product names to the nearest canonical sticker in the catalog.""" from __future__ import annotations from difflib import SequenceMatcher, get_close_matches from data import SAMPLE_CATALOG from parser_fallback import PRODUCT_ALIASES _CATALOG_LOWER = {p.lower(): p for p in SAMPLE_CATALOG} def resolve_product(raw: str, catalog: list[str] | None = None) -> str: """Return the closest catalog name for a parsed product string.""" if not raw or not str(raw).strip(): return raw catalog = catalog or SAMPLE_CATALOG catalog_lower = {p.lower(): p for p in catalog} cleaned = str(raw).strip() key = cleaned.lower() if key in catalog_lower: return catalog_lower[key] for alias, canonical in sorted(PRODUCT_ALIASES.items(), key=lambda x: -len(x[0])): if alias in key or key in alias: if canonical.lower() in catalog_lower: return catalog_lower[canonical.lower()] return canonical for cat_lower, canonical in catalog_lower.items(): if cat_lower in key or key in cat_lower: return canonical matches = get_close_matches(key, catalog_lower.keys(), n=1, cutoff=0.55) if matches: return catalog_lower[matches[0]] best_name, best_score = cleaned, 0.0 for cat_lower, canonical in catalog_lower.items(): score = SequenceMatcher(None, key, cat_lower).ratio() if score > best_score: best_score, best_name = score, canonical if best_score >= 0.5: return best_name return cleaned def resolve_items(items: list[dict], catalog: list[str] | None = None) -> list[dict]: """Normalize product field on each parsed item.""" out = [] for it in items: row = dict(it) raw = row.get("product", "") resolved = resolve_product(raw, catalog) if resolved != raw and not row.get("notes"): row["notes"] = f"matched from: {raw}" elif resolved != raw: row["notes"] = f"{row['notes']} (matched from: {raw})" row["product"] = resolved out.append(row) return out