73 lines
3.5 KiB
TypeScript
73 lines
3.5 KiB
TypeScript
import type { PrismaClient } from "@/generated/prisma/client";
|
|
import { fail, ok } from "@/lib/validation-result";
|
|
import type { ValidationResult } from "@/lib/validation-result";
|
|
import type { BookingRuleConfig } from "./business-rules.service";
|
|
|
|
/**
|
|
* Read/write for the BookingRule row itself — deliberately separate from
|
|
* business-rules.service.ts, which is pure evaluation logic with no Prisma
|
|
* access by design (see that file's own header). This file owns the one
|
|
* thing that one doesn't: loading and validating the actual configuration
|
|
* an owner edits, which every booking-time check then reads via
|
|
* AvailabilityService exactly as before — nothing about how rules are
|
|
* *evaluated* changes here, only how they're *set*.
|
|
*/
|
|
|
|
export type BookingRuleErrorCode = "INVALID_INPUT";
|
|
|
|
export type BookingRuleInput = BookingRuleConfig;
|
|
|
|
export function validateBookingRuleInput(input: BookingRuleInput): ValidationResult<BookingRuleErrorCode> {
|
|
if (!Number.isInteger(input.minNoticeMinutes) || input.minNoticeMinutes < 0) {
|
|
return fail({ code: "INVALID_INPUT", message: "Minimum notice can't be negative." });
|
|
}
|
|
if (!Number.isInteger(input.maxAdvanceDays) || input.maxAdvanceDays < 1) {
|
|
return fail({ code: "INVALID_INPUT", message: "Maximum advance booking must be at least 1 day." });
|
|
}
|
|
if (!Number.isInteger(input.cancellationWindowHours) || input.cancellationWindowHours < 0) {
|
|
return fail({ code: "INVALID_INPUT", message: "Cancellation window can't be negative." });
|
|
}
|
|
if (!Number.isInteger(input.rescheduleWindowHours) || input.rescheduleWindowHours < 0) {
|
|
return fail({ code: "INVALID_INPUT", message: "Reschedule window can't be negative." });
|
|
}
|
|
if (!Number.isInteger(input.slotIntervalMinutes) || input.slotIntervalMinutes < 5 || input.slotIntervalMinutes > 120) {
|
|
return fail({ code: "INVALID_INPUT", message: "Slot interval must be between 5 and 120 minutes." });
|
|
}
|
|
if (!Number.isInteger(input.bufferBeforeMinutes) || input.bufferBeforeMinutes < 0) {
|
|
return fail({ code: "INVALID_INPUT", message: "Buffer before can't be negative." });
|
|
}
|
|
if (!Number.isInteger(input.bufferAfterMinutes) || input.bufferAfterMinutes < 0) {
|
|
return fail({ code: "INVALID_INPUT", message: "Buffer after can't be negative." });
|
|
}
|
|
if (input.maxAppointmentsPerDay !== null && (!Number.isInteger(input.maxAppointmentsPerDay) || input.maxAppointmentsPerDay < 1)) {
|
|
return fail({ code: "INVALID_INPUT", message: "Max appointments per day must be at least 1, or left unlimited." });
|
|
}
|
|
if (
|
|
input.maxAppointmentsPerStaffPerDay !== null &&
|
|
(!Number.isInteger(input.maxAppointmentsPerStaffPerDay) || input.maxAppointmentsPerStaffPerDay < 1)
|
|
) {
|
|
return fail({ code: "INVALID_INPUT", message: "Max appointments per staff member per day must be at least 1, or left unlimited." });
|
|
}
|
|
return ok();
|
|
}
|
|
|
|
export async function getBookingRule(prisma: PrismaClient, businessId: string) {
|
|
return prisma.bookingRule.findUnique({ where: { businessId } });
|
|
}
|
|
|
|
export async function updateBookingRule(
|
|
prisma: PrismaClient,
|
|
params: { businessId: string; input: BookingRuleInput },
|
|
): Promise<{ success: boolean; validation: ValidationResult<BookingRuleErrorCode> }> {
|
|
const validation = validateBookingRuleInput(params.input);
|
|
if (!validation.valid) return { success: false, validation };
|
|
|
|
await prisma.bookingRule.upsert({
|
|
where: { businessId: params.businessId },
|
|
create: { businessId: params.businessId, ...params.input },
|
|
update: { ...params.input },
|
|
});
|
|
|
|
return { success: true, validation: ok() };
|
|
}
|