# sowai-fiscal-svc — serviço fiscal compartilhado — Design **Data:** 2026-07-17 · **Decisão de origem:** Estágio 2 / Caminho B (Jonatan, 2026-07-17). **Onde este spec vive:** no repo do auto (primeiro consumidor) até o repo do serviço existir; então é copiado para lá e este vira ponteiro. ## Objetivo Emissão fiscal (NF-e 55 agora; NFS-e Nacional e NFC-e 65 depois) como **serviço compartilhado** entre produtos SowAI, consumido via REST. A 1b.2 (transporte SEFAZ + DANFE + reconciliador) **nasce dentro dele**. Python **permanente** (a comunidade OCA mantém o leiaute NT — nfelib/erpbrasil; Go descartado para este serviço por decisão registrada). ## Decisões batidas (2026-07-17 — não re-litigar) 1. **Dois repos novos (Gitea):** `sowai-fiscal` (lib — núcleo puro extraído do auto: `resolver`, `xml_builder`, `chave_acesso`, `domains`, `presets`, `tpag`; versionada SemVer; **EMENDA F1 2026-07-17: consumida via `git+https@tag` — pinada por hash no lock — em vez de registry PyPI do Gitea**; o registry fica como opção futura se o nº de consumidores crescer. Consequência operacional: `git` precisa existir nas imagens que rodam `uv sync` — Dockerfile do auto ajustado) e `sowai-fiscal-svc` (FastAPI + SQLAlchemy async + Alembic + `uv`, gabarito e convenções do auto — incluindo lock de pod de teste e teste de migration real). 2. **Banco:** schema próprio `fiscal_svc` na MESMA instância Postgres do cluster (dev); instância separada é decisão de produção futura. `fiscal_documents`, `fiscal_certificates`, `fiscal_document_series` **mudam de dono**: nascem no serviço via migrations dele; os dados de teste do auto migram por script one-shot; as tabelas do auto são dropadas ao fim do corte. 3. **O motor de regras FICA no produto.** `TaxRule`/`TaxProfile`/resolver rodam no auto (config de imposto é do produto); o serviço recebe o **`FiscalResult` já resolvido** dentro do `DadosEmissao`. O serviço valida coerência estrutural (somas, campos obrigatórios do leiaute), nunca recalcula imposto. 4. **Tenancy:** `(product_id, tenant_ref, branch_ref)` — refs OPACAS (o serviço não conhece o modelo de org de nenhum produto). O vínculo forte é o **CNPJ** da filial, validado contra o certificado no upload e contra o emitente na emissão. 5. **Auth:** API key por produto (`X-Api-Key`, hash argon2/bcrypt no banco, uma por `product_id`), sem ingress público — ClusterIP + NetworkPolicy no namespace. Webhooks assinados com HMAC (segredo por produto). 6. **Idempotência:** `Idempotency-Key` obrigatória no `POST /v1/emissoes` (o produto usa `sale_id` + tentativa). Chave repetida → devolve o documento existente (200), nunca emite duas vezes. 7. **Os 3 seguros de portabilidade** fazem parte do "pronto" de toda fase: (a) suíte de contrato HTTP language-agnostic (bate na API, não no interior); (b) **corpus golden** `DadosEmissao (JSON) → XML PRÉ-ASSINATURA esperado` como fixtures (**EMENDA F1**: pré-assinatura para determinismo byte a byte com dh_emi/cnf pinados. GAP REGISTRADO: o caminho de ASSINATURA fica sem seguro golden — a F2 DEVE acrescentar goldens assinados com certificado de TESTE fixo do repo, viável porque RSA PKCS#1v1.5 é determinístico dado input+chave iguais); (c) OpenAPI versionado como fonte de verdade do contrato. 8. **Invariantes herdados do 1b.1 permanecem intactos DENTRO do serviço:** outbox (alocar nNF + persistir documento num commit), cNF persistido (anti-539), chave módulo-11, certificado Fernet decifrado só em memória, um certificado vivo por branch_ref (índice parcial único), imutabilidade pós-ASSINADO, fail-closed com 409 estruturado. 9. **Transmissão (1b.2, dentro do serviço):** síncrona `indSinc=1` (SVRS rejeita lote assíncrono de 1 — Rej. 452), timeout → retransmitir o MESMO XML → 204 → `NfeConsultaProtocolo`; reconciliador = CronJob k8s → endpoint interno varrendo `TRANSMITINDO`/`PENDENTE_CONSULTA`. Homologação primeiro, com o **A1 da própria SowAI** (CNPJ 63329985000113; senha só o Jonatan digita, no upload). 10. **Auto consome via port `FiscalEmitter`:** duas implementações — `InProcessEmitter` (a atual, morre no corte) e `HttpEmitter` (client do serviço). Corte por flag/env; rollback = voltar a flag. ## API v1 (contrato) | Rota | O quê | |---|---| | `POST /v1/emissoes` | `DadosEmissao` completo + `Idempotency-Key`. Fluxo: valida → aloca → monta → assina → persiste (ASSINADO) → transmite (1b.2) → devolve estado final (`AUTORIZADA`/`REJEITADA`) ou `PENDENTE_CONSULTA` (timeout). Erros de config → 409 estruturado (`fiscal_config_missing` etc., os mesmos códigos do 1b.1). | | `GET /v1/documentos/{id}` | estado + metadados (chave, número, protocolo, rejeição). | | `GET /v1/documentos/{id}/xml` · `/danfe` | XML assinado/autorizado · DANFE PDF (1b.2). | | `GET /v1/documentos?tenant_ref=&branch_ref=&status=` | listagem paginada. | | `POST /v1/certificados` (por `branch_ref`) · `GET` (metadados) · `DELETE` | ciclo do A1 (as validações do 1b.1: senha/CNPJ/validade). | | `POST /v1/series` · `GET` · `PATCH` | numeração por `branch_ref`+modelo (guard retroativo `next_number > max(numero)` vem junto). | | `POST /v1/webhooks` | registro de callback por produto; eventos `documento.status_changed` com HMAC; retry com backoff; o produto mantém polling de reconciliação como fallback. | `DadosEmissao` v1 = o dataclass do 1b.1 congelado (emitente, destinatário, itens+FiscalResult por item, pagamento, ambiente) + `product_id/tenant_ref/branch_ref`. Congelar nomes = contrato; mudanças = v2. ## Sequência de entrega (cada fase = plano próprio, ciclo completo com Fable) 1. **F1 — lib `sowai-fiscal`:** extração do núcleo puro (o guard `test_fiscal_core_purity` do auto garante que continua extraível) + **goldens gerados do 1b.1 atual** (N casos: Padrão intra/inter, ST, devolução, com/sem UB) + publicação no registry. Auto passa a depender da lib (deleta o código duplicado). 2. **F2 — serviço com paridade 1b.1** (pronto inclui: OpenAPI **commitado como artefato versionado** — o seguro (c) — e os goldens ASSINADOS do gap da emenda 7b; `ver_proc` vira parâmetro por produto no contrato, não default da lib): repo, schema `fiscal_svc`, migrations, tenancy, API v1 (sem transmissão), certificados, séries. **Prova de paridade: os goldens do F1 passam byte a byte.** Suíte de contrato HTTP nasce aqui. 3. **F3 — corte do auto:** `HttpEmitter` atrás do port, migração one-shot dos dados de teste, flag virada, tabelas fiscais do auto dropadas (migration), frontend intocado (o contrato do auto com o frontend não muda — o auto proxia). 4. **F4 — 1b.2 no serviço:** transporte SEFAZ homologação (SVRS), estados AUTORIZADA/REJEITADA/PENDENTE_CONSULTA, reconciliador CronJob, DANFE, webhooks. Validação de ponta: primeira nota AUTORIZADA em homologação com o A1 da SowAI. 5. **F5 — NFS-e Nacional no serviço** (spec próprio; deadline do cliente 01/09). ## Fora de escopo (deste spec) NFC-e/contingência (1b.4), eventos (1b.3), cálculo IBS/CBS (R1 — fica no motor do produto), produção SEFAZ (após homologação validada), multi-região. ## Riscos e mitigações - **Paridade de XML na extração** → goldens byte a byte (F1/F2) — regressão fiscal é impossível de passar silenciosa. - **Corte do auto** → port + flag + rollback trivial; dados de teste migrados por script idempotente conferido por contagem+chaves. - **Webhook perdido** → polling de reconciliação no produto (o status cacheado na venda nunca é a fonte de verdade; a fonte é o serviço). - **Reforma/NTs** → a lib versionada absorve upgrades de nfelib; produtos sobem a lib, o serviço sobe primeiro.