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