diff --git a/supabase/migrations/20260808000001_initial_schema.sql b/supabase/migrations/20260808000001_initial_schema.sql new file mode 100644 index 0000000..38ae53b --- /dev/null +++ b/supabase/migrations/20260808000001_initial_schema.sql @@ -0,0 +1,128 @@ +-- Products, services, availability schedule, bookings, inquiries, and the +-- public booked_slots mirror. See docs/superpowers/specs/2026-08-08-phase2-backend-foundation-supabase-design.md. + +create table public.products ( + id text primary key, + name text not null, + category text not null check (category in ('hair-care', 'extensions', 'tools', 'accessories')), + price numeric not null, + image text not null, + description text not null, + in_stock boolean not null default true +); + +create table public.services ( + id text primary key, + name text not null, + duration_minutes integer not null, + price_from numeric not null, + description text not null, + image text not null +); + +create table public.schedule_template ( + id text primary key default 'default', + working_days integer[] not null, + start_time time not null, + end_time time not null, + slot_minutes integer not null +); + +create table public.schedule_exceptions ( + date date primary key, + closed boolean not null, + note text +); + +create table public.bookings ( + id uuid primary key default gen_random_uuid(), + service_id text not null references public.services(id), + date date not null, + time text not null, + contact_name text not null, + contact_phone text not null, + contact_email text not null, + contact_notes text not null default '', + status text not null default 'confirmed' check (status in ('confirmed', 'cancelled')), + created_at timestamptz not null default now() +); + +create index bookings_service_id_idx on public.bookings (service_id); + +create table public.booked_slots ( + date date not null, + time text not null, + primary key (date, time) +); + +create table public.inquiries ( + id uuid primary key default gen_random_uuid(), + product_id text not null references public.products(id), + contact_name text not null, + contact_email text not null, + contact_notes text not null default '', + status text not null default 'new' check (status in ('new', 'read')), + created_at timestamptz not null default now() +); + +create index inquiries_product_id_idx on public.inquiries (product_id); + +-- Row Level Security + +alter table public.products enable row level security; +alter table public.services enable row level security; +alter table public.schedule_template enable row level security; +alter table public.schedule_exceptions enable row level security; +alter table public.bookings enable row level security; +alter table public.booked_slots enable row level security; +alter table public.inquiries enable row level security; + +-- products: public read, admin write +create policy "products_public_read" on public.products + for select using (true); +create policy "products_admin_write" on public.products + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- services: public read, admin write +create policy "services_public_read" on public.services + for select using (true); +create policy "services_admin_write" on public.services + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- schedule_template: public read, admin write +create policy "schedule_template_public_read" on public.schedule_template + for select using (true); +create policy "schedule_template_admin_write" on public.schedule_template + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- schedule_exceptions: public read, admin write +create policy "schedule_exceptions_public_read" on public.schedule_exceptions + for select using (true); +create policy "schedule_exceptions_admin_write" on public.schedule_exceptions + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- bookings: anyone can insert (submit a booking); only admin can read/update/delete +create policy "bookings_public_insert" on public.bookings + for insert with check (true); +create policy "bookings_admin_all" on public.bookings + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- booked_slots: public read; only admin policy exists for writes (in practice only +-- the security-definer trigger in the next migration ever writes here) +create policy "booked_slots_public_read" on public.booked_slots + for select using (true); +create policy "booked_slots_admin_write" on public.booked_slots + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid); + +-- inquiries: anyone can insert; only admin can read/update/delete +create policy "inquiries_public_insert" on public.inquiries + for insert with check (true); +create policy "inquiries_admin_all" on public.inquiries + for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid) + with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);