From f4f7d95485c15efbe7a4b0532c116e6a57799a60 Mon Sep 17 00:00:00 2001 From: brobert Date: Sun, 2 Nov 2025 11:35:55 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20centralizar=20formateo=20UTC=20con=20ut?= =?UTF-8?q?il=20can=C3=B3nica=20y=20adaptar=20web/ICS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- apps/web/src/hooks.server.ts | 7 ++--- apps/web/src/lib/server/calendar-tokens.ts | 9 ++---- apps/web/src/lib/server/datetime.ts | 28 +++++++++++++++++++ apps/web/src/lib/server/dev-seed.ts | 14 ++++------ .../routes/api/tasks/[id]/claim/+server.ts | 4 --- .../routes/api/tasks/[id]/complete/+server.ts | 5 ++-- .../api/tasks/[id]/uncomplete/+server.ts | 7 ++--- .../ics/aggregate/[token].ics/+server.ts | 22 ++------------- .../routes/ics/group/[token].ics/+server.ts | 22 ++------------- .../ics/personal/[token].ics/+server.ts | 22 ++------------- apps/web/src/routes/login/+server.ts | 5 +--- src/db/migrator.ts | 3 +- src/services/commands/handlers/web.ts | 6 ++-- src/services/group-sync.ts | 5 ++-- src/services/maintenance.ts | 9 ++---- src/services/response-queue.ts | 5 ++-- src/utils/datetime.ts | 3 ++ 17 files changed, 71 insertions(+), 105 deletions(-) create mode 100644 apps/web/src/lib/server/datetime.ts create mode 100644 src/utils/datetime.ts diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index a282ddd..bbe50b4 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -2,10 +2,7 @@ import type { Handle } from '@sveltejs/kit'; import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { isProd, sessionIdleTtlMs, isDev, DEV_BYPASS_AUTH, DEV_DEFAULT_USER } from '$lib/server/env'; - -function toIsoSql(d: Date): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} +import { toIsoSqlUTC } from '$lib/server/datetime'; export const handle: Handle = async ({ event, resolve }) => { // Bypass de auth en desarrollo (siempre activo en dev para entorno de pruebas) @@ -47,7 +44,7 @@ export const handle: Handle = async ({ event, resolve }) => { event.locals.userId = row.user_id; // Renovar expiración por inactividad y last_seen_at - const newExpIso = toIsoSql(new Date(Date.now() + sessionIdleTtlMs)); + const newExpIso = toIsoSqlUTC(new Date(Date.now() + sessionIdleTtlMs)); try { db.prepare( `UPDATE web_sessions diff --git a/apps/web/src/lib/server/calendar-tokens.ts b/apps/web/src/lib/server/calendar-tokens.ts index 6a7282b..63c4cf5 100644 --- a/apps/web/src/lib/server/calendar-tokens.ts +++ b/apps/web/src/lib/server/calendar-tokens.ts @@ -1,13 +1,10 @@ import { getDb } from './db'; import { randomTokenBase64Url, sha256Hex } from './crypto'; import { WEB_BASE_URL } from './env'; +import { toIsoSqlUTC } from './datetime'; export type CalendarTokenType = 'personal' | 'group' | 'aggregate'; -function toIsoSql(d: Date = new Date()): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} - function requireBaseUrl(): string { const base = (WEB_BASE_URL || '').trim(); if (!base) { @@ -73,7 +70,7 @@ export async function createCalendarTokenUrl( const token = randomTokenBase64Url(32); const tokenHash = await sha256Hex(token); - const createdAt = toIsoSql(new Date()); + const createdAt = toIsoSqlUTC(new Date()); const insert = db.prepare(` INSERT INTO calendar_tokens (type, user_id, group_id, token_hash, token_plain, created_at) @@ -94,7 +91,7 @@ export async function rotateCalendarTokenUrl( groupId?: string | null ): Promise<{ url: string; token: string; id: number; revoked: number | null }> { const db = await getDb(); - const now = toIsoSql(new Date()); + const now = toIsoSqlUTC(new Date()); const existing = await findActiveToken(type, userId, groupId ?? null); let revoked: number | null = null; diff --git a/apps/web/src/lib/server/datetime.ts b/apps/web/src/lib/server/datetime.ts new file mode 100644 index 0000000..765f3fa --- /dev/null +++ b/apps/web/src/lib/server/datetime.ts @@ -0,0 +1,28 @@ +import { toIsoSqlUTC as coreToIsoSqlUTC } from '../../../../src/utils/datetime'; + +/** + * Serializa una fecha en UTC al formato SQL ISO "YYYY-MM-DD HH:MM:SS[.SSS]". + * Mantiene exactamente la semántica previa basada en toISOString().replace(). + */ +export function toIsoSqlUTC(d: Date = new Date()): string { + return coreToIsoSqlUTC(d); +} + +/** + * Devuelve YYYY-MM-DD en UTC (útil para consultas por rango de fecha). + */ +export function ymdUTC(date: Date): string { + const yyyy = String(date.getUTCFullYear()).padStart(4, '0'); + const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); + const dd = String(date.getUTCDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} + +/** + * Suma meses en UTC preservando día (ajustado por desbordes de fin de mes por el propio motor). + */ +export function addMonthsUTC(date: Date, months: number): Date { + const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + d.setUTCMonth(d.getUTCMonth() + months); + return d; +} diff --git a/apps/web/src/lib/server/dev-seed.ts b/apps/web/src/lib/server/dev-seed.ts index 43965a9..a364f3f 100644 --- a/apps/web/src/lib/server/dev-seed.ts +++ b/apps/web/src/lib/server/dev-seed.ts @@ -12,9 +12,7 @@ function toIsoYmd(d: Date): string { function addDays(base: Date, days: number): Date { return new Date(base.getTime() + days * 24 * 3600 * 1000); } -function isoSql(dt: Date): string { - return dt.toISOString().replace('T', ' ').replace('Z', ''); -} +import { toIsoSqlUTC } from './datetime'; export async function seedDev(db: any, defaultUser: string): Promise { try { db.exec(`PRAGMA foreign_keys = ON;`); } catch {} @@ -108,11 +106,11 @@ export async function seedDev(db: any, defaultUser: string): Promise { `); // Helpers para completadas - const completedRecent = isoSql(addDays(now, 0)); // ahora - const completed2hAgo = isoSql(new Date(Date.now() - 2 * 3600 * 1000)); - const completed12hAgo = isoSql(new Date(Date.now() - 12 * 3600 * 1000)); - const completed48hAgo = isoSql(new Date(Date.now() - 48 * 3600 * 1000)); - const completed72hAgo = isoSql(new Date(Date.now() - 72 * 3600 * 1000)); + const completedRecent = toIsoSqlUTC(addDays(now, 0)); // ahora + const completed2hAgo = toIsoSqlUTC(new Date(Date.now() - 2 * 3600 * 1000)); + const completed12hAgo = toIsoSqlUTC(new Date(Date.now() - 12 * 3600 * 1000)); + const completed48hAgo = toIsoSqlUTC(new Date(Date.now() - 48 * 3600 * 1000)); + const completed72hAgo = toIsoSqlUTC(new Date(Date.now() - 72 * 3600 * 1000)); type Spec = { desc: string; diff --git a/apps/web/src/routes/api/tasks/[id]/claim/+server.ts b/apps/web/src/routes/api/tasks/[id]/claim/+server.ts index 06996e0..84e6bd6 100644 --- a/apps/web/src/routes/api/tasks/[id]/claim/+server.ts +++ b/apps/web/src/routes/api/tasks/[id]/claim/+server.ts @@ -1,10 +1,6 @@ import type { RequestHandler } from './$types'; import { getDb } from '$lib/server/db'; -function toIsoSql(d: Date): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} - export const POST: RequestHandler = async (event) => { const userId = event.locals.userId ?? null; if (!userId) { diff --git a/apps/web/src/routes/api/tasks/[id]/complete/+server.ts b/apps/web/src/routes/api/tasks/[id]/complete/+server.ts index 6ab6da3..cb63441 100644 --- a/apps/web/src/routes/api/tasks/[id]/complete/+server.ts +++ b/apps/web/src/routes/api/tasks/[id]/complete/+server.ts @@ -1,6 +1,7 @@ import type { RequestHandler } from './$types'; import { getDb } from '$lib/server/db'; import { REACTIONS_ENABLED, REACTIONS_TTL_DAYS, REACTIONS_SCOPE, GROUP_GATING_MODE } from '$lib/server/env'; +import { toIsoSqlUTC } from '$lib/server/datetime'; export const POST: RequestHandler = async (event) => { const userId = event.locals.userId ?? null; @@ -142,8 +143,8 @@ export const POST: RequestHandler = async (event) => { if (withinTtl && allowed) { // Idempotencia 24h por metadata canónica exacta - const nowIso = new Date().toISOString().replace('T', ' ').replace('Z', ''); - const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').replace('Z', ''); + const nowIso = toIsoSqlUTC(new Date()); + const cutoff = toIsoSqlUTC(new Date(Date.now() - 24 * 60 * 60 * 1000)); const meta: any = { kind: 'reaction', emoji: '✅', chatId, messageId: String(origin.message_id) }; if (origin && (origin.from_me === 1 || origin.from_me === true)) meta.fromMe = true; diff --git a/apps/web/src/routes/api/tasks/[id]/uncomplete/+server.ts b/apps/web/src/routes/api/tasks/[id]/uncomplete/+server.ts index 7c4fec5..7a3f9a1 100644 --- a/apps/web/src/routes/api/tasks/[id]/uncomplete/+server.ts +++ b/apps/web/src/routes/api/tasks/[id]/uncomplete/+server.ts @@ -1,10 +1,7 @@ import type { RequestHandler } from './$types'; import { getDb } from '$lib/server/db'; import { UNCOMPLETE_WINDOW_MIN } from '$lib/server/env'; - -function toIsoSql(d: Date): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} +import { toIsoSqlUTC } from '$lib/server/datetime'; export const POST: RequestHandler = async (event) => { const userId = event.locals.userId ?? null; @@ -79,7 +76,7 @@ export const POST: RequestHandler = async (event) => { if (!task.completed_at) { return new Response('Forbidden', { status: 403 }); } - const cutoff = toIsoSql(new Date(Date.now() - UNCOMPLETE_WINDOW_MIN * 60 * 1000)); + const cutoff = toIsoSqlUTC(new Date(Date.now() - UNCOMPLETE_WINDOW_MIN * 60 * 1000)); if (String(task.completed_at) < String(cutoff)) { return new Response('Forbidden', { status: 403 }); } diff --git a/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts b/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts index cbab64d..a5d45e3 100644 --- a/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts @@ -3,23 +3,7 @@ import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { icsHorizonMonths } from '$lib/server/env'; import { buildIcsCalendar } from '$lib/server/ics'; - -function toIsoSql(d = new Date()): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} - -function ymdUTC(date: Date): string { - const yyyy = String(date.getUTCFullYear()).padStart(4, '0'); - const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); - const dd = String(date.getUTCDate()).padStart(2, '0'); - return `${yyyy}-${mm}-${dd}`; -} - -function addMonthsUTC(date: Date, months: number): Date { - const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); - d.setUTCMonth(d.getUTCMonth() + months); - return d; -} +import { toIsoSqlUTC, ymdUTC, addMonthsUTC } from '$lib/server/datetime'; export const GET: RequestHandler = async ({ params, request }) => { const db = await getDb(); @@ -73,11 +57,11 @@ export const GET: RequestHandler = async ({ params, request }) => { // 304 si ETag coincide const inm = request.headers.get('if-none-match'); if (inm && inm === etag) { - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response(null, { status: 304, headers: { ETag: etag, 'Cache-Control': 'public, max-age=300' } }); } - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response(body, { status: 200, diff --git a/apps/web/src/routes/ics/group/[token].ics/+server.ts b/apps/web/src/routes/ics/group/[token].ics/+server.ts index 4c2b78b..d66ec4f 100644 --- a/apps/web/src/routes/ics/group/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/group/[token].ics/+server.ts @@ -3,23 +3,7 @@ import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { icsHorizonMonths } from '$lib/server/env'; import { buildIcsCalendar } from '$lib/server/ics'; - -function toIsoSql(d = new Date()): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} - -function ymdUTC(date: Date): string { - const yyyy = String(date.getUTCFullYear()).padStart(4, '0'); - const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); - const dd = String(date.getUTCDate()).padStart(2, '0'); - return `${yyyy}-${mm}-${dd}`; -} - -function addMonthsUTC(date: Date, months: number): Date { - const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); - d.setUTCMonth(d.getUTCMonth() + months); - return d; -} +import { toIsoSqlUTC, ymdUTC, addMonthsUTC } from '$lib/server/datetime'; export const GET: RequestHandler = async ({ params, request }) => { const db = await getDb(); @@ -45,7 +29,7 @@ export const GET: RequestHandler = async ({ params, request }) => { .prepare(`SELECT COALESCE(active,1) as active, COALESCE(archived,0) as archived, COALESCE(is_community,0) as is_community FROM groups WHERE id = ?`) .get(row.group_id) as any; if (!gRow || Number(gRow.active || 0) !== 1 || Number(gRow.archived || 0) === 1 || Number(gRow.is_community || 0) === 1) { - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response('Gone', { status: 410 }); } @@ -85,7 +69,7 @@ export const GET: RequestHandler = async ({ params, request }) => { return new Response(null, { status: 304, headers: { ETag: etag, 'Cache-Control': 'public, max-age=300' } }); } - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response(body, { status: 200, diff --git a/apps/web/src/routes/ics/personal/[token].ics/+server.ts b/apps/web/src/routes/ics/personal/[token].ics/+server.ts index 539f99e..3507d41 100644 --- a/apps/web/src/routes/ics/personal/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/personal/[token].ics/+server.ts @@ -3,23 +3,7 @@ import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { icsHorizonMonths } from '$lib/server/env'; import { buildIcsCalendar } from '$lib/server/ics'; - -function toIsoSql(d = new Date()): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} - -function ymdUTC(date: Date): string { - const yyyy = String(date.getUTCFullYear()).padStart(4, '0'); - const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); - const dd = String(date.getUTCDate()).padStart(2, '0'); - return `${yyyy}-${mm}-${dd}`; -} - -function addMonthsUTC(date: Date, months: number): Date { - const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); - d.setUTCMonth(d.getUTCMonth() + months); - return d; -} +import { toIsoSqlUTC, ymdUTC, addMonthsUTC } from '$lib/server/datetime'; export const GET: RequestHandler = async ({ params, request }) => { const db = await getDb(); @@ -74,11 +58,11 @@ export const GET: RequestHandler = async ({ params, request }) => { // 304 si ETag coincide const inm = request.headers.get('if-none-match'); if (inm && inm === etag) { - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response(null, { status: 304, headers: { ETag: etag, 'Cache-Control': 'public, max-age=300' } }); } - db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); + db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSqlUTC(), row.id); return new Response(body, { status: 200, diff --git a/apps/web/src/routes/login/+server.ts b/apps/web/src/routes/login/+server.ts index 63f7e27..89c6588 100644 --- a/apps/web/src/routes/login/+server.ts +++ b/apps/web/src/routes/login/+server.ts @@ -4,9 +4,6 @@ import { getDb } from '$lib/server/db'; import { sha256Hex, randomTokenBase64Url } from '$lib/server/crypto'; import { sessionIdleTtlMs, isProd, isDev, DEV_BYPASS_AUTH } from '$lib/server/env'; -function toIsoSql(d: Date): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} function escapeHtml(s: string): string { return s @@ -154,7 +151,7 @@ export const POST: RequestHandler = async (event) => { const sessionToken = randomTokenBase64Url(32); const sessionHash = await sha256Hex(sessionToken); const sessionId = randomTokenBase64Url(16); - const expiresAtIso = toIsoSql(new Date(Date.now() + sessionIdleTtlMs)); + const expiresAtIso = toIsoSqlUTC(new Date(Date.now() + sessionIdleTtlMs)); // Datos de agente e IP (best-effort) const userAgent = event.request.headers.get('user-agent') || null; diff --git a/src/db/migrator.ts b/src/db/migrator.ts index f4dfabc..513aabc 100644 --- a/src/db/migrator.ts +++ b/src/db/migrator.ts @@ -2,12 +2,13 @@ import type { Database } from 'bun:sqlite'; import { mkdirSync, appendFileSync } from 'fs'; import { join } from 'path'; import { migrations, type Migration } from './migrations'; +import { toIsoSqlUTC } from '../utils/datetime'; const MIGRATIONS_LOG_LEVEL = (process.env.MIGRATIONS_LOG_LEVEL || '').toLowerCase(); const MIGRATIONS_QUIET = process.env.NODE_ENV === 'test' || MIGRATIONS_LOG_LEVEL === 'silent'; function nowIso(): string { - return new Date().toISOString().replace('T', ' ').replace('Z', ''); + return toIsoSqlUTC(new Date()); } function logEvent(level: 'info' | 'error', event: string, data: any = {}) { diff --git a/src/services/commands/handlers/web.ts b/src/services/commands/handlers/web.ts index d636550..7145c5f 100644 --- a/src/services/commands/handlers/web.ts +++ b/src/services/commands/handlers/web.ts @@ -3,6 +3,7 @@ import { ensureUserExists } from '../../../db'; import { isGroupId } from '../../../utils/whatsapp'; import { randomTokenBase64Url, sha256Hex } from '../../../utils/crypto'; import { Metrics } from '../../metrics'; +import { toIsoSqlUTC } from '../../../utils/datetime'; type Ctx = { sender: string; @@ -38,10 +39,9 @@ export async function handleWeb(context: Ctx, deps: { db: Database }): Promise d.toISOString().replace('T', ' ').replace('Z', ''); const now = new Date(); - const nowIso = toIso(now); - const expiresIso = toIso(new Date(now.getTime() + 10 * 60 * 1000)); // 10 minutos + const nowIso = toIsoSqlUTC(now); + const expiresIso = toIsoSqlUTC(new Date(now.getTime() + 10 * 60 * 1000)); // 10 minutos // Invalidar tokens vigentes (uso único) deps.db.prepare(` diff --git a/src/services/group-sync.ts b/src/services/group-sync.ts index 766f495..f95a637 100644 --- a/src/services/group-sync.ts +++ b/src/services/group-sync.ts @@ -5,6 +5,7 @@ import { Metrics } from './metrics'; import { IdentityService } from './identity'; import { AllowedGroups } from './allowed-groups'; import { ResponseQueue } from './response-queue'; +import { toIsoSqlUTC } from '../utils/datetime'; // In-memory cache for active groups // const activeGroupsCache = new Map(); // groupId -> groupName @@ -689,7 +690,7 @@ export class GroupSyncService { */ static upsertMemberSeen(groupId: string, userId: string, nowIso?: string): void { if (!groupId || !userId) return; - const now = nowIso || new Date().toISOString().replace('T', ' ').replace('Z', ''); + const now = nowIso || toIsoSqlUTC(new Date()); this.dbInstance.prepare(` INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES (?, ?, 0, 1, ?, ?) @@ -707,7 +708,7 @@ export class GroupSyncService { if (!groupId || !Array.isArray(snapshot)) { throw new Error('Invalid arguments for reconcileGroupMembers'); } - const now = nowIso || new Date().toISOString().replace('T', ' ').replace('Z', ''); + const now = nowIso || toIsoSqlUTC(new Date()); let added = 0, updated = 0, deactivated = 0; // Build quick lookup from snapshot diff --git a/src/services/maintenance.ts b/src/services/maintenance.ts index 965e8c4..df09850 100644 --- a/src/services/maintenance.ts +++ b/src/services/maintenance.ts @@ -1,9 +1,6 @@ import type { Database } from 'bun:sqlite'; import { db } from '../db'; - -function toIsoSql(d: Date): string { - return d.toISOString().replace('T', ' ').replace('Z', ''); -} +import { toIsoSqlUTC } from '../utils/datetime'; export class MaintenanceService { private static _timer: any = null; @@ -38,7 +35,7 @@ export class MaintenanceService { static async cleanupInactiveMembersOnce(instance: Database = db, retentionDays: number = this.retentionDays): Promise { if (retentionDays <= 0) return 0; - const threshold = toIsoSql(new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)); + const threshold = toIsoSqlUTC(new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)); const res = instance.prepare(` DELETE FROM group_members WHERE is_active = 0 @@ -63,7 +60,7 @@ export class MaintenanceService { const real = String(r.user_id); instance.transaction(() => { - const nowIso = toIsoSql(new Date()); + const nowIso = toIsoSqlUTC(new Date()); // Asegurar existencia del usuario real try { instance.prepare(`INSERT OR IGNORE INTO users (id, created_at, updated_at) VALUES (?, ?, ?)`) diff --git a/src/services/response-queue.ts b/src/services/response-queue.ts index b9a3e21..0e58a7d 100644 --- a/src/services/response-queue.ts +++ b/src/services/response-queue.ts @@ -3,6 +3,7 @@ import { db } from '../db'; import { IdentityService } from './identity'; import { normalizeWhatsAppId } from '../utils/whatsapp'; import { Metrics } from './metrics'; +import { toIsoSqlUTC } from '../utils/datetime'; const MAX_FALLBACK_DIGITS = (() => { const raw = (process.env.ONBOARDING_FALLBACK_MAX_DIGITS || '').trim(); @@ -60,11 +61,11 @@ export const ResponseQueue = { _cleanupRunCount: 0, nowIso(): string { - return new Date().toISOString().replace('T', ' ').replace('Z', ''); + return toIsoSqlUTC(new Date()); }, futureIso(ms: number): string { - return new Date(Date.now() + ms).toISOString().replace('T', ' ').replace('Z', ''); + return toIsoSqlUTC(new Date(Date.now() + ms)); }, computeDelayMs(attempt: number): number { diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts new file mode 100644 index 0000000..c3d9a9b --- /dev/null +++ b/src/utils/datetime.ts @@ -0,0 +1,3 @@ +export function toIsoSqlUTC(d: Date = new Date()): string { + return d.toISOString().replace('T', ' ').replace('Z', ''); +}