mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 10:14:20 +00:00
852 lines
30 KiB
Python
852 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
from agente_contas_tim.constants.ic_tags_enum import VAATag
|
|
from agente_contas_tim.integrations.rct_policy import RCTOperation
|
|
from agente_contas_tim.observability import get_session_id
|
|
from agente_contas_tim.protocol_triplets import resolve_protocol_triplet
|
|
from agente_contas_tim.workflows.actions.inputs import parse_cancel_vas_items
|
|
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 (
|
|
_build_ic_context,
|
|
_emit_vaa,
|
|
_find_matching_service,
|
|
_first_text_from_params_or_state,
|
|
_format_amount,
|
|
_idempotency_get,
|
|
_idempotency_set,
|
|
_parse_amount,
|
|
_resolve_service_csp_id,
|
|
_result_failed_or_missing_data,
|
|
_service_from_dict,
|
|
_to_dict,
|
|
)
|
|
from agente_contas_tim.workflows.actions.vas_avulso.helpers import (
|
|
_build_active_services,
|
|
_empty_cancel_result,
|
|
_service_can_be_canceled,
|
|
)
|
|
|
|
logger = logging.getLogger("agente_contas_tim.workflows.actions.tim_actions")
|
|
|
|
|
|
@workflow_action("consulta_vas")
|
|
def query_vas(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
app_id = str(input_state.get("app_id", ""))
|
|
service_context = input_state.get("service_context")
|
|
if isinstance(service_context, dict):
|
|
context_app_id = str(service_context.get("app_id", "")).strip()
|
|
if not app_id or context_app_id == app_id:
|
|
return ActionResult.ok(
|
|
{
|
|
"services": [service_context],
|
|
"service_found": True,
|
|
"service_status": str(service_context.get("status", "")),
|
|
"service": service_context,
|
|
"consulta_metadata": {"source": "service_context"},
|
|
}
|
|
)
|
|
|
|
result = runtime.factory.create_query_vas(
|
|
msisdn=str(params["msisdn"])
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha na consulta",
|
|
**result.metadata,
|
|
)
|
|
|
|
payload = _to_dict(result.data)
|
|
services = payload.get("services", [])
|
|
target = next(
|
|
(service for service in services if str(service.get("app_id", "")) == app_id),
|
|
None,
|
|
)
|
|
return ActionResult.ok(
|
|
{
|
|
"services": services,
|
|
"service_found": target is not None,
|
|
"service_status": str((target or {}).get("status", "")),
|
|
"service": target,
|
|
"consulta_metadata": dict(result.metadata),
|
|
},
|
|
**result.metadata,
|
|
)
|
|
|
|
|
|
@workflow_action("cancelamento_vas_avulso_batch")
|
|
def cancelamento_vas_avulso_batch(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
csp_id = str(params.get("csp_id", "740")).strip() or "740"
|
|
channel = "AIAGENTCR"
|
|
request_status = str(params.get("request_status", "Fechado")).strip() or "Fechado"
|
|
status = str(params.get("status", "CLOSED")).strip() or "CLOSED"
|
|
message_id = _first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"message_id",
|
|
"messageId",
|
|
)
|
|
idempotency_key = str(params.get("idempotency_key", "")).strip()
|
|
social_sec_no = re.sub(
|
|
r"\D",
|
|
"",
|
|
_first_text_from_params_or_state(
|
|
state,
|
|
params,
|
|
"social_sec_no",
|
|
"socialSecNo",
|
|
"customer_document",
|
|
"customerDocument",
|
|
),
|
|
)
|
|
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
_ic_base: dict[str, Any] = {
|
|
"sessionId": str(get_session_id() or ""),
|
|
"gsm": "",
|
|
"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(),
|
|
}
|
|
|
|
|
|
parsed_items = parse_cancel_vas_items(params.get("items"))
|
|
|
|
if not parsed_items:
|
|
return ActionResult.ok(
|
|
_empty_cancel_result(
|
|
"", mensagem="Nenhum serviço informado para contestação."
|
|
)
|
|
)
|
|
|
|
normalized_items: list[tuple[str, str, float]] = [
|
|
(item.msisdn, item.service, item.value) for item in parsed_items
|
|
]
|
|
_emit_vaa(VAATag.INICIO_FLUXO, _ic_base, gsm=normalized_items[0][0])
|
|
|
|
msisdns_in_order: list[str] = []
|
|
seen_msisdns: set[str] = set()
|
|
for item_msisdn, _, _ in normalized_items:
|
|
if item_msisdn not in seen_msisdns:
|
|
seen_msisdns.add(item_msisdn)
|
|
msisdns_in_order.append(item_msisdn)
|
|
|
|
protocol_entries: list[dict[str, str]] = []
|
|
|
|
active_by_msisdn: dict[str, dict[str, dict[str, Any]]] = {}
|
|
query_failures: set[str] = set()
|
|
for line_msisdn in msisdns_in_order:
|
|
query_result = runtime.factory.create_query_vas(
|
|
msisdn=line_msisdn
|
|
).execute()
|
|
if _result_failed_or_missing_data(query_result, state=state):
|
|
query_failures.add(line_msisdn)
|
|
active_by_msisdn[line_msisdn] = {}
|
|
continue
|
|
active_by_msisdn[line_msisdn] = _build_active_services(
|
|
getattr(query_result.data, "services", [])
|
|
)
|
|
|
|
canceled_items: list[dict[str, Any]] = []
|
|
errors: list[dict[str, str]] = []
|
|
not_found_services: list[dict[str, str]] = []
|
|
contestation_candidates: list[dict[str, Any]] = []
|
|
technical_error_in_iu = False
|
|
|
|
def _to_display_amount(value: Any) -> str:
|
|
candidates: list[Any] = [value]
|
|
|
|
def _collect_from_any(raw: Any) -> list[Any]:
|
|
collected: list[Any] = []
|
|
if raw is None:
|
|
return collected
|
|
|
|
if isinstance(raw, (str, int, float, Decimal)):
|
|
collected.append(raw)
|
|
return collected
|
|
|
|
if isinstance(raw, dict):
|
|
for key in (
|
|
"valor",
|
|
"price",
|
|
"amount",
|
|
"cost",
|
|
"value",
|
|
"valor_numeric",
|
|
"valor_float",
|
|
"price_numeric",
|
|
"amount_float",
|
|
):
|
|
if key in raw:
|
|
candidate = raw.get(key)
|
|
if candidate not in (None, ""):
|
|
collected.append(candidate)
|
|
details = raw.get("details")
|
|
if isinstance(details, dict):
|
|
collected.extend(_collect_from_any(details))
|
|
extra = raw.get("extra")
|
|
if isinstance(extra, dict):
|
|
collected.extend(_collect_from_any(extra))
|
|
|
|
if isinstance(raw, (list, tuple, set)):
|
|
for item in raw:
|
|
if item not in (None, ""):
|
|
collected.extend(_collect_from_any(item))
|
|
|
|
return collected
|
|
|
|
for candidate in _collect_from_any(value):
|
|
parsed = _parse_amount(candidate)
|
|
if parsed is None:
|
|
continue
|
|
try:
|
|
return _format_amount(parsed)
|
|
except Exception:
|
|
continue
|
|
return ""
|
|
|
|
def _resolve_item_amount(
|
|
requested: Any,
|
|
matched_service: dict[str, Any],
|
|
) -> str:
|
|
service_context = matched_service.get("service_context")
|
|
context = service_context if isinstance(service_context, dict) else {}
|
|
return (
|
|
_to_display_amount(requested)
|
|
or _to_display_amount(
|
|
{
|
|
"valor": matched_service.get("valor"),
|
|
"price": matched_service.get("price"),
|
|
"amount": matched_service.get("amount"),
|
|
"cost": matched_service.get("cost"),
|
|
"value": matched_service.get("value"),
|
|
"amount_numeric": matched_service.get("amount_numeric"),
|
|
"valor_numeric": matched_service.get("valor_numeric"),
|
|
"valor_float": matched_service.get("valor_float"),
|
|
"price_numeric": matched_service.get("price_numeric"),
|
|
"service_context": context,
|
|
}
|
|
)
|
|
)
|
|
|
|
def _parse_amount_from_item(item: dict[str, Any]) -> Decimal | None:
|
|
for key in ("valor", "value", "amount", "price", "servico_valor"):
|
|
raw_value = item.get(key)
|
|
if raw_value is None:
|
|
continue
|
|
parsed = _parse_amount(raw_value)
|
|
if parsed is not None:
|
|
return parsed
|
|
return None
|
|
|
|
def _build_contestation_candidate(
|
|
*,
|
|
msisdn: str,
|
|
service_name: str,
|
|
amount: Any,
|
|
app_id: str = "",
|
|
codigo_boleto: str = "",
|
|
iu_reason: str = "",
|
|
) -> dict[str, Any]:
|
|
resolved_amount = _to_display_amount(amount) or "0"
|
|
return {
|
|
"msisdn": msisdn,
|
|
"servico": str(service_name or "").strip(),
|
|
"service": str(service_name or "").strip(),
|
|
"app_id": str(app_id or "").strip(),
|
|
"valor": resolved_amount,
|
|
"value": resolved_amount,
|
|
"amount": resolved_amount,
|
|
"price": resolved_amount,
|
|
"codigo_boleto": str(codigo_boleto or "").strip(),
|
|
"iu_reason": str(iu_reason or "").strip(),
|
|
}
|
|
|
|
for item_msisdn, requested_service, requested_value in normalized_items:
|
|
if item_msisdn in query_failures:
|
|
technical_error_in_iu = True
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": requested_service,
|
|
"erro": (
|
|
"Falha ao consultar a linha de final "
|
|
f"{item_msisdn[-2:]}."
|
|
),
|
|
}
|
|
)
|
|
continue
|
|
|
|
active_services = active_by_msisdn.get(item_msisdn, {})
|
|
matched_service = _find_matching_service(
|
|
requested_service, active_services,
|
|
)
|
|
if not matched_service:
|
|
logger.info(
|
|
"cancelamento_vas_avulso_batch.skip reason=service_not_found msisdn=%s requested_service=%s available_services=%s",
|
|
item_msisdn,
|
|
requested_service,
|
|
list(active_services.keys()),
|
|
)
|
|
not_found_services.append(
|
|
{"msisdn": item_msisdn, "servico": requested_service}
|
|
)
|
|
contestation_candidates.append(
|
|
_build_contestation_candidate(
|
|
msisdn=item_msisdn,
|
|
service_name=requested_service,
|
|
amount=requested_value,
|
|
iu_reason="service_not_found",
|
|
)
|
|
)
|
|
continue
|
|
if not _service_can_be_canceled(matched_service):
|
|
logger.info(
|
|
"cancelamento_vas_avulso_batch.skip reason=can_cancel_false msisdn=%s requested_service=%s matched_service=%s",
|
|
item_msisdn,
|
|
requested_service,
|
|
str(matched_service.get("service_name", "")).strip(),
|
|
)
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": requested_service,
|
|
"erro": (
|
|
"Serviço já se encontra cancelado, não sendo possível "
|
|
"realizar recancelamento."
|
|
),
|
|
}
|
|
)
|
|
contestation_candidates.append(
|
|
_build_contestation_candidate(
|
|
msisdn=item_msisdn,
|
|
service_name=str(
|
|
matched_service.get("service_name", "")
|
|
).strip()
|
|
or requested_service,
|
|
amount=requested_value,
|
|
app_id=str(matched_service.get("app_id", "")).strip(),
|
|
codigo_boleto=str(
|
|
matched_service.get("codigo_boleto", "")
|
|
).strip(),
|
|
iu_reason="already_inactive_or_blocked",
|
|
)
|
|
)
|
|
continue
|
|
|
|
service_name = str(matched_service.get("service_name", ""))
|
|
app_id = str(matched_service.get("app_id", ""))
|
|
valor = _resolve_item_amount(requested_value, matched_service)
|
|
codigo_boleto = str(matched_service.get("codigo_boleto", "")).strip()
|
|
|
|
cache_key = ""
|
|
if idempotency_key and service_name:
|
|
cache_key = (
|
|
f"{idempotency_key}:{item_msisdn}:"
|
|
f"{service_name.lower()}:{app_id}"
|
|
)
|
|
cached_item = _idempotency_get(cache_key)
|
|
if cached_item is not None:
|
|
merged_item = dict(cached_item)
|
|
if not str(
|
|
merged_item.get("valor", "")
|
|
or merged_item.get("value", "")
|
|
or merged_item.get("amount", "")
|
|
).strip():
|
|
merged_item.update(
|
|
{
|
|
"valor": valor,
|
|
"value": valor,
|
|
"amount": valor,
|
|
"price": valor,
|
|
}
|
|
)
|
|
if cache_key:
|
|
_idempotency_set(cache_key, merged_item)
|
|
canceled_items.append(merged_item)
|
|
continue
|
|
|
|
resolved_csp_id = _resolve_service_csp_id(
|
|
matched_service,
|
|
fallback_csp_id=csp_id,
|
|
)
|
|
|
|
_emit_vaa(VAATag.BLOQUEIO_INICIO, _ic_base, gsm=item_msisdn)
|
|
block_result = runtime.factory.create_block_vas(
|
|
msisdn=item_msisdn,
|
|
app_id=app_id,
|
|
csp_id=resolved_csp_id,
|
|
service=_service_from_dict(
|
|
matched_service.get("service_context")
|
|
),
|
|
ic_context={**_ic_base, "gsm": item_msisdn},
|
|
).execute()
|
|
if not block_result.success:
|
|
technical_error_in_iu = True
|
|
_emit_vaa(VAATag.ITEM_CANCELADO_FAIL, _ic_base, gsm=item_msisdn)
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": service_name,
|
|
"erro": block_result.error or "Falha no bloqueio",
|
|
}
|
|
)
|
|
continue
|
|
|
|
cancel_result = runtime.factory.create_cancellation_vas(
|
|
msisdn=item_msisdn,
|
|
app_id=app_id,
|
|
csp_id=resolved_csp_id,
|
|
channel=channel,
|
|
interaction_protocol="",
|
|
service=_service_from_dict(
|
|
matched_service.get("service_context")
|
|
),
|
|
).execute()
|
|
if not cancel_result.success:
|
|
technical_error_in_iu = True
|
|
_emit_vaa(VAATag.ITEM_CANCELADO_FAIL, _ic_base, gsm=item_msisdn)
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": service_name,
|
|
"erro": cancel_result.error or "Falha no cancelamento",
|
|
}
|
|
)
|
|
continue
|
|
_emit_vaa(VAATag.ITEM_CANCELADO_OK, _ic_base, gsm=item_msisdn)
|
|
cancel_payload = _to_dict(cancel_result.data)
|
|
cancelamento = (
|
|
cancel_payload.get("cancelamento", {})
|
|
if isinstance(cancel_payload, dict)
|
|
else {}
|
|
)
|
|
cancellation_protocol = ""
|
|
if isinstance(cancelamento, dict):
|
|
cancellation_protocol = str(
|
|
cancelamento.get("interactionProtocol")
|
|
or cancelamento.get("protocolId")
|
|
or cancelamento.get("protocolo_id")
|
|
or ""
|
|
).strip()
|
|
if not cancellation_protocol:
|
|
body_payload = cancelamento.get("body", {})
|
|
if isinstance(body_payload, dict):
|
|
cancellation_protocol = str(
|
|
body_payload.get("interactionProtocol")
|
|
or body_payload.get("protocolId")
|
|
or body_payload.get("protocolo_id")
|
|
or ""
|
|
).strip()
|
|
|
|
canceled_item: dict[str, Any] = {
|
|
"msisdn": item_msisdn,
|
|
"servico": service_name,
|
|
"app_id": app_id,
|
|
"valor": valor,
|
|
"value": valor,
|
|
"amount": valor,
|
|
"price": valor,
|
|
"codigo_boleto": codigo_boleto,
|
|
}
|
|
canceled_items.append(canceled_item)
|
|
contestation_candidates.append(dict(canceled_item))
|
|
if cache_key:
|
|
_idempotency_set(cache_key, canceled_item)
|
|
|
|
_emit_vaa(VAATag.PROTOCOLO_INICIO, _ic_base, gsm=item_msisdn)
|
|
close_triplet = resolve_protocol_triplet(
|
|
"cancelamento_vas_avulso",
|
|
stage="close",
|
|
context={"servico": service_name},
|
|
)
|
|
protocol_result = runtime.factory.create_protocol_v2(
|
|
msisdn=item_msisdn,
|
|
interaction_protocol=cancellation_protocol,
|
|
flag_sms=True,
|
|
social_sec_no=social_sec_no,
|
|
source="CHAT",
|
|
reason1=close_triplet.reason1 or "Informação",
|
|
reason2=close_triplet.reason2 or "Conta",
|
|
reason3=close_triplet.reason3 or "Valor",
|
|
direction_contact="FROM-CLIENT",
|
|
type="CLIENTE",
|
|
request_flag=True,
|
|
request_status=request_status,
|
|
status=status,
|
|
service_request_notes=f"Serviço cancelado: {service_name}",
|
|
client_id="AIAGENTCR",
|
|
ic_context={**_ic_base, "gsm": str(item_msisdn)},
|
|
rct_operation=RCTOperation.REG_ATEND_VAS_AVULSO,
|
|
message_id=message_id,
|
|
).execute()
|
|
if _result_failed_or_missing_data(protocol_result, state=state):
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": service_name,
|
|
"erro": (
|
|
protocol_result.error
|
|
or "Falha ao registrar protocolo após cancelamento"
|
|
),
|
|
}
|
|
)
|
|
continue
|
|
|
|
_emit_vaa(VAATag.PROTOCOLO_OK, _ic_base, gsm=item_msisdn)
|
|
protocol_payload = _to_dict(protocol_result.data)
|
|
protocol_response = (
|
|
protocol_payload.get("response", {})
|
|
if isinstance(protocol_payload, dict)
|
|
else {}
|
|
)
|
|
if not isinstance(protocol_response, dict):
|
|
protocol_response = {}
|
|
protocolo_id = str(
|
|
cancellation_protocol
|
|
or protocol_response.get("interactionProtocol")
|
|
or protocol_response.get("protocolId")
|
|
or protocol_response.get("protocolo_id")
|
|
or ""
|
|
).strip()
|
|
if not protocolo_id:
|
|
errors.append(
|
|
{
|
|
"msisdn": item_msisdn,
|
|
"servico": service_name,
|
|
"erro": "Protocolo não retornado no registro de atendimento",
|
|
}
|
|
)
|
|
continue
|
|
protocol_entries.append(
|
|
{"msisdn": item_msisdn, "protocolo_id": protocolo_id}
|
|
)
|
|
|
|
summary_parts: list[str] = []
|
|
if canceled_items:
|
|
by_line: dict[str, list[str]] = {}
|
|
for item in canceled_items:
|
|
by_line.setdefault(
|
|
str(item.get("msisdn", "")), []
|
|
).append(str(item.get("servico", "")))
|
|
for line, names in by_line.items():
|
|
summary_parts.append(
|
|
"Cancelados com sucesso na linha de final "
|
|
f"{line[-2:]}: {', '.join(names)}."
|
|
)
|
|
|
|
for error in errors:
|
|
summary_parts.append(
|
|
f"Erro ao cancelar {error['servico']} "
|
|
f"(linha {error['msisdn'][-2:]}): {error['erro']}."
|
|
)
|
|
|
|
if not_found_services:
|
|
joined = ", ".join(
|
|
f"{nf['servico']} (linha {nf['msisdn'][-2:]})"
|
|
for nf in not_found_services
|
|
)
|
|
summary_parts.append(f"Não encontrados: {joined}.")
|
|
|
|
total_valor = Decimal("0")
|
|
total_tem_valor = False
|
|
cancelados_descritivo: list[str] = []
|
|
for item in canceled_items:
|
|
servico = str(item.get("servico", "")).strip()
|
|
valor = str(item.get("valor", "")).strip()
|
|
parsed = _parse_amount_from_item(item)
|
|
if parsed is not None:
|
|
total_valor += parsed
|
|
total_tem_valor = True
|
|
if servico:
|
|
valor_txt = f" (valor: R$ {valor})" if valor else ""
|
|
cancelados_descritivo.append(f"{servico}{valor_txt}")
|
|
|
|
total_valor_txt = (
|
|
f"R$ {_format_amount(total_valor)}"
|
|
if total_tem_valor
|
|
else "R$ 0,00"
|
|
)
|
|
|
|
if technical_error_in_iu:
|
|
contestation_candidates = []
|
|
|
|
primary_msisdn = msisdns_in_order[0] if msisdns_in_order else ""
|
|
resumo_agente = (
|
|
"Cancelamento VAS avulso concluido. "
|
|
f"Telefone: {primary_msisdn}. "
|
|
"Servicos cancelados: "
|
|
f"{', '.join(cancelados_descritivo) if cancelados_descritivo else 'nenhum'}. "
|
|
f"Total dos servicos cancelados: {total_valor_txt}."
|
|
)
|
|
|
|
return ActionResult.ok(
|
|
{
|
|
"success": len(canceled_items) > 0,
|
|
"mensagem": " ".join(summary_parts),
|
|
"resumo_agente": resumo_agente,
|
|
"msisdn": primary_msisdn,
|
|
"cancelados": canceled_items,
|
|
"erros": errors,
|
|
"nao_encontrados": not_found_services,
|
|
"itens_para_contestacao": contestation_candidates,
|
|
"protocolos_por_linha": protocol_entries,
|
|
"protocol_closed": bool(protocol_entries),
|
|
"prompt_signals": {
|
|
"servicos_cancelados": [
|
|
item.get("servico") for item in canceled_items
|
|
],
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
@workflow_action("avaliar_proxima_acao")
|
|
def evaluate_next_action(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Action exemplo: decide se deve aguardar confirmação do usuário."""
|
|
services = []
|
|
previous_query = state.get("vars", {}).get("node1")
|
|
if isinstance(previous_query, dict):
|
|
previous_services = previous_query.get("services", [])
|
|
if isinstance(previous_services, list):
|
|
services = previous_services
|
|
|
|
metadata: dict[str, Any] = {}
|
|
if not services:
|
|
result = runtime.factory.create_query_vas(
|
|
msisdn=str(params["msisdn"])
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha na consulta para avaliação",
|
|
**result.metadata,
|
|
)
|
|
services = _to_dict(result.data).get("services", [])
|
|
metadata = dict(result.metadata)
|
|
|
|
tim_music = next(
|
|
(
|
|
s
|
|
for s in services
|
|
if str(s.get("service_name", "")).strip().lower() == "tim music"
|
|
and str(s.get("status", "")).upper() == "ACTIVE"
|
|
),
|
|
None,
|
|
)
|
|
if tim_music:
|
|
client_payload = {
|
|
"type": "question",
|
|
"text": (
|
|
"O motivo do aumento da sua fatura é o serviço TIM Music, que está ativo em sua linha. "
|
|
"Deseja cancelar o TIM Music? Se sim, responda SIM para que eu possa prosseguir com o bloqueio e cancelamento."
|
|
),
|
|
"options": ["SIM", "NAO"],
|
|
"service": {
|
|
"app_id": tim_music.get("app_id", ""),
|
|
"service_name": tim_music.get("service_name", ""),
|
|
"status": tim_music.get("status", ""),
|
|
},
|
|
}
|
|
return ActionResult.ok(
|
|
{
|
|
"await_user_input": True,
|
|
"client_payload": client_payload,
|
|
"service_context": tim_music,
|
|
},
|
|
**metadata,
|
|
)
|
|
|
|
# Caso não haja TIM Music ativo, segue fluxo normal
|
|
app_id = str(params.get("app_id", state.get("input", {}).get("app_id", "")))
|
|
target = next(
|
|
(service for service in services if str(service.get("app_id", "")) == app_id),
|
|
None,
|
|
)
|
|
if target is None:
|
|
return ActionResult.ok(
|
|
{
|
|
"await_user_input": False,
|
|
"client_payload": {
|
|
"type": "info",
|
|
"text": f"Serviço appId={app_id} não localizado para confirmação.",
|
|
},
|
|
}
|
|
)
|
|
|
|
status = str(target.get("status", "")).upper()
|
|
await_user_input = status == "ACTIVE"
|
|
|
|
if await_user_input:
|
|
client_payload = {
|
|
"type": "question",
|
|
"text": "Deseja continuar com a operação deste serviço?",
|
|
"options": ["SIM", "NAO"],
|
|
"service": {
|
|
"app_id": target.get("app_id", ""),
|
|
"service_name": target.get("service_name", ""),
|
|
"status": target.get("status", ""),
|
|
},
|
|
}
|
|
else:
|
|
client_payload = {
|
|
"type": "info",
|
|
"text": "Serviço não requer confirmação adicional.",
|
|
"service": {
|
|
"app_id": target.get("app_id", ""),
|
|
"service_name": target.get("service_name", ""),
|
|
"status": target.get("status", ""),
|
|
},
|
|
}
|
|
|
|
return ActionResult.ok(
|
|
{
|
|
"await_user_input": await_user_input,
|
|
"client_payload": client_payload,
|
|
"service_context": target,
|
|
},
|
|
**metadata,
|
|
)
|
|
|
|
|
|
@workflow_action("bloquear_vas")
|
|
def bloquear_vas(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
service_context = state.get("vars", {}).get("node2", {}).get("service_context")
|
|
msisdn = str(params["msisdn"])
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
result = runtime.factory.create_block_vas(
|
|
msisdn=msisdn,
|
|
app_id=str(params["app_id"]),
|
|
csp_id=str(params.get("csp_id", "740")),
|
|
service=_service_from_dict(service_context),
|
|
ic_context=_build_ic_context(input_state, gsm=msisdn),
|
|
query_metadata=state.get("vars", {})
|
|
.get("node1", {})
|
|
.get("consulta_metadata", {}),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha no bloqueio",
|
|
**result.metadata,
|
|
)
|
|
return ActionResult.ok(_to_dict(result.data), **result.metadata)
|
|
|
|
|
|
@workflow_action("cancelar_vas")
|
|
def cancelar_vas(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
service_context = state.get("vars", {}).get("node2", {}).get("service_context")
|
|
result = runtime.factory.create_cancellation_vas(
|
|
msisdn=str(params["msisdn"]),
|
|
app_id=str(params["app_id"]),
|
|
csp_id=str(params.get("csp_id", "740")),
|
|
interaction_protocol=str(params.get("interaction_protocol", "")),
|
|
channel="AIAGENTCR",
|
|
service=_service_from_dict(service_context),
|
|
query_metadata=state.get("vars", {})
|
|
.get("node1", {})
|
|
.get("consulta_metadata", {}),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha no cancelamento",
|
|
**result.metadata,
|
|
)
|
|
return ActionResult.ok(_to_dict(result.data), **result.metadata)
|
|
|
|
|
|
@workflow_action("bloquear_vas_single")
|
|
def bloquear_vas_single(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Bloqueia um serviço VAS usando msisdn + app_id dos params."""
|
|
service_context = (
|
|
state.get("vars", {})
|
|
.get("consulta_vas", {})
|
|
.get("service")
|
|
)
|
|
msisdn = str(params["msisdn"])
|
|
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
|
result = runtime.factory.create_block_vas(
|
|
msisdn=msisdn,
|
|
app_id=str(params["app_id"]),
|
|
csp_id=str(params.get("csp_id", "740")),
|
|
service=_service_from_dict(service_context),
|
|
ic_context=_build_ic_context(input_state, gsm=msisdn),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha no bloqueio",
|
|
**result.metadata,
|
|
)
|
|
return ActionResult.ok(_to_dict(result.data), **result.metadata)
|
|
|
|
|
|
@workflow_action("cancelar_vas_single")
|
|
def cancelar_vas_single(
|
|
state: dict[str, Any],
|
|
params: dict[str, Any],
|
|
runtime: WorkflowRuntimeContext,
|
|
) -> ActionResult:
|
|
"""Cancela um serviço VAS usando msisdn + app_id dos params."""
|
|
service_context = (
|
|
state.get("vars", {})
|
|
.get("consulta_vas", {})
|
|
.get("service")
|
|
)
|
|
result = runtime.factory.create_cancellation_vas(
|
|
msisdn=str(params["msisdn"]),
|
|
app_id=str(params["app_id"]),
|
|
csp_id=str(params.get("csp_id", "740")),
|
|
channel="AIAGENTCR",
|
|
interaction_protocol=str(params.get("interaction_protocol", "")),
|
|
service=_service_from_dict(service_context),
|
|
).execute()
|
|
if _result_failed_or_missing_data(result, state=state):
|
|
return ActionResult.fail(
|
|
result.error or "Falha no cancelamento",
|
|
**result.metadata,
|
|
)
|
|
return ActionResult.ok(_to_dict(result.data), **result.metadata)
|
|
|
|
|
|
__all__ = [
|
|
'query_vas',
|
|
'cancelamento_vas_avulso_batch',
|
|
'evaluate_next_action',
|
|
'bloquear_vas',
|
|
'cancelar_vas',
|
|
'bloquear_vas_single',
|
|
'cancelar_vas_single',
|
|
]
|