112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
// Admin smoke-test: take a booking on behalf of a customer end-to-end.
|
|
//
|
|
// Usage:
|
|
// pnpm tsx scripts/book-on-behalf.ts <customerEmail> <serviceName> <localISO>
|
|
//
|
|
// Example:
|
|
// pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00
|
|
//
|
|
// Walks: lookup → loadAvailabilityState → findSlots → pick first candidate
|
|
// pair → createHold → confirmHold → sendBookingConfirmation.
|
|
|
|
import "dotenv/config";
|
|
import { fromZonedTime } from "date-fns-tz";
|
|
import { addMinutes } from "date-fns";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
import { PrismaClient } from "../src/generated/prisma/client";
|
|
import { findSlots } from "../src/lib/availability";
|
|
import { loadAvailabilityState } from "../src/lib/availability-loader";
|
|
import { confirmHold, createHold } from "../src/lib/booking";
|
|
import { sendBookingConfirmation } from "../src/lib/email";
|
|
import { scheduleReminderForBooking } from "../src/lib/reminders";
|
|
import { stopJobs } from "../src/lib/jobs";
|
|
|
|
async function main() {
|
|
const [customerEmail, serviceName, localIso] = process.argv.slice(2);
|
|
if (!customerEmail || !serviceName || !localIso) {
|
|
console.error(
|
|
"usage: pnpm tsx scripts/book-on-behalf.ts <customerEmail> <serviceName> <localISO>",
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
const tz = process.env.APP_TZ ?? "America/Detroit";
|
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
const db = new PrismaClient({ adapter });
|
|
|
|
try {
|
|
const customer = await db.user.findUnique({
|
|
where: { email: customerEmail },
|
|
include: { customer: true },
|
|
});
|
|
if (!customer || !customer.customer) {
|
|
throw new Error(`No customer found for email: ${customerEmail}`);
|
|
}
|
|
|
|
const service = await db.service.findFirst({
|
|
where: { name: serviceName, active: true },
|
|
});
|
|
if (!service) throw new Error(`No active service named: ${serviceName}`);
|
|
|
|
const startsAt = fromZonedTime(localIso, tz);
|
|
const window = {
|
|
from: startsAt,
|
|
to: addMinutes(startsAt, service.durationMin + 1),
|
|
};
|
|
|
|
const state = await loadAvailabilityState(db, {
|
|
from: window.from,
|
|
to: window.to,
|
|
serviceId: service.id,
|
|
});
|
|
if (!state) throw new Error("Could not load availability state");
|
|
|
|
const slots = findSlots({
|
|
service: state.service,
|
|
therapists: state.therapists,
|
|
rooms: state.rooms,
|
|
practiceTz: tz,
|
|
from: window.from,
|
|
to: window.to,
|
|
});
|
|
const slot = slots.find((s) => s.startsAt.getTime() === startsAt.getTime());
|
|
if (!slot) {
|
|
console.error(`Requested slot ${localIso} (${startsAt.toISOString()}) is not available.`);
|
|
console.error(`Found ${slots.length} slot(s) in window:`);
|
|
for (const s of slots) console.error(` - ${s.startsAt.toISOString()}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const therapistId = slot.candidateTherapistIds[0];
|
|
const roomId = slot.candidateRoomIds[0];
|
|
console.log(`Booking ${service.name} for ${customer.name}:`);
|
|
console.log(` When: ${startsAt.toISOString()} (${localIso} ${tz})`);
|
|
console.log(` Therapist: ${therapistId}`);
|
|
console.log(` Room: ${roomId}`);
|
|
|
|
const hold = await createHold(db, {
|
|
customerId: customer.id,
|
|
serviceId: service.id,
|
|
therapistId,
|
|
roomId,
|
|
startsAt,
|
|
});
|
|
await confirmHold(db, hold.id);
|
|
|
|
const result = await sendBookingConfirmation({ db, bookingId: hold.id });
|
|
await scheduleReminderForBooking(hold.id, startsAt);
|
|
console.log(` Booking: ${hold.id} (CONFIRMED)`);
|
|
console.log(` Email: ${result.status} (notification ${result.notificationId})`);
|
|
console.log(` Reminder: scheduled for 24h before`);
|
|
console.log(` View: http://localhost:8025`);
|
|
} finally {
|
|
await stopJobs();
|
|
await db.$disconnect();
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|