1180 lines
43 KiB
PL/PgSQL
1180 lines
43 KiB
PL/PgSQL
create extension if not exists pgcrypto;
|
||
|
||
create or replace function public.next_order_group_sms_check_at(
|
||
start_from timestamptz default now(),
|
||
delay interval default interval '0 minutes'
|
||
)
|
||
returns timestamptz
|
||
language plpgsql
|
||
stable
|
||
as $$
|
||
declare
|
||
v_timezone text := 'Europe/Simferopol';
|
||
v_local_time timestamp;
|
||
v_local_date date;
|
||
v_work_start timestamp;
|
||
v_work_end timestamp;
|
||
v_candidate timestamp;
|
||
begin
|
||
v_local_time := (start_from at time zone v_timezone) + delay;
|
||
v_local_date := v_local_time::date;
|
||
v_work_start := v_local_date + time '09:00';
|
||
v_work_end := v_local_date + time '20:00';
|
||
|
||
if v_local_time < v_work_start then
|
||
v_candidate := v_work_start;
|
||
elsif v_local_time >= v_work_end then
|
||
v_candidate := (v_local_date + 1) + time '09:00';
|
||
else
|
||
v_candidate := v_local_time;
|
||
end if;
|
||
|
||
return v_candidate at time zone v_timezone;
|
||
end;
|
||
$$;
|
||
|
||
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.order_groups (
|
||
id uuid primary key default gen_random_uuid(),
|
||
group_key text not null,
|
||
customer jsonb,
|
||
order_numbers text[] not null default '{}',
|
||
status text not null default 'ready_for_notification',
|
||
delivery_status text not null default 'pending_confirmation',
|
||
sms_sent_at timestamptz,
|
||
created_at timestamptz not null default timezone('utc', now()),
|
||
updated_at timestamptz not null default timezone('utc', now()),
|
||
created_from_exchange_at timestamptz,
|
||
source_key text,
|
||
customer_name text,
|
||
customer_phone text,
|
||
customer_phone_normalized text,
|
||
customer_date text,
|
||
orders_total integer,
|
||
orders_ready integer,
|
||
orders_not_ready integer,
|
||
source_orders jsonb,
|
||
delivery_invitation_id uuid,
|
||
delivery_link text,
|
||
notification_status text not null default 'not_started',
|
||
sms_attempts integer not null default 0,
|
||
first_sms_sent_at timestamptz,
|
||
second_sms_sent_at timestamptz,
|
||
last_sms_error text,
|
||
next_notification_check_at timestamptz,
|
||
delivery_date date,
|
||
delivery_time text
|
||
);
|
||
|
||
create table if not exists public.delivery_invitations (
|
||
id uuid primary key default gen_random_uuid(),
|
||
order_id uuid references public.orders (id) on delete cascade unique,
|
||
order_group_id uuid references public.order_groups (id) on delete cascade,
|
||
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['Первая половина дня', 'Вторая половина дня'],
|
||
expires_at timestamptz,
|
||
revoked_at timestamptz,
|
||
access_count integer not null default 0,
|
||
last_accessed_at timestamptz,
|
||
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())
|
||
);
|
||
|
||
create table if not exists public.rate_limits (
|
||
id uuid primary key default gen_random_uuid(),
|
||
scope text not null,
|
||
rate_key text not null,
|
||
window_start timestamptz not null,
|
||
count integer not null default 1,
|
||
blocked_until timestamptz,
|
||
created_at timestamptz not null default timezone('utc', now()),
|
||
updated_at timestamptz not null default timezone('utc', now()),
|
||
unique (scope, rate_key, window_start)
|
||
);
|
||
|
||
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 alter column order_id drop not null;
|
||
alter table public.delivery_invitations add column if not exists order_group_id uuid references public.order_groups(id) on delete cascade;
|
||
alter table public.delivery_invitations add column if not exists expires_at timestamptz;
|
||
alter table public.delivery_invitations add column if not exists revoked_at timestamptz;
|
||
alter table public.delivery_invitations add column if not exists access_count integer not null default 0;
|
||
alter table public.delivery_invitations add column if not exists last_accessed_at timestamptz;
|
||
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.order_groups add column if not exists delivery_invitation_id uuid references public.delivery_invitations(id) on delete set null;
|
||
alter table public.order_groups add column if not exists delivery_link text;
|
||
alter table public.order_groups add column if not exists notification_status text not null default 'not_started';
|
||
alter table public.order_groups add column if not exists sms_attempts integer not null default 0;
|
||
alter table public.order_groups add column if not exists first_sms_sent_at timestamptz;
|
||
alter table public.order_groups add column if not exists second_sms_sent_at timestamptz;
|
||
alter table public.order_groups add column if not exists last_sms_error text;
|
||
alter table public.order_groups add column if not exists next_notification_check_at timestamptz;
|
||
alter table public.order_groups add column if not exists delivery_date date;
|
||
alter table public.order_groups add column if not exists delivery_time text;
|
||
|
||
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;
|
||
alter table public.rate_limits add column if not exists scope text not null;
|
||
alter table public.rate_limits add column if not exists rate_key text not null;
|
||
alter table public.rate_limits add column if not exists window_start timestamptz not null;
|
||
alter table public.rate_limits add column if not exists count integer not null default 1;
|
||
alter table public.rate_limits add column if not exists blocked_until timestamptz;
|
||
alter table public.rate_limits add column if not exists created_at timestamptz not null default timezone('utc', now());
|
||
alter table public.rate_limits add column if not exists updated_at timestamptz not null default timezone('utc', now());
|
||
|
||
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_order_group_id on public.delivery_invitations (order_group_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_delivery_invitations_expires_at on public.delivery_invitations (expires_at);
|
||
create index if not exists idx_order_groups_status on public.order_groups (status);
|
||
create index if not exists idx_order_groups_delivery_status on public.order_groups (delivery_status);
|
||
create index if not exists idx_order_groups_notification_status on public.order_groups (notification_status, next_notification_check_at);
|
||
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);
|
||
create index if not exists idx_rate_limits_scope_key_window on public.rate_limits (scope, rate_key, window_start desc);
|
||
create index if not exists idx_rate_limits_blocked_until on public.rate_limits (blocked_until);
|
||
|
||
create or replace function public.check_rate_limit(
|
||
p_scope text,
|
||
p_key text,
|
||
p_max_count integer,
|
||
p_window_seconds integer,
|
||
p_block_seconds integer default 0
|
||
)
|
||
returns table (
|
||
allowed boolean,
|
||
current_count integer,
|
||
limit_count integer,
|
||
blocked_until timestamptz,
|
||
window_start timestamptz
|
||
)
|
||
language plpgsql
|
||
security definer
|
||
set search_path = public
|
||
as $$
|
||
declare
|
||
v_now timestamptz := timezone('utc', now());
|
||
v_window_start timestamptz;
|
||
v_count integer;
|
||
v_blocked_until timestamptz;
|
||
begin
|
||
if p_max_count <= 0 then
|
||
raise exception 'max_count must be positive';
|
||
end if;
|
||
|
||
if p_window_seconds <= 0 then
|
||
raise exception 'window_seconds must be positive';
|
||
end if;
|
||
|
||
v_window_start := to_timestamp(floor(extract(epoch from v_now) / p_window_seconds) * p_window_seconds);
|
||
|
||
select rl.blocked_until
|
||
into v_blocked_until
|
||
from public.rate_limits rl
|
||
where rl.scope = p_scope
|
||
and rl.rate_key = p_key
|
||
and rl.blocked_until is not null
|
||
and rl.blocked_until > v_now
|
||
order by rl.blocked_until desc
|
||
limit 1;
|
||
|
||
if v_blocked_until is not null then
|
||
return query
|
||
select false, 0, p_max_count, v_blocked_until, v_window_start;
|
||
return;
|
||
end if;
|
||
|
||
insert into public.rate_limits (scope, rate_key, window_start, count, blocked_until)
|
||
values (p_scope, p_key, v_window_start, 1, null)
|
||
on conflict (scope, rate_key, window_start)
|
||
do update set
|
||
count = public.rate_limits.count + 1,
|
||
blocked_until = case
|
||
when public.rate_limits.count + 1 > p_max_count and p_block_seconds > 0 then greatest(
|
||
coalesce(public.rate_limits.blocked_until, v_now),
|
||
v_now + make_interval(secs => p_block_seconds)
|
||
)
|
||
else public.rate_limits.blocked_until
|
||
end,
|
||
updated_at = v_now
|
||
returning count, blocked_until
|
||
into v_count, v_blocked_until;
|
||
|
||
return query
|
||
select
|
||
v_count <= p_max_count and (v_blocked_until is null or v_blocked_until <= v_now),
|
||
v_count,
|
||
p_max_count,
|
||
v_blocked_until,
|
||
v_window_start;
|
||
end;
|
||
$$;
|
||
|
||
create or replace function public.get_delivery_invitation_by_token(p_token text)
|
||
returns jsonb
|
||
language plpgsql
|
||
security definer
|
||
set search_path = public, extensions
|
||
as $$
|
||
declare
|
||
v_invitation public.delivery_invitations%rowtype;
|
||
v_group public.order_groups%rowtype;
|
||
v_order record;
|
||
v_token_hash text;
|
||
v_state text;
|
||
v_order_number text;
|
||
v_customer_name text;
|
||
v_customer_phone text;
|
||
v_order_items jsonb;
|
||
v_order_numbers jsonb;
|
||
v_now timestamptz := timezone('utc', now());
|
||
begin
|
||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
||
raise exception 'token is required';
|
||
end if;
|
||
|
||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||
|
||
select *
|
||
into v_invitation
|
||
from public.delivery_invitations
|
||
where token_hash = v_token_hash;
|
||
|
||
if not found then
|
||
raise exception 'Invitation not found';
|
||
end if;
|
||
|
||
if v_invitation.revoked_at is not null then
|
||
raise exception 'Invitation expired';
|
||
end if;
|
||
|
||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
||
raise exception 'Invitation expired';
|
||
end if;
|
||
|
||
if v_invitation.order_group_id is not null then
|
||
select *
|
||
into v_group
|
||
from public.order_groups
|
||
where id = v_invitation.order_group_id;
|
||
|
||
if not found then
|
||
raise exception 'Order group not found';
|
||
end if;
|
||
|
||
v_state := case
|
||
when v_group.delivery_status = 'agreed' then 'agreed'
|
||
when v_group.delivery_status = 'delivered' then 'delivered'
|
||
when v_invitation.state in ('awaiting_choice', 'opened', 'reminder_sent') then v_invitation.state
|
||
else 'default'
|
||
end;
|
||
|
||
update public.delivery_invitations
|
||
set
|
||
opened_at = case
|
||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
||
else opened_at
|
||
end,
|
||
access_count = coalesce(access_count, 0) + 1,
|
||
last_accessed_at = v_now
|
||
where id = v_invitation.id
|
||
returning * into v_invitation;
|
||
|
||
v_order_number := coalesce(
|
||
nullif(v_invitation.order_number, ''),
|
||
nullif(v_group.group_key, ''),
|
||
to_jsonb(v_group.order_numbers) ->> 0
|
||
);
|
||
v_customer_name := coalesce(
|
||
nullif(v_group.customer_name, ''),
|
||
nullif(v_group.customer ->> 'name', ''),
|
||
nullif(v_invitation.customer_name, '')
|
||
);
|
||
v_customer_phone := coalesce(
|
||
nullif(v_group.customer_phone, ''),
|
||
nullif(v_group.customer ->> 'phone', ''),
|
||
nullif(v_invitation.customer_phone, '')
|
||
);
|
||
select coalesce(
|
||
jsonb_agg(jsonb_build_object('name', order_number, 'quantity', '')),
|
||
'[]'::jsonb
|
||
)
|
||
into v_order_items
|
||
from jsonb_array_elements_text(
|
||
case
|
||
when jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' then to_jsonb(v_group.order_numbers)
|
||
else '[]'::jsonb
|
||
end
|
||
) as order_number;
|
||
|
||
return jsonb_build_object(
|
||
'ok', true,
|
||
'invitation', jsonb_build_object(
|
||
'orderId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
||
'orderGroupId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
||
'state', v_state,
|
||
'token', p_token,
|
||
'orderNumber', v_order_number,
|
||
'customerName', v_customer_name,
|
||
'customerPhone', v_customer_phone,
|
||
'orderItems', v_order_items,
|
||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||
'deliveryDate', v_invitation.delivery_date,
|
||
'deliveryTime', v_invitation.delivery_time,
|
||
'orderStatus', null,
|
||
'deliveryAgreementStatus', null
|
||
)
|
||
);
|
||
end if;
|
||
|
||
select id, order_number, status, delivery_agreement_status, customer
|
||
into v_order
|
||
from public.orders
|
||
where id = v_invitation.order_id;
|
||
|
||
if not found then
|
||
raise exception 'Order not found';
|
||
end if;
|
||
|
||
v_state := case v_order.status
|
||
when 'Ожидает ответа клиента' then 'awaiting_choice'
|
||
when 'Ожидает согласования доставки' then 'opened'
|
||
when 'Напоминание отправлено' then 'reminder_sent'
|
||
when 'Переход отправлен' then 'reminder_sent'
|
||
when 'Передан логисту' then 'transferred_to_logistics'
|
||
when 'Платное хранение' then 'paid_storage'
|
||
when 'Доставлен' then 'delivered'
|
||
when 'Доставка согласована' then 'agreed'
|
||
else 'default'
|
||
end;
|
||
|
||
update public.delivery_invitations
|
||
set
|
||
opened_at = case
|
||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
||
else opened_at
|
||
end,
|
||
access_count = coalesce(access_count, 0) + 1,
|
||
last_accessed_at = v_now
|
||
where id = v_invitation.id
|
||
returning * into v_invitation;
|
||
|
||
v_order_items := case
|
||
when jsonb_typeof(v_order.customer -> 'items') = 'array' then v_order.customer -> 'items'
|
||
else '[]'::jsonb
|
||
end;
|
||
|
||
return jsonb_build_object(
|
||
'ok', true,
|
||
'invitation', jsonb_build_object(
|
||
'orderId', v_invitation.order_id::text,
|
||
'state', v_state,
|
||
'token', p_token,
|
||
'orderNumber', coalesce(nullif(v_order.order_number, ''), nullif(v_invitation.order_number, '')),
|
||
'customerName', coalesce(nullif(v_order.customer ->> 'name', ''), nullif(v_invitation.customer_name, '')),
|
||
'customerPhone', coalesce(nullif(v_order.customer ->> 'phone', ''), nullif(v_invitation.customer_phone, '')),
|
||
'orderItems', v_order_items,
|
||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||
'deliveryDate', v_invitation.delivery_date,
|
||
'deliveryTime', v_invitation.delivery_time,
|
||
'orderStatus', v_order.status,
|
||
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
||
)
|
||
);
|
||
end;
|
||
$$;
|
||
|
||
create or replace function public.confirm_delivery_choice_by_token(
|
||
p_token text,
|
||
p_delivery_date date,
|
||
p_delivery_time text
|
||
)
|
||
returns jsonb
|
||
language plpgsql
|
||
security definer
|
||
set search_path = public, extensions
|
||
as $$
|
||
declare
|
||
v_invitation public.delivery_invitations%rowtype;
|
||
v_group public.order_groups%rowtype;
|
||
v_order record;
|
||
v_token_hash text;
|
||
v_slot_label text;
|
||
v_now timestamptz := timezone('utc', now());
|
||
begin
|
||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
||
raise exception 'token is required';
|
||
end if;
|
||
|
||
if p_delivery_date is null or nullif(trim(coalesce(p_delivery_time, '')), '') is null then
|
||
raise exception 'Selected slot is not available';
|
||
end if;
|
||
|
||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||
v_slot_label := concat(p_delivery_date::text, ', ', trim(p_delivery_time));
|
||
|
||
select *
|
||
into v_invitation
|
||
from public.delivery_invitations
|
||
where token_hash = v_token_hash
|
||
for update;
|
||
|
||
if not found then
|
||
raise exception 'Invitation not found';
|
||
end if;
|
||
|
||
if v_invitation.revoked_at is not null then
|
||
raise exception 'Invitation expired';
|
||
end if;
|
||
|
||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
||
raise exception 'Invitation expired';
|
||
end if;
|
||
|
||
if v_invitation.state not in ('awaiting_choice', 'opened', 'reminder_sent') then
|
||
raise exception 'Invitation is no longer active';
|
||
end if;
|
||
|
||
if cardinality(v_invitation.available_slots) > 0 and not (v_slot_label = any(v_invitation.available_slots)) then
|
||
raise exception 'Selected slot is not available';
|
||
end if;
|
||
|
||
if v_invitation.order_group_id is not null then
|
||
select *
|
||
into v_group
|
||
from public.order_groups
|
||
where id = v_invitation.order_group_id
|
||
for update;
|
||
|
||
if not found then
|
||
raise exception 'Order group not found';
|
||
end if;
|
||
|
||
if v_group.delivery_status <> 'pending_confirmation' then
|
||
raise exception 'Invitation is no longer active';
|
||
end if;
|
||
|
||
update public.delivery_invitations
|
||
set
|
||
state = 'agreed',
|
||
delivery_date = p_delivery_date,
|
||
delivery_time = trim(p_delivery_time),
|
||
confirmed_at = v_now,
|
||
access_count = coalesce(access_count, 0) + 1,
|
||
last_accessed_at = v_now
|
||
where id = v_invitation.id;
|
||
|
||
update public.order_groups
|
||
set
|
||
delivery_status = 'agreed',
|
||
delivery_date = p_delivery_date,
|
||
delivery_time = trim(p_delivery_time),
|
||
notification_status = 'confirmed',
|
||
updated_at = v_now
|
||
where id = v_group.id;
|
||
|
||
insert into public.integration_events (
|
||
order_id,
|
||
event_type,
|
||
direction,
|
||
status,
|
||
payload
|
||
)
|
||
values (
|
||
null,
|
||
'delivery_choice_confirmed',
|
||
'inbound',
|
||
'success',
|
||
jsonb_build_object(
|
||
'order_group_id', v_group.id,
|
||
'delivery_invitation_id', v_invitation.id,
|
||
'delivery_date', p_delivery_date,
|
||
'delivery_time', trim(p_delivery_time)
|
||
)
|
||
);
|
||
|
||
return jsonb_build_object(
|
||
'ok', true,
|
||
'orderGroupId', v_group.id,
|
||
'deliveryStatus', 'agreed'
|
||
);
|
||
end if;
|
||
|
||
select id, status, delivery_agreement_status
|
||
into v_order
|
||
from public.orders
|
||
where id = v_invitation.order_id
|
||
for update;
|
||
|
||
if not found then
|
||
raise exception 'Order not found';
|
||
end if;
|
||
|
||
if v_order.status not in ('Ожидает ответа клиента', 'Ожидает согласования доставки') then
|
||
raise exception 'Invitation is no longer active';
|
||
end if;
|
||
|
||
update public.delivery_invitations
|
||
set
|
||
state = 'agreed',
|
||
delivery_date = p_delivery_date,
|
||
delivery_time = trim(p_delivery_time),
|
||
confirmed_at = v_now,
|
||
access_count = coalesce(access_count, 0) + 1,
|
||
last_accessed_at = v_now
|
||
where id = v_invitation.id;
|
||
|
||
update public.orders
|
||
set
|
||
status = 'Доставка согласована',
|
||
delivery_agreement_status = 'Подтверждено клиентом'
|
||
where id = v_order.id;
|
||
|
||
insert into public.delivery_slots (
|
||
order_id,
|
||
delivery_date,
|
||
delivery_time,
|
||
logistician_id,
|
||
status
|
||
)
|
||
values (
|
||
v_order.id,
|
||
p_delivery_date,
|
||
trim(p_delivery_time),
|
||
null,
|
||
'confirmed_by_client'
|
||
);
|
||
|
||
insert into public.order_history (
|
||
order_id,
|
||
action,
|
||
old_status,
|
||
new_status,
|
||
metadata
|
||
)
|
||
values (
|
||
v_order.id,
|
||
'Подтверждение выбора доставки клиентом',
|
||
v_order.status,
|
||
'Доставка согласована',
|
||
jsonb_build_object(
|
||
'old_delivery_agreement_status', v_order.delivery_agreement_status,
|
||
'new_delivery_agreement_status', 'Подтверждено клиентом',
|
||
'delivery_date', p_delivery_date,
|
||
'delivery_time', trim(p_delivery_time)
|
||
)
|
||
);
|
||
|
||
insert into public.integration_events (
|
||
order_id,
|
||
event_type,
|
||
direction,
|
||
status,
|
||
payload
|
||
)
|
||
values (
|
||
v_order.id,
|
||
'delivery_choice_confirmed',
|
||
'inbound',
|
||
'success',
|
||
jsonb_build_object(
|
||
'delivery_date', p_delivery_date,
|
||
'delivery_time', trim(p_delivery_time)
|
||
)
|
||
);
|
||
|
||
return jsonb_build_object(
|
||
'ok', true,
|
||
'orderId', v_order.id,
|
||
'status', 'Доставка согласована',
|
||
'deliveryAgreementStatus', 'Подтверждено клиентом'
|
||
);
|
||
end;
|
||
$$;
|
||
|
||
revoke all on function public.get_delivery_invitation_by_token(text) from public;
|
||
grant execute on function public.get_delivery_invitation_by_token(text) to anon, authenticated;
|
||
|
||
revoke all on function public.confirm_delivery_choice_by_token(text, date, text) from public;
|
||
grant execute on function public.confirm_delivery_choice_by_token(text, date, text) to anon, authenticated;
|
||
|
||
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.order_groups 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() = 'admin' or id = auth.uid());
|
||
|
||
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 "order groups select by role" on public.order_groups;
|
||
create policy "order groups select by role" on public.order_groups
|
||
for select
|
||
using (true);
|
||
|
||
drop policy if exists "order groups update coordination roles" on public.order_groups;
|
||
create policy "order groups update coordination roles" on public.order_groups
|
||
for update
|
||
using (public.current_role_name() in ('manager', 'logistician', 'admin'))
|
||
with check (public.current_role_name() in ('manager', 'logistician', 'admin'));
|
||
|
||
drop policy if exists "order groups insert service roles" on public.order_groups;
|
||
create policy "order groups insert service roles" on public.order_groups
|
||
for insert
|
||
with check (public.current_role_name() in ('manager', 'logistician', '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');
|
||
|
||
alter table public.rate_limits enable row level security;
|
||
drop policy if exists "rate limits admin only" on public.rate_limits;
|
||
create policy "rate limits admin only" on public.rate_limits
|
||
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');
|