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:

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:


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