From c42202b7271757b0502aab88beb113416390e61e Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Sun, 29 Mar 2026 17:10:28 -0300 Subject: [PATCH] core: fix timezone Signed-off-by: Rodrigo Bertoldo de Sousa Martins --- Dockerfile | 1 + docker-compose.yml | 3 ++ src/utils/dateProcessor.js | 99 +++++++++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7a5465e..1297e70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ EXPOSE 3000 # Definir variáveis de ambiente padrão ENV NODE_ENV=production ENV BOT_DEBUG=false +ENV TZ=America/Araguaina # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ diff --git a/docker-compose.yml b/docker-compose.yml index fe822a4..7bb1ba0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,9 @@ services: # Usuários autorizados - ALLOWED_USERS=${ALLOWED_USERS} + + # Fuso horário + - TZ=America/Araguaina # Volumes para logs persistentes volumes: diff --git a/src/utils/dateProcessor.js b/src/utils/dateProcessor.js index 1e99aaa..78022ba 100644 --- a/src/utils/dateProcessor.js +++ b/src/utils/dateProcessor.js @@ -111,10 +111,13 @@ class DateProcessor { if (this.diasSemana.hasOwnProperty(diaNome)) { const diaSemanaTarget = this.diasSemana[diaNome]; - const data = this.obterProximoDiaSemana(diaSemanaTarget); - data.setHours(hora, minuto, 0, 0); + const proxData = this.obterProximoDiaSemana(diaSemanaTarget); if (this.isValidTime(hora, minuto)) { + // Obter componentes da data no fuso Brasil + const partes = this.extrairPartesBrasil(proxData); + const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto); + return { success: true, data, @@ -153,11 +156,15 @@ class DateProcessor { if (this.palavrasTemporais.hasOwnProperty(palavraTemporal)) { const diasOffset = this.palavrasTemporais[palavraTemporal]; - const data = new Date(); - data.setDate(data.getDate() + diasOffset); - data.setHours(hora, minuto, 0, 0); if (this.isValidTime(hora, minuto)) { + // Basear no dia atual no fuso Brasil e aplicar offset + const hoje = this.extrairPartesBrasil(new Date()); + const base = this.criarDataBrasil(hoje.ano, hoje.mes, hoje.dia, 12, 0); + base.setDate(base.getDate() + diasOffset); + const partes = this.extrairPartesBrasil(base); + const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto); + return { success: true, data, @@ -213,7 +220,7 @@ class DateProcessor { if (ano < 100) ano += 2000; // Converter anos de 2 dígitos if (mes !== undefined && this.isValidDate(dia, mes, ano) && this.isValidTime(hora, minuto)) { - const data = new Date(ano, mes, dia, hora, minuto, 0, 0); + const data = this.criarDataBrasil(ano, mes, dia, hora, minuto); return { success: true, @@ -251,34 +258,55 @@ class DateProcessor { const quantidade = parseInt(match[1]); const unidade = match[2].toLowerCase(); - const data = new Date(); + let data = new Date(); if (unidade.includes('dia')) { - data.setDate(data.getDate() + quantidade); + const base = this.extrairPartesBrasil(new Date()); + const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0); + tmpDate.setDate(tmpDate.getDate() + quantidade); if (match[3] && match[4] !== undefined) { const hora = parseInt(match[3]); const minuto = match[4] ? parseInt(match[4]) : 0; if (this.isValidTime(hora, minuto)) { - data.setHours(hora, minuto, 0, 0); + const partes = this.extrairPartesBrasil(tmpDate); + data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto); + } else { + data = tmpDate; } + } else { + data = tmpDate; } } else if (unidade.includes('semana')) { - data.setDate(data.getDate() + (quantidade * 7)); + const base = this.extrairPartesBrasil(new Date()); + const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0); + tmpDate.setDate(tmpDate.getDate() + (quantidade * 7)); if (match[3] && match[4] !== undefined) { const hora = parseInt(match[3]); const minuto = match[4] ? parseInt(match[4]) : 0; if (this.isValidTime(hora, minuto)) { - data.setHours(hora, minuto, 0, 0); + const partes = this.extrairPartesBrasil(tmpDate); + data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto); + } else { + data = tmpDate; } + } else { + data = tmpDate; } } else if (unidade.includes('mes')) { - data.setMonth(data.getMonth() + quantidade); + const base = this.extrairPartesBrasil(new Date()); + const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0); + tmpDate.setMonth(tmpDate.getMonth() + quantidade); if (match[3] && match[4] !== undefined) { const hora = parseInt(match[3]); const minuto = match[4] ? parseInt(match[4]) : 0; if (this.isValidTime(hora, minuto)) { - data.setHours(hora, minuto, 0, 0); + const partes = this.extrairPartesBrasil(tmpDate); + data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto); + } else { + data = tmpDate; } + } else { + data = tmpDate; } } else if (unidade.includes('hora')) { data.setHours(data.getHours() + quantidade); @@ -340,6 +368,31 @@ class DateProcessor { return hora >= 0 && hora <= 23 && minuto >= 0 && minuto <= 59; } + /** + * Criar um Date com hora no fuso America/Araguaina, + * independente do TZ do servidor. + */ + criarDataBrasil(ano, mes, dia, hora, minuto) { + // Constrói uma string ISO no fuso -03:00 para garantir + // que a hora digitada pelo usuário seja tratada como BR. + const pad = (n) => String(n).padStart(2, '0'); + const isoStr = `${ano}-${pad(mes + 1)}-${pad(dia)}T${pad(hora)}:${pad(minuto)}:00-03:00`; + return new Date(isoStr); + } + + /** + * Extrair {ano, mes(0-11), dia} de um Date no fuso Brasil. + */ + extrairPartesBrasil(data) { + const fmt = new Intl.DateTimeFormat('en-CA', { + timeZone: 'America/Araguaina', + year: 'numeric', month: '2-digit', day: '2-digit' + }); + // en-CA produz YYYY-MM-DD + const [ano, mes, dia] = fmt.format(data).split('-').map(Number); + return { ano, mes: mes - 1, dia }; + } + /** * Formatar data para string legível */ @@ -352,7 +405,7 @@ class DateProcessor { day: 'numeric', hour: '2-digit', minute: '2-digit', - timeZone: 'America/Sao_Paulo' + timeZone: 'America/Araguaina' }; return data.toLocaleDateString('pt-BR', opcoes); @@ -360,18 +413,20 @@ class DateProcessor { /** * Formatar data para o formato do sistema (YYYY-MM-DD HH:mm:ss) + * Retorna sempre no horário de Brasília. */ formatarParaSistema(data) { if (!(data instanceof Date)) return null; - const ano = data.getFullYear(); - const mes = String(data.getMonth() + 1).padStart(2, '0'); - const dia = String(data.getDate()).padStart(2, '0'); - const hora = String(data.getHours()).padStart(2, '0'); - const minuto = String(data.getMinutes()).padStart(2, '0'); - const segundo = String(data.getSeconds()).padStart(2, '0'); - - return `${ano}-${mes}-${dia} ${hora}:${minuto}:${segundo}`; + // Formata no fuso de Brasília para não depender do TZ do servidor + const fmt = new Intl.DateTimeFormat('sv-SE', { + timeZone: 'America/Araguaina', + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false + }); + // sv-SE produz YYYY-MM-DD HH:mm:ss + return fmt.format(data).replace('T', ' '); } }