fix(delivery): switch public flow to rpc

This commit is contained in:
Codex 2026-05-13 18:59:00 +03:00
parent 7e399f2517
commit eeb2620547
5 changed files with 818 additions and 57 deletions

View File

@ -0,0 +1,389 @@
create extension if not exists pgcrypto;
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;

View File

@ -311,6 +311,10 @@ export const ClientDeliveryPage = () => {
<Panel className="space-y-2 p-5 sm:p-6">
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Ваш выбор</p>
<h2 className="text-xl font-semibold leading-tight">Сохранено: {savedChoiceLabel}</h2>
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
Заказ {invitation?.orderNumber || "—"}
{invitation?.customerName ? ` · ${invitation.customerName}` : ""}
</p>
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
Статус: доставка уже согласована. При повторном открытии этой ссылки будет показан тот же выбор.
</p>

View File

@ -1,8 +1,6 @@
import {
supabase,
hasSupabaseConfig,
supabaseAnonKey,
supabaseUrl,
} from "../supabaseClient";
const SHOWCASE_TOKEN = "showcase";
@ -163,29 +161,19 @@ const invokeDeliveryFunction = async (functionName, body) => {
return data;
};
const fetchDeliveryFunction = async (functionName, params) => {
if (!hasSupabaseConfig || !supabaseUrl || !supabaseAnonKey) {
const callDeliveryRpc = async (functionName, params) => {
if (!hasSupabaseConfig || !supabase?.rpc) {
throw new Error("Supabase is not configured");
}
const url = new URL(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${functionName}`);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
const { data, error } = await supabase.rpc(functionName, params);
const response = await fetch(url, {
method: "GET",
headers: {
apikey: supabaseAnonKey,
Authorization: `Bearer ${supabaseAnonKey}`,
},
});
const data = await response.json().catch(() => null);
if (error) {
throw error;
}
if (!response.ok) {
throw new Error(data?.error || `Request failed with status ${response.status}`);
if (data && data.ok === false) {
throw new Error(data.error || "Request failed");
}
return data;
@ -216,7 +204,9 @@ export const fetchDeliveryInvitation = async (token) => {
}
try {
const response = await fetchDeliveryFunction("get-delivery-invitation", { token });
const response = await callDeliveryRpc("get_delivery_invitation_by_token", {
p_token: token,
});
if (response?.invitation) {
return cacheInvitation(response.invitation);
}
@ -248,10 +238,10 @@ export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime
return { ok: true, invitation };
}
return invokeDeliveryFunction("confirm-delivery-choice", {
token,
deliveryDate,
deliveryTime,
return callDeliveryRpc("confirm_delivery_choice_by_token", {
p_token: token,
p_delivery_date: deliveryDate,
p_delivery_time: deliveryTime,
});
};

View File

@ -1,8 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock, invoke } = vi.hoisted(() => ({
const { fetchMock, invoke, rpc } = vi.hoisted(() => ({
fetchMock: vi.fn(),
invoke: vi.fn(),
rpc: vi.fn(),
}));
vi.mock("../supabaseClient", () => ({
@ -10,6 +11,7 @@ vi.mock("../supabaseClient", () => ({
supabaseAnonKey: "anon-key",
supabaseUrl: "https://supa.example.test",
supabase: {
rpc,
functions: {
invoke,
},
@ -30,6 +32,7 @@ describe("deliveryInvitationApi", () => {
beforeEach(() => {
fetchMock.mockReset();
invoke.mockReset();
rpc.mockReset();
vi.stubGlobal("fetch", fetchMock);
__resetLocalDeliveryInvitationCache();
const storage = new Map();
@ -48,15 +51,15 @@ describe("deliveryInvitationApi", () => {
});
it("loads a delivery invitation by token", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
rpc.mockResolvedValueOnce({
data: {
ok: true,
invitation: {
orderId: "order-1",
token: "token-1",
},
}),
},
error: null,
});
await expect(fetchDeliveryInvitation("token-1")).resolves.toEqual({
@ -64,32 +67,20 @@ describe("deliveryInvitationApi", () => {
token: "token-1",
});
expect(fetchMock).toHaveBeenCalledWith(
expect.objectContaining({
href: "https://supa.example.test/functions/v1/get-delivery-invitation?token=token-1",
}),
{
method: "GET",
headers: {
apikey: "anon-key",
Authorization: "Bearer anon-key",
},
},
);
expect(rpc).toHaveBeenCalledWith("get_delivery_invitation_by_token", {
p_token: "token-1",
});
expect(fetchMock).not.toHaveBeenCalled();
expect(invoke).not.toHaveBeenCalled();
});
it("throws a readable error when loading invitation fails", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 405,
json: async () => ({
ok: false,
error: "Method not allowed",
}),
rpc.mockResolvedValueOnce({
data: null,
error: new Error("Invitation not found"),
});
await expect(fetchDeliveryInvitation("token-1")).rejects.toThrow("Method not allowed");
await expect(fetchDeliveryInvitation("token-1")).rejects.toThrow("Invitation not found");
});
it("returns a local showcase invitation for the preview token", async () => {
@ -155,7 +146,7 @@ describe("deliveryInvitationApi", () => {
});
it("confirms a delivery choice with the chosen slot", async () => {
invoke.mockResolvedValueOnce({
rpc.mockResolvedValueOnce({
data: {
ok: true,
orderId: "order-1",
@ -174,13 +165,12 @@ describe("deliveryInvitationApi", () => {
orderId: "order-1",
});
expect(invoke).toHaveBeenCalledWith("confirm-delivery-choice", {
body: {
token: "token-1",
deliveryDate: "2026-04-01",
deliveryTime: "Первая половина дня",
},
expect(rpc).toHaveBeenCalledWith("confirm_delivery_choice_by_token", {
p_token: "token-1",
p_delivery_date: "2026-04-01",
p_delivery_time: "Первая половина дня",
});
expect(invoke).not.toHaveBeenCalled();
});
it("updates the local client invitation when confirmation falls back to cache", async () => {

View File

@ -540,6 +540,394 @@ begin
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;