Files
first_contas/legacy_reference/workflows/actions/contestacao/actions.py
2026-06-16 20:54:49 -03:00

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'
]