Multi-Tenant - 09 - Exemplo de NFSe para o Ambiente Nacional em Golang
Show time do mapper 😄
Vou criar um parser.go dentro do contexto nfse com dois mapeamentos principais:
EmissaoNfseRequest→ domínio interno (Nfse+[]NfseItem)EmissaoNfseRequest→ payload do Ambiente Nacional (NfseNacionalPayload)
Observação importante:
ONfseNacionalPayloadabaixo é inspirado no layout nacional (ABRASF/Nacional), não é uma cópia fiel do schema oficial. A ideia é você ter um ponto único para ajustar quando estiver com o schema definitivo em mãos (XML/JSON do provedor).
internal/nfse/parser.go
package nfse
import (
"fmt"
"time"
)
const (
layoutDataRFC3339 = time.RFC3339
layoutDataSimples = "2006-01-02"
)
// ============================================================================
// 1) Mapper: EmissaoNfseRequest -> Domínio interno (Nfse + []NfseItem)
// ============================================================================
func MapEmissaoToDomain(tenantID int64, req EmissaoNfseRequest) (*Nfse, []NfseItem, error) {
// Parse datas
dataEmissao, err := time.Parse(layoutDataRFC3339, req.Transmissao.DataEmissao)
if err != nil {
return nil, nil, fmt.Errorf("data_emissao inválida (use RFC3339): %w", err)
}
competencia, err := time.Parse(layoutDataSimples, req.Identificacao.Competencia)
if err != nil {
return nil, nil, fmt.Errorf("competencia inválida (use YYYY-MM-DD): %w", err)
}
// Monta cabeçalho interno
nf := &Nfse{
TenantID: tenantID,
NumeroNfse: req.Identificacao.NumeroNfse,
Serie: req.Identificacao.Serie,
Ambiente: req.Identificacao.Ambiente,
// Situação inicial – você pode ajustar depois (ex.: "em_emissao", "autorizada", etc)
Situacao: "emitida",
PrestadorID: 0, // você provavelmente vai buscar pelo CNPJ/IM em outra tabela
TomadorID: 0, // idem para o tomador
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: &req.Tomador.Endereco.CodigoMunicipio,
// Como regra geral, usamos o primeiro item como base para esses campos
// (ajuste se preferir consolidar de outra forma)
CodigoTribMunicipio: firstItemCodigoTrib(req),
ItemListaServico: firstItemListaServ(req),
Discriminacao: req.InformacoesAdicionais.DiscriminacaoGeral,
// Campos de retorno/protocolo/xml serão preenchidos após o envio
}
if req.Transmissao.DataEmissao != "" {
// já parseado em dataEmissao, se quiser guardar num outro campo, ok
// aqui não temos um campo extra pra isso no domínio
}
// Monta itens
var itens []NfseItem
for _, it := range req.Servicos.Itens {
valorServ := it.ValorServicos
if it.Quantidade == 0 {
it.Quantidade = 1
}
item := NfseItem{
TenantID: tenantID,
// nf.ID será preenchido pelo repo ao inserir, aqui deixamos 0
NumeroItem: it.NumeroItem,
CodigoServico: it.CodigoServico,
Descricao: it.Discriminacao,
Quantidade: it.Quantidade,
ValorUnitario: valorServ / it.Quantidade,
ValorTotal: valorServ,
AliquotaIss: it.AliquotaIss,
ValorIss: valorServ * it.AliquotaIss,
}
// Campos opcionais
if it.CodigoTributacaoMunicipio != "" {
item.CodigoTribMunicipio = &it.CodigoTributacaoMunicipio
}
if it.ItemListaServico != "" {
item.ItemListaServico = &it.ItemListaServiço
}
itens = append(itens, item)
}
return nf, itens, nil
}
func firstItemCodigoTrib(req EmissaoNfseRequest) string {
if len(req.Servicos.Itens) == 0 {
return ""
}
return req.Servicos.Itens[0].CodigoTributacaoMunicipio
}
func firstItemListaServ(req EmissaoNfseRequest) string {
if len(req.Servicos.Itens) == 0 {
return ""
}
return req.Servicos.Itens[0].ItemListaServico
}
// ============================================================================
// 2) Mapper: EmissaoNfseRequest -> Payload Nacional (para webservice)
// ============================================================================
// Estrutura genérica inspirada no layout nacional.
// A ideia é: você ajusta esses nomes/campos conforme o schema oficial
// (XML/JSON) do Ambiente Nacional.
type NfseNacionalPayload struct {
IdentificacaoRps NacIdentificacaoRps `json:"identificacao_rps"`
PrestadorServico NacPrestadorServico `json:"prestador_servico"`
TomadorServico NacTomadorServico `json:"tomador_servico"`
Servico NacServico `json:"servico"`
RegimeTributario NacRegimeTributario `json:"regime_tributario"`
InformacoesAdicionais NacInformacoesAdicionais `json:"informacoes_adicionais"`
Transmissao NacTransmissao `json:"transmissao"`
}
// ------------------------
// Blocos básicos nacionais
// ------------------------
type NacIdentificacaoRps struct {
NumeroRps int64 `json:"numero_rps"`
SerieRps string `json:"serie_rps"`
TipoRps string `json:"tipo_rps"` // geralmente "1" (RPS)
Competencia string `json:"competencia"` // "YYYY-MM-DD"
}
type NacPrestadorServico struct {
Cnpj string `json:"cnpj"`
InscricaoMunicipal string `json:"inscricao_municipal"`
CodigoMunicipio string `json:"codigo_municipio"`
}
type NacTomadorServico struct {
CPFCNPJ string `json:"cpf_cnpj"`
RazaoSocial string `json:"razao_social"`
Endereco NacEndereco `json:"endereco"`
Contato *NacContato `json:"contato,omitempty"`
}
type NacEndereco struct {
Logradouro string `json:"logradouro"`
Numero string `json:"numero"`
Complemento *string `json:"complemento,omitempty"`
Bairro string `json:"bairro"`
CodigoMunicipio string `json:"codigo_municipio"`
UF string `json:"uf"`
CEP string `json:"cep"`
Pais string `json:"pais"`
}
type NacContato struct {
Telefone *string `json:"telefone,omitempty"`
Email *string `json:"email,omitempty"`
}
type NacRegimeTributario struct {
NaturezaOperacao string `json:"natureza_operacao"`
RegimeEspecialTributacao string `json:"regime_especial_tributacao"`
OptanteSimplesNacional bool `json:"optante_simples_nacional"`
IncentivadorCultural bool `json:"incentivador_cultural"`
}
type NacServico struct {
// Campos consolidados
ValorServicos float64 `json:"valor_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"`
// Campos específicos de serviço
ItemListaServico string `json:"item_lista_servico"`
CodigoTributacaoMunicipio string `json:"codigo_tributacao_municipio"`
Descricao string `json:"discriminacao"`
// Se o layout nacional permitir itens detalhados, você pode expor também:
Itens []NacServicoItem `json:"itens,omitempty"`
}
type NacServicoItem struct {
NumeroItem int `json:"numero_item"`
CodigoTributacaoMunicipio string `json:"codigo_tributacao_municipio"`
ItemListaServico string `json:"item_lista_servico"`
Descricao string `json:"discriminacao"`
CodigoCNAE *string `json:"codigo_cnae,omitempty"`
CodigoServico string `json:"codigo_servico"`
Quantidade float64 `json:"quantidade"`
ValorServicos float64 `json:"valor_servicos"`
AliquotaIss float64 `json:"aliquota_iss"`
IssRetido bool `json:"iss_retido"`
}
type NacInformacoesAdicionais struct {
DiscriminacaoGeral string `json:"discriminacao_geral"`
Observacoes *string `json:"observacoes,omitempty"`
}
type NacTransmissao struct {
LoteIDExterno string `json:"lote_id_externo"`
DataEmissao string `json:"data_emissao"` // RFC3339
Ambiente string `json:"ambiente"` // "PROD" ou "HOM"
TipoEmissao string `json:"tipo_emissao"` // "NORMAL", etc.
}
// ============================================================================
// Mapper: EmissaoNfseRequest -> NfseNacionalPayload
// ============================================================================
func MapEmissaoToNacionalPayload(req EmissaoNfseRequest) (*NfseNacionalPayload, error) {
payload := &NfseNacionalPayload{
IdentificacaoRps: NacIdentificacaoRps{
NumeroRps: req.Identificacao.NumeroNfse,
SerieRps: req.Identificacao.Serie,
TipoRps: "1", // RPS – ajuste se o nacional exigir outro código
Competencia: req.Identificacao.Competencia,
},
PrestadorServico: NacPrestadorServico{
Cnpj: req.Prestador.CNPJ,
InscricaoMunicipal: req.Prestador.InscricaoMunicipal,
CodigoMunicipio: req.Prestador.CodigoMunicipio,
},
TomadorServico: NacTomadorServico{
CPFCNPJ: req.Tomador.CPFCNPJ,
RazaoSocial: req.Tomador.RazaoSocial,
Endereco: NacEndereco{
Logradouro: req.Tomador.Endereco.Logradouro,
Numero: req.Tomador.Endereco.Numero,
Complemento: req.Tomador.Endereco.Complemento,
Bairro: req.Tomador.Endereco.Bairro,
CodigoMunicipio: req.Tomador.Endereco.CodigoMunicipio,
UF: req.Tomador.Endereco.UF,
CEP: req.Tomador.Endereco.CEP,
Pais: req.Tomador.Endereco.Pais,
},
Contato: nil,
},
RegimeTributario: NacRegimeTributario{
NaturezaOperacao: req.Identificacao.NaturezaOperacao,
RegimeEspecialTributacao: req.Identificacao.RegimeEspecialTributacao,
OptanteSimplesNacional: req.Identificacao.OptanteSimplesNacional,
IncentivadorCultural: req.Identificacao.IncentivadorCultural,
},
Servico: NacServico{
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,
OutrasRetencoes: req.Servicos.OutrasRetencoes,
DescontoIncondicionado: req.Servicos.DescontoIncondicionado,
DescontoCondicionado: req.Servicos.DescontoCondicionado,
ItemListaServico: firstItemListaServ(req),
CodigoTributacaoMunicipio: firstItemCodigoTrib(req),
Descricao: req.InformacoesAdicionais.DiscriminacaoGeral,
},
InformacoesAdicionais: NacInformacoesAdicionais{
DiscriminacaoGeral: req.InformacoesAdicionais.DiscriminacaoGeral,
Observacoes: req.InformacoesAdicionais.Observacoes,
},
Transmissao: NacTransmissao{
LoteIDExterno: req.Transmissao.LoteIDExterno,
DataEmissao: req.Transmissao.DataEmissao,
Ambiente: req.Identificacao.Ambiente,
TipoEmissao: req.Transmissao.TipoEmissao,
},
}
// Contato opcional
if req.Tomador.Contato != nil {
payload.TomadorServico.Contato = &NacContato{
Telefone: req.Tomador.Contato.Telefone,
Email: req.Tomador.Contato.Email,
}
}
// Itens (caso queira mandar detalhado pro nacional)
for _, it := range req.Servicos.Itens {
item := NacServicoItem{
NumeroItem: it.NumeroItem,
CodigoTributacaoMunicipio: it.CodigoTributacaoMunicipio,
ItemListaServico: it.ItemListaServico,
Descricao: it.Discriminacao,
CodigoCNAE: it.CodigoCNAE,
CodigoServico: it.CodigoServico,
Quantidade: it.Quantidade,
ValorServicos: it.ValorServicos,
AliquotaIss: it.AliquotaIss,
IssRetido: it.IssRetido,
}
payload.Servico.Itens = append(payload.Servico.Itens, item)
}
return payload, nil
}
Como usar isso na prática
No seu handler de emissão, você fica com algo assim:
// 1) Recebe EmissaoNfseRequest (sua API)
var req EmissaoNfseRequest
_ = json.NewDecoder(r.Body).Decode(&req)
// 2) Converte para domínio interno
nf, itens, err := nfse.MapEmissaoToDomain(tenantID, req)
// 3) Persiste nf + itens (service/repo)
nfCompleta, err := s.CriarNfseCompleta(ctx, tenantID, *nf, itens)
// 4) Em paralelo ou depois, gera payload nacional
payload, err := nfse.MapEmissaoToNacionalPayload(req)
// -> aqui você converte payload pra XML/JSON conforme o webservice exigir
// e envia usando ACBr / HTTP / etc.