/** * 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 }; }