booking flow, loader, email, admin cli
This commit is contained in:
106
scripts/book-on-behalf.ts
Normal file
106
scripts/book-on-behalf.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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";
|
||||
|
||||
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 });
|
||||
console.log(` Booking: ${hold.id} (CONFIRMED)`);
|
||||
console.log(` Email: ${result.status} (notification ${result.notificationId})`);
|
||||
console.log(` View: http://localhost:8025`);
|
||||
} finally {
|
||||
await db.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user