Files
sowai-fiscal-svc/src/fiscal_svc/emission/schemas.py
T
jonatanritterandClaude Opus 4.8 4b8532c604 docs(emission): fix stale schemas.py docstring re FiscalResultPayload/TributoLinhaPayload
MINOR (F2 review): the module docstring claimed FiscalResult/TributoLinha
'are NOT re-mirrored here' but the file defines FiscalResultPayload/
TributoLinhaPayload right below it. Corrects the prose to describe reality:
they ARE re-mirrored, deliberately kept separate from the lib's own pydantic
classes (this module is the wire contract the service owns), converted via
emission.service._to_fiscal_result/_to_tributo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 15:37:15 -03:00

178 lines
6.5 KiB
Python

"""Task 5: `EmissaoRequest` -- the pydantic mirror of `sowai_fiscal.
xml_builder.DadosEmissao` (+ `sowai_fiscal.resolver.FiscalResult`/
`TributoLinha`, which the lib itself already defines as pydantic
`BaseModel`s -- FIX 6, F2 review: these two ARE re-mirrored here, as
`FiscalResultPayload`/`TributoLinhaPayload` below, kept SEPARATE from the
lib's own classes on purpose -- this module is the wire contract this
SERVICE owns (a field the lib adds/renames should never silently change
what a caller across the network is allowed to send), while the lib's
`FiscalResult`/`TributoLinha` are what `sowai_fiscal.xml_builder.build_nfe`
actually consumes internally. `emission.service._to_fiscal_result`/
`_to_tributo` convert Payload -> lib class (`FiscalResult(**payload.
model_dump())`, not the payload objects themselves), adapted per the porte
table and design spec:
* `tenant_ref`/`branch_ref` (opaque strings, decision #4) replace what in
the auto was implicit (`sale.branch_id`).
* `document_model`/`serie` select WHICH `FiscalSeries` to allocate from
(Task 4) -- `chave_acesso`/`numero`/`cnf`/`dh_emi` are DROPPED from this
payload entirely (unlike the lib's own `DadosEmissao`, which expects
them pre-computed): this SERVICE computes all four itself (`cNF do
serviço, persistido`, plan Task 5) -- a caller can never inject its own
chave/número, closing the exact class of bug the auto's own emission
code had to defend against for `branch_id` (C1, "auditoria Fable
2026-07-16", `auto/backend/app/modules/fiscal/emissao.py`'s module
docstring).
* `ver_proc` is REQUIRED (no default) -- design spec's "EMENDA F1": the
lib's own `DadosEmissao.ver_proc` defaults to `"sowai-auto/1b.1"`, a
default that made sense when this code lived IN the auto and is
actively WRONG for every other product calling this shared service. A
plain (no-default) pydantic field is already "required, 422 if absent"
-- no extra validator needed.
* `fiscal_result` per item is the motor de regras' OUTPUT (design spec
decision #3: "O serviço NÃO resolve imposto (FiscalResult vem no
payload)") -- this service treats it as opaque data to embed in the
XML, never recomputes it."""
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
class TributoLinhaPayload(BaseModel):
tax_domain: str
cst: str | None = None
csosn: str | None = None
base_calc: Decimal
base_calc_percent: Decimal
aliquota: Decimal | None = None
valor: Decimal
mva: Decimal | None = None
aliquota_st: Decimal | None = None
fcp_percent: Decimal | None = None
codigo_beneficio: str | None = None
rule_id: uuid.UUID
class FiscalResultPayload(BaseModel):
cfop: str
cst: str | None = None
csosn: str | None = None
origem: str | None = None
consumidor_final: bool
indicador_ie: str
tributos: list[TributoLinhaPayload] = Field(default_factory=list)
class EmitenteDataPayload(BaseModel):
cnpj: str = Field(min_length=14, max_length=14)
razao_social: str
nome_fantasia: str | None = None
ie: str
crt: str = Field(min_length=1, max_length=1)
address_street: str
address_number: str
address_complement: str | None = None
address_district: str
address_city: str
address_state: str = Field(min_length=2, max_length=2)
address_zip: str
address_city_ibge_code: str
fone: str | None = None
class DestinatarioDataPayload(BaseModel):
"""`None` no `EmissaoRequest.destinatario` == consumidor final não
identificado -- espelha `sowai_fiscal.xml_builder.DestinatarioData`."""
nome: str
cnpj: str | None = None
cpf: str | None = None
indicador_ie: str
ie: str | None = None
address_street: str | None = None
address_number: str | None = None
address_complement: str | None = None
address_district: str | None = None
address_city: str | None = None
address_state: str | None = None
address_zip: str | None = None
address_city_ibge_code: str | None = None
email: str | None = None
class ItemDataPayload(BaseModel):
codigo: str
descricao: str
ncm: str
cfop: str
unidade_comercial: str
unidade_tributavel: str
quantidade: Decimal = Field(gt=0)
valor_unitario: Decimal = Field(gt=0)
fiscal_result: FiscalResultPayload
gtin: str | None = None
cest: str | None = None
peso_liquido_kg: Decimal | None = None
peso_bruto_kg: Decimal | None = None
class PagamentoDataPayload(BaseModel):
tpag: str
valor: Decimal = Field(gt=0)
indpag: str = "0"
class EmissaoRequest(BaseModel):
tenant_ref: str = Field(max_length=64)
branch_ref: str = Field(max_length=64)
# Only "55" for now -- see `series.schemas.FiscalSeriesCreate`'s
# docstring for why (`xml_builder.build_nfe` hardcodes `mod="55"`).
document_model: Literal["55"] = "55"
serie: int = Field(ge=0)
emitente: EmitenteDataPayload
itens: list[ItemDataPayload] = Field(min_length=1)
pagamento: PagamentoDataPayload
ambiente: Literal["homologacao", "producao"]
uf_destino_tipo: Literal["interna", "interestadual"]
destinatario: DestinatarioDataPayload | None = None
nat_op: str = "Venda"
tp_emis: str = Field(default="1", min_length=1, max_length=1)
ind_final: str = Field(default="1", min_length=1, max_length=1)
ind_pres: str = Field(default="1", min_length=1, max_length=1)
fin_nfe: str = Field(default="1", min_length=1, max_length=1)
# OBRIGATÓRIO -- ver o docstring do módulo (EMENDA F1). Nenhum default:
# ausente no payload -> 422 na borda do FastAPI, antes de qualquer
# lógica de negócio rodar.
ver_proc: str = Field(min_length=1, max_length=20)
class FiscalDocumentRead(BaseModel):
"""NUNCA inclui `xml_assinado` -- o XML sai só por `GET /v1/documentos/
{id}/xml` (`Response(media_type="application/xml")`), mesmo racional de
`FiscalCertificateRead` nunca incluir o binário do certificado. Sem
`sale_id`/`service_order_id` (porte table: este serviço não conhece o
domínio do produto chamador)."""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
product_id: uuid.UUID
tenant_ref: str
branch_ref: str
series_id: uuid.UUID
document_model: str
serie: int
numero: int
chave_acesso: str
codigo_numerico: str
status: str
ambiente: str
rejeicao_codigo: str | None
rejeicao_motivo: str | None
protocolo: str | None
autorizada_em: datetime | None
created_at: datetime