fix(delivery): switch public flow to rpc
This commit is contained in:
parent
7e399f2517
commit
eeb2620547
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
apikey: supabaseAnonKey,
|
||||
Authorization: `Bearer ${supabaseAnonKey}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json().catch(() => null);
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue