feat: match services to supplied catalog

This commit is contained in:
Repinoid
2026-10-31 10:03:50 +03:00
parent 56997cb64e
commit cba97e25dc
+63
View File
@@ -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": []}