diff --git a/docs/openapi-v1.json b/docs/openapi-v1.json
new file mode 100644
index 0000000..050e838
--- /dev/null
+++ b/docs/openapi-v1.json
@@ -0,0 +1,1867 @@
+{
+ "components": {
+ "schemas": {
+ "Body_upload_certificate_endpoint_v1_certificados_post": {
+ "properties": {
+ "cnpj": {
+ "maxLength": 14,
+ "minLength": 14,
+ "title": "Cnpj",
+ "type": "string"
+ },
+ "file": {
+ "contentMediaType": "application/octet-stream",
+ "title": "File",
+ "type": "string"
+ },
+ "password": {
+ "title": "Password",
+ "type": "string"
+ }
+ },
+ "required": [
+ "cnpj",
+ "file",
+ "password"
+ ],
+ "title": "Body_upload_certificate_endpoint_v1_certificados_post",
+ "type": "object"
+ },
+ "DestinatarioDataPayload": {
+ "description": "`None` no `EmissaoRequest.destinatario` == consumidor final n\u00e3o\nidentificado -- espelha `sowai_fiscal.xml_builder.DestinatarioData`.",
+ "properties": {
+ "address_city": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address City"
+ },
+ "address_city_ibge_code": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address City Ibge Code"
+ },
+ "address_complement": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address Complement"
+ },
+ "address_district": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address District"
+ },
+ "address_number": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address Number"
+ },
+ "address_state": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address State"
+ },
+ "address_street": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address Street"
+ },
+ "address_zip": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address Zip"
+ },
+ "cnpj": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cnpj"
+ },
+ "cpf": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpf"
+ },
+ "email": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Email"
+ },
+ "ie": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Ie"
+ },
+ "indicador_ie": {
+ "title": "Indicador Ie",
+ "type": "string"
+ },
+ "nome": {
+ "title": "Nome",
+ "type": "string"
+ }
+ },
+ "required": [
+ "nome",
+ "indicador_ie"
+ ],
+ "title": "DestinatarioDataPayload",
+ "type": "object"
+ },
+ "EmissaoRequest": {
+ "properties": {
+ "ambiente": {
+ "enum": [
+ "homologacao",
+ "producao"
+ ],
+ "title": "Ambiente",
+ "type": "string"
+ },
+ "branch_ref": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ },
+ "destinatario": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/DestinatarioDataPayload"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "document_model": {
+ "const": "55",
+ "default": "55",
+ "title": "Document Model",
+ "type": "string"
+ },
+ "emitente": {
+ "$ref": "#/components/schemas/EmitenteDataPayload"
+ },
+ "fin_nfe": {
+ "default": "1",
+ "maxLength": 1,
+ "minLength": 1,
+ "title": "Fin Nfe",
+ "type": "string"
+ },
+ "ind_final": {
+ "default": "1",
+ "maxLength": 1,
+ "minLength": 1,
+ "title": "Ind Final",
+ "type": "string"
+ },
+ "ind_pres": {
+ "default": "1",
+ "maxLength": 1,
+ "minLength": 1,
+ "title": "Ind Pres",
+ "type": "string"
+ },
+ "itens": {
+ "items": {
+ "$ref": "#/components/schemas/ItemDataPayload"
+ },
+ "minItems": 1,
+ "title": "Itens",
+ "type": "array"
+ },
+ "nat_op": {
+ "default": "Venda",
+ "title": "Nat Op",
+ "type": "string"
+ },
+ "pagamento": {
+ "$ref": "#/components/schemas/PagamentoDataPayload"
+ },
+ "serie": {
+ "minimum": 0.0,
+ "title": "Serie",
+ "type": "integer"
+ },
+ "tenant_ref": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ },
+ "tp_emis": {
+ "default": "1",
+ "maxLength": 1,
+ "minLength": 1,
+ "title": "Tp Emis",
+ "type": "string"
+ },
+ "uf_destino_tipo": {
+ "enum": [
+ "interna",
+ "interestadual"
+ ],
+ "title": "Uf Destino Tipo",
+ "type": "string"
+ },
+ "ver_proc": {
+ "maxLength": 20,
+ "minLength": 1,
+ "title": "Ver Proc",
+ "type": "string"
+ }
+ },
+ "required": [
+ "tenant_ref",
+ "branch_ref",
+ "serie",
+ "emitente",
+ "itens",
+ "pagamento",
+ "ambiente",
+ "uf_destino_tipo",
+ "ver_proc"
+ ],
+ "title": "EmissaoRequest",
+ "type": "object"
+ },
+ "EmitenteDataPayload": {
+ "properties": {
+ "address_city": {
+ "title": "Address City",
+ "type": "string"
+ },
+ "address_city_ibge_code": {
+ "title": "Address City Ibge Code",
+ "type": "string"
+ },
+ "address_complement": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Address Complement"
+ },
+ "address_district": {
+ "title": "Address District",
+ "type": "string"
+ },
+ "address_number": {
+ "title": "Address Number",
+ "type": "string"
+ },
+ "address_state": {
+ "maxLength": 2,
+ "minLength": 2,
+ "title": "Address State",
+ "type": "string"
+ },
+ "address_street": {
+ "title": "Address Street",
+ "type": "string"
+ },
+ "address_zip": {
+ "title": "Address Zip",
+ "type": "string"
+ },
+ "cnpj": {
+ "maxLength": 14,
+ "minLength": 14,
+ "title": "Cnpj",
+ "type": "string"
+ },
+ "crt": {
+ "maxLength": 1,
+ "minLength": 1,
+ "title": "Crt",
+ "type": "string"
+ },
+ "fone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Fone"
+ },
+ "ie": {
+ "title": "Ie",
+ "type": "string"
+ },
+ "nome_fantasia": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Nome Fantasia"
+ },
+ "razao_social": {
+ "title": "Razao Social",
+ "type": "string"
+ }
+ },
+ "required": [
+ "cnpj",
+ "razao_social",
+ "ie",
+ "crt",
+ "address_street",
+ "address_number",
+ "address_district",
+ "address_city",
+ "address_state",
+ "address_zip",
+ "address_city_ibge_code"
+ ],
+ "title": "EmitenteDataPayload",
+ "type": "object"
+ },
+ "FiscalCertificateRead": {
+ "description": "Metadados do A1 -- NUNCA o bin\u00e1rio (`pfx_encrypted`/\n`password_encrypted` ficam de fora de prop\u00f3sito; ver\n`certificates.router.read_certificate_endpoint`'s docstring).",
+ "properties": {
+ "branch_ref": {
+ "title": "Branch Ref",
+ "type": "string"
+ },
+ "cnpj": {
+ "title": "Cnpj",
+ "type": "string"
+ },
+ "cnpj_certificado": {
+ "title": "Cnpj Certificado",
+ "type": "string"
+ },
+ "created_at": {
+ "format": "date-time",
+ "title": "Created At",
+ "type": "string"
+ },
+ "id": {
+ "format": "uuid",
+ "title": "Id",
+ "type": "string"
+ },
+ "not_valid_after": {
+ "format": "date-time",
+ "title": "Not Valid After",
+ "type": "string"
+ },
+ "not_valid_before": {
+ "format": "date-time",
+ "title": "Not Valid Before",
+ "type": "string"
+ },
+ "product_id": {
+ "format": "uuid",
+ "title": "Product Id",
+ "type": "string"
+ },
+ "subject_cn": {
+ "title": "Subject Cn",
+ "type": "string"
+ },
+ "tenant_ref": {
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "product_id",
+ "tenant_ref",
+ "branch_ref",
+ "cnpj",
+ "subject_cn",
+ "cnpj_certificado",
+ "not_valid_before",
+ "not_valid_after",
+ "created_at"
+ ],
+ "title": "FiscalCertificateRead",
+ "type": "object"
+ },
+ "FiscalDocumentRead": {
+ "description": "NUNCA inclui `xml_assinado` -- o XML sai s\u00f3 por `GET /v1/documentos/\n{id}/xml` (`Response(media_type=\"application/xml\")`), mesmo racional de\n`FiscalCertificateRead` nunca incluir o bin\u00e1rio do certificado. Sem\n`sale_id`/`service_order_id` (porte table: este servi\u00e7o n\u00e3o conhece o\ndom\u00ednio do produto chamador).",
+ "properties": {
+ "ambiente": {
+ "title": "Ambiente",
+ "type": "string"
+ },
+ "autorizada_em": {
+ "anyOf": [
+ {
+ "format": "date-time",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Autorizada Em"
+ },
+ "branch_ref": {
+ "title": "Branch Ref",
+ "type": "string"
+ },
+ "chave_acesso": {
+ "title": "Chave Acesso",
+ "type": "string"
+ },
+ "codigo_numerico": {
+ "title": "Codigo Numerico",
+ "type": "string"
+ },
+ "created_at": {
+ "format": "date-time",
+ "title": "Created At",
+ "type": "string"
+ },
+ "document_model": {
+ "title": "Document Model",
+ "type": "string"
+ },
+ "id": {
+ "format": "uuid",
+ "title": "Id",
+ "type": "string"
+ },
+ "numero": {
+ "title": "Numero",
+ "type": "integer"
+ },
+ "product_id": {
+ "format": "uuid",
+ "title": "Product Id",
+ "type": "string"
+ },
+ "protocolo": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Protocolo"
+ },
+ "rejeicao_codigo": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Rejeicao Codigo"
+ },
+ "rejeicao_motivo": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Rejeicao Motivo"
+ },
+ "serie": {
+ "title": "Serie",
+ "type": "integer"
+ },
+ "series_id": {
+ "format": "uuid",
+ "title": "Series Id",
+ "type": "string"
+ },
+ "status": {
+ "title": "Status",
+ "type": "string"
+ },
+ "tenant_ref": {
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ "required": [
+ "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"
+ ],
+ "title": "FiscalDocumentRead",
+ "type": "object"
+ },
+ "FiscalResultPayload": {
+ "properties": {
+ "cfop": {
+ "title": "Cfop",
+ "type": "string"
+ },
+ "consumidor_final": {
+ "title": "Consumidor Final",
+ "type": "boolean"
+ },
+ "csosn": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Csosn"
+ },
+ "cst": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cst"
+ },
+ "indicador_ie": {
+ "title": "Indicador Ie",
+ "type": "string"
+ },
+ "origem": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Origem"
+ },
+ "tributos": {
+ "items": {
+ "$ref": "#/components/schemas/TributoLinhaPayload"
+ },
+ "title": "Tributos",
+ "type": "array"
+ }
+ },
+ "required": [
+ "cfop",
+ "consumidor_final",
+ "indicador_ie"
+ ],
+ "title": "FiscalResultPayload",
+ "type": "object"
+ },
+ "FiscalSeriesCreate": {
+ "description": "POST /v1/series body. `serie` allows `0` (SEFAZ convention for\n\"s\u00e9rie \u00fanica\"/sem s\u00e9rie formal) -- `ge=0`, unlike `next_number` which\nmust start at 1 (`ge=1`, a document numbering never starts at 0). Only\n`\"55\"` is accepted for now (Global Constraints/spec: F2 covers NF-e 55\nemission only -- `xml_builder.build_nfe` itself hardcodes `mod=\"55\"`;\naccepting `\"65\"` here would create a series nothing can ever allocate\nagainst without silently wrong output).",
+ "properties": {
+ "branch_ref": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ },
+ "document_model": {
+ "const": "55",
+ "default": "55",
+ "title": "Document Model",
+ "type": "string"
+ },
+ "next_number": {
+ "minimum": 1.0,
+ "title": "Next Number",
+ "type": "integer"
+ },
+ "serie": {
+ "minimum": 0.0,
+ "title": "Serie",
+ "type": "integer"
+ },
+ "tenant_ref": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ "required": [
+ "tenant_ref",
+ "branch_ref",
+ "serie",
+ "next_number"
+ ],
+ "title": "FiscalSeriesCreate",
+ "type": "object"
+ },
+ "FiscalSeriesPatch": {
+ "additionalProperties": false,
+ "description": "PATCH /v1/series/{series_id} body -- ONLY `next_number` is editable\n(a manual correction, e.g. realigning the counter after a migration\nfrom another emissor). `tenant_ref`/`branch_ref`/`document_model`/\n`serie` are immutable once created -- delete and recreate the series\ninstead if it was set up wrong (no DELETE endpoint exists yet for this\nresource per the F2 API contract; ported convention documented here for\nwhen it does). `next_number` maps to a NOT NULL column, so explicit\n`null` is rejected with 422, same convention as the auto's\n`FiscalDocumentSeriesPatch`.",
+ "properties": {
+ "next_number": {
+ "anyOf": [
+ {
+ "minimum": 1.0,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Next Number"
+ }
+ },
+ "title": "FiscalSeriesPatch",
+ "type": "object"
+ },
+ "FiscalSeriesRead": {
+ "properties": {
+ "branch_ref": {
+ "title": "Branch Ref",
+ "type": "string"
+ },
+ "document_model": {
+ "title": "Document Model",
+ "type": "string"
+ },
+ "id": {
+ "format": "uuid",
+ "title": "Id",
+ "type": "string"
+ },
+ "next_number": {
+ "title": "Next Number",
+ "type": "integer"
+ },
+ "product_id": {
+ "format": "uuid",
+ "title": "Product Id",
+ "type": "string"
+ },
+ "serie": {
+ "title": "Serie",
+ "type": "integer"
+ },
+ "tenant_ref": {
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "product_id",
+ "tenant_ref",
+ "branch_ref",
+ "document_model",
+ "serie",
+ "next_number"
+ ],
+ "title": "FiscalSeriesRead",
+ "type": "object"
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "title": "Detail",
+ "type": "array"
+ }
+ },
+ "title": "HTTPValidationError",
+ "type": "object"
+ },
+ "ItemDataPayload": {
+ "properties": {
+ "cest": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cest"
+ },
+ "cfop": {
+ "title": "Cfop",
+ "type": "string"
+ },
+ "codigo": {
+ "title": "Codigo",
+ "type": "string"
+ },
+ "descricao": {
+ "title": "Descricao",
+ "type": "string"
+ },
+ "fiscal_result": {
+ "$ref": "#/components/schemas/FiscalResultPayload"
+ },
+ "gtin": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Gtin"
+ },
+ "ncm": {
+ "title": "Ncm",
+ "type": "string"
+ },
+ "peso_bruto_kg": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Peso Bruto Kg"
+ },
+ "peso_liquido_kg": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Peso Liquido Kg"
+ },
+ "quantidade": {
+ "anyOf": [
+ {
+ "exclusiveMinimum": 0.0,
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Quantidade"
+ },
+ "unidade_comercial": {
+ "title": "Unidade Comercial",
+ "type": "string"
+ },
+ "unidade_tributavel": {
+ "title": "Unidade Tributavel",
+ "type": "string"
+ },
+ "valor_unitario": {
+ "anyOf": [
+ {
+ "exclusiveMinimum": 0.0,
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Valor Unitario"
+ }
+ },
+ "required": [
+ "codigo",
+ "descricao",
+ "ncm",
+ "cfop",
+ "unidade_comercial",
+ "unidade_tributavel",
+ "quantidade",
+ "valor_unitario",
+ "fiscal_result"
+ ],
+ "title": "ItemDataPayload",
+ "type": "object"
+ },
+ "PagamentoDataPayload": {
+ "properties": {
+ "indpag": {
+ "default": "0",
+ "title": "Indpag",
+ "type": "string"
+ },
+ "tpag": {
+ "title": "Tpag",
+ "type": "string"
+ },
+ "valor": {
+ "anyOf": [
+ {
+ "exclusiveMinimum": 0.0,
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Valor"
+ }
+ },
+ "required": [
+ "tpag",
+ "valor"
+ ],
+ "title": "PagamentoDataPayload",
+ "type": "object"
+ },
+ "TributoLinhaPayload": {
+ "properties": {
+ "aliquota": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Aliquota"
+ },
+ "aliquota_st": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Aliquota St"
+ },
+ "base_calc": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Base Calc"
+ },
+ "base_calc_percent": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Base Calc Percent"
+ },
+ "codigo_beneficio": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Codigo Beneficio"
+ },
+ "csosn": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Csosn"
+ },
+ "cst": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cst"
+ },
+ "fcp_percent": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Fcp Percent"
+ },
+ "mva": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mva"
+ },
+ "rule_id": {
+ "format": "uuid",
+ "title": "Rule Id",
+ "type": "string"
+ },
+ "tax_domain": {
+ "title": "Tax Domain",
+ "type": "string"
+ },
+ "valor": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
+ "type": "string"
+ }
+ ],
+ "title": "Valor"
+ }
+ },
+ "required": [
+ "tax_domain",
+ "base_calc",
+ "base_calc_percent",
+ "valor",
+ "rule_id"
+ ],
+ "title": "TributoLinhaPayload",
+ "type": "object"
+ },
+ "ValidationError": {
+ "properties": {
+ "ctx": {
+ "title": "Context",
+ "type": "object"
+ },
+ "input": {
+ "title": "Input"
+ },
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "title": "Location",
+ "type": "array"
+ },
+ "msg": {
+ "title": "Message",
+ "type": "string"
+ },
+ "type": {
+ "title": "Error Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError",
+ "type": "object"
+ }
+ }
+ },
+ "info": {
+ "title": "sowai-fiscal-svc",
+ "version": "0.1.0"
+ },
+ "openapi": "3.1.0",
+ "paths": {
+ "/v1/certificados": {
+ "delete": {
+ "operationId": "deactivate_certificate_endpoint_v1_certificados_delete",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "tenant_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "branch_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Deactivate Certificate Endpoint",
+ "tags": [
+ "certificados"
+ ]
+ },
+ "get": {
+ "description": "Metadados do certificado VIVO -- NUNCA o bin\u00e1rio (o .pfx/senha\ncifrados nunca saem do banco por esta rota; s\u00f3 `certificates.crypto.\nload_private_key_and_cert`, uso interno da emiss\u00e3o, Task 5, os\ndescriptografa, e s\u00f3 em mem\u00f3ria).",
+ "operationId": "read_certificate_endpoint_v1_certificados_get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "tenant_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "branch_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalCertificateRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Read Certificate Endpoint",
+ "tags": [
+ "certificados"
+ ]
+ },
+ "post": {
+ "operationId": "upload_certificate_endpoint_v1_certificados_post",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "tenant_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "branch_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_upload_certificate_endpoint_v1_certificados_post"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalCertificateRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Upload Certificate Endpoint",
+ "tags": [
+ "certificados"
+ ]
+ }
+ },
+ "/v1/documentos": {
+ "get": {
+ "operationId": "list_fiscal_documents_endpoint_v1_documentos_get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "tenant_ref",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "maxLength": 64,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Tenant Ref"
+ }
+ },
+ {
+ "in": "query",
+ "name": "branch_ref",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "maxLength": 64,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Branch Ref"
+ }
+ },
+ {
+ "in": "query",
+ "name": "status",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "maxLength": 20,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Status"
+ }
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "required": false,
+ "schema": {
+ "default": 50,
+ "maximum": 200,
+ "minimum": 1,
+ "title": "Limit",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "offset",
+ "required": false,
+ "schema": {
+ "default": 0,
+ "minimum": 0,
+ "title": "Offset",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/FiscalDocumentRead"
+ },
+ "title": "Response List Fiscal Documents Endpoint V1 Documentos Get",
+ "type": "array"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "List Fiscal Documents Endpoint",
+ "tags": [
+ "emissao"
+ ]
+ }
+ },
+ "/v1/documentos/{document_id}": {
+ "get": {
+ "operationId": "get_fiscal_document_endpoint_v1_documentos__document_id__get",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "document_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Document Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalDocumentRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Get Fiscal Document Endpoint",
+ "tags": [
+ "emissao"
+ ]
+ }
+ },
+ "/v1/documentos/{document_id}/xml": {
+ "get": {
+ "operationId": "get_fiscal_document_xml_endpoint_v1_documentos__document_id__xml_get",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "document_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Document Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Get Fiscal Document Xml Endpoint",
+ "tags": [
+ "emissao"
+ ]
+ }
+ },
+ "/v1/emissoes": {
+ "post": {
+ "description": "`Idempotency-Key` OBRIGAT\u00d3RIO (design spec decis\u00e3o #6) -- ausente\nvira 422 na borda do FastAPI (`Header(...)`, sem default), antes de\nqualquer l\u00f3gica rodar. Repetida com a MESMA `product_id` -> 200 com o\ndocumento j\u00e1 existente (`created=False`); nova -> 201 (`created=True`).\nVer `emission.service.emitir_documento`'s docstring para o padr\u00e3o de\nduas camadas que garante isto mesmo sob duas requisi\u00e7\u00f5es concorrentes\ncom a mesma chave.",
+ "operationId": "emitir_documento_endpoint_v1_emissoes_post",
+ "parameters": [
+ {
+ "in": "header",
+ "name": "Idempotency-Key",
+ "required": true,
+ "schema": {
+ "maxLength": 255,
+ "minLength": 1,
+ "title": "Idempotency-Key",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EmissaoRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalDocumentRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Emitir Documento Endpoint",
+ "tags": [
+ "emissao"
+ ]
+ }
+ },
+ "/v1/health": {
+ "get": {
+ "operationId": "health_check_v1_health_get",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "title": "Response Health Check V1 Health Get",
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful Response"
+ }
+ },
+ "summary": "Health Check"
+ }
+ },
+ "/v1/series": {
+ "get": {
+ "operationId": "list_fiscal_series_endpoint_v1_series_get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "tenant_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Tenant Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "branch_ref",
+ "required": true,
+ "schema": {
+ "maxLength": 64,
+ "title": "Branch Ref",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/FiscalSeriesRead"
+ },
+ "title": "Response List Fiscal Series Endpoint V1 Series Get",
+ "type": "array"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "List Fiscal Series Endpoint",
+ "tags": [
+ "series"
+ ]
+ },
+ "post": {
+ "operationId": "create_fiscal_series_endpoint_v1_series_post",
+ "parameters": [
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalSeriesCreate"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalSeriesRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Create Fiscal Series Endpoint",
+ "tags": [
+ "series"
+ ]
+ }
+ },
+ "/v1/series/{series_id}": {
+ "patch": {
+ "operationId": "update_fiscal_series_endpoint_v1_series__series_id__patch",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "series_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Series Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Api-Key",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Api-Key"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalSeriesPatch"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FiscalSeriesRead"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Update Fiscal Series Endpoint",
+ "tags": [
+ "series"
+ ]
+ }
+ }
+ }
+}
diff --git a/scripts/generate_signed_goldens.py b/scripts/generate_signed_goldens.py
new file mode 100644
index 0000000..65963fb
--- /dev/null
+++ b/scripts/generate_signed_goldens.py
@@ -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()
diff --git a/scripts/generate_test_cert.py b/scripts/generate_test_cert.py
new file mode 100644
index 0000000..9684b88
--- /dev/null
+++ b/scripts/generate_test_cert.py
@@ -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()
diff --git a/tests/fixtures/test_cert.pfx b/tests/fixtures/test_cert.pfx
new file mode 100644
index 0000000..5c68153
Binary files /dev/null and b/tests/fixtures/test_cert.pfx differ
diff --git a/tests/goldens_signed/caso_com_ub.expected.xml b/tests/goldens_signed/caso_com_ub.expected.xml
new file mode 100644
index 0000000..67257fb
--- /dev/null
+++ b/tests/goldens_signed/caso_com_ub.expected.xml
@@ -0,0 +1,18 @@
+4110000005Venda55110052026-07-16T10:20:00-03:0011410690211021110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123005102UN1100.00100.00SEM GTINUN1100.00101020808000000001100.000.100010.000.900090.000.000.000.000.000.000.000.000.00100.000.000.000.000.000.000.000.000.000.00100.009001100.00EIBfqHSPgzGamG7niiWJY8QjvaY=lMywqdUdSFsbf5SR1kOmyByaFI1aSoUJwwgbz+P7Qri6iW+pJy+JJcapJnQcEdYbbFTqMJYJeRM87xHlQngg0SnPqd1ZjcLqZgXf6tm7GZriS3SNaMiXUC5EG9BnawvP4p7m5GviX3JJNKs2lvG0unBUtptq4aH/cZe5jd62SxeqyaeX4PxW/pbvkSBu3WndTLvM56vajMgimjbjQ8zLid85XJkVEAk9rV7KObZsHl0vuvFUqnZsAVaIJxB8nmmmcn/clT4AsPzhlkozR5YV0OjpP4sm08bYcvPcGcxxidxHE1o40xr6S6ozpiQENPEUBgTpNqMYaJTI0Rft3ERWaA==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=
+
\ No newline at end of file
diff --git a/tests/goldens_signed/caso_devolucao_intra.expected.xml b/tests/goldens_signed/caso_devolucao_intra.expected.xml
new file mode 100644
index 0000000..ae21a3d
--- /dev/null
+++ b/tests/goldens_signed/caso_devolucao_intra.expected.xml
@@ -0,0 +1,18 @@
+4110000004Devolucao de venda55110042026-07-16T10:15:00-03:0011410690211521110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123001202UN175.0075.00SEM GTINUN175.0010102080875.000.000.000.000.000.000.000.0075.000.000.000.000.000.000.000.000.000.0075.00909075.00dWH2Qpht8rN1pncCUL31+ymheWE=iyBjOX1COZz2vFk1SNaQvmLqvsXGo63DINjVvAsoAA2g6XsnoLdrNjXOtxATh4Iez1SEcVoOex5V6x4/Vdd7EkX8TSu5QKqIAbDUjystojZAVW+wXj7enUgb24qbrsFU40EhEPUdZp51grKgm0d2R4qNeodXZiFGbuheRZQ4EhY5b+2EPX6kSdxGuYi4F0Xq0JgkG90CTJIUFNEDkY2PKCXOipNBpx6nOYiWnEBGfMw2oTmJeXQ2yKyXjrr01S9Lj48UZybGMIStQJoMTHn+p9+3ccTwbBAJ66Et38SEsP8gmWaLkGq5+zMCtriiwEJzCJMzTUMnDPee+/nada0KWg==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=
+
\ No newline at end of file
diff --git a/tests/goldens_signed/caso_fracao_centavo.expected.xml b/tests/goldens_signed/caso_fracao_centavo.expected.xml
new file mode 100644
index 0000000..27e96e9
--- /dev/null
+++ b/tests/goldens_signed/caso_fracao_centavo.expected.xml
@@ -0,0 +1,18 @@
+4110000006Venda55110062026-07-16T10:25:00-03:0011410690211621110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC900SEM GTINArruela avulsa731819005102UN1.50.010.02SEM GTINUN1.50.01101020808PC901SEM GTINArruela avulsa 2731819005102UN1.50.010.02SEM GTINUN1.50.011010208080.040.000.000.000.000.000.000.000.040.000.000.000.000.000.000.000.000.000.0490010.04UrxPctSfvF8tf9cYULFIdWbEUMA=Bdi9HkxrMIPD7dtfd8wq1Mg2hCt+ZFffU3n79WqAxcDDLgtjCOb/vX4hvXj+J+LZ+SEcNzKFLF5kRyriHb7+e9YkS7BFgljNOGZunqkTz6/ZdmrPNlZB89z6SOB5aI+Z+J2KHtOO4l7kyIji5EdJl+NrDfRoCpBFFctBVMKCKtwBh31KoNDWq0XzqmfI+FDfZskdgR+W0AGFg6pGbNfv4dhpOAPT5/UY8HQDU4Zv6hVleUv/ifsqKTKzEa7z9/IL+5894NoPk9JyhNAFgKQDs4y2r1irykOsu1LHH+BvnPPCGKeKCLsVxqTgQxseKkfhiis2hTt97e365a7UmZ1JaA==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=
+
\ No newline at end of file
diff --git a/tests/goldens_signed/caso_padrao_inter.expected.xml b/tests/goldens_signed/caso_padrao_inter.expected.xml
new file mode 100644
index 0000000..c496ec2
--- /dev/null
+++ b/tests/goldens_signed/caso_padrao_inter.expected.xml
@@ -0,0 +1,18 @@
+4110000002Venda55110022026-07-16T10:05:00-03:0012410690211421110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890111222333000144NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Paulista1000Sala 10Bela Vista3550308Sao PauloSP013100001058Brasil11122334455compras@clienteinter.com.brPC001SEM GTINFiltro de oleo842123006102UN1075.00750.00SEM GTINUN1075.00101020808750.000.000.000.000.000.000.000.00750.000.000.000.000.000.000.000.000.000.00750.009017750.00wMN3wf/wyPYVP/p8oSMeLIwQ0sI=YTzAGw2xRWQSgMsJuF1XB2elWaTAHRRnLvOwM3Bo6YHNGVM3AqViOlhf0jRmbuQe4VCj8ugSEAe24p50Mn0YGLqH91emub4ADsewNAjeIcb7C9n/cYk8Sh2e/wPc3DvS5cOSICUOroPak0A3bWFb8DjRAcUdYvX/uI0IrzMlN0Qxzjd+lZwo/1cmzIWnW0G+nDvd2rykGOZTwCxd+yZf+qHw+A1iLFgeI9YHyWcWoUtKH746GvlJQ59Oz4dTXgtJL3Nnhx2O7dsgbU0/ee8Q6bYGELZz6Jtdup3EOVI7T5K/BBgVeNpchQfj7hHdL1gzmVAnGdVnBR01EDQnscVtZw==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=
+
\ No newline at end of file
diff --git a/tests/goldens_signed/caso_padrao_intra.expected.xml b/tests/goldens_signed/caso_padrao_intra.expected.xml
new file mode 100644
index 0000000..1ac7e9b
--- /dev/null
+++ b/tests/goldens_signed/caso_padrao_intra.expected.xml
@@ -0,0 +1,18 @@
+4110000001Venda55110012026-07-16T10:00:00-03:0011410690211921110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123005102UN275.00150.00SEM GTINUN275.00101020808PC002SEM GTINPastilha de freio870830905102PC445.50182.00SEM GTINPC445.50101020808332.000.000.000.000.000.000.000.00332.000.000.000.000.000.000.000.000.000.00332.009001332.001inHNlmRb0maeftKaq/JExhNc8A=ked+Fn1wxCy5EdNT289eLof7cqwuWvj1bh/AV0CQ3zbrLD5kL5/fXj2pLMZWS9wyl3NG8ZFATJ5B1xmeDcc0Niigs9O+e9wwWgX+3STDHZOogLuOU/CKhgFq/6e4wkZlMhV7Yffo+swo5qf82itGxUkkQkD00TDuFP7OnKOZBW8Ti+CPPPm+HLv613+BolP14If/2BXJW9HZiSu4Cua66/E32RSqPSp319DPvvpGwefJWDddOYfK9TkChirgdygAQUrkeJutcQdzzo9LnSVKE3FAu6q5w3rkNIrqA0OTEctGpojwfXc9E+IzRehof6fPvDCUjwSe89fP4JpK6tYiYA==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=
+
\ No newline at end of file
diff --git a/tests/goldens_signed/caso_st_intra.expected.xml b/tests/goldens_signed/caso_st_intra.expected.xml
new file mode 100644
index 0000000..5a30aa4
--- /dev/null
+++ b/tests/goldens_signed/caso_st_intra.expected.xml
@@ -0,0 +1,18 @@
+4110000003Venda55110032026-07-16T10:10:00-03:0011410690211021110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890112345678909NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALRua Oficina22Industrial4106902CuritibaPR800002001058Brasil9OL500SEM GTINOleo lubrificante 15W40 1L2710125906001005405UN1100.00100.00SEM GTINUN1100.0010500140.0018.000013.2004040.000.000.000.00140.0013.200.000.00100.000.000.000.000.000.000.000.000.000.00100.009001100.00GynTwfOg2ofaLE8WyWWwdpLBjWg=1KMXWaK4qME2/kgazsMvEIB0B2Oxr6bLCNtfa6IrWd/i+muv9CDRhOm/yKumdRdmC6wys0o0krkwOW0Zh91iCikvHVyO/sh4ze4ycFzE2cODFpEciS7+59pMZM4vARhoZga855QKTzCB6CvFx99VAeq7X0wxXc7FWEmz2ENH6Vn0pkFkiMlpIgT1yFtGZbOQ5lcuE0hGTQJlEool/dbZRWWNp2CPbpx4Hys/CwLUM/MIA7lgfOqvEE0VxrfsU4Cfk2azBylFmprGnySot6HMPl0nkmGoVWEnaX3hxIlJJjfHXPZMnRp/HeryZpNb5GRzWjW9oJlXZUkzPcc02gkI8g==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=
+
\ No newline at end of file
diff --git a/tests/test_contract.py b/tests/test_contract.py
new file mode 100644
index 0000000..0017d9d
--- /dev/null
+++ b/tests/test_contract.py
@@ -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 ".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'"
+ )