bugfix: search engine
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
@@ -41,6 +42,48 @@ class TimApiClient:
|
|||||||
path = self.FIXTURES / f"{name}.json"
|
path = self.FIXTURES / f"{name}.json"
|
||||||
return json.loads(path.read_text(encoding="utf-8"))
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _env_first(*names: str, default: str = "") -> str:
|
||||||
|
"""Retorna a primeira variável não vazia, preservando aliases do Contas original."""
|
||||||
|
for name in names:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is not None and str(value).strip():
|
||||||
|
return str(value).strip()
|
||||||
|
return default
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auth_value(
|
||||||
|
cls,
|
||||||
|
*auth_names: str,
|
||||||
|
user_names: tuple[str, ...] = (),
|
||||||
|
password_names: tuple[str, ...] = (),
|
||||||
|
) -> str:
|
||||||
|
user = cls._env_first(*user_names) if user_names else ""
|
||||||
|
password = cls._env_first(*password_names) if password_names else ""
|
||||||
|
if user and password:
|
||||||
|
token = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("ascii")
|
||||||
|
return f"Basic {token}"
|
||||||
|
raw = cls._env_first(*auth_names)
|
||||||
|
if not raw:
|
||||||
|
return ""
|
||||||
|
low = raw.lower()
|
||||||
|
if low.startswith("basic ") or low.startswith("bearer "):
|
||||||
|
return raw
|
||||||
|
if ":" in raw and " " not in raw:
|
||||||
|
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _url_with_msisdn(base: str, msisdn: str, *, normalize_country: bool = False) -> str:
|
||||||
|
text = str(base or "").strip()
|
||||||
|
digits = "".join(ch for ch in str(msisdn or "") if ch.isdigit())
|
||||||
|
if normalize_country and digits and not digits.startswith("55"):
|
||||||
|
digits = f"55{digits}"
|
||||||
|
value = digits or str(msisdn or "").strip()
|
||||||
|
if "{msisdn}" in text:
|
||||||
|
return text.replace("{msisdn}", quote(value))
|
||||||
|
return f"{text.rstrip('/')}/{quote(value)}" if text else value
|
||||||
|
|
||||||
def _headers(self, *, client_id: str | None = None, auth: str | None = None, extra: dict[str, str] | None = None) -> dict[str, str]:
|
def _headers(self, *, client_id: str | None = None, auth: str | None = None, extra: dict[str, str] | None = None) -> dict[str, str]:
|
||||||
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
if client_id:
|
if client_id:
|
||||||
@@ -107,22 +150,33 @@ class TimApiClient:
|
|||||||
def consultar_faturas(self, msisdn: str) -> Any:
|
def consultar_faturas(self, msisdn: str) -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("complete_invoices")
|
return self.fixture("complete_invoices")
|
||||||
url = os.getenv("TIM_COMPLETE_INVOICES_URL", "")
|
url = self._env_first("TIM_COMPLETE_INVOICES_URL")
|
||||||
return self.request("POST", url, payload={"msisdn": msisdn}, headers=self._headers(client_id=None, auth=os.getenv("TIM_COMPLETE_INVOICES_AUTH", ""), extra={"ClientID": os.getenv("TIM_COMPLETE_INVOICES_CLIENT_ID", "AIAGENTCR")}))
|
return self.request(
|
||||||
|
"POST", url, payload={"msisdn": msisdn},
|
||||||
|
headers=self._headers(
|
||||||
|
client_id=None, auth=self._env_first("TIM_COMPLETE_INVOICES_AUTH"),
|
||||||
|
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", default="AIAGENTCR")},
|
||||||
|
),
|
||||||
|
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", default="30")),
|
||||||
|
)
|
||||||
|
|
||||||
def billing_analysis(self, msisdn: str, **context: Any) -> Any:
|
def billing_analysis(self, msisdn: str, **context: Any) -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("divergencia")
|
return self.fixture("divergencia")
|
||||||
base = os.getenv("TIM_DIVERGENCIA_URL", "").rstrip("/")
|
base = self._env_first("TIM_DIVERGENCIA_URL").rstrip("/")
|
||||||
url = f"{base}/{quote(msisdn)}"
|
url = f"{base}/{quote(msisdn)}"
|
||||||
auth = os.getenv("TIM_DIVERGENCIA_AUTH", "") or os.getenv("TIM_SECURE_PDF_AUTH", "")
|
auth = self._auth_value(
|
||||||
|
"TIM_DIVERGENCIA_AUTH", "TIM_DIVERGENCE_AUTH",
|
||||||
|
user_names=("TIM_DIVERGENCIA_USER", "TIM_DIVERGENCIA_USERNAME", "TIM_DIVERGENCE_USER", "TIM_DIVERGENCE_USERNAME"),
|
||||||
|
password_names=("TIM_DIVERGENCIA_PASSWORD", "TIM_DIVERGENCIA_PASS", "TIM_DIVERGENCE_PASSWORD", "TIM_DIVERGENCE_PASS"),
|
||||||
|
)
|
||||||
headers = self._headers(client_id=None, auth=auth, extra={
|
headers = self._headers(client_id=None, auth=auth, extra={
|
||||||
"clientID": os.getenv("TIM_DIVERGENCIA_CLIENT_ID", "AIAGENTCR"),
|
"clientID": self._env_first("TIM_DIVERGENCIA_CLIENT_ID", default="AIAGENTCR"),
|
||||||
})
|
})
|
||||||
params = {"channel": context.get("channel") or "AIAGENTCR"}
|
params = {"channel": context.get("channel") or "AIAGENTCR"}
|
||||||
attempts: list[dict[str, Any]] = []
|
attempts: list[dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
result = self.request("GET", url, params=params, headers=headers, timeout=int(os.getenv("TIM_DIVERGENCIA_TIMEOUT", "120")), attempt_log=attempts)
|
result = self.request("GET", url, params=params, headers=headers, timeout=int(self._env_first("TIM_DIVERGENCIA_TIMEOUT", "TIM_DIVERGENCE_TIMEOUT", default="120")), attempt_log=attempts)
|
||||||
return self._attach_transport(result, operation="base_conhecimento", attempts=attempts)
|
return self._attach_transport(result, operation="base_conhecimento", attempts=attempts)
|
||||||
except TimApiError as exc:
|
except TimApiError as exc:
|
||||||
if not exc.attempts:
|
if not exc.attempts:
|
||||||
@@ -132,9 +186,16 @@ class TimApiClient:
|
|||||||
def consultar_vas(self, msisdn: str) -> Any:
|
def consultar_vas(self, msisdn: str) -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("query_vas")
|
return self.fixture("query_vas")
|
||||||
base = os.getenv("TIM_URL_CONSULTA_VAS", "")
|
base = self._env_first("TIM_URL_CONSULTA_VAS", "TIM_CONSULTA_URL")
|
||||||
url = base.replace("{msisdn}", quote(msisdn))
|
url = self._url_with_msisdn(base, msisdn, normalize_country=True)
|
||||||
return self.request("GET", url, headers=self._headers(client_id=None, auth=os.getenv("TIM_CONSULTA_AUTH", ""), extra={"clientId": os.getenv("TIM_CONSULTA_CLIENT_ID", "AIAAGENTCR")}))
|
return self.request(
|
||||||
|
"GET", url,
|
||||||
|
headers=self._headers(
|
||||||
|
client_id=None, auth=self._env_first("TIM_QUERY_AUTH", "TIM_CONSULTA_AUTH"),
|
||||||
|
extra={"clientId": self._env_first("TIM_CONSULTA_CLIENT_ID", default="AIAAGENTCR")},
|
||||||
|
),
|
||||||
|
timeout=int(self._env_first("TIM_QUERY_TIMEOUT", "TIM_CONSULTA_TIMEOUT", default="30")),
|
||||||
|
)
|
||||||
|
|
||||||
def historico_vas(self, msisdn: str) -> Any:
|
def historico_vas(self, msisdn: str) -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
@@ -146,7 +207,7 @@ class TimApiClient:
|
|||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_VAS_HISTORY_AUTH", ""),
|
auth=os.getenv("TIM_VAS_HISTORY_AUTH", ""),
|
||||||
extra={
|
extra={
|
||||||
"clientId": os.getenv("TIM_VAS_HISTORY_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "CHAT")),
|
"clientId": self._env_first("TIM_VAS_HISTORY_CLIENT_ID", default="AIAGENTCR"),
|
||||||
"messageId": os.getenv("TIM_VAS_HISTORY_MESSAGE_ID", "") or str(uuid.uuid4()),
|
"messageId": os.getenv("TIM_VAS_HISTORY_MESSAGE_ID", "") or str(uuid.uuid4()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -155,35 +216,35 @@ class TimApiClient:
|
|||||||
def bloquear_vas(self, msisdn: str, service: dict[str, Any]) -> Any:
|
def bloquear_vas(self, msisdn: str, service: dict[str, Any]) -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("block_vas")
|
return self.fixture("block_vas")
|
||||||
url = os.getenv("TIM_URL_BLOQUEIO_VAS", "")
|
url = self._env_first("TIM_URL_BLOQUEIO_VAS", "TIM_BLOQUEIO_URL")
|
||||||
digits = "".join(ch for ch in str(msisdn) if ch.isdigit())
|
digits = "".join(ch for ch in str(msisdn) if ch.isdigit())
|
||||||
normalized = digits[2:] if len(digits) == 13 else digits
|
normalized = digits[2:] if len(digits) == 13 else digits
|
||||||
app_id = str(service.get("appId") or service.get("app_id") or "")
|
app_id = str(service.get("appId") or service.get("app_id") or "")
|
||||||
csp_id = str(service.get("cspId") or service.get("csp_id") or os.getenv("TIM_DEFAULT_CSP_ID", "740"))
|
csp_id = str(service.get("cspId") or service.get("csp_id") or os.getenv("TIM_DEFAULT_CSP_ID", "740"))
|
||||||
op = os.getenv("TIM_BLOQUEIO_OPERATION_TYPE", "block")
|
op = self._env_first("TIM_BLOCK_OPERATION_TYPE", "TIM_BLOQUEIO_OPERATION_TYPE", default="block")
|
||||||
base = {"Customer": {"Msisdn": normalized}, "AppId": app_id, "CspId": csp_id, "TypeOperation": op}
|
base = {"Customer": {"Msisdn": normalized}, "AppId": app_id, "CspId": csp_id, "TypeOperation": op}
|
||||||
payloads = [
|
payloads = [
|
||||||
{"customer": {"msisdn": normalized}, "appId": app_id, "cspId": csp_id, "typeOperation": op},
|
{"customer": {"msisdn": normalized}, "appId": app_id, "cspId": csp_id, "typeOperation": op},
|
||||||
{"input": base, "Input": base},
|
{"input": base, "Input": base},
|
||||||
{"vasBlock": {"msisdn": normalized, "appId": app_id, "cspId": csp_id, "type": op}},
|
{"vasBlock": {"msisdn": normalized, "appId": app_id, "cspId": csp_id, "type": op}},
|
||||||
]
|
]
|
||||||
mode = os.getenv("TIM_BLOQUEIO_PAYLOAD_MODE", "auto").strip().lower()
|
mode = self._env_first("TIM_BLOCK_PAYLOAD_MODE", "TIM_BLOQUEIO_PAYLOAD_MODE", default="auto").lower()
|
||||||
if mode == "vasblock": payloads = [payloads[2], payloads[0]]
|
if mode == "vasblock": payloads = [payloads[2], payloads[0]]
|
||||||
elif mode == "input": payloads = [payloads[1], payloads[0]]
|
elif mode == "input": payloads = [payloads[1], payloads[0]]
|
||||||
elif mode == "pmid": payloads = [payloads[0], payloads[1]]
|
elif mode == "pmid": payloads = [payloads[0], payloads[1]]
|
||||||
headers = self._headers(
|
headers = self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_BLOQUEIO_AUTH", ""),
|
auth=self._env_first("TIM_BLOCK_AUTH", "TIM_BLOQUEIO_AUTH"),
|
||||||
extra={
|
extra={
|
||||||
"Accept-Encoding": os.getenv("TIM_BLOQUEIO_ACCEPT_ENCODING", "gzip,deflate"),
|
"Accept-Encoding": self._env_first("TIM_BLOCK_ACCEPT_ENCODING", "TIM_BLOQUEIO_ACCEPT_ENCODING", default="gzip,deflate"),
|
||||||
"clientId": os.getenv("TIM_BLOQUEIO_CLIENT_ID", "AIAGENTCR"),
|
"clientId": self._env_first("TIM_BLOCK_CLIENT_ID", "TIM_BLOQUEIO_CLIENT_ID", default="AIAGENTCR"),
|
||||||
"messageId": str(uuid.uuid4()),
|
"messageId": str(uuid.uuid4()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
last = None
|
last = None
|
||||||
for index, payload in enumerate(payloads):
|
for index, payload in enumerate(payloads):
|
||||||
try:
|
try:
|
||||||
return self.request("POST", url, payload=payload, headers=headers, timeout=int(os.getenv("TIM_BLOQUEIO_TIMEOUT", "30")))
|
return self.request("POST", url, payload=payload, headers=headers, timeout=int(self._env_first("TIM_BLOCK_TIMEOUT", "TIM_BLOQUEIO_TIMEOUT", default="30")))
|
||||||
except TimApiError as exc:
|
except TimApiError as exc:
|
||||||
last = exc
|
last = exc
|
||||||
if exc.status_code != 400 or index >= len(payloads) - 1:
|
if exc.status_code != 400 or index >= len(payloads) - 1:
|
||||||
@@ -193,7 +254,7 @@ class TimApiClient:
|
|||||||
def cancelar_vas(self, msisdn: str, service: dict[str, Any], *, protocol: str = "") -> Any:
|
def cancelar_vas(self, msisdn: str, service: dict[str, Any], *, protocol: str = "") -> Any:
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("cancel_vas")
|
return self.fixture("cancel_vas")
|
||||||
url = os.getenv("TIM_CANCELAMENTO_URL", "")
|
url = self._env_first("TIM_CANCELLATION_URL", "TIM_CANCELAMENTO_URL")
|
||||||
payload = {
|
payload = {
|
||||||
"channel": "AIAGENTCR",
|
"channel": "AIAGENTCR",
|
||||||
"msisdn": msisdn,
|
"msisdn": msisdn,
|
||||||
@@ -203,17 +264,17 @@ class TimApiClient:
|
|||||||
}
|
}
|
||||||
headers = self._headers(
|
headers = self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_CANCELAMENTO_AUTH", ""),
|
auth=self._env_first("TIM_CANCELLATION_AUTH", "TIM_CANCELAMENTO_AUTH"),
|
||||||
extra={
|
extra={
|
||||||
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR")),
|
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR")),
|
||||||
"messageId": str(uuid.uuid4()),
|
"messageId": str(uuid.uuid4()),
|
||||||
"AuthorizationOAM": os.getenv("TIM_CANCELAMENTO_AUTHORIZATION_OAM", ""),
|
"AuthorizationOAM": self._env_first("TIM_CANCELLATION_AUTH_OAM", "TIM_CANCELAMENTO_AUTH_OAM", "TIM_CANCELAMENTO_AUTHORIZATION_OAM"),
|
||||||
"Cn_field": os.getenv("TIM_CANCELAMENTO_CN_FIELD", ""),
|
"Cn_field": self._env_first("TIM_CANCELLATION_CN_FIELD", "TIM_CANCELAMENTO_CN_FIELD"),
|
||||||
"Type_field": os.getenv("TIM_CANCELAMENTO_TYPE_FIELD", ""),
|
"Type_field": self._env_first("TIM_CANCELLATION_TYPE_FIELD", "TIM_CANCELAMENTO_TYPE_FIELD"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
attempts: list[dict[str, Any]] = []
|
attempts: list[dict[str, Any]] = []
|
||||||
result = self.request("DELETE", url, payload=payload, headers=headers, timeout=int(os.getenv("TIM_CANCELAMENTO_TIMEOUT", "30")), attempt_log=attempts)
|
result = self.request("DELETE", url, payload=payload, headers=headers, timeout=int(self._env_first("TIM_CANCELLATION_TIMEOUT", "TIM_CANCELAMENTO_TIMEOUT", default="30")), attempt_log=attempts)
|
||||||
return self._attach_transport(result, operation="cancela_vas", attempts=attempts)
|
return self._attach_transport(result, operation="cancela_vas", attempts=attempts)
|
||||||
|
|
||||||
def contrato(self, msisdn: str) -> Any:
|
def contrato(self, msisdn: str) -> Any:
|
||||||
@@ -223,7 +284,7 @@ class TimApiClient:
|
|||||||
headers = self._headers(
|
headers = self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_CONTRATO_AUTH", ""),
|
auth=os.getenv("TIM_CONTRATO_AUTH", ""),
|
||||||
extra={"clientId": os.getenv("TIM_CONTRATO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "CHAT"))},
|
extra={"clientId": self._env_first("TIM_CONTRATO_CLIENT_ID", default="AIAGENTCR")},
|
||||||
)
|
)
|
||||||
return self.request("GET", f"{base}/{quote(msisdn)}", headers=headers, timeout=int(os.getenv("TIM_CONTRATO_TIMEOUT", "30")))
|
return self.request("GET", f"{base}/{quote(msisdn)}", headers=headers, timeout=int(os.getenv("TIM_CONTRATO_TIMEOUT", "30")))
|
||||||
|
|
||||||
@@ -231,13 +292,13 @@ class TimApiClient:
|
|||||||
if self.mock:
|
if self.mock:
|
||||||
# contract fixture carries representative customer identity in local mode.
|
# contract fixture carries representative customer identity in local mode.
|
||||||
return self.fixture("contrato")
|
return self.fixture("contrato")
|
||||||
url = os.getenv("TIM_PROFILE_FULL_URL", "").replace("{msisdn}", quote(msisdn))
|
url = self._url_with_msisdn(self._env_first("TIM_PROFILE_FULL_URL"), msisdn)
|
||||||
headers = self._headers(
|
headers = self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
||||||
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
||||||
)
|
)
|
||||||
return self.request("GET", url, headers=headers)
|
return self.request("GET", url, headers=headers, timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")))
|
||||||
|
|
||||||
def abrir_protocolo(self, payload: dict[str, Any]) -> Any:
|
def abrir_protocolo(self, payload: dict[str, Any]) -> Any:
|
||||||
"""Registra protocolo V2 preservando o contrato externo do Contas original.
|
"""Registra protocolo V2 preservando o contrato externo do Contas original.
|
||||||
@@ -317,7 +378,7 @@ class TimApiClient:
|
|||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("contestacao_tool")
|
return self.fixture("contestacao_tool")
|
||||||
data = dict(payload)
|
data = dict(payload)
|
||||||
data.setdefault("userId", "AIAGENTCR")
|
data.setdefault("userId", self._env_first("TIM_CUSTOMER_CONTESTATION_USER_ID", default="AIAGENTCR"))
|
||||||
data.setdefault("customerIdCurrent", data.get("customerId") or "")
|
data.setdefault("customerIdCurrent", data.get("customerId") or "")
|
||||||
data.setdefault("customerType", "2")
|
data.setdefault("customerType", "2")
|
||||||
data.setdefault("customerStatus", "1")
|
data.setdefault("customerStatus", "1")
|
||||||
@@ -445,17 +506,17 @@ class TimApiClient:
|
|||||||
"""Perfil de faturamento usando o mesmo contrato CompleteInvoices do original."""
|
"""Perfil de faturamento usando o mesmo contrato CompleteInvoices do original."""
|
||||||
if self.mock:
|
if self.mock:
|
||||||
return self.fixture("profile_bill")
|
return self.fixture("profile_bill")
|
||||||
url = os.getenv("TIM_URL_PERFIL_FATURA", "") or os.getenv("TIM_COMPLETE_INVOICES_URL", "")
|
url = self._env_first("TIM_COMPLETE_INVOICES_URL", "TIM_URL_PERFIL_FATURA")
|
||||||
return self.request(
|
return self.request(
|
||||||
"POST",
|
"POST",
|
||||||
url,
|
url,
|
||||||
payload={"msisdn": msisdn},
|
payload={"msisdn": msisdn},
|
||||||
headers=self._headers(
|
headers=self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_PROFILE_BILL_AUTH", "") or os.getenv("TIM_COMPLETE_INVOICES_AUTH", ""),
|
auth=self._env_first("TIM_COMPLETE_INVOICES_AUTH", "TIM_PROFILE_BILL_AUTH"),
|
||||||
extra={"ClientID": os.getenv("TIM_PROFILE_BILL_CLIENT_ID", "AIAGENTCR")},
|
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", "TIM_PROFILE_BILL_CLIENT_ID", default="AIAGENTCR")},
|
||||||
),
|
),
|
||||||
timeout=int(os.getenv("TIM_PROFILE_BILL_TIMEOUT", "30")),
|
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", "TIM_PROFILE_BILL_TIMEOUT", default="30")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def line_info(self, msisdn: str) -> Any:
|
def line_info(self, msisdn: str) -> Any:
|
||||||
@@ -463,7 +524,7 @@ class TimApiClient:
|
|||||||
if self.mock:
|
if self.mock:
|
||||||
payload = self.fixture("contrato")
|
payload = self.fixture("contrato")
|
||||||
else:
|
else:
|
||||||
url = os.getenv("TIM_PROFILE_FULL_URL", "").replace("{msisdn}", quote(msisdn))
|
url = self._url_with_msisdn(self._env_first("TIM_PROFILE_FULL_URL"), msisdn)
|
||||||
payload = self.request(
|
payload = self.request(
|
||||||
"GET",
|
"GET",
|
||||||
url,
|
url,
|
||||||
@@ -472,6 +533,7 @@ class TimApiClient:
|
|||||||
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
||||||
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
||||||
),
|
),
|
||||||
|
timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")),
|
||||||
)
|
)
|
||||||
def extract(value: Any) -> str:
|
def extract(value: Any) -> str:
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
@@ -517,9 +579,9 @@ class TimApiClient:
|
|||||||
}
|
}
|
||||||
headers = self._headers(
|
headers = self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_BILL_PDF_AUTH", "") or os.getenv("TIM_SECURE_PDF_AUTH", ""),
|
auth=self._env_first("TIM_BILL_PDF_AUTH", "TIM_SECURE_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
|
||||||
extra={
|
extra={
|
||||||
"clientId": os.getenv("TIM_BILL_PDF_CLIENT_ID", "AIAGENTCR"),
|
"clientId": self._env_first("TIM_BILL_PDF_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR"),
|
||||||
"Accept": "application/pdf",
|
"Accept": "application/pdf",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -528,7 +590,7 @@ class TimApiClient:
|
|||||||
url,
|
url,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(os.getenv("TIM_BILL_PDF_TIMEOUT", os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30"))),
|
timeout=int(self._env_first("TIM_BILL_PDF_TIMEOUT", "TIM_SECURE_PDF_TIMEOUT", "TIM_INVOICE_RECOVER_TIMEOUT", default="30")),
|
||||||
)
|
)
|
||||||
raw_content = result.get("raw_content") if isinstance(result, dict) else None
|
raw_content = result.get("raw_content") if isinstance(result, dict) else None
|
||||||
parsed = result.get("parsed_content") if isinstance(result, dict) else None
|
parsed = result.get("parsed_content") if isinstance(result, dict) else None
|
||||||
@@ -558,8 +620,8 @@ class TimApiClient:
|
|||||||
params=params,
|
params=params,
|
||||||
headers=self._headers(
|
headers=self._headers(
|
||||||
client_id=None,
|
client_id=None,
|
||||||
auth=os.getenv("TIM_SECURE_PDF_AUTH", ""),
|
auth=self._env_first("TIM_SECURE_PDF_AUTH", "TIM_BILL_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
|
||||||
extra={"clientId": os.getenv("TIM_INVOICE_RECOVER_CLIENT_ID", "AIAGENTCR")},
|
extra={"clientid": self._env_first("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR")},
|
||||||
),
|
),
|
||||||
timeout=int(os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30")),
|
timeout=int(os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30")),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ class InvoiceResolver:
|
|||||||
for mention in mentioned_items:
|
for mention in mentioned_items:
|
||||||
if not isinstance(mention, str) or not mention.strip():
|
if not isinstance(mention, str) or not mention.strip():
|
||||||
continue
|
continue
|
||||||
resolved.extend(self._resolve_one_mention(mention, invoice_detail))
|
resolved.extend(self._resolve_one_mention(mention, invoice_detail, include_identity_oos=False))
|
||||||
return self._dedupe_exact_matches(resolved)
|
return self._dedupe_exact_matches(resolved)
|
||||||
|
|
||||||
def resolve_each(
|
def resolve_each(
|
||||||
@@ -344,7 +344,7 @@ class InvoiceResolver:
|
|||||||
ordered.append((idx, name))
|
ordered.append((idx, name))
|
||||||
lines[idx] = msisdn
|
lines[idx] = msisdn
|
||||||
dates[idx] = date
|
dates[idx] = date
|
||||||
matches = self._resolve_one_mention(name, invoice_detail)
|
matches = self._resolve_one_mention(name, invoice_detail, include_identity_oos=True)
|
||||||
if matches:
|
if matches:
|
||||||
deterministic[idx] = matches
|
deterministic[idx] = matches
|
||||||
elif (
|
elif (
|
||||||
@@ -537,6 +537,8 @@ class InvoiceResolver:
|
|||||||
self,
|
self,
|
||||||
mention: str,
|
mention: str,
|
||||||
invoice_detail: dict[str, Any],
|
invoice_detail: dict[str, Any],
|
||||||
|
*,
|
||||||
|
include_identity_oos: bool = False,
|
||||||
) -> list[ResolvedInvoiceItem]:
|
) -> list[ResolvedInvoiceItem]:
|
||||||
"""Match determinístico de UMA menção contra todas as combinações
|
"""Match determinístico de UMA menção contra todas as combinações
|
||||||
msisdn × seção × entry, com precedência de match exato sobre substring.
|
msisdn × seção × entry, com precedência de match exato sobre substring.
|
||||||
@@ -564,20 +566,33 @@ class InvoiceResolver:
|
|||||||
casava por substring o prefixo ``"VOD + Canais Abertos"`` — item errado;
|
casava por substring o prefixo ``"VOD + Canais Abertos"`` — item errado;
|
||||||
a normalização a torna exata ao item ``+Fechados``.)"""
|
a normalização a torna exata ao item ``+Fechados``.)"""
|
||||||
mention_normalized = self._normalize_match_text(mention)
|
mention_normalized = self._normalize_match_text(mention)
|
||||||
|
|
||||||
|
# Primeiro procure igualdade EXATA em todo o catálogo da fatura, inclusive
|
||||||
|
# seções não acionáveis (Plano, descontos etc.). Isso é um guard de identidade:
|
||||||
|
# se o cliente nomeou precisamente um item conhecido, esse item precisa vencer
|
||||||
|
# antes de qualquer fuzzy matching. Sem isso, um plano explicitamente citado
|
||||||
|
# pode ser removido do universo tratável e o matcher acabar autorizando outro
|
||||||
|
# VAS apenas por similaridade (ex.: TIM CTRL Redes Sociais -> TIM Fashion).
|
||||||
|
if include_identity_oos:
|
||||||
|
identity_candidates = list(self._iter_identity_candidates(invoice_detail))
|
||||||
|
exact_identity = [
|
||||||
|
cand
|
||||||
|
for cand in identity_candidates
|
||||||
|
if mention_normalized
|
||||||
|
and self._normalize_match_text(cand.desc) == mention_normalized
|
||||||
|
]
|
||||||
|
if exact_identity:
|
||||||
|
return [self._build_item(cand) for cand in exact_identity]
|
||||||
|
|
||||||
|
# Fuzzy/substring continua restrito ao universo de serviços acionáveis +
|
||||||
|
# out-of-scope de seções de serviço. Seções explicitamente não acionáveis
|
||||||
|
# jamais entram no matcher aproximado; elas só podem bloquear por igualdade
|
||||||
|
# exata acima. Isso mantém o comportamento conservador do fluxo.
|
||||||
candidates = list(self._iter_candidates(invoice_detail))
|
candidates = list(self._iter_candidates(invoice_detail))
|
||||||
exact_cands: list[_Candidate] = []
|
|
||||||
substring_cands: list[_Candidate] = []
|
substring_cands: list[_Candidate] = []
|
||||||
for cand in candidates:
|
for cand in candidates:
|
||||||
desc_normalized = self._normalize_match_text(cand.desc)
|
if self._was_mentioned(cand.desc, [mention]):
|
||||||
if mention_normalized and desc_normalized == mention_normalized:
|
|
||||||
exact_cands.append(cand)
|
|
||||||
elif self._was_mentioned(cand.desc, [mention]):
|
|
||||||
substring_cands.append(cand)
|
substring_cands.append(cand)
|
||||||
# Match exato (chave normalizada) tem precedência absoluta. ``canonical_name``
|
|
||||||
# e o payload seguem sempre o ``desc`` CRU da fatura — a normalização é só
|
|
||||||
# chave de comparação.
|
|
||||||
if exact_cands:
|
|
||||||
return [self._build_item(cand) for cand in exact_cands]
|
|
||||||
guarded = self._apply_prefix_guard(mention, substring_cands, candidates)
|
guarded = self._apply_prefix_guard(mention, substring_cands, candidates)
|
||||||
return [self._build_item(cand) for cand in guarded]
|
return [self._build_item(cand) for cand in guarded]
|
||||||
|
|
||||||
@@ -828,6 +843,45 @@ class InvoiceResolver:
|
|||||||
|
|
||||||
# ----- construção de candidatos e itens --------------------------------
|
# ----- construção de candidatos e itens --------------------------------
|
||||||
|
|
||||||
|
def _iter_identity_candidates(
|
||||||
|
self, invoice_detail: dict[str, Any]
|
||||||
|
) -> Iterable[_Candidate]:
|
||||||
|
"""Itera itens nomeáveis de TODAS as seções da fatura para match exato.
|
||||||
|
|
||||||
|
Diferente de :meth:`_iter_candidates`, este catálogo inclui também seções
|
||||||
|
não acionáveis como ``Plano``/``Planos``. Esses itens são construídos como
|
||||||
|
``out_of_scope`` e servem somente para preservar a identidade explicitamente
|
||||||
|
citada pelo cliente. Eles nunca participam do fuzzy matcher.
|
||||||
|
"""
|
||||||
|
for parent_msisdn, sections in self._iter_msisdn_buckets(invoice_detail):
|
||||||
|
if not isinstance(sections, dict):
|
||||||
|
continue
|
||||||
|
for section, entries in sections.items():
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
continue
|
||||||
|
section_tool, section_type = SECTION_DEFAULTS.get(
|
||||||
|
section, (None, _OUT_OF_SCOPE_TYPE)
|
||||||
|
)
|
||||||
|
for entry in entries:
|
||||||
|
if not isinstance(entry, dict) or self._is_non_service_item(entry):
|
||||||
|
continue
|
||||||
|
desc = str(entry.get("desc") or "").strip()
|
||||||
|
if not desc:
|
||||||
|
continue
|
||||||
|
default_tool, default_type = section_tool, section_type
|
||||||
|
# Se a seção é explicitamente não acionável e a entry não foi
|
||||||
|
# carimbada como tratável pelo parser, force out_of_scope.
|
||||||
|
if section in _NON_SERVICE_SECTIONS and not self._has_treatable_flag(entry):
|
||||||
|
default_tool, default_type = None, _OUT_OF_SCOPE_TYPE
|
||||||
|
yield _Candidate(
|
||||||
|
desc=desc,
|
||||||
|
section=section,
|
||||||
|
default_tool=default_tool,
|
||||||
|
default_type=default_type,
|
||||||
|
parent_msisdn=parent_msisdn,
|
||||||
|
entry=entry,
|
||||||
|
)
|
||||||
|
|
||||||
def _iter_candidates(
|
def _iter_candidates(
|
||||||
self, invoice_detail: dict[str, Any]
|
self, invoice_detail: dict[str, Any]
|
||||||
) -> Iterable[_Candidate]:
|
) -> Iterable[_Candidate]:
|
||||||
|
|||||||
104
docs/LEGACY_INTEGRATION_PARITY_20260822.md
Normal file
104
docs/LEGACY_INTEGRATION_PARITY_20260822.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Paridade de Integração Contas — Mock x Sistemas Reais
|
||||||
|
|
||||||
|
Data: 2026-08-22
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Validar o agente Contas migrado contra o código original, usando o legado como fonte de verdade para contratos HTTP, autenticação, headers, payloads, URLs, timeouts e comportamento de integração. O objetivo é manter o funcionamento com mocks locais sem impedir a execução contra sistemas reais quando `TIM_GATEWAY_MODE`/`TIM_USE_MOCK_GATEWAY` forem configurados para modo real.
|
||||||
|
|
||||||
|
## Correção crítica — identidade do item transacional
|
||||||
|
|
||||||
|
Foi corrigido o caso em que um pedido explícito para `TIM CTRL Redes Sociais 8.0` podia ser reinterpretado pelo matcher fuzzy como `TIM Fashion Mensal`.
|
||||||
|
|
||||||
|
### Causa
|
||||||
|
|
||||||
|
O `InvoiceResolver` eliminava seções não transacionáveis (por exemplo, planos) antes da resolução de identidade e, em seguida, aplicava similaridade somente sobre VAS. Como `TIM Fashion Mensal` ultrapassava o threshold do matcher, o `subject` era substituído antes da execução.
|
||||||
|
|
||||||
|
### Correção
|
||||||
|
|
||||||
|
- A resolução exata de identidade agora acontece antes de qualquer fuzzy matching.
|
||||||
|
- A busca exata considera também itens fora do escopo transacional, como planos.
|
||||||
|
- Um plano encontrado exatamente é classificado como `out_of_scope` para a operação VAS.
|
||||||
|
- O fuzzy matching continua restrito aos candidatos realmente tratáveis.
|
||||||
|
- `resolve_items()` preserva o comportamento anterior onde necessário para compatibilidade; o fluxo operacional usa a proteção de identidade.
|
||||||
|
|
||||||
|
Resultado esperado para o caso:
|
||||||
|
|
||||||
|
`TIM CTRL Redes Sociais 8.0` -> exact match -> `plano` -> `out_of_scope` -> não substituir por outro VAS -> não executar cancelamento.
|
||||||
|
|
||||||
|
## Comparação com o código original
|
||||||
|
|
||||||
|
Foram comparados os comandos do projeto original (`agente_contas_tim/commands`), `factory.py`, `config.py` e o gateway HTTP com o adaptador atual `app/domain/contas/client.py`.
|
||||||
|
|
||||||
|
| Serviço/Integração | Contrato encontrado no original | Situação no migrado após revisão |
|
||||||
|
|---|---|---|
|
||||||
|
| Consulta VAS | GET, URL com `{msisdn}` ou append `/msisdn`, normalização para prefixo 55, clientId | Corrigido: aliases originais, timeout, append e prefixo 55 |
|
||||||
|
| Histórico VAS | GET com `?msisdn=`, clientId/messageId/auth | Corrigido default `clientId=AIAGENTCR` |
|
||||||
|
| Bloqueio VAS | POST, contratos de payload `pmid`/`input`/`vasBlock`, headers extras | Corrigidos aliases de URL/auth/timeout/clientId/operation/payload/encoding |
|
||||||
|
| Cancelamento VAS | DELETE, body com channel/msisdn/appId/cspId/interactionProtocol, OAM/CN/type opcionais | Corrigidos aliases `TIM_CANCELLATION_*` e `TIM_CANCELAMENTO_*` |
|
||||||
|
| Divergência / explicação de fatura | GET `<base>/<msisdn>?channel=AIAGENTCR`, Basic opcional user/password, clientID | Corrigidos aliases, Basic auth e timeout |
|
||||||
|
| CompleteInvoices | POST `{"msisdn": ...}`, `ClientID=AIAGENTCR` | Compatível; timeout respeitado |
|
||||||
|
| Profile bill | Factory original usa configuração de CompleteInvoices | Corrigido para priorizar contrato/config de CompleteInvoices |
|
||||||
|
| Profile full | GET com placeholder ou append `/msisdn`, `ClientID=AIAGENTCR` | Corrigido append e timeout |
|
||||||
|
| Line info | Mesmo padrão de URL do profile full | Corrigido append e timeout |
|
||||||
|
| Contrato | GET `<base>/<msisdn>`, clientId do legado | Corrigido default `AIAGENTCR` |
|
||||||
|
| Protocolo V2 | POST serviceRequest/interaction, headers opcionais OAM/CN/type | Mantido no adaptador atual |
|
||||||
|
| Contestação do cliente | POST, clientId/messageId/X-Agent-Id, user configurável | Corrigido default via `TIM_CUSTOMER_CONTESTATION_USER_ID` |
|
||||||
|
| Atualização de Service Request | POST, channel/serviceRequest, headers de integração | Mantido |
|
||||||
|
| Tracking Activities | POST com customer/protocol/invoice/activity/user | Mantido |
|
||||||
|
| SMS | POST com msisdn/sender/message/URL e receipt opcional | Mantido |
|
||||||
|
| Bill PDF detalhada | POST com invoiceId/customerId e invoiceType `DETALHADA`, retorno PDF | Aliases ampliados |
|
||||||
|
| Secure PDF / invoice recover | GET com invoiceId/msisdn/customerId | Aliases/header ajustados |
|
||||||
|
|
||||||
|
## Configurações presentes no legado sem uso operacional comprovado
|
||||||
|
|
||||||
|
- `status_customer`: configuração encontrada, mas sem comando/runtime consumidor localizado na revisão.
|
||||||
|
- configuração OAuth específica de SMS: declarada no config original, mas sem consumidor runtime localizado.
|
||||||
|
|
||||||
|
Esses itens não foram tratados como requisito ativo sem evidência de uso no código original.
|
||||||
|
|
||||||
|
## Mock x modo real
|
||||||
|
|
||||||
|
O mock continua suportado. Em mock, o cliente retorna fixtures locais para as operações previstas. Em modo real, o mesmo adaptador segue os contratos HTTP reconstruídos a partir do código original.
|
||||||
|
|
||||||
|
A principal diferença de risco é que um mock tende a responder `200/OK` para cenários preparados. Por isso, a validação de identidade deve ocorrer antes do gateway — como agora ocorre — para impedir que um erro de resolução de entidade seja mascarado pelo mock e, principalmente, que chegue a um backend real.
|
||||||
|
|
||||||
|
## Testes executados
|
||||||
|
|
||||||
|
### Regressão + contratos existentes
|
||||||
|
|
||||||
|
- 101 testes passaram no conjunto de contratos, paridade, idempotência e resolução.
|
||||||
|
|
||||||
|
### Novos testes de compatibilidade legado/real
|
||||||
|
|
||||||
|
- 7 testes passaram cobrindo:
|
||||||
|
- prefixo 55 e composição da URL de consulta VAS;
|
||||||
|
- aliases originais e timeout;
|
||||||
|
- aliases de cancelamento;
|
||||||
|
- append de MSISDN em profile full;
|
||||||
|
- Basic auth de divergência via usuário/senha;
|
||||||
|
- client IDs de histórico VAS e contrato;
|
||||||
|
- uso de CompleteInvoices no profile bill.
|
||||||
|
|
||||||
|
### Suite `tests/migration`
|
||||||
|
|
||||||
|
Resultado observado após as mudanças:
|
||||||
|
|
||||||
|
- 660 passed
|
||||||
|
- 4 failed
|
||||||
|
|
||||||
|
As quatro falhas remanescentes são de configuração/contexto de guardrails (`conversation_history` e FRASEOLOGIA) e não estão relacionadas ao `InvoiceResolver` nem aos contratos de integração revisados.
|
||||||
|
|
||||||
|
## Limite desta validação
|
||||||
|
|
||||||
|
A revisão comprova paridade de contrato em nível de código-fonte e testes locais. Ela não é uma certificação de conectividade real porque não foram usados endpoints, credenciais ou rede dos sistemas legados neste ambiente.
|
||||||
|
|
||||||
|
Para homologação real, recomenda-se executar testes de contrato contra um ambiente não produtivo dos serviços TIM, verificando status HTTP, schemas reais, autenticação, timeouts, headers obrigatórios e respostas de erro.
|
||||||
|
|
||||||
|
## Gaps de endurecimento recomendados
|
||||||
|
|
||||||
|
1. Adicionar validação de readiness no startup quando `mock=false`, falhando cedo se endpoint/auth obrigatórios estiverem ausentes.
|
||||||
|
2. Criar testes de contrato contra ambiente de homologação para cada integração ativa.
|
||||||
|
3. Comparar periodicamente fixtures mock com schemas/respostas reais para evitar drift.
|
||||||
|
4. Manter invariantes transacionais: item solicitado, item resolvido e item executado nunca podem divergir silenciosamente.
|
||||||
|
5. Evoluir mascaramento/observabilidade do cliente migrado para o mesmo nível do `HttpGateway` original, sem registrar secrets.
|
||||||
@@ -70,3 +70,25 @@ def test_correct_subject_is_preserved():
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["subject"] == "TIM CTRL Redes Sociais 8.0"
|
assert result["subject"] == "TIM CTRL Redes Sociais 8.0"
|
||||||
assert args.get("_subject_corrected_from") is None
|
assert args.get("_subject_corrected_from") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_preflight_exact_plan_is_out_of_scope_and_never_rebound_to_vas():
|
||||||
|
args = _args(subject="TIM CTRL Redes Sociais 8.0")
|
||||||
|
result = _preflight_subject("cancelar_vas_avulso", args)
|
||||||
|
assert result is not None
|
||||||
|
assert result["status"] == "OUT_OF_SCOPE"
|
||||||
|
assert args["subject"] == "TIM CTRL Redes Sociais 8.0"
|
||||||
|
assert "TIM Fashion" not in str(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoice_resolver_exact_plan_identity_wins_before_similarity_matcher():
|
||||||
|
from app.domain.contas.invoice_resolver import InvoiceResolver
|
||||||
|
from app.domain.contas.item_matcher import SimilarityItemMatcher
|
||||||
|
|
||||||
|
detail = _args(subject="TIM CTRL Redes Sociais 8.0")["billing_analysis"]
|
||||||
|
outcome = InvoiceResolver(matcher=SimilarityItemMatcher()).resolve(
|
||||||
|
["TIM CTRL Redes Sociais 8.0"], detail
|
||||||
|
)
|
||||||
|
assert outcome.resolved == []
|
||||||
|
assert len(outcome.out_of_scope) == 1
|
||||||
|
assert outcome.out_of_scope[0].matches[0].canonical_name == "TIM CTRL Redes Sociais 8.0"
|
||||||
|
|||||||
101
tests/migration/test_real_legacy_integration_compat.py
Normal file
101
tests/migration/test_real_legacy_integration_compat.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from app.domain.contas.client import TimApiClient
|
||||||
|
|
||||||
|
|
||||||
|
def _real(monkeypatch) -> TimApiClient:
|
||||||
|
monkeypatch.setenv("TIM_USE_MOCK_GATEWAY", "false")
|
||||||
|
monkeypatch.setenv("TIM_GATEWAY_MODE", "real")
|
||||||
|
return TimApiClient()
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_vas_legacy_base_url_appends_country_code_and_msisdn(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.setenv("TIM_URL_CONSULTA_VAS", "http://tim/access/v1")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.consultar_vas("11999999999")
|
||||||
|
assert calls[0][0] == ("GET", "http://tim/access/v1/5511999999999")
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_vas_accepts_original_aliases_and_timeout(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.delenv("TIM_URL_CONSULTA_VAS", raising=False)
|
||||||
|
monkeypatch.setenv("TIM_CONSULTA_URL", "http://tim/query")
|
||||||
|
monkeypatch.setenv("TIM_QUERY_AUTH", "Basic old")
|
||||||
|
monkeypatch.setenv("TIM_QUERY_TIMEOUT", "44")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.consultar_vas("5511999999999")
|
||||||
|
_, kw = calls[0]
|
||||||
|
assert kw["headers"]["Authorization"] == "Basic old"
|
||||||
|
assert kw["timeout"] == 44
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancelamento_accepts_original_cancellation_aliases(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.delenv("TIM_CANCELAMENTO_URL", raising=False)
|
||||||
|
monkeypatch.setenv("TIM_CANCELLATION_URL", "http://tim/cancel")
|
||||||
|
monkeypatch.setenv("TIM_CANCELLATION_AUTH", "Basic legacy")
|
||||||
|
monkeypatch.setenv("TIM_CANCELLATION_AUTH_OAM", "OAM")
|
||||||
|
monkeypatch.setenv("TIM_CANCELLATION_CN_FIELD", "CN")
|
||||||
|
monkeypatch.setenv("TIM_CANCELLATION_TYPE_FIELD", "TYPE")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.cancelar_vas("119", {"appId": "1", "cspId": "740"})
|
||||||
|
args, kw = calls[0]
|
||||||
|
assert args[:2] == ("DELETE", "http://tim/cancel")
|
||||||
|
assert kw["headers"]["Authorization"] == "Basic legacy"
|
||||||
|
assert kw["headers"]["AuthorizationOAM"] == "OAM"
|
||||||
|
assert kw["headers"]["Cn_field"] == "CN"
|
||||||
|
assert kw["headers"]["Type_field"] == "TYPE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_full_appends_msisdn_when_original_url_has_no_placeholder(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.setenv("TIM_PROFILE_FULL_URL", "http://tim/profile")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.profile_full("11999999999")
|
||||||
|
assert calls[0][0] == ("GET", "http://tim/profile/11999999999")
|
||||||
|
|
||||||
|
|
||||||
|
def test_divergencia_preserves_original_user_password_basic_auth(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.setenv("TIM_DIVERGENCIA_URL", "http://tim/div")
|
||||||
|
monkeypatch.setenv("TIM_DIVERGENCIA_USER", "user")
|
||||||
|
monkeypatch.setenv("TIM_DIVERGENCIA_PASSWORD", "pass")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.billing_analysis("119")
|
||||||
|
auth = calls[0][1]["headers"]["Authorization"]
|
||||||
|
assert auth == "Basic " + base64.b64encode(b"user:pass").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def test_vas_history_and_contrato_default_client_id_match_original(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.setenv("TIM_VAS_HISTORY_URL", "http://tim/history")
|
||||||
|
monkeypatch.setenv("TIM_CONTRATO_URL", "http://tim/contract")
|
||||||
|
monkeypatch.delenv("TIM_VAS_HISTORY_CLIENT_ID", raising=False)
|
||||||
|
monkeypatch.delenv("TIM_CONTRATO_CLIENT_ID", raising=False)
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.historico_vas("119")
|
||||||
|
c.contrato("119")
|
||||||
|
assert calls[0][1]["headers"]["clientId"] == "AIAGENTCR"
|
||||||
|
assert calls[1][1]["headers"]["clientId"] == "AIAGENTCR"
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_bill_uses_complete_invoices_contract_like_original_factory(monkeypatch):
|
||||||
|
c = _real(monkeypatch)
|
||||||
|
monkeypatch.setenv("TIM_COMPLETE_INVOICES_URL", "http://tim/complete")
|
||||||
|
monkeypatch.setenv("TIM_URL_PERFIL_FATURA", "http://tim/legacy-profile")
|
||||||
|
monkeypatch.setenv("TIM_COMPLETE_INVOICES_CLIENT_ID", "CID")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(c, "request", lambda *a, **kw: calls.append((a, kw)) or {})
|
||||||
|
c.profile_bill("119")
|
||||||
|
args, kw = calls[0]
|
||||||
|
assert args[:2] == ("POST", "http://tim/complete")
|
||||||
|
assert kw["headers"]["ClientID"] == "CID"
|
||||||
Reference in New Issue
Block a user