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

Perfeito, vamos “puxar” isso pro mundo Delphi + ACBr de um jeito que você consiga plugar rápido no que já tem.

Vou montar:

  1. Visão geral do serviço.
  2. Modelo de tabelas auxiliares mínimas.
  3. DataModule Delphi com ACBrNFSe + conexão Postgres.
  4. Rotina ProcessarPendentes lendo nfse pendente, montando ACBrNFSe, enviando, e atualizando a tabela.

Tudo em pseudo-código Delphi bem próximo do real.


1. Visão geral do serviço

Ideia: um executável/serviço Delphi que roda em loop:

  1. Conecta no Postgres.
  2. Busca NFS-e com situacao = 'pendente_transmissao' (ou algo assim).
  3. Para cada registro:
    • Busca config fiscal (certificado, CNPJ, IM, município) do prestador.

    • Monta ACBrNFSe.NotasFiscais.Add com os dados da nota.

    • Chama o método de envio (ex: ACBrNFSe.Enviar / ACBrNFSe.Emitir).

    • Lê o retorno (protocolo, número NFS-e, XML autorizado, erros).

    • Atualiza a tabela nfse com situacao, protocolo_envio, xml_envio, xml_retorno, codigo_mensagem_retorno, mensagem_retorno.

Pode ser:

Vou usar console pra simplificar.


2. Tabelas auxiliares mínimas (além de nfse / nfse_itens)

Você já tem as tabelas nfse e nfse_itens que definimos.

Eu sugiro ter algo assim:

CREATE TABLE public.nfse_config (
  id                    BIGSERIAL PRIMARY KEY,
  tenant_id             BIGINT NOT NULL,
  prestador_id          BIGINT NOT NULL, -- referencia pessoa/empresa
  cnpj                  TEXT NOT NULL,
  inscricao_municipal   TEXT NOT NULL,
  codigo_municipio      VARCHAR(7) NOT NULL,
  certificado_arquivo   TEXT NOT NULL,   -- caminho do .pfx / .p12
  certificado_senha     TEXT NOT NULL,
  provedor              TEXT NOT NULL,   -- nome do provedor no ACBr (BH, Nacional, etc.)
  ambiente              TEXT NOT NULL,   -- "PROD" ou "HOM"
  url_ws                TEXT NULL        -- se precisar sobrescrever
  -- demais parâmetros específicos
);

Na nfse, você já tem tenant_id e prestador_id, então dá pra achar a config com:

SELECT *
  FROM nfse_config
 WHERE tenant_id = :tenant_id
   AND prestador_id = :prestador_id;

3. DataModule Delphi com ACBrNFSe + Postgres

3.1. Estrutura básica

unit uDmNFSe;

interface

uses
  System.SysUtils, System.Classes,
  Data.DB,
  FireDAC.Comp.Client, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf,
  FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async,
  FireDAC.Phys, FireDAC.Phys.PG, FireDAC.Phys.PGDef,
  FireDAC.VCLUI.Wait,
  ACBrNFSe, ACBrBase;

type
  TDmNFSe = class(TDataModule)
    Conn: TFDConnection;
    QPend: TFDQuery;
    QItens: TFDQuery;
    QCfg: TFDQuery;
    QUpd: TFDQuery;
    ACBrNFSe: TACBrNFSe;
  public
    procedure ConectarBanco;
    procedure ProcessarPendentes;
  end;

implementation

{%CLASSGROUP 'Vcl.Controls.TControl'}

{$R *.dfm}

{ TDmNFSe }

procedure TDmNFSe.ConectarBanco;
begin
  Conn.Params.Clear;
  Conn.Params.DriverID := 'PG';
  Conn.Params.Values['Server']   := 'localhost';
  Conn.Params.Values['Port']     := '5432';
  Conn.Params.Values['Database'] := 'sua_base';
  Conn.Params.Values['User_Name'] := 'usuario';
  Conn.Params.Values['Password']  := 'senha';
  Conn.LoginPrompt := False;
  Conn.Connected := True;
end;

end.

4. Lendo pendentes, montando ACBrNFSe, enviando e atualizando

4.1. Query para pendentes

No DataModule, configure a QPend mais ou menos assim:

procedure TDmNFSe.ProcessarPendentes;
begin
  // Busca NFS-e pendentes, por exemplo: situacao = 'pendente_transmissao'
  QPend.Close;
  QPend.SQL.Text :=
    'SELECT id, tenant_id, prestador_id, tomador_id, '+
    '       numero_nfse, serie, ambiente, ' +
    '       data_emissao, competencia, ' +
    '       valor_servicos, valor_deducoes, valor_iss, valor_iss_retido, ' +
    '       codigo_municipio_prestacao, codigo_municipio_tomador, ' +
    '       codigo_tributacao_municipio, item_lista_servico, discriminacao ' +
    '  FROM nfse ' +
    ' WHERE situacao = :situacao ' +
    ' ORDER BY id ' +
    ' LIMIT 10'; // processa em lotes pequenos
  QPend.ParamByName('situacao').AsString := 'pendente_transmissao';
  QPend.Open;

  while not QPend.Eof do
  begin
    try
      ProcessarUmaNFSe;
      QPend.Next;
    except
      on E: Exception do
      begin
        // loga erro, marca nf como erro_interno se quiser
        // mas segue para a próxima pra não travar o loop
        QPend.Next;
      end;
    end;
  end;
end;

4.2. Montar/emissão de uma NFS-e (função separada)

procedure TDmNFSe.ProcessarUmaNFSe;
var
  NFSeID    : Int64;
  TenantID  : Int64;
  PrestadorID : Int64;
  XMLRet    : String;
begin
  NFSeID    := QPend.FieldByName('id').AsLargeInt;
  TenantID  := QPend.FieldByName('tenant_id').AsLargeInt;
  PrestadorID := QPend.FieldByName('prestador_id').AsLargeInt;

  // 1) Buscar configurações do prestador (certificado, CNPJ etc.)
  if not CarregarConfigPrestador(TenantID, PrestadorID) then
  begin
    // marca como erro de configuração
    MarcarErroConfig(NFSeID, 'Configuração de prestador não encontrada');
    Exit;
  end;

  // 2) Buscar itens da NFSe
  CarregarItensNFSe(NFSeID, TenantID);

  // 3) Montar ACBrNFSe.NotasFiscais
  MontarACBrNFSe(NFSeID);

  // 4) Enviar
  XMLRet := EnviarViaACBr;

  // 5) Atualizar tabela nfse com retorno (protocolo, situacao, xmls, mensagens)
  AtualizarRetornoNFSe(NFSeID, XMLRet);
end;

4.3. Carregar config do prestador

function TDmNFSe.CarregarConfigPrestador(ATenantID, APrestadorID: Int64): Boolean;
begin
  Result := False;

  QCfg.Close;
  QCfg.SQL.Text :=
    'SELECT cnpj, inscricao_municipal, codigo_municipio, '+
    '       certificado_arquivo, certificado_senha, provedor, ambiente, url_ws '+
    '  FROM nfse_config '+
    ' WHERE tenant_id = :tenant_id '+
    '   AND prestador_id = :prestador_id';

  QCfg.ParamByName('tenant_id').AsLargeInt := ATenantID;
  QCfg.ParamByName('prestador_id').AsLargeInt := APrestadorID;
  QCfg.Open;

  if QCfg.IsEmpty then
    Exit;

  // aqui você configura o ACBrNFSe com esses dados
  ACBrNFSe.Configuracoes.Geral.Emitente.CNPJ := QCfg.FieldByName('cnpj').AsString;
  ACBrNFSe.Configuracoes.Geral.Emitente.InscricaoMunicipal := QCfg.FieldByName('inscricao_municipal').AsString;
  ACBrNFSe.Configuracoes.Geral.Emitente.CodigoMunicipio := QCfg.FieldByName('codigo_municipio').AsString;

  ACBrNFSe.Configuracoes.Certificados.ArquivoPFX := QCfg.FieldByName('certificado_arquivo').AsString;
  ACBrNFSe.Configuracoes.Certificados.Senha     := QCfg.FieldByName('certificado_senha').AsString;

  // provedor / ambiente
  ACBrNFSe.Configuracoes.Geral.SSLLib := libOpenSSL;
  // ACBrNFSe.Configuracoes.Geral.Provedor := provXX; // depende da enum do ACBr
  // ACBrNFSe.Configuracoes.WebServices.Ambiente := taProducao / taHomologacao;

  Result := True;
end;

Os detalhes exatos das propriedades variam de versão do ACBr, mas a ideia é essa: usar os dados do banco para configurar.


4.4. Carregar itens da NFSe

procedure TDmNFSe.CarregarItensNFSe(ANFSeID, ATenantID: Int64);
begin
  QItens.Close;
  QItens.SQL.Text :=
    'SELECT numero_item, codigo_servico, descricao, '+
    '       quantidade, valor_unitario, valor_total, '+
    '       aliquota_iss, valor_iss, '+
    '       codigo_tributacao_municipio, item_lista_servico '+
    '  FROM nfse_itens '+
    ' WHERE tenant_id = :tenant_id '+
    '   AND nfse_id = :nfse_id '+
    ' ORDER BY numero_item';
  QItens.ParamByName('tenant_id').AsLargeInt := ATenantID;
  QItens.ParamByName('nfse_id').AsLargeInt := ANFSeID;
  QItens.Open;
end;

4.5. Montar ACBrNFSe.NotasFiscais

procedure TDmNFSe.MontarACBrNFSe(ANFSeID: Int64);
var
  Nota : TACBrNFSeNota;
begin
  ACBrNFSe.NotasFiscais.Clear;

  Nota := ACBrNFSe.NotasFiscais.Add;

  // Identificação / RPS
  Nota.IdentificacaoRps.Numero := QPend.FieldByName('numero_nfse').AsInteger;
  Nota.IdentificacaoRps.Serie  := QPend.FieldByName('serie').AsString;
  Nota.IdentificacaoRps.Tipo   := 1; // RPS

  Nota.DataEmissao := QPend.FieldByName('data_emissao').AsDateTime;

  // Serviço (valores totais)
  Nota.Servico.Valores.ValorServicos     := QPend.FieldByName('valor_servicos').AsFloat;
  Nota.Servico.Valores.ValorDeducoes     := QPend.FieldByName('valor_deducoes').AsFloat;
  Nota.Servico.Valores.ValorISS          := QPend.FieldByName('valor_iss').AsFloat;
  Nota.Servico.Valores.OutrasRetencoes   := 0;
  Nota.Servico.Valores.ValorISSRetido    := QPend.FieldByName('valor_iss_retido').AsFloat;
  Nota.Servico.Valores.DescontoIncondicionado := 0;
  Nota.Servico.Valores.DescontoCondicionado   := 0;

  Nota.Servico.ItemListaServico          := QPend.FieldByName('item_lista_servico').AsString;
  Nota.Servico.CodigoTributacaoMunicipio := QPend.FieldByName('codigo_tributacao_municipio').AsString;
  Nota.Servico.Discriminacao             := QPend.FieldByName('discriminacao').AsString;
  Nota.Servico.CodigoMunicipio           := QPend.FieldByName('codigo_municipio_prestacao').AsString;

  // Prestador - já configurado em CarregarConfigPrestador via ACBrNFSe.Configuracoes

  // Tomador - você pode ter que buscar em outra tabela (pessoa/empresa), aqui está simplificado
  // Nota.Tomador.IdentificacaoTomador.CpfCnpj.CNPJ := ...;
  // Nota.Tomador.RazaoSocial := ...;
  // Nota.Tomador.Endereco.*  := ...;

  // Itens
  QItens.First;
  while not QItens.Eof do
  begin
    with Nota.Servico.ItensServico.New do
    begin
      CodigoServico := QItens.FieldByName('codigo_servico').AsString;
      Discriminacao := QItens.FieldByName('descricao').AsString;
      Quantidade    := QItens.FieldByName('quantidade').AsFloat;
      ValorUnitario := QItens.FieldByName('valor_unitario').AsFloat;
      ValorTotal    := QItens.FieldByName('valor_total').AsFloat;
      Aliquota      := QItens.FieldByName('aliquota_iss').AsFloat * 100; // depende de como está a escala
    end;

    QItens.Next;
  end;
end;

Aqui o uso de ItensServico depende de como o ACBrNFSe implementa (algumas versões usam ItensServico.AddItem etc.), mas a ideia é clara.


4.6. Enviar via ACBr e capturar retorno

function TDmNFSe.EnviarViaACBr: String;
begin
  Result := '';

  // Dependendo do provedor, você pode usar:
  // ACBrNFSe.Emitir( ... );
  // ou ACBrNFSe.Enviar( ... );
  // ou ACBrNFSe.EnviarLoteRps( ... );

  ACBrNFSe.Enviar(1);  // 1 nota, por exemplo

  // Pega o XML gerado (envio)
  Result := ACBrNFSe.NotasFiscais.Items[0].XML; // XML da NFSe autorizada ou do envio, conforme provedor

  // Você também pode pegar protocolo, número da NFSe, status:
  // ACBrNFSe.NotasFiscais.Items[0].NFSe.Numero;
  // ACBrNFSe.NotasFiscais.Items[0].NFSe.CodigoVerificacao;
  // ACBrNFSe.NotasFiscais.Items[0].NFSe.DataEmissao;
end;

Os métodos exatos (Enviar, Emitir, GerarLote) variam de provedor/versão, mas o padrão é esse: você manda, e o ACBr preenche as propriedades da nota e mantém o XML.


4.7. Atualizar retorno na tabela nfse

procedure TDmNFSe.AtualizarRetornoNFSe(ANFSeID: Int64; const AXmlRetorno: String);
var
  Protocolo, MsgRet, CodMsg: String;
  Situacao: String;
begin
  // Extrai informações do retorno a partir do ACBr
  // Isso depende muito de cada provedor, mas em geral:

  Protocolo := ACBrNFSe.NotasFiscais.Items[0].NFSe.Protocolo;
  // ou às vezes está em ACBrNFSe.WebServices.Retorno.Protocolo, etc.

  // Situação: se chegou NFS-e com número, consideramos autorizada
  if ACBrNFSe.NotasFiscais.Items[0].NFSe.Numero > 0 then
    Situacao := 'autorizada'
  else
    Situacao := 'rejeitada';

  // Mensagens de erro ou alerta
  CodMsg := ''; // pode vir do WebServices
  MsgRet := ACBrNFSe.NotasFiscais.Items[0].NFSe.MensagemRetorno;

  // Atualiza no banco
  QUpd.Close;
  QUpd.SQL.Text :=
    'UPDATE nfse '+
    '   SET situacao = :situacao, '+
    '       protocolo_envio = :protocolo, '+
    '       xml_retorno = :xml_retorno, '+
    '       codigo_mensagem_retorno = :cod_msg, '+
    '       mensagem_retorno = :msg_ret, '+
    '       updated_at = NOW() '+
    ' WHERE id = :id';
  QUpd.ParamByName('situacao').AsString := Situacao;
  QUpd.ParamByName('protocolo').AsString := Protocolo;
  QUpd.ParamByName('xml_retorno').AsString := AXmlRetorno;
  QUpd.ParamByName('cod_msg').AsString := CodMsg;
  QUpd.ParamByName('msg_ret').AsString := MsgRet;
  QUpd.ParamByName('id').AsLargeInt := ANFSeID;
  QUpd.ExecSQL;
end;

5. Loop principal do serviço (console simplificado)

program SvcNFSe;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  uDmNFSe in 'uDmNFSe.pas';

var
  DmNFSe: TDmNFSe;

begin
  ReportMemoryLeaksOnShutdown := True;
  DmNFSe := TDmNFSe.Create(nil);
  try
    DmNFSe.ConectarBanco;

    while True do
    begin
      try
        DmNFSe.ProcessarPendentes;
      except
        on E: Exception do
        begin
          // logar erro geral
        end;
      end;

      Sleep(5000); // espera 5 segundos e verifica novamente
    end;

  finally
    DmNFSe.Free;
  end;
end.

Amarrando com o que você já tem no Go