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 { 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 }> { 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() }; }