first commit
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user