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. Ajuste na tabela nfse

ALTER TABLE public.nfse
  ADD COLUMN substatus_tecnico TEXT NULL,
  ADD COLUMN detalhe_tecnico   TEXT NULL;

Ideia

Exemplos de substatus_tecnico:


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

E mais pra frente você pode: