fix(services): add double-booking check and schedule validation on create

Before creating a service, now:
1. Checks if the requested slot (day + range1_hour1) is already occupied by
   a non-cancelled/denied service for that professional — rejects with 400
2. Validates the requested day has an enabled schedule for the professional;
   rejects if the professional does not work that weekday
3. Validates the requested hour falls within the configured hour ranges
   (continuous day: range1_hour1→range2_hour2; split day: morning + afternoon
   blocks) — rejects if outside configured hours

day_of_week conversion: JS Date.getDay() (0=Sun) → DB convention (0=Mon)
via (getDay()+6)%7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-12 17:55:55 -05:00
co-authored by Claude Sonnet 4.6
parent 72777df52c
commit c8b2c0c901
+44
View File
@@ -36,6 +36,50 @@ export class ServicesService {
if (!prof) prof = await this.prisma.professionals.findUnique({ where: { user_id: data.professional_id } });
if (!prof || !prof.is_active) throw new BadRequestException('Profesional no disponible');
if (data.range1_hour1) {
const slotTime = new Date(`1970-01-01T${data.range1_hour1}:00`);
// Double-booking check: reject if this slot already has a non-cancelled/denied service
const conflict = await this.prisma.services.findFirst({
where: {
professional_id: prof.id,
day: new Date(data.day),
range1_hour1: slotTime,
status: { notIn: ['denied', 'cancelled'] },
},
});
if (conflict) throw new BadRequestException('Este horario ya está ocupado');
// Schedule validation: confirm the slot is within the professional's configured hours
// JS getDay() → 0=Sun…6=Sat; DB convention → 0=Mon…6=Sun, so: (getDay()+6)%7
const jsDay = new Date(data.day).getDay();
const dayOfWeek = (jsDay + 6) % 7;
const schedule = await this.prisma.schedules.findFirst({
where: { professional_id: prof.id, day_of_week: dayOfWeek, enabled: true },
});
if (!schedule) throw new BadRequestException('El profesional no atiende ese día');
const toMins = (d: Date | null) => d ? d.getUTCHours() * 60 + d.getUTCMinutes() : null;
const [sh, sm] = data.range1_hour1.split(':').map(Number);
const reqMins = sh * 60 + sm;
let inRange = false;
if (schedule.continuous_day) {
// Full day: range1_hour1 → range2_hour2
const start = toMins(schedule.range1_hour1);
const end = toMins(schedule.range2_hour2);
if (start !== null && end !== null) inRange = reqMins >= start && reqMins < end;
} else {
// Morning block: range1_hour1 → range1_hour2
const s1 = toMins(schedule.range1_hour1), e1 = toMins(schedule.range1_hour2);
if (s1 !== null && e1 !== null) inRange = inRange || (reqMins >= s1 && reqMins < e1);
// Afternoon block: range2_hour1 → range2_hour2
const s2 = toMins(schedule.range2_hour1), e2 = toMins(schedule.range2_hour2);
if (s2 !== null && e2 !== null) inRange = inRange || (reqMins >= s2 && reqMins < e2);
}
if (!inRange) throw new BadRequestException('La hora seleccionada está fuera del horario del profesional');
}
return this.prisma.services.create({
data: {
professional_id: prof.id,