datewise/app/engine/customer.ts
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

67 lines
2.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, string> = {},
): 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, string>): string {
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => vars[k] ?? "");
}