fix: scheduling en UTC explícito para Argentina (UTC-3), sin depender del TZ del container
Deploy / deploy (push) Successful in 3m57s

getHours() en Docker devuelve UTC → slots aparecían 3hs antes (5:30 en vez de 8:30).
Ahora usa getUTCHours/getUTCMinutes + offset -180min en todas las comparaciones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Beldor
2026-06-22 22:42:43 -03:00
parent e0779bf2d4
commit 8167f6c901
+49 -28
View File
@@ -8,31 +8,54 @@ const FIN_M = 0;
const INICIO_MINUTOS = INICIO_H * 60 + INICIO_M; // 510 const INICIO_MINUTOS = INICIO_H * 60 + INICIO_M; // 510
const FIN_MINUTOS = FIN_H * 60 + FIN_M; // 1020 const FIN_MINUTOS = FIN_H * 60 + FIN_M; // 1020
// Argentina es siempre UTC-3 (sin horario de verano desde 2008)
const ARG_OFFSET_MIN = -180;
// Minutos del día en hora Argentina (0-1439)
function minutosArgentina(fecha: Date): number {
const utcMin = fecha.getUTCHours() * 60 + fecha.getUTCMinutes();
return ((utcMin + ARG_OFFSET_MIN) + 1440) % 1440;
}
// Día de la semana en Argentina (0=Dom, 6=Sab)
function diaArgentina(fecha: Date): number {
const arg = new Date(fecha.getTime() + ARG_OFFSET_MIN * 60 * 1000);
return arg.getUTCDay();
}
// Componentes de fecha Argentina (para construir fechas UTC)
function fechaArgentina(fecha: Date): { year: number; month: number; date: number } {
const arg = new Date(fecha.getTime() + ARG_OFFSET_MIN * 60 * 1000);
return { year: arg.getUTCFullYear(), month: arg.getUTCMonth(), date: arg.getUTCDate() };
}
// Devuelve la fecha UTC correspondiente a las 8:30 Argentina del día que contiene `fecha`
function inicioJornada(fecha: Date): Date {
const { year, month, date } = fechaArgentina(fecha);
// 8:30 Argentina + 3h = 11:30 UTC
return new Date(Date.UTC(year, month, date, INICIO_H - ARG_OFFSET_MIN / 60, INICIO_M, 0, 0));
}
function esDiaHabil(fecha: Date): boolean { function esDiaHabil(fecha: Date): boolean {
const dia = fecha.getDay(); const dia = diaArgentina(fecha);
return dia >= 1 && dia <= 5; return dia >= 1 && dia <= 5;
} }
function minutosDelDia(fecha: Date): number {
return fecha.getHours() * 60 + fecha.getMinutes();
}
function horasDentroDeJornada(inicio: Date, fin: Date): boolean { function horasDentroDeJornada(inicio: Date, fin: Date): boolean {
return ( const iMin = minutosArgentina(inicio);
minutosDelDia(inicio) >= INICIO_MINUTOS && const fMin = minutosArgentina(fin);
minutosDelDia(fin) <= FIN_MINUTOS && const iArg = fechaArgentina(inicio);
inicio.toDateString() === fin.toDateString() const fArg = fechaArgentina(fin);
); const mismodia =
iArg.year === fArg.year && iArg.month === fArg.month && iArg.date === fArg.date;
return mismodia && iMin >= INICIO_MINUTOS && fMin <= FIN_MINUTOS;
} }
async function turnosSolapados(inicio: Date, fin: Date): Promise<number> { async function turnosSolapados(inicio: Date, fin: Date): Promise<number> {
const count = await prisma.turno.count({ const count = await prisma.turno.count({
where: { where: {
estado: { in: ["PENDIENTE", "CONFIRMADO"] }, estado: { in: ["PENDIENTE", "CONFIRMADO"] },
AND: [ AND: [{ fechaInicio: { lt: fin } }, { fechaFin: { gt: inicio } }],
{ fechaInicio: { lt: fin } },
{ fechaFin: { gt: inicio } },
],
}, },
}); });
return count; return count;
@@ -51,33 +74,31 @@ export async function obtenerSlotsDisponibles(
const ahora = new Date(); const ahora = new Date();
const duracionMs = duracionHoras * 60 * 60 * 1000; const duracionMs = duracionHoras * 60 * 60 * 1000;
const fechaLimite = new Date(ahora); const fechaLimite = new Date(ahora.getTime() + diasAdelante * 24 * 60 * 60 * 1000);
fechaLimite.setDate(fechaLimite.getDate() + diasAdelante);
// Avanzar al próximo slot (:00 o :30 Argentina) a partir de ahora
let cursor = new Date(ahora); let cursor = new Date(ahora);
// Avanzar al siguiente slot de :00 o :30 const argMNow = minutosArgentina(cursor) % 60;
cursor.setMinutes(cursor.getMinutes() >= 30 ? 60 : 30, 0, 0); const avanzarMin = argMNow < 30 ? 30 - argMNow : 60 - argMNow;
cursor = new Date(cursor.getTime() + avanzarMin * 60 * 1000);
cursor.setUTCSeconds(0, 0);
while (cursor < fechaLimite && slots.length < 20) { while (cursor < fechaLimite && slots.length < 20) {
if (!esDiaHabil(cursor)) { if (!esDiaHabil(cursor)) {
cursor.setDate(cursor.getDate() + 1); cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000));
cursor.setHours(INICIO_H, INICIO_M, 0, 0);
continue; continue;
} }
if (minutosDelDia(cursor) < INICIO_MINUTOS) { const minActual = minutosArgentina(cursor);
cursor.setHours(INICIO_H, INICIO_M, 0, 0); if (minActual < INICIO_MINUTOS) {
cursor = inicioJornada(cursor);
continue; continue;
} }
const fin = new Date(cursor.getTime() + duracionMs); const fin = new Date(cursor.getTime() + duracionMs);
if ( if (!horasDentroDeJornada(cursor, fin)) {
fin.toDateString() !== cursor.toDateString() || cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000));
minutosDelDia(fin) > FIN_MINUTOS
) {
cursor.setDate(cursor.getDate() + 1);
cursor.setHours(INICIO_H, INICIO_M, 0, 0);
continue; continue;
} }