From cba97e25dc8f7f9a1b5b9b7c259d356622a82471 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 31 Oct 2026 10:03:50 +0300 Subject: [PATCH] feat: match services to supplied catalog --- site/services/catalog_matcher.py | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 site/services/catalog_matcher.py diff --git a/site/services/catalog_matcher.py b/site/services/catalog_matcher.py new file mode 100644 index 0000000..f01adfb --- /dev/null +++ b/site/services/catalog_matcher.py @@ -0,0 +1,63 @@ +"""Детерминированное сопоставление услуги с переданным каталогом SKU. + +В workspace нет утверждённого каталога коммерческих артикулов, поэтому модуль +не содержит выдуманных SKU и принимает каталог явным аргументом. Не найденное +соответствие возвращает `UNKNOWN`, не мешая сверять даты и суммы договора. +""" +import re + + +def _normalize_text(value): + """Схлопнуть регистр и пунктуацию для точного сравнения названий каталога.""" + return re.sub(r"[^0-9A-ZА-ЯЁ]", "", str(value or "").upper().replace("Ё", "Е")) + + +def _normalize_sku(value): + """Нормализовать SKU, сохраняя значимый PAYG-суффикс `-m`.""" + text = str(value or "").strip().upper() + payg = text.endswith("-M") + core = re.sub(r"[^0-9A-ZА-ЯЁ]", "", text[:-2] if payg else text) + return core + ("-M" if payg else "") + + +def match_catalog_sku(service_item, catalog_entries): + """Найти единственный SKU по явному SKU или точному нормализованному имени. + + PAYG-артикулы с суффиксом `-m` доступны только для тарифа `payg`; обычные + абонентские услуги не поглощают такой артикул. При нескольких точных + совпадениях возвращается `AMBIGUOUS`, а при пустом каталоге — `UNKNOWN`. + """ + entries = list(catalog_entries or []) + charge_type = str(service_item.get("charge_type") or "unknown").lower() + service_sku = _normalize_sku(service_item.get("catalog_sku")) + service_name = _normalize_text( + service_item.get("canonical_name") or service_item.get("raw_name") or "", + ) + matches = [] + for entry in entries: + if not isinstance(entry, dict) or not entry.get("sku"): + continue + entry_sku = _normalize_sku(entry["sku"]) + entry_charge = str(entry.get("charge_type") or "unknown").lower() + is_payg_sku = entry_sku.endswith("-M") + if is_payg_sku and charge_type != "payg": + continue + if entry_charge not in {"unknown", charge_type}: + continue + exact_sku = bool(service_sku and service_sku == entry_sku) + exact_name = bool( + service_name + and service_name in { + _normalize_text(entry.get("name")), + *(_normalize_text(alias) for alias in entry.get("aliases", [])), + } + ) + if exact_sku or exact_name: + matches.append(entry) + unique = { _normalize_sku(entry["sku"]): entry for entry in matches } + if len(unique) == 1: + sku, entry = next(iter(unique.items())) + return {"status": "MATCHED", "sku": entry["sku"], "catalog_entry": entry} + if unique: + return {"status": "AMBIGUOUS", "sku": None, "candidates": list(unique.values())} + return {"status": "UNKNOWN", "sku": None, "candidates": []} \ No newline at end of file