Files
braidsbymira/supabase/migrations/20260812210641_duration_aware_booked_slots.sql
belviskhoremk 43f4f5f46e fix: block duration-aware slot overlaps, not just exact time matches
booked_slots previously recorded only (date, time), so availability
checking excluded an exact time match but not a real overlap — a
4-hour service booked at 9:00 AM didn't block 11:00 AM or 1:00 PM.
Add duration_minutes to booked_slots (populated by sync_booked_slot()
from the booked service's own duration) and switch client-side
computeAvailableSlots to a half-open-interval overlap check, also
rejecting candidates that wouldn't finish before closing time.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 23:15:05 +02:00

85 lines
3.8 KiB
PL/PgSQL

-- Follow-up to Phase 2C's final whole-branch review: booked_slots only
-- recorded (date, time), so availability checking only ever excluded an
-- exact time match. A 4-hour service booked at 9:00 AM did not block
-- 11:00 AM or 1:00 PM, even though the two would physically overlap. This
-- was inert while nothing persisted (Phase 2A/2B); Phase 2C is what makes
-- it reachable by real customers, so it's fixed before that branch merges
-- rather than shipped as a known gap.
--
-- Fix: booked_slots gains duration_minutes, populated from the booked
-- service's own duration_minutes at sync time. Availability computation
-- (client-side, src/data/availability.ts) changes from an exact-time-match
-- check to a real half-open-interval overlap check: a candidate slot
-- [candidateStart, candidateStart + requestedDuration) is unavailable if it
-- overlaps ANY existing booked slot's [bookedStart, bookedStart +
-- bookedDuration) interval. booked_slots keeps exactly one row per active
-- booking (not one row per slot-grid-interval it spans) -- the overlap math
-- lives entirely in the client's pure computation function, matching this
-- project's established preference for a thin client over new DB logic.
alter table public.booked_slots add column duration_minutes integer not null default 0;
-- The default above only exists so the column can be added without a
-- backfill value for any pre-existing rows; every row from this point
-- forward is written by sync_booked_slot() below, which always supplies a
-- real duration. Drop the default so a future direct insert can't silently
-- create a zero-duration (and therefore never-overlapping, i.e. bookable
-- over) row by omitting the column.
alter table public.booked_slots alter column duration_minutes drop default;
create or replace function public.sync_booked_slot()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
v_duration integer;
begin
if tg_op = 'DELETE' then
if old.status in ('confirmed', 'completed') then
delete from public.booked_slots
where date = old.date and time = old.time
and not exists (
select 1 from public.bookings b
where b.date = old.date and b.time = old.time and b.status in ('confirmed', 'completed')
);
end if;
return old;
end if;
if tg_op = 'UPDATE' then
-- Covers both a status change away from 'confirmed'/'completed'
-- (cancellation) and a reschedule of a still-occupying booking to a
-- different (date, time): either way, the OLD slot may no longer have
-- any confirmed/completed booking holding it and its mirror row needs
-- to go.
if old.status in ('confirmed', 'completed')
and (old.date, old.time, old.status) is distinct from (new.date, new.time, new.status) then
delete from public.booked_slots
where date = old.date and time = old.time
and not exists (
select 1 from public.bookings b
where b.date = old.date and b.time = old.time and b.status in ('confirmed', 'completed')
);
end if;
if new.status in ('confirmed', 'completed') then
select duration_minutes into v_duration from public.services where id = new.service_id;
insert into public.booked_slots (date, time, duration_minutes)
values (new.date, new.time, coalesce(v_duration, 0))
on conflict (date, time) do update set duration_minutes = excluded.duration_minutes;
end if;
return new;
end if;
-- INSERT
if new.status in ('confirmed', 'completed') then
select duration_minutes into v_duration from public.services where id = new.service_id;
insert into public.booked_slots (date, time, duration_minutes)
values (new.date, new.time, coalesce(v_duration, 0))
on conflict (date, time) do update set duration_minutes = excluded.duration_minutes;
end if;
return new;
end;
$$;