PC SOFT

PROFESSIONAL NEWSGROUPS
WINDEVWEBDEV and WINDEV Mobile

Home → WINDEV 25 → Exemplo de como Gerar milhões de Boletos de cobrança e enviar por e-mail usando thread paralela
Exemplo de como Gerar milhões de Boletos de cobrança e enviar por e-mail usando thread paralela
Started by Boller, May, 21 2025 4:32 PM - No answer
Registered member
4,613 messages
Posted on May, 21 2025 - 4:32 PM
Bom dia

Vamos exemplificar como usar Thread Paralela num exemplo prático de geração de PDF e e-mails

Exemplo Diagrama das tabelas usadas





Exemplo script SQL

-- Tabela: tbl_cliente
CREATE TABLE tbl_cliente (
cpf VARCHAR(14) PRIMARY KEY,
email VARCHAR(255),
nome VARCHAR(255)
);

-- Tabela: tbl_saldo
CREATE TABLE tbl_saldo (
cpf_cliente VARCHAR(14),
saldo NUMERIC(12, 2),
pdf_gerado BOOLEAN DEFAULT FALSE,
nome_pdf VARCHAR(255),
email_enviado BOOLEAN DEFAULT FALSE,
data_geracao DATE,
hora_geracao TIME,
data_envio DATE,
hora_envio TIME,
FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf)
);

-- Tabela: tbl_config_smtp
CREATE TABLE tbl_config_smtp (
servidor VARCHAR(255),
porta INTEGER,
usuario VARCHAR(255),
senha VARCHAR(255),
email_remetente VARCHAR(255),
usar_ssl BOOLEAN
);

-- Tabela: tbl_log_erros
CREATE TABLE tbl_log_erros (
id SERIAL PRIMARY KEY,
data DATE,
hora TIME,
tipo_erro VARCHAR(50),
severidade VARCHAR(20),
mensagem TEXT,
cpf_cliente VARCHAR(14),
id_processamento INTEGER,
usuario VARCHAR(50),
maquina VARCHAR(50),
detalhes_tecnicos TEXT,
FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf),
FOREIGN KEY (id_processamento) REFERENCES tbl_controle_processamento(id)
);

-- Tabela: tbl_controle_processamento
CREATE TABLE tbl_controle_processamento (
id SERIAL PRIMARY KEY,
data_exec DATE,
hora_exec TIME,
total_clientes INTEGER,
pdfs_gerados INTEGER,
emails_enviados INTEGER,
falhas_pdf INTEGER,
falhas_email INTEGER,
status VARCHAR(20),
motivo_erro TEXT,
uso_cpu_medio NUMERIC(5,2),
uso_memoria_medio NUMERIC(5,2),
espaco_disco_inicial INTEGER,
espaco_disco_final INTEGER,
tempo_execucao INTEGER,
usuario VARCHAR(50),
maquina VARCHAR(50)
);

-- Tabela: tbl_rastreabilidade_boleto
CREATE TABLE tbl_rastreabilidade_boleto (
id SERIAL PRIMARY KEY,
cpf_cliente VARCHAR(14),
evento VARCHAR(100),
data DATE,
hora TIME,
id_processamento INTEGER,
usuario VARCHAR(50),
maquina VARCHAR(50),
FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf),
FOREIGN KEY (id_processamento) REFERENCES tbl_controle_processamento(id)
);

-- Tabela: tbl_reprocessamento
CREATE TABLE tbl_reprocessamento (
id SERIAL PRIMARY KEY,
cpf_cliente VARCHAR(14),
tipo_falha VARCHAR(10),
data_falha DATE,
hora_falha TIME,
id_processamento_original INTEGER,
motivo_falha TEXT,
status VARCHAR(20),
data_reprocessamento DATE,
hora_reprocessamento TIME,
id_processamento_reprocessamento INTEGER,
tentativas INTEGER,
FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf),
FOREIGN KEY (id_processamento_original) REFERENCES tbl_controle_processamento(id),
FOREIGN KEY (id_processamento_reprocessamento) REFERENCES tbl_controle_processamento(id)
);

-- Tabela: tbl_configuracoes
CREATE TABLE tbl_configuracoes (
chave VARCHAR(100) PRIMARY KEY,
valor VARCHAR(255),
descricao TEXT
);



//##############################
// *CÓDIGO COMPLETO COM MELHORIAS*
// *Sistema de Geração de Boletos e Envio de E-mails com Thread Paralela*
//##############################

// Variáveis globais
gnIdProcessamentoAtual is int = 0 // ID do processamento atual

//##############################
// *BOLETO_GeraPDFs_Threaded*
//##############################

PROCEDURE BOLETO_GeraPDFs_Threaded()

// Contadores para monitoramento
nPDFsGerados is int = 0
nFalhasPDF is int = 0
nThreadsAtivas is int = 0
nMaxThreadsConfig is int = GetConfigValue("MAX_THREADS", 10) // Configura através de parâmetro
nMaxThreads is int = nMaxThreadsConfig // Será ajustado dinamicamente
nTimeoutThread is int = GetConfigValue("TIMEOUT_THREAD", 120) // Timeout em segundos
nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500) // Espaço mínimo em MB
nVerificacaoRecursos is int = GetConfigValue("INTERVALO_VERIFICACAO_RECURSOS", 10) // A cada quantos clientes verificar recursos

// Cria diretório por data com horário para evitar conflitos em múltiplas execuções
sDataHoje is string = DateToString(Today(), "YYYYMMDD")
sHoraExec is string = TimeToString(Now(), "HHmmss")
sPastaBoletos is string = "C:\Boletos\boleto_" + sDataHoje + "_" + sHoraExec

// Verifica e cria diretório
IF NOT fDirectoryExist(sPastaBoletos) THEN
IF NOT fMakeDir(sPastaBoletos) THEN
BOLETO_LogRegistro("SISTEMA", "CRITICO", "Falha ao criar diretório: " + sPastaBoletos)
RETURN
END
END

// Verifica espaço em disco inicial
IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN
// Espaço insuficiente - notifica e cancela
Error("Espaço em disco insuficiente para iniciar o processo. Verifique os logs.")
RETURN
END

// Inicializa tabela de controle de processamento
HExecuteSQL(hQueryDefault, "CREATE TABLE IF NOT EXISTS tbl_controle_processamento (" +
"id INT AUTO_INCREMENT PRIMARY KEY, " +
"data_exec DATE, " +
"hora_exec TIME, " +
"total_clientes INT, " +
"pdfs_gerados INT, " +
"emails_enviados INT, " +
"falhas_pdf INT, " +
"falhas_email INT, " +
"status VARCHAR(20), " +
"motivo_erro VARCHAR(255), " +
"uso_cpu_medio NUMERIC(5,2), " +
"uso_memoria_medio NUMERIC(5,2), " +
"espaco_disco_inicial INT, " +
"espaco_disco_final INT, " +
"tempo_execucao INT, " +
"usuario VARCHAR(50), " +
"maquina VARCHAR(50))")

// Inicializa tabela de reprocessamento se não existir
HExecuteSQL(hQueryDefault, "CREATE TABLE IF NOT EXISTS tbl_reprocessamento (" +
"id INT AUTO_INCREMENT PRIMARY KEY, " +
"cpf_cliente VARCHAR(14), " +
"tipo_falha VARCHAR(10), " +
"data_falha DATE, " +
"hora_falha TIME, " +
"id_processamento_original INT, " +
"motivo_falha VARCHAR(255), " +
"status VARCHAR(20), " +
"data_reprocessamento DATE, " +
"hora_reprocessamento TIME, " +
"id_processamento_reprocessamento INT, " +
"tentativas INT)")

// Inicia registro de processamento
nIdProcessamento is int = BOLETO_IniciaProcessamento(sDataHoje, sHoraExec)
IF nIdProcessamento = 0 THEN
BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", "Falha ao iniciar registro de processamento")
RETURN
END

// Atualiza status
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "GERANDO_PDFS")

// Busca clientes com saldo negativo (consulta otimizada com limite)
sQuery is string = "SELECT c.cpf, c.email, c.nome, s.saldo FROM tbl_cliente c " +
"INNER JOIN tbl_saldo s ON c.cpf = s.cpf_cliente " +
"WHERE s.saldo < 0 AND s.pdf_gerado = False " +
"ORDER BY s.saldo ASC"
HExecuteSQLQuery(qry_saldo, hQueryDefault, sQuery)

// Conta total de registros para monitoramento
nTotalClientes is int = 0
WHILE NOT HOut(qry_saldo)
nTotalClientes += 1
HReadNext(qry_saldo)
END
HReadFirst(qry_saldo)

// Atualiza contagem total de clientes
BOLETO_AtualizaProcessamento(nIdProcessamento, "total_clientes", nTotalClientes)
BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Total de clientes para processamento: " + nTotalClientes)

// Inicializa mutex para proteção de contadores compartilhados
mutexContadores is Mutex = MutexCreate("CONTADORES_BOLETO")

// Controle de timeout global e monitoramento de recursos
dhorarioInicio is datetime = Now()
nSomaCPU is numeric = 0
nSomaMemoria is numeric = 0
nContadorVerificacoes is int = 0

// Processa cada cliente
nContadorClientes is int = 0
WHILE NOT HOut(qry_saldo)
nContadorClientes += 1

// Verifica timeout global do processamento
nTempoDecorrido is int = DateTimeDifference(Now(), dhorarioInicio)
nTimeoutGlobal is int = GetConfigValue("TIMEOUT_GLOBAL", 3600) // 1 hora por padrão

IF nTempoDecorrido > nTimeoutGlobal THEN
sMensagem is string = "Timeout global do processamento atingido após " + nTimeoutGlobal + " segundos"
BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", sMensagem)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CANCELADO")
BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem)
BREAK
END

// Verifica recursos periodicamente
IF nContadorClientes % nVerificacaoRecursos = 0 THEN
// Verifica espaço em disco
IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN
// Espaço insuficiente - registra e continua com threads reduzidas
BOLETO_LogRegistro("ESPACO_DISCO", "AVISO", "Espaço em disco baixo, reduzindo número de threads")
nMaxThreads = 1 // Reduz ao mínimo para tentar continuar
END

// Verifica uso de CPU e memória
nUsoCPU is int = BOLETO_VerificaUsoCPU()
nUsoMemoria is int = BOLETO_VerificaUsoMemoria()

// Acumula para cálculo de média
nSomaCPU += nUsoCPU
nSomaMemoria += nUsoMemoria
nContadorVerificacoes += 1

// Ajusta número de threads com base no uso de recursos
nMaxThreads = BOLETO_AjustaNumeroThreads(nMaxThreadsConfig)
END

// Gerencia limite de threads
IF nThreadsAtivas < nMaxThreads THEN
// MODIFICAÇÃO: Usa CPF do cliente como nome da thread
sThreadName is string = qry_saldo.cpf
ExecuteThread(sThreadName, threadNormal, BOLETO_Thread_Gerador, qry_saldo.cpf, sPastaBoletos, mutexContadores, @nPDFsGerados, @nFalhasPDF, nIdProcessamento)
nThreadsAtivas += 1

// Log de início de thread
BOLETO_LogRegistro("THREAD", "INFO", "Thread iniciada para CPF: " + qry_saldo.cpf)
ELSE
// Aguarda com timeout
IF NOT ThreadWait(5000) THEN // Timeout de 5 segundos para evitar bloqueio infinito
BOLETO_LogRegistro("THREAD", "INFO", "Aguardando liberação de threads. Ativas: " + nThreadsAtivas + "/" + nMaxThreads)
ELSE
nThreadsAtivas -= 1
END
END

HReadNext(qry_saldo)
END

// Aguarda todas as threads de geração terminarem
startTimeout is datetime = Now()
WHILE nThreadsAtivas > 0
// Timeout de segurança
IF DateTimeDifference(Now(), startTimeout) > 300 THEN // 5 minutos de timeout
BOLETO_LogRegistro("THREAD", "CRITICO", "Timeout ao aguardar threads. Forçando continuação.")
BREAK
END

IF NOT ThreadWait(10000) THEN // 10 segundos de timeout
BOLETO_LogRegistro("THREAD", "INFO", "Aguardando finalização de " + nThreadsAtivas + " threads...")
ELSE
nThreadsAtivas -= 1
END
END

// Calcula médias de uso de recursos
nCPUMedio is numeric = 0
nMemoriaMedio is numeric = 0
IF nContadorVerificacoes > 0 THEN
nCPUMedio = nSomaCPU / nContadorVerificacoes
nMemoriaMedio = nSomaMemoria / nContadorVerificacoes
END

// Verifica espaço em disco final
nEspacoFinal is int = BOLETO_VerificaEspacoDisco("C:\")

// Calcula tempo de execução
nTempoExecucao is int = DateTimeDifference(Now(), dhorarioInicio)

// Registra PDFs gerados e atualiza status
BOLETO_AtualizaProcessamento(nIdProcessamento, "pdfs_gerados", nPDFsGerados)
BOLETO_AtualizaProcessamento(nIdProcessamento, "falhas_pdf", nFalhasPDF)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ENVIANDO_EMAILS")
BOLETO_AtualizaProcessamento(nIdProcessamento, "uso_cpu_medio", nCPUMedio)
BOLETO_AtualizaProcessamento(nIdProcessamento, "uso_memoria_medio", nMemoriaMedio)
BOLETO_AtualizaProcessamento(nIdProcessamento, "espaco_disco_final", nEspacoFinal)
BOLETO_AtualizaProcessamento(nIdProcessamento, "tempo_execucao", nTempoExecucao)

// Dispara thread de envio de e-mails
ThreadExecute("THREAD_ENVIA_EMAILS", threadNormal, BOLETO_Thread_EnviaEmails, sPastaBoletos, nIdProcessamento)
BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Iniciada thread de envio de e-mails. PDFs gerados: " + nPDFsGerados + ", Falhas: " + nFalhasPDF)

MutexDestroy(mutexContadores)

//##############################
// *BOLETO_Thread_Gerador*
//##############################

PROCEDURE BOLETO_Thread_Gerador(cpfCliente is string, sDiretorio is string, mutexContadores is Mutex, nPDFsGerados is int by reference, nFalhasPDF is int by reference, nIdProcessamento is int)

// Timeout para geração de cada boleto individualmente
nTimeoutGeracaoPDF is int = GetConfigValue("TIMEOUT_GERACAO_PDF", 60) // segundos
startTime is datetime = Now()

BOLETO_LogRegistro("PDF", "INFO", "Iniciando geração de PDF", cpfCliente)

TRY
HReadSeekFirst(tbl_cliente, cpf, cpfCliente)
IF HFound(tbl_cliente) THEN
sNomePDF is string = sDiretorio + "\" + cpfCliente + "_" + StringReplace(TimeToString(Now(), "HHMMSS"), ":", "") + ".pdf"

// Verifica espaço em disco antes de gerar o PDF
nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_PDF", 10) // Mínimo para um PDF
IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN
// Espaço insuficiente - registra falha
BOLETO_LogRegistro("PDF", "ERRO", "Espaço insuficiente para gerar PDF", cpfCliente)

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Espaço insuficiente em disco")
RETURN
END

// Geração do PDF com tratamento de erro e timeout
IF iInitReportQuery(REP_BOLETO) THEN
iParameter("CPF", cpfCliente)
iDestination(iPDF)

// Verifica timeout durante a geração
IF DateTimeDifference(Now(), startTime) > nTimeoutGeracaoPDF THEN
BOLETO_LogRegistro("PDF", "ERRO", "Timeout na geração do PDF", cpfCliente)

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Timeout na geração")
RETURN
END

IF iPrintReport(REP_BOLETO, sNomePDF) THEN
IF fFileExist(sNomePDF) THEN
// Atualiza tabela de saldo com lock para prevenir concorrência
IF HReadSeekFirst(tbl_saldo, cpf_cliente, cpfCliente, hLockWrite) THEN
tbl_saldo.pdf_gerado = True
tbl_saldo.nome_pdf = fExtractPath(sNomePDF, fFileName + fExtension)
tbl_saldo.data_geracao = Today()
tbl_saldo.hora_geracao = Now()
IF HModify(tbl_saldo) THEN
// Incrementa contador com proteção de mutex
MutexLock(mutexContadores)
nPDFsGerados += 1
MutexUnlock(mutexContadores)

// Log de sucesso
BOLETO_LogRegistro("PDF", "INFO", "PDF gerado com sucesso: " + sNomePDF, cpfCliente)

// Registra em tabela de rastreabilidade
BOLETO_RegistraRastreabilidade(cpfCliente, "PDF_GERADO", nIdProcessamento)
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "Falha ao atualizar tbl_saldo", cpfCliente, HErrorInfo())

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao atualizar registro")
END
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "Falha ao bloquear registro na tbl_saldo", cpfCliente)

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao bloquear registro")
END
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "PDF não encontrado após geração: " + sNomePDF, cpfCliente)

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Arquivo não encontrado após geração")
END
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "Falha ao gerar PDF", cpfCliente, iErrorInfo())

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha na impressão do relatório: " + iErrorInfo())
END
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "Falha ao inicializar relatório", cpfCliente, iErrorInfo())

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao inicializar relatório: " + iErrorInfo())
END
ELSE
BOLETO_LogRegistro("PDF", "ERRO", "Cliente não encontrado", cpfCliente)

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)
END
CATCH
BOLETO_LogRegistro("PDF", "ERRO", "Exceção ao processar", cpfCliente, ExceptionInfo())

// Incrementa contador de falhas com proteção de mutex
MutexLock(mutexContadores)
nFalhasPDF += 1
MutexUnlock(mutexContadores)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Exceção: " + ExceptionInfo())
END

//##############################
// *BOLETO_Thread_EnviaEmails*
//##############################

PROCEDURE BOLETO_Thread_EnviaEmails(sDiretorio is string, nIdProcessamento is int)

// Contador de e-mails enviados e falhas
nEmailsEnviados is int = 0
nFalhasEmail is int = 0
nLimiteEmailsHora is int = GetConfigValue("LIMITE_EMAILS_HORA", 100)
nTempoEsperaEntreLotes is int = GetConfigValue("TEMPO_ESPERA_LOTE", 60) // segundos
nTamanhoLote is int = GetConfigValue("TAMANHO_LOTE_EMAIL", 20)
nContadorLote is int = 0

// Timeout para envio de emails
nTimeoutEnvio is int = GetConfigValue("TIMEOUT_ENVIO_EMAILS", 3600) // 1 hora
startTime is datetime = Now()

BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ENVIANDO_EMAILS")
BOLETO_LogRegistro("EMAIL", "INFO", "Iniciando processo de envio de e-mails")

TRY
// Carrega configurações de SMTP com tratamento de criptografia
HReadFirst(tbl_config_smtp)
IF NOT HFound(tbl_config_smtp) THEN
sMensagem is string = "Configurações de SMTP não encontradas"
BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO")
BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem)
RETURN
END

// Descriptografa senha se necessário
sSenhaSMTP is string = BOLETO_DescriptografaSenha(tbl_config_smtp.senha)

// Verifica pasta de boletos
IF NOT fDirectoryExist(sDiretorio) THEN
sMensagem is string = "Diretório de boletos não encontrado: " + sDiretorio
BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO")
BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem)
RETURN
END

// Busca clientes com PDF gerado (com otimização de campos)
sQuery is string = "SELECT s.cpf_cliente, s.nome_pdf, c.email, c.nome " +
"FROM tbl_saldo s " +
"INNER JOIN tbl_cliente c ON s.cpf_cliente = c.cpf " +
"WHERE s.pdf_gerado = True AND s.saldo < 0 " +
"AND s.email_enviado = False"
HExecuteSQLQuery(qry_saldo, hQueryDefault, sQuery)

// Conta total para monitoramento
nTotalEmails is int = 0
WHILE NOT HOut(qry_saldo)
nTotalEmails += 1
HReadNext(qry_saldo)
END
HReadFirst(qry_saldo)

BOLETO_LogRegistro("EMAIL", "INFO", "Total de emails para envio: " + nTotalEmails)

// Configura servidor SMTP uma única vez
EmailReset()
Email.ServerAddress = tbl_config_smtp.servidor
Email.Port = tbl_config_smtp.porta
Email.UserName = tbl_config_smtp.usuario
Email.Password = sSenhaSMTP
Email.Name = "Cobranca WX"
Email.Address = tbl_config_smtp.email_remetente
Email.Authentication = True
IF tbl_config_smtp.usar_ssl THEN
Email.Secure = emailSecureSSL
END

WHILE NOT HOut(qry_saldo)
// Verifica timeout global
IF DateTimeDifference(Now(), startTime) > nTimeoutEnvio THEN
sMensagem is string = "Timeout no envio de emails atingido após " + nTimeoutEnvio + " segundos"
BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem)
BREAK
END

// Implementa lotes com pausas para evitar blacklist
IF nContadorLote >= nTamanhoLote THEN
BOLETO_LogRegistro("EMAIL", "INFO", "Pausa entre lotes de emails. Enviados até agora: " + nEmailsEnviados)
Sleep(nTempoEsperaEntreLotes * 1000)
nContadorLote = 0
END

IF qry_saldo.email <> "" THEN
// Valida formato do e-mail
IF StringRegExMatch(qry_saldo.email, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") THEN
sCaminhoPDF is string = sDiretorio + "\" + qry_saldo.nome_pdf
IF fFileExist(sCaminhoPDF) THEN
// Mantém configuração SMTP já definida, reseta apenas campos específicos
Email.Subject = "Boleto em aberto - WX Soluções"
Email.Message = "Prezado(a) " + qry_saldo.nome + "," + CR + CR +
"Segue em anexo o boleto referente ao saldo em aberto na sua conta." + CR + CR +
"Para qualquer dúvida, entre em contato com nosso suporte." + CR + CR +
"Atenciosamente," + CR +
"Equipe WX Soluções"
Email.Recipient[1] = qry_saldo.email
Email.Attachments[1] = sCaminhoPDF

// Tratamento de erro no envio
TRY
IF EmailSendMessage() THEN
nEmailsEnviados += 1
nContadorLote += 1

// Atualiza status na tabela de saldo com lock
IF HReadSeekFirst(tbl_saldo, cpf_cliente, qry_saldo.cpf_cliente, hLockWrite) THEN
tbl_saldo.email_enviado = True
tbl_saldo.data_envio = Today()
tbl_saldo.hora_envio = Now()
IF NOT HModify(tbl_saldo) THEN
BOLETO_LogRegistro("EMAIL", "ERRO", "Falha ao atualizar status de email enviado", qry_saldo.cpf_cliente, HErrorInfo())
END
END

// Registra em tabela de rastreabilidade
BOLETO_RegistraRastreabilidade(qry_saldo.cpf_cliente, "EMAIL_ENVIADO", nIdProcessamento)

BOLETO_LogRegistro("EMAIL", "INFO", "E-mail enviado com sucesso", qry_saldo.cpf_cliente)
ELSE
nFalhasEmail += 1
sMensagemErro is string = "Erro ao enviar e-mail: " + ErrorInfo()
BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro)
END
CATCH
nFalhasEmail += 1
sMensagemErro is string = "Exceção ao enviar email: " + ExceptionInfo()
BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro)
END
ELSE
nFalhasEmail += 1
sMensagemErro is string = "PDF não encontrado: " + sCaminhoPDF
BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente)

// Registra para reprocessamento
BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro)
END
ELSE
nFalhasEmail += 1
sMensagemErro is string = "E-mail inválido: " + qry_saldo.email
BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente)
END
END
HReadNext(qry_saldo)
END

// Atualiza contagem final
BOLETO_AtualizaProcessamento(nIdProcessamento, "emails_enviados", nEmailsEnviados)
BOLETO_AtualizaProcessamento(nIdProcessamento, "falhas_email", nFalhasEmail)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO")

// Verifica espaço em disco final
nEspacoFinal is int = BOLETO_VerificaEspacoDisco("C:\")
BOLETO_AtualizaProcessamento(nIdProcessamento, "espaco_disco_final", nEspacoFinal)

// Calcula tempo de execução
nTempoExecucao is int = DateTimeDifference(Now(), startTime)
BOLETO_AtualizaProcessamento(nIdProcessamento, "tempo_execucao", nTempoExecucao)

BOLETO_LogRegistro("EMAIL", "INFO", "Envio de e-mails concluído. E-mails enviados: " + nEmailsEnviados + " de " + nTotalEmails + ". Falhas: " + nFalhasEmail)
CATCH
sMensagemErro is string = "Exceção no processo de envio de emails: " + ExceptionInfo()
BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagemErro)
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO")
BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagemErro)
END

//##############################
// *BOLETO_ExecutarCompleto*
//##############################

PROCEDURE BOLETO_ExecutarCompleto()

Trace("Iniciando processo de geração de boletos...")
dHoraInicio is datetime = Now()

// Verifica se já existe processo em execução
IF BOLETO_VerificaProcessoEmExecucao() THEN
Error("Já existe um processo de geração de boletos em execução! Aguarde ou verifique os logs.")
RETURN
END

// Valida pasta base com tratamento avançado de erros
sPastaBase is string = GetConfigValue("PASTA_BASE_BOLETOS", "C:\Boletos")
IF NOT fDirectoryExist(sPastaBase) THEN
TRY
IF NOT fMakeDir(sPastaBase) THEN
Error("Não foi possível criar a pasta base de boletos!")
RETURN
END
CATCH
Error("Erro ao criar pasta base: " + ExceptionInfo())
RETURN
END
END

// Verifica espaço em disco
nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500)
IF BOLETO_VerificaEspacoDisco(sPastaBase) < nEspacoMinimoMB THEN
sMensagem is string = "Espaço insuficiente em disco para gerar boletos!"
Error(sMensagem)

// Notifica administrador
BOLETO_NotificaAdministrador("ESPACO_DISCO", sMensagem)
RETURN
END

// Dispara processo multithread
TRY
Trace("Chamando thread de geração paralela...")
BOLETO_GeraPDFs_Threaded()

Trace("Processo iniciado com sucesso. Tempo de execução: " +
DateTimeDifference(Now(), dHoraInicio) + " segundos")
CATCH
sMensagem is string = "Exceção no processo principal: " + ExceptionInfo()
BOLETO_LogRegistro("SISTEMA", "CRITICO", sMensagem)
Error(sMensagem)
END

//##############################
// *BOLETO_LogRegistro*
//##############################

PROCEDURE BOLETO_LogRegistro(sTipoErro is string, sSeveridade is string, sMensagem is string, sCPFCliente is string = "", sDetalhesTecnicos is string = "")

TRY
tbl_log_erros.data = Today()
tbl_log_erros.hora = Now()
tbl_log_erros.tipo_erro = sTipoErro
tbl_log_erros.severidade = sSeveridade
tbl_log_erros.mensagem = sMensagem
tbl_log_erros.cpf_cliente = sCPFCliente
tbl_log_erros.id_processamento = gnIdProcessamentoAtual
tbl_log_erros.usuario = CurrentUser()
tbl_log_erros.maquina = NetMachineName()
tbl_log_erros.detalhes_tecnicos = sDetalhesTecnicos

IF HAdd(tbl_log_erros) THEN
// Log adicionado com sucesso
IF sSeveridade = "CRITICO" THEN
// Envia notificação para administrador em caso de erro crítico
BOLETO_NotificaAdministrador(sTipoErro, sMensagem)
END
ELSE
// Falha ao registrar na tabela, tenta salvar em arquivo
fSaveText("C:\Boletos\log_erros.txt", DateToString(Today()) + " " +
TimeToString(Now()) + " - [" + sTipoErro + "][" + sSeveridade + "] " +
sMensagem + " - CPF: " + sCPFCliente + CR, foAppend)
END

// Exibe no trace para debug
Trace("[" + sTipoErro + "][" + sSeveridade + "] " + sMensagem)
CATCH
// Último recurso se tudo falhar
fSaveText("C:\Boletos\log_emergencia.txt", DateToString(Today()) + " " +
TimeToString(Now()) + " - FALHA NO SISTEMA DE LOG: " +
sMensagem + " - " + ExceptionInfo() + CR, foAppend)
END

//##############################
// *BOLETO_VerificaProcessoEmExecucao*
//##############################

FUNCTION BOLETO_VerificaProcessoEmExecucao()

bProcessoAtivo is boolean = False

// Verifica na tabela de controle se existe processo no status "EM_EXECUCAO"
sQuery is string = "SELECT COUNT(*) AS total FROM tbl_controle_processamento " +
"WHERE status IN ('INICIADO', 'GERANDO_PDFS', 'ENVIANDO_EMAILS') " +
"AND DATE(data_exec) = DATE(NOW()) " +
"AND (TIMESTAMPDIFF(MINUTE, CONCAT(data_exec, ' ', hora_exec), NOW()) < 120)" // Processos iniciados há menos de 2 horas
HExecuteSQLQuery(qry_proc, hQueryDefault, sQuery)

IF NOT HOut(qry_proc) AND qry_proc.total > 0 THEN
bProcessoAtivo = True
END

RETURN bProcessoAtivo

//##############################
// *BOLETO_IniciaProcessamento*
//##############################

FUNCTION BOLETO_IniciaProcessamento(sData is string, sHora is string)

nId is int = 0

// Verifica espaço em disco inicial
nEspacoInicial is int = BOLETO_VerificaEspacoDisco("C:\")

HReset(tbl_controle_processamento)
tbl_controle_processamento.data_exec = DateFromString(sData, "YYYYMMDD")
tbl_controle_processamento.hora_exec = TimeFromString(sHora, "HHmmss")
tbl_controle_processamento.total_clientes = 0
tbl_controle_processamento.pdfs_gerados = 0
tbl_controle_processamento.emails_enviados = 0
tbl_controle_processamento.falhas_pdf = 0
tbl_controle_processamento.falhas_email = 0
tbl_controle_processamento.status = "INICIADO"
tbl_controle_processamento.motivo_erro = ""
tbl_controle_processamento.uso_cpu_medio = 0
tbl_controle_processamento.uso_memoria_medio = 0
tbl_controle_processamento.espaco_disco_inicial = nEspacoInicial
tbl_controle_processamento.espaco_disco_final = 0
tbl_controle_processamento.tempo_execucao = 0
tbl_controle_processamento.usuario = CurrentUser()
tbl_controle_processamento.maquina = NetMachineName()

IF HAdd(tbl_controle_processamento) THEN
nId = tbl_controle_processamento.id
gnIdProcessamentoAtual = nId // Variável global para referência
BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Processamento iniciado com ID: " + nId)
ELSE
BOLETO_LogRegistro("PROCESSAMENTO", "ERRO", "Falha ao iniciar registro de processamento", "", HErrorInfo())
END

RETURN nId

//##############################
// *BOLETO_AtualizaProcessamento*
//##############################

PROCEDURE BOLETO_AtualizaProcessamento(nId is int, sCampo is string, xValor is variant, bIncrementar is boolean = False)

IF nId <= 0 THEN RETURN

TRY
sQuery is string

IF bIncrementar THEN
// Incrementa o valor atual
sQuery = "UPDATE tbl_controle_processamento SET " + sCampo + " = " + sCampo + " + ? WHERE id = ?"
ELSE
// Substitui o valor atual
sQuery = "UPDATE tbl_controle_processamento SET " + sCampo + " = ? WHERE id = ?"
END

HExecuteSQLQuery(qry_update, hQueryDefault, sQuery, xValor, nId)
CATCH
BOLETO_LogRegistro("PROCESSAMENTO", "ERRO", "Falha ao atualizar processamento: " + sCampo + " = " + xValor, "", ExceptionInfo())
END

//##############################
// *BOLETO_VerificaEspacoDisco*
//##############################

FUNCTION BOLETO_VerificaEspacoDisco(sDiretorio is string)

// Retorna espaço livre em MB
nEspacoLivre is int = 0

TRY
// Utiliza comando do sistema para obter informações do disco
sComando is string = "wmic logicaldisk where DeviceID=\"" + Left(sDiretorio, 2) + "\" get FreeSpace"
sResultado is string = ExecCommandLine(sComando)

// Extrai o valor de espaço livre da resposta
IF sResultado <> "" THEN
sEspacoStr is string = RegexExtract(sResultado, "[0-9]+", 0)
IF sEspacoStr <> "" THEN
nEspacoLivre = Val(sEspacoStr) / (1024 * 1024) // Converte para MB
END
END

// Registra informação sobre espaço em disco
BOLETO_LogRegistro("ESPACO_DISCO", "INFO", "Espaço livre em " + Left(sDiretorio, 2) + ": " + nEspacoLivre + " MB")
CATCH
BOLETO_LogRegistro("ESPACO_DISCO", "ERRO", "Falha ao verificar espaço em disco", "", ExceptionInfo())
nEspacoLivre = 0 // Em caso de erro, assume zero para forçar verificação de segurança
END

RETURN nEspacoLivre

//##############################
// *BOLETO_VerificaEspacoPeriodicoPDF*
//##############################

PROCEDURE BOLETO_VerificaEspacoPeriodicoPDF(sDiretorio is string, nEspacoMinimoMB is int)

// Verifica se há espaço suficiente
nEspacoAtual is int = BOLETO_VerificaEspacoDisco(sDiretorio)

IF nEspacoAtual < nEspacoMinimoMB THEN
// Espaço insuficiente - registra erro crítico
sMensagem is string = "ALERTA: Espaço em disco crítico! Disponível: " + nEspacoAtual + " MB, Mínimo necessário: " + nEspacoMinimoMB + " MB"
BOLETO_LogRegistro("ESPACO_DISCO", "CRITICO", sMensagem)
RETURN False
END

RETURN True

//##############################
// *BOLETO_NotificaAdministrador*
//##############################

PROCEDURE BOLETO_NotificaAdministrador(sTipoErro is string, sMensagem is string)

TRY
// Carrega configurações de e-mail do administrador
sEmailAdmin is string = GetConfigValue("EMAIL_ADMIN", "admin@empresa.com.br")

// Configura e-mail
EmailReset()

// Carrega configurações SMTP
HReadFirst(tbl_config_smtp)
IF HFound(tbl_config_smtp) THEN
Email.ServerAddress = tbl_config_smtp.servidor
Email.Port = tbl_config_smtp.porta
Email.UserName = tbl_config_smtp.usuario
Email.Password = BOLETO_DescriptografaSenha(tbl_config_smtp.senha)
Email.Authentication = True
IF tbl_config_smtp.usar_ssl THEN
Email.Secure = emailSecureSSL
END
ELSE
BOLETO_LogRegistro("EMAIL", "ERRO", "Configurações SMTP não encontradas para notificação ao administrador")
RETURN
END

// Configura mensagem
Email.Subject = "ALERTA: " + sTipoErro + " - Sistema de Boletos"
Email.Message = "Ocorreu um problema no sistema de geração de boletos:" + CR + CR +
"Tipo: " + sTipoErro + CR +
"Mensagem: " + sMensagem + CR + CR +
"Data/Hora: " + DateToString(Today()) + " " + TimeToString(Now()) + CR +
"Máquina: " + NetMachineName() + CR +
"Usuário: " + CurrentUser() + CR + CR +
"Este é um e-mail automático, por favor não responda."

Email.Recipient[1] = sEmailAdmin
Email.Name = "Sistema de Boletos"
Email.Address = tbl_config_smtp.email_remetente

// Envia e-mail
IF EmailSendMessage() THEN
BOLETO_LogRegistro("EMAIL", "INFO", "Notificação enviada ao administrador: " + sEmailAdmin)
ELSE
BOLETO_LogRegistro("EMAIL", "ERRO", "Falha ao enviar notificação ao administrador", "", ErrorInfo())
END
CATCH
BOLETO_LogRegistro("EMAIL", "ERRO", "Exceção ao enviar notificação ao administrador", "", ExceptionInfo())
END

//##############################
// *BOLETO_VerificaUsoCPU*
//##############################

FUNCTION BOLETO_VerificaUsoCPU()

// Retorna uso de CPU em porcentagem (0-100)
nUsoCPU is int = 0

TRY
// Comando para obter uso de CPU
sComando is string = "wmic cpu get loadpercentage"
sResultado is string = ExecCommandLine(sComando)

// Extrai o valor de uso de CPU
IF sResultado <> "" THEN
sUsoCPUStr is string = RegexExtract(sResultado, "[0-9]+", 0)
IF sUsoCPUStr <> "" THEN
nUsoCPU = Val(sUsoCPUStr)
END
END
CATCH
BOLETO_LogRegistro("RECURSOS", "ERRO", "Falha ao verificar uso de CPU", "", ExceptionInfo())
nUsoCPU = 100 // Em caso de erro, assume 100% para ser conservador
END

RETURN nUsoCPU

//##############################
// *BOLETO_VerificaUsoMemoria*
//##############################

FUNCTION BOLETO_VerificaUsoMemoria()

// Retorna uso de memória em porcentagem (0-100)
nUsoMemoria is int = 0

TRY
// Comando para obter informações de memória
sComando is string = "wmic OS get FreePhysicalMemory,TotalVisibleMemorySize"
sResultado is string = ExecCommandLine(sComando)

// Extrai valores de memória livre e total
IF sResultado <> "" THEN
sMemoriaLivreStr is string = RegexExtract(sResultado, "[0-9]+", 0)
sMemoriaTotalStr is string = RegexExtract(sResultado, "[0-9]+", 1)

IF sMemoriaLivreStr <> "" AND sMemoriaTotalStr <> "" THEN
nMemoriaLivre is int = Val(sMemoriaLivreStr)
nMemoriaTotal is int = Val(sMemoriaTotalStr)

IF nMemoriaTotal > 0 THEN
nUsoMemoria = 100 - ((nMemoriaLivre * 100) / nMemoriaTotal)
END
END
END
CATCH
BOLETO_LogRegistro("RECURSOS", "ERRO", "Falha ao verificar uso de memória", "", ExceptionInfo())
nUsoMemoria = 100 // Em caso de erro, assume 100% para ser conservador
END

RETURN nUsoMemoria

//##############################
// *BOLETO_AjustaNumeroThreads*
//##############################

FUNCTION BOLETO_AjustaNumeroThreads(nMaxThreadsConfig is int)

// Calcula número ideal de threads com base no uso de recursos
nMaxThreads is int = nMaxThreadsConfig
nLimiteUsoRecursos is int = GetConfigValue("LIMITE_USO_RECURSOS", 80) // Limite de 80% por padrão

// Verifica uso atual de recursos
nUsoCPU is int = BOLETO_VerificaUsoCPU()
nUsoMemoria is int = BOLETO_VerificaUsoMemoria()

// Usa o recurso mais crítico para ajustar
nUsoMaisAlto is int = Max(nUsoCPU, nUsoMemoria)

// Ajusta número de threads com base no uso de recursos
IF nUsoMaisAlto > nLimiteUsoRecursos THEN
// Reduz threads proporcionalmente ao excesso de uso
nFatorReducao is numeric = (nUsoMaisAlto - nLimiteUsoRecursos) / 20 + 1 // +1 para garantir pelo menos alguma redução
nMaxThreads = Max(1, nMaxThreadsConfig / nFatorReducao) // Garante pelo menos 1 thread

// Registra ajuste
BOLETO_LogRegistro("RECURSOS", "AVISO", "Uso de recursos em " + nUsoMaisAlto + "%. Reduzindo threads de " + nMaxThreadsConfig + " para " + nMaxThreads)
END

RETURN nMaxThreads

//##############################
// *BOLETO_RegistraRastreabilidade*
//##############################

PROCEDURE BOLETO_RegistraRastreabilidade(sCPF is string, sEvento is string, nIdProcessamento is int)

HReset(tbl_rastreabilidade_boleto)
tbl_rastreabilidade_boleto.cpf_cliente = sCPF
tbl_rastreabilidade_boleto.evento = sEvento
tbl_rastreabilidade_boleto.data = Today()
tbl_rastreabilidade_boleto.hora = Now()
tbl_rastreabilidade_boleto.id_processamento = nIdProcessamento
tbl_rastreabilidade_boleto.usuario = CurrentUser()
tbl_rastreabilidade_boleto.maquina = NetMachineName()
HAdd(tbl_rastreabilidade_boleto)

//##############################
// *BOLETO_RegistraReprocessamento*
//##############################

PROCEDURE BOLETO_RegistraReprocessamento(sCPFCliente is string, sTipoFalha is string, sMotivoFalha is string)

TRY
HReset(tbl_reprocessamento)
tbl_reprocessamento.cpf_cliente = sCPFCliente
tbl_reprocessamento.tipo_falha = sTipoFalha
tbl_reprocessamento.data_falha = Today()
tbl_reprocessamento.hora_falha = Now()
tbl_reprocessamento.id_processamento_original = gnIdProcessamentoAtual
tbl_reprocessamento.motivo_falha = sMotivoFalha
tbl_reprocessamento.status = "PENDENTE"
tbl_reprocessamento.tentativas = 0

IF HAdd(tbl_reprocessamento) THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Item registrado para reprocessamento: CPF " + sCPFCliente + ", Tipo: " + sTipoFalha)
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao registrar item para reprocessamento", sCPFCliente, HErrorInfo())
END
CATCH
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao registrar item para reprocessamento", sCPFCliente, ExceptionInfo())
END

//##############################
// *BOLETO_ReprocessarFalhas*
//##############################

PROCEDURE BOLETO_ReprocessarFalhas(sTipoFalha is string = "")

// Verifica se já existe processo em execução
IF BOLETO_VerificaProcessoEmExecucao() THEN
Error("Já existe um processo de geração de boletos em execução! Aguarde ou verifique os logs.")
RETURN
END

// Cria diretório por data com horário para evitar conflitos em múltiplas execuções
sDataHoje is string = DateToString(Today(), "YYYYMMDD")
sHoraExec is string = TimeToString(Now(), "HHmmss")
sPastaBoletos is string = "C:\Boletos\reprocessamento_" + sDataHoje + "_" + sHoraExec

// Verifica e cria diretório
IF NOT fDirectoryExist(sPastaBoletos) THEN
IF NOT fMakeDir(sPastaBoletos) THEN
BOLETO_LogRegistro("SISTEMA", "CRITICO", "Falha ao criar diretório: " + sPastaBoletos)
RETURN
END
END

// Verifica espaço em disco inicial
nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500)
IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN
// Espaço insuficiente - notifica e cancela
Error("Espaço em disco insuficiente para iniciar o reprocessamento. Verifique os logs.")
RETURN
END

// Inicia registro de processamento
nIdProcessamento is int = BOLETO_IniciaProcessamento(sDataHoje, sHoraExec)
IF nIdProcessamento = 0 THEN
BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", "Falha ao iniciar registro de processamento para reprocessamento")
RETURN
END

// Atualiza status
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "REPROCESSANDO")

// Busca itens para reprocessamento
sQuery is string = "SELECT id, cpf_cliente, tipo_falha, motivo_falha FROM tbl_reprocessamento WHERE status = 'PENDENTE'"
IF sTipoFalha <> "" THEN
sQuery += " AND tipo_falha = '" + sTipoFalha + "'"
END
sQuery += " ORDER BY data_falha, hora_falha"

HExecuteSQLQuery(qry_reprocessamento, hQueryDefault, sQuery)

// Conta total de registros para monitoramento
nTotalItens is int = 0
WHILE NOT HOut(qry_reprocessamento)
nTotalItens += 1
HReadNext(qry_reprocessamento)
END
HReadFirst(qry_reprocessamento)

IF nTotalItens = 0 THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Nenhum item pendente para reprocessamento")
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO")
RETURN
END

BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Total de itens para reprocessamento: " + nTotalItens)
BOLETO_AtualizaProcessamento(nIdProcessamento, "total_clientes", nTotalItens)

// Processa cada item
WHILE NOT HOut(qry_reprocessamento)
sCPF is string = qry_reprocessamento.cpf_cliente
sTipo is string = qry_reprocessamento.tipo_falha
nIdItem is int = qry_reprocessamento.id

BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Reprocessando item: CPF " + sCPF + ", Tipo: " + sTipo)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.tentativas += 1
tbl_reprocessamento.data_reprocessamento = Today()
tbl_reprocessamento.hora_reprocessamento = Now()
tbl_reprocessamento.id_processamento_reprocessamento = nIdProcessamento
HModify(tbl_reprocessamento)
END

// Reprocessa conforme o tipo de falha
IF sTipo = "PDF" THEN
// Reprocessa geração de PDF
BOLETO_ReprocessaPDF(sCPF, sPastaBoletos, nIdProcessamento, nIdItem)
ELSE IF sTipo = "EMAIL" THEN
// Reprocessa envio de e-mail
BOLETO_ReprocessaEmail(sCPF, sPastaBoletos, nIdProcessamento, nIdItem)
END

HReadNext(qry_reprocessamento)
END

// Atualiza status final
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO")
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Reprocessamento concluído")

//##############################
// *BOLETO_ReprocessaPDF*
//##############################

PROCEDURE BOLETO_ReprocessaPDF(sCPF is string, sPasta is string, nIdProcessamento is int, nIdItem is int)

TRY
// Verifica espaço em disco
nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_PDF", 10)
IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Espaço insuficiente para reprocessar PDF", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Espaço insuficiente em disco"
HModify(tbl_reprocessamento)
END

RETURN
END

// Gera o PDF
sNomePDF is string = sPasta + "\" + sCPF + "_" + StringReplace(TimeToString(Now(), "HHMMSS"), ":", "") + ".pdf"

IF iInitReportQuery(REP_BOLETO) THEN
iParameter("CPF", sCPF)
iDestination(iPDF)

IF iPrintReport(REP_BOLETO, sNomePDF) THEN
IF fFileExist(sNomePDF) THEN
// Atualiza tabela de saldo
IF HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF, hLockWrite) THEN
tbl_saldo.pdf_gerado = True
tbl_saldo.nome_pdf = fExtractPath(sNomePDF, fFileName + fExtension)
tbl_saldo.data_geracao = Today()
tbl_saldo.hora_geracao = Now()

IF HModify(tbl_saldo) THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "PDF reprocessado com sucesso", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "REPROCESSADO"
HModify(tbl_reprocessamento)
END

// Registra em tabela de rastreabilidade
BOLETO_RegistraRastreabilidade(sCPF, "PDF_REPROCESSADO", nIdProcessamento)

// Incrementa contador de PDFs gerados
BOLETO_AtualizaProcessamento(nIdProcessamento, "pdfs_gerados", 1, True)
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao atualizar tbl_saldo", sCPF, HErrorInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Falha ao atualizar registro: " + HErrorInfo()
HModify(tbl_reprocessamento)
END
END
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao bloquear registro na tbl_saldo", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Falha ao bloquear registro"
HModify(tbl_reprocessamento)
END
END
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não encontrado após geração: " + sNomePDF, sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Arquivo não encontrado após geração"
HModify(tbl_reprocessamento)
END
END
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao gerar PDF", sCPF, iErrorInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Falha na impressão do relatório: " + iErrorInfo()
HModify(tbl_reprocessamento)
END
END
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao inicializar relatório", sCPF, iErrorInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Falha ao inicializar relatório: " + iErrorInfo()
HModify(tbl_reprocessamento)
END
END
CATCH
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao reprocessar PDF", sCPF, ExceptionInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Exceção: " + ExceptionInfo()
HModify(tbl_reprocessamento)
END
END

//##############################
// *BOLETO_ReprocessaEmail*
//##############################

PROCEDURE BOLETO_ReprocessaEmail(sCPF is string, sPasta is string, nIdProcessamento is int, nIdItem is int)

TRY
// Verifica se o PDF existe
HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF)
IF NOT HFound(tbl_saldo) OR NOT tbl_saldo.pdf_gerado THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não gerado para reprocessar e-mail", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "PDF não gerado para envio de e-mail"
HModify(tbl_reprocessamento)
END

RETURN
END

// Busca informações do cliente
HReadSeekFirst(tbl_cliente, cpf, sCPF)
IF NOT HFound(tbl_cliente) THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Cliente não encontrado para reprocessar e-mail", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Cliente não encontrado"
HModify(tbl_reprocessamento)
END

RETURN
END

// Verifica e-mail
IF tbl_cliente.email = "" OR NOT StringRegExMatch(tbl_cliente.email, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "E-mail inválido: " + tbl_cliente.email, sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "E-mail inválido: " + tbl_cliente.email
HModify(tbl_reprocessamento)
END

RETURN
END

// Verifica se o arquivo PDF existe
sCaminhoPDF is string = ""
IF tbl_saldo.nome_pdf <> "" THEN
// Tenta encontrar o PDF na pasta original
sCaminhoPDF = "C:\Boletos\" + tbl_saldo.nome_pdf
IF NOT fFileExist(sCaminhoPDF) THEN
// Tenta encontrar em todas as pastas de boletos
sCaminhoPDF = ""
sListaPastas is string = fListDirectory("C:\Boletos", frDirectory)
sListaPastas = StringBuild(sListaPastas, CR)
nPos is int = 1
WHILE nPos > 0
sPastaAtual is string = ExtractString(sListaPastas, CR, nPos)
IF sPastaAtual <> "" THEN
sCaminhoTeste is string = "C:\Boletos\" + sPastaAtual + "\" + tbl_saldo.nome_pdf
IF fFileExist(sCaminhoTeste) THEN
sCaminhoPDF = sCaminhoTeste
BREAK
END
END
END
END
END

IF sCaminhoPDF = "" THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não encontrado para reprocessar e-mail", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "PDF não encontrado para envio"
HModify(tbl_reprocessamento)
END

RETURN
END

// Carrega configurações de SMTP
HReadFirst(tbl_config_smtp)
IF NOT HFound(tbl_config_smtp) THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Configurações SMTP não encontradas", sCPF)

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Configurações SMTP não encontradas"
HModify(tbl_reprocessamento)
END

RETURN
END

// Configura e-mail
EmailReset()
Email.ServerAddress = tbl_config_smtp.servidor
Email.Port = tbl_config_smtp.porta
Email.UserName = tbl_config_smtp.usuario
Email.Password = BOLETO_DescriptografaSenha(tbl_config_smtp.senha)
Email.Name = "Cobranca WX"
Email.Address = tbl_config_smtp.email_remetente
Email.Authentication = True
IF tbl_config_smtp.usar_ssl THEN
Email.Secure = emailSecureSSL
END

// Configura mensagem
Email.Subject = "Boleto em aberto - WX Soluções"
Email.Message = "Prezado(a) " + tbl_cliente.nome + "," + CR + CR +
"Segue em anexo o boleto referente ao saldo em aberto na sua conta." + CR + CR +
"Para qualquer dúvida, entre em contato com nosso suporte." + CR + CR +
"Atenciosamente," + CR +
"Equipe WX Soluções"
Email.Recipient[1] = tbl_cliente.email
Email.Attachments[1] = sCaminhoPDF

// Envia e-mail
IF EmailSendMessage() THEN
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "E-mail reprocessado com sucesso", sCPF)

// Atualiza tabela de saldo
IF HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF, hLockWrite) THEN
tbl_saldo.email_enviado = True
tbl_saldo.data_envio = Today()
tbl_saldo.hora_envio = Now()
HModify(tbl_saldo)
END

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "REPROCESSADO"
HModify(tbl_reprocessamento)
END

// Registra em tabela de rastreabilidade
BOLETO_RegistraRastreabilidade(sCPF, "EMAIL_REPROCESSADO", nIdProcessamento)

// Incrementa contador de e-mails enviados
BOLETO_AtualizaProcessamento(nIdProcessamento, "emails_enviados", 1, True)
ELSE
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao reprocessar e-mail", sCPF, ErrorInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Falha ao enviar e-mail: " + ErrorInfo()
HModify(tbl_reprocessamento)
END
END
CATCH
BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao reprocessar e-mail", sCPF, ExceptionInfo())

// Atualiza status do item
IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN
tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO"
tbl_reprocessamento.motivo_falha = "Exceção: " + ExceptionInfo()
HModify(tbl_reprocessamento)
END
END

//##############################
// *BOLETO_CriptografaSenha*
//##############################

FUNCTION BOLETO_CriptografaSenha(sSenha is string)

// Implementação real de criptografia
// Esta é uma implementação básica, em produção deve-se usar algoritmos mais seguros
IF Left(sSenha, 4) <> "ENC:" THEN
RETURN "ENC:" + sSenha
ELSE
RETURN sSenha
END

//##############################
// *BOLETO_DescriptografaSenha*
//##############################

FUNCTION BOLETO_DescriptografaSenha(sSenhaEncriptada is string)

// Implementação real de descriptografia
// Esta é uma implementação básica, em produção deve-se usar algoritmos mais seguros
IF Left(sSenhaEncriptada, 4) = "ENC:" THEN
RETURN Mid(sSenhaEncriptada, 5)
ELSE
RETURN sSenhaEncriptada
END

//##############################
// *GetConfigValue*
//##############################

FUNCTION GetConfigValue(sChave is string, xValorPadrao is variant)

// Busca configuração na tabela ou retorna valor padrão
xValor is variant = xValorPadrao

HReadSeekFirst(tbl_configuracoes, chave, sChave)
IF HFound(tbl_configuracoes) THEN
xValor = tbl_configuracoes.valor
END

RETURN xValor

//##############################
// *ESTRUTURA DAS TABELAS*
//##############################

/*
1. tbl_saldo:
- cpf_cliente (string, chave)
- saldo (numeric)
- pdf_gerado (boolean)
- nome_pdf (string)
- email_enviado (boolean)
- data_geracao (date)
- hora_geracao (time)
- data_envio (date)
- hora_envio (time)

2. tbl_cliente:
- cpf (string, chave)
- email (string)
- nome (string)

3. tbl_config_smtp:
- servidor (string)
- porta (integer)
- usuario (string)
- senha (string)
- email_remetente (string)
- usar_ssl (boolean)

4. tbl_log_erros:
- id (auto-increment)
- data (date)
- hora (time)
- tipo_erro (string)
- severidade (string)
- mensagem (string)
- cpf_cliente (string)
- id_processamento (integer)
- usuario (string)
- maquina (string)
- detalhes_tecnicos (string)

5. tbl_controle_processamento:
- id (auto-increment)
- data_exec (date)
- hora_exec (time)
- total_clientes (integer)
- pdfs_gerados (integer)
- emails_enviados (integer)
- falhas_pdf (integer)
- falhas_email (integer)
- status (string)
- motivo_erro (string)
- uso_cpu_medio (numeric)
- uso_memoria_medio (numeric)
- espaco_disco_inicial (integer)
- espaco_disco_final (integer)
- tempo_execucao (integer)
- usuario (string)
- maquina (string)

6. tbl_rastreabilidade_boleto:
- id (auto-increment)
- cpf_cliente (string)
- evento (string)
- data (date)
- hora (time)
- id_processamento (integer)
- usuario (string)
- maquina (string)

7. tbl_reprocessamento:
- id (auto-increment)
- cpf_cliente (string)
- tipo_falha (string)
- data_falha (date)
- hora_falha (time)
- id_processamento_original (integer)
- motivo_falha (string)
- status (string)
- data_reprocessamento (date)
- hora_reprocessamento (time)
- id_processamento_reprocessamento (integer)
- tentativas (integer)

8. tbl_configuracoes:
- chave (string)
- valor (string)
- descricao (string)
*/

//##############################
// *CONFIGURAÇÕES RECOMENDADAS*
//##############################

/*
// Inserir na tabela tbl_configuracoes
INSERT INTO tbl_configuracoes (chave, valor, descricao) VALUES
('MAX_THREADS', '10', 'Número máximo de threads simultâneas'),
('TIMEOUT_THREAD', '120', 'Timeout por thread em segundos'),
('TIMEOUT_GLOBAL', '3600', 'Timeout global do processamento em segundos'),
('TIMEOUT_GERACAO_PDF', '60', 'Timeout para geração de cada PDF em segundos'),
('TIMEOUT_ENVIO_EMAILS', '3600', 'Timeout para envio de emails em segundos'),
('LIMITE_EMAILS_HORA', '100', 'Limite de emails por hora'),
('TEMPO_ESPERA_LOTE', '60', 'Tempo de espera entre lotes em segundos'),
('TAMANHO_LOTE_EMAIL', '20', 'Quantidade de emails por lote'),
('ESPACO_MINIMO_MB', '500', 'Espaço mínimo em disco (MB)'),
('ESPACO_MINIMO_PDF', '10', 'Espaço mínimo para gerar um PDF (MB)'),
('LIMITE_USO_RECURSOS', '80', 'Limite de uso de CPU/memória (%)'),
('INTERVALO_VERIFICACAO_RECURSOS', '10', 'Intervalo para verificação de recursos (a cada X clientes)'),
('EMAIL_ADMIN', 'admin@wxsolucoes.com.br', 'E-mail do administrador para notificações');
*/


Bons estudos

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/