Multi-Tenant - 10 - Exemplo de NFSe para o Ambiente Nacional em Golang

Beleza, vamos amarrar tudo: XML de envio “estilo nacional/ABRASF” + fluxo completo API → map → persistir → ACBr → retorno → atualizar nfse.

Vou dividir em duas partes:

  1. Esboço de XML (a partir do NfseNacionalPayload que montamos).
  2. Fluxo completo em passos + diagrama.

Observação: o XML abaixo é ilustrativo, inspirado no padrão ABRASF / Nacional, não é cópia literal do schema oficial. A ideia é servir de “ponte mental” entre o DTO e o que o ACBr / Ambiente Nacional esperam.


1. Esboço do XML de envio

Supondo que você vai enviar um RPS dentro de um lote para o Ambiente Nacional via ACBr (algo como “Enviar Lote RPS”):

<EnviarLoteRpsEnvio xmlns="http://www.abrasf.org.br/nfse.xsd">
  <LoteRps Id="Lote_ABC-2025-0001" versao="2.04">
    <NumeroLote>1</NumeroLote>
    <Cnpj>12345678000199</Cnpj>
    <InscricaoMunicipal>123456</InscricaoMunicipal>
    <QuantidadeRps>1</QuantidadeRps>

    <ListaRps>
      <Rps>
        <InfRps Id="RPS_12345">
          <!-- Identificação do RPS -->
          <IdentificacaoRps>
            <Numero>12345</Numero>
            <Serie>UNICA</Serie>
            <Tipo>1</Tipo>
          </IdentificacaoRps>

          <!-- Data de emissão e natureza da operação -->
          <DataEmissao>2025-11-10T10:30:00</DataEmissao>
          <NaturezaOperacao>1</NaturezaOperacao>
          <RegimeEspecialTributacao>6</RegimeEspecialTributacao>
          <OptanteSimplesNacional>1</OptanteSimplesNacional>
          <IncentivadorCultural>2</IncentivadorCultural> <!-- 1=sim, 2=não -->

          <!-- Serviço -->
          <Servico>
            <Valores>
              <ValorServicos>1000.00</ValorServicos>
              <ValorDeducoes>0.00</ValorDeducoes>
              <ValorPis>0.00</ValorPis>
              <ValorCofins>0.00</ValorCofins>
              <ValorInss>0.00</ValorInss>
              <ValorIr>0.00</ValorIr>
              <ValorCsll>0.00</ValorCsll>
              <OutrasRetencoes>0.00</OutrasRetencoes>
              <DescontoIncondicionado>0.00</DescontoIncondicionado>
              <DescontoCondicionado>0.00</DescontoCondicionado>
              <ValorIss>20.00</ValorIss>
              <IssRetido>2</IssRetido> <!-- 1=sim,2=não -->
            </Valores>

            <ItemListaServico>14.01</ItemListaServico>
            <CodigoTributacaoMunicipio>0107</CodigoTributacaoMunicipio>
            <Discriminacao>Serviços prestados conforme contrato nº 123</Discriminacao>
            <CodigoMunicipio>3106200</CodigoMunicipio>
          </Servico>

          <!-- Prestador -->
          <PrestadorServico>
            <IdentificacaoPrestador>
              <Cnpj>12345678000199</Cnpj>
              <InscricaoMunicipal>123456</InscricaoMunicipal>
            </IdentificacaoPrestador>
          </PrestadorServico>

          <!-- Tomador -->
          <TomadorServico>
            <IdentificacaoTomador>
              <CpfCnpj>
                <Cnpj>98765432000177</Cnpj>
              </CpfCnpj>
              <InscricaoMunicipal>000000</InscricaoMunicipal>
            </IdentificacaoTomador>

            <RazaoSocial>Cliente de Exemplo LTDA</RazaoSocial>

            <Endereco>
              <Endereco>Rua X</Endereco>
              <Numero>100</Numero>
              <Complemento>Sala 3</Complemento>
              <Bairro>Centro</Bairro>
              <CodigoMunicipio>3106200</CodigoMunicipio>
              <Uf>MG</Uf>
              <Cep>30140000</Cep>
              <CodigoPais>1058</CodigoPais>
            </Endereco>

            <Contato>
              <Telefone>31999999999</Telefone>
              <Email>financeiro@cliente.com</Email>
            </Contato>
          </TomadorServico>

          <!-- Informações adicionais -->
          <OutrasInformacoes>Observação extra que vai no campo livre</OutrasInformacoes>
        </InfRps>
      </Rps>
    </ListaRps>
  </LoteRps>
</EnviarLoteRpsEnvio>

Como isso casa com o seu NfseNacionalPayload:

Na prática, no Delphi/ACBr você vai popular as propriedades do componente (ACBrNFSe.NotasFiscais.Add.NFSe.Servico.Valores.ValorServicos, etc.), e ele gera esse XML conforme o provedor.


2. Fluxo completo: API → map → persistir → ACBr → retorno → atualizar nfse

Vou considerar um cenário síncrono “clássico” (tudo na mesma requisição), e em seguida dou o pitaco de como fazer assíncrono (que é o que eu recomendo pro mundo real).

2.1. Passo a passo (visão síncrona)

  1. Cliente (frontend / Delphi / outro sistema) → sua API Go

    • Envia POST /tenants/{tenantID}/nfse com JSON no formato EmissaoNfseRequest.
  2. Handler HTTP (Go)

    • Recupera tenant_id (da rota ou do contexto).

    • json.NewDecoder(r.Body).Decode(&req)EmissaoNfseRequest.

    • Chama MapEmissaoToDomain(tenantID, req):

      • Gera Nfse (cabeçalho interno).

      • Gera []NfseItem (itens internos).

  3. Persiste no banco (Go + Postgres)

    • Middleware de Tx abre transação e faz:

      SET LOCAL app.tenant_id = :tenant_id;
      
    • service.CriarNfseCompleta(ctx, tenantID, nf, itens):

      • repo.CriarNfse(ctx, &nf, itens)

      • Insere na tabela nfse.

      • Insere na tabela nfse_itens.

      • nf.id retornado.

    • Situação inicial da NFS-e → algo como situacao = 'pendente_transmissao' ou situacao = 'em_processamento'.

  4. Mapeia para payload Nacional

    • Ainda dentro da requisição, chama:

      payload, err := MapEmissaoToNacionalPayload(req)
      
    • Você tem uma struct NfseNacionalPayload pronta pra ser:

      • Convertida em XML (via encoding/xml ou template).

      • Ou em JSON, se o Ambiente Nacional aceitar.

  5. Chamada ao ACBr (Delphi)
    Aqui entram 3 opções típicas:

    A. ACBr rodando como “serviço / daemon” Delphi, consumindo do banco

    • A API só marca a NFS-e como pendente_transmissao e grava os dados.

    • Um serviço em Delphi (com ACBrNFSe) roda em background:

      • Lê as NFS-e pendentes do banco (por tenant, cliente, etc.).

      • Monta os objetos ACBr (ACBrNFSe.NotasFiscais.Add...).

      • Chama os métodos próprios (Emitir, EnviarLoteRps, etc.).

      • Recebe o XML de retorno / protocolo / número da NFS-e.

      • Atualiza as tabelas (nfse.xml_envio, nfse.xml_retorno, nfse.protocolo, nfse.situacao = 'autorizada' ou rejeitada).

    B. ACBrMonitorPLUS via arquivos/comandos

    • A API escreve um arquivo INI/XML ou manda um comando para o ACBrMonitor (por TCP/pipe).

    • O ACBrMonitor monta o XML, envia ao provedor e retorna outro arquivo/txt/xml.

    • Sua API (ou um worker) lê esse retorno e atualiza a nfse.

    C. ACBr exposto como “serviço HTTP” próprio

    • Você monta um mini servidor HTTP em Delphi que recebe um JSON/XML equivalente ao NfseNacionalPayload.

    • A API Go faz HTTP POST pra esse serviço Delphi.

    • O serviço Delphi:

      • Converte JSON → objetos ACBr.

      • Envia pro Ambiente Nacional.

      • Devolve um JSON com status, protocolo, xml_autorizado, erros etc.

    • A API Go recebe esse retorno e atualiza a nfse.

    Como você já tem a aplicação Delphi com ACBr, o caminho mais limpo costuma ser:

    • Persistir NFS-e no banco.

    • Ter um serviço Delphi que processa essas NFS-e pendentes e atualiza o status.

  6. Retorno do ACBr → atualização da nfse

    • Quando o ACBr recebe o retorno (OK ou erro):

      • Se autorizada:

        • nfse.situacao = 'autorizada'

        • nfse.protocolo_envio = '...'

        • nfse.xml_envio = '...'

        • nfse.xml_retorno = '...'

        • nfse.codigo_mensagem_retorno = null (ou código de sucesso)

        • nfse.mensagem_retorno = null (ou mensagem de sucesso)

      • Se rejeitada:

        • nfse.situacao = 'rejeitada'

        • nfse.codigo_mensagem_retorno = 'codigo_erro'

        • nfse.mensagem_retorno = 'mensagem detalhada do provedor'

        • Pode ou não gravar o XML de retorno conforme sua política.

    • Isso pode ser feito por:

      • Serviço Delphi atualizando direto o Postgres.

      • Ou, se você quiser mais “isolamento”: serviço Delphi chamando um endpoint da sua API (PATCH /tenants/{tenantID}/nfse/{id}/status).

  7. Resposta pro cliente da API

    • Se você fizer tudo síncrono (não muito recomendado para alta carga), pode:

      • Só finalizar a request quando tiver o retorno da prefeitura.

      • Responder já com situacao = 'autorizada' e os dados completos.

    • Se você fizer assíncrono (recomendado):

      • A API responde imediatamente após o persistir com algo como:

        {
          "id": 123,
          "situacao": "pendente_transmissao"
        }
        
      • O cliente depois chama GET /tenants/{tenantID}/nfse/{id} pra ver o status atualizado (autorizada, rejeitada, etc).


2.2. Diagrama de fluxo (Mermaid)

sequenceDiagram
    participant Cli as Cliente (Front/ERP)
    participant API as API Go (NFSe)
    participant DB as PostgreSQL (multi-tenant)
    participant Svc as Serviço Delphi + ACBrNFSe
    participant Pref as Ambiente Nacional NFSe

    Cli->>API: POST /tenants/{tenantID}/nfse (EmissaoNfseRequest)
    API->>API: MapEmissaoToDomain (Nfse + Itens)
    API->>DB: BEGIN TX + SET LOCAL app.tenant_id
    API->>DB: INSERT nfse / nfse_itens (situacao = pendente_transmissao)
    DB-->>API: id da nfse
    API->>DB: COMMIT
    API-->>Cli: 201 Created (id, situacao=pendente_transmissao)

    Note over API,Svc: Fluxo assíncrono recomendado (fila de transmissão)

    Svc->>DB: SELECT nfse WHERE situacao = pendente_transmissao
    DB-->>Svc: nfse + nfse_itens
    Svc->>Svc: montar objetos ACBrNFSe
    Svc->>Pref: EnviarLoteRpsEnvio (XML)
    Pref-->>Svc: XML de retorno (protocolo, NFSe, erros)
    Svc->>DB: UPDATE nfse (situacao, protocolo, xml_envio, xml_retorno, mensagens)

    Cli->>API: GET /tenants/{tenantID}/nfse/{id}
    API->>DB: SELECT nfse + nfse_itens WHERE tenant_id = ...
    DB-->>API: nfse (situacao=autorizada/rejeitada)
    API-->>Cli: JSON com status final e dados da NFS-e

2.3. Resumo da arquitetura que você fica: