feat: portability safeguards — signed goldens, HTTP contract suite, committed OpenAPI (Task 6)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3aae8b67ef
commit
fb2b8ce372
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
"""Task 6: one-shot generator for `tests/goldens_signed/*.expected.xml` --
|
||||||
|
NOT run automatically by the test suite. Run manually whenever a golden case
|
||||||
|
in the `sowai-fiscal` lib is added/changed (or `tests/fixtures/test_cert.pfx`
|
||||||
|
is rotated), then commit the regenerated files.
|
||||||
|
|
||||||
|
Signs each `sowai_fiscal.goldens/*.input.json` case's `DadosEmissao` (loaded
|
||||||
|
via `sowai_fiscal.golden_helpers.dados_from_json` -- SAME helper the lib's
|
||||||
|
own pre-signature goldens use, so `chave_acesso`/`numero`/`cnf`/`dh_emi` are
|
||||||
|
whatever that JSON already pins, unchanged) with the FIXED test certificate.
|
||||||
|
Deterministic because RSA PKCS#1v1.5 signing has no random padding: the SAME
|
||||||
|
(input XML, private key) pair always produces the SAME signature bytes --
|
||||||
|
this is the byte-exact assertion `tests/test_signed_goldens.py` checks.
|
||||||
|
|
||||||
|
uv run python scripts/generate_signed_goldens.py
|
||||||
|
"""
|
||||||
|
import importlib.resources
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||||
|
from sowai_fiscal.golden_helpers import dados_from_json
|
||||||
|
from sowai_fiscal.xml_builder import build_nfe
|
||||||
|
|
||||||
|
from fiscal_svc.emission.service import _serialize_nfe, sign_nfe_xml
|
||||||
|
|
||||||
|
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
CERT_PATH = REPO_DIR / "tests" / "fixtures" / "test_cert.pfx"
|
||||||
|
CERT_PASSWORD = "test-cert-password" # must match generate_test_cert.py
|
||||||
|
OUT_DIR = REPO_DIR / "tests" / "goldens_signed"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if not CERT_PATH.exists():
|
||||||
|
raise SystemExit(
|
||||||
|
f"{CERT_PATH} não existe -- rode scripts/generate_test_cert.py primeiro"
|
||||||
|
)
|
||||||
|
pfx_bytes = CERT_PATH.read_bytes()
|
||||||
|
private_key, cert, _ca_certs = pkcs12.load_key_and_certificates(
|
||||||
|
pfx_bytes, CERT_PASSWORD.encode("utf-8")
|
||||||
|
)
|
||||||
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
goldens_dir = Path(str(importlib.resources.files("sowai_fiscal") / "goldens"))
|
||||||
|
for input_path in sorted(goldens_dir.glob("*.input.json")):
|
||||||
|
case = input_path.name.removesuffix(".input.json")
|
||||||
|
dados = dados_from_json(input_path)
|
||||||
|
nfe = build_nfe(dados)
|
||||||
|
xml_str = _serialize_nfe(nfe)
|
||||||
|
xml_assinado = sign_nfe_xml(xml_str, dados.chave_acesso, private_key, cert)
|
||||||
|
out_path = OUT_DIR / f"{case}.expected.xml"
|
||||||
|
out_path.write_text(xml_assinado, encoding="utf-8")
|
||||||
|
print(f"wrote {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Task 6: one-shot generator for `tests/fixtures/test_cert.pfx` -- a
|
||||||
|
self-signed TEST-ONLY A1 certificate, NEVER a real one (design spec's "os 3
|
||||||
|
seguros de portabilidade", item (b)/(c) extended by the F2 emenda: goldens
|
||||||
|
ASSINADOS need a FIXED test certificate, committed as a binary fixture).
|
||||||
|
|
||||||
|
Re-running this script produces a DIFFERENT RSA key pair (key generation is
|
||||||
|
not seeded) -- that is fine and expected: this is meant to be run ONCE and
|
||||||
|
the output COMMITTED. The determinism `tests/test_signed_goldens.py` relies
|
||||||
|
on comes from every test run reusing the SAME committed `.pfx` file (so the
|
||||||
|
same private key signs the same canonicalized XML the same way every time,
|
||||||
|
RSA PKCS#1v1.5 having no random padding) -- NOT from this generator being
|
||||||
|
reproducible bit-for-bit across runs. Re-run only if the fixture needs to be
|
||||||
|
rotated, and regenerate `tests/goldens_signed/*.expected.xml`
|
||||||
|
(`scripts/generate_signed_goldens.py`) together with it, in the same commit.
|
||||||
|
|
||||||
|
uv run python scripts/generate_test_cert.py
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
|
||||||
|
# Mesmo CNPJ do emitente usado em TODOS os casos golden da lib
|
||||||
|
# (`sowai_fiscal.goldens/*.input.json`) -- ver esse pacote se algum caso
|
||||||
|
# novo usar um CNPJ diferente; este script/fixture precisaria acompanhar.
|
||||||
|
CNPJ = "12345678000190"
|
||||||
|
PASSWORD = "test-cert-password"
|
||||||
|
OUT_PATH = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "test_cert.pfx"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
subject = issuer = x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, f"AUTOPECAS THIAGO LTDA:{CNPJ}"),
|
||||||
|
x509.NameAttribute(NameOID.SERIAL_NUMBER, CNPJ),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now - timedelta(days=1))
|
||||||
|
.not_valid_after(now + timedelta(days=3650))
|
||||||
|
.sign(key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
pfx_bytes = pkcs12.serialize_key_and_certificates(
|
||||||
|
name=b"sowai-fiscal-svc-test",
|
||||||
|
key=key,
|
||||||
|
cert=cert,
|
||||||
|
cas=None,
|
||||||
|
encryption_algorithm=serialization.BestAvailableEncryption(PASSWORD.encode("utf-8")),
|
||||||
|
)
|
||||||
|
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT_PATH.write_bytes(pfx_bytes)
|
||||||
|
print(f"wrote {OUT_PATH} (cnpj={CNPJ}, password={PASSWORD!r})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010051100000050"><ide><cUF>41</cUF><cNF>10000005</cNF><natOp>Venda</natOp><mod>55</mod><serie>1</serie><nNF>1005</nNF><dhEmi>2026-07-16T10:20:00-03:00</dhEmi><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>0</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CNPJ>99887766000155</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Av Cliente</xLgr><nro>50</nro><xBairro>Bairro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000100</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>9</indIEDest></dest><det nItem="1"><prod><cProd>PC001</cProd><cEAN>SEM GTIN</cEAN><xProd>Filtro de oleo</xProd><NCM>84212300</NCM><CFOP>5102</CFOP><uCom>UN</uCom><qCom>1</qCom><vUnCom>100.00</vUnCom><vProd>100.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>1</qTrib><vUnTrib>100.00</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS><IBSCBS><CST>000</CST><cClassTrib>000001</cClassTrib><gIBSCBS><vBC>100.00</vBC><gIBSUF><pIBSUF>0.1000</pIBSUF><vIBSUF>10.00</vIBSUF></gIBSUF><gCBS><pCBS>0.9000</pCBS><vCBS>90.00</vCBS></gCBS></gIBSCBS></IBSCBS></imposto></det><total><ICMSTot><vBC>0.00</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>100.00</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>100.00</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>01</tPag><vPag>100.00</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010051100000050"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>EIBfqHSPgzGamG7niiWJY8QjvaY=</DigestValue></Reference></SignedInfo><SignatureValue>lMywqdUdSFsbf5SR1kOmyByaFI1aSoUJwwgbz+P7Qri6iW+pJy+JJcapJnQcEdYbbFTqMJYJeRM87xHlQngg0SnPqd1ZjcLqZgXf6tm7GZriS3SNaMiXUC5EG9BnawvP4p7m5GviX3JJNKs2lvG0unBUtptq4aH/cZe5jd62SxeqyaeX4PxW/pbvkSBu3WndTLvM56vajMgimjbjQ8zLid85XJkVEAk9rV7KObZsHl0vuvFUqnZsAVaIJxB8nmmmcn/clT4AsPzhlkozR5YV0OjpP4sm08bYcvPcGcxxidxHE1o40xr6S6ozpiQENPEUBgTpNqMYaJTI0Rft3ERWaA==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010041100000045"><ide><cUF>41</cUF><cNF>10000004</cNF><natOp>Devolucao de venda</natOp><mod>55</mod><serie>1</serie><nNF>1004</nNF><dhEmi>2026-07-16T10:15:00-03:00</dhEmi><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>5</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CNPJ>99887766000155</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Av Cliente</xLgr><nro>50</nro><xBairro>Bairro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000100</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>9</indIEDest></dest><det nItem="1"><prod><cProd>PC001</cProd><cEAN>SEM GTIN</cEAN><xProd>Filtro de oleo</xProd><NCM>84212300</NCM><CFOP>1202</CFOP><uCom>UN</uCom><qCom>1</qCom><vUnCom>75.00</vUnCom><vProd>75.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>1</qTrib><vUnTrib>75.00</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><total><ICMSTot><vBC>75.00</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>75.00</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>75.00</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>90</tPag><vPag>75.00</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010041100000045"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>dWH2Qpht8rN1pncCUL31+ymheWE=</DigestValue></Reference></SignedInfo><SignatureValue>iyBjOX1COZz2vFk1SNaQvmLqvsXGo63DINjVvAsoAA2g6XsnoLdrNjXOtxATh4Iez1SEcVoOex5V6x4/Vdd7EkX8TSu5QKqIAbDUjystojZAVW+wXj7enUgb24qbrsFU40EhEPUdZp51grKgm0d2R4qNeodXZiFGbuheRZQ4EhY5b+2EPX6kSdxGuYi4F0Xq0JgkG90CTJIUFNEDkY2PKCXOipNBpx6nOYiWnEBGfMw2oTmJeXQ2yKyXjrr01S9Lj48UZybGMIStQJoMTHn+p9+3ccTwbBAJ66Et38SEsP8gmWaLkGq5+zMCtriiwEJzCJMzTUMnDPee+/nada0KWg==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010061100000066"><ide><cUF>41</cUF><cNF>10000006</cNF><natOp>Venda</natOp><mod>55</mod><serie>1</serie><nNF>1006</nNF><dhEmi>2026-07-16T10:25:00-03:00</dhEmi><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>6</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CNPJ>99887766000155</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Av Cliente</xLgr><nro>50</nro><xBairro>Bairro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000100</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>9</indIEDest></dest><det nItem="1"><prod><cProd>PC900</cProd><cEAN>SEM GTIN</cEAN><xProd>Arruela avulsa</xProd><NCM>73181900</NCM><CFOP>5102</CFOP><uCom>UN</uCom><qCom>1.5</qCom><vUnCom>0.01</vUnCom><vProd>0.02</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>1.5</qTrib><vUnTrib>0.01</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><det nItem="2"><prod><cProd>PC901</cProd><cEAN>SEM GTIN</cEAN><xProd>Arruela avulsa 2</xProd><NCM>73181900</NCM><CFOP>5102</CFOP><uCom>UN</uCom><qCom>1.5</qCom><vUnCom>0.01</vUnCom><vProd>0.02</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>1.5</qTrib><vUnTrib>0.01</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><total><ICMSTot><vBC>0.04</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>0.04</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>0.04</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>01</tPag><vPag>0.04</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010061100000066"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>UrxPctSfvF8tf9cYULFIdWbEUMA=</DigestValue></Reference></SignedInfo><SignatureValue>Bdi9HkxrMIPD7dtfd8wq1Mg2hCt+ZFffU3n79WqAxcDDLgtjCOb/vX4hvXj+J+LZ+SEcNzKFLF5kRyriHb7+e9YkS7BFgljNOGZunqkTz6/ZdmrPNlZB89z6SOB5aI+Z+J2KHtOO4l7kyIji5EdJl+NrDfRoCpBFFctBVMKCKtwBh31KoNDWq0XzqmfI+FDfZskdgR+W0AGFg6pGbNfv4dhpOAPT5/UY8HQDU4Zv6hVleUv/ifsqKTKzEa7z9/IL+5894NoPk9JyhNAFgKQDs4y2r1irykOsu1LHH+BvnPPCGKeKCLsVxqTgQxseKkfhiis2hTt97e365a7UmZ1JaA==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010021100000024"><ide><cUF>41</cUF><cNF>10000002</cNF><natOp>Venda</natOp><mod>55</mod><serie>1</serie><nNF>1002</nNF><dhEmi>2026-07-16T10:05:00-03:00</dhEmi><tpNF>1</tpNF><idDest>2</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>4</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CNPJ>11222333000144</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Av Paulista</xLgr><nro>1000</nro><xCpl>Sala 10</xCpl><xBairro>Bela Vista</xBairro><cMun>3550308</cMun><xMun>Sao Paulo</xMun><UF>SP</UF><CEP>01310000</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>1</indIEDest><IE>1122334455</IE><email>compras@clienteinter.com.br</email></dest><det nItem="1"><prod><cProd>PC001</cProd><cEAN>SEM GTIN</cEAN><xProd>Filtro de oleo</xProd><NCM>84212300</NCM><CFOP>6102</CFOP><uCom>UN</uCom><qCom>10</qCom><vUnCom>75.00</vUnCom><vProd>750.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>10</qTrib><vUnTrib>75.00</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><total><ICMSTot><vBC>750.00</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>750.00</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>750.00</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>17</tPag><vPag>750.00</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010021100000024"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>wMN3wf/wyPYVP/p8oSMeLIwQ0sI=</DigestValue></Reference></SignedInfo><SignatureValue>YTzAGw2xRWQSgMsJuF1XB2elWaTAHRRnLvOwM3Bo6YHNGVM3AqViOlhf0jRmbuQe4VCj8ugSEAe24p50Mn0YGLqH91emub4ADsewNAjeIcb7C9n/cYk8Sh2e/wPc3DvS5cOSICUOroPak0A3bWFb8DjRAcUdYvX/uI0IrzMlN0Qxzjd+lZwo/1cmzIWnW0G+nDvd2rykGOZTwCxd+yZf+qHw+A1iLFgeI9YHyWcWoUtKH746GvlJQ59Oz4dTXgtJL3Nnhx2O7dsgbU0/ee8Q6bYGELZz6Jtdup3EOVI7T5K/BBgVeNpchQfj7hHdL1gzmVAnGdVnBR01EDQnscVtZw==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010011100000019"><ide><cUF>41</cUF><cNF>10000001</cNF><natOp>Venda</natOp><mod>55</mod><serie>1</serie><nNF>1001</nNF><dhEmi>2026-07-16T10:00:00-03:00</dhEmi><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>9</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CNPJ>99887766000155</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Av Cliente</xLgr><nro>50</nro><xBairro>Bairro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000100</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>9</indIEDest></dest><det nItem="1"><prod><cProd>PC001</cProd><cEAN>SEM GTIN</cEAN><xProd>Filtro de oleo</xProd><NCM>84212300</NCM><CFOP>5102</CFOP><uCom>UN</uCom><qCom>2</qCom><vUnCom>75.00</vUnCom><vProd>150.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>2</qTrib><vUnTrib>75.00</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><det nItem="2"><prod><cProd>PC002</cProd><cEAN>SEM GTIN</cEAN><xProd>Pastilha de freio</xProd><NCM>87083090</NCM><CFOP>5102</CFOP><uCom>PC</uCom><qCom>4</qCom><vUnCom>45.50</vUnCom><vProd>182.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>PC</uTrib><qTrib>4</qTrib><vUnTrib>45.50</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN102><orig>0</orig><CSOSN>102</CSOSN></ICMSSN102></ICMS><PIS><PISNT><CST>08</CST></PISNT></PIS><COFINS><COFINSNT><CST>08</CST></COFINSNT></COFINS></imposto></det><total><ICMSTot><vBC>332.00</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>332.00</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>332.00</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>01</tPag><vPag>332.00</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010011100000019"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>1inHNlmRb0maeftKaq/JExhNc8A=</DigestValue></Reference></SignedInfo><SignatureValue>ked+Fn1wxCy5EdNT289eLof7cqwuWvj1bh/AV0CQ3zbrLD5kL5/fXj2pLMZWS9wyl3NG8ZFATJ5B1xmeDcc0Niigs9O+e9wwWgX+3STDHZOogLuOU/CKhgFq/6e4wkZlMhV7Yffo+swo5qf82itGxUkkQkD00TDuFP7OnKOZBW8Ti+CPPPm+HLv613+BolP14If/2BXJW9HZiSu4Cua66/E32RSqPSp319DPvvpGwefJWDddOYfK9TkChirgdygAQUrkeJutcQdzzo9LnSVKE3FAu6q5w3rkNIrqA0OTEctGpojwfXc9E+IzRehof6fPvDCUjwSe89fP4JpK6tYiYA==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe41260712345678000190550010000010031100000030"><ide><cUF>41</cUF><cNF>10000003</cNF><natOp>Venda</natOp><mod>55</mod><serie>1</serie><nNF>1003</nNF><dhEmi>2026-07-16T10:10:00-03:00</dhEmi><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>0</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>sowai-auto/1b.1</verProc></ide><emit><CNPJ>12345678000190</CNPJ><xNome>AUTOPECAS THIAGO LTDA</xNome><xFant>Thiago Auto Center</xFant><enderEmit><xLgr>Rua das Autopecas</xLgr><nro>1500</nro><xBairro>Centro</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000000</CEP><cPais>1058</cPais><xPais>Brasil</xPais><fone>4133334444</fone></enderEmit><IE>1234567890</IE><CRT>1</CRT></emit><dest><CPF>12345678909</CPF><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>Rua Oficina</xLgr><nro>22</nro><xBairro>Industrial</xBairro><cMun>4106902</cMun><xMun>Curitiba</xMun><UF>PR</UF><CEP>80000200</CEP><cPais>1058</cPais><xPais>Brasil</xPais></enderDest><indIEDest>9</indIEDest></dest><det nItem="1"><prod><cProd>OL500</cProd><cEAN>SEM GTIN</cEAN><xProd>Oleo lubrificante 15W40 1L</xProd><NCM>27101259</NCM><CEST>0600100</CEST><CFOP>5405</CFOP><uCom>UN</uCom><qCom>1</qCom><vUnCom>100.00</vUnCom><vProd>100.00</vProd><cEANTrib>SEM GTIN</cEANTrib><uTrib>UN</uTrib><qTrib>1</qTrib><vUnTrib>100.00</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMSSN500><orig>0</orig><CSOSN>500</CSOSN><vBCSTRet>140.00</vBCSTRet><pST>18.0000</pST><vICMSSTRet>13.20</vICMSSTRet></ICMSSN500></ICMS><PIS><PISNT><CST>04</CST></PISNT></PIS><COFINS><COFINSNT><CST>04</CST></COFINSNT></COFINS></imposto></det><total><ICMSTot><vBC>0.00</vBC><vICMS>0.00</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>140.00</vBCST><vST>13.20</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>100.00</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.00</vCOFINS><vOutro>0.00</vOutro><vNF>100.00</vNF></ICMSTot></total><transp><modFrete>9</modFrete></transp><pag><detPag><indPag>0</indPag><tPag>01</tPag><vPag>100.00</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41260712345678000190550010000010031100000030"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>GynTwfOg2ofaLE8WyWWwdpLBjWg=</DigestValue></Reference></SignedInfo><SignatureValue>1KMXWaK4qME2/kgazsMvEIB0B2Oxr6bLCNtfa6IrWd/i+muv9CDRhOm/yKumdRdmC6wys0o0krkwOW0Zh91iCikvHVyO/sh4ze4ycFzE2cODFpEciS7+59pMZM4vARhoZga855QKTzCB6CvFx99VAeq7X0wxXc7FWEmz2ENH6Vn0pkFkiMlpIgT1yFtGZbOQ5lcuE0hGTQJlEool/dbZRWWNp2CPbpx4Hys/CwLUM/MIA7lgfOqvEE0VxrfsU4Cfk2azBylFmprGnySot6HMPl0nkmGoVWEnaX3hxIlJJjfHXPZMnRp/HeryZpNb5GRzWjW9oJlXZUkzPcc02gkI8g==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIDHDCCAgSgAwIBAgIUOfWhuJYF+atF0Sv3tUJMbQzcRqwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwSDEtMCsGA1UEAwwkQVVUT1BFQ0FTIFRISUFHTyBMVERBOjEyMzQ1Njc4MDAw
|
||||||
|
MTkwMRcwFQYDVQQFEw4xMjM0NTY3ODAwMDE5MDAeFw0yNjA3MjExOTQ3NDNaFw0z
|
||||||
|
NjA3MTkxOTQ3NDNaMEgxLTArBgNVBAMMJEFVVE9QRUNBUyBUSElBR08gTFREQTox
|
||||||
|
MjM0NTY3ODAwMDE5MDEXMBUGA1UEBRMOMTIzNDU2NzgwMDAxOTAwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZUIX5xprSUIivdhvDZSwLu8pug0QXNHGk
|
||||||
|
oIyyRjG9OIGd3nkn8X+9CP9yukYEyPvr1Yz/TM3pn/4d/kSixIi422kadKMka2QR
|
||||||
|
uax/dRwj0UZJwm7MnC/Osa3FgEln3Yz4S6HWEPwPa/PNeETiXTLtnIQiY1M0GYld
|
||||||
|
ccVsNUjDhvmjhIhuLzcZ3goWlKyLoeFoL8inFb+awFt6u2pao8JGyqAV3/7es5Ul
|
||||||
|
gpLg10KfKcJyvGPvDhFo723FKACdOse7clLtMOrRt0Z/ST9i+17txFo/OjwXWEhC
|
||||||
|
vGOTxNu667uA+1SzUk8ivouHOKtPX1ZdicEmTYY90Re/0B208nV7AgMBAAEwDQYJ
|
||||||
|
KoZIhvcNAQELBQADggEBAGAPlWqX6VSVviqyxQu4AstW0xfgd461wy0lTgfh4KQ4
|
||||||
|
7J6ikP5I+o8O9neSQ2bGaYbfTUja3Oq7VEoLtbumpZcmkrvlmXKmzUnkALoHlTab
|
||||||
|
ih+9p3In9HgUhgQ4wwrAFurQzrtmfsSKC9hqUZMSn1BYGEs3oa2ocE3JvkBZ7YF+
|
||||||
|
BLWs5cd/eqb4fPgDO6yZMnMOzTc9tjOhufW5eQKtGfTuR6YXqc7qRouFk+mqNbGA
|
||||||
|
wAW+WjFafx9FENC3HdEt7UhI2cOjVuC0TUIAKl3dH3HpebaI3O0Gbd/AA+sGYezz
|
||||||
|
lXy6GZUYxpBNNdP9avLSwabIMcoEN37ARvaY2VDULBQ=
|
||||||
|
</X509Certificate></X509Data></KeyInfo></Signature></NFe>
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
"""Task 6: suíte de CONTRATO HTTP -- design spec's "os 3 seguros de
|
||||||
|
portabilidade", item (a): "suíte de contrato HTTP language-agnostic (bate na
|
||||||
|
API, não no interior)". A REGRA DESTE ARQUIVO: zero import de `fiscal_svc.*`
|
||||||
|
além de `fiscal_svc.main.app` (para montar o `ASGITransport` -- inevitável
|
||||||
|
em Python; um cliente HTTP real bateria numa URL, não precisaria nem disso).
|
||||||
|
Nenhum import de model/service/schema interno -- é a suíte que uma
|
||||||
|
implementação GO deste MESMO contrato (`docs/openapi-v1.json`) teria que
|
||||||
|
passar batendo na porta 8140 de fora, sem NUNCA ver este código-fonte.
|
||||||
|
|
||||||
|
Provisionamento de fixtures (criar um Product/API key) é feito via
|
||||||
|
`scripts/create_product.py`'s MESMA função (`tenancy.service.create_
|
||||||
|
product`) em qualquer OUTRO arquivo de teste deste repo -- mas aqui, para
|
||||||
|
manter a regra "zero import do interior", cada teste usa o endpoint HTTP
|
||||||
|
disponível (upload de certificado, criação de série) e trata a criação do
|
||||||
|
Product como um pré-requisito de infraestrutura resolvido pela fixture
|
||||||
|
`product_api_key` (que É a única concessão: sem UM jeito de provisionar um
|
||||||
|
Product por HTTP -- design spec decisão #5, "provisionar um Product é uma
|
||||||
|
ação de operador, não self-service" -- não há como popular um cenário sem
|
||||||
|
tocar a camada de app UMA vez por teste; a asserção em si nunca o faz)."""
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from fiscal_svc.core.db import get_session
|
||||||
|
from fiscal_svc.main import app
|
||||||
|
|
||||||
|
_CNPJ = "14200166000187"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _override_db(db_session):
|
||||||
|
async def _get_session_override():
|
||||||
|
yield db_session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_session] = _get_session_override
|
||||||
|
yield
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _fiscal_cert_encryption_key(monkeypatch):
|
||||||
|
monkeypatch.setenv("FISCAL_CERT_ENCRYPTION_KEY", Fernet.generate_key().decode("ascii"))
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def product_api_key(db_session):
|
||||||
|
"""A ÚNICA concessão à regra "zero import do interior" -- provisionar
|
||||||
|
um Product não tem endpoint HTTP (decisão de design deliberada, ver o
|
||||||
|
docstring do módulo), então esta fixture chama a função de serviço
|
||||||
|
diretamente. Nenhum teste abaixo importa nada além disto."""
|
||||||
|
from fiscal_svc.tenancy.service import create_product
|
||||||
|
|
||||||
|
key = f"k-{uuid.uuid4().hex}"
|
||||||
|
await create_product(db_session, name=f"contract-{uuid.uuid4().hex[:8]}", api_key=key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def _headers(api_key: str) -> dict[str, str]:
|
||||||
|
return {"X-Api-Key": api_key}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_test_pfx(cnpj: str = _CNPJ, password: str = "senha123") -> bytes:
|
||||||
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
subject = issuer = x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, f"EMPRESA TESTE LTDA:{cnpj}"),
|
||||||
|
x509.NameAttribute(NameOID.SERIAL_NUMBER, cnpj),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now - timedelta(days=1))
|
||||||
|
.not_valid_after(now + timedelta(days=365))
|
||||||
|
.sign(key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
return pkcs12.serialize_key_and_certificates(
|
||||||
|
name=b"test", key=key, cert=cert, cas=None,
|
||||||
|
encryption_algorithm=serialization.BestAvailableEncryption(password.encode("utf-8")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- health ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_returns_200_ok_shape():
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.get("/v1/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# --- auth (require_product) --------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"method,path,kwargs",
|
||||||
|
[
|
||||||
|
("get", "/v1/certificados", {"params": {"tenant_ref": "t", "branch_ref": "b"}}),
|
||||||
|
("get", "/v1/series", {"params": {"tenant_ref": "t", "branch_ref": "b"}}),
|
||||||
|
("get", "/v1/documentos", {}),
|
||||||
|
("post", "/v1/series", {"json": {"tenant_ref": "t", "branch_ref": "b", "document_model": "55", "serie": 1, "next_number": 1}}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_every_v1_route_requires_api_key(method, path, kwargs):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await getattr(client, method)(path, **kwargs)
|
||||||
|
|
||||||
|
assert response.status_code == 401, response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_wrong_api_key_is_401():
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.get(
|
||||||
|
"/v1/series",
|
||||||
|
params={"tenant_ref": "t", "branch_ref": "b"},
|
||||||
|
headers=_headers("not-a-real-key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401, response.text
|
||||||
|
|
||||||
|
|
||||||
|
# --- series --------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_series_create_list_patch_shapes(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
create_response = await client.post(
|
||||||
|
"/v1/series",
|
||||||
|
json={"tenant_ref": "t1", "branch_ref": "b1", "document_model": "55", "serie": 1, "next_number": 1},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
assert create_response.status_code == 201, create_response.text
|
||||||
|
body = create_response.json()
|
||||||
|
for field in ("id", "product_id", "tenant_ref", "branch_ref", "document_model", "serie", "next_number"):
|
||||||
|
assert field in body, f"campo {field!r} ausente no shape de FiscalSeriesRead"
|
||||||
|
series_id = body["id"]
|
||||||
|
|
||||||
|
duplicate_response = await client.post(
|
||||||
|
"/v1/series",
|
||||||
|
json={"tenant_ref": "t1", "branch_ref": "b1", "document_model": "55", "serie": 1, "next_number": 2},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
assert duplicate_response.status_code == 409, duplicate_response.text
|
||||||
|
assert duplicate_response.json()["detail"]["code"] == "duplicate_fiscal_series"
|
||||||
|
|
||||||
|
list_response = await client.get(
|
||||||
|
"/v1/series", params={"tenant_ref": "t1", "branch_ref": "b1"}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert list_response.status_code == 200, list_response.text
|
||||||
|
assert isinstance(list_response.json(), list)
|
||||||
|
assert len(list_response.json()) == 1
|
||||||
|
|
||||||
|
patch_response = await client.patch(
|
||||||
|
f"/v1/series/{series_id}", json={"next_number": 5}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert patch_response.status_code == 200, patch_response.text
|
||||||
|
assert patch_response.json()["next_number"] == 5
|
||||||
|
|
||||||
|
not_found_response = await client.patch(
|
||||||
|
f"/v1/series/{uuid.uuid4()}", json={"next_number": 5}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert not_found_response.status_code == 404, not_found_response.text
|
||||||
|
|
||||||
|
|
||||||
|
# --- certificados ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_certificados_upload_get_delete_shapes(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
upload_response = await client.post(
|
||||||
|
"/v1/certificados",
|
||||||
|
params={"tenant_ref": "t1", "branch_ref": "b1"},
|
||||||
|
files={"file": ("cert.pfx", _build_test_pfx(), "application/x-pkcs12")},
|
||||||
|
data={"password": "senha123", "cnpj": _CNPJ},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
assert upload_response.status_code == 201, upload_response.text
|
||||||
|
body = upload_response.json()
|
||||||
|
for field in (
|
||||||
|
"id", "product_id", "tenant_ref", "branch_ref", "cnpj",
|
||||||
|
"subject_cn", "cnpj_certificado", "not_valid_before", "not_valid_after", "created_at",
|
||||||
|
):
|
||||||
|
assert field in body, f"campo {field!r} ausente no shape de FiscalCertificateRead"
|
||||||
|
assert "pfx_encrypted" not in body
|
||||||
|
assert "password_encrypted" not in body
|
||||||
|
|
||||||
|
get_response = await client.get(
|
||||||
|
"/v1/certificados", params={"tenant_ref": "t1", "branch_ref": "b1"}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert get_response.status_code == 200, get_response.text
|
||||||
|
|
||||||
|
delete_response = await client.delete(
|
||||||
|
"/v1/certificados", params={"tenant_ref": "t1", "branch_ref": "b1"}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert delete_response.status_code == 204, delete_response.text
|
||||||
|
|
||||||
|
get_after_delete_response = await client.get(
|
||||||
|
"/v1/certificados", params={"tenant_ref": "t1", "branch_ref": "b1"}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert get_after_delete_response.status_code == 404, get_after_delete_response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_certificado_invalido_e_422(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/v1/certificados",
|
||||||
|
params={"tenant_ref": "t1", "branch_ref": "b1"},
|
||||||
|
files={"file": ("cert.pfx", b"nao e um pfx", "application/x-pkcs12")},
|
||||||
|
data={"password": "qualquer", "cnpj": _CNPJ},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422, response.text
|
||||||
|
|
||||||
|
|
||||||
|
# --- emissão ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _minimal_emissao_payload(*, tenant_ref="t1", branch_ref="b1", serie=1) -> dict:
|
||||||
|
return {
|
||||||
|
"tenant_ref": tenant_ref,
|
||||||
|
"branch_ref": branch_ref,
|
||||||
|
"document_model": "55",
|
||||||
|
"serie": serie,
|
||||||
|
"emitente": {
|
||||||
|
"cnpj": _CNPJ,
|
||||||
|
"razao_social": "EMPRESA TESTE LTDA",
|
||||||
|
"nome_fantasia": None,
|
||||||
|
"ie": "1234567890",
|
||||||
|
"crt": "1",
|
||||||
|
"address_street": "Rua Teste",
|
||||||
|
"address_number": "100",
|
||||||
|
"address_complement": None,
|
||||||
|
"address_district": "Centro",
|
||||||
|
"address_city": "Curitiba",
|
||||||
|
"address_state": "PR",
|
||||||
|
"address_zip": "80000000",
|
||||||
|
"address_city_ibge_code": "4106902",
|
||||||
|
},
|
||||||
|
"itens": [
|
||||||
|
{
|
||||||
|
"codigo": "PC001",
|
||||||
|
"descricao": "Peca teste",
|
||||||
|
"ncm": "84212300",
|
||||||
|
"cfop": "5102",
|
||||||
|
"unidade_comercial": "UN",
|
||||||
|
"unidade_tributavel": "UN",
|
||||||
|
"quantidade": "1",
|
||||||
|
"valor_unitario": "10.00",
|
||||||
|
"fiscal_result": {
|
||||||
|
"cfop": "5102",
|
||||||
|
"cst": None,
|
||||||
|
"csosn": "102",
|
||||||
|
"origem": "0",
|
||||||
|
"consumidor_final": True,
|
||||||
|
"indicador_ie": "9",
|
||||||
|
"tributos": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagamento": {"tpag": "01", "valor": "10.00", "indpag": "0"},
|
||||||
|
"ambiente": "homologacao",
|
||||||
|
"uf_destino_tipo": "interna",
|
||||||
|
"destinatario": None,
|
||||||
|
"ver_proc": "contract-test/1.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_emissao_requires_idempotency_key_header(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/v1/emissoes", json=_minimal_emissao_payload(), headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422, response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_emissao_requires_ver_proc(product_api_key):
|
||||||
|
payload = _minimal_emissao_payload()
|
||||||
|
del payload["ver_proc"]
|
||||||
|
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/v1/emissoes", json=payload,
|
||||||
|
headers={**_headers(product_api_key), "Idempotency-Key": f"k-{uuid.uuid4().hex}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422, response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_emissao_without_certificate_or_series_is_409_fiscal_config_missing(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/v1/emissoes", json=_minimal_emissao_payload(),
|
||||||
|
headers={**_headers(product_api_key), "Idempotency-Key": f"k-{uuid.uuid4().hex}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 409, response.text
|
||||||
|
body = response.json()
|
||||||
|
assert body["detail"]["code"] == "fiscal_config_missing"
|
||||||
|
assert "message" in body["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_emissao_happy_path_shapes_and_idempotency(product_api_key):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
upload_response = await client.post(
|
||||||
|
"/v1/certificados",
|
||||||
|
params={"tenant_ref": "t1", "branch_ref": "b1"},
|
||||||
|
files={"file": ("cert.pfx", _build_test_pfx(), "application/x-pkcs12")},
|
||||||
|
data={"password": "senha123", "cnpj": _CNPJ},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
assert upload_response.status_code == 201, upload_response.text
|
||||||
|
|
||||||
|
series_response = await client.post(
|
||||||
|
"/v1/series",
|
||||||
|
json={"tenant_ref": "t1", "branch_ref": "b1", "document_model": "55", "serie": 1, "next_number": 1},
|
||||||
|
headers=_headers(product_api_key),
|
||||||
|
)
|
||||||
|
assert series_response.status_code == 201, series_response.text
|
||||||
|
|
||||||
|
idem_key = f"k-{uuid.uuid4().hex}"
|
||||||
|
first_response = await client.post(
|
||||||
|
"/v1/emissoes", json=_minimal_emissao_payload(),
|
||||||
|
headers={**_headers(product_api_key), "Idempotency-Key": idem_key},
|
||||||
|
)
|
||||||
|
assert first_response.status_code == 201, first_response.text
|
||||||
|
body = first_response.json()
|
||||||
|
for field in (
|
||||||
|
"id", "product_id", "tenant_ref", "branch_ref", "series_id", "document_model",
|
||||||
|
"serie", "numero", "chave_acesso", "codigo_numerico", "status", "ambiente",
|
||||||
|
"rejeicao_codigo", "rejeicao_motivo", "protocolo", "autorizada_em", "created_at",
|
||||||
|
):
|
||||||
|
assert field in body, f"campo {field!r} ausente no shape de FiscalDocumentRead"
|
||||||
|
assert "xml_assinado" not in body
|
||||||
|
assert body["status"] == "ASSINADO"
|
||||||
|
document_id = body["id"]
|
||||||
|
|
||||||
|
second_response = await client.post(
|
||||||
|
"/v1/emissoes", json=_minimal_emissao_payload(),
|
||||||
|
headers={**_headers(product_api_key), "Idempotency-Key": idem_key},
|
||||||
|
)
|
||||||
|
assert second_response.status_code == 200, second_response.text
|
||||||
|
assert second_response.json()["id"] == document_id
|
||||||
|
|
||||||
|
get_response = await client.get(f"/v1/documentos/{document_id}", headers=_headers(product_api_key))
|
||||||
|
assert get_response.status_code == 200, get_response.text
|
||||||
|
assert "xml_assinado" not in get_response.json()
|
||||||
|
|
||||||
|
xml_response = await client.get(f"/v1/documentos/{document_id}/xml", headers=_headers(product_api_key))
|
||||||
|
assert xml_response.status_code == 200
|
||||||
|
assert xml_response.headers["content-type"].startswith("application/xml")
|
||||||
|
assert "<NFe" in xml_response.text
|
||||||
|
|
||||||
|
list_response = await client.get(
|
||||||
|
"/v1/documentos", params={"tenant_ref": "t1", "branch_ref": "b1"}, headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert list_response.status_code == 200, list_response.text
|
||||||
|
assert any(d["id"] == document_id for d in list_response.json())
|
||||||
|
|
||||||
|
not_found_response = await client.get(
|
||||||
|
f"/v1/documentos/{uuid.uuid4()}", headers=_headers(product_api_key)
|
||||||
|
)
|
||||||
|
assert not_found_response.status_code == 404, not_found_response.text
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Task 6: design spec's "os 3 seguros de portabilidade", item (c) --
|
||||||
|
"OpenAPI versionado como fonte de verdade do contrato" -- and the F2 plan's
|
||||||
|
own requirement: `docs/openapi-v1.json` COMMITTED + a test that fails if it
|
||||||
|
diverges from `app.openapi()`. Regenerate with:
|
||||||
|
|
||||||
|
uv run python -c "
|
||||||
|
import json
|
||||||
|
from fiscal_svc.main import app
|
||||||
|
with open('docs/openapi-v1.json', 'w') as f:
|
||||||
|
json.dump(app.openapi(), f, indent=2, sort_keys=True)
|
||||||
|
f.write('\n')
|
||||||
|
"
|
||||||
|
|
||||||
|
...and commit the result -- NEVER hand-edit `docs/openapi-v1.json`."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fiscal_svc.main import app
|
||||||
|
|
||||||
|
_OPENAPI_PATH = Path(__file__).resolve().parents[1] / "docs" / "openapi-v1.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_committed_openapi_matches_generated_schema():
|
||||||
|
committed = json.loads(_OPENAPI_PATH.read_text(encoding="utf-8"))
|
||||||
|
generated = app.openapi()
|
||||||
|
|
||||||
|
assert committed == generated, (
|
||||||
|
"docs/openapi-v1.json divergiu do schema gerado por app.openapi() -- "
|
||||||
|
"regenere (ver o docstring deste teste) e commite o resultado; nunca "
|
||||||
|
"edite o JSON à mão"
|
||||||
|
)
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""Task 6: fecha o gap da EMENDA 7b (design spec's "os 3 seguros de
|
||||||
|
portabilidade", item (b)) -- o corpus golden da lib (`sowai_fiscal.
|
||||||
|
goldens`) só cobre XML PRÉ-assinatura; o caminho de ASSINATURA fica sem
|
||||||
|
seguro golden até aqui. Para cada caso golden, este teste emite ATRAVÉS DA
|
||||||
|
API real (`POST /v1/emissoes`, o mesmo caminho de qualquer produto
|
||||||
|
consumidor) com `cnf`/`dh_emi` PINADOS (via monkeypatch -- os únicos dois
|
||||||
|
elementos do payload real que a SERVICE gera de forma não-determinística;
|
||||||
|
`numero` é controlado plantando o `next_number` da série) e o certificado de
|
||||||
|
TESTE fixo (`tests/fixtures/test_cert.pfx`, NUNCA um certificado real) e
|
||||||
|
compara o XML assinado resultante, BYTE A BYTE, contra `tests/
|
||||||
|
goldens_signed/<case>.expected.xml` (gerado uma vez por
|
||||||
|
`scripts/generate_signed_goldens.py`).
|
||||||
|
|
||||||
|
RSA PKCS#1v1.5 (o que `erpbrasil.assinatura` usa) não tem padding
|
||||||
|
aleatório -- a MESMA chave privada assinando o MESMO XML canonicalizado
|
||||||
|
produz sempre a MESMA assinatura. Isso é o que torna esta comparação byte a
|
||||||
|
byte estável entre execuções (provado por `scripts/generate_signed_goldens.
|
||||||
|
py` sendo rodado duas vezes seguidas e comparado -- ver seu commit)."""
|
||||||
|
import importlib.resources
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from fiscal_svc.core.db import get_session
|
||||||
|
from fiscal_svc.emission import service as emission_service
|
||||||
|
from fiscal_svc.main import app
|
||||||
|
from fiscal_svc.tenancy.service import create_product
|
||||||
|
|
||||||
|
_GOLDENS_DIR = Path(str(importlib.resources.files("sowai_fiscal") / "goldens"))
|
||||||
|
_EXPECTED_DIR = Path(__file__).resolve().parent / "goldens_signed"
|
||||||
|
_CERT_PATH = Path(__file__).resolve().parent / "fixtures" / "test_cert.pfx"
|
||||||
|
_CERT_PASSWORD = "test-cert-password"
|
||||||
|
|
||||||
|
_CASES = sorted(p.name.removesuffix(".input.json") for p in _GOLDENS_DIR.glob("*.input.json"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _override_db(db_session):
|
||||||
|
async def _get_session_override():
|
||||||
|
yield db_session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_session] = _get_session_override
|
||||||
|
yield
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _fiscal_cert_encryption_key(monkeypatch):
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
from fiscal_svc.certificates import crypto as certificate_lib
|
||||||
|
|
||||||
|
monkeypatch.setenv("FISCAL_CERT_ENCRYPTION_KEY", Fernet.generate_key().decode("ascii"))
|
||||||
|
certificate_lib._get_fernet.cache_clear()
|
||||||
|
yield
|
||||||
|
certificate_lib._get_fernet.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _headers(api_key: str) -> dict[str, str]:
|
||||||
|
return {"X-Api-Key": api_key}
|
||||||
|
|
||||||
|
|
||||||
|
class _FrozenDatetime:
|
||||||
|
"""Stand-in for `emission.service`'s module-level `datetime` NAME
|
||||||
|
(`from datetime import datetime` -- a plain attribute of the module,
|
||||||
|
monkeypatch-replaceable). Exposes only `.now(tz)`, the ONLY method
|
||||||
|
`emission.service` calls on it -- returns the SAME fixed instant
|
||||||
|
regardless of the requested `tz` (converted via `.astimezone`), so both
|
||||||
|
call sites (`datetime.now(_TZ_EMISSAO)` for `dh_emi`/AAMM and `datetime.
|
||||||
|
now(timezone.utc)` for the certificate validity check) see a coherent,
|
||||||
|
pinned "now"."""
|
||||||
|
|
||||||
|
def __init__(self, fixed: datetime):
|
||||||
|
self._fixed = fixed
|
||||||
|
|
||||||
|
def now(self, tz=None):
|
||||||
|
return self._fixed.astimezone(tz) if tz is not None else self._fixed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("case", _CASES)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_signed_golden_matches_byte_for_byte(db_session, monkeypatch, case):
|
||||||
|
raw = json.loads((_GOLDENS_DIR / f"{case}.input.json").read_text(encoding="utf-8"))
|
||||||
|
expected = (_EXPECTED_DIR / f"{case}.expected.xml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# cNF pinado -- o único elemento aleatório de `emitir_documento` além do
|
||||||
|
# relógio.
|
||||||
|
monkeypatch.setattr(emission_service, "gerar_cnf", lambda numero: raw["cnf"])
|
||||||
|
# dh_emi/AAMM pinados no MESMO instante do golden.
|
||||||
|
fixed_now = datetime.fromisoformat(raw["dh_emi"])
|
||||||
|
monkeypatch.setattr(emission_service, "datetime", _FrozenDatetime(fixed_now))
|
||||||
|
|
||||||
|
key = f"k-{uuid.uuid4().hex}"
|
||||||
|
product = await create_product(db_session, name=f"golden-{case}", api_key=key)
|
||||||
|
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
pfx_bytes = _CERT_PATH.read_bytes()
|
||||||
|
upload_response = await client.post(
|
||||||
|
"/v1/certificados",
|
||||||
|
params={"tenant_ref": "t1", "branch_ref": "b1"},
|
||||||
|
files={"file": ("cert.pfx", pfx_bytes, "application/x-pkcs12")},
|
||||||
|
data={"password": _CERT_PASSWORD, "cnpj": raw["emitente"]["cnpj"]},
|
||||||
|
headers=_headers(key),
|
||||||
|
)
|
||||||
|
assert upload_response.status_code == 201, upload_response.text
|
||||||
|
|
||||||
|
# `numero` é controlado plantando o `next_number` da série -- a
|
||||||
|
# primeira (e única) alocação desta série devolve exatamente
|
||||||
|
# `raw["numero"]`.
|
||||||
|
series_response = await client.post(
|
||||||
|
"/v1/series",
|
||||||
|
json={
|
||||||
|
"tenant_ref": "t1", "branch_ref": "b1", "document_model": "55",
|
||||||
|
"serie": raw["serie"], "next_number": raw["numero"],
|
||||||
|
},
|
||||||
|
headers=_headers(key),
|
||||||
|
)
|
||||||
|
assert series_response.status_code == 201, series_response.text
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"tenant_ref": "t1",
|
||||||
|
"branch_ref": "b1",
|
||||||
|
"document_model": "55",
|
||||||
|
"serie": raw["serie"],
|
||||||
|
"emitente": raw["emitente"],
|
||||||
|
"itens": raw["itens"],
|
||||||
|
"pagamento": raw["pagamento"],
|
||||||
|
"ambiente": raw["ambiente"],
|
||||||
|
"uf_destino_tipo": raw["uf_destino_tipo"],
|
||||||
|
"destinatario": raw.get("destinatario"),
|
||||||
|
# A lib congela `ver_proc="sowai-auto/1b.1"` como default do
|
||||||
|
# PRÓPRIO `DadosEmissao` quando o golden JSON não o especifica
|
||||||
|
# (todos os 6 casos hoje) -- reproduzido aqui EXPLICITAMENTE
|
||||||
|
# (design spec EMENDA F1: este serviço nunca aplica esse default
|
||||||
|
# sozinho, o produto chamador sempre declara).
|
||||||
|
"ver_proc": raw.get("ver_proc", "sowai-auto/1b.1"),
|
||||||
|
}
|
||||||
|
emit_response = await client.post(
|
||||||
|
"/v1/emissoes", json=payload,
|
||||||
|
headers={**_headers(key), "Idempotency-Key": f"golden-{case}-{uuid.uuid4().hex}"},
|
||||||
|
)
|
||||||
|
assert emit_response.status_code == 201, emit_response.text
|
||||||
|
document_id = emit_response.json()["id"]
|
||||||
|
|
||||||
|
xml_response = await client.get(f"/v1/documentos/{document_id}/xml", headers=_headers(key))
|
||||||
|
|
||||||
|
assert emit_response.json()["chave_acesso"] == raw["chave_acesso"], (
|
||||||
|
"a chave de acesso montada pelo serviço deveria bater com a do golden -- todos os "
|
||||||
|
"componentes (uf_ibge/aamm/cnpj/modelo/serie/numero/tp_emis/cnf) precisam coincidir"
|
||||||
|
)
|
||||||
|
assert xml_response.text == expected, (
|
||||||
|
f"XML assinado do caso {case!r} divergiu byte a byte do golden esperado -- "
|
||||||
|
"regenere com scripts/generate_signed_goldens.py SE E SOMENTE SE a mudança for "
|
||||||
|
"intencional (ex.: golden novo/atualizado na lib), nunca para 'fazer passar'"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user