Files
NextChair/src/services/scheduling/booking-rule.service.ts
T
2026-09-05 09:32:05 +01:00

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