from __future__ import annotations import asyncio from types import SimpleNamespace from typing import Any from contas_mcp.servers.contas_mcp_server import main as mcp_main def _result(*, status: str = "COMPLETED", output: dict[str, Any] | None = None, error: str | None = None, trace: list[dict[str, Any]] | None = None, error_details: dict[str, Any] | None = None) -> SimpleNamespace: return SimpleNamespace( execution_id="exec-test", status=status, output=output or {}, error=error, trace=trace or [], state={}, error_details=error_details, model_dump=lambda: { "execution_id": "exec-test", "status": status, "output": output or {}, "error": error, "trace": trace or [], "state": {}, "error_details": error_details, }, ) class _Runtime: def __init__(self, *results: Any): self.results = list(results) self.calls: list[tuple[str, dict[str, Any], str | None]] = [] async def arun(self, name: str, payload: dict[str, Any], execution_id: str | None = None): self.calls.append((name, dict(payload), execution_id)) if not self.results: raise AssertionError("resultado não configurado") return self.results.pop(0) def test_contestacao_wrapper_preserva_tipo_atendimento_e_contexto_do_turno(monkeypatch): runtime = _Runtime(_result(output={"abrir_contestacao_cliente": {"success": True}})) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) args = { "msisdn": "15981110720", "subject": "VAS", "valor": "10", "message_id": "msg-123", "customer_message": "Sim", "session_created_at": "2026-05-05T12:00:00+00:00", "ura_call_id": "CALL-123", "channel_id": "URA", "ani": "11900000000", "invoice_id": "3000132705", } result = asyncio.run(mcp_main._run_workflow("contestacao_tool", args)) assert result["status"] == "COMPLETED" _, payload, _ = runtime.calls[0] assert payload["tipo_atendimento"] == "contestacao" for key in ("message_id", "customer_message", "session_created_at", "ura_call_id", "channel_id", "ani", "invoice_id"): assert payload[key] == args[key] def test_contestacao_wrapper_mapeia_falha_tecnica_como_sistemica(monkeypatch): runtime = _Runtime(_result(status="FAILED", error="gateway indisponivel")) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_workflow("contestacao_tool", {"msisdn": "62999999999", "subject": "VAS"})) assert result["success"] is False assert result["erro_sistemico"] is True assert result["erro"] == "erro_falha_sistema" assert "contestation_error_description" not in result def test_contestacao_wrapper_preserva_erro_de_negocio_e_protocolo(monkeypatch): msg = "Impossível prosseguir, pois não existe os itens informados na fatura questionada." runtime = _Runtime(_result( status="FAILED", error=msg, output={"registrar_protocolo": {"protocolo_id": "PRT-123"}}, error_details={"body": {"description": "Itens já contestados", "provider": {"errorMessage": msg}}}, )) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_workflow("contestacao_tool", {"msisdn": "62999999999", "subject": "VAS"})) assert result["success"] is False assert result["erro_sistemico"] is False assert result["contestation_error_description"] == msg assert result["mensagem"] == msg assert result["contestacao_protocol"] == "PRT-123" def test_invoice_explanation_branch_nao_sem_vas_recomenda_nao_resolvido(): result = mcp_main._result_payload( _result(trace=[{"node": "finalizar_nao_resolvido"}]), workflow_name="invoice_explanation", ) assert result["recomenda_finalizacao"] is True assert result["status_finalizacao_sugerido"] == "nao_resolvido" def test_invoice_explanation_branch_devolver_orquestrador_nao_auto_finaliza(): result = mcp_main._result_payload( _result(trace=[{"node": "devolver_orquestrador"}]), workflow_name="invoice_explanation", ) assert "recomenda_finalizacao" not in result assert "status_finalizacao_sugerido" not in result def test_pro_rata_branch_devolver_orquestrador_nao_auto_finaliza(): result = mcp_main._result_payload( _result(trace=[{"node": "devolver_orquestrador"}]), workflow_name="pro_rata", ) assert "recomenda_finalizacao" not in result assert "status_finalizacao_sugerido" not in result def test_pro_rata_aceite_e_nao_controle_recomendam_resolvido(): for node in ("registrar_aceitou", "registrar_nao_controle"): result = mcp_main._result_payload(_result(trace=[{"node": node}]), workflow_name="pro_rata") assert result["recomenda_finalizacao"] is True assert result["status_finalizacao_sugerido"] == "resolvido" def test_vas_estrategico_workflow_payload_preserva_tres_itens_e_action_agrupa_duas_linhas(): from app.domain.contas.workflow_actions import build_contas_workflow_actions class Service: pass action = build_contas_workflow_actions(Service()).get("preparar_vas_estrategico") items = [ {"type": "bundle", "msisdn": "5511999999999", "name": "Netflix"}, {"type": "estrategico", "msisdn": "5511999999999", "name": "Aya E-Books"}, {"type": "estrategico", "msisdn": "5511988888888", "name": "YouTube Premium"}, ] payload = mcp_main._workflow_payload("vas_estrategico", {"msisdn": "5511999999999", "items": items}) assert len(payload["items"]) == 3 result = action(payload, {"input": payload}) assert len(result["linhas"]) == 2 first = result["linhas"][0] assert first["msisdn"] == "5511999999999" assert "Netflix" in first["bundle_names"] assert "Aya E-Books" in first["estrategico_names"] def test_vas_estrategico_rejeitado_declara_rag_para_cada_servico_estrategico(): from app.domain.contas.workflow_actions import build_contas_workflow_actions class Service: pass action = build_contas_workflow_actions(Service()).get("registrar_atendimento_vas_estrategico") params = { "linhas": [ {"msisdn": "5511999999999", "bundle_names": ["Netflix"], "estrategico_names": ["Aya E-Books"]}, {"msisdn": "5511988888888", "bundle_names": [], "estrategico_names": ["YouTube Premium"]}, ], "mensagem_base": "concluido", } result = action(params, {"input": {"resposta_usuario": "NAO"}}) assert result["requires_rag"] is True assert len(result["rag_queries"]) == 2 assert any("Aya E-Books" in q for q in result["rag_queries"]) assert any("YouTube Premium" in q for q in result["rag_queries"]) def test_cancelamento_sem_candidatos_nao_dispara_contestacao(monkeypatch): cancel = _result( output={"cancelar_vas_avulso": { "success": True, "cancelados": [{"subject": "VAS X", "success": True}], "contestation_candidates": [], "protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}], }} ) runtime = _Runtime(cancel) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({ "msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X"}], })) assert len(runtime.calls) == 1 assert result["success"] is True assert result["cancelamento_vas_protocol"] == "PRT-CANCEL-1" def test_cancelamento_falha_operacional_com_candidato_dispara_contestacao(monkeypatch): cancel = _result(output={"cancelar_vas_avulso": { "success": False, "cancelados": [], "nao_cancelados": [{"subject": "VAS X", "success": False, "error": "block_vas_failed"}], "contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00", "eligible_for_contestation": True}], "protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}], }}) contest = _result(output={ "registrar_protocolo": {"protocolo_id": "PRT-CONT-1"}, "abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "CRIAR", "itemName": "VAS X"}]}, "enviar_sms": {"success": True}, }) runtime = _Runtime(cancel, contest) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({ "msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}], "complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]}, })) assert [c[0] for c in runtime.calls] == ["cancelamento_vas_avulso", "contestacao_tool"] assert result["contestacao_protocol"] == "PRT-CONT-1" assert result["cancelamento_vas_protocol"] == "PRT-CANCEL-1" def test_cancelamento_sms_falho_preserva_sucesso_e_flag(monkeypatch): cancel = _result(output={"cancelar_vas_avulso": { "success": True, "cancelados": [{"subject": "VAS X", "success": True}], "contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00"}], "protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}], }}) contest = _result(output={ "registrar_protocolo": {"protocolo_id": "PRT-CONT-1"}, "abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "CRIAR", "itemName": "VAS X"}]}, "enviar_sms": {"success": False, "error": "sms indisponível"}, }) runtime = _Runtime(cancel, contest) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({ "msisdn": "11999999999", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}], "complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]}, })) assert result["success"] is True assert result["sms_not_send_error"] is True def test_cancelamento_aceita_cpf_como_alias_social_sec_no(monkeypatch): cancel = _result(output={"cancelar_vas_avulso": { "success": True, "cancelados": [{"servico": "VAS X"}], "contestation_candidates": [{"subject": "VAS X", "msisdn": "11999999999", "value": "10.00"}], "protocolos_por_linha": [{"protocolo_id": "PRT-CANCEL-1"}], }}) contest = _result(output={ "registrar_protocolo": {"protocolo_id": "PRT-CONT-1"}, "abrir_contestacao_cliente": {"success": True}, }) runtime = _Runtime(cancel, contest) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) asyncio.run(mcp_main._run_cancelamento_com_contestacao({ "msisdn": "11999999999", "cpf": "123.456.789-01", "subject": "VAS X", "items": [{"msisdn": "11999999999", "name": "VAS X", "value": "10.00"}], "complete_invoices_payload": {"billingProfile": {"customer": {"customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]}, })) _, cancel_payload, _ = runtime.calls[0] _, contest_payload, _ = runtime.calls[1] assert cancel_payload["social_sec_no"] == "12345678901" assert contest_payload["social_sec_no"] == "12345678901" def test_cancelamento_falha_workflow_preserva_output_parcial_nao_cancelados(monkeypatch): failed = _result( status="FAILED", error="Falha técnica ao processar serviços VAS no IU.", output={"cancelar_vas_avulso": { "cancelados": [], "nao_cancelados": [{"msisdn": "11964460192", "servico": "VOD + Canais Abertos", "erro": "Falha no IU", "motivo": "query_vas_failed"}], "nao_encontrados": [], }}, ) runtime = _Runtime(failed) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({"msisdn": "11964460192", "subject": "VOD + Canais Abertos"})) assert result["status"] == "FAILED" assert result["output"]["cancelar_vas_avulso"]["nao_cancelados"][0]["motivo"] == "query_vas_failed" def test_cancelamento_aceita_alias_itens_para_contestacao(monkeypatch): cancel = _result(output={"cancelar_vas_avulso": { "success": False, "cancelados": [], "itens_para_contestacao": [{"servico": "Inexistente", "msisdn": "11999999999", "valor": "14,99", "iu_reason": "service_not_found"}], "nao_encontrados": [{"servico": "Inexistente"}], }}) contest = _result(output={ "registrar_protocolo": {"protocolo_id": "PRT-CANDIDATO"}, "abrir_contestacao_cliente": {"success": True, "items_response": [{"status": "INICIADA", "itemName": "Inexistente"}]}, }) runtime = _Runtime(cancel, contest) monkeypatch.setattr(mcp_main, "get_workflow_runtime", lambda: runtime) result = asyncio.run(mcp_main._run_cancelamento_com_contestacao({ "msisdn": "11999999999", "subject": "Inexistente", "items": [{"msisdn": "11999999999", "name": "Inexistente", "value": "14,99"}], "complete_invoices_payload": {"billingProfile": {"customer": {"document": "123", "customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}]}, })) assert [c[0] for c in runtime.calls] == ["cancelamento_vas_avulso", "contestacao_tool"] assert result["contestacao_protocol"] == "PRT-CANDIDATO" def test_invoice_explanation_service_failure_preserva_fraseologia_sem_auto_finalize(): from contas_mcp.servers.contas_mcp_server.main import _result_payload result = { "execution_id": "exec-ie-falha", "workflow_name": "invoice_explanation", "workflow_version": 2, "status": "COMPLETED", "output": {"resposta_falha_servico": {"success": False, "service_failed": True, "mensagem": "texto incorreto"}}, "trace": [{"node": "resposta_falha_servico"}], "error": None, } payload = _result_payload(result, workflow_name="invoice_explanation") assert payload["success"] is False assert payload["service_failed"] is True assert payload["auto_finalize_on_failure"] is False assert payload["mensagem"] == "Para continuar com a sua solicitação, aguarde um instante."