FastAPI + SQLAlchemy async + Alembic scaffold, src-layout (mirrors the
sowai-fiscal lib's own convention), pyproject wired to sowai-fiscal@v0.1.0
via git+https (uv.lock pins the commit). Makefile mirrors auto/Makefile's
k8s-test workflow: syncs into /app/fiscal-svc in the SAME auto-tests pod,
against a dedicated fiscal_svc_test database on the shared Postgres
sidecar, serialized by the SAME lock file the auto uses on purpose so the
two repos' test runs never race in the pod. Dockerfile installs git (the
git+https dependency needs it at uv sync time) and splits the dependency
layer from the project's own editable install for build caching.
GET /v1/health -> {"status": "ok"}, verified green via `make k8s-test`.
8.0 KiB
F2 — sowai-fiscal-svc com paridade 1b.1 — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: O serviço fiscal existe: API v1 multi-produto que emite (sem transmitir — paridade 1b.1), guarda certificados e séries como DONO, com idempotência, goldens (incl. ASSINADOS), suíte de contrato HTTP e OpenAPI commitado.
Architecture: FastAPI + SQLAlchemy async + Alembic no repo novo (git.sowai.com.br/jonatan/sowai-fiscal-svc, já criado, vazio), embutindo a lib sowai-fiscal@v0.1.0. A lógica de emissão/certificado é PORTADA do auto (código nosso, já revisado 3x — portar com a tabela de adaptações abaixo, não reinventar). Tenancy (product_id, tenant_ref, branch_ref) com refs opacas. Testes no MESMO pod k8s do auto (database própria fiscal_svc_test no sidecar Postgres — o lock compartilhado serializa com as suítes do auto de graça).
Tech Stack: Python 3.11, uv, FastAPI, SQLAlchemy 2.0 async, Alembic, pydantic v2, sowai-fiscal @ git+https…@v0.1.0, erpbrasil.assinatura, cryptography.
Spec (governa): auto/docs/superpowers/specs/2026-07-17-sowai-fiscal-svc-design.md (F2 + decisões 1–8 + emendas F1). Este plano é COPIADO para o repo do serviço no Task 1 (docs/plans/) junto do spec.
Global Constraints
- Convenções do auto valem aqui: zero enum PG (String + enum Python), soft delete, Decimal com bounds espelhando colunas,
max_length, 409 estruturado (shared/errors.pyportado), migration real test (INSERT cru), AST guard doFOR UPDATE populate_existing(portar o teste), testes de detecção provados nas duas direções. - Tabela de adaptações do porte (vale para TODO código portado do auto):
No auto No serviço organization_id: UUID(FK organizations)product_id: UUID(FK products) +tenant_ref: String(64)+ índices por (product_id, tenant_ref)branch_id: UUID(FK branches)branch_ref: String(64)+cnpj: String(14)(o vínculo forte — validado contra certificado/emitente)require_permission(...)require_product(...)(API key → Product)Sale/rotas por vendanão existem — a emissão recebe DadosEmissaocompletoanti-oracle 404 por org anti-oracle 404 por (product_id, tenant_ref) ver_procé OBRIGATÓRIO no payload de emissão (emenda F1 — nada de default "sowai-auto" da lib).- Idempotência:
Idempotency-Keyheader obrigatório no POST /v1/emissoes; repetida → 200 com o documento existente. - O Dockerfile do serviço JÁ NASCE com
gitno apt (lição C1 da F1). - OpenAPI:
docs/openapi-v1.jsonCOMMITADO + teste que falha se divergir do gerado (app.openapi()). - Testes: Makefile próprio espelhando o do auto (lock compartilhado
/tmp/auto-k8s-test.lock— MESMO arquivo, de propósito; sync para um diretório PRÓPRIO no pod/app/fiscal-svc; databasefiscal_svc_test). - Commits em inglês; sem push do auto (o auto não muda na F2).
Task 1: scaffold + test-infra + health
Files (repo /Volumes/MacHD1/sow/sowai-fiscal-svc): pyproject.toml (deps acima + dev: pytest, pytest-asyncio, httpx), src/fiscal_svc/{__init__,main,core/config,core/db}.py, Dockerfile (base slim + git + libs mínimas — SEM WeasyPrint por ora; DANFE é F4), Makefile (k8s-test/k8s-test-file/k8s-deps espelhados do auto, sync p/ /app/fiscal-svc, database fiscal_svc_test, MESMO lock), docs/ (copiar spec + este plano), tests/conftest.py (engine/session/override no padrão do auto), tests/test_health.py.
- Health:
GET /v1/health→{"status":"ok"}; teste passa NO POD viamake k8s-test; commitfeat: service scaffold, k8s test infra, health.
Task 2: tenancy — products + API keys
Files: src/fiscal_svc/tenancy/{models,service,deps}.py, migration products (id, name, api_key_hash — bcrypt via passlib —, webhook_secret nullable p/ F4, timestamps/soft-delete), scripts/create_product.py (CLI: gera key aleatória, imprime UMA vez, salva hash), tests/test_tenancy.py.
require_productdependency:X-Api-Key→ Product vivo (hash check) ou 401; key ausente → 401; produto soft-deletado → 401. Migration real test. Commit.
Task 3: models + migrations (as tabelas mudam de dono)
Files: src/fiscal_svc/documents/models.py — portar FiscalCertificate, FiscalDocument, e FiscalSeries (novo dono da numeração: portar FiscalDocumentSeries de auto/backend/app/modules/tenants/models.py + o allocate_fiscal_number de tenants/service.py COM o contrato no-commit + populate_existing + o guard retroativo next_number > max(numero)), com a tabela de adaptações. Migrations + real tests (INSERT cru). Constraints preservadas: UNIQUE chave_acesso; índice parcial único cert-vivo-por-branch_ref; UNIQUE (product_id, tenant_ref, branch_ref, document_model, serie) na série.
- AST guard portado (
tests/test_for_update_populate_existing.pyadaptado ao src novo). Commit.
Task 4: certificados + séries (API v1)
Files: src/fiscal_svc/certificates/{service,router}.py (portar auto/.../fiscal/certificate.py + rotas: POST/GET/DELETE /v1/certificados por branch_ref+cnpj; Fernet com env FISCAL_CERT_ENCRYPTION_KEY própria; validações intactas: senha/CNPJ — agora contra o cnpj do payload —, vencido, not-yet-valid, replace atômico, corrida→409), src/fiscal_svc/series/router.py (POST/GET/PATCH /v1/series), tests/.
- Testes portados do auto (test_certificate, test_fiscal_series guard) adaptados. Cross-product/tenant → 404 anti-oracle. Commit.
Task 5: emissão v1 (paridade 1b.1) + idempotência
Files: src/fiscal_svc/emission/{schemas,service,router}.py — portar auto/.../fiscal/emissao.py com as adaptações: input é EmissaoRequest = DadosEmissaoPayload (pydantic espelhando o DadosEmissao da lib + tenant_ref/branch_ref + ver_proc OBRIGATÓRIO + serie + document_model) — o serviço NÃO monta dados de venda (decisão 3: recebe pronto), só valida completude estrutural (os mesmos 409 fiscal_config_missing), resolve NADA (o FiscalResult vem dentro), aloca (série própria), monta chave (cNF do serviço, persistido), build_nfe da lib, assina (certificado do branch_ref), persiste ASSINADO num commit. Idempotency-Key UNIQUE por (product_id): repetida → 200 existente. Rotas: POST /v1/emissoes, GET /v1/documentos/{id}, /xml, lista com filtros.
- Testes: caminho feliz com goldens da lib como payload-fonte; outbox prova (assinatura sabotada → nNF não queimado); idempotência (2x mesma key → 1 documento, 200);
ver_procausente → 422; XSD válido. Commit.
Task 6: os seguros — goldens assinados + contrato HTTP + OpenAPI
Files: tests/fixtures/test_cert.pfx (gerado por script commitado, chave de TESTE — nunca real), tests/test_signed_goldens.py (para cada golden da lib: emitir via API com dh_emi/cnf pinados + cert de teste → XML ASSINADO determinístico comparado byte a byte com tests/goldens_signed/*.expected.xml gerados uma vez — RSA PKCS#1v1.5 é determinístico; fecha o gap da emenda 7b), tests/test_contract.py (a suíte de contrato: SÓ httpx contra o app — status codes, shapes, códigos 409 — zero import do interior; é a suíte que uma implementação Go teria que passar), docs/openapi-v1.json + tests/test_openapi_committed.py (gerado == commitado, senão falha mandando regenerar).
- Suíte completa verde no pod. Commit. Tag
v0.1.0do serviço.
Após as tasks
- Review Opus (porte fiel? adaptações corretas? idempotência sem corrida?) + Fable (spec F2 vs construído; drift de porte).
- Deploy dev: Deployment+Service ClusterIP no namespace
autopecas-dev(por ora — namespace próprio quando um 2º produto chegar), Secret próprio (fiscal-svc-secrets: Fernet key NOVA + database URL), NetworkPolicy. Smoke: health + emissão de teste via port-forward. - Kanban (card dd1fa67c → F2) + memória. Próximo: F3 (corte do auto).