283 lines
10 KiB
SQL
283 lines
10 KiB
SQL
-- Menulio initial schema (PRD §22, §21)
|
|
-- User -> Restaurant -> Location kept as separate tables from day one even
|
|
-- though V1 UX is 1:1:1, so multi-location support (V2) needs no migration.
|
|
|
|
create extension if not exists "pgcrypto";
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Enums
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create type subscription_status as enum ('active', 'grace_period', 'suspended', 'deleted');
|
|
create type ai_import_status as enum ('pending', 'processing', 'completed', 'failed');
|
|
create type domain_status as enum ('pending', 'verified', 'failed');
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Core identity
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table users (
|
|
id uuid primary key references auth.users (id) on delete cascade,
|
|
full_name text,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table restaurants (
|
|
id uuid primary key default gen_random_uuid(),
|
|
name text not null,
|
|
slug text not null unique,
|
|
logo_url text,
|
|
phone text,
|
|
address text,
|
|
created_by uuid not null references users (id) on delete restrict,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
create table restaurant_members (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
user_id uuid not null references users (id) on delete cascade,
|
|
role text not null default 'owner',
|
|
created_at timestamptz not null default now(),
|
|
unique (restaurant_id, user_id)
|
|
);
|
|
|
|
create table locations (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
name text not null,
|
|
address text,
|
|
phone text,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Menu
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table menus (
|
|
id uuid primary key default gen_random_uuid(),
|
|
location_id uuid not null references locations (id) on delete cascade,
|
|
name text not null default 'Menü',
|
|
is_published boolean not null default false,
|
|
published_at timestamptz,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
create table menu_categories (
|
|
id uuid primary key default gen_random_uuid(),
|
|
menu_id uuid not null references menus (id) on delete cascade,
|
|
name text not null,
|
|
description text,
|
|
sort_order integer not null default 0,
|
|
is_active boolean not null default true,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table menu_items (
|
|
id uuid primary key default gen_random_uuid(),
|
|
category_id uuid not null references menu_categories (id) on delete cascade,
|
|
name text not null,
|
|
description text,
|
|
price numeric(10, 2) not null,
|
|
image_url text,
|
|
sort_order integer not null default 0,
|
|
is_active boolean not null default true,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
create table menu_item_options (
|
|
id uuid primary key default gen_random_uuid(),
|
|
menu_item_id uuid not null references menu_items (id) on delete cascade,
|
|
group_name text not null,
|
|
label text not null,
|
|
price_delta numeric(10, 2) not null default 0,
|
|
sort_order integer not null default 0
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Theming
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table themes (
|
|
id uuid primary key default gen_random_uuid(),
|
|
key text not null unique,
|
|
name text not null,
|
|
config jsonb not null default '{}'::jsonb
|
|
);
|
|
|
|
create table restaurant_themes (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
theme_id uuid not null references themes (id) on delete restrict,
|
|
overrides jsonb not null default '{}'::jsonb,
|
|
created_at timestamptz not null default now(),
|
|
unique (restaurant_id)
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Domains & QR
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table domains (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
hostname text not null unique,
|
|
is_custom boolean not null default false,
|
|
status domain_status not null default 'pending',
|
|
verified_at timestamptz,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table qr_codes (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
target_url text not null,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- AI import
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table ai_imports (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
source_image text not null,
|
|
status ai_import_status not null default 'pending',
|
|
model text,
|
|
raw_response jsonb,
|
|
error text,
|
|
processed_at timestamptz,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table ai_import_items (
|
|
id uuid primary key default gen_random_uuid(),
|
|
ai_import_id uuid not null references ai_imports (id) on delete cascade,
|
|
category_name text,
|
|
item_name text not null,
|
|
description text,
|
|
price numeric(10, 2),
|
|
confidence numeric(4, 3),
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Subscription
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table subscriptions (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
revenuecat_entitlement text not null default 'pro',
|
|
status subscription_status not null default 'active',
|
|
product_id text,
|
|
trial_ends_at timestamptz,
|
|
current_period_ends_at timestamptz,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
unique (restaurant_id)
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Analytics & media
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table analytics_events (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
event_type text not null,
|
|
metadata jsonb not null default '{}'::jsonb,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table media (
|
|
id uuid primary key default gen_random_uuid(),
|
|
restaurant_id uuid not null references restaurants (id) on delete cascade,
|
|
storage_path text not null,
|
|
kind text not null default 'image',
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Indexes
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create index on restaurant_members (user_id);
|
|
create index on locations (restaurant_id);
|
|
create index on menus (location_id);
|
|
create index on menu_categories (menu_id);
|
|
create index on menu_items (category_id);
|
|
create index on menu_item_options (menu_item_id);
|
|
create index on domains (restaurant_id);
|
|
create index on ai_imports (restaurant_id);
|
|
create index on ai_import_items (ai_import_id);
|
|
create index on analytics_events (restaurant_id, created_at);
|
|
create index on media (restaurant_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Row Level Security
|
|
-- ---------------------------------------------------------------------------
|
|
-- Business logic lives in the API layer (PRD §23), so most tables are locked
|
|
-- to the service role. Owner-facing tables get membership-scoped policies for
|
|
-- direct client reads; public menu tables get an anon read policy scoped to
|
|
-- published menus only.
|
|
|
|
alter table users enable row level security;
|
|
alter table restaurants enable row level security;
|
|
alter table restaurant_members enable row level security;
|
|
alter table locations enable row level security;
|
|
alter table menus enable row level security;
|
|
alter table menu_categories enable row level security;
|
|
alter table menu_items enable row level security;
|
|
alter table menu_item_options enable row level security;
|
|
alter table themes enable row level security;
|
|
alter table restaurant_themes enable row level security;
|
|
alter table domains enable row level security;
|
|
alter table qr_codes enable row level security;
|
|
alter table ai_imports enable row level security;
|
|
alter table ai_import_items enable row level security;
|
|
alter table subscriptions enable row level security;
|
|
alter table analytics_events enable row level security;
|
|
alter table media enable row level security;
|
|
|
|
create policy "users read own row" on users
|
|
for select using (id = auth.uid());
|
|
|
|
create policy "members read own restaurants" on restaurants
|
|
for select using (
|
|
exists (
|
|
select 1 from restaurant_members rm
|
|
where rm.restaurant_id = restaurants.id and rm.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
create policy "members read own membership rows" on restaurant_members
|
|
for select using (user_id = auth.uid());
|
|
|
|
create policy "anyone can read published menus" on menus
|
|
for select using (is_published = true);
|
|
|
|
create policy "anyone can read categories of published menus" on menu_categories
|
|
for select using (
|
|
exists (
|
|
select 1 from menus m where m.id = menu_categories.menu_id and m.is_published = true
|
|
)
|
|
);
|
|
|
|
create policy "anyone can read items of published menus" on menu_items
|
|
for select using (
|
|
exists (
|
|
select 1
|
|
from menu_categories mc
|
|
join menus m on m.id = mc.menu_id
|
|
where mc.id = menu_items.category_id and m.is_published = true
|
|
)
|
|
);
|
|
|
|
create policy "themes are public" on themes for select using (true);
|