new feature: External guardrails/judges
This commit is contained in:
@@ -10,6 +10,7 @@ from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from .integrations.secure_pdf_crypto import encrypt_secure_pdf_value
|
||||
from .parsers import parse_tim_bill_pdf
|
||||
@@ -23,6 +24,29 @@ class TimApiError(RuntimeError):
|
||||
self.attempts = list(attempts or [])
|
||||
|
||||
|
||||
|
||||
_INTEGRATION_DEFAULTS_CACHE: dict[str, str] | None = None
|
||||
|
||||
def get_tim_integration_value(*names: str, default: str = "") -> str:
|
||||
"""Env > config/tim_integration_defaults.yaml > default explícito."""
|
||||
global _INTEGRATION_DEFAULTS_CACHE
|
||||
for name in names:
|
||||
value = os.getenv(name)
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
if _INTEGRATION_DEFAULTS_CACHE is None:
|
||||
path = Path(os.getenv("TIM_INTEGRATION_DEFAULTS_PATH", "./config/tim_integration_defaults.yaml"))
|
||||
try:
|
||||
raw = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {}
|
||||
except Exception as exc:
|
||||
raise TimApiError(f"Falha ao ler defaults de integração TIM: {path}: {exc}") from exc
|
||||
_INTEGRATION_DEFAULTS_CACHE = {str(k): str(v) for k, v in (raw or {}).items() if v not in (None, "")}
|
||||
for name in names:
|
||||
value = _INTEGRATION_DEFAULTS_CACHE.get(name)
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
return default
|
||||
|
||||
class TimApiClient:
|
||||
"""Thin TIM integration adapter.
|
||||
|
||||
@@ -31,9 +55,11 @@ class TimApiClient:
|
||||
"""
|
||||
|
||||
FIXTURES = Path(__file__).with_name("fixtures")
|
||||
INTEGRATION_DEFAULTS_PATH = Path(os.getenv("TIM_INTEGRATION_DEFAULTS_PATH", "./config/tim_integration_defaults.yaml"))
|
||||
_integration_defaults_cache: dict[str, str] | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.mock = os.getenv("TIM_USE_MOCK_GATEWAY", "true").lower() in {"1", "true", "yes", "on"} or os.getenv("TIM_GATEWAY_MODE", "mock").lower() == "mock"
|
||||
self.mock = os.getenv("TIM_USE_MOCK_GATEWAY", "false").lower() in {"1", "true", "yes", "on"} or os.getenv("TIM_GATEWAY_MODE", "real").lower() == "mock"
|
||||
self.timeout = int(os.getenv("TIM_GATEWAY_DEFAULT_TIMEOUT", "30"))
|
||||
self.max_retries = int(os.getenv("TIM_GATEWAY_RETRY_MAX_RETRIES", "3"))
|
||||
self.backoff = float(os.getenv("TIM_GATEWAY_RETRY_BACKOFF_FACTOR", "0.5"))
|
||||
@@ -42,14 +68,14 @@ class TimApiClient:
|
||||
path = self.FIXTURES / f"{name}.json"
|
||||
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 _integration_defaults(cls) -> dict[str, str]:
|
||||
get_tim_integration_value("__warmup__")
|
||||
return dict(_INTEGRATION_DEFAULTS_CACHE or {})
|
||||
|
||||
@classmethod
|
||||
def _env_first(cls, *names: str, default: str = "") -> str:
|
||||
return get_tim_integration_value(*names, default=default)
|
||||
|
||||
@classmethod
|
||||
def _auth_value(
|
||||
@@ -155,7 +181,7 @@ class TimApiClient:
|
||||
"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")},
|
||||
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", )},
|
||||
),
|
||||
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", default="30")),
|
||||
)
|
||||
@@ -171,9 +197,9 @@ class TimApiClient:
|
||||
password_names=("TIM_DIVERGENCIA_PASSWORD", "TIM_DIVERGENCIA_PASS", "TIM_DIVERGENCE_PASSWORD", "TIM_DIVERGENCE_PASS"),
|
||||
)
|
||||
headers = self._headers(client_id=None, auth=auth, extra={
|
||||
"clientID": self._env_first("TIM_DIVERGENCIA_CLIENT_ID", default="AIAGENTCR"),
|
||||
"clientID": self._env_first("TIM_DIVERGENCIA_CLIENT_ID", ),
|
||||
})
|
||||
params = {"channel": context.get("channel") or "AIAGENTCR"}
|
||||
params = {"channel": context.get("channel") or self._env_first("TIM_DIVERGENCIA_CHANNEL")}
|
||||
attempts: list[dict[str, Any]] = []
|
||||
try:
|
||||
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)
|
||||
@@ -192,7 +218,7 @@ class TimApiClient:
|
||||
"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")},
|
||||
extra={"clientId": self._env_first("TIM_CONSULTA_CLIENT_ID", )},
|
||||
),
|
||||
timeout=int(self._env_first("TIM_QUERY_TIMEOUT", "TIM_CONSULTA_TIMEOUT", default="30")),
|
||||
)
|
||||
@@ -207,7 +233,7 @@ class TimApiClient:
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_VAS_HISTORY_AUTH", ""),
|
||||
extra={
|
||||
"clientId": self._env_first("TIM_VAS_HISTORY_CLIENT_ID", default="AIAGENTCR"),
|
||||
"clientId": self._env_first("TIM_VAS_HISTORY_CLIENT_ID", ),
|
||||
"messageId": os.getenv("TIM_VAS_HISTORY_MESSAGE_ID", "") or str(uuid.uuid4()),
|
||||
},
|
||||
)
|
||||
@@ -220,7 +246,7 @@ class TimApiClient:
|
||||
digits = "".join(ch for ch in str(msisdn) if ch.isdigit())
|
||||
normalized = digits[2:] if len(digits) == 13 else digits
|
||||
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 self._env_first("TIM_DEFAULT_CSP_ID"))
|
||||
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}
|
||||
payloads = [
|
||||
@@ -237,7 +263,7 @@ class TimApiClient:
|
||||
auth=self._env_first("TIM_BLOCK_AUTH", "TIM_BLOQUEIO_AUTH"),
|
||||
extra={
|
||||
"Accept-Encoding": self._env_first("TIM_BLOCK_ACCEPT_ENCODING", "TIM_BLOQUEIO_ACCEPT_ENCODING", default="gzip,deflate"),
|
||||
"clientId": self._env_first("TIM_BLOCK_CLIENT_ID", "TIM_BLOQUEIO_CLIENT_ID", default="AIAGENTCR"),
|
||||
"clientId": self._env_first("TIM_BLOCK_CLIENT_ID", "TIM_BLOQUEIO_CLIENT_ID", ),
|
||||
"messageId": str(uuid.uuid4()),
|
||||
},
|
||||
)
|
||||
@@ -256,17 +282,17 @@ class TimApiClient:
|
||||
return self.fixture("cancel_vas")
|
||||
url = self._env_first("TIM_CANCELLATION_URL", "TIM_CANCELAMENTO_URL")
|
||||
payload = {
|
||||
"channel": "AIAGENTCR",
|
||||
"channel": self._env_first("TIM_CANCELAMENTO_CHANNEL"),
|
||||
"msisdn": msisdn,
|
||||
"appId": str(service.get("appId") or service.get("app_id") or ""),
|
||||
"cspId": str(service.get("cspId") or service.get("csp_id") or os.getenv("TIM_DEFAULT_CSP_ID", "740")),
|
||||
"cspId": str(service.get("cspId") or service.get("csp_id") or self._env_first("TIM_DEFAULT_CSP_ID")),
|
||||
"interactionProtocol": protocol,
|
||||
}
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=self._env_first("TIM_CANCELLATION_AUTH", "TIM_CANCELAMENTO_AUTH"),
|
||||
extra={
|
||||
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR")),
|
||||
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", self._env_first("TIM_DEFAULT_CLIENT_ID")),
|
||||
"messageId": str(uuid.uuid4()),
|
||||
"AuthorizationOAM": self._env_first("TIM_CANCELLATION_AUTH_OAM", "TIM_CANCELAMENTO_AUTH_OAM", "TIM_CANCELAMENTO_AUTHORIZATION_OAM"),
|
||||
"Cn_field": self._env_first("TIM_CANCELLATION_CN_FIELD", "TIM_CANCELAMENTO_CN_FIELD"),
|
||||
@@ -284,7 +310,7 @@ class TimApiClient:
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_CONTRATO_AUTH", ""),
|
||||
extra={"clientId": self._env_first("TIM_CONTRATO_CLIENT_ID", default="AIAGENTCR")},
|
||||
extra={"clientId": self._env_first("TIM_CONTRATO_CLIENT_ID", )},
|
||||
)
|
||||
return self.request("GET", f"{base}/{quote(msisdn)}", headers=headers, timeout=int(os.getenv("TIM_CONTRATO_TIMEOUT", "30")))
|
||||
|
||||
@@ -296,7 +322,7 @@ class TimApiClient:
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
||||
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
||||
extra={"ClientID": self._env_first("TIM_PROFILE_FULL_CLIENT_ID")},
|
||||
)
|
||||
return self.request("GET", url, headers=headers, timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")))
|
||||
|
||||
@@ -321,7 +347,7 @@ class TimApiClient:
|
||||
)
|
||||
body = {
|
||||
"socialSecNo": str(data.get("socialSecNo") or data.get("social_sec_no") or ""),
|
||||
"channel": "AIAGENTCR",
|
||||
"channel": self._env_first("TIM_PROTOCOL_CHANNEL"),
|
||||
"customerId": str(data.get("customerId") or data.get("customer_id") or ""),
|
||||
"assetId": str(data.get("assetId") or data.get("asset_id") or msisdn),
|
||||
"customerName": str(data.get("customerName") or data.get("customer_name") or ""),
|
||||
@@ -340,7 +366,7 @@ class TimApiClient:
|
||||
"interaction": {
|
||||
"protocol": str(data.get("interactionProtocol") or data.get("interaction_protocol") or ""),
|
||||
"flagSms": True,
|
||||
"source": "AIAGENTCR",
|
||||
"source": self._env_first("TIM_DEFAULT_SOURCE"),
|
||||
"callId": str(data.get("interactionCallId") or data.get("interaction_call_id") or ""),
|
||||
"crmSource": "Siebel Pós",
|
||||
"reasonId": str(data.get("interactionReasonId") or data.get("interaction_reason_id") or ""),
|
||||
@@ -355,7 +381,7 @@ class TimApiClient:
|
||||
client_id=None,
|
||||
auth=str(data.get("Authorization") or os.getenv("TIM_PROTOCOL_AUTH", "")),
|
||||
extra={
|
||||
"clientId": str(data.get("clientId") or data.get("client_id") or os.getenv("TIM_PROTOCOL_CLIENT_ID", "BFFDIGITAL")),
|
||||
"clientId": str(data.get("clientId") or data.get("client_id") or self._env_first("TIM_PROTOCOL_CLIENT_ID")),
|
||||
"messageId": message_id,
|
||||
"Authorizationoam": str(data.get("authorization_oam") or os.getenv("TIM_PROTOCOL_AUTHORIZATION_OAM", "")),
|
||||
"Cn_field": str(data.get("cn_field") or os.getenv("TIM_PROTOCOL_CN_FIELD", "")),
|
||||
@@ -378,7 +404,7 @@ class TimApiClient:
|
||||
if self.mock:
|
||||
return self.fixture("contestacao_tool")
|
||||
data = dict(payload)
|
||||
data.setdefault("userId", self._env_first("TIM_CUSTOMER_CONTESTATION_USER_ID", default="AIAGENTCR"))
|
||||
data.setdefault("userId", self._env_first("TIM_CUSTOMER_CONTESTATION_USER_ID", ))
|
||||
data.setdefault("customerIdCurrent", data.get("customerId") or "")
|
||||
data.setdefault("customerType", "2")
|
||||
data.setdefault("customerStatus", "1")
|
||||
@@ -401,9 +427,9 @@ class TimApiClient:
|
||||
data.setdefault("refundOption", "0")
|
||||
data.setdefault("doubleRefund", False)
|
||||
data.setdefault("manualContaCertaIndicator", False)
|
||||
client_id = str(data.pop("clientId", "") or os.getenv("TIM_CUSTOMER_CONTESTATION_CLIENT_ID", "AIAGENTCR"))
|
||||
client_id = str(data.pop("clientId", "") or self._env_first("TIM_CUSTOMER_CONTESTATION_CLIENT_ID"))
|
||||
message_id = str(data.pop("messageId", "") or uuid.uuid4())
|
||||
user_id = str(data.get("userId") or "AIAGENTCR").upper()
|
||||
user_id = str(data.get("userId") or self._env_first("TIM_DEFAULT_CLIENT_ID")).upper()
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_CUSTOMER_CONTESTATION_AUTH", ""),
|
||||
@@ -428,7 +454,7 @@ class TimApiClient:
|
||||
}
|
||||
for key in ("reason1", "reason2", "reason3", "auditChecks", "attachments"):
|
||||
if data.get(key): service_request[key] = data[key]
|
||||
body = {"channel": data.get("channel") or "AIAGENTCR", "serviceRequest": service_request}
|
||||
body = {"channel": data.get("channel") or self._env_first("TIM_DEFAULT_CLIENT_ID"), "serviceRequest": service_request}
|
||||
for key in ("msisdn", "customers", "date"):
|
||||
if data.get(key): body[key] = data[key]
|
||||
message_id = str(data.get("messageId") or data.get("ura_call_id") or data.get("session_id") or uuid.uuid4())
|
||||
@@ -436,7 +462,7 @@ class TimApiClient:
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_SERVICE_REQUEST_STATUS_AUTH", ""),
|
||||
extra={
|
||||
"clientId": str(data.get("clientId") or os.getenv("TIM_SERVICE_REQUEST_STATUS_CLIENT_ID", "AIAGENTCR")),
|
||||
"clientId": str(data.get("clientId") or self._env_first("TIM_SERVICE_REQUEST_STATUS_CLIENT_ID")),
|
||||
"messageId": message_id,
|
||||
"Authorizationoam": data.get("authorization_oam") or os.getenv("TIM_SERVICE_REQUEST_STATUS_AUTHORIZATION_OAM", ""),
|
||||
"Cn_field": data.get("cn_field") or os.getenv("TIM_SERVICE_REQUEST_STATUS_CN_FIELD", ""),
|
||||
@@ -463,7 +489,7 @@ class TimApiClient:
|
||||
"totalAmount": data.get("invoiceTotalAmount") or "",
|
||||
}
|
||||
body = {
|
||||
"channel": data.get("channel") or os.getenv("TIM_TRACKING_ACTIVITIES_CHANNEL", "AIAGENTCR"),
|
||||
"channel": data.get("channel") or self._env_first("TIM_TRACKING_ACTIVITIES_CHANNEL"),
|
||||
"customer": {"socialSecNo": data.get("socialSecNo") or "", "msisdn": data.get("msisdn") or ""},
|
||||
"protocolNumber": data.get("protocolNumber") or "",
|
||||
"invoice": invoice,
|
||||
@@ -472,12 +498,12 @@ class TimApiClient:
|
||||
"status": data.get("activityStatus") or "",
|
||||
"id": data.get("activityId") or "",
|
||||
},
|
||||
"user": {"login": data.get("userLogin") or os.getenv("TIM_TRACKING_ACTIVITIES_USER_LOGIN", "SIEBELPOS_INBOUND")},
|
||||
"user": {"login": data.get("userLogin") or self._env_first("TIM_TRACKING_ACTIVITIES_USER_LOGIN")},
|
||||
}
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_TRACKING_ACTIVITIES_AUTH", ""),
|
||||
extra={"clientId": data.get("clientId") or os.getenv("TIM_TRACKING_ACTIVITIES_CLIENT_ID", "AIAGENTCR")},
|
||||
extra={"clientId": data.get("clientId") or self._env_first("TIM_TRACKING_ACTIVITIES_CLIENT_ID")},
|
||||
)
|
||||
return self.request("POST", os.getenv("TIM_TRACKING_ACTIVITIES_URL", ""), payload=body, headers=headers, timeout=int(os.getenv("TIM_TRACKING_ACTIVITIES_TIMEOUT", "30")))
|
||||
|
||||
@@ -487,7 +513,7 @@ class TimApiClient:
|
||||
payload: dict[str, Any] = {
|
||||
"msisdn": msisdn,
|
||||
"senderAddress": context.get("sender_address") or os.getenv("TIM_SMS_SENDER_ADDRESS", "324"),
|
||||
"senderName": context.get("sender_name") or os.getenv("TIM_SMS_SENDER_NAME", "TIM Brasil"),
|
||||
"senderName": context.get("sender_name") or self._env_first("TIM_SMS_SENDER_NAME"),
|
||||
"message": message,
|
||||
"longURL": context.get("long_url") or message,
|
||||
}
|
||||
@@ -496,7 +522,7 @@ class TimApiClient:
|
||||
headers = self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_SMS_AUTH", ""),
|
||||
extra={"clientId": os.getenv("TIM_SMS_CLIENT_ID", "AIAGENTCR")},
|
||||
extra={"clientId": self._env_first("TIM_SMS_CLIENT_ID")},
|
||||
)
|
||||
attempts: list[dict[str, Any]] = []
|
||||
result = self.request("POST", os.getenv("TIM_SMS_URL", ""), payload=payload, headers=headers, timeout=int(os.getenv("TIM_SMS_TIMEOUT", "30")), attempt_log=attempts)
|
||||
@@ -514,7 +540,7 @@ class TimApiClient:
|
||||
headers=self._headers(
|
||||
client_id=None,
|
||||
auth=self._env_first("TIM_COMPLETE_INVOICES_AUTH", "TIM_PROFILE_BILL_AUTH"),
|
||||
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", "TIM_PROFILE_BILL_CLIENT_ID", default="AIAGENTCR")},
|
||||
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", "TIM_PROFILE_BILL_CLIENT_ID", )},
|
||||
),
|
||||
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", "TIM_PROFILE_BILL_TIMEOUT", default="30")),
|
||||
)
|
||||
@@ -531,7 +557,7 @@ class TimApiClient:
|
||||
headers=self._headers(
|
||||
client_id=None,
|
||||
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
|
||||
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
|
||||
extra={"ClientID": self._env_first("TIM_PROFILE_FULL_CLIENT_ID")},
|
||||
),
|
||||
timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")),
|
||||
)
|
||||
@@ -581,7 +607,7 @@ class TimApiClient:
|
||||
client_id=None,
|
||||
auth=self._env_first("TIM_BILL_PDF_AUTH", "TIM_SECURE_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
|
||||
extra={
|
||||
"clientId": self._env_first("TIM_BILL_PDF_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR"),
|
||||
"clientId": self._env_first("TIM_BILL_PDF_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", ),
|
||||
"Accept": "application/pdf",
|
||||
},
|
||||
)
|
||||
@@ -621,7 +647,7 @@ class TimApiClient:
|
||||
headers=self._headers(
|
||||
client_id=None,
|
||||
auth=self._env_first("TIM_SECURE_PDF_AUTH", "TIM_BILL_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
|
||||
extra={"clientid": self._env_first("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR")},
|
||||
extra={"clientid": self._env_first("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", )},
|
||||
),
|
||||
timeout=int(os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30")),
|
||||
)
|
||||
|
||||
576
app/domain/contas/contestation_validation.py
Normal file
576
app/domain/contas/contestation_validation.py
Normal file
@@ -0,0 +1,576 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import nullcontext
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import unicodedata as ud
|
||||
from typing import Any
|
||||
|
||||
_CENT = Decimal("0.01")
|
||||
_GUARDRAIL_ACTION = "abrir_contestacao_cliente"
|
||||
_GUARDRAIL_CODE = "CVAL"
|
||||
_STRATEGIC_SERVICE_ALIASES = (
|
||||
"apple music",
|
||||
"deezer",
|
||||
"disney",
|
||||
"fuze",
|
||||
"forge",
|
||||
"hbo",
|
||||
"looke",
|
||||
"netflix",
|
||||
"paramount",
|
||||
"paramount+",
|
||||
"paramount plus",
|
||||
"tim cloud gaming",
|
||||
"youtube",
|
||||
"youtube premium",
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _money(value: Decimal) -> Decimal:
|
||||
return value.quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _parse_amount(value: str) -> Decimal | None:
|
||||
if not value:
|
||||
return None
|
||||
cleaned = (
|
||||
str(value)
|
||||
.replace("R$", "")
|
||||
.replace(" ", "")
|
||||
.replace(".", "")
|
||||
.replace(",", ".")
|
||||
)
|
||||
try:
|
||||
return Decimal(cleaned)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _decimal_from_any(value: Any) -> Decimal | None:
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, Decimal):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return Decimal(str(value))
|
||||
return _parse_amount(str(value or ""))
|
||||
|
||||
|
||||
def _first_decimal_from_mapping(data: dict[str, Any], *keys: str) -> Decimal | None:
|
||||
for key in keys:
|
||||
if key not in data:
|
||||
continue
|
||||
value = _decimal_from_any(data.get(key))
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_number_text(value: Any, *, default: str = "0") -> str:
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return default
|
||||
cleaned = text.replace("R$", "").replace(" ", "")
|
||||
if "," in cleaned:
|
||||
cleaned = cleaned.replace(".", "").replace(",", ".")
|
||||
try:
|
||||
normalized = format(Decimal(cleaned), "f")
|
||||
except Exception:
|
||||
return default
|
||||
if "." in normalized:
|
||||
normalized = normalized.rstrip("0").rstrip(".")
|
||||
return normalized or default
|
||||
|
||||
|
||||
def _normalize_match_text(value: Any) -> str:
|
||||
text = re.sub(r"\s*\([^)]*\)", "", str(value or "")).strip()
|
||||
text = ud.normalize("NFKD", text)
|
||||
text = "".join(ch for ch in text if not ud.combining(ch))
|
||||
text = text.casefold()
|
||||
text = re.sub(r"[^a-z0-9]+", " ", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def _is_same_plan_name(left: Any, right: Any) -> bool:
|
||||
left_key = _normalize_match_text(left)
|
||||
right_key = _normalize_match_text(right)
|
||||
if not left_key or not right_key:
|
||||
return False
|
||||
return left_key == right_key or left_key in right_key or right_key in left_key
|
||||
|
||||
|
||||
def _normalize_service_name_for_match(value: Any) -> str:
|
||||
normalized = ud.normalize("NFKD", str(value or "").lower())
|
||||
without_accents = "".join(ch for ch in normalized if not ud.combining(ch))
|
||||
return re.sub(r"[^a-z0-9]+", "", without_accents)
|
||||
|
||||
|
||||
def _is_strategic_partner_service(value: Any) -> bool:
|
||||
normalized = _normalize_service_name_for_match(value)
|
||||
if not normalized:
|
||||
return False
|
||||
for alias in _STRATEGIC_SERVICE_ALIASES:
|
||||
normalized_alias = _normalize_service_name_for_match(alias)
|
||||
if normalized_alias and normalized_alias in normalized:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_vas_section_name(section_name: str) -> bool:
|
||||
normalized = _normalize_match_text(section_name)
|
||||
return (
|
||||
"vas" in normalized
|
||||
or "valor adicionado" in normalized
|
||||
or "servicos de valor adicionado" in normalized
|
||||
or "servicos valor adicionado" in normalized
|
||||
or "sva detalhe total" in normalized
|
||||
or "servicos contratados de parceiros" in normalized
|
||||
or "servico contratado de parceiro" in normalized
|
||||
)
|
||||
|
||||
|
||||
def _extract_invoice_total_geral(payload: Any) -> Decimal | None:
|
||||
if isinstance(payload, dict):
|
||||
desc = _normalize_match_text(payload.get("desc", ""))
|
||||
if desc == "total geral":
|
||||
total = _decimal_from_any(
|
||||
payload.get("value")
|
||||
if "value" in payload
|
||||
else payload.get("valor")
|
||||
)
|
||||
if total is not None:
|
||||
return total
|
||||
for value in payload.values():
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
result = _extract_invoice_total_geral(value)
|
||||
if result is not None:
|
||||
return result
|
||||
elif isinstance(payload, (list, tuple)):
|
||||
for entry in payload:
|
||||
if isinstance(entry, (dict, list, tuple)):
|
||||
result = _extract_invoice_total_geral(entry)
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def _extract_contestation_invoice_items(
|
||||
payload: Any,
|
||||
*,
|
||||
section_name: str = "",
|
||||
) -> list[dict[str, Any]]:
|
||||
found: list[dict[str, Any]] = []
|
||||
if isinstance(payload, dict):
|
||||
candidate_name = str(
|
||||
payload.get("desc")
|
||||
or payload.get("name")
|
||||
or payload.get("service_name")
|
||||
or payload.get("item_name")
|
||||
or payload.get("itemName")
|
||||
or payload.get("servico")
|
||||
or ""
|
||||
).strip()
|
||||
candidate_amount = _first_decimal_from_mapping(
|
||||
payload,
|
||||
"valor_final",
|
||||
"valor",
|
||||
"price",
|
||||
"amount",
|
||||
"value",
|
||||
"valor_bruto",
|
||||
"claimedAmount",
|
||||
"validatedAmount",
|
||||
)
|
||||
if candidate_name and candidate_amount is not None and candidate_amount > 0:
|
||||
payload_type = str(payload.get("type") or payload.get("tipo") or "").strip()
|
||||
payload_desc = str(payload.get("desc") or "").strip()
|
||||
classe = str(payload.get("classe", "")).strip().lower()
|
||||
is_vas = (
|
||||
_is_vas_section_name(section_name)
|
||||
or _is_vas_section_name(payload_type)
|
||||
or classe in {"avulso", "estrategico"}
|
||||
)
|
||||
found.append(
|
||||
{
|
||||
"name": candidate_name,
|
||||
"amount": _money(candidate_amount),
|
||||
"is_vas": is_vas,
|
||||
"section": section_name,
|
||||
"source_type": payload_type,
|
||||
"source_desc": payload_desc,
|
||||
"classe": classe,
|
||||
"estrategico": bool(payload.get("estrategico")),
|
||||
"verb": str(payload.get("verb", "")).strip().lower(),
|
||||
}
|
||||
)
|
||||
for key, value in payload.items():
|
||||
next_section = section_name
|
||||
if isinstance(key, str) and _is_vas_section_name(key):
|
||||
next_section = key
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
found.extend(
|
||||
_extract_contestation_invoice_items(
|
||||
value,
|
||||
section_name=next_section,
|
||||
)
|
||||
)
|
||||
return found
|
||||
if isinstance(payload, (list, tuple)):
|
||||
for item in payload:
|
||||
if isinstance(item, (dict, list, tuple)):
|
||||
found.extend(
|
||||
_extract_contestation_invoice_items(
|
||||
item,
|
||||
section_name=section_name,
|
||||
)
|
||||
)
|
||||
return found
|
||||
|
||||
|
||||
def _has_langfuse_credentials() -> bool:
|
||||
return bool(
|
||||
os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
|
||||
and os.getenv("LANGFUSE_SECRET_KEY", "").strip()
|
||||
)
|
||||
|
||||
|
||||
def _start_guardrail_observation(
|
||||
*,
|
||||
name: str,
|
||||
input: dict[str, Any] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
if not _has_langfuse_credentials():
|
||||
return nullcontext(None)
|
||||
try:
|
||||
from langfuse import get_client
|
||||
|
||||
return get_client().start_as_current_observation(
|
||||
name=name,
|
||||
as_type="span",
|
||||
input=input,
|
||||
metadata=metadata,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"langfuse.contestation_guardrail_start_failed name=%s",
|
||||
name,
|
||||
exc_info=True,
|
||||
)
|
||||
return nullcontext(None)
|
||||
|
||||
|
||||
def _summarize_requested_items(items: list[dict[str, Any]]) -> list[dict[str, str]]:
|
||||
summary: list[dict[str, str]] = []
|
||||
for item in items:
|
||||
summary.append(
|
||||
{
|
||||
"item_name": str(item.get("item_name", "") or "").strip(),
|
||||
"claimed_amount": _normalize_number_text(
|
||||
item.get("claimed_amount", "0")
|
||||
),
|
||||
"validated_amount": _normalize_number_text(
|
||||
item.get("validated_amount", "0")
|
||||
),
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def _validation_reason(validation_log: list[dict[str, Any]]) -> str:
|
||||
for entry in validation_log:
|
||||
reason = entry.get("erro")
|
||||
if reason:
|
||||
return str(reason).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _emit_contestation_validation_block_span(
|
||||
*,
|
||||
items: list[dict[str, Any]],
|
||||
candidates: list[dict[str, Any]],
|
||||
validation_log: list[dict[str, Any]],
|
||||
validation_error: str,
|
||||
) -> None:
|
||||
reason = _validation_reason(validation_log)
|
||||
approved_count = sum(
|
||||
1 for entry in validation_log if entry.get("status") == "aprovado"
|
||||
)
|
||||
rejected_count = sum(
|
||||
1 for entry in validation_log if entry.get("status") == "reprovado"
|
||||
)
|
||||
try:
|
||||
with _start_guardrail_observation(
|
||||
name=f"guardrail.{_GUARDRAIL_CODE}.blocked",
|
||||
input={
|
||||
"items_count": len(items),
|
||||
"items": _summarize_requested_items(items),
|
||||
"invoice_candidates_count": len(candidates),
|
||||
},
|
||||
metadata={
|
||||
"mechanism": "guardrail_action_validation",
|
||||
"code": _GUARDRAIL_CODE,
|
||||
"action": _GUARDRAIL_ACTION,
|
||||
"reason": reason,
|
||||
},
|
||||
) as obs:
|
||||
if obs is None:
|
||||
return
|
||||
obs.update(
|
||||
level="WARNING",
|
||||
output={
|
||||
"blocked": True,
|
||||
"error": validation_error,
|
||||
"items_validated_count": len(validation_log),
|
||||
"items_approved_count": approved_count,
|
||||
"items_rejected_count": rejected_count,
|
||||
"validation_log": validation_log,
|
||||
"code": _GUARDRAIL_CODE,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"langfuse.contestation_guardrail_update_failed code=%s",
|
||||
_GUARDRAIL_CODE,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
|
||||
def validate_contestation_items(
|
||||
items: list[dict[str, Any]],
|
||||
invoice_payload: dict[str, Any],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], str | None]:
|
||||
candidates = _extract_contestation_invoice_items(invoice_payload)
|
||||
validation_log: list[dict[str, Any]] = []
|
||||
|
||||
with _start_guardrail_observation(
|
||||
name=f"guardrail.{_GUARDRAIL_CODE}.evaluated",
|
||||
input={
|
||||
"items_count": len(items),
|
||||
"items": _summarize_requested_items(items),
|
||||
"invoice_candidates_count": len(candidates),
|
||||
},
|
||||
metadata={
|
||||
"mechanism": "guardrail_action_validation",
|
||||
"code": _GUARDRAIL_CODE,
|
||||
"action": _GUARDRAIL_ACTION,
|
||||
},
|
||||
) as obs:
|
||||
|
||||
def _safe_update(**kwargs: Any) -> None:
|
||||
if obs is None:
|
||||
return
|
||||
try:
|
||||
obs.update(**kwargs)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"langfuse.contestation_guardrail_update_failed code=%s",
|
||||
_GUARDRAIL_CODE,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
first_error: str | None = None
|
||||
|
||||
def _record_failure(
|
||||
item_log: dict[str, Any],
|
||||
erro: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
nonlocal first_error
|
||||
item_log["status"] = "reprovado"
|
||||
item_log["erro"] = erro
|
||||
validation_log.append(item_log)
|
||||
if first_error is None:
|
||||
first_error = message
|
||||
|
||||
for item in items:
|
||||
claimed = Decimal(_normalize_number_text(item.get("claimed_amount", "0")))
|
||||
validated = Decimal(
|
||||
_normalize_number_text(item.get("validated_amount", "0"))
|
||||
)
|
||||
item_name = str(item.get("item_name", "")).strip()
|
||||
if not item_name:
|
||||
continue
|
||||
item_log: dict[str, Any] = {
|
||||
"item_name": item_name,
|
||||
"item_na_fatura": False,
|
||||
"item_confirmado": False,
|
||||
"secao_vas": False,
|
||||
"valor_item_fatura": "",
|
||||
"valor_ajuste_solicitado": _normalize_number_text(
|
||||
format(validated, "f")
|
||||
),
|
||||
"valor_ajuste_valido": False,
|
||||
"vas_estrategico": False,
|
||||
"status": "em_validacao",
|
||||
}
|
||||
matching_candidates = [
|
||||
candidate
|
||||
for candidate in candidates
|
||||
if _is_same_plan_name(candidate.get("name", ""), item_name)
|
||||
]
|
||||
# A mesma cobrança pode aparecer em múltiplas visões da fatura.
|
||||
# Prefira a evidência que traz classificação explícita de VAS em vez
|
||||
# de aceitar a primeira ocorrência genérica e concluir incorretamente
|
||||
# que o item está fora da seção VAS.
|
||||
matching_candidates.sort(
|
||||
key=lambda candidate: (
|
||||
0 if (
|
||||
str(candidate.get("classe", "")).strip().lower() in {"avulso", "estrategico"}
|
||||
or bool(candidate.get("is_vas"))
|
||||
) else 1,
|
||||
0 if _normalize_match_text(candidate.get("name", "")) == _normalize_match_text(item_name) else 1,
|
||||
)
|
||||
)
|
||||
matched_candidate = matching_candidates[0] if matching_candidates else None
|
||||
if matched_candidate is None:
|
||||
_record_failure(
|
||||
item_log,
|
||||
"item_nao_encontrado_na_fatura",
|
||||
f"Item '{item_name}' nao encontrado no json da fatura.",
|
||||
)
|
||||
continue
|
||||
item_log["item_na_fatura"] = True
|
||||
item_log["item_confirmado"] = True
|
||||
item_log["item_fatura_resolvido"] = str(matched_candidate.get("name", "") or "")
|
||||
item_log["secao_fatura"] = str(matched_candidate.get("section", "") or "")
|
||||
item_log["tipo_fatura"] = str(matched_candidate.get("source_type", "") or "")
|
||||
|
||||
classe = str(matched_candidate.get("classe", "")).strip().lower()
|
||||
is_strategic = (
|
||||
classe == "estrategico"
|
||||
or bool(matched_candidate.get("estrategico"))
|
||||
or _is_strategic_partner_service(item_name)
|
||||
)
|
||||
is_vas_avulso = classe == "avulso" or (
|
||||
not classe
|
||||
and not is_strategic
|
||||
and bool(matched_candidate.get("is_vas"))
|
||||
)
|
||||
if not (is_vas_avulso or is_strategic):
|
||||
_record_failure(
|
||||
item_log,
|
||||
"item_fora_secao_vas",
|
||||
f"Item '{item_name}' nao e do tipo VAS no json da fatura.",
|
||||
)
|
||||
continue
|
||||
item_log["secao_vas"] = True
|
||||
|
||||
item_amount = matched_candidate.get("amount")
|
||||
if not isinstance(item_amount, Decimal) or item_amount <= 0:
|
||||
_record_failure(
|
||||
item_log,
|
||||
"valor_item_invalido_na_fatura",
|
||||
f"Nao foi possivel validar o valor do item '{item_name}' na fatura.",
|
||||
)
|
||||
continue
|
||||
item_log["valor_item_fatura"] = _normalize_number_text(
|
||||
format(item_amount, "f")
|
||||
)
|
||||
|
||||
if is_strategic:
|
||||
item_log["vas_estrategico"] = True
|
||||
_record_failure(
|
||||
item_log,
|
||||
"vas_estrategico_nao_permitido",
|
||||
f"Item '{item_name}' identificado como VAS estrategico e nao pode ser ajustado.",
|
||||
)
|
||||
continue
|
||||
|
||||
if claimed <= 0:
|
||||
claimed = item_amount
|
||||
if validated <= 0:
|
||||
validated = claimed
|
||||
if validated > item_amount:
|
||||
_record_failure(
|
||||
item_log,
|
||||
"valor_ajuste_maior_que_item",
|
||||
f"Valor de ajuste do item '{item_name}' excede o valor cobrado na fatura.",
|
||||
)
|
||||
continue
|
||||
item_log["valor_ajuste_solicitado"] = _normalize_number_text(
|
||||
format(validated, "f")
|
||||
)
|
||||
item_log["valor_ajuste_valido"] = True
|
||||
item_log["status"] = "aprovado"
|
||||
validation_log.append(item_log)
|
||||
item["claimed_amount"] = _normalize_number_text(format(claimed, "f"))
|
||||
item["validated_amount"] = _normalize_number_text(format(validated, "f"))
|
||||
|
||||
invoice_total = _extract_invoice_total_geral(invoice_payload)
|
||||
if invoice_total is not None and invoice_total > 0:
|
||||
total_ajustes = sum(
|
||||
(
|
||||
Decimal(
|
||||
_normalize_number_text(entry.get("valor_ajuste_solicitado", "0"))
|
||||
)
|
||||
for entry in validation_log
|
||||
if entry.get("status") == "aprovado"
|
||||
),
|
||||
Decimal("0"),
|
||||
)
|
||||
if total_ajustes > invoice_total:
|
||||
total_log: dict[str, Any] = {
|
||||
"item_name": "<total_ajustes>",
|
||||
"status": "reprovado",
|
||||
"erro": "total_ajustes_excede_fatura",
|
||||
"valor_total_ajustes": _normalize_number_text(
|
||||
format(_money(total_ajustes), "f")
|
||||
),
|
||||
"valor_total_fatura": _normalize_number_text(
|
||||
format(_money(invoice_total), "f")
|
||||
),
|
||||
}
|
||||
validation_log.append(total_log)
|
||||
if first_error is None:
|
||||
first_error = (
|
||||
"Valor total de ajustes ("
|
||||
f"{total_log['valor_total_ajustes']}) excede o "
|
||||
f"valor total da fatura ({total_log['valor_total_fatura']})."
|
||||
)
|
||||
|
||||
approved_count = sum(
|
||||
1 for entry in validation_log if entry.get("status") == "aprovado"
|
||||
)
|
||||
rejected_count = sum(
|
||||
1 for entry in validation_log if entry.get("status") == "reprovado"
|
||||
)
|
||||
|
||||
if first_error is not None:
|
||||
_emit_contestation_validation_block_span(
|
||||
items=items,
|
||||
candidates=candidates,
|
||||
validation_log=validation_log,
|
||||
validation_error=first_error,
|
||||
)
|
||||
_safe_update(
|
||||
level="WARNING",
|
||||
output={
|
||||
"approved": False,
|
||||
"items_count": len(items),
|
||||
"items_validated_count": len(validation_log),
|
||||
"items_approved_count": approved_count,
|
||||
"items_rejected_count": rejected_count,
|
||||
"validation_log": validation_log,
|
||||
"error": first_error,
|
||||
"reason": _validation_reason(validation_log),
|
||||
},
|
||||
)
|
||||
return items, validation_log, first_error
|
||||
|
||||
_safe_update(
|
||||
output={
|
||||
"approved": True,
|
||||
"items_count": len(items),
|
||||
"items_validated_count": len(validation_log),
|
||||
"items_approved_count": approved_count,
|
||||
"items_rejected_count": rejected_count,
|
||||
"validation_log": validation_log,
|
||||
},
|
||||
)
|
||||
return items, validation_log, None
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .client import TimApiClient
|
||||
@@ -225,7 +227,7 @@ class ContasDomainService:
|
||||
"socialSecNo": customer.get("document") or args.get("social_sec_no") or "",
|
||||
"customerId": customer.get("customerId") or customer.get("id") or args.get("customer_id") or "",
|
||||
"invoiceNumber": args.get("invoice_id") or "",
|
||||
"userId": "AIAGENTCR",
|
||||
"userId": os.getenv("TIM_DEFAULT_CLIENT_ID"),
|
||||
"items": [{"itemName": subject, "itemType": "VAS_AVULSO", "claimedAmount": str(valor), "validatedAmount": str(valor)}],
|
||||
"description": motivo,
|
||||
}
|
||||
@@ -234,7 +236,7 @@ class ContasDomainService:
|
||||
return {"success": True, "protocol": protocol, "contestation": contestation, "tracking": tracking, "invoices": invoices, "contract": contract, "profile": profile}
|
||||
|
||||
def consultar_status_solicitacao(self, *, msisdn: str = "", protocol: str = "", **_: Any) -> Any:
|
||||
return self.client.status_sr({"msisdn": msisdn, "protocolNumber": protocol, "status": "CONSULTA", "channel": "AIAGENTCR"})
|
||||
return self.client.status_sr({"msisdn": msisdn, "protocolNumber": protocol, "status": "CONSULTA", "channel": os.getenv("TIM_DEFAULT_CHANNEL")})
|
||||
|
||||
def enviar_sms(self, *, msisdn: str, message: str, **_: Any) -> Any:
|
||||
return self.client.sms(msisdn, message)
|
||||
@@ -295,11 +297,11 @@ class ContasDomainService:
|
||||
response["service_request_status"] = self.client.status_sr({
|
||||
"protocolNumber": protocol,
|
||||
"status": "Fechado",
|
||||
"channel": "AIAGENTCR",
|
||||
"channel": os.getenv("TIM_DEFAULT_CHANNEL"),
|
||||
"notes": normalized_summary,
|
||||
})
|
||||
return response
|
||||
|
||||
|
||||
def os_source(args: dict[str, Any]) -> str:
|
||||
return str(args.get("channel") or "AIAGENTCR")
|
||||
return str(args.get("channel") or os.getenv("TIM_DEFAULT_CHANNEL"))
|
||||
|
||||
@@ -13,7 +13,8 @@ from typing import Any
|
||||
|
||||
from agent_framework.workflows import WorkflowActionRegistry
|
||||
from agent_framework.idempotency import InMemoryIdempotencyStore
|
||||
from agent_framework.guardrails.calibrated import validate_contestation_items
|
||||
from app.domain.contas.contestation_validation import validate_contestation_items
|
||||
from app.domain.contas.client import get_tim_integration_value
|
||||
|
||||
from .client import TimApiError
|
||||
from .contestation_rules import (
|
||||
@@ -798,9 +799,9 @@ def build_contas_workflow_actions(service: ContasDomainService, *, idempotency_s
|
||||
"refundOption": str(params.get("refund_option") or ""),
|
||||
"manualContaCertaIndicator": bool(params.get("manual_conta_certa_indicator", False)),
|
||||
"doubleRefund": bool(params.get("double_refund", False)),
|
||||
"userId": str(params.get("user_id") or "AIAGENTCR"),
|
||||
"userId": str(params.get("user_id") or os.getenv("TIM_DEFAULT_CLIENT_ID")),
|
||||
"messageId": str(params.get("message_id") or _first(params, state, "message_id") or ""),
|
||||
"clientId": str(params.get("client_id") or "AIAGENTCR"),
|
||||
"clientId": str(params.get("client_id") or os.getenv("TIM_DEFAULT_CLIENT_ID")),
|
||||
"tipo_atendimento": "pro_rata",
|
||||
"skip_invoice_item_validation": True,
|
||||
"items": items,
|
||||
@@ -1227,9 +1228,9 @@ def build_contas_workflow_actions(service: ContasDomainService, *, idempotency_s
|
||||
"doubleRefund": bool(params.get("double_refund")),
|
||||
"description": params.get("descricao") or "",
|
||||
"items": items,
|
||||
"userId": params.get("user_id") or "AIAGENTCR",
|
||||
"userId": params.get("user_id") or os.getenv("TIM_DEFAULT_CLIENT_ID"),
|
||||
"messageId": params.get("message_id") or "",
|
||||
"clientId": params.get("client_id") or "AIAGENTCR",
|
||||
"clientId": params.get("client_id") or os.getenv("TIM_DEFAULT_CLIENT_ID"),
|
||||
}
|
||||
# CVAL é capability do framework: antes do side effect, valida que os
|
||||
# itens existem na fatura, pertencem a VAS, não são estratégicos e que o
|
||||
@@ -1357,8 +1358,8 @@ def build_contas_workflow_actions(service: ContasDomainService, *, idempotency_s
|
||||
try:
|
||||
if hasattr(service, "client") and hasattr(service.client, "sms"):
|
||||
result = service.client.sms(
|
||||
msisdn, message, sender_name="TIM Brasil", long_url="https://meutim.com.br",
|
||||
notify_url=os.getenv("TIM_SMS_NOTIFY_URL", "http://10.114.200.57:9003/teste"),
|
||||
msisdn, message, sender_name=get_tim_integration_value("TIM_SMS_SENDER_NAME"), long_url=get_tim_integration_value("TIM_SMS_LONG_URL"),
|
||||
notify_url=get_tim_integration_value("TIM_SMS_NOTIFY_URL"),
|
||||
)
|
||||
else:
|
||||
result = service.enviar_sms(msisdn=msisdn, message=message)
|
||||
@@ -1447,8 +1448,8 @@ def build_contas_workflow_actions(service: ContasDomainService, *, idempotency_s
|
||||
status = str(params.get("status") or "Fechado")
|
||||
status_payload = {
|
||||
"msisdn": msisdn, "socialSecNo": re.sub(r"\D", "", str(params.get("social_sec_no") or "")),
|
||||
"protocolNumber": protocol_id, "status": status, "channel": "AIAGENTCR",
|
||||
"clientId": params.get("client_id") or "AIAGENTCR",
|
||||
"protocolNumber": protocol_id, "status": status, "channel": os.getenv("TIM_DEFAULT_CHANNEL"),
|
||||
"clientId": params.get("client_id") or os.getenv("TIM_DEFAULT_CLIENT_ID"),
|
||||
"reason1": "Reclamação", "reason2": "Conta", "reason3": "Valor", "notes": "",
|
||||
}
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user