Booking Engine
Architecture: Six focused services with the BookingService/SchedulingService overlap resolved by giving SchedulingService exactly one job, the transactional double-booking guard , while BookingService owns the create/reschedule/cancel workflow around it. 59 unit tests, 24 integration tests
This commit is contained in:
28 files changed
+4853
-15
No files matched your search
@@ -0,0 +1,49 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
import * as AppointmentService from "@/services/scheduling/appointment.service";
|
||||
|
||||
export async function getAppointmentAction(appointmentId: string) {
|
||||
return AppointmentService.getAppointmentById(prisma, appointmentId);
|
||||
}
|
||||
|
||||
export async function listAppointmentsAction(params: Parameters<typeof AppointmentService.listAppointments>[1]) {
|
||||
return AppointmentService.listAppointments(prisma, params);
|
||||
}
|
||||
|
||||
// Next's Server Actions compiler statically analyses "use server" files for
|
||||
// exported `async function` declarations — a `const` bound to a factory's
|
||||
// return value (however trivial) isn't recognized as a callable action, so
|
||||
// each one below is a real function declaration even though the bodies
|
||||
// are identical apart from which AppointmentService transition they call.
|
||||
|
||||
export async function checkInAction(appointmentId: string) {
|
||||
const result = await AppointmentService.checkIn(prisma, appointmentId);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function startAppointmentAction(appointmentId: string) {
|
||||
const result = await AppointmentService.start(prisma, appointmentId);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function completeAppointmentAction(appointmentId: string) {
|
||||
const result = await AppointmentService.complete(prisma, appointmentId);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function markNoShowAction(appointmentId: string) {
|
||||
const result = await AppointmentService.markNoShow(prisma, appointmentId);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function confirmAppointmentAction(appointmentId: string) {
|
||||
const result = await AppointmentService.confirm(prisma, appointmentId);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
import * as AvailabilityService from "@/services/scheduling/availability.service";
|
||||
import { fail } from "@/services/scheduling/types";
|
||||
import {
|
||||
getAvailableBarbersSchema,
|
||||
getAvailableDatesSchema,
|
||||
getAvailableServicesSchema,
|
||||
getAvailableTimeSlotsSchema,
|
||||
} from "../schemas";
|
||||
|
||||
/**
|
||||
* Thin wrappers over AvailabilityService — no business logic here, only
|
||||
* input parsing and the client↔server boundary. See
|
||||
* docs/implementation-plan.md §11 ("Server Actions... orchestrate the
|
||||
* appropriate domain services").
|
||||
*/
|
||||
|
||||
export async function getAvailableServicesAction(input: unknown) {
|
||||
const parsed = getAvailableServicesSchema.safeParse(input);
|
||||
if (!parsed.success) return { services: [], validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
const services = await AvailabilityService.getAvailableServices(prisma, parsed.data);
|
||||
return { services };
|
||||
}
|
||||
|
||||
export async function getAvailableBarbersAction(input: unknown) {
|
||||
const parsed = getAvailableBarbersSchema.safeParse(input);
|
||||
if (!parsed.success) return { staff: [], validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
const staff = await AvailabilityService.getAvailableStaff(prisma, parsed.data);
|
||||
return { staff };
|
||||
}
|
||||
|
||||
export async function getAvailableDatesAction(input: unknown) {
|
||||
const parsed = getAvailableDatesSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return { dates: [], validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
}
|
||||
return AvailabilityService.getAvailableDates(prisma, parsed.data);
|
||||
}
|
||||
|
||||
export async function getAvailableTimeSlotsAction(input: unknown) {
|
||||
const parsed = getAvailableTimeSlotsSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return { slots: [], validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
}
|
||||
return AvailabilityService.getAvailableTimeSlots(prisma, parsed.data);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
import * as BookingService from "@/services/scheduling/booking.service";
|
||||
import { fail } from "@/services/scheduling/types";
|
||||
import { cancelBookingSchema, createBookingSchema, rescheduleBookingSchema, updateBookingNotesSchema } from "../schemas";
|
||||
|
||||
export async function createBookingAction(input: unknown) {
|
||||
const parsed = createBookingSchema.safeParse(input);
|
||||
if (!parsed.success) return { success: false as const, validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
|
||||
const result = await BookingService.createBooking(prisma, {
|
||||
...parsed.data,
|
||||
startAt: new Date(parsed.data.startAt),
|
||||
});
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function rescheduleBookingAction(input: unknown) {
|
||||
const parsed = rescheduleBookingSchema.safeParse(input);
|
||||
if (!parsed.success) return { success: false as const, validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
|
||||
const result = await BookingService.rescheduleBooking(prisma, {
|
||||
...parsed.data,
|
||||
startAt: new Date(parsed.data.startAt),
|
||||
});
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function cancelBookingAction(input: unknown) {
|
||||
const parsed = cancelBookingSchema.safeParse(input);
|
||||
if (!parsed.success) return { success: false as const, validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
|
||||
const result = await BookingService.cancelBooking(prisma, parsed.data);
|
||||
if (result.success) revalidatePath("/dev/scheduling-test");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** "Update booking" — notes only. Time/staff changes go through reschedule (a distinct, re-validated workflow), not this. */
|
||||
export async function updateBookingNotesAction(input: unknown) {
|
||||
const parsed = updateBookingNotesSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return { success: false as const, validation: fail({ code: "INVALID_INPUT", message: parsed.error.message }) };
|
||||
}
|
||||
|
||||
const appointment = await prisma.appointment.update({
|
||||
where: { id: parsed.data.appointmentId },
|
||||
data: { notes: parsed.data.notes },
|
||||
include: { services: { include: { service: true } }, staff: { include: { user: true } }, customer: true },
|
||||
});
|
||||
revalidatePath("/dev/scheduling-test");
|
||||
return { success: true as const, appointment };
|
||||
}
|
||||
Reference in new issue
Block a user