import { DateTime } from "luxon"; /** * Resolve and validate the scheduledAt for `updateReminderAction`. * * Why this exists: the action is invoked from four edit forms * (account / message / groups / when). Three of those don't change * the time — they pass the original `scheduledAt` straight through. * For a paused/ended one-off whose original time is now in the past * (the operator paused it after a fire and is editing the message), * a strict "must be future" check rejects the edit even though the * operator only wanted to fix a typo. So: * * - Reject malformed ISO outright. * - Allow past timestamps when: * * the reminder's existing status is `paused` or `ended` — * it won't fire in those states regardless, AND/OR * * the submitted timestamp matches the existing one (within * a second of rounding), i.e. the form is passing it through * unchanged. * - Otherwise reject past timestamps so an active reminder can't * be scheduled into a moment that's already gone. * * Pure function — takes `now` as input so tests can pin the clock. */ export function validateUpdateScheduledAt(args: { iso: string; timezone: string; existingStatus: string; existingScheduledAt: Date | null; now: Date; }): { ok: true; scheduledAt: Date } | { ok: false; error: string } { const dt = DateTime.fromISO(args.iso, { zone: args.timezone }).toJSDate(); if (Number.isNaN(dt.getTime())) { return { ok: false, error: "Invalid date" }; } const isPaused = args.existingStatus === "paused" || args.existingStatus === "inactive"; const sameAsExisting = args.existingScheduledAt !== null && Math.abs(args.existingScheduledAt.getTime() - dt.getTime()) < 1000; if (dt.getTime() <= args.now.getTime() && !isPaused && !sameAsExisting) { return { ok: false, error: "Time is in the past" }; } return { ok: true, scheduledAt: dt }; }