50 lines
2.4 KiB
TypeScript
50 lines
2.4 KiB
TypeScript
/**
|
||
* 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 };
|
||
}
|