"""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