# 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.py` portado), migration real test (INSERT cru), AST guard do `FOR 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 venda | não existem — a emissão recebe `DadosEmissao` completo | | anti-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-Key` header obrigatório no POST /v1/emissoes; repetida → 200 com o documento existente. - O Dockerfile do serviço JÁ NASCE com `git` no apt (lição C1 da F1). - OpenAPI: `docs/openapi-v1.json` COMMITADO + 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`; database `fiscal_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 via `make k8s-test`; commit `feat: 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_product` dependency: `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.py` adaptado 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_proc` ausente → 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.0` do serviço. --- ## Após as tasks 1. Review Opus (porte fiel? adaptações corretas? idempotência sem corrida?) + **Fable** (spec F2 vs construído; drift de porte). 2. 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. 3. Kanban (card dd1fa67c → F2) + memória. Próximo: F3 (corte do auto).