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:
- Visão geral do serviço.
- Modelo de tabelas auxiliares mínimas.
- DataModule Delphi com ACBrNFSe + conexão Postgres.
- Rotina
ProcessarPendenteslendonfsependente, 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:
- Conecta no Postgres.
- Busca NFS-e com
situacao = 'pendente_transmissao'(ou algo assim). - Para cada registro:
-
Busca config fiscal (certificado, CNPJ, IM, município) do prestador.
-
Monta
ACBrNFSe.NotasFiscais.Addcom 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
nfsecomsituacao,protocolo_envio,xml_envio,xml_retorno,codigo_mensagem_retorno,mensagem_retorno.
-
Pode ser:
- Aplicativo console rodando em loop com um
Sleep, gerenciado via agendador de tarefas / NSSM. - Ou serviço Windows de fato (TService).
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
ItensServicodepende de como o ACBrNFSe implementa (algumas versões usamItensServico.AddItemetc.), 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
-
Go recebe o JSON, mapeia, persiste
nfse/nfse_itenscomsituacao = 'pendente_transmissao'. -
Serviço Delphi roda em paralelo, pega essas NFS-e e conversa com o Ambiente Nacional via ACBr.
-
Delphi devolve o resultado para o Postgres (situação, protocolo, xmls).
-
O frontend / outros sistemas só consultam a API Go (
GET /nfse/{id}) para ver o status consolidado.