from __future__ import annotations import json from pathlib import Path import pytest import yaml from app.domain.contas.service import ContasDomainService from app.domain.contas.workflow_actions import build_contas_workflow_actions from contas_mcp.servers.contas_mcp_server import main as mcp ROOT = Path(__file__).resolve().parents[2] REQUESTED = { "consultar_faturas", "consultar_plano", "invoice_explanation", "buscar_informacao", "consultar_vas", "consultar_historico_vas", "cancelar_vas_avulso", "tratar_vas_estrategico", "validar_contestacao", "contestar_cobranca", "finalizar_atendimento", "consultar_status_solicitacao", "enviar_sms", "recuperar_fatura_pdf", "pro_rata", "termino_desconto", "valor_divergente", "retomar_workflow", } def test_todas_as_capabilities_solicitadas_estao_expostas_no_mcp(): assert REQUESTED <= set(mcp.TOOLS) def test_todas_as_capabilities_solicitadas_estao_habilitadas_no_registry(): cfg = yaml.safe_load((ROOT / "config/tools.yaml").read_text(encoding="utf-8"))["tools"] assert REQUESTED <= set(cfg) assert all(cfg[name].get("enabled", True) for name in REQUESTED) def test_buscar_informacao_preserva_tool_original_mas_delega_rag_ao_framework(): out = ContasDomainService().buscar_informacao( queries=["Aya Audiobooks Premium o que é", "Food Balance o que é"] ) assert out["requires_rag"] is True assert out["source"] == "agent_framework.rag" assert out["rag_queries"] == ["Aya Audiobooks Premium o que é", "Food Balance o que é"] def test_consultar_plano_extrai_planos_da_evidencia_sem_llm(): billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8")) out = ContasDomainService().consultar_plano(msisdn="119", billing_analysis=billing) assert out["count"] == 2 assert {p["name"] for p in out["plans"]} == {"TIM Black A 8.0", "TIM CTRL Redes Sociais 8.0"} def test_invoice_explanation_preserva_detail_e_resumo_semantico_para_composicao(): svc = ContasDomainService() detail = {"parsed_content": {"total_geral": 197.43}} out = svc.invoice_explanation( msisdn="119", complete_invoices_payload={"paymentItems": []}, billing_analysis={"invoiceExplanation": "Base"}, invoice_detail=detail, invoice_amount="197.43", invoice_period="14/10 a 13/11", ) assert out["invoice_detail"] == detail assert out["invoice_amount"] == "197.43" assert out["invoice_period"] == "14/10 a 13/11" def test_invoice_explanation_restaura_composicao_llm_no_framework(): reg = build_contas_workflow_actions(ContasDomainService()) out = reg.get("formatar_invoice_explanation")( {"explicacao_base": "TIM Fashion R$ 10,00", "invoice_detail": {}}, {"input": {}} ) assert out["await_user_input"] is True assert out["requires_llm_composition"] is True assert "não invente" in out["response_instruction"] assert "Com essa explicação, sanei sua dúvida?" in out["mensagem"] def test_pro_rata_deriva_exatamente_dois_planos_do_pdf_sem_llm(): detail = json.loads((ROOT / "app/domain/contas/fixtures/invoice_pdf_include_danfe_true.json").read_text(encoding="utf-8")) args = {"msisdn": "11999999999", "invoice_detail": detail} plans = mcp._derive_pro_rata_plans(args) assert len(plans) == 2 assert {p["desc"] for p in plans} == {"TIM Black A 8.0", "TIM CTRL Redes Sociais 8.0"} assert mcp._prepare_pro_rata(args) is None assert args["has_plano_controle"] is True def test_pro_rata_falha_fechado_sem_exatamente_dois_planos(): args = {"msisdn": "119", "planos": [{"desc": "Plano único"}]} out = mcp._prepare_pro_rata(args) assert out["success"] is False assert out["reason"] == "requires_exactly_two_plans" assert out["plans_found"] == 1 @pytest.mark.asyncio async def test_validar_contestacao_usa_mesma_cval_e_rejeita_valor_acima_da_fatura(monkeypatch): billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8")) async def no_enrich(name, args): return None monkeypatch.setattr(mcp, "_enrich_invoice_context", no_enrich) out = await mcp._validate_contestation({ "msisdn": "11999999999", "subject": "TIM Fashion Mensal", "valor": 150, "motivo": "quero contestar TIM Fashion Mensal", "billing_analysis": billing, }) assert out["eligible"] is False assert out["reason"] == "CVAL" assert out["validation_log"][0]["erro"] == "valor_ajuste_maior_que_item" @pytest.mark.asyncio async def test_validar_contestacao_aprova_quando_item_e_valor_sao_comprovados(monkeypatch): billing = json.loads((ROOT / "app/domain/contas/fixtures/divergencia.json").read_text(encoding="utf-8")) async def no_enrich(name, args): return None monkeypatch.setattr(mcp, "_enrich_invoice_context", no_enrich) out = await mcp._validate_contestation({ "msisdn": "11999999999", "subject": "TIM Fashion Mensal", "valor": 50, "motivo": "quero contestar TIM Fashion Mensal", "billing_analysis": billing, }) assert out["eligible"] is True assert out["status"] == "ELIGIBLE" def test_termino_desconto_e_valor_divergente_preservam_semantica_do_original(): reg = build_contas_workflow_actions(ContasDomainService()) action = reg.get("formatar_capability_resposta") end = action({"tipo": "termino_desconto", "nome_plano": "TIM Black", "msisdn": "11999999999"}, {"input": {}}) div = action({"tipo": "valor_divergente", "msisdn": "11999999999"}, {"input": {}}) assert "desconto de fidelidade do plano TIM Black" in end["mensagem"] assert "período promocional contratado expirou" in end["mensagem"] assert "alteração no valor do plano" in div["mensagem"] assert "final 99" in div["mensagem"] def test_status_solicitacao_nao_confunde_message_id_com_protocolo(): mapping = yaml.safe_load((ROOT / "config/mcp_parameter_mapping.yaml").read_text(encoding="utf-8"))["mcp_parameter_mapping"]["tools"]["consultar_status_solicitacao"] assert "interaction_key" not in mapping.get("map", {}) assert "protocol" in mapping.get("extract", {}) def test_finalizacao_preserva_status_explicito_do_contrato_original(): cfg = yaml.safe_load((ROOT / "config/tools.yaml").read_text(encoding="utf-8"))["tools"]["finalizar_atendimento"] assert "status" in cfg.get("requires", []) mapping = yaml.safe_load((ROOT / "config/mcp_parameter_mapping.yaml").read_text(encoding="utf-8"))["mcp_parameter_mapping"]["tools"]["finalizar_atendimento"] assert "status" in mapping.get("extract", {}) class _DirectClient: def consultar_vas(self, msisdn): return {"products": [{"name": "VAS"}]} def historico_vas(self, msisdn): return {"services": [{"name": "OLD"}]} def status_sr(self, payload): return {"protocolNumber": payload.get("protocolNumber"), "status": "Fechado"} def sms(self, msisdn, message): return {"sent": True, "msisdn": msisdn, "message": message} def secure_pdf(self, msisdn, invoice_id, customer_id=""): return {"invoiceId": invoice_id, "url": "secure"} def test_tools_diretas_vas_historico_status_sms_pdf_mantem_contratos_de_dominio(): svc = ContasDomainService(client=_DirectClient()) # type: ignore[arg-type] assert svc.consultar_vas(msisdn="119")["products"][0]["name"] == "VAS" assert svc.consultar_historico_vas(msisdn="119")["services"][0]["name"] == "OLD" assert svc.consultar_status_solicitacao(msisdn="119", protocol="P1")["protocolNumber"] == "P1" assert svc.enviar_sms(msisdn="119", message="ok")["sent"] is True assert svc.recuperar_fatura_pdf(msisdn="119", invoice_id="I1")["invoiceId"] == "I1"