feat: match services to supplied catalog
This commit is contained in:
@@ -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": []}
|
||||
Reference in New Issue
Block a user