supersam/supabase/schema.sql

446 lines
15 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

create extension if not exists pgcrypto;
create table if not exists public.roles (
id uuid primary key default gen_random_uuid(),
name text not null unique,
permissions jsonb not null default '[]'::jsonb,
created_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.users (
id uuid primary key references auth.users (id) on delete cascade,
email text not null unique,
name text not null,
role_id uuid not null references public.roles (id),
last_login timestamptz,
created_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.orders (
id uuid primary key default gen_random_uuid(),
order_number text not null unique,
customer jsonb not null,
status text not null,
delivery_agreement_status text not null default 'Не начато',
manager_id uuid references public.users (id),
logistician_id uuid references public.users (id),
assigned_driver_id uuid references public.users (id),
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.order_logisticians (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders (id) on delete cascade,
logistician_id uuid not null references public.users (id) on delete cascade,
assigned_at timestamptz not null default timezone('utc', now()),
assigned_by uuid references public.users (id),
unique (order_id, logistician_id)
);
create table if not exists public.order_history (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders (id) on delete cascade,
action text not null,
old_status text,
new_status text,
user_id uuid references public.users (id),
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.delivery_slots (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders (id) on delete cascade,
delivery_date date not null,
delivery_time text not null,
logistician_id uuid references public.users (id),
status text not null default 'pending_confirmation',
created_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.chat_messages (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders (id) on delete cascade,
sender_name text,
sender_type text not null check (sender_type in ('client', 'bot', 'operator', 'system')),
channel text not null check (channel in ('telegram', 'vk', 'messenger_max', 'sms', 'email')),
text text not null,
external_message_id text,
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.error_logs (
id uuid primary key default gen_random_uuid(),
order_id uuid references public.orders (id) on delete set null,
provider text,
level text not null default 'error',
message text not null,
details jsonb not null default '{}'::jsonb,
created_at timestamptz not null default timezone('utc', now())
);
alter table public.orders add column if not exists delivery_agreement_status text not null default 'Не начато';
alter table public.orders add column if not exists assigned_driver_id uuid references public.users (id);
alter table public.chat_messages drop constraint if exists chat_messages_channel_check;
alter table public.chat_messages
add constraint chat_messages_channel_check
check (channel in ('telegram', 'vk', 'messenger_max', 'sms', 'email'));
insert into public.roles (name, permissions)
values
(
'manager',
'["orders.create","orders.update.own","orders.read.own","comments.manage"]'::jsonb
),
(
'production_lead',
'["orders.read.all","production.queue.manage","orders.status.production"]'::jsonb
),
(
'logistician',
'["orders.read.assigned","delivery.manage","chatbots.manage"]'::jsonb
),
(
'driver',
'["orders.read.assigned_driver","orders.status.driver"]'::jsonb
),
(
'admin',
'["*"]'::jsonb
)
on conflict (name) do nothing;
create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = timezone('utc', now());
return new;
end;
$$;
drop trigger if exists orders_set_updated_at on public.orders;
create trigger orders_set_updated_at
before update on public.orders
for each row
execute function public.set_updated_at();
create or replace function public.current_role_name()
returns text
language sql
stable
security definer
set search_path = public
as $$
select r.name
from public.users u
join public.roles r on r.id = u.role_id
where u.id = auth.uid()
$$;
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
default_role_id uuid;
begin
select id
into default_role_id
from public.roles
where name = coalesce(new.raw_user_meta_data ->> 'role', 'manager')
limit 1;
if default_role_id is null then
select id into default_role_id from public.roles where name = 'manager' limit 1;
end if;
insert into public.users (id, email, name, role_id, last_login)
values (
new.id,
new.email,
coalesce(new.raw_user_meta_data ->> 'name', split_part(new.email, '@', 1)),
default_role_id,
timezone('utc', now())
)
on conflict (id) do update
set email = excluded.email,
last_login = timezone('utc', now());
return new;
end;
$$;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row
execute function public.handle_new_user();
create or replace function public.log_order_status_change()
returns trigger
language plpgsql
security definer
as $$
begin
if tg_op = 'INSERT' then
insert into public.order_history (order_id, action, old_status, new_status, user_id)
values (new.id, 'Создан заказ', null, new.status, auth.uid());
return new;
end if;
if old.status is distinct from new.status then
insert into public.order_history (order_id, action, old_status, new_status, user_id)
values (new.id, 'Изменение статуса', old.status, new.status, auth.uid());
end if;
if old.delivery_agreement_status is distinct from new.delivery_agreement_status then
insert into public.order_history (order_id, action, old_status, new_status, user_id, metadata)
values (
new.id,
'Изменение согласования доставки',
old.delivery_agreement_status,
new.delivery_agreement_status,
auth.uid(),
jsonb_build_object('scope', 'delivery_agreement')
);
end if;
return new;
end;
$$;
drop trigger if exists orders_history_insert on public.orders;
create trigger orders_history_insert
after insert or update on public.orders
for each row
execute function public.log_order_status_change();
create index if not exists idx_users_role_id on public.users (role_id);
create index if not exists idx_orders_status on public.orders (status);
create index if not exists idx_orders_manager_id on public.orders (manager_id);
create index if not exists idx_orders_logistician_id on public.orders (logistician_id);
create index if not exists idx_orders_assigned_driver_id on public.orders (assigned_driver_id);
create index if not exists idx_orders_created_at on public.orders (created_at desc);
create index if not exists idx_order_logisticians_order_id on public.order_logisticians (order_id);
create index if not exists idx_order_logisticians_logistician_id on public.order_logisticians (logistician_id);
create index if not exists idx_order_history_order_id on public.order_history (order_id, created_at desc);
create index if not exists idx_delivery_slots_order_id on public.delivery_slots (order_id);
create index if not exists idx_delivery_slots_logistician_id on public.delivery_slots (logistician_id);
create index if not exists idx_chat_messages_order_id on public.chat_messages (order_id, created_at desc);
create index if not exists idx_chat_messages_external_message_id on public.chat_messages (external_message_id);
create unique index if not exists idx_chat_messages_channel_external_unique
on public.chat_messages (channel, external_message_id)
where external_message_id is not null;
create index if not exists idx_orders_search on public.orders using gin (
to_tsvector(
'simple',
coalesce(order_number, '') || ' ' || coalesce(customer ->> 'name', '') || ' ' || coalesce(customer ->> 'phone', '')
)
);
create index if not exists idx_chat_messages_search on public.chat_messages using gin (
to_tsvector('russian', coalesce(text, ''))
);
alter table public.roles enable row level security;
alter table public.users enable row level security;
alter table public.orders enable row level security;
alter table public.order_logisticians enable row level security;
alter table public.order_history enable row level security;
alter table public.delivery_slots enable row level security;
alter table public.chat_messages enable row level security;
alter table public.error_logs enable row level security;
drop policy if exists "roles admin only" on public.roles;
create policy "roles admin only" on public.roles
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');
drop policy if exists "users self or admin" on public.users;
create policy "users self or admin" on public.users
for select
using (id = auth.uid() or public.current_role_name() = 'admin');
drop policy if exists "users admin update" on public.users;
create policy "users admin update" on public.users
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');
drop policy if exists "orders select by role" on public.orders;
create policy "orders select by role" on public.orders
for select
using (
public.current_role_name() = 'admin'
or public.current_role_name() = 'production_lead'
or (public.current_role_name() = 'manager' and manager_id = auth.uid())
or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
or (
public.current_role_name() = 'logistician'
and (
logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = orders.id and ol.logistician_id = auth.uid()
)
)
)
);
drop policy if exists "orders insert managers admin" on public.orders;
create policy "orders insert managers admin" on public.orders
for insert
with check (public.current_role_name() in ('manager', 'admin'));
drop policy if exists "orders update by workflow role" on public.orders;
create policy "orders update by workflow role" on public.orders
for update
using (
public.current_role_name() = 'admin'
or (public.current_role_name() = 'manager' and manager_id = auth.uid())
or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
or public.current_role_name() = 'production_lead'
or (
public.current_role_name() = 'logistician'
and (
logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = orders.id and ol.logistician_id = auth.uid()
)
)
)
)
with check (
public.current_role_name() = 'admin'
or (public.current_role_name() = 'manager' and manager_id = auth.uid())
or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
or public.current_role_name() = 'production_lead'
or (
public.current_role_name() = 'logistician'
and (
logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = orders.id and ol.logistician_id = auth.uid()
)
)
)
);
drop policy if exists "history select by order role" on public.order_history;
create policy "history select by order role" on public.order_history
for select
using (
exists (
select 1
from public.orders o
where o.id = order_history.order_id
and (
public.current_role_name() = 'admin'
or public.current_role_name() = 'production_lead'
or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
or (
public.current_role_name() = 'logistician'
and (
o.logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = o.id and ol.logistician_id = auth.uid()
)
)
)
)
)
);
drop policy if exists "history insert workflow" on public.order_history;
create policy "history insert workflow" on public.order_history
for insert
with check (public.current_role_name() in ('manager', 'production_lead', 'logistician', 'driver', 'admin'));
drop policy if exists "slots by order role" on public.delivery_slots;
create policy "slots by order role" on public.delivery_slots
for all
using (
exists (
select 1
from public.orders o
where o.id = delivery_slots.order_id
and (
public.current_role_name() = 'admin'
or public.current_role_name() = 'production_lead'
or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
or (
public.current_role_name() = 'logistician'
and (
o.logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = o.id and ol.logistician_id = auth.uid()
)
)
)
)
)
)
with check (public.current_role_name() in ('logistician', 'admin'));
drop policy if exists "chat by order role" on public.chat_messages;
create policy "chat by order role" on public.chat_messages
for select
using (
exists (
select 1
from public.orders o
where o.id = chat_messages.order_id
and (
public.current_role_name() = 'admin'
or public.current_role_name() = 'production_lead'
or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
or (
public.current_role_name() = 'logistician'
and (
o.logistician_id = auth.uid()
or exists (
select 1
from public.order_logisticians ol
where ol.order_id = o.id and ol.logistician_id = auth.uid()
)
)
)
)
)
);
drop policy if exists "chat insert workflow" on public.chat_messages;
create policy "chat insert workflow" on public.chat_messages
for insert
with check (public.current_role_name() in ('manager', 'logistician', 'admin'));
drop policy if exists "order logisticians by role" on public.order_logisticians;
create policy "order logisticians by role" on public.order_logisticians
for all
using (public.current_role_name() in ('logistician', 'admin'))
with check (public.current_role_name() in ('logistician', 'admin'));
drop policy if exists "error logs admin only" on public.error_logs;
create policy "error logs admin only" on public.error_logs
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');