mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-10 02:34:20 +00:00
first commit
This commit is contained in:
726
legacy_reference/workflows/actions/pro_rata/actions.py
Normal file
726
legacy_reference/workflows/actions/pro_rata/actions.py
Normal file
@@ -0,0 +1,726 @@
|
||||
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',
|
||||
]
|
||||
793
legacy_reference/workflows/actions/pro_rata/helpers.py
Normal file
793
legacy_reference/workflows/actions/pro_rata/helpers.py
Normal file
@@ -0,0 +1,793 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata as ud
|
||||
|
||||
from datetime import date
|
||||
from decimal import (
|
||||
Decimal,
|
||||
InvalidOperation,
|
||||
ROUND_HALF_UP,
|
||||
)
|
||||
from typing import Any
|
||||
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,
|
||||
_format_list_pt_br,
|
||||
_normalize_bool,
|
||||
_normalize_number_text,
|
||||
_parse_amount,
|
||||
_result_failed_or_missing_data,
|
||||
)
|
||||
from agente_contas_tim.workflows.actions.contestacao.helpers import _build_contestation_items
|
||||
|
||||
_CENT = Decimal("0.01")
|
||||
|
||||
|
||||
def _format_plano_pro_rata(plano: dict[str, Any]) -> str:
|
||||
desc = str(plano.get("desc", "")).strip()
|
||||
valor_txt = _format_plano_pro_rata_value(plano)
|
||||
final = _final_msisdn(str(plano.get("msisdn", "")))
|
||||
return f"{desc} (valor R$ {valor_txt}, linha final {final})"
|
||||
|
||||
|
||||
def _format_plano_pro_rata_value(plano: dict[str, Any]) -> str:
|
||||
raw_value = (
|
||||
plano.get("valor_final")
|
||||
or plano.get("valor_liquido")
|
||||
or plano.get("value")
|
||||
or plano.get("valor_bruto")
|
||||
)
|
||||
if isinstance(raw_value, bool):
|
||||
valor_parsed = None
|
||||
elif isinstance(raw_value, (int, float, Decimal)):
|
||||
valor_parsed = Decimal(str(raw_value))
|
||||
else:
|
||||
valor_parsed = _parse_amount(str(raw_value or ""))
|
||||
return _format_amount(valor_parsed) or "0,00"
|
||||
|
||||
|
||||
def _format_plano_pro_rata_display_name(
|
||||
plano: dict[str, Any],
|
||||
*,
|
||||
fallback: str,
|
||||
) -> str:
|
||||
desc = str(plano.get("desc", "")).strip()
|
||||
clean = re.sub(r"\s*\([^)]*\)\s*$", "", desc).strip()
|
||||
return clean or fallback
|
||||
|
||||
|
||||
def _format_pro_rata_period_reference(plano: dict[str, Any]) -> str:
|
||||
days = _resolve_days_number(
|
||||
plano.get("days") if plano.get("days") is not None else plano.get("dias")
|
||||
)
|
||||
if days is None:
|
||||
return "referente ao período de uso na fatura"
|
||||
unidade = "dia" if days == 1 else "dias"
|
||||
return f"referente a {days} {unidade} de uso no período"
|
||||
|
||||
|
||||
def _format_planos_pro_rata(planos: list[dict[str, Any]]) -> str:
|
||||
formatted = [
|
||||
_format_plano_pro_rata(p) for p in planos if isinstance(p, dict)
|
||||
]
|
||||
return _format_list_pt_br(formatted)
|
||||
|
||||
|
||||
def _build_pro_rata_controle_msg(planos: list[dict[str, Any]]) -> str:
|
||||
resolved = _resolve_plano_controle(planos)
|
||||
if resolved is None:
|
||||
return (
|
||||
"Houve uma troca de plano e, por isso, na sua fatura "
|
||||
"apareceram duas cobranças.\n\n"
|
||||
"Uma é cobrança proporcional de outro plano, referente ao "
|
||||
"período de uso na fatura.\n\n"
|
||||
"A outra é do plano Controle.\n\n"
|
||||
"No plano Controle funciona assim: sempre que a franquia é "
|
||||
"renovada, o valor do plano é cobrado por completo.\n\n"
|
||||
"Isso acontece porque, a partir da renovação, você já passa a "
|
||||
"ter acesso a todos os benefícios do plano imediatamente, como "
|
||||
"internet, ligações e outras vantagens.\n\n"
|
||||
"Consegui esclarecer sua dúvida?"
|
||||
)
|
||||
|
||||
plano_controle, outro_plano = resolved
|
||||
controle_nome = _format_plano_pro_rata_display_name(
|
||||
plano_controle,
|
||||
fallback="Controle",
|
||||
)
|
||||
outro_nome = _format_plano_pro_rata_display_name(
|
||||
outro_plano,
|
||||
fallback="identificado",
|
||||
)
|
||||
controle_valor = _format_plano_pro_rata_value(plano_controle)
|
||||
outro_valor = _format_plano_pro_rata_value(outro_plano)
|
||||
outro_periodo = _format_pro_rata_period_reference(outro_plano)
|
||||
return (
|
||||
"Houve uma troca de plano e, por isso, na sua fatura apareceram "
|
||||
"duas cobranças.\n\n"
|
||||
f"Uma é cobrança proporcional do plano {outro_nome}, no valor de "
|
||||
f"R$ {outro_valor}, {outro_periodo}.\n\n"
|
||||
f"A outra é do plano {controle_nome}, no valor de R$ "
|
||||
f"{controle_valor}.\n\n"
|
||||
"No plano Controle funciona assim: sempre que a franquia é "
|
||||
"renovada, o valor do plano é cobrado por completo.\n\n"
|
||||
"Isso acontece porque, a partir da renovação, você já passa a ter "
|
||||
"acesso a todos os benefícios do plano imediatamente, como "
|
||||
"internet, ligações e outras vantagens.\n\n"
|
||||
"Consegui esclarecer sua dúvida?"
|
||||
)
|
||||
|
||||
|
||||
def _build_pro_rata_sem_controle_msg(planos: list[dict[str, Any]]) -> str:
|
||||
return (
|
||||
"Identifiquei a cobranca proporcional na sua fatura "
|
||||
f"envolvendo os planos {_format_planos_pro_rata(planos)}. "
|
||||
"Como não envolveu troca para o Plano Controle, segue como "
|
||||
"cobranca proporcional padrao dos dias usados em cada plano."
|
||||
)
|
||||
|
||||
|
||||
def _build_pro_rata_oferta_ajuste_msg() -> str:
|
||||
return (
|
||||
"Para buscarmos a "
|
||||
"melhor solução, posso solicitar o "
|
||||
"ajuste proporcional do plano Controle?"
|
||||
)
|
||||
|
||||
|
||||
def _build_pro_rata_ajuste_recusado_msg() -> str:
|
||||
return (
|
||||
"Entendi. Sem a sua confirmação, a solicitação de ajuste não "
|
||||
"será aberta neste momento."
|
||||
)
|
||||
|
||||
|
||||
def _money(value: Decimal) -> Decimal:
|
||||
return value.quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _decimal_from_any(value: Any) -> Decimal | None:
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, Decimal):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return Decimal(str(value))
|
||||
parsed = _parse_amount(str(value or ""))
|
||||
return parsed
|
||||
|
||||
|
||||
def _first_decimal_from_mapping(
|
||||
data: dict[str, Any],
|
||||
*keys: str,
|
||||
) -> Decimal | None:
|
||||
for key in keys:
|
||||
if key not in data:
|
||||
continue
|
||||
value = _decimal_from_any(data.get(key))
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_match_text(value: Any) -> str:
|
||||
text = re.sub(r"\s*\([^)]*\)", "", str(value or "")).strip()
|
||||
text = ud.normalize("NFKD", text)
|
||||
text = "".join(ch for ch in text if not ud.combining(ch))
|
||||
text = text.casefold()
|
||||
text = re.sub(r"[^a-z0-9]+", " ", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def _is_same_plan_name(left: Any, right: Any) -> bool:
|
||||
left_key = _normalize_match_text(left)
|
||||
right_key = _normalize_match_text(right)
|
||||
if not left_key or not right_key:
|
||||
return False
|
||||
return left_key == right_key or left_key in right_key or right_key in left_key
|
||||
|
||||
|
||||
def _resolve_plano_controle(
|
||||
planos: list[dict[str, Any]],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]] | None:
|
||||
controles = [
|
||||
plano
|
||||
for plano in planos
|
||||
if isinstance(plano, dict) and bool(plano.get("is_controle"))
|
||||
]
|
||||
if len(controles) != 1:
|
||||
return None
|
||||
controle = controles[0]
|
||||
outro = next((plano for plano in planos if plano is not controle), None)
|
||||
if not isinstance(outro, dict):
|
||||
return None
|
||||
return controle, outro
|
||||
|
||||
|
||||
def _resolve_pro_rata_liquid_value(
|
||||
plano_controle: dict[str, Any],
|
||||
) -> Decimal | None:
|
||||
valor_liquido = _first_decimal_from_mapping(
|
||||
plano_controle,
|
||||
"valor_final",
|
||||
"valorFinal",
|
||||
"valor_liquido",
|
||||
"valorLiquido",
|
||||
"net_value",
|
||||
"netValue",
|
||||
"subtotal",
|
||||
"value_final",
|
||||
)
|
||||
if valor_liquido is None:
|
||||
valor_bruto = _first_decimal_from_mapping(
|
||||
plano_controle,
|
||||
"valor_bruto",
|
||||
"valorBruto",
|
||||
"gross_value",
|
||||
"grossValue",
|
||||
"valor_bruto_plano",
|
||||
"valorBrutoPlano",
|
||||
"preco_unit",
|
||||
)
|
||||
total_descontos = _first_decimal_from_mapping(
|
||||
plano_controle,
|
||||
"total_descontos",
|
||||
"totalDescontos",
|
||||
"discount_total",
|
||||
)
|
||||
if valor_bruto is not None and total_descontos is not None:
|
||||
valor_liquido = valor_bruto + total_descontos
|
||||
return valor_liquido
|
||||
|
||||
|
||||
def _parse_emissao_year(
|
||||
invoice_payload: dict[str, Any],
|
||||
*,
|
||||
invoice_emissao: str = "",
|
||||
) -> int | None:
|
||||
match = re.search(r"\b\d{2}/\d{2}/(?P<year>\d{4})\b", invoice_emissao)
|
||||
if match:
|
||||
return int(match.group("year"))
|
||||
for item in invoice_payload.get("Fatura Resumo", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
emissao = str(item.get("emissao", "") or "").strip()
|
||||
match = re.search(r"\b\d{2}/\d{2}/(?P<year>\d{4})\b", emissao)
|
||||
if match:
|
||||
return int(match.group("year"))
|
||||
return None
|
||||
|
||||
|
||||
def _extract_resumo_period(
|
||||
invoice_payload: dict[str, Any],
|
||||
*,
|
||||
invoice_period: str = "",
|
||||
) -> str:
|
||||
if invoice_period:
|
||||
return invoice_period
|
||||
for item in invoice_payload.get("Fatura Resumo", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
desc = _normalize_match_text(item.get("desc", ""))
|
||||
if desc == "periodo":
|
||||
return str(item.get("period", "") or "").strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_period_range(value: Any, *, emission_year: int) -> tuple[date, date] | None:
|
||||
text = str(value or "").strip()
|
||||
match = re.search(
|
||||
r"(?P<start_day>\d{2})/(?P<start_month>\d{2})\s+a\s+"
|
||||
r"(?P<end_day>\d{2})/(?P<end_month>\d{2})",
|
||||
text,
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
start_day = int(match.group("start_day"))
|
||||
start_month = int(match.group("start_month"))
|
||||
end_day = int(match.group("end_day"))
|
||||
end_month = int(match.group("end_month"))
|
||||
start_year = emission_year
|
||||
end_year = emission_year
|
||||
if start_month > end_month:
|
||||
start_year -= 1
|
||||
try:
|
||||
start = date(start_year, start_month, start_day)
|
||||
end = date(end_year, end_month, end_day)
|
||||
except ValueError:
|
||||
return None
|
||||
if start > end:
|
||||
return None
|
||||
return start, end
|
||||
|
||||
|
||||
def _inclusive_days(period: tuple[date, date]) -> int:
|
||||
return (period[1] - period[0]).days + 1
|
||||
|
||||
|
||||
def _resolve_days_number(value: Any) -> int | None:
|
||||
try:
|
||||
days = Decimal(str(value))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
return None
|
||||
if days <= 0:
|
||||
return None
|
||||
return int(days.to_integral_value(rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _resolve_pro_rata_period_days(
|
||||
*,
|
||||
outro_plano: dict[str, Any],
|
||||
invoice_payload: dict[str, Any],
|
||||
invoice_period: str = "",
|
||||
invoice_emissao: str = "",
|
||||
) -> tuple[int, int] | None:
|
||||
emission_year = _parse_emissao_year(
|
||||
invoice_payload,
|
||||
invoice_emissao=invoice_emissao,
|
||||
)
|
||||
resumo_period_text = _extract_resumo_period(
|
||||
invoice_payload,
|
||||
invoice_period=invoice_period,
|
||||
)
|
||||
if emission_year is None or not resumo_period_text:
|
||||
return None
|
||||
|
||||
ciclo_period = _parse_period_range(
|
||||
resumo_period_text,
|
||||
emission_year=emission_year,
|
||||
)
|
||||
if ciclo_period is None:
|
||||
return None
|
||||
dias_ciclo = _inclusive_days(ciclo_period)
|
||||
if dias_ciclo <= 0:
|
||||
return None
|
||||
|
||||
dias_outro = _resolve_days_number(
|
||||
outro_plano.get("days")
|
||||
if outro_plano.get("days") is not None
|
||||
else outro_plano.get("dias")
|
||||
)
|
||||
if dias_outro is None:
|
||||
return None
|
||||
|
||||
if dias_outro >= dias_ciclo:
|
||||
return dias_ciclo, 1
|
||||
dias_controle = dias_ciclo - dias_outro
|
||||
if dias_controle <= 0:
|
||||
dias_controle = 1
|
||||
return dias_ciclo, dias_controle
|
||||
|
||||
|
||||
def _parse_invoice_detail_payload(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return {"_items": value}
|
||||
if not isinstance(value, str):
|
||||
return {}
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
if isinstance(parsed, list):
|
||||
return {"_items": parsed}
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_danfe_from_state_or_params(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
payload = _extract_invoice_payload_from_state_or_params(state, params)
|
||||
danfe = payload.get("DANFE-COM") if isinstance(payload, dict) else None
|
||||
return danfe if isinstance(danfe, dict) else {}
|
||||
|
||||
|
||||
def _extract_invoice_payload_from_state_or_params(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
detail = _first_value_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"invoice_detail",
|
||||
"invoiceDetail",
|
||||
"danfe_detail",
|
||||
"danfeDetail",
|
||||
)
|
||||
return _parse_invoice_detail_payload(detail)
|
||||
|
||||
|
||||
def _fetch_invoice_payload_pro_rata(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> tuple[dict[str, Any], str | None]:
|
||||
payload = _extract_invoice_payload_from_state_or_params(state, params)
|
||||
if payload.get("DANFE-COM"):
|
||||
return payload, None
|
||||
|
||||
invoice_id = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"invoice_id",
|
||||
"invoiceId",
|
||||
"current_invoice_number",
|
||||
"currentInvoiceNumber",
|
||||
)
|
||||
customer_id = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"customer_id",
|
||||
"customerId",
|
||||
)
|
||||
msisdn = _first_text_from_params_or_state(state, params, "msisdn")
|
||||
if not invoice_id or not customer_id or not msisdn:
|
||||
return payload, (
|
||||
"Pro-rata exige invoice_id, customer_id e msisdn para buscar "
|
||||
"fatura com DANFE."
|
||||
)
|
||||
|
||||
result = runtime.factory.create_bill_pdf(
|
||||
invoice_id=invoice_id,
|
||||
msisdn=msisdn,
|
||||
customer_id=customer_id,
|
||||
include_danfe=True,
|
||||
).execute()
|
||||
if _result_failed_or_missing_data(result, state=state):
|
||||
return payload, result.error or "Falha ao buscar fatura com DANFE."
|
||||
parsed = result.data.parsed_content or {}
|
||||
if not isinstance(parsed, dict):
|
||||
return payload, "Fatura com DANFE retornou conteudo invalido."
|
||||
return parsed, None
|
||||
|
||||
|
||||
def _fetch_danfe_pro_rata(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> tuple[dict[str, Any], str | None]:
|
||||
payload, error = _fetch_invoice_payload_pro_rata(state, params, runtime)
|
||||
if error:
|
||||
return {}, error
|
||||
danfe = payload.get("DANFE-COM")
|
||||
if not isinstance(danfe, dict) or not danfe:
|
||||
return {}, "DANFE-COM nao encontrado na fatura recuperada."
|
||||
return danfe, None
|
||||
|
||||
|
||||
def _find_danfe_plan_items(
|
||||
danfe: dict[str, Any],
|
||||
plano_controle: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
planos = danfe.get("Planos")
|
||||
if not isinstance(planos, dict):
|
||||
return []
|
||||
controle_desc = str(plano_controle.get("desc", "")).strip()
|
||||
for plan_name, raw_items in planos.items():
|
||||
if not _is_same_plan_name(plan_name, controle_desc):
|
||||
continue
|
||||
if isinstance(raw_items, list):
|
||||
return [item for item in raw_items if isinstance(item, dict)]
|
||||
return []
|
||||
|
||||
|
||||
def _build_pro_rata_contestation_items(
|
||||
*,
|
||||
danfe: dict[str, Any],
|
||||
plano_controle: dict[str, Any],
|
||||
valor_devolver: Decimal,
|
||||
) -> tuple[list[dict[str, float | str]], Decimal]:
|
||||
items = _find_danfe_plan_items(danfe, plano_controle)
|
||||
restante = _money(valor_devolver)
|
||||
contestation_items: list[dict[str, float | str]] = []
|
||||
|
||||
for item in items:
|
||||
desc = str(item.get("desc", "")).strip()
|
||||
if not desc:
|
||||
continue
|
||||
claimed_amount = _first_decimal_from_mapping(
|
||||
item,
|
||||
"valor_final",
|
||||
"valorFinal",
|
||||
)
|
||||
if claimed_amount is None or claimed_amount <= 0:
|
||||
continue
|
||||
|
||||
claimed_amount = _money(claimed_amount)
|
||||
validated_amount = min(restante, claimed_amount)
|
||||
if validated_amount <= 0:
|
||||
continue
|
||||
|
||||
contestation_items.append(
|
||||
{
|
||||
"itemName": desc,
|
||||
"itemType": "PRO_RATA",
|
||||
"claimedAmount": float(claimed_amount),
|
||||
"validatedAmount": float(validated_amount),
|
||||
}
|
||||
)
|
||||
restante = _money(restante - validated_amount)
|
||||
if restante <= 0:
|
||||
break
|
||||
|
||||
return contestation_items, restante
|
||||
|
||||
|
||||
def _build_pro_rata_human_validation_text(
|
||||
*,
|
||||
plano_controle: dict[str, Any],
|
||||
valor_liquido: Decimal,
|
||||
dias_ciclo: int,
|
||||
dias_controle: int,
|
||||
valor_usado: Decimal,
|
||||
valor_devolver: Decimal,
|
||||
items: list[dict[str, float | str]],
|
||||
) -> str:
|
||||
plano_nome = re.sub(
|
||||
r"\s*\([^)]*\)",
|
||||
"",
|
||||
str(plano_controle.get("desc") or "").strip(),
|
||||
).strip()
|
||||
if not plano_nome:
|
||||
plano_nome = "Plano Controle"
|
||||
|
||||
item_lines = []
|
||||
for index, item in enumerate(items, start=1):
|
||||
item_name = str(item.get("itemName") or "").strip()
|
||||
claimed = _decimal_from_any(item.get("claimedAmount")) or Decimal("0")
|
||||
validated = _decimal_from_any(item.get("validatedAmount")) or Decimal("0")
|
||||
item_lines.append(
|
||||
f"{index}. {item_name}: valor DANFE R$ {_format_amount(_money(claimed))}; "
|
||||
f"valor a abater R$ {_format_amount(_money(validated))}"
|
||||
)
|
||||
|
||||
itens_txt = "; ".join(item_lines) if item_lines else "nenhum item gerado"
|
||||
return (
|
||||
"Validacao humana pro-rata:"
|
||||
f"Plano Controle identificado: {plano_nome}. "
|
||||
f"Base liquida do plano: R$ {_format_amount(valor_liquido)}. "
|
||||
f"Calculo: ciclo de {dias_ciclo} dias, uso considerado de "
|
||||
f"{dias_controle} dias; valor usado R$ {_format_amount(_money(valor_usado))}; "
|
||||
f"valor a devolver R$ {_format_amount(valor_devolver)}. "
|
||||
f"Itens selecionados no DANFE, na ordem de abatimento: {itens_txt}. "
|
||||
)
|
||||
|
||||
|
||||
def _extract_pro_rata_contestation_context(
|
||||
state: dict[str, Any], params: dict[str, Any]
|
||||
) -> tuple[dict[str, Any] | None, str | None]:
|
||||
devolucao = params.get("devolucao")
|
||||
contestation_params = params
|
||||
if isinstance(devolucao, dict):
|
||||
contestation_params = {**devolucao, **params}
|
||||
if "items" not in params:
|
||||
contestation_params["items"] = devolucao.get("items")
|
||||
|
||||
msisdn = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"msisdn",
|
||||
"Msisdn",
|
||||
"MSISDN",
|
||||
)
|
||||
if not msisdn:
|
||||
return None, "msisdn obrigatorio para contestacao de 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",
|
||||
),
|
||||
)
|
||||
customer_id = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"customer_id",
|
||||
"customerId",
|
||||
)
|
||||
if not customer_id:
|
||||
return None, "customer_id obrigatorio para contestacao de pro_rata"
|
||||
|
||||
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 None, "invoice_number obrigatorio para contestacao de pro_rata"
|
||||
|
||||
items = _build_contestation_items(state, contestation_params)
|
||||
if not items:
|
||||
return None, "items obrigatorio para contestacao de pro_rata"
|
||||
|
||||
fallback_amount = _normalize_number_text(
|
||||
format(
|
||||
sum(
|
||||
(
|
||||
_decimal_from_any(item.get("validated_amount"))
|
||||
or Decimal("0")
|
||||
)
|
||||
for item in items
|
||||
if isinstance(item, dict)
|
||||
),
|
||||
"f",
|
||||
),
|
||||
default="0",
|
||||
)
|
||||
invoice_amount_open = _normalize_number_text(
|
||||
_first_text_from_params_or_state(
|
||||
state,
|
||||
contestation_params,
|
||||
"invoice_amount_open",
|
||||
"invoiceAmountOpen",
|
||||
),
|
||||
default=fallback_amount,
|
||||
)
|
||||
invoice_amount = _normalize_number_text(
|
||||
_first_text_from_params_or_state(
|
||||
state,
|
||||
contestation_params,
|
||||
"invoice_amount",
|
||||
"invoiceAmount",
|
||||
),
|
||||
default=invoice_amount_open,
|
||||
)
|
||||
context = {
|
||||
"msisdn": msisdn,
|
||||
"social_sec_no": social_sec_no,
|
||||
"customer_id": customer_id,
|
||||
"invoice_number": invoice_number,
|
||||
"items": items,
|
||||
"invoice_amount_open": invoice_amount_open,
|
||||
"invoice_amount": invoice_amount,
|
||||
}
|
||||
return context, None
|
||||
|
||||
|
||||
def _build_pro_rata_contestacao_tool_payload(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
*,
|
||||
context: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"msisdn": context["msisdn"],
|
||||
"social_sec_no": context["social_sec_no"],
|
||||
"customer_id": context["customer_id"],
|
||||
"current_invoice_number": context["invoice_number"],
|
||||
"invoice_id": context["invoice_number"],
|
||||
"customer_type": (
|
||||
_first_text_from_params_or_state(
|
||||
state, params, "customer_type", "customerType"
|
||||
)
|
||||
or "2"
|
||||
),
|
||||
"customer_status": (
|
||||
_first_text_from_params_or_state(
|
||||
state, params, "customer_status", "customerStatus"
|
||||
)
|
||||
or "1"
|
||||
),
|
||||
"invoice_status": (
|
||||
_first_text_from_params_or_state(
|
||||
state, params, "invoice_status", "invoiceStatus"
|
||||
)
|
||||
or "1"
|
||||
),
|
||||
"invoice_amount_open": context["invoice_amount_open"],
|
||||
"invoice_amount": context["invoice_amount"],
|
||||
"current_invoice_due_date": _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"current_invoice_due_date",
|
||||
"currentInvoiceDueDate",
|
||||
),
|
||||
"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": (
|
||||
_first_text_from_params_or_state(
|
||||
state, params, "refund_option", "refundOption"
|
||||
)
|
||||
or ""
|
||||
),
|
||||
"manual_conta_certa_indicator": _first_value_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"manual_conta_certa_indicator",
|
||||
"manualContaCertaIndicator",
|
||||
),
|
||||
"double_refund": _normalize_bool(
|
||||
_first_value_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"double_refund",
|
||||
"doubleRefund",
|
||||
),
|
||||
default=False,
|
||||
),
|
||||
"user_id": _first_text_from_params_or_state(
|
||||
state, params, "user_id", "userId"
|
||||
),
|
||||
"message_id": _first_text_from_params_or_state(
|
||||
state, params, "message_id", "messageId"
|
||||
),
|
||||
"client_id": _first_text_from_params_or_state(
|
||||
state, params, "client_id", "clientId"
|
||||
),
|
||||
"tipo_atendimento": "pro_rata",
|
||||
"skip_invoice_item_validation": True,
|
||||
"items": context["items"],
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
'_format_plano_pro_rata',
|
||||
'_format_plano_pro_rata_value',
|
||||
'_format_plano_pro_rata_display_name',
|
||||
'_format_pro_rata_period_reference',
|
||||
'_format_planos_pro_rata',
|
||||
'_build_pro_rata_controle_msg',
|
||||
'_build_pro_rata_sem_controle_msg',
|
||||
'_build_pro_rata_oferta_ajuste_msg',
|
||||
'_build_pro_rata_ajuste_recusado_msg',
|
||||
'_money',
|
||||
'_decimal_from_any',
|
||||
'_first_decimal_from_mapping',
|
||||
'_normalize_match_text',
|
||||
'_is_same_plan_name',
|
||||
'_resolve_plano_controle',
|
||||
'_resolve_pro_rata_liquid_value',
|
||||
'_parse_emissao_year',
|
||||
'_extract_resumo_period',
|
||||
'_parse_period_range',
|
||||
'_inclusive_days',
|
||||
'_resolve_days_number',
|
||||
'_resolve_pro_rata_period_days',
|
||||
'_parse_invoice_detail_payload',
|
||||
'_extract_danfe_from_state_or_params',
|
||||
'_extract_invoice_payload_from_state_or_params',
|
||||
'_fetch_invoice_payload_pro_rata',
|
||||
'_fetch_danfe_pro_rata',
|
||||
'_find_danfe_plan_items',
|
||||
'_build_pro_rata_contestation_items',
|
||||
'_build_pro_rata_human_validation_text',
|
||||
'_extract_pro_rata_contestation_context',
|
||||
'_build_pro_rata_contestacao_tool_payload',
|
||||
]
|
||||
Reference in New Issue
Block a user