fix(emission): validate Σ vPag against the document total (vNF)

IMPORTANT (F2 review): the service promises to validate structural
coherence (sums, required leiaute fields) per design spec decision #3, but
the one sum that can genuinely diverge -- pagamento.valor vs the document
total -- was unchecked. The lib documents SEFAZ rejects this with 'Valor do
Pagamento difere do total' (sowai_fiscal.xml_builder).

Adds PagamentoTotalMismatchError, computed in the completeness block
(before allocate_fiscal_number) by reusing sowai_fiscal.xml_builder.
soma_itens_quantizados -- the SAME function the lib's own _build_total uses
for vNF/vProd, avoiding a second source of truth for the same total.
Decimal-exact comparison (never float), both sides quantized to cents the
same way _build_pag does. Router maps it to 409 pagamento_total_diverge.

Test: tests/emission/test_emissao.py::
test_pagamento_divergente_do_total_dos_itens_e_409_e_nao_queima_numero --
pagamento.valor != Σ itens -> 409 pagamento_total_diverge, no fiscal number
consumed. The existing golden-backed happy-path tests (already balanced)
continue to prove the matching-totals case still succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jonatanritter
2026-08-08 15:34:24 -03:00
co-authored by Claude Opus 4.8
parent 72bb089222
commit 0b64602716
3 changed files with 87 additions and 4 deletions
+40
View File
@@ -15,6 +15,7 @@ import importlib.resources
import json
import uuid
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from pathlib import Path
import pytest
@@ -475,6 +476,45 @@ async def test_dois_tenants_do_mesmo_produto_reusando_branch_ref_tem_certificado
assert emit_b.status_code == 201, emit_b.text
# --- FIX 3 (F2 review): Σ vPag vs vNF ---------------------------------------
@pytest.mark.asyncio
async def test_pagamento_divergente_do_total_dos_itens_e_409_e_nao_queima_numero(db_session):
"""A SEFAZ rejeita `pagamento.valor != vNF` com "Valor do Pagamento
difere do total" (`sowai_fiscal.xml_builder` module docstring) -- este
serviço promete validar "coerência estrutural (somas)" (spec decisão
#3) antes de queimar um número, não só deixar a rejeição acontecer do
lado de lá depois de já ter um `FiscalDocument` ASSINADO persistido."""
product, key = await _product_and_key(db_session)
payload, raw = _payload_from_golden("caso_padrao_intra")
valor_correto = Decimal(str(payload["pagamento"]["valor"]))
payload["pagamento"]["valor"] = str(valor_correto + Decimal("10.00"))
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
await _setup_certificate_and_series(
db_session, client, key, tenant_ref="t1", branch_ref="b1",
serie=raw["serie"], next_number=raw["numero"],
)
response = await client.post(
"/v1/emissoes", json=payload,
headers={**_headers(key), "Idempotency-Key": f"idem-{uuid.uuid4().hex}"},
)
assert response.status_code == 409, response.text
assert response.json()["detail"]["code"] == "pagamento_total_diverge"
result = await db_session.execute(
select(FiscalSeries).where(FiscalSeries.product_id == product.id, FiscalSeries.tenant_ref == "t1")
)
series = result.scalar_one()
assert series.next_number == raw["numero"], (
"o número NÃO pode ter sido queimado por um pagamento divergente do total dos itens"
)
# --- prova do outbox --------------------------------------------------------