from __future__ import annotations import logging import re from decimal import Decimal from typing import Any from agente_contas_tim.protocol_triplets import resolve_protocol_triplet from agente_contas_tim.text_utils import vocalize_digits 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 ( _final_msisdn, _first_text_from_params_or_state, _first_value_from_params_or_state, _format_amount, _normalize_number_text, _result_failed_or_missing_data, _runtime_llm_callbacks, _runtime_llm_metadata, _to_dict, _utc_now_iso, ) from agente_contas_tim.workflows.actions.invoice_explanation.helpers import _normalizar_mensagem_voz from agente_contas_tim.workflows.actions.pro_rata.helpers import ( _build_pro_rata_ajuste_recusado_msg, _build_pro_rata_contestacao_tool_payload, _build_pro_rata_contestation_items, _build_pro_rata_controle_msg, _build_pro_rata_human_validation_text, _build_pro_rata_oferta_ajuste_msg, _build_pro_rata_sem_controle_msg, _decimal_from_any, _extract_pro_rata_contestation_context, _fetch_invoice_payload_pro_rata, _money, _resolve_plano_controle, _resolve_pro_rata_liquid_value, _resolve_pro_rata_period_days, ) logger = logging.getLogger("agente_contas_tim.workflows.actions.tim_actions") @workflow_action("formatar_pro_rata") def formatar_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Reescreve a mensagem crua do pro-rata aplicando vocalizacao para TTS. Recebe `mensagem_base` (obrigatorio) e, opcionalmente, `trailer_override` para forcar uma frase final (ex.: caminho de reperguntar). Em caso de falha do LLM, faz fallback para a mensagem crua para nao bloquear o fluxo. """ mensagem_base = str(params.get("mensagem_base", "") or "").strip() if not mensagem_base: return ActionResult.fail( "mensagem_base obrigatoria para formatar_pro_rata" ) trailer_override = str(params.get("trailer_override", "") or "").strip() mensagem = "" if runtime.llm_gateway is not None: try: llm_result = runtime.llm_gateway.execute( capability_id="fluxo_pro_rata_reescrita", variables={ "mensagem_base": mensagem_base, "trailer_override": trailer_override, }, user_text=mensagem_base, callbacks=_runtime_llm_callbacks(runtime), tags=["workflow_action"], metadata=_runtime_llm_metadata(runtime), ) mensagem = str(getattr(llm_result, "content", "") or "").strip() mensagem = _normalizar_mensagem_voz( mensagem, trailer_override=trailer_override, ) except Exception: logger.exception( "formatar_pro_rata: falha ao invocar capability " "fluxo_pro_rata_reescrita" ) if not mensagem: mensagem = ( f"{mensagem_base} {trailer_override}".strip() if trailer_override else mensagem_base ) return ActionResult.ok({"mensagem": mensagem}) @workflow_action("preparar_pro_rata") def preparar_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Monta mensagem inicial do pro-rata e decide se pausa por Plano Controle.""" raw_planos = params.get("planos") planos = [p for p in (raw_planos or []) if isinstance(p, dict)] has_controle = bool(params.get("has_plano_controle", False)) if len(planos) != 2 or not all( str(p.get("desc", "")).strip() for p in planos ): return ActionResult.fail( "pro_rata exige exatamente dois planos com 'desc' preenchido." ) if not has_controle: return ActionResult.ok( { "await_user_input": False, "mensagem": _build_pro_rata_sem_controle_msg(planos), "mensagem_pos_aceite": "", "mensagem_reperguntar_esclarecimento": "", "mensagem_oferta_ajuste": "", "mensagem_reperguntar_ajuste": "", "mensagem_ajuste_recusado": "", "has_plano_controle": False, } ) mensagem = _build_pro_rata_controle_msg(planos) mensagem_oferta_ajuste = _build_pro_rata_oferta_ajuste_msg() return ActionResult.ok( { "await_user_input": True, "mensagem": mensagem, "mensagem_pos_aceite": "", "mensagem_reperguntar_esclarecimento": ( "Para seguirmos, preciso de uma confirmacao. " + mensagem ), "mensagem_oferta_ajuste": mensagem_oferta_ajuste, "mensagem_reperguntar_ajuste": ( "Para seguirmos, preciso de uma confirmacao. " + mensagem_oferta_ajuste ), "mensagem_ajuste_recusado": _build_pro_rata_ajuste_recusado_msg(), "has_plano_controle": True, } ) @workflow_action("calcular_itens_contestacao_pro_rata") def calcular_itens_contestacao_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Calcula itens/valores a contestar no fluxo de pro rata.""" raw_planos = _first_value_from_params_or_state(state, params, "planos") planos = [p for p in (raw_planos or []) if isinstance(p, dict)] if not planos: return ActionResult.fail( "planos obrigatorio para calcular itens de contestacao do pro_rata" ) items: list[dict[str, Any]] = [] total = Decimal("0") for plano in planos: item_name = str( plano.get("desc") or plano.get("name") or plano.get("item_name") or "" ).strip() if not item_name: continue valor = _normalize_number_text( plano.get("value") or plano.get("valor") or plano.get("claimed_amount") or "0" ) valor_decimal = Decimal(valor) total += valor_decimal items.append( { "item_name": item_name, "item_type": "PRO_RATA", "claimed_amount": valor, "validated_amount": valor, } ) if not items: return ActionResult.fail( "Nao foi possivel calcular itens de contestacao a partir dos planos." ) total_txt = _normalize_number_text(format(total, "f")) return ActionResult.ok( { "items": items, "invoice_amount_open": total_txt, "invoice_amount": total_txt, } ) @workflow_action("executar_contestacao_plano_controle") def executar_contestacao_plano_controle( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Delega para o workflow/tool contestacao_tool apos calcular os itens.""" context, context_error = _extract_pro_rata_contestation_context(state, params) if context is None: return ActionResult.fail( context_error or "Dados invalidos para contestacao de pro_rata" ) if runtime.workflow_runner is None: return ActionResult.fail( "workflow_runner nao configurado para delegar contestacao_tool" ) contestacao_payload = _build_pro_rata_contestacao_tool_payload( state, params, context=context, ) contestacao_result = runtime.workflow_runner( "contestacao_tool", contestacao_payload, None, None, ) if contestacao_result.status != "COMPLETED" or not isinstance( contestacao_result.data, dict ): return ActionResult.fail( str(contestacao_result.error or "Falha ao abrir contestacao de pro_rata"), **( contestacao_result.metadata if isinstance(contestacao_result.metadata, dict) else {} ), ) contest_payload = contestacao_result.data contestacao_lines = ( contest_payload.get("linhas", []) if isinstance(contest_payload.get("linhas"), list) else [] ) protocolo_id = "" if contestacao_lines and isinstance(contestacao_lines[0], dict): protocolo_id = str(contestacao_lines[0].get("protocolo_id", "")).strip() if not protocolo_id: protocolo_id = str( contest_payload.get("protocol_number") or contest_payload.get("protocolo_id") or "" ).strip() mensagem = str(contest_payload.get("mensagem", "")).strip() if not mensagem: mensagem = ( "Contestacao de pro rata registrada com sucesso. " f"Protocolo: {protocolo_id or 'n/a'}." ) plano_names = [ str(item.get("item_name", "")).strip() for item in context["items"] if isinstance(item, dict) and str(item.get("item_name", "")).strip() ] transaction_id = str( contest_payload.get("contestation_id") or contest_payload.get("sr") or protocolo_id or "" ).strip() audit = { "plano_cliente": ", ".join(plano_names), "valor_calculado_credito": str(context["invoice_amount"]), "data_hora_contestacao": _utc_now_iso(), "transaction_id_ajuste": transaction_id, "mensagem_confirmacao": mensagem, } raw_devolucao = params.get("devolucao") devolucao = dict(raw_devolucao) if isinstance(raw_devolucao, dict) else {} devolucao.setdefault("items", context["items"]) devolucao["invoice_amount_open"] = context["invoice_amount_open"] devolucao["invoice_amount"] = context["invoice_amount"] format_text = str( contest_payload.get("format_text") or ( "sms" if bool( contest_payload.get("sms_enviado") or contest_payload.get("sms_sent") ) else "conta_futura" ) ).strip() devolucao["format_text"] = format_text devolucao["resolution_type"] = str( contest_payload.get("resolution_type") or ("new_boleto" if format_text == "sms" else "credit_bill") ).strip() devolucao["sms_enviado"] = bool( contest_payload.get("sms_enviado") or contest_payload.get("sms_sent") or format_text == "sms" ) devolucao["sms_sent"] = devolucao["sms_enviado"] for key in ( "decision_reason", "data_credito_proxima_fatura", "barcode", "items_response", "sr_conta_certa_id", ): if key in contest_payload: devolucao[key] = contest_payload[key] if protocolo_id: devolucao["protocolo_id"] = protocolo_id if transaction_id: devolucao["transaction_id_ajuste"] = transaction_id logger.info( "pro_rata.contestacao.audit msisdn_final=%s plano_cliente=%s valor_credito=%s transaction_id=%s data_hora=%s mensagem_confirmacao=%s", _final_msisdn(str(context["msisdn"])), audit["plano_cliente"], audit["valor_calculado_credito"], audit["transaction_id_ajuste"], audit["data_hora_contestacao"], audit["mensagem_confirmacao"], ) return ActionResult.ok( { "success": True, "mensagem": mensagem, "protocolo_id": protocolo_id, "contestation_id": str( contest_payload.get("contestation_id") or "" ).strip(), "sr": str( contest_payload.get("sr") or protocolo_id ).strip(), "items": context["items"], "invoice_amount_open": context["invoice_amount_open"], "invoice_amount": context["invoice_amount"], "devolucao": devolucao, "audit": audit, "response": contest_payload, }, **( contestacao_result.metadata if isinstance(contestacao_result.metadata, dict) else {} ), ) @workflow_action("definir_devolucao_ajuste_pro_rata") def definir_devolucao_ajuste_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Calcula devolucao do ajuste pro-rata para Plano Controle. A base do pro-rata e o valor liquido do Plano Controle. A devolucao e alocada nos itens do DANFE vinculados ao mesmo plano. """ raw_planos = _first_value_from_params_or_state(state, params, "planos") planos = [p for p in (raw_planos or []) if isinstance(p, dict)] if len(planos) != 2: return ActionResult.fail( "pro_rata exige exatamente dois planos para calcular devolucao." ) resolved = _resolve_plano_controle(planos) if resolved is None: return ActionResult.fail( "Nao foi possivel identificar exatamente um Plano Controle." ) plano_controle, outro_plano = resolved valor_liquido = _resolve_pro_rata_liquid_value(plano_controle) if valor_liquido is None or valor_liquido <= 0: return ActionResult.fail( "Valor liquido do Plano Controle ausente ou invalido." ) invoice_payload, error = _fetch_invoice_payload_pro_rata(state, params, runtime) if error: return ActionResult.fail(error) period_days = _resolve_pro_rata_period_days( outro_plano=outro_plano, invoice_payload=invoice_payload, invoice_period=_first_text_from_params_or_state( state, params, "invoice_period", "invoicePeriod", "periodo_fatura", "periodoFatura", ), invoice_emissao=_first_text_from_params_or_state( state, params, "invoice_emissao", "invoiceEmissao", "emissao_fatura", "emissaoFatura", ), ) if period_days is None: return ActionResult.fail( "Periodo da fatura ou dos planos ausente ou invalido para calcular pro-rata." ) dias_ciclo, dias_controle = period_days valor_liquido = _money(valor_liquido) valor_usado = (valor_liquido / Decimal(dias_ciclo)) * Decimal(dias_controle) valor_devolver = _money(valor_liquido - valor_usado) danfe = invoice_payload.get("DANFE-COM") if not isinstance(danfe, dict) or not danfe: return ActionResult.fail("DANFE-COM nao encontrado na fatura recuperada.") items, restante = _build_pro_rata_contestation_items( danfe=danfe, plano_controle=plano_controle, valor_devolver=valor_devolver, ) if restante > 0: return ActionResult.fail( "Itens do DANFE insuficientes para cobrir devolucao de " f"R$ {_format_amount(restante)}." ) total_validado = sum( ( _decimal_from_any(item.get("validatedAmount")) or Decimal("0") ) for item in items if isinstance(item, dict) ) logger.info( "definir_devolucao_ajuste_pro_rata: items=%s total_validado=%s", items, _format_amount(_money(total_validado)), ) total_validado_txt = _normalize_number_text(format(_money(total_validado), "f")) texto_validacao_humana = _build_pro_rata_human_validation_text( plano_controle=plano_controle, valor_liquido=valor_liquido, dias_ciclo=dias_ciclo, dias_controle=dias_controle, valor_usado=valor_usado, valor_devolver=valor_devolver, items=items, ) return ActionResult.ok( { "items": items, "invoice_amount_open": total_validado_txt, "invoice_amount": total_validado_txt, "texto_validacao_humana": texto_validacao_humana, } ) @workflow_action("executar_devolucao_pro_rata") def executar_devolucao_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """A implementar: dispara devolucao do ajuste pro-rata. Por enquanto repassa o payload de definir_devolucao para o proximo no. """ devolucao = params.get("devolucao") if not isinstance(devolucao, dict): devolucao = {} items = devolucao.get("items") if not isinstance(items, list): items = [] logger.warning( "executar_devolucao_pro_rata: a implementar (mock ativo)" ) output = dict(devolucao) output["items"] = items output["mocked"] = True return ActionResult.ok(output) @workflow_action("orientar_pagamento_pro_rata") def orientar_pagamento_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Orienta o cliente sobre pagamento apos definir o credito.""" devolucao = params.get("devolucao") if not isinstance(devolucao, dict): devolucao = {} raw_items = devolucao.get("items") items = raw_items if isinstance(raw_items, list) else [] valor = Decimal("0") plano_nome = "" for item in items: if not isinstance(item, dict): continue if not plano_nome: plano_nome = str( item.get("itemName") or item.get("item_name") or "" ).strip() item_value = _decimal_from_any( item.get("validatedAmount") if item.get("validatedAmount") is not None else item.get("validated_amount") ) if item_value is not None: valor += item_value valor_txt = _format_amount(_money(valor)) plano_txt = f" {plano_nome}" if plano_nome else "" protocolo_id = str(devolucao.get("protocolo_id", "")).strip() protocolo_txt = ( f" Seu numero de protocolo e {vocalize_digits(protocolo_id)}." if protocolo_id else "" ) data_credito = str( devolucao.get("data_credito_proxima_fatura", "") ).strip() data_credito_txt = ( f" na fatura com vencimento em {data_credito}, considerando o seu ciclo de faturamento" if data_credito else " em uma proxima fatura" ) sms_codigo_txt = ( "com o codigo de barras atualizado" if str(devolucao.get("barcode", "")).strip() else "com as orientacoes para pagamento" ) mensagem = ( ( "Realizei a contestacao da fatura considerando o valor " f"proporcional do Plano Controle{plano_txt}. O valor " f"contestado, de R$ {valor_txt}, foi retirado da sua fatura. " f"Enviamos uma mensagem {sms_codigo_txt}, com prazo de 10 dias " f"para pagamento.{protocolo_txt}" ) if str(devolucao.get("format_text", "")).strip() == "sms" else ( "Realizei a contestacao considerando o valor proporcional do " f"Plano Controle{plano_txt}. O valor contestado, de " f"R$ {valor_txt}, ficou registrado como credito{data_credito_txt}." f"{protocolo_txt}" ) ).strip() return ActionResult.ok({"mensagem": mensagem, "devolucao": devolucao}) @workflow_action("registrar_atendimento_pro_rata") def registrar_atendimento_pro_rata( state: dict[str, Any], params: dict[str, Any], runtime: WorkflowRuntimeContext, ) -> ActionResult: """Registra protocolo de fechamento do atendimento pro rata.""" mensagem_base = str(params.get("mensagem_base", "")).strip() caminho = str(params.get("caminho", "")).strip() devolucao = params.get("devolucao") if not isinstance(devolucao, dict): devolucao = {} msisdn = _first_text_from_params_or_state( state, params, "msisdn", "Msisdn", "MSISDN", ) if not msisdn: return ActionResult.fail("msisdn obrigatorio para registrar_atendimento_pro_rata") social_sec_no = re.sub( r"\D", "", _first_text_from_params_or_state( state, params, "social_sec_no", "socialSecNo", "cpf", "customer_document", "customerDocument", "document", ), ) caminho_norm = caminho.strip().lower() scenario = ( "pro_rata_reclamacao" if caminho_norm == "nao_aceitou" else "pro_rata_mensalidade" ) close_triplet = resolve_protocol_triplet(scenario, stage="close") reason1 = close_triplet.reason1 or "Informação" reason2 = close_triplet.reason2 or "Conta" reason3 = close_triplet.reason3 or "Mensalidade" request_status = str(params.get("request_status", "Fechado")).strip() or "Fechado" status = str(params.get("status", "CLOSED")).strip() or "CLOSED" ic = str(params.get("ic", "")).strip() message_id = _first_text_from_params_or_state( state, params, "message_id", "messageId", ) register_result = runtime.factory.create_protocol_v2( msisdn=msisdn, interaction_protocol="", flag_sms=True, social_sec_no=social_sec_no, source="CHAT", reason1=reason1, reason2=reason2, reason3=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", message_id=message_id, ).execute() if _result_failed_or_missing_data(register_result, state=state): return ActionResult.fail( register_result.error or "Falha ao registrar protocolo do pro_rata", **register_result.metadata, ) payload = _to_dict(register_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 "" ).strip() if not protocolo_id: return ActionResult.fail("Protocolo nao retornado para pro_rata") if not mensagem_base and caminho_norm == "aceitou": mensagem_base = ( f"Sua solicitação foi registrada. " f"Seu número de protocolo é {vocalize_digits(protocolo_id)}." ) logger.info( "registrar_atendimento_pro_rata caminho=%s msisdn=%s protocolo=%s", caminho_norm or "n/a", _final_msisdn(msisdn), protocolo_id, ) result: dict[str, Any] = { "success": True, "mensagem": mensagem_base, "protocolo": protocolo_id, "protocolo_id": protocolo_id, "pro_rata_protocol": protocolo_id, "protocolos_por_linha": [ { "msisdn": msisdn, "nome": "pro_rata", "protocolo_id": protocolo_id, } ], "tripleta": { "reason1": reason1, "reason2": reason2, "reason3": reason3, }, "protocol_closed": True, "devolucao": devolucao, "requires_protocol_in_response": True, "protocols_for_response": [protocolo_id], } if ic: result["ic"] = ic return ActionResult.ok(result) __all__ = [ 'formatar_pro_rata', 'preparar_pro_rata', 'calcular_itens_contestacao_pro_rata', 'executar_contestacao_plano_controle', 'definir_devolucao_ajuste_pro_rata', 'executar_devolucao_pro_rata', 'orientar_pagamento_pro_rata', 'registrar_atendimento_pro_rata', ]