supersam/supabase/schema.sql

579 lines
23 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),
ready_for_delivery_at timestamptz,
delivery_flow_started_at timestamptz,
delivery_flow_source text,
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',
selected_by_client_at timestamptz,
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())
);
create table if not exists public.delivery_invitations (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders (id) on delete cascade unique,
token_hash text not null unique,
state text not null default 'awaiting_choice',
order_number text,
customer_name text,
customer_phone text,
customer_messenger text,
available_slots text[] not null default array['Первая половина дня', 'Вторая половина дня'],
delivery_date date,
delivery_time text,
sent_at timestamptz,
opened_at timestamptz,
confirmed_at timestamptz,
logistics_transferred_at timestamptz,
paid_storage_at timestamptz,
delivered_at timestamptz,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.integration_events (
id uuid primary key default gen_random_uuid(),
order_id uuid references public.orders (id) on delete set null,
event_type text not null,
direction text not null default 'internal',
source text not null default 'supabase-function',
status text not null default 'success',
payload jsonb not null default '{}'::jsonb,
error_message text,
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.orders add column if not exists ready_for_delivery_at timestamptz;
alter table public.orders add column if not exists delivery_flow_started_at timestamptz;
alter table public.orders add column if not exists delivery_flow_source text;
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'));
alter table public.delivery_invitations add column if not exists state text not null default 'awaiting_choice';
alter table public.delivery_invitations add column if not exists delivery_date date;
alter table public.delivery_invitations add column if not exists delivery_time text;
alter table public.delivery_invitations add column if not exists sent_at timestamptz;
alter table public.delivery_invitations add column if not exists opened_at timestamptz;
alter table public.delivery_invitations add column if not exists confirmed_at timestamptz;
alter table public.delivery_invitations add column if not exists logistics_transferred_at timestamptz;
alter table public.delivery_invitations add column if not exists paid_storage_at timestamptz;
alter table public.delivery_invitations add column if not exists delivered_at timestamptz;
alter table public.delivery_invitations add column if not exists updated_at timestamptz not null default timezone('utc', now());
alter table public.orders add column if not exists source_order_number text;
alter table public.orders add column if not exists source_order_date date;
alter table public.orders add column if not exists source_customer_name text;
alter table public.orders add column if not exists source_customer_phone text;
alter table public.orders add column if not exists source_customer_email text;
alter table public.orders add column if not exists source_customer_city text;
alter table public.orders add column if not exists source_total_sum numeric;
alter table public.orders add column if not exists source_paid_at timestamptz;
alter table public.orders add column if not exists source_gateway text;
alter table public.orders add column if not exists source_associated_bills_text text;
alter table public.orders add column if not exists source_production_at timestamptz;
alter table public.orders add column if not exists source_saw_at timestamptz;
alter table public.orders add column if not exists source_glue_at timestamptz;
alter table public.orders add column if not exists source_h_glue_at timestamptz;
alter table public.orders add column if not exists source_curve_at timestamptz;
alter table public.orders add column if not exists source_accept_at timestamptz;
alter table public.orders add column if not exists source_ship_at timestamptz;
alter table public.orders add column if not exists source_payload jsonb;
alter table public.orders add column if not exists delivery_set_key text;
alter table public.orders add column if not exists delivery_set_name text;
alter table public.orders add column if not exists delivery_set_status text;
alter table public.orders add column if not exists delivery_set_ready_at timestamptz;
alter table public.orders add column if not exists delivery_ready_reason text;
alter table public.orders add column if not exists source_sms_legacy_at timestamptz;
comment on column public.orders.source_sms_legacy_at is 'Informational only: legacy 1C SMS timestamp. Must NOT be used to start new delivery automation scenarios.';
alter table public.integration_events add column if not exists direction text not null default 'internal';
alter table public.integration_events add column if not exists source text not null default 'supabase-function';
alter table public.integration_events add column if not exists status text not null default 'success';
alter table public.integration_events add column if not exists payload jsonb not null default '{}'::jsonb;
alter table public.integration_events add column if not exists error_message text;
create index if not exists idx_orders_delivery_set_key on public.orders (delivery_set_key);
create index if not exists idx_orders_delivery_set_status on public.orders (delivery_set_status);
create index if not exists idx_orders_source_accept_at on public.orders (source_accept_at);
create index if not exists idx_orders_source_ship_at on public.orders (source_ship_at);
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();
drop trigger if exists delivery_invitations_set_updated_at on public.delivery_invitations;
create trigger delivery_invitations_set_updated_at
before update on public.delivery_invitations
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_ready_for_delivery_at on public.orders (ready_for_delivery_at);
create index if not exists idx_orders_delivery_flow_started_at on public.orders (delivery_flow_started_at);
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_delivery_slots_selected_by_client_at on public.delivery_slots (selected_by_client_at);
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, ''))
);
create index if not exists idx_delivery_invitations_order_id on public.delivery_invitations (order_id);
create index if not exists idx_delivery_invitations_token_hash on public.delivery_invitations (token_hash);
create index if not exists idx_delivery_invitations_state on public.delivery_invitations (state);
create index if not exists idx_integration_events_order_id on public.integration_events (order_id, created_at desc);
create index if not exists idx_integration_events_event_type on public.integration_events (event_type);
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;
alter table public.delivery_invitations enable row level security;
alter table public.integration_events enable row level security;
drop policy if exists "roles select authenticated" on public.roles;
create policy "roles select authenticated" on public.roles
for select
using (public.current_role_name() is not null);
drop policy if exists "roles admin mutate" on public.roles;
create policy "roles admin mutate" on public.roles
for insert
with check (public.current_role_name() = 'admin');
drop policy if exists "roles admin update" on public.roles;
create policy "roles admin update" on public.roles
for update
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');
drop policy if exists "roles admin delete" on public.roles;
create policy "roles admin delete" on public.roles
for delete
using (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 (public.current_role_name() is not null);
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');
drop policy if exists "delivery invitations admin only" on public.delivery_invitations;
create policy "delivery invitations admin only" on public.delivery_invitations
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');
drop policy if exists "integration events admin only" on public.integration_events;
create policy "integration events admin only" on public.integration_events
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');