Multi-Tenant - 08 - Exemplo de NFSe para o Ambiente Nacional em Golang
Boa, então vamos alinhar o DTO da sua API com o layout Nacional da NFS-e de forma organizada.
Como eu não tenho aqui, dentro da conversa, o JSON oficial detalhado do Ambiente Nacional (e ele é relativamente extenso e cheio de campos opcionais), eu vou seguir uma abordagem prática:
- Montar um modelo de DTO “inspirado” no padrão nacional, mas:
- Organizado em blocos:
identificacao,prestador,tomador,servicos,valores, etc. - Com nomes em snake_case próximos aos usados em layouts oficiais.
- Deixar claro onde você pluga os campos do layout real (mapeamento 1–1 é você que vai fechar depois, com base na documentação mais atual do portal NFS-e Nacional / ABRASF / LC 214).
Vou me concentrar em request de emissão (ou seja, algo próximo do que você mandaria ao Ambiente Nacional).
1. Estrutura geral sugerida do JSON da sua API
Request da sua API (payload para POST /tenants/{tenantID}/nfse):
{
"identificacao": {
"numero_nfse": 12345,
"serie": "UNICA",
"ambiente": "PROD", // ou "HOM"
"competencia": "2025-11-01", // YYYY-MM-DD
"natureza_operacao": "1", // conforme tabela
"regime_especial_tributacao": "6",
"optante_simples_nacional": true,
"incentivador_cultural": false
},
"prestador": {
"cnpj": "12345678000199",
"inscricao_municipal": "123456",
"codigo_municipio": "3106200" // IBGE
},
"tomador": {
"cpf_cnpj": "98765432000177",
"inscricao_municipal": null,
"razao_social": "Cliente de Exemplo LTDA",
"endereco": {
"logradouro": "Rua X",
"numero": "100",
"complemento": "Sala 3",
"bairro": "Centro",
"codigo_municipio": "3106200",
"uf": "MG",
"cep": "30140000",
"pais": "1058" // código IBGE do país
},
"contato": {
"telefone": "31999999999",
"email": "financeiro@cliente.com"
}
},
"servicos": {
"itens": [
{
"numero_item": 1,
"codigo_tributacao_municipio": "0107",
"item_lista_servico": "14.01",
"discriminacao": "Serviço de consultoria em TI",
"codigo_cnae": "6204000",
"codigo_servico": "CONSULT",
"aliquota_iss": 0.02,
"iss_retido": false,
"valor_servicos": 1000.0,
"valor_desconto_incondicionado": 0.0,
"valor_desconto_condicionado": 0.0
}
],
"valor_total_servicos": 1000.0,
"valor_deducoes": 0.0,
"valor_pis": 0.0,
"valor_cofins": 0.0,
"valor_inss": 0.0,
"valor_ir": 0.0,
"valor_csll": 0.0,
"valor_iss": 20.0,
"valor_iss_retido": 0.0,
"outras_retencoes": 0.0,
"desconto_incondicionado": 0.0,
"desconto_condicionado": 0.0
},
"informacoes_adicionais": {
"discriminacao_geral": "Serviços prestados conforme contrato nº 123",
"observacoes": "Observação extra que vai no campo livre"
},
"transmissao": {
"lote_id_externo": "ABC-2025-0001",
"data_emissao": "2025-11-10T10:30:00Z",
"tipo_emissao": "NORMAL"
}
}
Esse modelo:
- já separa bem os blocos (facilita mapear pro XML/JSON do Nacional);
- mantém os campos essenciais que você já tinha:
numero_nfse,serie,competencia, prestador/tomador, itens, valores, etc.
2. DTO em Go (request) alinhado com esse JSON
Arquivo: internal/nfse/dto_request.go
package nfse
// DTO da sua API para RECEBER um pedido de emissão de NFS-e
// inspirado no layout nacional, organizado por blocos.
type EmissaoNfseRequest struct {
Identificacao IdentificacaoNfseRequest `json:"identificacao"`
Prestador PrestadorRequest `json:"prestador"`
Tomador TomadorRequest `json:"tomador"`
Servicos ServicosRequest `json:"servicos"`
InformacoesAdicionais InformacoesAdicionaisRequest `json:"informacoes_adicionais"`
Transmissao TransmissaoRequest `json:"transmissao"`
}
// -----------------------------
// Bloco: Identificação
// -----------------------------
type IdentificacaoNfseRequest struct {
NumeroNfse int64 `json:"numero_nfse"`
Serie string `json:"serie"`
Ambiente string `json:"ambiente"` // "PROD" ou "HOM"
Competencia string `json:"competencia"` // "YYYY-MM-DD"
NaturezaOperacao string `json:"natureza_operacao"` // tabela nacional
RegimeEspecialTributacao string `json:"regime_especial_tributacao"` // tabela nacional
OptanteSimplesNacional bool `json:"optante_simples_nacional"`
IncentivadorCultural bool `json:"incentivador_cultural"`
}
// -----------------------------
// Bloco: Prestador
// -----------------------------
type PrestadorRequest struct {
CNPJ string `json:"cnpj"`
InscricaoMunicipal string `json:"inscricao_municipal"`
CodigoMunicipio string `json:"codigo_municipio"` // IBGE
}
// -----------------------------
// Bloco: Tomador
// -----------------------------
type TomadorRequest struct {
CPFCNPJ string `json:"cpf_cnpj"`
InscricaoMunicipal *string `json:"inscricao_municipal,omitempty"`
RazaoSocial string `json:"razao_social"`
Endereco EnderecoRequest `json:"endereco"`
Contato *ContatoRequest `json:"contato,omitempty"`
}
type EnderecoRequest struct {
Logradouro string `json:"logradouro"`
Numero string `json:"numero"`
Complemento *string `json:"complemento,omitempty"`
Bairro string `json:"bairro"`
CodigoMunicipio string `json:"codigo_municipio"` // IBGE
UF string `json:"uf"`
CEP string `json:"cep"`
Pais string `json:"pais"` // código IBGE do país
}
type ContatoRequest struct {
Telefone *string `json:"telefone,omitempty"`
Email *string `json:"email,omitempty"`
}
// -----------------------------
// Bloco: Serviços + Itens
// -----------------------------
type ServicosRequest struct {
Itens []ServicoItemRequest `json:"itens"`
ValorTotalServicos float64 `json:"valor_total_servicos"`
ValorDeducoes float64 `json:"valor_deducoes"`
ValorPis float64 `json:"valor_pis"`
ValorCofins float64 `json:"valor_cofins"`
ValorInss float64 `json:"valor_inss"`
ValorIr float64 `json:"valor_ir"`
ValorCsll float64 `json:"valor_csll"`
ValorIss float64 `json:"valor_iss"`
ValorIssRetido float64 `json:"valor_iss_retido"`
OutrasRetencoes float64 `json:"outras_retencoes"`
DescontoIncondicionado float64 `json:"desconto_incondicionado"`
DescontoCondicionado float64 `json:"desconto_condicionado"`
}
type ServicoItemRequest struct {
NumeroItem int `json:"numero_item"`
CodigoTributacaoMunicipio string `json:"codigo_tributacao_municipio"`
ItemListaServico string `json:"item_lista_servico"`
Discriminacao string `json:"discriminacao"`
CodigoCNAE *string `json:"codigo_cnae,omitempty"`
CodigoServico string `json:"codigo_servico"`
Quantidade float64 `json:"quantidade"`
ValorServicos float64 `json:"valor_servicos"`
ValorDescontoIncondicionado float64 `json:"valor_desconto_incondicionado"`
ValorDescontoCondicionado float64 `json:"valor_desconto_condicionado"`
AliquotaIss float64 `json:"aliquota_iss"`
IssRetido bool `json:"iss_retido"`
}
// -----------------------------
// Bloco: Informações adicionais
// -----------------------------
type InformacoesAdicionaisRequest struct {
DiscriminacaoGeral string `json:"discriminacao_geral"`
Observacoes *string `json:"observacoes,omitempty"`
}
// -----------------------------
// Bloco: Transmissão
// -----------------------------
type TransmissaoRequest struct {
LoteIDExterno string `json:"lote_id_externo"`
DataEmissao string `json:"data_emissao"` // RFC3339
TipoEmissao string `json:"tipo_emissao"` // "NORMAL", "CONJUGADA", etc, conforme padrão
}
3. Como mapear esse DTO pro seu Nfse e NfseItem
No seu handler de criação, em vez de usar diretamente o criarNfseRequest antigo, você passa a trabalhar com o EmissaoNfseRequest e faz o parse + mapeamento pro seu domínio interno:
func (h *Handler) CriarNfse(w http.ResponseWriter, r *http.Request) {
tenantID, err := h.getTenantID(r)
if err != nil || tenantID <= 0 {
http.Error(w, "tenant_id inválido", http.StatusBadRequest)
return
}
var req EmissaoNfseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON inválido", http.StatusBadRequest)
return
}
// Parse de datas
dataEmissao, err := time.Parse(time.RFC3339, req.Transmissao.DataEmissao)
if err != nil {
http.Error(w, "data_emissao inválida (use RFC3339)", http.StatusBadRequest)
return
}
competencia, err := time.Parse("2006-01-02", req.Identificacao.Competencia)
if err != nil {
http.Error(w, "competencia inválida (YYYY-MM-DD)", http.StatusBadRequest)
return
}
// Monta o Nfse (domínio interno)
nf := Nfse{
TenantID: tenantID,
NumeroNfse: req.Identificacao.NumeroNfse,
Serie: req.Identificacao.Serie,
Ambiente: req.Identificacao.Ambiente,
Situacao: "emitida", // ou derivado da lógica/transmissão
PrestadorID: 0, // (você faz o lookup do prestador pelo CNPJ/IM se quiser)
TomadorID: 0, // idem
DataEmissao: dataEmissao,
Competencia: competencia,
ValorServicos: req.Servicos.ValorTotalServicos,
ValorDeducoes: req.Servicos.ValorDeducoes,
ValorPis: req.Servicos.ValorPis,
ValorCofins: req.Servicos.ValorCofins,
ValorInss: req.Servicos.ValorInss,
ValorIr: req.Servicos.ValorIr,
ValorCsll: req.Servicos.ValorCsll,
ValorIss: req.Servicos.ValorIss,
ValorIssRetido: req.Servicos.ValorIssRetido,
IssRetido: req.Servicos.ValorIssRetido > 0,
CodigoMunicipioPrest: req.Prestador.CodigoMunicipio,
// CodigoMunicipioTomador pode vir de req.Tomador.Endereco
CodigoTribMunicipio: req.Servicos.Itens[0].CodigoTributacaoMunicipio, // ou outra lógica
ItemListaServico: req.Servicos.Itens[0].ItemListaServico,
Discriminacao: req.InformacoesAdicionais.DiscriminacaoGeral,
// campos de retorno (protocolo, xml, etc) serão preenchidos após envio
}
var itens []NfseItem
for _, it := range req.Servicos.Itens {
item := NfseItem{
TenantID: tenantID,
NumeroItem: it.NumeroItem,
CodigoServico: it.CodigoServico,
Descricao: it.Discriminacao,
Quantidade: it.Quantidade,
ValorUnitario: it.ValorServicos / it.Quantidade,
ValorTotal: it.ValorServicos,
AliquotaIss: it.AliquotaIss,
ValorIss: it.ValorServicos * it.AliquotaIss,
CodigoTribMunicipio: &it.CodigoTributacaoMunicipio,
ItemListaServico: &it.ItemListaServico,
}
itens = append(itens, item)
}
nfCompleta, err := h.svc.CriarNfseCompleta(r.Context(), tenantID, nf, itens)
if err != nil {
http.Error(w, "erro ao criar NFS-e: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, nfCompleta)
}
4. Como isso te ajuda na prática
-
O DTO da sua API fica bem próximo do modelo semântico do Padrão Nacional:
- Basta mapear
EmissaoNfseRequest→ struct de envio do Ambiente Nacional (XML/JSON).
- Basta mapear
-
Você mantém o domínio interno estável (
Nfse,NfseItem, tabelas SQL etc.), mesmo que o layout nacional mude algum detalhe. -
Se amanhã entrar uma mudança no layout (ex: novo campo em
servicos, nova flag de regime), você altera:-
O DTO do request (
EmissaoNfseRequest) -
O mapper DTO → domínio e DTO → XML Provedor
-
Sem quebrar o resto do ERP.
-