feat(booking): add validation to prevent past bookings and implement slot validation logic

This commit is contained in:
Jose Selesan
2026-06-08 15:46:17 -03:00
parent 49d2a13672
commit 630dedb507
4 changed files with 134 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import { expect, test } from 'bun:test';
import { isSlotInPast } from '@/lib/slot-validator';
/**
* Helper to create a bookingDate like parseIsoDate does (UTC midnight).
*/
function bookingDate(year: number, month: number, day: number): Date {
return new Date(Date.UTC(year, month - 1, day));
}
/**
* Helper to create a "now" date at a specific local time.
* Using local date constructor guarantees the date parts
* (getFullYear, getMonth, getDate) match the arguments
* regardless of the test runner's timezone.
*/
function localDate(year: number, month: number, day: number, hours: number, minutes: number): Date {
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
test('future date — always allowed', () => {
const future = bookingDate(2027, 6, 10); // 2027-06-10
const now = localDate(2026, 6, 8, 12, 0); // 2026-06-08 12:00
expect(isSlotInPast(future, '10:00', 60, now)).toBe(false);
});
test('past date — rejected', () => {
const past = bookingDate(2025, 6, 8); // 2025-06-08
const now = localDate(2026, 6, 8, 12, 0); // 2026-06-08 12:00
expect(isSlotInPast(past, '10:00', 60, now)).toBe(true);
});
test('today, slot not started yet — allowed', () => {
const today = bookingDate(2026, 6, 8); // 2026-06-08
const now = localDate(2026, 6, 8, 9, 0); // 09:00, slot empieza a las 10:00
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
});
test('today, slot in progress less than half elapsed — allowed', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 10, 10); // 10:10, slot 10:00-11:00 (60 min), 10 min elapsed
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
});
test('today, slot in progress more than half elapsed — rejected', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 10, 40); // 10:40, slot 10:00-11:00 (60 min), 40 min elapsed
expect(isSlotInPast(today, '10:00', 60, now)).toBe(true);
});
test('today, slot already ended — rejected', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 11, 1); // 11:01, slot 10:00-11:00 terminó
expect(isSlotInPast(today, '10:00', 60, now)).toBe(true);
});
test('today, exactly half elapsed — allowed (inclusive boundary)', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 10, 30); // 10:30, slot 10:00-11:00 (60 min), exactamente 30 min
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
});
test('today, slot starts exactly now — allowed', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 10, 0); // 10:00 exacto
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
});
test('90 min slot with 10 min elapsed — allowed', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 12, 10); // 12:10, slot 12:00-13:30, 10/90 elapsed
expect(isSlotInPast(today, '12:00', 90, now)).toBe(false);
});
test('90 min slot with 50 min elapsed — rejected', () => {
const today = bookingDate(2026, 6, 8);
const now = localDate(2026, 6, 8, 12, 50); // 12:50, slot 12:00-13:30, 50/90 elapsed
expect(isSlotInPast(today, '12:00', 90, now)).toBe(true);
});