Projeto do Agent Contas ORACLE

This commit is contained in:
2026-08-19 09:35:50 -03:00
commit 950a2bcd33
1366 changed files with 177217 additions and 0 deletions

566
app/domain/contas/client.py Normal file
View File

@@ -0,0 +1,566 @@
from __future__ import annotations
import json
import os
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.parse import quote
import requests
from .integrations.secure_pdf_crypto import encrypt_secure_pdf_value
from .parsers import parse_tim_bill_pdf
class TimApiError(RuntimeError):
def __init__(self, message: str, *, status_code: int | None = None, body: Any = None, attempts: list[dict[str, Any]] | None = None):
super().__init__(message)
self.status_code = status_code
self.body = body
self.attempts = list(attempts or [])
class TimApiClient:
"""Thin TIM integration adapter.
This class deliberately contains no agent, LangGraph, memory, routing or LLM logic.
Those responsibilities belong to agent_framework_oci.
"""
FIXTURES = Path(__file__).with_name("fixtures")
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.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"))
def fixture(self, name: str) -> Any:
path = self.FIXTURES / f"{name}.json"
return json.loads(path.read_text(encoding="utf-8"))
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"}
if client_id:
h["client_id"] = client_id
if auth:
h["Authorization"] = auth
if extra:
h.update({k: v for k, v in extra.items() if v not in (None, "")})
return h
def request(self, method: str, url: str, *, payload: dict[str, Any] | None = None, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: int | None = None, attempt_log: list[dict[str, Any]] | None = None) -> Any:
if not url:
raise TimApiError("Endpoint TIM não configurado")
last: Exception | None = None
attempts = max(1, self.max_retries)
for attempt in range(attempts):
started = time.monotonic()
try:
resp = requests.request(method, url, json=payload, params=params, headers=headers or {}, timeout=timeout or self.timeout)
if resp.status_code in {204, 205}:
if attempt_log is not None:
attempt_log.append({"attempt": attempt + 1, "success": True, "status_code": resp.status_code, "latency_ms": int((time.monotonic()-started)*1000), "api_url": url})
return {"status": resp.status_code, "message": "OK"}
resp.raise_for_status()
if attempt_log is not None:
attempt_log.append({"attempt": attempt + 1, "success": True, "status_code": resp.status_code, "latency_ms": int((time.monotonic()-started)*1000), "api_url": url})
if not resp.content:
return {"status": resp.status_code, "message": "OK"}
ctype = resp.headers.get("content-type", "").lower()
if "json" in ctype:
return resp.json()
return {"status": resp.status_code, "raw_content": resp.content, "content_type": ctype}
except requests.RequestException as exc:
last = exc
if attempt_log is not None:
response = getattr(exc, "response", None)
attempt_log.append({"attempt": attempt + 1, "success": False, "status_code": getattr(response, "status_code", None), "latency_ms": int((time.monotonic()-started)*1000), "error": str(exc), "api_url": url})
if attempt >= attempts - 1:
break
time.sleep(self.backoff * (2 ** attempt))
response = getattr(last, "response", None)
status_code = getattr(response, "status_code", None)
body = None
if response is not None:
try:
body = response.json()
except Exception:
body = getattr(response, "text", None)
raise TimApiError(str(last), status_code=status_code, body=body, attempts=list(attempt_log or []))
@staticmethod
def _attach_transport(result: Any, *, operation: str, attempts: list[dict[str, Any]]) -> Any:
metadata = {
"rct_operation": operation,
"attempts": list(attempts),
"api_response_payload": result if isinstance(result, (dict, list, str, int, float, bool)) else str(result),
}
if isinstance(result, dict):
out = dict(result)
out["_transport"] = metadata
return out
return {"data": result, "_transport": metadata}
def consultar_faturas(self, msisdn: str) -> Any:
if self.mock:
return self.fixture("complete_invoices")
url = os.getenv("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")}))
def billing_analysis(self, msisdn: str, **context: Any) -> Any:
if self.mock:
return self.fixture("divergencia")
base = os.getenv("TIM_DIVERGENCIA_URL", "").rstrip("/")
url = f"{base}/{quote(msisdn)}"
auth = os.getenv("TIM_DIVERGENCIA_AUTH", "") or os.getenv("TIM_SECURE_PDF_AUTH", "")
headers = self._headers(client_id=None, auth=auth, extra={
"clientID": os.getenv("TIM_DIVERGENCIA_CLIENT_ID", "AIAGENTCR"),
})
params = {"channel": context.get("channel") or "AIAGENTCR"}
attempts: list[dict[str, Any]] = []
try:
result = self.request("GET", url, params=params, headers=headers, timeout=int(os.getenv("TIM_DIVERGENCIA_TIMEOUT", "120")), attempt_log=attempts)
return self._attach_transport(result, operation="base_conhecimento", attempts=attempts)
except TimApiError as exc:
if not exc.attempts:
exc.attempts = attempts
raise
def consultar_vas(self, msisdn: str) -> Any:
if self.mock:
return self.fixture("query_vas")
base = os.getenv("TIM_URL_CONSULTA_VAS", "")
url = base.replace("{msisdn}", quote(msisdn))
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")}))
def historico_vas(self, msisdn: str) -> Any:
if self.mock:
return self.fixture("vas_history")
base = os.getenv("TIM_VAS_HISTORY_URL", "")
sep = "&" if "?" in base else "?"
url = f"{base}{sep}msisdn={quote(msisdn)}"
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_VAS_HISTORY_AUTH", ""),
extra={
"clientId": os.getenv("TIM_VAS_HISTORY_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "CHAT")),
"messageId": os.getenv("TIM_VAS_HISTORY_MESSAGE_ID", "") or str(uuid.uuid4()),
},
)
return self.request("GET", url, headers=headers, timeout=int(os.getenv("TIM_VAS_HISTORY_TIMEOUT", "30")))
def bloquear_vas(self, msisdn: str, service: dict[str, Any]) -> Any:
if self.mock:
return self.fixture("block_vas")
url = os.getenv("TIM_URL_BLOQUEIO_VAS", "")
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"))
op = os.getenv("TIM_BLOQUEIO_OPERATION_TYPE", "block")
base = {"Customer": {"Msisdn": normalized}, "AppId": app_id, "CspId": csp_id, "TypeOperation": op}
payloads = [
{"customer": {"msisdn": normalized}, "appId": app_id, "cspId": csp_id, "typeOperation": op},
{"input": base, "Input": base},
{"vasBlock": {"msisdn": normalized, "appId": app_id, "cspId": csp_id, "type": op}},
]
mode = os.getenv("TIM_BLOQUEIO_PAYLOAD_MODE", "auto").strip().lower()
if mode == "vasblock": payloads = [payloads[2], payloads[0]]
elif mode == "input": payloads = [payloads[1], payloads[0]]
elif mode == "pmid": payloads = [payloads[0], payloads[1]]
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_BLOQUEIO_AUTH", ""),
extra={
"Accept-Encoding": os.getenv("TIM_BLOQUEIO_ACCEPT_ENCODING", "gzip,deflate"),
"clientId": os.getenv("TIM_BLOQUEIO_CLIENT_ID", "AIAGENTCR"),
"messageId": str(uuid.uuid4()),
},
)
last = None
for index, payload in enumerate(payloads):
try:
return self.request("POST", url, payload=payload, headers=headers, timeout=int(os.getenv("TIM_BLOQUEIO_TIMEOUT", "30")))
except TimApiError as exc:
last = exc
if exc.status_code != 400 or index >= len(payloads) - 1:
raise
raise last or TimApiError("Bloqueio VAS falhou")
def cancelar_vas(self, msisdn: str, service: dict[str, Any], *, protocol: str = "") -> Any:
if self.mock:
return self.fixture("cancel_vas")
url = os.getenv("TIM_CANCELAMENTO_URL", "")
payload = {
"channel": "AIAGENTCR",
"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")),
"interactionProtocol": protocol,
}
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_CANCELAMENTO_AUTH", ""),
extra={
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR")),
"messageId": str(uuid.uuid4()),
"AuthorizationOAM": os.getenv("TIM_CANCELAMENTO_AUTHORIZATION_OAM", ""),
"Cn_field": os.getenv("TIM_CANCELAMENTO_CN_FIELD", ""),
"Type_field": os.getenv("TIM_CANCELAMENTO_TYPE_FIELD", ""),
},
)
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)
return self._attach_transport(result, operation="cancela_vas", attempts=attempts)
def contrato(self, msisdn: str) -> Any:
if self.mock:
return self.fixture("contrato")
base = os.getenv("TIM_CONTRATO_URL", "").rstrip("/")
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_CONTRATO_AUTH", ""),
extra={"clientId": os.getenv("TIM_CONTRATO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "CHAT"))},
)
return self.request("GET", f"{base}/{quote(msisdn)}", headers=headers, timeout=int(os.getenv("TIM_CONTRATO_TIMEOUT", "30")))
def profile_full(self, msisdn: str) -> Any:
if self.mock:
# contract fixture carries representative customer identity in local mode.
return self.fixture("contrato")
url = os.getenv("TIM_PROFILE_FULL_URL", "").replace("{msisdn}", quote(msisdn))
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
)
return self.request("GET", url, headers=headers)
def abrir_protocolo(self, payload: dict[str, Any]) -> Any:
"""Registra protocolo V2 preservando o contrato externo do Contas original.
As workflow actions trabalham com um payload lógico/achatado. O adapter é
responsável por converter esse modelo para o contrato PMid/Siebel, sem
contaminar o domínio ou o WorkflowRuntime com detalhes HTTP.
"""
if self.mock:
return self.fixture("protocol")
data = dict(payload or {})
msisdn = str(data.get("msisdn") or data.get("assetId") or "")
service_status = str(data.get("requestStatus") or data.get("status") or "")
message_id = str(
data.get("messageId")
or data.get("interactionCallId")
or data.get("ura_call_id")
or data.get("session_id")
or uuid.uuid4()
)
body = {
"socialSecNo": str(data.get("socialSecNo") or data.get("social_sec_no") or ""),
"channel": "AIAGENTCR",
"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 ""),
"customerEmail": str(data.get("customerEmail") or data.get("customer_email") or ""),
"customerPhone1": str(data.get("customerPhone1") or data.get("customer_phone1") or ""),
"accessType": str(data.get("accessType") or data.get("access_type") or ""),
"serviceRequest": {
"userId": str(data.get("serviceRequestUserId") or data.get("service_request_user_id") or ""),
"reason1": str(data.get("reason1") or ""),
"reason2": str(data.get("reason2") or ""),
"reason3": str(data.get("reason3") or ""),
"status": service_status,
"notes": str(data.get("serviceRequestNotes") or data.get("service_request_notes") or data.get("notes") or ""),
"type": str(data.get("type") or "CLIENTE"),
},
"interaction": {
"protocol": str(data.get("interactionProtocol") or data.get("interaction_protocol") or ""),
"flagSms": True,
"source": "AIAGENTCR",
"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 ""),
"amount": str(data.get("interactionAmount") or data.get("interaction_amount") or ""),
"directionContact": str(data.get("directionContact") or data.get("direction_contact") or "FROM-CLIENT"),
"requestFlag": False,
"requestSla": str(data.get("interactionRequestSla") or data.get("interaction_request_sla") or ""),
"status": str(data.get("status") or service_status),
},
}
headers = self._headers(
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")),
"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", "")),
"Type_field": str(data.get("type_field") or os.getenv("TIM_PROTOCOL_TYPE_FIELD", "")),
},
)
attempts: list[dict[str, Any]] = []
result = self.request(
"POST",
os.getenv("TIM_PROTOCOL_URL", ""),
payload=body,
headers=headers,
timeout=int(os.getenv("TIM_PROTOCOL_TIMEOUT", "30")),
attempt_log=attempts,
)
operation = str(data.get("rct_operation") or "")
return self._attach_transport(result, operation=operation, attempts=attempts) if operation else result
def contestar(self, payload: dict[str, Any]) -> Any:
if self.mock:
return self.fixture("contestacao_tool")
data = dict(payload)
data.setdefault("userId", "AIAGENTCR")
data.setdefault("customerIdCurrent", data.get("customerId") or "")
data.setdefault("customerType", "2")
data.setdefault("customerStatus", "1")
status_map = {"ABERTA": "0", "ABERTO": "0", "OPEN": "0", "FECHADA": "1", "FECHADO": "1", "CLOSED": "1"}
raw_status = str(data.get("invoiceStatus") or "").strip()
data["invoiceStatus"] = status_map.get(raw_status.upper(), raw_status or "0")
data.setdefault("invoiceAmountOpen", "0")
data.setdefault("invoiceAmount", "0")
due = "".join(ch for ch in str(data.get("invoiceDueDate") or "") if ch.isdigit())
if len(due) == 8 and str(data.get("invoiceDueDate") or "").startswith(tuple(str(y) for y in range(19, 22))):
# YYYYMMDD already normalized
pass
elif len(due) == 8:
# common ddMMyyyy -> yyyyMMdd
due = due[4:]+due[2:4]+due[:2]
data["invoiceDueDate"] = due
data.setdefault("contestationType", "0")
data.setdefault("adjustReason", "SERVICO_NAO_SOLICITADO")
data.setdefault("observation", data.pop("description", ""))
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"))
message_id = str(data.pop("messageId", "") or uuid.uuid4())
user_id = str(data.get("userId") or "AIAGENTCR").upper()
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_CUSTOMER_CONTESTATION_AUTH", ""),
extra={"clientId": client_id, "messageId": message_id, "X-Agent-Id": user_id},
)
attempts: list[dict[str, Any]] = []
result = self.request("POST", os.getenv("TIM_CUSTOMER_CONTESTATION_URL", ""), payload=data, headers=headers, timeout=int(os.getenv("TIM_CUSTOMER_CONTESTATION_TIMEOUT", "30")), attempt_log=attempts)
return self._attach_transport(result, operation="contestacao", attempts=attempts)
def status_sr(self, payload: dict[str, Any]) -> Any:
if self.mock:
return self.fixture("service_request_status")
data = dict(payload)
service_request = data.get("serviceRequest") if isinstance(data.get("serviceRequest"), dict) else {
"status": data.get("status") or "",
"notes": data.get("notes") or "",
"protocolNumber": data.get("protocolNumber") or data.get("protocol") or "",
"auditResult": data.get("auditResult") or "",
"auditReasonResult": data.get("auditReasonResult") or "",
"auditConvenienceTime": data.get("auditConvenienceTime") or "",
"auditSubStatus": data.get("auditSubStatus") or "",
}
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}
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())
headers = self._headers(
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")),
"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", ""),
"Type_field": data.get("type_field") or os.getenv("TIM_SERVICE_REQUEST_STATUS_TYPE_FIELD", ""),
},
)
attempts: list[dict[str, Any]] = []
result = self.request(
"POST", os.getenv("TIM_SERVICE_REQUEST_STATUS_URL", ""), payload=body, headers=headers,
timeout=int(os.getenv("TIM_SERVICE_REQUEST_STATUS_TIMEOUT", "30")), attempt_log=attempts
)
return self._attach_transport(result, operation="", attempts=attempts)
def tracking(self, payload: dict[str, Any]) -> Any:
if self.mock:
return self.fixture("tracking_activities")
data = dict(payload)
invoice = data.get("invoice") if isinstance(data.get("invoice"), dict) else {
"emissionDate": data.get("invoiceEmissionDate") or "",
"expirationDate": data.get("invoiceExpirationDate") or "",
"number": data.get("invoiceNumber") or "",
"status": data.get("invoiceStatus") or "",
"openAmount": data.get("invoiceOpenAmount") or "",
"totalAmount": data.get("invoiceTotalAmount") or "",
}
body = {
"channel": data.get("channel") or os.getenv("TIM_TRACKING_ACTIVITIES_CHANNEL", "AIAGENTCR"),
"customer": {"socialSecNo": data.get("socialSecNo") or "", "msisdn": data.get("msisdn") or ""},
"protocolNumber": data.get("protocolNumber") or "",
"invoice": invoice,
"activity": {
"type": data.get("activityType") or "",
"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")},
}
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")},
)
return self.request("POST", os.getenv("TIM_TRACKING_ACTIVITIES_URL", ""), payload=body, headers=headers, timeout=int(os.getenv("TIM_TRACKING_ACTIVITIES_TIMEOUT", "30")))
def sms(self, msisdn: str, message: str, **context: Any) -> Any:
if self.mock:
return self.fixture("sms")
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"),
"message": message,
"longURL": context.get("long_url") or message,
}
if context.get("notify_url"):
payload["receiptRequest"] = {"notifyURL": context["notify_url"]}
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_SMS_AUTH", ""),
extra={"clientId": os.getenv("TIM_SMS_CLIENT_ID", "AIAGENTCR")},
)
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)
return self._attach_transport(result, operation="sgr_codbar", attempts=attempts)
def profile_bill(self, msisdn: str) -> Any:
"""Perfil de faturamento usando o mesmo contrato CompleteInvoices do original."""
if self.mock:
return self.fixture("profile_bill")
url = os.getenv("TIM_URL_PERFIL_FATURA", "") or os.getenv("TIM_COMPLETE_INVOICES_URL", "")
return self.request(
"POST",
url,
payload={"msisdn": msisdn},
headers=self._headers(
client_id=None,
auth=os.getenv("TIM_PROFILE_BILL_AUTH", "") or os.getenv("TIM_COMPLETE_INVOICES_AUTH", ""),
extra={"ClientID": os.getenv("TIM_PROFILE_BILL_CLIENT_ID", "AIAGENTCR")},
),
timeout=int(os.getenv("TIM_PROFILE_BILL_TIMEOUT", "30")),
)
def line_info(self, msisdn: str) -> Any:
"""Consulta dados cadastrais da linha/dependente e preserva o CPF correto."""
if self.mock:
payload = self.fixture("contrato")
else:
url = os.getenv("TIM_PROFILE_FULL_URL", "").replace("{msisdn}", quote(msisdn))
payload = self.request(
"GET",
url,
headers=self._headers(
client_id=None,
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
),
)
def extract(value: Any) -> str:
if not isinstance(value, dict):
return ""
direct = value.get("socialSecNo")
if direct not in (None, ""):
return str(direct).strip()
for key in ("customer", "contract", "billingProfile", "subscriber"):
found = extract(value.get(key))
if found:
return found
return ""
return {"social_sec_no": extract(payload), "raw": payload if isinstance(payload, dict) else {}}
def bill_pdf(
self,
msisdn: str,
invoice_id: str,
customer_id: str,
*,
include_danfe: bool = False,
output: str = "",
) -> Any:
"""Recupera a fatura detalhada via POST e normaliza seu PDF.
Esta operação é distinta de ``secure_pdf`` e corresponde ao BillPdfCommand
original, usado por pró-rata, matching e explicação detalhada de fatura.
"""
if self.mock:
parsed = self.fixture("invoice_pdf_include_danfe_true" if include_danfe else "invoice_pdf_include_danfe_false")
return {
"status": "SUCCESS",
"message": "Fatura recuperada com sucesso",
"file_content": None,
"file_name": output or "antiga.pdf",
"parsed_content": parsed,
}
url = os.getenv("TIM_BILL_PDF_URL", "") or os.getenv("TIM_SECURE_PDF_URL", "") or os.getenv("TIM_URL_INVOICE_RECOVER", "")
payload = {
"invoiceId": encrypt_secure_pdf_value(invoice_id),
"customerId": encrypt_secure_pdf_value(customer_id),
"invoiceType": "DETALHADA",
}
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_BILL_PDF_AUTH", "") or os.getenv("TIM_SECURE_PDF_AUTH", ""),
extra={
"clientId": os.getenv("TIM_BILL_PDF_CLIENT_ID", "AIAGENTCR"),
"Accept": "application/pdf",
},
)
result = self.request(
"POST",
url,
payload=payload,
headers=headers,
timeout=int(os.getenv("TIM_BILL_PDF_TIMEOUT", os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30"))),
)
raw_content = result.get("raw_content") if isinstance(result, dict) else None
parsed = result.get("parsed_content") if isinstance(result, dict) else None
if raw_content and parsed is None:
parsed = parse_tim_bill_pdf(raw_content, include_danfe=include_danfe)
return {
"status": "SUCCESS" if raw_content or parsed is not None else str((result or {}).get("status") if isinstance(result, dict) else ""),
"message": "Fatura recuperada com sucesso" if raw_content or parsed is not None else "Resposta de fatura sem conteúdo",
"file_content": raw_content,
"file_name": output or "antiga.pdf",
"parsed_content": parsed,
"raw": result,
}
def secure_pdf(self, msisdn: str, invoice_id: str, customer_id: str = "") -> Any:
if self.mock:
return self.fixture("invoice_pdf_include_danfe_false")
url = os.getenv("TIM_URL_INVOICE_RECOVER", "")
params = {
"invoiceId": encrypt_secure_pdf_value(invoice_id),
"msisdn": encrypt_secure_pdf_value(msisdn),
"customerId": encrypt_secure_pdf_value(customer_id),
}
return self.request(
"GET",
url,
params=params,
headers=self._headers(
client_id=None,
auth=os.getenv("TIM_SECURE_PDF_AUTH", ""),
extra={"clientId": os.getenv("TIM_INVOICE_RECOVER_CLIENT_ID", "AIAGENTCR")},
),
timeout=int(os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30")),
)