670 lines
28 KiB
Python
670 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
class FakeRuntime:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "exec-cancel",
|
|
"workflow_name": name,
|
|
"workflow_version": 1,
|
|
"status": "COMPLETED",
|
|
"output": {
|
|
"cancelar_vas_avulso": {
|
|
"success": True,
|
|
"results": [
|
|
{
|
|
"success": True,
|
|
"msisdn": "5511999999999",
|
|
"subject": "TIM Fashion Mensal",
|
|
}
|
|
],
|
|
"contestation_candidates": [
|
|
{
|
|
"success": True,
|
|
"msisdn": "5511999999999",
|
|
"subject": "TIM Fashion Mensal",
|
|
}
|
|
],
|
|
}
|
|
},
|
|
"state": {},
|
|
"trace": [],
|
|
}
|
|
assert name == "contestacao_tool"
|
|
return {
|
|
"execution_id": "exec-cont",
|
|
"workflow_name": name,
|
|
"workflow_version": 2,
|
|
"status": "COMPLETED",
|
|
"output": {
|
|
"registrar_protocolo": {"protocolo_id": "PRT-100"},
|
|
"abrir_contestacao_cliente": {
|
|
"success": True,
|
|
"items_response": [
|
|
{
|
|
"correctAccountStatus": "CRIAR",
|
|
"itemName": "TIM Fashion Mensal",
|
|
"status": "INICIADA",
|
|
}
|
|
],
|
|
"contested_invoice_amount_open": "14,99",
|
|
},
|
|
},
|
|
"state": {},
|
|
"trace": [],
|
|
}
|
|
|
|
|
|
class FakeService:
|
|
def consultar_faturas(self, *, msisdn: str):
|
|
assert msisdn == "5511999999999"
|
|
return {
|
|
"billingProfile": {
|
|
"customer": {
|
|
"document": "12345678901",
|
|
"customerId": "344575482",
|
|
}
|
|
},
|
|
"paymentItems": [
|
|
{
|
|
"invoiceId": "5536806781",
|
|
"dueDate": "2025-08-11",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_encadeia_contestacao_no_workflow_runtime(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
runtime = FakeRuntime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
|
|
result = await main._run_cancelamento_com_contestacao(
|
|
{
|
|
"msisdn": "5511999999999",
|
|
"subject": "TIM Fashion Mensal",
|
|
"items": [
|
|
{
|
|
"msisdn": "5511999999999",
|
|
"name": "TIM Fashion Mensal",
|
|
"value": "14,99",
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
assert [call[0] for call in runtime.calls] == [
|
|
"cancelamento_vas_avulso",
|
|
"contestacao_tool",
|
|
]
|
|
contest_payload = runtime.calls[1][1]
|
|
assert contest_payload["social_sec_no"] == "12345678901"
|
|
assert contest_payload["customer_id"] == "344575482"
|
|
assert contest_payload["current_invoice_number"] == "5536806781"
|
|
assert contest_payload["items"][0]["itemName"] == "TIM Fashion Mensal"
|
|
assert contest_payload["items"][0]["claimedAmount"] == "14,99"
|
|
assert result["contestacao_protocol"] == "PRT-100"
|
|
assert result["protocols_for_response"] == ["PRT-100"]
|
|
assert result["requires_protocol_in_response"] is True
|
|
assert result["metadata"]["composite_workflows"] == [
|
|
"cancelamento_vas_avulso",
|
|
"contestacao_tool",
|
|
]
|
|
assert result["mensagem"].endswith("Seu número de protocolo é PRT-100.")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_sem_candidato_nao_abre_contestacao(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class NoMatchRuntime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
return {
|
|
"execution_id": "exec-cancel",
|
|
"workflow_name": name,
|
|
"workflow_version": 1,
|
|
"status": "COMPLETED",
|
|
"output": {
|
|
"cancelar_vas_avulso": {
|
|
"success": False,
|
|
"results": [
|
|
{
|
|
"success": False,
|
|
"msisdn": payload["items"][0]["msisdn"],
|
|
"subject": payload["items"][0]["name"],
|
|
"error": "Serviço não encontrado entre os VAS ativos",
|
|
}
|
|
],
|
|
"contestation_candidates": [],
|
|
}
|
|
},
|
|
"state": {},
|
|
"trace": [],
|
|
}
|
|
|
|
runtime = NoMatchRuntime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
result = await main._run_cancelamento_com_contestacao(
|
|
{"msisdn": "5511999999999", "subject": "Inexistente"}
|
|
)
|
|
assert [call[0] for call in runtime.calls] == ["cancelamento_vas_avulso"]
|
|
assert result["metadata"]["composite_workflows"] == ["cancelamento_vas_avulso"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_action_cancelamento_lote_aproveita_um_sucesso_entre_falhas():
|
|
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
|
|
|
class Service:
|
|
def cancelar_vas_avulso(self, *, msisdn, subject, protocol=""):
|
|
if subject == "OK":
|
|
return {"success": True, "service": {"name": subject}}
|
|
return {"success": False, "subject": subject, "reason": "service_not_found", "error": "não encontrado"}
|
|
|
|
class Store:
|
|
@staticmethod
|
|
def canonical_key(*parts): return "|".join(map(str, parts))
|
|
async def get(self, key): return None
|
|
async def set(self, key, value, ttl_seconds=None): return None
|
|
|
|
reg = build_contas_workflow_actions(Service(), idempotency_store=Store()) # type: ignore[arg-type]
|
|
action = reg.get("cancelamento_vas_avulso_batch")
|
|
result = await action(
|
|
{"items": [
|
|
{"msisdn": "119", "name": "FALHA1"},
|
|
{"msisdn": "119", "name": "OK"},
|
|
{"msisdn": "119", "name": "FALHA2"},
|
|
]},
|
|
{"input": {}, "execution_id": "e1"},
|
|
)
|
|
assert result["success"] is True
|
|
assert len(result["cancelados"]) == 1
|
|
assert len(result["nao_encontrados"]) == 2
|
|
assert len(result["itens_para_contestacao"]) == 1
|
|
assert result["itens_para_contestacao"][0]["subject"] == "OK"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plano_familia_mantem_titular_na_contestacao_e_dependente_no_cancelamento(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class FamilyRuntime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "exec-cancel",
|
|
"workflow_name": name,
|
|
"workflow_version": 1,
|
|
"status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {
|
|
"success": True,
|
|
"results": [{"success": True, "msisdn": "5511999999999", "subject": "TIM Fashion Mensal"}],
|
|
"contestation_candidates": [{"success": True, "msisdn": "5511999999999", "subject": "TIM Fashion Mensal"}],
|
|
}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
assert name == "contestacao_tool"
|
|
return {
|
|
"execution_id": "exec-cont", "workflow_name": name, "workflow_version": 2,
|
|
"status": "COMPLETED",
|
|
"output": {"registrar_protocolo": {"protocolo_id": "PRT-FAM"}, "abrir_contestacao_cliente": {"success": True}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = FamilyRuntime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
prefetch = {"billingProfile": {"customer": {"document": "12345678901", "customerId": "344575482"}}, "paymentItems": [{"invoiceId": "5536806781"}]}
|
|
result = await main._run_cancelamento_com_contestacao({
|
|
"msisdn": "5511888880000", # titular
|
|
"items": [{"msisdn": "5511999999999", "name": "TIM Fashion Mensal", "value": "14,99"}],
|
|
"complete_invoices_payload": prefetch,
|
|
})
|
|
assert result["contestacao_protocol"] == "PRT-FAM"
|
|
cancel_payload = runtime.calls[0][1]
|
|
assert cancel_payload["msisdn"] == "5511888880000"
|
|
assert cancel_payload["items"][0]["msisdn"] == "5511999999999"
|
|
contest_payload = runtime.calls[1][1]
|
|
assert contest_payload["msisdn"] == "5511888880000"
|
|
assert contest_payload["item_msisdn"] == "5511888880000"
|
|
assert contest_payload["dependent_invoice_item"] is True
|
|
assert contest_payload["complete_invoices_payload"] == prefetch
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invoice_detail_corrige_linha_do_item_antes_do_cancelamento(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class Runtime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
return {
|
|
"execution_id": "exec-cancel", "workflow_name": name, "workflow_version": 1,
|
|
"status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {"success": True, "results": [], "contestation_candidates": []}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = Runtime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
await main._run_cancelamento_com_contestacao({
|
|
"msisdn": "11964460192",
|
|
"items": [
|
|
{"msisdn": "11964460192", "name": "Aluguel de Filme 2", "value": 19.9},
|
|
{"msisdn": "11964460192", "name": "Aluguel de Filme 3", "value": 19.9},
|
|
],
|
|
"invoice_detail": {
|
|
"11964460192": {"SVA Detalhe Total": [{"desc": "Aluguel de Filme 2", "value": 19.9, "msisdn": "11964460192"}]},
|
|
"11964490143": {"SVA Detalhe Total": [{"desc": "Aluguel de Filme 3", "value": 19.9, "msisdn": "11964490143"}]},
|
|
},
|
|
})
|
|
items = runtime.calls[0][1]["items"]
|
|
assert items[0]["msisdn"] == "11964460192"
|
|
assert items[1]["msisdn"] == "11964490143"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_propaga_bloqueio_da_contestacao_sem_mascarar_sucesso(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class Runtime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "c", "workflow_name": name, "workflow_version": 1, "status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {"success": True, "results": [{"success": True, "msisdn": payload["msisdn"], "subject": "Disney"}], "contestation_candidates": [{"success": True, "msisdn": payload["msisdn"], "subject": "Disney"}]}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
return {
|
|
"execution_id": "x", "workflow_name": name, "workflow_version": 2, "status": "COMPLETED",
|
|
"output": {"abrir_contestacao_cliente": {"success": False, "blocked": True, "guardrail_code": "CVAL", "guardrail_reason": "VAS estratégico não pode ser ajustado"}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = Runtime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
result = await main._run_cancelamento_com_contestacao({"msisdn": "5511999999999", "subject": "Disney", "valor": 10})
|
|
assert result["success"] is False
|
|
assert result["erro_sistemico"] is True
|
|
assert result["mensagem"] == "VAS estratégico não pode ser ajustado"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_lote_nao_executa_side_effect_se_protocolo_por_linha_falhar():
|
|
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
|
|
|
class Client:
|
|
def abrir_protocolo(self, payload):
|
|
return {}
|
|
|
|
class Service:
|
|
client = Client()
|
|
def __init__(self): self.cancel_calls = 0
|
|
def cancelar_vas_avulso(self, *, msisdn, subject, protocol=""):
|
|
self.cancel_calls += 1
|
|
return {"success": True}
|
|
|
|
class Store:
|
|
@staticmethod
|
|
def canonical_key(*parts): return "|".join(map(str, parts))
|
|
async def get(self, key): return None
|
|
async def set(self, key, value, ttl_seconds=None): return None
|
|
|
|
service = Service()
|
|
reg = build_contas_workflow_actions(service, idempotency_store=Store()) # type: ignore[arg-type]
|
|
result = await reg.get("cancelamento_vas_avulso_batch")(
|
|
{"items": [{"msisdn": "119", "name": "VAS A"}], "message_id": "m1"},
|
|
{"input": {}, "execution_id": "e1"},
|
|
)
|
|
assert result["success"] is False
|
|
assert result["nao_cancelados"][0]["reason"] == "protocol_registration_failed"
|
|
assert service.cancel_calls == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_lote_limita_concorrencia_a_cinco(monkeypatch):
|
|
import threading
|
|
import time
|
|
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
|
|
|
monkeypatch.setenv("TIM_CANCELAMENTO_BATCH_CONCURRENCY", "5")
|
|
|
|
class Service:
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self.active = 0
|
|
self.max_active = 0
|
|
def cancelar_vas_avulso(self, *, msisdn, subject, protocol=""):
|
|
with self.lock:
|
|
self.active += 1
|
|
self.max_active = max(self.max_active, self.active)
|
|
time.sleep(0.03)
|
|
with self.lock:
|
|
self.active -= 1
|
|
return {"success": True, "service": {"name": subject}}
|
|
|
|
class Store:
|
|
@staticmethod
|
|
def canonical_key(*parts): return "|".join(map(str, parts))
|
|
async def get(self, key): return None
|
|
async def set(self, key, value, ttl_seconds=None): return None
|
|
|
|
service = Service()
|
|
reg = build_contas_workflow_actions(service, idempotency_store=Store()) # type: ignore[arg-type]
|
|
items = [{"msisdn": "119", "name": f"VAS {i}", "protocol": f"P{i}"} for i in range(10)]
|
|
result = await reg.get("cancelamento_vas_avulso_batch")(
|
|
{"items": items}, {"input": {}, "execution_id": "e1"}
|
|
)
|
|
assert result["success"] is True
|
|
assert len(result["cancelados"]) == 10
|
|
assert service.max_active == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_falha_de_bloqueio_permanece_candidata_a_contestacao():
|
|
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
|
|
|
class Client:
|
|
def consultar_vas(self, msisdn):
|
|
return {"products": [{"name": "TIM Fashion Mensal", "canCancel": True, "appId": "1", "cspId": "740"}]}
|
|
def historico_vas(self, msisdn): return {"products": []}
|
|
def bloquear_vas(self, msisdn, service): raise RuntimeError("Timeout no bloqueio")
|
|
def cancelar_vas(self, msisdn, service, protocol=""):
|
|
raise AssertionError("cancelamento não deve ocorrer após falha de bloqueio")
|
|
|
|
class Service:
|
|
client = Client()
|
|
def cancelar_vas_avulso(self, *, msisdn, subject, protocol=""):
|
|
from app.domain.contas.service import ContasDomainService
|
|
return ContasDomainService(self.client).cancelar_vas_avulso(msisdn=msisdn, subject=subject, protocol=protocol)
|
|
|
|
class Store:
|
|
@staticmethod
|
|
def canonical_key(*parts): return "|".join(map(str, parts))
|
|
async def get(self, key): return None
|
|
async def set(self, key, value, ttl_seconds=None): return None
|
|
|
|
# Pass a pre-existing protocol so this unit test isolates the VAS side effect.
|
|
reg = build_contas_workflow_actions(Service(), idempotency_store=Store()) # type: ignore[arg-type]
|
|
result = await reg.get("cancelamento_vas_avulso_batch")(
|
|
{"items": [{"msisdn": "5511999999999", "name": "TIM Fashion Mensal", "protocol": "PRT-1"}]},
|
|
{"input": {}, "execution_id": "e-block"},
|
|
)
|
|
assert result["success"] is False
|
|
assert result["results"][0]["reason"] == "block_vas_failed"
|
|
assert result["results"][0]["eligible_for_contestation"] is True
|
|
assert len(result["itens_para_contestacao"]) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resposta_composta_preserva_protocolos_cancelamento_e_contestacao(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class Runtime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "cancel", "workflow_name": name, "workflow_version": 1, "status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {
|
|
"success": True,
|
|
"results": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal"}],
|
|
"contestation_candidates": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal"}],
|
|
"protocolos_por_linha": [{"msisdn": "5511999999999", "protocolo_id": "PRT-CANCEL-1"}],
|
|
}}, "state": {}, "trace": [],
|
|
}
|
|
return {
|
|
"execution_id": "cont", "workflow_name": name, "workflow_version": 2, "status": "COMPLETED",
|
|
"output": {
|
|
"registrar_protocolo": {"protocolo_id": "PRT-CONTEST-1", "protocol_closed": True},
|
|
"abrir_contestacao_cliente": {"success": True, "items_response": []},
|
|
}, "state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = Runtime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
result = await main._run_cancelamento_com_contestacao({
|
|
"msisdn": "5511999999999", "subject": "Tamboro Mensal", "valor": "14,99",
|
|
})
|
|
assert result["success"] is True
|
|
assert result["recomenda_finalizacao"] is True
|
|
assert result["status_finalizacao_sugerido"] == "resolvido"
|
|
assert result["protocols_for_response"] == ["PRT-CANCEL-1", "PRT-CONTEST-1"]
|
|
assert result["requires_protocol_in_response"] is True
|
|
assert result["protocol_closed"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_dependente_resolve_cpf_da_linha_antes_do_protocolo(monkeypatch):
|
|
from app.domain.contas.workflow_actions import build_contas_workflow_actions
|
|
|
|
recorded = {}
|
|
class Client:
|
|
def line_info(self, msisdn):
|
|
assert msisdn == "5511999999999"
|
|
return {"social_sec_no": "CPF-DEPENDENTE"}
|
|
def abrir_protocolo(self, payload):
|
|
recorded.update(payload)
|
|
return {"protocol": "PRT-DEP"}
|
|
|
|
class Service:
|
|
client = Client()
|
|
def cancelar_vas_avulso(self, *, msisdn, subject, protocol=""):
|
|
return {"success": True, "service": {"name": subject}}
|
|
|
|
class Store:
|
|
@staticmethod
|
|
def canonical_key(*parts): return "|".join(map(str, parts))
|
|
async def get(self, key): return None
|
|
async def set(self, key, value, ttl_seconds=None): return None
|
|
|
|
reg = build_contas_workflow_actions(Service(), idempotency_store=Store()) # type: ignore[arg-type]
|
|
result = await reg.get("cancelamento_vas_avulso_batch")(
|
|
{
|
|
"msisdn": "5511888880000",
|
|
"social_sec_no": "CPF-TITULAR",
|
|
"items": [{"msisdn": "5511999999999", "name": "TIM Fashion Mensal"}],
|
|
},
|
|
{"input": {}, "execution_id": "e-family"},
|
|
)
|
|
assert result["success"] is True
|
|
assert recorded["socialSecNo"] == "CPF-DEPENDENTE"
|
|
assert recorded["msisdn"] == "5511999999999"
|
|
|
|
|
|
def test_result_payload_invoice_explanation_recomenda_finalizacao_por_last_node():
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
accepted = main._result_payload({
|
|
"execution_id": "ie-1",
|
|
"workflow_name": "invoice_explanation",
|
|
"workflow_version": 2,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": "registrar_protocolo_aceite"}],
|
|
"error_details": {},
|
|
}, workflow_name="invoice_explanation")
|
|
assert accepted["recomenda_finalizacao"] is True
|
|
assert accepted["status_finalizacao_sugerido"] == "resolvido"
|
|
|
|
refused = main._result_payload({
|
|
"execution_id": "ie-2",
|
|
"workflow_name": "invoice_explanation",
|
|
"workflow_version": 2,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": "finalizar_nao_resolvido"}],
|
|
"error_details": {},
|
|
}, workflow_name="invoice_explanation")
|
|
assert refused["recomenda_finalizacao"] is True
|
|
assert refused["status_finalizacao_sugerido"] == "nao_resolvido"
|
|
|
|
vas_branch = main._result_payload({
|
|
"execution_id": "ie-3",
|
|
"workflow_name": "invoice_explanation",
|
|
"workflow_version": 2,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": "checar_vas_variacao"}],
|
|
"error_details": {},
|
|
}, workflow_name="invoice_explanation")
|
|
assert "recomenda_finalizacao" not in vas_branch
|
|
|
|
|
|
def test_result_payload_pro_rata_recomenda_finalizacao_somente_nos_ramos_finais():
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
for node in ("registrar_aceitou", "registrar_nao_controle"):
|
|
result = main._result_payload({
|
|
"execution_id": f"pr-{node}",
|
|
"workflow_name": "pro_rata",
|
|
"workflow_version": 3,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": node}],
|
|
"error_details": {},
|
|
}, workflow_name="pro_rata")
|
|
assert result["recomenda_finalizacao"] is True
|
|
assert result["status_finalizacao_sugerido"] == "resolvido"
|
|
|
|
unresolved = main._result_payload({
|
|
"execution_id": "pr-no",
|
|
"workflow_name": "pro_rata",
|
|
"workflow_version": 3,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": "devolver_orquestrador"}],
|
|
"error_details": {},
|
|
}, workflow_name="pro_rata")
|
|
assert "recomenda_finalizacao" not in unresolved
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_redireciona_item_estrategico_para_workflow_correto(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
from types import SimpleNamespace
|
|
|
|
resolved = SimpleNamespace(
|
|
canonical_name="Paramount+ Mensal",
|
|
msisdn="5511999999999",
|
|
charge_date=None,
|
|
value=12.34,
|
|
tool_category="vas_estrategico",
|
|
item_type="estrategico",
|
|
)
|
|
outcome = SimpleNamespace(ambiguous=[], resolved=[resolved], out_of_scope=[])
|
|
monkeypatch.setattr(main.invoice_resolver, "resolve", lambda *_args, **_kwargs: outcome)
|
|
monkeypatch.setattr(main.service.client, "billing_analysis", lambda _msisdn: {"currentInvoice": {}})
|
|
monkeypatch.setattr(main, "_enrich_invoice_context", lambda *args, **kwargs: __import__('asyncio').sleep(0))
|
|
|
|
class Runtime:
|
|
async def arun(self, name, payload, execution_id=None):
|
|
assert name == "vas_estrategico"
|
|
assert payload["items"] == [{
|
|
"type": "estrategico",
|
|
"msisdn": "5511999999999",
|
|
"name": "Paramount+ Mensal",
|
|
}]
|
|
return {
|
|
"execution_id": "exec-vas",
|
|
"workflow_name": name,
|
|
"workflow_version": 3,
|
|
"status": "COMPLETED",
|
|
"output": {},
|
|
"state": {},
|
|
"trace": [{"node": "registrar_atendimento"}],
|
|
"error_details": {},
|
|
}
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: Runtime())
|
|
|
|
result = await main._invoke("cancelar_vas_avulso", {
|
|
"msisdn": "5511999999999",
|
|
"subject": "Paramount+ Mensal",
|
|
"billing_analysis": {"currentInvoice": {}},
|
|
"complete_invoices_payload": {},
|
|
})
|
|
assert result["metadata"]["domain_redirect_from"] == "cancelar_vas_avulso"
|
|
assert result["metadata"]["domain_redirect_to"] == "tratar_vas_estrategico"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_usa_valor_vas_validado_quando_usuario_nao_informa_valor(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class Runtime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "cancel-valor", "workflow_name": name, "workflow_version": 1, "status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {
|
|
"success": True,
|
|
"results": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal",
|
|
"service": {"name": "Tamboro Mensal", "details": {"valor": "14,99"}}}],
|
|
"contestation_candidates": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal",
|
|
"service": {"name": "Tamboro Mensal", "details": {"valor": "14,99"}}}],
|
|
}}, "state": {}, "trace": [],
|
|
}
|
|
return {
|
|
"execution_id": "cont-valor", "workflow_name": name, "workflow_version": 2, "status": "COMPLETED",
|
|
"output": {"registrar_protocolo": {"protocolo_id": "PRT-V"}, "abrir_contestacao_cliente": {"success": True, "items_response": []}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = Runtime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
await main._run_cancelamento_com_contestacao({"msisdn": "5511999999999", "subject": "Tamboro Mensal"})
|
|
item = runtime.calls[1][1]["items"][0]
|
|
assert item["claimedAmount"] == "14,99"
|
|
assert item["validatedAmount"] == "14,99"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelamento_preserva_valor_informado_e_valor_vas_como_evidencias_distintas(monkeypatch):
|
|
import contas_mcp.servers.contas_mcp_server.main as main
|
|
|
|
class Runtime(FakeRuntime):
|
|
async def arun(self, name, payload, execution_id=None):
|
|
self.calls.append((name, payload, execution_id))
|
|
if name == "cancelamento_vas_avulso":
|
|
return {
|
|
"execution_id": "cancel-div", "workflow_name": name, "workflow_version": 1, "status": "COMPLETED",
|
|
"output": {"cancelar_vas_avulso": {
|
|
"success": True,
|
|
"results": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal",
|
|
"service": {"name": "Tamboro Mensal", "details": {"valor": "14,99"}}}],
|
|
"contestation_candidates": [{"success": True, "msisdn": "5511999999999", "subject": "Tamboro Mensal",
|
|
"service": {"name": "Tamboro Mensal", "details": {"valor": "14,99"}}}],
|
|
}}, "state": {}, "trace": [],
|
|
}
|
|
return {
|
|
"execution_id": "cont-div", "workflow_name": name, "workflow_version": 2, "status": "COMPLETED",
|
|
"output": {"registrar_protocolo": {"protocolo_id": "PRT-D"}, "abrir_contestacao_cliente": {"success": True, "items_response": []}},
|
|
"state": {}, "trace": [],
|
|
}
|
|
|
|
runtime = Runtime()
|
|
monkeypatch.setattr(main, "get_workflow_runtime", lambda: runtime)
|
|
monkeypatch.setattr(main, "service", FakeService())
|
|
await main._run_cancelamento_com_contestacao({
|
|
"msisdn": "5511999999999", "subject": "Tamboro Mensal", "valor": "29,98",
|
|
})
|
|
item = runtime.calls[1][1]["items"][0]
|
|
assert item["claimedAmount"] == "29,98"
|
|
assert item["validatedAmount"] == "14,99"
|