mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 10:14:20 +00:00
2017 lines
68 KiB
Python
2017 lines
68 KiB
Python
from __future__ import annotations
|
|
|
|
import functools
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import unicodedata as ud
|
|
|
|
from contextlib import nullcontext
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from agente_contas_tim.constants.ic_tags_enum import CVNTag, RCTTag, TMDTag, VAATag, VEBTag
|
|
from agente_contas_tim.integrations.rct_policy import RCTOperation, rct_tags_for_attempt
|
|
from agente_contas_tim.observability import (
|
|
get_session_id,
|
|
langfuse_context_has_active_span,
|
|
mask_msisdn,
|
|
)
|
|
from agente_contas_tim.protocol_triplets import resolve_protocol_triplet
|
|
from agente_contas_tim.workflows.actions.registry import (
|
|
WorkflowRuntimeContext,
|
|
workflow_action,
|
|
)
|
|
from agente_contas_tim.workflows.runtime_types import ActionResult
|
|
from agente_contas_tim.workflows.actions.common.helpers import (
|
|
_emit_ic,
|
|
_emit_vaa,
|
|
_first_text_from_params_or_state,
|
|
_first_value_from_params_or_state,
|
|
_normalize_bool,
|
|
_normalize_number_text,
|
|
_result_failed_or_missing_data,
|
|
_to_dict,
|
|
)
|
|
from agente_contas_tim.workflows.actions.contestacao.helpers import (
|
|
_collect_invoice_status_texts,
|
|
_has_open_bill_from_payload,
|
|
_extrair_dia_vencimento,
|
|
_resolve_billing_cutoff_reference,
|
|
_resolve_next_cutoff_from_cut_date,
|
|
_first_invoice_date_from_payload_by_id,
|
|
_first_invoice_status_from_payload,
|
|
_find_invoice_item_by_id,
|
|
_invoice_status_texts_from_payload,
|
|
_is_plano_familia_dependent_access,
|
|
_resolve_contestation_due_date,
|
|
_resolve_refund_and_resolution_decision,
|
|
_resolve_contestation_request_rules,
|
|
_map_invoice_status_to_state,
|
|
)
|
|
from agente_contas_tim.workflows.actions.pro_rata.helpers import (
|
|
_extract_invoice_payload_from_state_or_params,
|
|
)
|
|
|
|
logger = logging.getLogger("agente_contas_tim.workflows.actions.tim_actions")
|
|
|
|
_CONTESTATION_API_ERROR_KEYS = (
|
|
"contestation_error_description",
|
|
)
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def _import_langfuse_get_client() -> Any:
|
|
from langfuse import get_client
|
|
return get_client
|
|
|
|
|
|
def _has_langfuse_credentials() -> bool:
|
|
return bool(
|
|
os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
|
|
and os.getenv("LANGFUSE_SECRET_KEY", "").strip()
|
|
)
|
|
|
|
|
|
def _langfuse_client() -> Any | None:
|
|
if not _has_langfuse_credentials():
|
|
return None
|
|
if not langfuse_context_has_active_span():
|
|
return None
|
|
try:
|
|
client = _import_langfuse_get_client()()
|
|
except Exception:
|
|
logger.debug("langfuse.sms_get_client_failed", exc_info=True)
|
|
return None
|
|
try:
|
|
if not client.get_current_trace_id():
|
|
return None
|
|
except Exception:
|
|
logger.debug("langfuse.sms_current_trace_lookup_failed", exc_info=True)
|
|
return None
|
|
return client
|
|
|
|
|
|
def _start_sms_observation(
|
|
*,
|
|
msisdn: str,
|
|
has_barcode: bool,
|
|
long_url: str,
|
|
notify_url: str,
|
|
) -> Any:
|
|
client = _langfuse_client()
|
|
if client is None:
|
|
return nullcontext(None)
|
|
try:
|
|
return client.start_as_current_observation(
|
|
name="workflow.sms.enviar",
|
|
as_type="span",
|
|
input={
|
|
"msisdn": mask_msisdn(msisdn),
|
|
"has_barcode": has_barcode,
|
|
"long_url_configured": bool(long_url),
|
|
"notify_url_configured": bool(notify_url),
|
|
},
|
|
metadata={
|
|
"component": "workflow_action",
|
|
"action": "enviar_sms",
|
|
"channel": "sms",
|
|
},
|
|
)
|
|
except Exception:
|
|
logger.debug("langfuse.sms_start_observation_failed", exc_info=True)
|
|
return nullcontext(None)
|
|
|
|
|
|
def _update_sms_observation(
|
|
observation: Any | None,
|
|
*,
|
|
output: dict[str, Any],
|
|
level: str | None = None,
|
|
status_message: str | None = None,
|
|
) -> None:
|
|
if observation is None:
|
|
return
|
|
try:
|
|
kwargs: dict[str, Any] = {"output": output}
|
|
if level is not None:
|
|
kwargs["level"] = level
|
|
if status_message is not None:
|
|
kwargs["status_message"] = status_message
|
|
observation.update(**kwargs)
|
|
except Exception:
|
|
logger.debug("langfuse.sms_update_observation_failed", exc_info=True)
|
|
|
|
|
|
def _is_recoverable_contestation_api_error(result: Any) -> bool:
|
|
metadata = getattr(result, "metadata", {})
|
|
if not isinstance(metadata, dict):
|
|
return False
|
|
return isinstance(metadata.get("error_body"), dict)
|
|
|
|
|
|
def _build_contestation_api_error_output(
|
|
result: Any,
|
|
*,
|
|
protocolo_id: str,
|
|
items: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
metadata = getattr(result, "metadata", {})
|
|
if not isinstance(metadata, dict):
|
|
metadata = {}
|
|
error_body = metadata.get("error_body")
|
|
if not isinstance(error_body, dict):
|
|
error_body = {}
|
|
provider = error_body.get("provider", {})
|
|
if not isinstance(provider, dict):
|
|
provider = {}
|
|
|
|
description = str(error_body.get("description") or "").strip()
|
|
provider_message = str(provider.get("errorMessage") or "").strip()
|
|
error_message = description or provider_message or str(
|
|
getattr(result, "error", "") or "Falha ao abrir contestação"
|
|
).strip()
|
|
return {
|
|
"manual_conta_certa_indicator": False,
|
|
"contestation_id": "",
|
|
"sr": protocolo_id,
|
|
"protocolo_id": protocolo_id,
|
|
"protocol_number": protocolo_id,
|
|
"barcode": "",
|
|
"validated_by_actions": True,
|
|
"validated_items": items,
|
|
"items_response": [],
|
|
"contested_items": [],
|
|
"not_contested_items": [],
|
|
"contested_invoice_amount_open": "0",
|
|
"contested_invoice_amount": "0",
|
|
"body": error_body,
|
|
"response": {"body": error_body},
|
|
"validation_log": [],
|
|
"contestation_error_description": error_message,
|
|
}
|
|
|
|
|
|
_CLOSE_SR_CHANNEL = "AIAGENTCR"
|
|
_CLOSE_SR_CLIENT_ID = "AIAGENTCR"
|
|
_CONTESTATION_SUCCESS_STATUSES = frozenset({"INICIADA", "INICIADO"})
|
|
_CONTESTATION_SUCCESS_CORRECT_ACCOUNT_STATUSES = frozenset({"CRIAR"})
|
|
|
|
|
|
def _normalize_contestation_item_name(value: Any) -> str:
|
|
normalized = ud.normalize("NFKD", str(value or "").strip())
|
|
normalized = "".join(ch for ch in normalized if not ud.combining(ch))
|
|
normalized = re.sub(r"[^a-zA-Z0-9]+", " ", normalized)
|
|
return re.sub(r"\s+", " ", normalized).upper().strip()
|
|
|
|
|
|
def _same_contestation_item_name(left: Any, right: Any) -> bool:
|
|
left_name = _normalize_contestation_item_name(left)
|
|
right_name = _normalize_contestation_item_name(right)
|
|
if not left_name or not right_name:
|
|
return False
|
|
if left_name == right_name:
|
|
return True
|
|
return left_name in right_name or right_name in left_name
|
|
|
|
|
|
def _sum_contested_item_amounts(
|
|
*,
|
|
requested_items: list[dict[str, Any]],
|
|
contested_items: list[dict[str, Any]],
|
|
) -> dict[str, str]:
|
|
contested_names = {
|
|
_normalize_contestation_item_name(item.get("itemName"))
|
|
for item in contested_items
|
|
if isinstance(item, dict)
|
|
}
|
|
claimed_total = Decimal("0")
|
|
validated_total = Decimal("0")
|
|
if not contested_names:
|
|
return {
|
|
"contested_invoice_amount_open": "0",
|
|
"contested_invoice_amount": "0",
|
|
}
|
|
|
|
for item in requested_items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
item_name = _normalize_contestation_item_name(
|
|
item.get("item_name") or item.get("itemName")
|
|
)
|
|
if not any(_same_contestation_item_name(item_name, requested_name) for requested_name in contested_names):
|
|
continue
|
|
claimed_total += Decimal(_normalize_number_text(item.get("claimed_amount", "0")))
|
|
validated_total += Decimal(
|
|
_normalize_number_text(item.get("validated_amount", "0"))
|
|
)
|
|
|
|
return {
|
|
"contested_invoice_amount_open": format(claimed_total, "f"),
|
|
"contested_invoice_amount": format(validated_total, "f"),
|
|
}
|
|
|
|
|
|
def _is_contested_item(item: dict[str, Any]) -> bool:
|
|
status = str(item.get("status") or "").strip().upper()
|
|
if status in _CONTESTATION_SUCCESS_STATUSES:
|
|
return True
|
|
|
|
correct_account_status = str(
|
|
item.get("correctAccountStatus")
|
|
or item.get("correctAccountstatus")
|
|
or item.get("correct_account_status")
|
|
or item.get("correctAccounStatus")
|
|
or ""
|
|
).strip().upper()
|
|
return correct_account_status in _CONTESTATION_SUCCESS_CORRECT_ACCOUNT_STATUSES
|
|
|
|
|
|
def _extract_item_list(payload: dict[str, Any], key: str, camel_key: str) -> list[dict[str, Any]]:
|
|
raw = payload.get(key)
|
|
if not isinstance(raw, list):
|
|
raw = payload.get(camel_key)
|
|
if not isinstance(raw, list):
|
|
return []
|
|
return [item for item in raw if isinstance(item, dict)]
|
|
|
|
|
|
def _get_sms_enviado_flag(vars_state: Any) -> bool:
|
|
if not isinstance(vars_state, dict):
|
|
return False
|
|
|
|
sms_payload = vars_state.get("enviar_sms")
|
|
if not isinstance(sms_payload, dict):
|
|
return False
|
|
|
|
if "sms_enviado" in sms_payload:
|
|
return bool(sms_payload.get("sms_enviado"))
|
|
if "sms_sent" in sms_payload:
|
|
return bool(sms_payload.get("sms_sent"))
|
|
if sms_payload.get("resource_url") or sms_payload.get("resourceUrl"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _get_sms_not_send_error_flag(vars_state: Any) -> bool:
|
|
if not isinstance(vars_state, dict):
|
|
return False
|
|
|
|
sms_payload = vars_state.get("enviar_sms")
|
|
if not isinstance(sms_payload, dict):
|
|
return False
|
|
|
|
return bool(
|
|
sms_payload.get("sms_not_send_error")
|
|
or sms_payload.get("sms_nao_enviado_erro")
|
|
)
|
|
|
|
|
|
def _inject_complete_invoice_context(
|
|
payload: dict[str, Any],
|
|
invoice_id: str,
|
|
) -> dict[str, Any]:
|
|
invoice_id_text = str(invoice_id or "").strip()
|
|
if not invoice_id_text:
|
|
return payload
|
|
|
|
invoice_status = _first_invoice_status_from_payload(
|
|
payload,
|
|
invoice_id=invoice_id_text,
|
|
)
|
|
if invoice_status:
|
|
payload["invoice_status"] = invoice_status
|
|
elif invoice_id_text:
|
|
payload.pop("invoice_status", None)
|
|
|
|
invoice_item = _find_invoice_item_by_id(payload, invoice_id_text)
|
|
if not isinstance(invoice_item, dict):
|
|
if invoice_id_text:
|
|
payload.pop("bar_code", None)
|
|
payload.pop("due_date", None)
|
|
payload.pop("issue_date", None)
|
|
return payload
|
|
|
|
bar_code = str(
|
|
invoice_item.get("barCode")
|
|
or invoice_item.get("barcode")
|
|
or invoice_item.get("bar_code")
|
|
or ""
|
|
).strip()
|
|
if bar_code:
|
|
payload["bar_code"] = bar_code
|
|
|
|
due_date = str(
|
|
invoice_item.get("dueDate")
|
|
or invoice_item.get("due_date")
|
|
or ""
|
|
).strip()
|
|
if due_date:
|
|
payload["due_date"] = due_date
|
|
|
|
issue_date = str(
|
|
invoice_item.get("issueDate")
|
|
or invoice_item.get("issue_date")
|
|
or ""
|
|
).strip()
|
|
if issue_date:
|
|
payload["issue_date"] = issue_date
|
|
|
|
return payload
|
|
|
|
|
|
@workflow_action("consultar_perfil_fatura")
|
|
def query_profile_bill(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Consulta perfil de fatura para validar forma de pagamento."""
|
|
result = runtime.factory.create_profile_bill(
|
|
msisdn=str(params["msisdn"]),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao consultar perfil de fatura",
|
|
**result.metadata,
|
|
)
|
|
payload = _to_dict(result.data)
|
|
if isinstance(payload, dict):
|
|
method_payment = str(payload.get("method_payment", "")).strip().lower()
|
|
requires_sms = bool(payload.get("requires_sms", False))
|
|
payload["forma_pagamento"] = method_payment
|
|
payload["requer_sms"] = requires_sms
|
|
|
|
logger.info(
|
|
"""contestacao.decisao_perfil_fatura msisdn=%s
|
|
forma_pagamento=%s requires_sms=%s""",
|
|
str(params.get("msisdn", "")),
|
|
method_payment or "",
|
|
requires_sms,
|
|
)
|
|
return ActionResult.ok(payload, **result.metadata)
|
|
|
|
|
|
@workflow_action("registrar_protocolo_inicio")
|
|
def register_protocol_start(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Registra protocolo no inicio do atendimento."""
|
|
msisdn = _first_text_from_params_or_state(
|
|
state, params, "msisdn", "Msisdn", "MSISDN",
|
|
)
|
|
if not msisdn:
|
|
return ActionResult.fail("msisdn obrigatório para registrar_protocolo_inicio")
|
|
servico = str(params.get("servico", "")).strip()
|
|
scenario = str(params.get("scenario", "")).strip().lower()
|
|
ic = str(params.get("ic", "")).strip()
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"cpf",
|
|
"customer_document",
|
|
"customerDocument",
|
|
"document",
|
|
),
|
|
)
|
|
open_triplet = resolve_protocol_triplet(
|
|
"contestacao",
|
|
stage="open",
|
|
context={"servico": servico},
|
|
)
|
|
request_status = str(params.get("request_status", "Fechado")).strip() or "Fechado"
|
|
status = str(params.get("status", "CLOSED")).strip() or "CLOSED"
|
|
protocol_message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
protocol_message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(msisdn),
|
|
"ani": str((input_state.get("ani") if isinstance(input_state, dict)
|
|
else "") or "").strip(),
|
|
"uraCallId": str((input_state.get("ura_call_id")
|
|
or input_state.get("uraCallId")
|
|
if isinstance(input_state, dict) else "") or "").strip(),
|
|
"agentId": "contas",
|
|
"channelId": str((input_state.get("channel_id")
|
|
or input_state.get("channelId")
|
|
if isinstance(input_state, dict) else "URA") or "URA").strip()
|
|
}
|
|
result = runtime.factory.create_protocol_v2(
|
|
msisdn=msisdn,
|
|
interaction_protocol="",
|
|
flag_sms=True,
|
|
social_sec_no=social_sec_no,
|
|
source="CHAT",
|
|
reason1=open_triplet.reason1 or "Solicitação",
|
|
reason2=open_triplet.reason2 or "cancelamento_vas",
|
|
reason3=open_triplet.reason3 or "Valor",
|
|
direction_contact="FROM-CLIENT",
|
|
type="CLIENTE",
|
|
request_flag=True,
|
|
request_status=request_status,
|
|
status=status,
|
|
client_id="AIAGENTCR",
|
|
ic_context=_ic_base,
|
|
message_id=protocol_message_id,
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
if scenario == "invoice_explanation_aceite_fechado":
|
|
cvn_input_state = dict(input_state) if isinstance(input_state, dict) else {}
|
|
if not cvn_input_state.get("msisdn"):
|
|
cvn_input_state["msisdn"] = msisdn
|
|
if not cvn_input_state.get("ura_call_id"):
|
|
cvn_input_state["ura_call_id"] = _ic_base.get("uraCallId", "")
|
|
if not cvn_input_state.get("channel_id"):
|
|
cvn_input_state["channel_id"] = _ic_base.get("channelId", "URA")
|
|
_emit_ic(CVNTag.REGISTRA_ATENDIMENTO_FAIL, cvn_input_state)
|
|
_emit_ic(VEBTag.REGISTRA_ATENDIMENTO_FAIL, cvn_input_state)
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao registrar protocolo",
|
|
**result.metadata,
|
|
)
|
|
payload = _to_dict(result.data)
|
|
response = payload.get("response", {}) if isinstance(payload, dict) else {}
|
|
protocolo_id = ""
|
|
if isinstance(response, dict):
|
|
protocolo_id = str(
|
|
response.get("interactionProtocol")
|
|
or response.get("protocolId")
|
|
or response.get("protocolo_id")
|
|
or ""
|
|
)
|
|
|
|
if scenario == "invoice_explanation_aceite_fechado":
|
|
cvn_input_state = dict(input_state) if isinstance(input_state, dict) else {}
|
|
if not cvn_input_state.get("msisdn"):
|
|
cvn_input_state["msisdn"] = msisdn
|
|
if not cvn_input_state.get("ura_call_id"):
|
|
cvn_input_state["ura_call_id"] = _ic_base.get("uraCallId", "")
|
|
if not cvn_input_state.get("channel_id"):
|
|
cvn_input_state["channel_id"] = _ic_base.get("channelId", "URA")
|
|
_emit_ic(CVNTag.REGISTRA_ATENDIMENTO_OK, cvn_input_state)
|
|
_emit_ic(VEBTag.REGISTRA_ATENDIMENTO_OK, cvn_input_state)
|
|
|
|
output: dict[str, Any] = {"protocolo_id": protocolo_id}
|
|
if ic:
|
|
output["ic"] = ic
|
|
message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
if message_id:
|
|
output["message_id"] = message_id
|
|
|
|
return ActionResult.ok(output, **result.metadata)
|
|
|
|
|
|
@workflow_action("registrar_protocolo")
|
|
def register_protocol(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Registra protocolo e retorna dados para contestacao."""
|
|
msisdn = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"msisdn",
|
|
"Msisdn",
|
|
"MSISDN",
|
|
)
|
|
if not msisdn:
|
|
return ActionResult.fail("msisdn obrigatório para registrar_protocolo")
|
|
asset_id = re.sub(r"\D", "", msisdn)
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"cpf",
|
|
"customer_document",
|
|
"customerDocument",
|
|
"document",
|
|
),
|
|
)
|
|
scenario = str(
|
|
params.get("scenario")
|
|
or params.get("tipo_atendimento")
|
|
or "atendimento_geral"
|
|
).strip()
|
|
open_triplet = resolve_protocol_triplet(scenario, stage="open")
|
|
request_status = str(params.get("request_status", "Aberto")).strip() or "Aberto"
|
|
status = str(params.get("status", "Encaminhado")).strip() or "Encaminhado"
|
|
protocol_message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(msisdn),
|
|
"ani": str((input_state.get("ani") if isinstance(input_state, dict)
|
|
else "") or "").strip(),
|
|
"uraCallId": str((input_state.get("ura_call_id")
|
|
if isinstance(input_state, dict) else "") or "").strip(),
|
|
"agentId": "contas",
|
|
"channelId": str((input_state.get("channel_id") if isinstance(input_state, dict)
|
|
else "URA") or "URA").strip(),
|
|
}
|
|
|
|
rct_operation = (
|
|
RCTOperation.REG_ATEND_CONTESTACAO
|
|
if scenario.strip().lower() == "contestacao"
|
|
else RCTOperation.REG_CHAMADO_BO
|
|
)
|
|
|
|
result = runtime.factory.create_protocol_v2(
|
|
msisdn=msisdn,
|
|
asset_id=asset_id,
|
|
social_sec_no=social_sec_no,
|
|
channel="AIAGENTCR",
|
|
access_type="Controle",
|
|
interaction_protocol="",
|
|
flag_sms="",
|
|
source="CHAT",
|
|
reason1=open_triplet.reason1,
|
|
reason2=open_triplet.reason2,
|
|
reason3=open_triplet.reason3,
|
|
direction_contact="FROM-CLIENT",
|
|
type="CLIENTE",
|
|
request_flag=True,
|
|
request_status=request_status,
|
|
status=status,
|
|
service_request_notes="Solicitação via chat",
|
|
client_id="AIAGENTCR",
|
|
ic_context=_ic_base,
|
|
rct_operation=rct_operation,
|
|
message_id=protocol_message_id,
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao registrar protocolo",
|
|
**result.metadata,
|
|
)
|
|
|
|
payload = _to_dict(result.data)
|
|
response = payload.get("response", {}) if isinstance(payload, dict) else {}
|
|
protocolo_id = ""
|
|
if isinstance(response, dict):
|
|
protocolo_id = str(
|
|
response.get("interactionProtocol")
|
|
or response.get("protocolId")
|
|
or response.get("protocolo_id")
|
|
or ""
|
|
)
|
|
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
perfil = vars_state.get("consultar_perfil_fatura", {})
|
|
forma_pagamento = ""
|
|
if isinstance(perfil, dict):
|
|
forma_pagamento = str(
|
|
perfil.get("forma_pagamento")
|
|
or perfil.get("method_payment")
|
|
or ""
|
|
).strip()
|
|
|
|
sms_enviado = _get_sms_enviado_flag(vars_state)
|
|
sms_not_send_error = _get_sms_not_send_error_flag(vars_state)
|
|
|
|
output = {
|
|
"protocolo_id": protocolo_id,
|
|
"forma_pagamento": forma_pagamento,
|
|
"tipo_fatura": forma_pagamento,
|
|
"sms_sent": sms_enviado,
|
|
"sms_not_send_error": sms_not_send_error,
|
|
}
|
|
logger.info(
|
|
"contestacao.decisao_registrar_protocolo msisdn=%s protocolo=%s sms_enviado=%s",
|
|
str(msisdn),
|
|
protocolo_id,
|
|
sms_enviado,
|
|
)
|
|
|
|
return ActionResult.ok(output, **result.metadata)
|
|
|
|
|
|
@workflow_action("atualizar_status_sr")
|
|
def update_service_request_status(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Atualiza o status de Service Request no Siebel Pós."""
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
protocolo_id = ""
|
|
if isinstance(vars_state, dict):
|
|
protocolo_id = str(
|
|
vars_state.get("registrar_protocolo", {}).get("protocolo_id", "")
|
|
).strip()
|
|
if not protocolo_id:
|
|
protocolo_id = str(
|
|
vars_state.get("registrar_protocolo_inicio", {}).get(
|
|
"protocolo_id", ""
|
|
)
|
|
).strip()
|
|
|
|
notes = "Solicitação via chat Fechado"
|
|
if not protocolo_id:
|
|
return ActionResult.ok(
|
|
{"updated": False, "reason": "no_protocol", "protocol_closed": False}
|
|
)
|
|
close_triplet = resolve_protocol_triplet("contestacao", stage="close", context={})
|
|
msisdn = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"msisdn",
|
|
"Msisdn",
|
|
"MSISDN",
|
|
)
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(msisdn),
|
|
"ani": str(
|
|
(input_state.get("ani") if isinstance(input_state, dict) else "") or ""
|
|
).strip(),
|
|
"uraCallId": str(
|
|
(input_state.get("ura_call_id") if isinstance(input_state, dict) else "")
|
|
or ""
|
|
).strip(),
|
|
"agentId": "contas",
|
|
"channelId": str(
|
|
(input_state.get("channel_id") if isinstance(input_state, dict) else "URA")
|
|
or "URA"
|
|
).strip(),
|
|
}
|
|
contestacao_payload = vars_state.get("abrir_contestacao_cliente", {})
|
|
if not isinstance(contestacao_payload, dict):
|
|
contestacao_payload = {}
|
|
check_invoice_payload = vars_state.get("check_invoice_status", {})
|
|
if not isinstance(check_invoice_payload, dict):
|
|
check_invoice_payload = {}
|
|
invoice_number = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"current_invoice_number",
|
|
"invoice_number",
|
|
"invoiceNumber",
|
|
"invoice_id",
|
|
"invoiceId",
|
|
)
|
|
perfil_payload = vars_state.get("consultar_perfil_fatura", {})
|
|
if not isinstance(perfil_payload, dict):
|
|
perfil_payload = {}
|
|
expiration_date = (
|
|
_first_invoice_date_from_payload_by_id(
|
|
check_invoice_payload, invoice_number, "dueDate"
|
|
)
|
|
or _first_invoice_date_from_payload_by_id(
|
|
perfil_payload, invoice_number, "dueDate"
|
|
)
|
|
)
|
|
invoice_status = (
|
|
_first_invoice_status_from_payload(
|
|
check_invoice_payload, invoice_id=invoice_number
|
|
)
|
|
or _first_invoice_status_from_payload(
|
|
perfil_payload, invoice_id=invoice_number
|
|
)
|
|
)
|
|
if not invoice_status:
|
|
if invoice_number:
|
|
invoice_status = ""
|
|
else:
|
|
status_list = check_invoice_payload.get("invoice_statuses")
|
|
if not isinstance(status_list, list):
|
|
status_list = list(perfil_payload.get("invoice_statuses", []))
|
|
else:
|
|
status_list = list(status_list)
|
|
for raw_status in status_list:
|
|
status_text = str(raw_status or "").strip()
|
|
if status_text:
|
|
invoice_status = status_text
|
|
break
|
|
invoice_open_amount = _normalize_number_text(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_amount_open",
|
|
"invoiceAmountOpen",
|
|
"valor",
|
|
"amount",
|
|
),
|
|
)
|
|
invoice_total_amount = _normalize_number_text(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_amount",
|
|
"invoiceAmount",
|
|
"valor",
|
|
"amount",
|
|
),
|
|
default=invoice_open_amount,
|
|
)
|
|
if "contested_invoice_amount_open" in contestacao_payload:
|
|
invoice_open_amount = _normalize_number_text(
|
|
contestacao_payload.get("contested_invoice_amount_open"),
|
|
default="0",
|
|
)
|
|
invoice_total_amount = _normalize_number_text(
|
|
contestacao_payload.get("contested_invoice_amount"),
|
|
default=invoice_open_amount,
|
|
)
|
|
emission_date = (
|
|
_first_invoice_date_from_payload_by_id(
|
|
check_invoice_payload, invoice_number, "issueDate"
|
|
)
|
|
or _first_invoice_date_from_payload_by_id(
|
|
perfil_payload, invoice_number, "issueDate"
|
|
)
|
|
)
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"cpf",
|
|
"customer_document",
|
|
"customerDocument",
|
|
),
|
|
)
|
|
tracking_result = runtime.factory.create_tracking_activities(
|
|
msisdn=msisdn,
|
|
social_sec_no=social_sec_no,
|
|
protocol_number=protocolo_id,
|
|
invoice_emission_date=emission_date,
|
|
invoice_expiration_date=expiration_date,
|
|
invoice_number=invoice_number,
|
|
invoice_status=_map_invoice_status_to_state(invoice_status),
|
|
invoice_open_amount=invoice_open_amount,
|
|
invoice_total_amount=invoice_total_amount,
|
|
activity_type="Contestação",
|
|
activity_status="Aberto",
|
|
activity_id="",
|
|
).execute()
|
|
if not tracking_result.success:
|
|
return ActionResult.fail(
|
|
tracking_result.error or "Falha ao enviar trackingActivities",
|
|
**tracking_result.metadata,
|
|
)
|
|
|
|
status = "Fechado"
|
|
status_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
|
service_request_message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
result = runtime.factory.create_service_request_status(
|
|
channel=_CLOSE_SR_CHANNEL,
|
|
status=status,
|
|
protocol_number=protocolo_id,
|
|
reason1=close_triplet.reason1 or "Reclamação",
|
|
reason2=close_triplet.reason2 or "Conta",
|
|
reason3=close_triplet.reason3 or "Valor",
|
|
notes=notes,
|
|
msisdn=msisdn,
|
|
date=status_date,
|
|
client_id=_CLOSE_SR_CLIENT_ID,
|
|
ura_call_id=str((input_state.get("ura_call_id") if isinstance(input_state, dict)
|
|
else "") or "").strip(),
|
|
ic_context=_ic_base,
|
|
message_id=service_request_message_id,
|
|
rct_operation=RCTOperation.REG_ATEND_STATUS_SR,
|
|
).execute()
|
|
|
|
if not result.success:
|
|
_emit_vaa(VAATag.STATUS_SR_FAIL, _ic_base, gsm=msisdn)
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao atualizar status da SR",
|
|
**result.metadata,
|
|
)
|
|
_emit_vaa(VAATag.STATUS_SR_OK, _ic_base, gsm=msisdn)
|
|
|
|
payload = _to_dict(result.data)
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
if not isinstance(input_state, dict):
|
|
input_state = {}
|
|
if not isinstance(vars_state, dict):
|
|
vars_state = {}
|
|
|
|
sms_enviado = _get_sms_enviado_flag(vars_state)
|
|
sms_not_send_error = _get_sms_not_send_error_flag(vars_state)
|
|
|
|
_emit_vaa(VAATag.STATUS_SR_COM_SMS if sms_enviado else VAATag.STATUS_SR_SEM_SMS,
|
|
_ic_base, gsm=msisdn)
|
|
perfil_payload = vars_state.get("consultar_perfil_fatura", {})
|
|
check_payload = vars_state.get("check_invoice_status", {})
|
|
requires_sms = (
|
|
bool(
|
|
perfil_payload.get("requires_sms")
|
|
or perfil_payload.get("requer_sms")
|
|
)
|
|
if isinstance(perfil_payload, dict)
|
|
else False
|
|
)
|
|
has_open_bill = (
|
|
bool(
|
|
_has_open_bill_from_payload(
|
|
check_payload,
|
|
invoice_id=invoice_number,
|
|
default=False,
|
|
)
|
|
if isinstance(check_payload, dict)
|
|
else False
|
|
)
|
|
or bool(
|
|
_has_open_bill_from_payload(
|
|
perfil_payload,
|
|
invoice_id=invoice_number,
|
|
default=False,
|
|
)
|
|
if isinstance(perfil_payload, dict)
|
|
else False
|
|
)
|
|
)
|
|
payment_method = ""
|
|
invoice_status_texts: list[str] = []
|
|
if isinstance(perfil_payload, dict):
|
|
payment_method = str(
|
|
perfil_payload.get("method_payment")
|
|
or perfil_payload.get("forma_pagamento")
|
|
or ""
|
|
).strip()
|
|
invoice_status_texts.extend(
|
|
_invoice_status_texts_from_payload(perfil_payload, invoice_id=invoice_number)
|
|
)
|
|
if isinstance(check_payload, dict):
|
|
invoice_status_texts.extend(
|
|
_invoice_status_texts_from_payload(check_payload, invoice_id=invoice_number)
|
|
)
|
|
|
|
if not invoice_status_texts:
|
|
if isinstance(perfil_payload, dict):
|
|
invoice_status_texts.extend(_collect_invoice_status_texts(perfil_payload))
|
|
if isinstance(check_payload, dict):
|
|
invoice_status_texts.extend(_collect_invoice_status_texts(check_payload))
|
|
|
|
if not invoice_number:
|
|
has_open_bill = (
|
|
bool(check_payload.get("has_open_bill"))
|
|
if isinstance(check_payload, dict)
|
|
else bool(
|
|
perfil_payload.get("has_open_bill")
|
|
if isinstance(perfil_payload, dict)
|
|
else False
|
|
)
|
|
)
|
|
decision = _resolve_refund_and_resolution_decision(
|
|
payment_method=payment_method,
|
|
has_open_bill=has_open_bill,
|
|
invoice_status_texts=invoice_status_texts,
|
|
)
|
|
|
|
contestacao_payload = vars_state.get("abrir_contestacao_cliente", {})
|
|
if not isinstance(contestacao_payload, dict):
|
|
contestacao_payload = {}
|
|
check_invoice_payload = vars_state.get("check_invoice_status", {})
|
|
if not isinstance(check_invoice_payload, dict):
|
|
check_invoice_payload = {}
|
|
invoice_number = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"current_invoice_number",
|
|
"invoice_number",
|
|
"invoiceNumber",
|
|
"invoice_id",
|
|
"invoiceId",
|
|
)
|
|
perfil_payload = vars_state.get("consultar_perfil_fatura", {})
|
|
if not isinstance(perfil_payload, dict):
|
|
perfil_payload = {}
|
|
expiration_date = (
|
|
_first_invoice_date_from_payload_by_id(
|
|
check_invoice_payload, invoice_number, "dueDate"
|
|
)
|
|
or _first_invoice_date_from_payload_by_id(
|
|
perfil_payload, invoice_number, "dueDate"
|
|
)
|
|
)
|
|
invoice_status = (
|
|
_first_invoice_status_from_payload(
|
|
check_invoice_payload, invoice_id=invoice_number
|
|
)
|
|
or _first_invoice_status_from_payload(
|
|
perfil_payload, invoice_id=invoice_number
|
|
)
|
|
)
|
|
if not invoice_status:
|
|
if invoice_number:
|
|
invoice_status = ""
|
|
else:
|
|
status_list = check_invoice_payload.get("invoice_statuses")
|
|
if not isinstance(status_list, list):
|
|
status_list = list(perfil_payload.get("invoice_statuses", []))
|
|
else:
|
|
status_list = list(status_list)
|
|
for raw_status in status_list:
|
|
status_text = str(raw_status or "").strip()
|
|
if status_text:
|
|
invoice_status = status_text
|
|
break
|
|
invoice_open_amount = _normalize_number_text(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_amount_open",
|
|
"invoiceAmountOpen",
|
|
"valor",
|
|
"amount",
|
|
),
|
|
)
|
|
invoice_total_amount = _normalize_number_text(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_amount",
|
|
"invoiceAmount",
|
|
"valor",
|
|
"amount",
|
|
),
|
|
default=invoice_open_amount,
|
|
)
|
|
emission_date = (
|
|
_first_invoice_date_from_payload_by_id(
|
|
check_invoice_payload, invoice_number, "issueDate"
|
|
)
|
|
or _first_invoice_date_from_payload_by_id(
|
|
perfil_payload, invoice_number, "issueDate"
|
|
)
|
|
)
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"cpf",
|
|
"customer_document",
|
|
"customerDocument",
|
|
),
|
|
)
|
|
|
|
format_text = str(decision["format_text"])
|
|
result_output: dict[str, Any] = {
|
|
"updated": True,
|
|
"protocol_closed": status == "Fechado",
|
|
"format_text": format_text,
|
|
"resolution_type": str(decision["resolution_type"]),
|
|
"sms_enviado": sms_enviado,
|
|
"sms_sent": sms_enviado,
|
|
"sms_not_send_error": sms_not_send_error,
|
|
"requires_sms": requires_sms,
|
|
"has_open_bill": has_open_bill,
|
|
"decision_reason": str(decision["decision_reason"]),
|
|
"data_credito_proxima_fatura": str(
|
|
input_state.get("data_credito_proxima_fatura", "")
|
|
).strip(),
|
|
"contestation_id": str(
|
|
contestacao_payload.get("contestation_id", "")
|
|
).strip(),
|
|
"barcode": str(contestacao_payload.get("barcode", "")).strip(),
|
|
"items_response": contestacao_payload.get("items_response", []),
|
|
"payment_method": payment_method,
|
|
**payload,
|
|
}
|
|
result_output["body"] = contestacao_payload.get("body", {})
|
|
result_output["items_response"] = contestacao_payload.get("items_response", [])
|
|
result_output["contested_items"] = contestacao_payload.get("contested_items", [])
|
|
result_output["not_contested_items"] = contestacao_payload.get(
|
|
"not_contested_items", []
|
|
)
|
|
result_output["contested_invoice_amount_open"] = str(
|
|
contestacao_payload.get("contested_invoice_amount_open", "0")
|
|
)
|
|
result_output["contested_invoice_amount"] = str(
|
|
contestacao_payload.get("contested_invoice_amount", "0")
|
|
)
|
|
for key in _CONTESTATION_API_ERROR_KEYS:
|
|
if key in contestacao_payload:
|
|
result_output[key] = contestacao_payload[key]
|
|
|
|
if protocolo_id:
|
|
result_output["protocol_number"] = protocolo_id
|
|
result_output["sr"] = str(
|
|
contestacao_payload.get("sr", protocolo_id)
|
|
).strip()
|
|
|
|
return ActionResult.ok(result_output, **result.metadata)
|
|
|
|
|
|
@workflow_action("check_invoice_status")
|
|
def query_complete_invoices(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Consulta faturas do cliente (aberta/fechada)."""
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
invoice_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_id",
|
|
"invoiceId",
|
|
"current_invoice_number",
|
|
"currentInvoiceNumber",
|
|
"invoice_number",
|
|
"invoiceNumber",
|
|
)
|
|
cached = (
|
|
vars_state.get("check_invoice_status", {})
|
|
if isinstance(vars_state, dict)
|
|
else {}
|
|
)
|
|
if isinstance(cached, dict) and cached:
|
|
logger.info(
|
|
"contestacao.check_invoice_status cache_hit=True "
|
|
"reutilizando_resultado_complete_invoices"
|
|
)
|
|
return ActionResult.ok(
|
|
_inject_complete_invoice_context(dict(cached), invoice_id)
|
|
)
|
|
|
|
perfil_payload = (
|
|
vars_state.get("consultar_perfil_fatura", {})
|
|
if isinstance(vars_state, dict)
|
|
else {}
|
|
)
|
|
if isinstance(perfil_payload, dict) and "has_open_bill" in perfil_payload:
|
|
logger.info(
|
|
"contestacao.check_invoice_status cache_hit=True "
|
|
"origem=consultar_perfil_fatura"
|
|
)
|
|
payload = {
|
|
"has_open_bill": bool(perfil_payload.get("has_open_bill")),
|
|
"payment_method": str(
|
|
perfil_payload.get("method_payment")
|
|
or perfil_payload.get("forma_pagamento")
|
|
or ""
|
|
).strip(),
|
|
"payment_type_id": perfil_payload.get("payment_type_id"),
|
|
"invoice_statuses": perfil_payload.get("invoice_statuses", []),
|
|
"open_invoices": perfil_payload.get("open_invoices", []),
|
|
"closed_invoices": perfil_payload.get("closed_invoices", []),
|
|
"bar_code": str(perfil_payload.get("bar_code", "")).strip(),
|
|
"due_date": str(perfil_payload.get("due_date", "")).strip(),
|
|
"invoice_status": "",
|
|
}
|
|
return ActionResult.ok(
|
|
_inject_complete_invoice_context(payload, invoice_id)
|
|
)
|
|
|
|
result = runtime.factory.create_complete_invoices(
|
|
msisdn=str(params["msisdn"]),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao consultar faturas",
|
|
**result.metadata,
|
|
)
|
|
payload = _to_dict(result.data)
|
|
if isinstance(payload, dict):
|
|
payload = _inject_complete_invoice_context(payload, invoice_id)
|
|
return ActionResult.ok(payload, **result.metadata)
|
|
|
|
|
|
@workflow_action("abrir_contestacao_cliente")
|
|
def open_customer_contestation(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Abre contestação no endpoint /interactions/v1/customerContestation."""
|
|
msisdn = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"msisdn",
|
|
"Msisdn",
|
|
"MSISDN",
|
|
)
|
|
if not msisdn:
|
|
return ActionResult.fail("msisdn obrigatório para abrir_contestacao_cliente")
|
|
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
factory_config = getattr(runtime.factory, "_config", None)
|
|
contestation_user_id = str(
|
|
getattr(factory_config, "customer_contestation_user_id", "") or ""
|
|
).strip()
|
|
contestation_channel_id = contestation_user_id or "AIAGENTCR"
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": msisdn,
|
|
"ani": str((input_state.get("ani") if isinstance(input_state, dict)
|
|
else "") or "").strip(),
|
|
"uraCallId": str((input_state.get("ura_call_id")
|
|
or input_state.get("uraCallId")
|
|
if isinstance(input_state, dict) else "") or "").strip(),
|
|
"agentId": "contas",
|
|
"channelId": str((input_state.get("channel_id")
|
|
or input_state.get("channelId")
|
|
if isinstance(input_state, dict) else "URA") or "URA").strip(),
|
|
}
|
|
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
protocolo_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"protocolo_id",
|
|
"protocol_number",
|
|
"protocolNumber",
|
|
"sr",
|
|
)
|
|
if not protocolo_id and isinstance(vars_state, dict):
|
|
register_payload = vars_state.get("registrar_protocolo", {})
|
|
if isinstance(register_payload, dict):
|
|
protocolo_id = str(register_payload.get("protocolo_id", "")).strip()
|
|
if not protocolo_id:
|
|
return ActionResult.fail("Protocolo obrigatório para abrir contestação")
|
|
|
|
customer_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"customer_id",
|
|
"customerId",
|
|
)
|
|
if not customer_id:
|
|
return ActionResult.fail("customer_id obrigatório para abrir contestação")
|
|
|
|
invoice_number = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"current_invoice_number",
|
|
"currentInvoiceNumber",
|
|
"invoice_id",
|
|
"invoiceId",
|
|
"invoice_number",
|
|
"invoiceNumber",
|
|
)
|
|
if not invoice_number:
|
|
return ActionResult.fail("invoice_number obrigatório para abrir contestação")
|
|
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"customer_document",
|
|
"customerDocument",
|
|
),
|
|
)
|
|
|
|
invoice_payload = _extract_invoice_payload_from_state_or_params(state, params)
|
|
contestation_request_rules = _resolve_contestation_request_rules(
|
|
state,
|
|
params,
|
|
runtime,
|
|
msisdn=msisdn,
|
|
invoice_number=invoice_number,
|
|
invoice_payload=invoice_payload,
|
|
)
|
|
validation_error = str(contestation_request_rules.get("validation_error") or "")
|
|
if validation_error:
|
|
validation_log = contestation_request_rules.get("validation_log", [])
|
|
return ActionResult.fail(validation_error, validation_log=validation_log)
|
|
|
|
if contestation_request_rules.get("item_validation_skipped"):
|
|
logger.warning(
|
|
"contestacao.pro_rata.warning validacao_desativada "
|
|
"reason=pro_rata items=%s",
|
|
[
|
|
item.get("item_name", "")
|
|
for item in contestation_request_rules.get("items", [])
|
|
],
|
|
)
|
|
|
|
validation_log = contestation_request_rules.get("validation_log", [])
|
|
for item_log in validation_log:
|
|
logger.info("contestacao.validacao_item %s", item_log)
|
|
items = contestation_request_rules.get("items", [])
|
|
customer_status = str(contestation_request_rules.get("customer_status") or "")
|
|
invoice_status = str(contestation_request_rules.get("invoice_status") or "")
|
|
invoice_due_date = str(contestation_request_rules.get("invoice_due_date") or "")
|
|
invoice_amount_open = str(
|
|
contestation_request_rules.get("invoice_amount_open") or ""
|
|
)
|
|
invoice_amount = str(contestation_request_rules.get("invoice_amount") or "")
|
|
refund_option = str(contestation_request_rules.get("refund_option") or "")
|
|
manual_conta_certa_indicator = bool(
|
|
contestation_request_rules.get("manual_conta_certa_indicator")
|
|
)
|
|
tipo_atendimento = (
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"tipo_atendimento",
|
|
"tipoAtendimento",
|
|
)
|
|
.strip()
|
|
.lower()
|
|
)
|
|
rct_operation = RCTOperation.CONTESTACAO
|
|
if tipo_atendimento == "pro_rata":
|
|
rct_operation = RCTOperation.CONTESTACAO_CONTROLE
|
|
elif tipo_atendimento == "valor_divergente":
|
|
rct_operation = RCTOperation.CONTESTACAO_VALOR_DIVERGENTE
|
|
|
|
result = runtime.factory.create_customer_contestation(
|
|
msisdn=msisdn,
|
|
sr=protocolo_id,
|
|
social_sec_no=social_sec_no,
|
|
customer_id=customer_id,
|
|
invoice_number=invoice_number,
|
|
user_id=contestation_user_id or contestation_channel_id,
|
|
customer_id_current=_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"customer_id_current",
|
|
"customerIdCurrent",
|
|
),
|
|
customer_type=(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"customer_type",
|
|
"customerType",
|
|
)
|
|
or "2"
|
|
),
|
|
customer_status=customer_status,
|
|
invoice_status=invoice_status,
|
|
invoice_amount_open=invoice_amount_open,
|
|
invoice_amount=invoice_amount,
|
|
invoice_due_date=invoice_due_date,
|
|
contestation_type=(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"contestation_type",
|
|
"contestationType",
|
|
)
|
|
or "0"
|
|
),
|
|
adjust_reason=(
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"adjust_reason",
|
|
"adjustReason",
|
|
)
|
|
or "SERVICO_NAO_SOLICITADO"
|
|
),
|
|
observation=_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"observation",
|
|
"descricao",
|
|
),
|
|
refund_option=refund_option or "0",
|
|
double_refund=_normalize_bool(
|
|
_first_value_from_params_or_state(
|
|
state,
|
|
params,
|
|
"double_refund",
|
|
"doubleRefund",
|
|
),
|
|
default=False,
|
|
),
|
|
manual_conta_certa_indicator=manual_conta_certa_indicator,
|
|
items=items,
|
|
message_id=_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
),
|
|
client_id="AIAGENTCR",
|
|
ic_context=_ic_base,
|
|
rct_operation=rct_operation,
|
|
).execute()
|
|
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
_emit_vaa(VAATag.CONTESTA_VAS_AVULSO_FAIL, _ic_base, gsm=msisdn)
|
|
if _is_recoverable_contestation_api_error(result):
|
|
action_response = _build_contestation_api_error_output(
|
|
result,
|
|
protocolo_id=protocolo_id,
|
|
items=items,
|
|
)
|
|
logger.warning(
|
|
"contestacao.api_error_recoverable msisdn=%s sr=%s "
|
|
"description=%s",
|
|
msisdn,
|
|
protocolo_id,
|
|
action_response["contestation_error_description"],
|
|
)
|
|
return ActionResult.ok(action_response, **result.metadata)
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao abrir contestação",
|
|
**result.metadata,
|
|
)
|
|
|
|
payload = _to_dict(result.data)
|
|
response = payload.get("response", {}) if isinstance(payload, dict) else {}
|
|
if not isinstance(response, dict):
|
|
response = {}
|
|
response_body = response.get("body", {})
|
|
if not isinstance(response_body, dict):
|
|
response_body = response
|
|
if bool(response.get("dry_run")):
|
|
logger.info(
|
|
"contestacao.dry_run payload_preview=%s",
|
|
response.get("request_preview", {}),
|
|
)
|
|
resolved_barcode = str(
|
|
payload.get("barcode")
|
|
or payload.get("codigo_boleto")
|
|
or payload.get("codigoBoleto")
|
|
or payload.get("bar_code")
|
|
or payload.get("barCode")
|
|
or response.get("barcode")
|
|
or response.get("codigo_boleto")
|
|
or response.get("codigoBoleto")
|
|
or response.get("bar_code")
|
|
or response.get("barCode")
|
|
or response_body.get("barcode")
|
|
or response_body.get("codigo_boleto")
|
|
or response_body.get("codigoBoleto")
|
|
or response_body.get("bar_code")
|
|
or response_body.get("barCode")
|
|
or ""
|
|
).strip()
|
|
# Invalid placeholder: if every barcode digit is zero, do not trigger SMS.
|
|
only_digits = re.sub(r"\D", "", resolved_barcode)
|
|
if only_digits and set(only_digits) == {"0"}:
|
|
resolved_barcode = ""
|
|
if resolved_barcode:
|
|
_emit_vaa(VAATag.CODIGO_BARRAS_ELEGIVEL, _ic_base, gsm=msisdn)
|
|
|
|
items_response = payload.get("items_response", [])
|
|
if not isinstance(items_response, list):
|
|
items_response = response.get("itemsResponse", [])
|
|
if not isinstance(items_response, list):
|
|
items_response = response_body.get("itemsResponse", [])
|
|
if not isinstance(items_response, list):
|
|
items_response = []
|
|
items_response = [item for item in items_response if isinstance(item, dict)]
|
|
payload_contested_items = _extract_item_list(
|
|
payload,
|
|
"contested_items",
|
|
"contestedItems",
|
|
)
|
|
response_contested_items = _extract_item_list(
|
|
response_body,
|
|
"contested_items",
|
|
"contestedItems",
|
|
)
|
|
response_contested_items += _extract_item_list(
|
|
response,
|
|
"contested_items",
|
|
"contestedItems",
|
|
)
|
|
contested_items = [
|
|
item
|
|
for item in items_response
|
|
if _is_contested_item(item)
|
|
]
|
|
not_contested_items = [
|
|
item
|
|
for item in items_response
|
|
if not _is_contested_item(item)
|
|
]
|
|
known_contested_names: set[str] = {
|
|
_normalize_contestation_item_name(item.get("itemName"))
|
|
for item in contested_items
|
|
}
|
|
for item in payload_contested_items + response_contested_items:
|
|
normalized = _normalize_contestation_item_name(
|
|
item.get("itemName") or item.get("item_name")
|
|
)
|
|
if normalized and normalized not in known_contested_names:
|
|
contested_items.append(item)
|
|
known_contested_names.add(normalized)
|
|
|
|
contested_amounts = _sum_contested_item_amounts(
|
|
requested_items=items,
|
|
contested_items=contested_items,
|
|
)
|
|
action_response = {
|
|
"manual_conta_certa_indicator": bool(manual_conta_certa_indicator),
|
|
"contestation_id": str(
|
|
payload.get("contestation_id")
|
|
or response.get("contestationId")
|
|
or response_body.get("contestationId")
|
|
or ""
|
|
).strip(),
|
|
"sr": str(
|
|
payload.get("sr")
|
|
or response.get("sr")
|
|
or response_body.get("sr")
|
|
or protocolo_id
|
|
).strip(),
|
|
"barcode": resolved_barcode,
|
|
"validated_by_actions": True,
|
|
"validated_items": items,
|
|
"items_response": items_response,
|
|
"contested_items": contested_items,
|
|
"not_contested_items": not_contested_items,
|
|
**contested_amounts,
|
|
"body": response_body,
|
|
"response": response,
|
|
"validation_log": validation_log,
|
|
}
|
|
logger.info(
|
|
"contestacao.actions.response %s",
|
|
json.dumps(action_response, ensure_ascii=False, default=str),
|
|
)
|
|
return ActionResult.ok(action_response, **result.metadata)
|
|
|
|
|
|
@workflow_action("consultar_contrato_corte")
|
|
def check_billing_cutoff(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Consulta contrato e aplica regra de data de corte.
|
|
|
|
Regra:
|
|
- data da reclamação >= dia_corte -> data_corte_flag = "0"
|
|
- data da reclamação < dia_corte -> data_corte_flag = "1"
|
|
"""
|
|
msisdn = _first_text_from_params_or_state(
|
|
state, params, "msisdn", "Msisdn", "MSISDN",
|
|
)
|
|
if not msisdn:
|
|
return ActionResult.fail("msisdn obrigatório para consultar_contrato_corte")
|
|
|
|
result = runtime.factory.create_contrato(
|
|
msisdn=msisdn,
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao consultar contrato",
|
|
**result.metadata,
|
|
)
|
|
|
|
payload = _to_dict(result.data)
|
|
plano_familia_acesso_dependente = _is_plano_familia_dependent_access(
|
|
state,
|
|
params,
|
|
payload if isinstance(payload, dict) else None,
|
|
)
|
|
billing = payload.get("billing_profile", {})
|
|
if not isinstance(billing, dict):
|
|
billing = {}
|
|
date_info = billing.get("date", {})
|
|
if not isinstance(date_info, dict):
|
|
date_info = {}
|
|
dia_corte = 0
|
|
try:
|
|
dia_corte = int(date_info.get("cutoffDay") or 0)
|
|
except (ValueError, TypeError):
|
|
dia_corte = 0
|
|
|
|
due_date_raw = _resolve_contestation_due_date(state, params)
|
|
dia_vencimento = _extrair_dia_vencimento(due_date_raw)
|
|
|
|
data_reclamacao = datetime.now()
|
|
dia_reclamacao = data_reclamacao.day
|
|
cutoff_reference: datetime | None = None
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
invoice_number = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"invoice_id",
|
|
"invoiceId",
|
|
"current_invoice_number",
|
|
"currentInvoiceNumber",
|
|
"invoice_number",
|
|
"invoiceNumber",
|
|
)
|
|
check_invoice_payload = (
|
|
vars_state.get("check_invoice_status", {})
|
|
if isinstance(vars_state, dict)
|
|
else {}
|
|
)
|
|
normalized_cut_date = ""
|
|
if isinstance(check_invoice_payload, dict):
|
|
normalized_cut_date = _first_invoice_date_from_payload_by_id(
|
|
check_invoice_payload,
|
|
invoice_number,
|
|
"cutDate",
|
|
"cutoffDate",
|
|
)
|
|
|
|
if normalized_cut_date:
|
|
try:
|
|
parsed_cut_date = datetime.strptime(
|
|
normalized_cut_date,
|
|
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
)
|
|
except ValueError:
|
|
apos_data_corte, cutoff_reference = _resolve_billing_cutoff_reference(
|
|
dia_corte,
|
|
reference_datetime=data_reclamacao,
|
|
)
|
|
else:
|
|
apos_data_corte, cutoff_reference = _resolve_next_cutoff_from_cut_date(
|
|
parsed_cut_date,
|
|
reference_datetime=data_reclamacao,
|
|
)
|
|
else:
|
|
apos_data_corte, cutoff_reference = _resolve_billing_cutoff_reference(
|
|
dia_corte,
|
|
reference_datetime=data_reclamacao,
|
|
)
|
|
data_corte_referencia = (
|
|
cutoff_reference.strftime("%Y-%m-%d") if cutoff_reference else ""
|
|
)
|
|
|
|
data_corte_flag = "0" if apos_data_corte else "1"
|
|
abrir_conta_certa_manual = bool(apos_data_corte)
|
|
operador_decisao = "maior ou igual a" if apos_data_corte else "menor que"
|
|
acao_decisao = (
|
|
"abrir Conta Certa Manual"
|
|
if abrir_conta_certa_manual
|
|
else "seguir fluxo sem Conta Certa Manual"
|
|
)
|
|
decisao_conta_certa = (
|
|
f"Data atual [{data_reclamacao.strftime('%Y-%m-%d')}] e {operador_decisao} "
|
|
f"data de corte [{data_corte_referencia or '-'}], {acao_decisao}."
|
|
)
|
|
contestacao_sucesso = False
|
|
conta_certa_status = "NAO_SE_APLICA"
|
|
if isinstance(vars_state, dict):
|
|
contestacao_payload = vars_state.get("abrir_contestacao_cliente", {})
|
|
if isinstance(contestacao_payload, dict):
|
|
items_response = contestacao_payload.get("items_response", [])
|
|
if isinstance(items_response, list):
|
|
for item in items_response:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
raw_status = item.get("correctAccountStatus")
|
|
if raw_status is None:
|
|
raw_status = item.get("correctAccounStatus")
|
|
normalized = str(raw_status or "").strip().upper()
|
|
if normalized:
|
|
conta_certa_status = normalized
|
|
if normalized == "CRIAR":
|
|
contestacao_sucesso = True
|
|
break
|
|
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(msisdn),
|
|
"ani": str(
|
|
(input_state.get("ani") if isinstance(input_state, dict) else "") or ""
|
|
).strip(),
|
|
"uraCallId": str(
|
|
(input_state.get("ura_call_id") if isinstance(input_state, dict) else "")
|
|
or ""
|
|
).strip(),
|
|
"agentId": "contas",
|
|
"channelId": str(
|
|
(input_state.get("channel_id") if isinstance(input_state, dict) else "URA")
|
|
or "URA"
|
|
).strip(),
|
|
}
|
|
_emit_vaa(
|
|
VAATag.CONTESTACAO_OK if contestacao_sucesso else VAATag.CONTESTACAO_FAIL,
|
|
_ic_base,
|
|
gsm=msisdn,
|
|
extra_metadata={
|
|
"decisaoContaCerta": decisao_conta_certa,
|
|
"dataCorteReferencia": data_corte_referencia,
|
|
"dataReclamacao": data_reclamacao.strftime("%Y-%m-%d"),
|
|
"abrirContaCertaManual": abrir_conta_certa_manual,
|
|
"diaCorte": dia_corte,
|
|
"diaReclamacao": dia_reclamacao,
|
|
},
|
|
)
|
|
|
|
logger.info(
|
|
"consultar_contrato_corte msisdn=%s dia_corte=%s dia_reclamacao=%s "
|
|
"data_corte_referencia=%s apos_data_corte=%s contestacao_sucesso=%s "
|
|
"conta_certa_status=%s dia_vencimento=%s",
|
|
msisdn,
|
|
dia_corte,
|
|
dia_reclamacao,
|
|
data_corte_referencia,
|
|
apos_data_corte,
|
|
contestacao_sucesso,
|
|
conta_certa_status,
|
|
dia_vencimento,
|
|
)
|
|
logger.info("consultar_contrato_corte decisao_conta_certa=%s", decisao_conta_certa)
|
|
|
|
return ActionResult.ok(
|
|
{
|
|
"apos_data_corte": apos_data_corte,
|
|
"data_corte_flag": data_corte_flag,
|
|
"abrir_conta_certa_manual": abrir_conta_certa_manual,
|
|
"decisao_conta_certa": decisao_conta_certa,
|
|
"contestation_success": contestacao_sucesso,
|
|
"conta_certa_status": conta_certa_status,
|
|
"plano_familia_acesso_dependente": plano_familia_acesso_dependente,
|
|
"dia_corte": dia_corte,
|
|
"dia_vencimento": dia_vencimento,
|
|
},
|
|
**result.metadata,
|
|
)
|
|
|
|
|
|
@workflow_action("abrir_sr_conta_certa_manual")
|
|
def open_manual_conta_certa_sr(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Abre SR de Conta Certa Manual quando contestação ocorre após data de corte."""
|
|
msisdn = _first_text_from_params_or_state(
|
|
state, params, "msisdn", "Msisdn", "MSISDN",
|
|
)
|
|
if not msisdn:
|
|
return ActionResult.fail("msisdn obrigatório para abrir_sr_conta_certa_manual")
|
|
|
|
social_sec_no = re.sub(
|
|
r"\D", "",
|
|
_first_text_from_params_or_state(
|
|
state, params, "social_sec_no", "socialSecNo",
|
|
),
|
|
)
|
|
dia_vencimento = str(params.get("dia_vencimento", "")).strip()
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
if not dia_vencimento and isinstance(vars_state, dict):
|
|
consultar_contrato_corte = vars_state.get("consultar_contrato_corte", {})
|
|
if isinstance(consultar_contrato_corte, dict):
|
|
dia_vencimento = str(
|
|
consultar_contrato_corte.get("dia_vencimento", "")
|
|
).strip()
|
|
if not dia_vencimento:
|
|
dia_vencimento = _resolve_contestation_due_date(state, params)
|
|
dia_vencimento = _extrair_dia_vencimento(dia_vencimento)
|
|
open_triplet = resolve_protocol_triplet(
|
|
"conta_certa_manual",
|
|
stage="open",
|
|
context={"dia_vencimento": dia_vencimento},
|
|
)
|
|
request_status = (str(params.get("request_status", "Encaminhado")).strip()
|
|
or "Encaminhado")
|
|
status = str(params.get("status", "Encaminhado")).strip() or "Encaminhado"
|
|
protocol_message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
service_request_notes = (
|
|
str(params.get("service_request_notes", "")).strip()
|
|
or "Solicitação via chat"
|
|
)
|
|
contestacao_payload = (
|
|
vars_state.get("abrir_contestacao_cliente", {})
|
|
if isinstance(vars_state, dict)
|
|
else {}
|
|
)
|
|
contested_items = (
|
|
contestacao_payload.get("contested_items", [])
|
|
if isinstance(contestacao_payload, dict)
|
|
else []
|
|
)
|
|
contested_services: list[str] = []
|
|
if isinstance(contested_items, list):
|
|
for item in contested_items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
service_name = str(
|
|
item.get("itemName")
|
|
or item.get("item_name")
|
|
or item.get("name")
|
|
or ""
|
|
).strip()
|
|
if service_name and service_name not in contested_services:
|
|
contested_services.append(service_name)
|
|
if contested_services:
|
|
contested_summary = f"Serviços contestados: {' | '.join(contested_services)}"
|
|
service_request_notes = f"{service_request_notes} | {contested_summary}"
|
|
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(msisdn),
|
|
"ani": str(
|
|
(input_state.get("ani") if isinstance(input_state, dict) else "") or ""
|
|
).strip(),
|
|
"uraCallId": str(
|
|
(input_state.get("ura_call_id") if isinstance(input_state, dict) else "")
|
|
or ""
|
|
).strip(),
|
|
"agentId": "contas",
|
|
"channelId": str(
|
|
(input_state.get("channel_id") if isinstance(input_state, dict) else "URA")
|
|
or "URA"
|
|
).strip(),
|
|
}
|
|
|
|
result = runtime.factory.create_protocol_v2(
|
|
msisdn=msisdn,
|
|
interaction_protocol="",
|
|
social_sec_no=social_sec_no,
|
|
source="CHAT",
|
|
reason1=open_triplet.reason1 or "Processo Interno",
|
|
reason2=open_triplet.reason2 or "Conta",
|
|
reason3=open_triplet.reason3 or "Valor",
|
|
direction_contact="FROM-CLIENT",
|
|
type="CLIENTE",
|
|
request_flag=True,
|
|
request_status=request_status,
|
|
status=status,
|
|
service_request_notes=service_request_notes,
|
|
channel="AIAGENTCR",
|
|
client_id="AIAGENTCR",
|
|
ic_context=_ic_base,
|
|
rct_operation=RCTOperation.REG_CHAMADO_BO,
|
|
message_id=protocol_message_id,
|
|
).execute()
|
|
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha ao abrir SR Conta Certa Manual",
|
|
**result.metadata,
|
|
)
|
|
|
|
payload = _to_dict(result.data)
|
|
response = payload.get("response", {}) if isinstance(payload, dict) else {}
|
|
if not isinstance(response, dict):
|
|
response = {}
|
|
sr_conta_certa_id = str(
|
|
response.get("interactionProtocol")
|
|
or response.get("protocolId")
|
|
or response.get("protocolo_id")
|
|
or ""
|
|
).strip()
|
|
protocol_status = status
|
|
|
|
logger.info(
|
|
"abrir_sr_conta_certa_manual msisdn=%s dia_venc=%s sr=%s",
|
|
msisdn, dia_vencimento, sr_conta_certa_id,
|
|
)
|
|
|
|
return ActionResult.ok(
|
|
{
|
|
"dia_vencimento": dia_vencimento,
|
|
"status": protocol_status,
|
|
},
|
|
**result.metadata,
|
|
)
|
|
|
|
|
|
@workflow_action("enviar_sms")
|
|
def send_sms(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Envia SMS via invoices/v1/smsBarcode."""
|
|
dados = params.get("dados", {})
|
|
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
|
contestacao_payload = (
|
|
vars_state.get("abrir_contestacao_cliente", {})
|
|
if isinstance(vars_state, dict)
|
|
else {}
|
|
)
|
|
barcode = (
|
|
str(contestacao_payload.get("barcode", "")).strip()
|
|
if isinstance(contestacao_payload, dict)
|
|
else ""
|
|
)
|
|
# Quando houver barcode retornado pela contestação, o SMS deve
|
|
# obrigatoriamente carregar esse código no corpo da mensagem.
|
|
if barcode:
|
|
message = f"Codigo do boleto: {barcode}"
|
|
else:
|
|
message = str(
|
|
params.get("message", "")
|
|
or dados.get("texto_sms", "")
|
|
).strip()
|
|
long_url = str(
|
|
params.get("long_url")
|
|
or params.get("longURL")
|
|
or "https://meutim.com.br"
|
|
).strip()
|
|
notify_url = str(
|
|
params.get("notify_url")
|
|
or params.get("notifyURL")
|
|
or "http://10.114.200.57:9003/teste"
|
|
).strip()
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": str(params["msisdn"]),
|
|
"ani": str((input_state.get("ani") if isinstance(input_state, dict)
|
|
else "") or "").strip(),
|
|
"uraCallId": str((input_state.get("ura_call_id")
|
|
or input_state.get("uraCallId")
|
|
if isinstance(input_state, dict) else "") or "").strip(),
|
|
"agentId": "contas",
|
|
"channelId": str((input_state.get("channel_id")
|
|
or input_state.get("channelId")
|
|
if isinstance(input_state, dict) else "URA") or "URA").strip()
|
|
}
|
|
|
|
with _start_sms_observation(
|
|
msisdn=str(params["msisdn"]),
|
|
has_barcode=bool(barcode),
|
|
long_url=long_url,
|
|
notify_url=notify_url,
|
|
) as sms_observation:
|
|
try:
|
|
result = runtime.factory.create_sms(
|
|
msisdn=str(params["msisdn"]),
|
|
message=message,
|
|
sender_address=str(params.get("sender_address", "324")),
|
|
sender_name=str(params.get("sender_name", "TIM Brasil")),
|
|
long_url=long_url,
|
|
notify_url=notify_url,
|
|
ic_context=_ic_base,
|
|
).execute()
|
|
command_failed = _result_failed_or_missing_data(result, state=state)
|
|
result_error = result.error or "Falha ao enviar SMS"
|
|
result_metadata = result.metadata
|
|
payload = (
|
|
_to_dict(result.data)
|
|
if getattr(result, "data", None) is not None
|
|
else {}
|
|
)
|
|
except Exception as exc:
|
|
logger.exception("Erro inesperado ao enviar SMS")
|
|
command_failed = True
|
|
result_error = f"Falha ao enviar SMS: {exc}"
|
|
result_metadata = {}
|
|
payload = {}
|
|
|
|
ic_context_sms_only = dict(_ic_base)
|
|
if str(ic_context_sms_only.get("channelId", "")).strip().upper() == "URA":
|
|
ic_context_sms_only["channelId"] = "SMS"
|
|
|
|
if not isinstance(payload, dict):
|
|
payload = {}
|
|
|
|
resource_url = str(payload.get("resource_url", "") or payload.get("resourceUrl", "")).strip()
|
|
if not resource_url:
|
|
resource_url = ""
|
|
|
|
if command_failed or not resource_url:
|
|
_emit_vaa(VAATag.SMS_FAIL, ic_context_sms_only, gsm=str(params["msisdn"]))
|
|
_emit_vaa(
|
|
"sms_nao_enviado_erro",
|
|
ic_context_sms_only,
|
|
gsm=str(params["msisdn"]),
|
|
extra_metadata={"error": result_error},
|
|
)
|
|
payload.update(
|
|
{
|
|
"sms_enviado": False,
|
|
"sms_sent": False,
|
|
"sms_not_send_error": True,
|
|
"sms_nao_enviado_erro": True,
|
|
"resource_url": resource_url,
|
|
"error": result_error,
|
|
}
|
|
)
|
|
_update_sms_observation(
|
|
sms_observation,
|
|
output={
|
|
"success": False,
|
|
"sms_enviado": False,
|
|
"sms_sent": False,
|
|
"sms_not_send_error": True,
|
|
"error": result_error,
|
|
},
|
|
level="ERROR",
|
|
status_message=result_error,
|
|
)
|
|
return ActionResult.ok(
|
|
payload,
|
|
**result_metadata,
|
|
)
|
|
_emit_vaa(VAATag.SMS_OK, _ic_base, gsm=str(params["msisdn"]))
|
|
payload["resource_url"] = resource_url
|
|
payload["sms_enviado"] = True
|
|
payload["sms_sent"] = True
|
|
payload["sms_not_send_error"] = False
|
|
payload["sms_nao_enviado_erro"] = False
|
|
_update_sms_observation(
|
|
sms_observation,
|
|
output={
|
|
"success": True,
|
|
"sms_enviado": True,
|
|
"sms_sent": True,
|
|
"sms_not_send_error": False,
|
|
"resource_url_present": bool(resource_url),
|
|
},
|
|
)
|
|
return ActionResult.ok(payload, **result_metadata)
|
|
|
|
|
|
__all__ = [
|
|
'query_profile_bill',
|
|
'register_protocol_start',
|
|
'register_protocol',
|
|
'update_service_request_status',
|
|
'query_complete_invoices',
|
|
'open_customer_contestation',
|
|
'check_billing_cutoff',
|
|
'open_manual_conta_certa_sr',
|
|
'send_sms'
|
|
]
|