datewise/app/services/waitlist.server.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

50 lines
2.4 KiB
TypeScript
Raw 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.

/**
* When a place frees up (cancellation or reschedule), offer it to the first person
* waiting: reserve it as a hold for `waitlistOfferMinutes` and message them a link
* that adopts the hold in their cart.
*/
import { randomBytes } from "node:crypto";
import prisma from "../db.server";
import { capsFor, type Selection } from "../engine/capacity";
import { loadShopConfig } from "./config.server";
import { send } from "./notifications.server";
import { features } from "./plans";
import { ReservationService } from "./reservations.server";
const reservations = new ReservationService(prisma);
export async function offerFreedPlace(domain: string, freed: Selection, now = new Date()) {
const cfg = await loadShopConfig(domain);
if (!features(cfg.shop.plan).waitlist) return null;
const entry = await prisma.waitlistEntry.findFirst({
where: {
shopId: cfg.shop.id,
locationId: freed.locationId,
method: freed.method,
date: freed.date,
slot: { in: [freed.slot, "*"] },
offeredAt: null,
},
orderBy: { createdAt: "asc" },
});
const loc = cfg.locations.find((l) => l.id === freed.locationId);
if (!entry || !loc) return null;
const token = `wl_${randomBytes(16).toString("base64url")}`;
const caps = capsFor(loc, freed.method, freed.date, freed.slot, cfg.peakDays);
const held = await reservations.hold(cfg.shop.id, token, freed, caps, now, cfg.settings.waitlistOfferMinutes);
if (!held.ok) return null;
await prisma.waitlistEntry.update({ where: { id: entry.id }, data: { offeredAt: now, offerToken: token } });
const link = `https://${domain}/cart?datewise_offer=${token}`;
const nice = new Date(`${freed.date}T12:00:00Z`).toLocaleDateString("en-CA", { weekday: "long", month: "long", day: "numeric", timeZone: "UTC" });
const body = `Good news${entry.name ? `, ${entry.name}` : ""}! A ${freed.method} place opened on ${nice} (${freed.slot.replace("-", "–")}). We're holding it for you for ${cfg.settings.waitlistOfferMinutes} minutes: ${link}`;
const channel = entry.email ? "email" : features(cfg.shop.plan).smsWhatsapp ? "sms" : null;
if (channel)
await send(cfg.settings, domain.replace(".myshopify.com", ""), {
channel,
to: (entry.email ?? entry.phone)!,
subject: "A place opened up for you",
body,
}).catch((e) => console.error("waitlist notify failed", e));
return { entryId: entry.id, token };
}