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