/** * Customer-facing post-purchase features: self-reschedule eligibility and reminders. */ import type { Booking, ReminderPolicy, ShopSettings } from "./types"; import { zonedToUtc } from "./time"; export function slotStartMs(b: Booking): number { return zonedToUtc(b.date, b.slotStart ?? "00:00", b.timezone); } export function canCustomerReschedule( b: Booking, policy: ShopSettings["reschedule"], now: number, ): { ok: true; deadline: number } | { ok: false; reason: string } { if (!policy.enabled) return { ok: false, reason: "Rescheduling isn't available for this store. Please contact us." }; if (b.status !== "confirmed") return { ok: false, reason: "This booking can't be changed." }; if ((b.rescheduleCount ?? 0) >= policy.maxChanges) return { ok: false, reason: `You've already changed this booking ${policy.maxChanges} time(s). Please contact us.` }; const deadline = slotStartMs(b) - policy.minHoursBefore * 3600000; if (now >= deadline) return { ok: false, reason: `Changes are allowed up to ${policy.minHoursBefore} hours before your time slot.` }; return { ok: true, deadline }; } export interface PlannedReminder { policyId: string; channel: ReminderPolicy["channel"]; sendAt: number; body: string; } export function planReminders( b: Booking, policies: ReminderPolicy[], now: number, vars: Record = {}, ): PlannedReminder[] { const start = slotStartMs(b); return policies .filter((p) => p.enabled && (!p.methods?.length || p.methods.includes(b.method))) .map((p) => ({ policyId: p.id, channel: p.channel, sendAt: start - p.minutesBefore * 60000, body: renderTemplate(p.template, { name: b.customer?.name ?? "there", order: b.orderName ?? "", date: b.date, time: b.slotStart && b.slotEnd ? `${b.slotStart}–${b.slotEnd}` : "", method: b.method, ...vars, }), })) .filter((r) => r.sendAt > now) .filter((r) => { // Respect channel requirements. if (r.channel === "email") return !!b.customer?.email; return !!b.customer?.phone; }); } /** {{var}} substitution; unknown vars render empty. */ export function renderTemplate(tpl: string, vars: Record): string { return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => vars[k] ?? ""); }