Multi-Tenant - 14 - Exemplo de NFSe para o Ambiente Nacional em Golang
Boa, vamos colocar uma “segunda camada” de diagnóstico sem poluir o status funcional que o usuário vê.
Vou te sugerir:
- 1 campo na tabela:
substatus_tecnico(string curta, tipo enum textual). - 1 campo auxiliar:
detalhe_tecnico(texto livre, pra mensagem/stack resumida). - Enum em Go + helpers (
IsRetryable,ShouldAlert). - Constantes em Delphi pros mesmos códigos.
1. Ajuste na tabela nfse
ALTER TABLE public.nfse
ADD COLUMN substatus_tecnico TEXT NULL,
ADD COLUMN detalhe_tecnico TEXT NULL;
Ideia
-
situacao= visão funcional (rascunho, pendente_transmissao, autorizada, rejeitada, cancelada, erro_interno). -
substatus_tecnico= causa técnica do último erro/tentativa (não precisa preencher quando está tudo OK). -
detalhe_tecnico= mensagem curta, exceção, timeout, stack resumida, etc.
Exemplos de substatus_tecnico:
-
timeout_ws– time-out falando com o webservice. -
ssl_error– erro de certificado/SSL/TLS. -
dns_error– não resolveu host. -
http_5xx– erro 500/502/503/504 no provedor. -
schema_invalido– XML/JSON fora do padrão. -
acbr_exception– exception não tratada no ACBr/Delphi. -
db_error– erro ao atualizar/ler banco (pode vir do serviço). -
config_invalida– certificado/cnpj/IM/config do prestador inconsistente.
2. Enum de substatus técnico em Go
Arquivo: internal/nfse/substatus_tecnico.go
package nfse
type NfseTechSubstatus string
const (
NfseTechNone NfseTechSubstatus = "" // sem problema técnico relevante
NfseTechTimeoutWS NfseTechSubstatus = "timeout_ws"
NfseTechSSLError NfseTechSubstatus = "ssl_error"
NfseTechDNSError NfseTechSubstatus = "dns_error"
NfseTechHTTP5xx NfseTechSubstatus = "http_5xx"
NfseTechSchemaInvalido NfseTechSubstatus = "schema_invalido"
NfseTechACBrException NfseTechSubstatus = "acbr_exception"
NfseTechDBError NfseTechSubstatus = "db_error"
NfseTechConfigInvalida NfseTechSubstatus = "config_invalida"
)
// Se vale a pena tentar reenviar automaticamente
func (t NfseTechSubstatus) IsRetryable() bool {
switch t {
case NfseTechTimeoutWS,
NfseTechSSLError,
NfseTechDNSError,
NfseTechHTTP5xx,
NfseTechDBError:
return true
default:
return false
}
}
// Se deve disparar alerta/monitoramento (Slack, e-mail, etc.)
func (t NfseTechSubstatus) ShouldAlert() bool {
switch t {
case NfseTechACBrException,
NfseTechConfigInvalida,
NfseTechSchemaInvalido:
return true
default:
return false
}
}
func ParseTechSubstatus(raw string) NfseTechSubstatus {
switch raw {
case string(NfseTechTimeoutWS):
return NfseTechTimeoutWS
case string(NfseTechSSLError):
return NfseTechSSLError
case string(NfseTechDNSError):
return NfseTechDNSError
case string(NfseTechHTTP5xx):
return NfseTechHTTP5xx
case string(NfseTechSchemaInvalido):
return NfseTechSchemaInvalido
case string(NfseTechACBrException):
return NfseTechACBrException
case string(NfseTechDBError):
return NfseTechDBError
case string(NfseTechConfigInvalida):
return NfseTechConfigInvalida
default:
return NfseTechNone
}
}
Uso típico no Go quando você for marcar erro:
// exemplo: timeout no worker/integração
nf.Situacao = string(NfseStatusErroInterno)
nf.SubstatusTecnico = string(NfseTechTimeoutWS)
nf.DetalheTecnico = err.Error()
3. Constantes equivalentes em Delphi
Arquivo: uNFSeSubstatusTecnico.pas
unit uNFSeSubstatusTecnico;
interface
type
TNFSeTechSubstatus = string;
const
NFSE_TECH_NONE : TNFSeTechSubstatus = '';
NFSE_TECH_TIMEOUT_WS : TNFSeTechSubstatus = 'timeout_ws';
NFSE_TECH_SSL_ERROR : TNFSeTechSubstatus = 'ssl_error';
NFSE_TECH_DNS_ERROR : TNFSeTechSubstatus = 'dns_error';
NFSE_TECH_HTTP_5XX : TNFSeTechSubstatus = 'http_5xx';
NFSE_TECH_SCHEMA_INVALIDO : TNFSeTechSubstatus = 'schema_invalido';
NFSE_TECH_ACBR_EXCEPTION : TNFSeTechSubstatus = 'acbr_exception';
NFSE_TECH_DB_ERROR : TNFSeTechSubstatus = 'db_error';
NFSE_TECH_CONFIG_INVALIDA : TNFSeTechSubstatus = 'config_invalida';
function NFSeTechIsRetryable(const ASub: TNFSeTechSubstatus): Boolean;
function NFSeTechShouldAlert(const ASub: TNFSeTechSubstatus): Boolean;
implementation
function NFSeTechIsRetryable(const ASub: TNFSeTechSubstatus): Boolean;
begin
Result :=
(ASub = NFSE_TECH_TIMEOUT_WS) or
(ASub = NFSE_TECH_SSL_ERROR) or
(ASub = NFSE_TECH_DNS_ERROR) or
(ASub = NFSE_TECH_HTTP_5XX) or
(ASub = NFSE_TECH_DB_ERROR);
end;
function NFSeTechShouldAlert(const ASub: TNFSeTechSubstatus): Boolean;
begin
Result :=
(ASub = NFSE_TECH_ACBR_EXCEPTION) or
(ASub = NFSE_TECH_CONFIG_INVALIDA) or
(ASub = NFSE_TECH_SCHEMA_INVALIDO);
end;
end.
Uso no serviço Delphi
Exemplo quando capturar exceção no envio:
uses uNFSeStatus, uNFSeSubstatusTecnico;
procedure TDmNFSe.ProcessarUmaNFSe;
begin
try
// ... montar ACBr, enviar, etc.
except
on E: EACBrNFSeErroSchema do
begin
AtualizarErroTecnico(
NFSeID,
NFSE_STATUS_ERRO_INTERNO,
NFSE_TECH_SCHEMA_INVALIDO,
E.Message
);
end;
on E: EACBrNFSeErroCertificado do
begin
AtualizarErroTecnico(
NFSeID,
NFSE_STATUS_ERRO_INTERNO,
NFSE_TECH_SSL_ERROR,
E.Message
);
end;
on E: Exception do
begin
AtualizarErroTecnico(
NFSeID,
NFSE_STATUS_ERRO_INTERNO,
NFSE_TECH_ACBR_EXCEPTION,
E.Message
);
end;
end;
end;
E o AtualizarErroTecnico:
procedure TDmNFSe.AtualizarErroTecnico(
ANFSeID: Int64;
const AStatus: TNFSeStatus;
const ASub: TNFSeTechSubstatus;
const ADetalhe: string);
begin
QUpd.Close;
QUpd.SQL.Text :=
'UPDATE nfse '+
' SET situacao = :situacao, '+
' substatus_tecnico = :substatus, '+
' detalhe_tecnico = :detalhe, '+
' updated_at = NOW() '+
' WHERE id = :id';
QUpd.ParamByName('situacao').AsString := AStatus;
QUpd.ParamByName('substatus').AsString := ASub;
QUpd.ParamByName('detalhe').AsString := Copy(ADetalhe, 1, 4000); // limita se quiser
QUpd.ParamByName('id').AsLargeInt := ANFSeID;
QUpd.ExecSQL;
end;
4. Benefício prático na operação
-
O usuário final continua vendo só:
- “Autorizada”, “Rejeitada”, “Erro interno”.
-
Você, no suporte/observabilidade, vê exatamente por que está em erro:
-
erro_interno + timeout_ws→ problema de rede/provedor. -
erro_interno + ssl_error→ certificado/SSL. -
erro_interno + config_invalida→ configuração do prestador errada. -
rejeitada + schema_invalido→ algo no layout do XML/JSON.
-
E mais pra frente você pode:
-
Criar Jobs que reprocessam automaticamente apenas os que têm
IsRetryable() == true. -
Enviar alerta (Slack, e-mail) só quando
ShouldAlert() == true.