from __future__ import annotations import pytest from agent_framework.cache.cache import InMemoryCache from app.domain.contas.invoice_context import InvoiceContextService class Client: def __init__(self): self.complete_calls = 0 self.billing_calls = 0 self.pdf_calls = 0 def consultar_faturas(self, msisdn): self.complete_calls += 1 return { "billingProfile": {"customer": {"customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}], } def billing_analysis(self, msisdn, **kwargs): self.billing_calls += 1 return {"invoiceExplanation": "explicacao"} def bill_pdf(self, msisdn, invoice_id, customer_id, **kwargs): self.pdf_calls += 1 return { "status": "SUCCESS", "parsed_content": { "total_geral": 191.97, "Fatura Resumo": [ {"desc": "Período", "period": "01/04/2026 a 30/04/2026"}, {"desc": "Emissão", "emissao": "01/04/2026"}, ], }, } @pytest.mark.asyncio async def test_invoice_context_reusa_cache_na_mesma_sessao(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] a = await svc.get(session_id="s1", msisdn="119", include_detail=True) b = await svc.get(session_id="s1", msisdn="119", include_detail=True) assert a.cache_hit is False assert b.cache_hit is True assert client.complete_calls == 1 assert client.billing_calls == 1 assert client.pdf_calls == 1 @pytest.mark.asyncio async def test_invoice_context_isola_cache_por_sessao(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] await svc.get(session_id="s1", msisdn="119") await svc.get(session_id="s2", msisdn="119") assert client.complete_calls == 2 assert client.billing_calls == 2 @pytest.mark.asyncio async def test_invoice_context_sem_session_nao_compartilha_cache_global(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] await svc.get(session_id="", msisdn="119") await svc.get(session_id="", msisdn="119") assert client.complete_calls == 2 assert client.billing_calls == 2 @pytest.mark.asyncio async def test_invoice_context_use_cache_false_forca_nova_busca(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] await svc.get(session_id="s1", msisdn="119") await svc.get(session_id="s1", msisdn="119", use_cache=False) assert client.complete_calls == 2 assert client.billing_calls == 2 def test_invoice_explanation_reusa_evidencia_prefetch_sem_nova_api(): from app.domain.contas.service import ContasDomainService class C(Client): def consultar_faturas(self, msisdn): raise AssertionError("nao deveria consultar novamente") def billing_analysis(self, msisdn, **kwargs): raise AssertionError("nao deveria consultar novamente") service = ContasDomainService(client=C()) # type: ignore[arg-type] result = service.invoice_explanation( msisdn="119", complete_invoices_payload={"paymentItems": []}, billing_analysis={"invoiceExplanation": "cache"}, ) assert result["billing_analysis"]["invoiceExplanation"] == "cache" @pytest.mark.asyncio async def test_invoice_context_singleflight_mesma_sessao_nao_duplica_chamadas(): import asyncio class Blocking(Client): def __init__(self): super().__init__() self.release = asyncio.Event() def consultar_faturas(self, msisdn): import time self.complete_calls += 1 time.sleep(0.08) return { "billingProfile": {"customer": {"customerId": "C1"}}, "paymentItems": [{"invoiceId": "I1"}], } def billing_analysis(self, msisdn, **kwargs): import time self.billing_calls += 1 time.sleep(0.08) return {"invoiceExplanation": "explicacao"} client = Blocking() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] a, b = await asyncio.gather( svc.get(session_id="s1", msisdn="119", message_id="m1"), svc.get(session_id="s1", msisdn="119", message_id="m1"), ) assert client.complete_calls == 1 assert client.billing_calls == 1 assert a.complete_invoices == b.complete_invoices @pytest.mark.asyncio async def test_invoice_context_prefetch_emite_cvn_uma_vez_por_sessao(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] first = await svc.get(session_id="s1", msisdn="119", message_id="msg-1", use_cache=False) second = await svc.get(session_id="s1", msisdn="119", message_id="msg-2", use_cache=False) assert [x["code"] for x in first.business_events or []] == ["CVN.002", "CVN.006"] assert second.business_events == [] assert first.business_events[0]["payload"]["messageId"] == "msg-1" assert first.business_events[1]["payload"]["agentSpecificData"] == '{"billingId": "I1"}' @pytest.mark.asyncio async def test_invoice_context_nova_sessao_pode_emitir_cvn_novamente(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] first = await svc.get(session_id="s1", msisdn="119", use_cache=False) second = await svc.get(session_id="s2", msisdn="119", use_cache=False) assert [x["code"] for x in first.business_events or []] == ["CVN.002", "CVN.006"] assert [x["code"] for x in second.business_events or []] == ["CVN.002", "CVN.006"] @pytest.mark.asyncio async def test_invoice_context_billing_analysis_falha_emite_cvn007_sem_perder_complete_invoices(): class Failing(Client): def billing_analysis(self, msisdn, **kwargs): self.billing_calls += 1 raise RuntimeError("HTTP 550") client = Failing() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] ctx = await svc.get(session_id="s1", msisdn="119", message_id="msg-fail", use_cache=False) assert ctx.complete_invoices is not None assert ctx.billing_analysis is None assert "billing_analysis" in str(ctx.error) assert [x["code"] for x in ctx.business_events or []] == ["CVN.002", "CVN.007"] @pytest.mark.asyncio async def test_invoice_context_cache_incompleto_busca_detail_quando_solicitado_depois(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] await svc.get(session_id="s1", msisdn="119", include_detail=False) detailed = await svc.get(session_id="s1", msisdn="119", include_detail=True) assert detailed.invoice_detail is not None assert client.pdf_calls == 1 @pytest.mark.asyncio async def test_invoice_context_expoe_timings_e_erros_por_subconsulta(): class Partial(Client): def billing_analysis(self, msisdn, **kwargs): self.billing_calls += 1 raise RuntimeError("indisponivel") client = Partial() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] ctx = await svc.get(session_id="s1", msisdn="119", include_detail=True, use_cache=False) assert ctx.metadata["cache_hit"] is False assert ctx.metadata["fetch_elapsed_ms"] >= 0 assert {"complete_invoices", "billing_analysis", "invoice_detail"} <= set(ctx.metadata["task_timings"]) assert "billing_analysis" in ctx.errors @pytest.mark.asyncio async def test_invoice_context_cache_hit_expoe_cache_age_sem_republicar_eventos(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] await svc.get(session_id="s1", msisdn="119") cached = await svc.get(session_id="s1", msisdn="119") assert cached.cache_hit is True assert cached.metadata["cache_hit"] is True assert cached.metadata["cache_age_ms"] >= 0 assert cached.business_events == [] def test_extract_invoice_summary_context_restaura_semantica_do_original(): from app.domain.contas.invoice_context import extract_invoice_summary_context result = extract_invoice_summary_context({ "parsed_content": { "total_geral": 191.97, "Fatura Resumo": [ {"desc": "Período", "period": "01/04/2026 a 30/04/2026"}, {"desc": "Emissão", "emissao": "01/04/2026"}, ], } }) assert result == { "invoice_amount": "191.97", "invoice_amount_open": "191.97", "invoice_period": "01/04/2026 a 30/04/2026", "invoice_emissao": "01/04/2026", } @pytest.mark.asyncio async def test_invoice_context_detail_expoe_invoice_amount_semantico(): client = Client() svc = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] ctx = await svc.get(session_id="s1", msisdn="119", include_detail=True) assert ctx.invoice_amount == "191.97" assert ctx.invoice_amount_open == "191.97" assert ctx.invoice_period == "01/04/2026 a 30/04/2026" assert ctx.invoice_emissao == "01/04/2026" assert ctx.as_dict()["invoice_amount"] == "191.97" def test_consultar_faturas_reusa_prefetch_e_publica_valor_sem_reconsultar_backend(): from app.domain.contas.service import ContasDomainService class C(Client): def consultar_faturas(self, msisdn): raise AssertionError("nao deveria consultar novamente") svc = ContasDomainService(client=C()) # type: ignore[arg-type] result = svc.consultar_faturas( msisdn="119", complete_invoices_payload={"paymentItems": [{"invoiceId": "I1"}]}, invoice_id="I1", customer_id="C1", invoice_amount="191.97", invoice_amount_open="191.97", ) assert result["invoice_amount"] == "191.97" assert result["invoice_amount_open"] == "191.97" assert result["invoice_id"] == "I1" assert result["customer_id"] == "C1" @pytest.mark.asyncio async def test_mcp_consultar_faturas_enriquece_com_detail_e_valor(monkeypatch): from contas_mcp.servers.contas_mcp_server import main client = Client() context_service = InvoiceContextService(client, InMemoryCache(), ttl_seconds=60) # type: ignore[arg-type] monkeypatch.setattr(main, "get_invoice_context_service", lambda: context_service) args = {"msisdn": "119", "session_id": "s1", "message_id": "m1"} await main._enrich_invoice_context("consultar_faturas", args) assert client.pdf_calls == 1 assert args["invoice_amount"] == "191.97" assert args["invoice_amount_open"] == "191.97" assert args["complete_invoices_payload"]["paymentItems"][0]["invoiceId"] == "I1"