feat: show order composition collapsed with product list from source_orders
- OrderCompositionPanel now collapsed by default with position count badge - Flattens all products from source_orders into a single list - RPC migration to extract items from source_orders (needs DB deploy) - Edge Function updated to include source_orders items - Removed Edge Function enrichment hack from fetchDeliveryInvitation
This commit is contained in:
parent
e6dc1972fb
commit
9abfbff654
|
|
@ -0,0 +1,199 @@
|
||||||
|
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||||
|
-- This replaces ONLY the orderItems building section for the group path.
|
||||||
|
-- Apply AFTER the base function is restored.
|
||||||
|
|
||||||
|
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||||
|
-- Step 2: Then run this migration
|
||||||
|
|
||||||
|
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, ''),
|
||||||
|
to_jsonb(v_group.order_numbers) ->> 0,
|
||||||
|
NULLIF(v_group.group_key, '')
|
||||||
|
);
|
||||||
|
v_customer_name := COALESCE(
|
||||||
|
NULLIF(v_group.customer_name, ''),
|
||||||
|
NULLIF(v_invitation.customer_name, '')
|
||||||
|
);
|
||||||
|
v_customer_phone := COALESCE(
|
||||||
|
NULLIF(v_group.customer_phone, ''),
|
||||||
|
NULLIF(v_group.customer_phone_normalized, ''),
|
||||||
|
NULLIF(v_invitation.customer_phone, '')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Build orderItems: use source_orders for real product lines if available,
|
||||||
|
-- otherwise fall back to invoice numbers from order_numbers.
|
||||||
|
v_order_items := CASE
|
||||||
|
WHEN v_group.source_orders IS NOT NULL
|
||||||
|
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||||
|
AND jsonb_array_length(v_group.source_orders) > 0
|
||||||
|
THEN COALESCE(
|
||||||
|
(SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||||
|
'quantity', '',
|
||||||
|
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||||
|
)
|
||||||
|
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
ELSE COALESCE(
|
||||||
|
(SELECT jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||||
|
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),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
END;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
create extension if not exists pgcrypto;
|
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||||
|
-- This replaces ONLY the orderItems building section for the group path.
|
||||||
|
-- Apply AFTER the base function is restored.
|
||||||
|
|
||||||
create or replace function public.get_delivery_invitation_by_token(p_token text)
|
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||||
returns jsonb
|
-- Step 2: Then run this migration
|
||||||
language plpgsql
|
|
||||||
security definer
|
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
||||||
set search_path = public, extensions
|
RETURNS jsonb
|
||||||
as $$
|
LANGUAGE plpgsql
|
||||||
declare
|
SECURITY DEFINER
|
||||||
|
SET search_path = public, extensions
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
v_invitation public.delivery_invitations%rowtype;
|
v_invitation public.delivery_invitations%rowtype;
|
||||||
v_group public.order_groups%rowtype;
|
v_group public.order_groups%rowtype;
|
||||||
v_order record;
|
v_order record;
|
||||||
|
|
@ -18,291 +23,275 @@ declare
|
||||||
v_order_items jsonb;
|
v_order_items jsonb;
|
||||||
v_order_numbers jsonb;
|
v_order_numbers jsonb;
|
||||||
v_now timestamptz := timezone('utc', now());
|
v_now timestamptz := timezone('utc', now());
|
||||||
begin
|
BEGIN
|
||||||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||||
raise exception 'token is required';
|
RAISE EXCEPTION 'token is required';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||||
|
|
||||||
select *
|
SELECT *
|
||||||
into v_invitation
|
INTO v_invitation
|
||||||
from public.delivery_invitations
|
FROM public.delivery_invitations
|
||||||
where token_hash = v_token_hash;
|
WHERE token_hash = v_token_hash;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Invitation not found';
|
RAISE EXCEPTION 'Invitation not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.revoked_at is not null then
|
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||||
raise exception 'Invitation expired';
|
RAISE EXCEPTION 'Invitation expired';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||||
raise exception 'Invitation expired';
|
RAISE EXCEPTION 'Invitation expired';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.order_group_id is not null then
|
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||||
select *
|
SELECT *
|
||||||
into v_group
|
INTO v_group
|
||||||
from public.order_groups
|
FROM public.order_groups
|
||||||
where id = v_invitation.order_group_id;
|
WHERE id = v_invitation.order_group_id;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Order group not found';
|
RAISE EXCEPTION 'Order group not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
v_state := case
|
v_state := CASE
|
||||||
when v_group.delivery_status = 'agreed' then 'agreed'
|
WHEN v_group.delivery_status = 'agreed' THEN 'agreed'
|
||||||
when v_group.delivery_status = 'delivered' then 'delivered'
|
WHEN v_group.delivery_status = 'delivered' THEN 'delivered'
|
||||||
when v_invitation.state in ('awaiting_choice', 'opened', 'reminder_sent') then v_invitation.state
|
WHEN v_invitation.state IN ('awaiting_choice', 'opened', 'reminder_sent') THEN v_invitation.state
|
||||||
else 'default'
|
ELSE 'default'
|
||||||
end;
|
END;
|
||||||
|
|
||||||
update public.delivery_invitations
|
UPDATE public.delivery_invitations
|
||||||
set
|
SET
|
||||||
opened_at = case
|
opened_at = CASE
|
||||||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||||
else opened_at
|
ELSE opened_at
|
||||||
end,
|
END,
|
||||||
access_count = coalesce(access_count, 0) + 1,
|
access_count = COALESCE(access_count, 0) + 1,
|
||||||
last_accessed_at = v_now
|
last_accessed_at = v_now
|
||||||
where id = v_invitation.id
|
WHERE id = v_invitation.id
|
||||||
returning * into v_invitation;
|
RETURNING * INTO v_invitation;
|
||||||
|
|
||||||
v_order_number := coalesce(
|
v_order_number := COALESCE(
|
||||||
nullif(v_invitation.order_number, ''),
|
NULLIF(v_invitation.order_number, ''),
|
||||||
to_jsonb(v_group.order_numbers) ->> 0,
|
to_jsonb(v_group.order_numbers) ->> 0,
|
||||||
nullif(v_group.group_key, '')
|
NULLIF(v_group.group_key, '')
|
||||||
);
|
);
|
||||||
v_customer_name := coalesce(
|
v_customer_name := COALESCE(
|
||||||
nullif(v_group.customer_name, ''),
|
NULLIF(v_group.customer_name, ''),
|
||||||
nullif(v_invitation.customer_name, '')
|
NULLIF(v_invitation.customer_name, '')
|
||||||
);
|
);
|
||||||
v_customer_phone := coalesce(
|
v_customer_phone := COALESCE(
|
||||||
nullif(v_group.customer_phone, ''),
|
NULLIF(v_group.customer_phone, ''),
|
||||||
nullif(v_group.customer_phone_normalized, ''),
|
NULLIF(v_group.customer_phone_normalized, ''),
|
||||||
nullif(v_invitation.customer_phone, '')
|
NULLIF(v_invitation.customer_phone, '')
|
||||||
);
|
);
|
||||||
-- Build orderItems from source_orders if available (real product lines),
|
|
||||||
-- otherwise fall back to invoice numbers from order_numbers.
|
|
||||||
v_order_items := case
|
|
||||||
when v_group.source_orders is not null
|
|
||||||
and jsonb_typeof(v_group.source_orders) = 'array'
|
|
||||||
and jsonb_array_length(v_group.source_orders) > 0
|
|
||||||
then
|
|
||||||
coalesce(
|
|
||||||
(select jsonb_agg(
|
|
||||||
jsonb_build_object(
|
|
||||||
'name', coalesce(src ->> 'nom', src ->> 'name', onum),
|
|
||||||
'quantity', '',
|
|
||||||
'items', coalesce(
|
|
||||||
src -> 'orderList',
|
|
||||||
src -> 'items',
|
|
||||||
'[]'::jsonb
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
from jsonb_array_elements(v_group.source_orders) with ordinality as t(src, idx)
|
|
||||||
cross join lateral (
|
|
||||||
select coalesce(
|
|
||||||
nullif(v_group.order_numbers[idx], ''),
|
|
||||||
src ->> 'nom',
|
|
||||||
src ->> 'name',
|
|
||||||
''
|
|
||||||
) as onum
|
|
||||||
) as names
|
|
||||||
),
|
|
||||||
'[]'::jsonb
|
|
||||||
)
|
|
||||||
else
|
|
||||||
coalesce(
|
|
||||||
(select jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
|
||||||
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),
|
|
||||||
'[]'::jsonb
|
|
||||||
)
|
|
||||||
end;
|
|
||||||
|
|
||||||
return jsonb_build_object(
|
-- Build orderItems: use source_orders for real product lines if available,
|
||||||
'ok', true,
|
-- otherwise fall back to invoice numbers from order_numbers.
|
||||||
|
v_order_items := CASE
|
||||||
|
WHEN v_group.source_orders IS NOT NULL
|
||||||
|
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||||
|
AND jsonb_array_length(v_group.source_orders) > 0
|
||||||
|
THEN COALESCE(
|
||||||
|
(SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||||
|
'quantity', '',
|
||||||
|
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||||
|
)
|
||||||
|
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
ELSE COALESCE(
|
||||||
|
(SELECT jsonb_agg(jsonb_build_object('name', onum, 'quantity', ''))
|
||||||
|
FROM unnest(v_group.order_numbers) AS onum
|
||||||
|
WHERE onum IS NOT NULL AND onum <> ''),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
END;
|
||||||
|
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', TRUE,
|
||||||
'invitation', jsonb_build_object(
|
'invitation', jsonb_build_object(
|
||||||
'orderId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
'orderId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||||
'orderGroupId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
'orderGroupId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||||
'state', v_state,
|
'state', v_state,
|
||||||
'token', p_token,
|
'token', p_token,
|
||||||
'orderNumber', v_order_number,
|
'orderNumber', v_order_number,
|
||||||
'customerName', v_customer_name,
|
'customerName', v_customer_name,
|
||||||
'customerPhone', v_customer_phone,
|
'customerPhone', v_customer_phone,
|
||||||
'orderItems', v_order_items,
|
'orderItems', v_order_items,
|
||||||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||||
'deliveryDate', v_invitation.delivery_date,
|
'deliveryDate', v_invitation.delivery_date,
|
||||||
'deliveryTime', v_invitation.delivery_time,
|
'deliveryTime', v_invitation.delivery_time,
|
||||||
'orderStatus', null,
|
'orderStatus', NULL,
|
||||||
'deliveryAgreementStatus', null
|
'deliveryAgreementStatus', NULL
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
select id, order_number, status, delivery_agreement_status, customer
|
SELECT id, order_number, status, delivery_agreement_status, customer
|
||||||
into v_order
|
INTO v_order
|
||||||
from public.orders
|
FROM public.orders
|
||||||
where id = v_invitation.order_id;
|
WHERE id = v_invitation.order_id;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Order not found';
|
RAISE EXCEPTION 'Order not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
v_state := case v_order.status
|
v_state := CASE v_order.status
|
||||||
when 'Ожидает ответа клиента' then 'awaiting_choice'
|
WHEN 'Ожидает ответа клиента' THEN 'awaiting_choice'
|
||||||
when 'Ожидает согласования доставки' then 'opened'
|
WHEN 'Ожидает согласования доставки' THEN 'opened'
|
||||||
when 'Напоминание отправлено' then 'reminder_sent'
|
WHEN 'Напоминание отправлено' THEN 'reminder_sent'
|
||||||
when 'Переход отправлен' then 'reminder_sent'
|
WHEN 'Переход отправлен' THEN 'reminder_sent'
|
||||||
when 'Передан логисту' then 'transferred_to_logistics'
|
WHEN 'Передан логисту' THEN 'transferred_to_logistics'
|
||||||
when 'Платное хранение' then 'paid_storage'
|
WHEN 'Платное хранение' THEN 'paid_storage'
|
||||||
when 'Доставлен' then 'delivered'
|
WHEN 'Доставлен' THEN 'delivered'
|
||||||
when 'Доставка согласована' then 'agreed'
|
WHEN 'Доставка согласована' THEN 'agreed'
|
||||||
else 'default'
|
ELSE 'default'
|
||||||
end;
|
END;
|
||||||
|
|
||||||
update public.delivery_invitations
|
UPDATE public.delivery_invitations
|
||||||
set
|
SET
|
||||||
opened_at = case
|
opened_at = CASE
|
||||||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||||
else opened_at
|
ELSE opened_at
|
||||||
end,
|
END,
|
||||||
access_count = coalesce(access_count, 0) + 1,
|
access_count = COALESCE(access_count, 0) + 1,
|
||||||
last_accessed_at = v_now
|
last_accessed_at = v_now
|
||||||
where id = v_invitation.id
|
WHERE id = v_invitation.id
|
||||||
returning * into v_invitation;
|
RETURNING * INTO v_invitation;
|
||||||
|
|
||||||
v_order_items := case
|
v_order_items := CASE
|
||||||
when jsonb_typeof(v_order.customer -> 'items') = 'array' then v_order.customer -> 'items'
|
WHEN jsonb_typeof(v_order.customer -> 'items') = 'array' THEN v_order.customer -> 'items'
|
||||||
else '[]'::jsonb
|
ELSE '[]'::jsonb
|
||||||
end;
|
END;
|
||||||
|
|
||||||
return jsonb_build_object(
|
RETURN jsonb_build_object(
|
||||||
'ok', true,
|
'ok', TRUE,
|
||||||
'invitation', jsonb_build_object(
|
'invitation', jsonb_build_object(
|
||||||
'orderId', v_invitation.order_id::text,
|
'orderId', v_invitation.order_id::text,
|
||||||
'state', v_state,
|
'state', v_state,
|
||||||
'token', p_token,
|
'token', p_token,
|
||||||
'orderNumber', coalesce(nullif(v_order.order_number, ''), nullif(v_invitation.order_number, '')),
|
'orderNumber', COALESCE(NULLIF(v_order.order_number, ''), NULLIF(v_invitation.order_number, '')),
|
||||||
'customerName', coalesce(nullif(v_order.customer ->> 'name', ''), nullif(v_invitation.customer_name, '')),
|
'customerName', COALESCE(NULLIF(v_order.customer ->> 'name', ''), NULLIF(v_invitation.customer_name, '')),
|
||||||
'customerPhone', coalesce(nullif(v_order.customer ->> 'phone', ''), nullif(v_invitation.customer_phone, '')),
|
'customerPhone', COALESCE(NULLIF(v_order.customer ->> 'phone', ''), NULLIF(v_invitation.customer_phone, '')),
|
||||||
'orderItems', v_order_items,
|
'orderItems', v_order_items,
|
||||||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||||
'deliveryDate', v_invitation.delivery_date,
|
'deliveryDate', v_invitation.delivery_date,
|
||||||
'deliveryTime', v_invitation.delivery_time,
|
'deliveryTime', v_invitation.delivery_time,
|
||||||
'orderStatus', v_order.status,
|
'orderStatus', v_order.status,
|
||||||
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
end;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
create or replace function public.confirm_delivery_choice_by_token(
|
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;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.confirm_delivery_choice_by_token(
|
||||||
p_token text,
|
p_token text,
|
||||||
p_delivery_date date,
|
p_delivery_date date,
|
||||||
p_delivery_time text
|
p_delivery_time text
|
||||||
)
|
)
|
||||||
returns jsonb
|
RETURNS jsonb
|
||||||
language plpgsql
|
LANGUAGE plpgsql
|
||||||
security definer
|
SECURITY DEFINER
|
||||||
set search_path = public, extensions
|
SET search_path = public, extensions
|
||||||
as $$
|
AS $$
|
||||||
declare
|
DECLARE
|
||||||
v_invitation public.delivery_invitations%rowtype;
|
v_invitation public.delivery_invitations%rowtype;
|
||||||
v_group public.order_groups%rowtype;
|
v_group public.order_groups%rowtype;
|
||||||
v_order record;
|
v_order record;
|
||||||
v_token_hash text;
|
v_token_hash text;
|
||||||
v_slot_label text;
|
v_slot_label text;
|
||||||
v_now timestamptz := timezone('utc', now());
|
v_now timestamptz := timezone('utc', now());
|
||||||
begin
|
BEGIN
|
||||||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||||
raise exception 'token is required';
|
RAISE EXCEPTION 'token is required';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if p_delivery_date is null or nullif(trim(coalesce(p_delivery_time, '')), '') is null then
|
IF p_delivery_date IS NULL OR nullif(trim(coalesce(p_delivery_time, '')), '') IS NULL THEN
|
||||||
raise exception 'Selected slot is not available';
|
RAISE EXCEPTION 'Selected slot is not available';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||||
v_slot_label := concat(p_delivery_date::text, ', ', trim(p_delivery_time));
|
v_slot_label := concat(p_delivery_date::text, ', ', trim(p_delivery_time));
|
||||||
|
|
||||||
select *
|
SELECT *
|
||||||
into v_invitation
|
INTO v_invitation
|
||||||
from public.delivery_invitations
|
FROM public.delivery_invitations
|
||||||
where token_hash = v_token_hash
|
WHERE token_hash = v_token_hash
|
||||||
for update;
|
FOR UPDATE;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Invitation not found';
|
RAISE EXCEPTION 'Invitation not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.revoked_at is not null then
|
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||||
raise exception 'Invitation expired';
|
RAISE EXCEPTION 'Invitation expired';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||||
raise exception 'Invitation expired';
|
RAISE EXCEPTION 'Invitation expired';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.state not in ('awaiting_choice', 'opened', 'reminder_sent') then
|
IF v_invitation.state NOT IN ('awaiting_choice', 'opened', 'reminder_sent') THEN
|
||||||
raise exception 'Invitation is no longer active';
|
RAISE EXCEPTION 'Invitation is no longer active';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if cardinality(v_invitation.available_slots) > 0 and not (v_slot_label = any(v_invitation.available_slots)) then
|
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';
|
RAISE EXCEPTION 'Selected slot is not available';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_invitation.order_group_id is not null then
|
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||||
select *
|
SELECT *
|
||||||
into v_group
|
INTO v_group
|
||||||
from public.order_groups
|
FROM public.order_groups
|
||||||
where id = v_invitation.order_group_id
|
WHERE id = v_invitation.order_group_id
|
||||||
for update;
|
FOR UPDATE;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Order group not found';
|
RAISE EXCEPTION 'Order group not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_group.delivery_status <> 'pending_confirmation' then
|
IF v_group.delivery_status <> 'pending_confirmation' THEN
|
||||||
raise exception 'Invitation is no longer active';
|
RAISE EXCEPTION 'Invitation is no longer active';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
update public.delivery_invitations
|
UPDATE public.delivery_invitations
|
||||||
set
|
SET
|
||||||
state = 'agreed',
|
state = 'agreed',
|
||||||
delivery_date = p_delivery_date,
|
delivery_date = p_delivery_date,
|
||||||
delivery_time = trim(p_delivery_time),
|
delivery_time = trim(p_delivery_time),
|
||||||
confirmed_at = v_now,
|
confirmed_at = v_now,
|
||||||
access_count = coalesce(access_count, 0) + 1,
|
access_count = COALESCE(access_count, 0) + 1,
|
||||||
last_accessed_at = v_now
|
last_accessed_at = v_now
|
||||||
where id = v_invitation.id;
|
WHERE id = v_invitation.id;
|
||||||
|
|
||||||
update public.order_groups
|
UPDATE public.order_groups
|
||||||
set
|
SET
|
||||||
delivery_status = 'agreed',
|
delivery_status = 'agreed',
|
||||||
delivery_date = p_delivery_date,
|
delivery_date = p_delivery_date,
|
||||||
delivery_time = trim(p_delivery_time),
|
delivery_time = trim(p_delivery_time),
|
||||||
notification_status = 'confirmed',
|
notification_status = 'confirmed',
|
||||||
updated_at = v_now
|
updated_at = v_now
|
||||||
where id = v_group.id;
|
WHERE id = v_group.id;
|
||||||
|
|
||||||
insert into public.integration_events (
|
INSERT INTO public.integration_events (
|
||||||
order_id,
|
order_id,
|
||||||
event_type,
|
event_type,
|
||||||
direction,
|
direction,
|
||||||
status,
|
status,
|
||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
values (
|
VALUES (
|
||||||
null,
|
NULL,
|
||||||
'delivery_choice_confirmed',
|
'delivery_choice_confirmed',
|
||||||
'inbound',
|
'inbound',
|
||||||
'success',
|
'success',
|
||||||
|
|
@ -314,66 +303,66 @@ begin
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonb_build_object(
|
RETURN jsonb_build_object(
|
||||||
'ok', true,
|
'ok', TRUE,
|
||||||
'orderGroupId', v_group.id,
|
'orderGroupId', v_group.id,
|
||||||
'deliveryStatus', 'agreed'
|
'deliveryStatus', 'agreed'
|
||||||
);
|
);
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
select id, status, delivery_agreement_status
|
SELECT id, status, delivery_agreement_status
|
||||||
into v_order
|
INTO v_order
|
||||||
from public.orders
|
FROM public.orders
|
||||||
where id = v_invitation.order_id
|
WHERE id = v_invitation.order_id
|
||||||
for update;
|
FOR UPDATE;
|
||||||
|
|
||||||
if not found then
|
IF NOT found THEN
|
||||||
raise exception 'Order not found';
|
RAISE EXCEPTION 'Order not found';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
if v_order.status not in ('Ожидает ответа клиента', 'Ожидает согласования доставки') then
|
IF v_order.status NOT IN ('Ожидает ответа клиента', 'Ожидает согласования доставки') THEN
|
||||||
raise exception 'Invitation is no longer active';
|
RAISE EXCEPTION 'Invitation is no longer active';
|
||||||
end if;
|
END IF;
|
||||||
|
|
||||||
update public.delivery_invitations
|
UPDATE public.delivery_invitations
|
||||||
set
|
SET
|
||||||
state = 'agreed',
|
state = 'agreed',
|
||||||
delivery_date = p_delivery_date,
|
delivery_date = p_delivery_date,
|
||||||
delivery_time = trim(p_delivery_time),
|
delivery_time = trim(p_delivery_time),
|
||||||
confirmed_at = v_now,
|
confirmed_at = v_now,
|
||||||
access_count = coalesce(access_count, 0) + 1,
|
access_count = COALESCE(access_count, 0) + 1,
|
||||||
last_accessed_at = v_now
|
last_accessed_at = v_now
|
||||||
where id = v_invitation.id;
|
WHERE id = v_invitation.id;
|
||||||
|
|
||||||
update public.orders
|
UPDATE public.orders
|
||||||
set
|
SET
|
||||||
status = 'Доставка согласована',
|
status = 'Доставка согласована',
|
||||||
delivery_agreement_status = 'Подтверждено клиентом'
|
delivery_agreement_status = 'Подтверждено клиентом'
|
||||||
where id = v_order.id;
|
WHERE id = v_order.id;
|
||||||
|
|
||||||
insert into public.delivery_slots (
|
INSERT INTO public.delivery_slots (
|
||||||
order_id,
|
order_id,
|
||||||
delivery_date,
|
delivery_date,
|
||||||
delivery_time,
|
delivery_time,
|
||||||
logistician_id,
|
logistician_id,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
values (
|
VALUES (
|
||||||
v_order.id,
|
v_order.id,
|
||||||
p_delivery_date,
|
p_delivery_date,
|
||||||
trim(p_delivery_time),
|
trim(p_delivery_time),
|
||||||
null,
|
NULL,
|
||||||
'confirmed_by_client'
|
'confirmed_by_client'
|
||||||
);
|
);
|
||||||
|
|
||||||
insert into public.order_history (
|
INSERT INTO public.order_history (
|
||||||
order_id,
|
order_id,
|
||||||
action,
|
action,
|
||||||
old_status,
|
old_status,
|
||||||
new_status,
|
new_status,
|
||||||
metadata
|
metadata
|
||||||
)
|
)
|
||||||
values (
|
VALUES (
|
||||||
v_order.id,
|
v_order.id,
|
||||||
'Подтверждение выбора доставки клиентом',
|
'Подтверждение выбора доставки клиентом',
|
||||||
v_order.status,
|
v_order.status,
|
||||||
|
|
@ -386,14 +375,14 @@ begin
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
insert into public.integration_events (
|
INSERT INTO public.integration_events (
|
||||||
order_id,
|
order_id,
|
||||||
event_type,
|
event_type,
|
||||||
direction,
|
direction,
|
||||||
status,
|
status,
|
||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
values (
|
VALUES (
|
||||||
v_order.id,
|
v_order.id,
|
||||||
'delivery_choice_confirmed',
|
'delivery_choice_confirmed',
|
||||||
'inbound',
|
'inbound',
|
||||||
|
|
@ -404,17 +393,14 @@ begin
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonb_build_object(
|
RETURN jsonb_build_object(
|
||||||
'ok', true,
|
'ok', TRUE,
|
||||||
'orderId', v_order.id,
|
'orderId', v_order.id,
|
||||||
'status', 'Доставка согласована',
|
'status', 'Доставка согласована',
|
||||||
'deliveryAgreementStatus', 'Подтверждено клиентом'
|
'deliveryAgreementStatus', 'Подтверждено клиентом'
|
||||||
);
|
);
|
||||||
end;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
revoke all on function public.get_delivery_invitation_by_token(text) from public;
|
REVOKE ALL ON FUNCTION public.confirm_delivery_choice_by_token(text, date, text) FROM public;
|
||||||
grant execute on function public.get_delivery_invitation_by_token(text) to anon, authenticated;
|
GRANT EXECUTE ON FUNCTION public.confirm_delivery_choice_by_token(text, date, 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;
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||||
|
-- This replaces ONLY the orderItems building section for the group path.
|
||||||
|
-- Apply AFTER the base function is restored.
|
||||||
|
|
||||||
|
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||||
|
-- Step 2: Then run this migration
|
||||||
|
|
||||||
|
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, ''),
|
||||||
|
to_jsonb(v_group.order_numbers) ->> 0,
|
||||||
|
NULLIF(v_group.group_key, '')
|
||||||
|
);
|
||||||
|
v_customer_name := COALESCE(
|
||||||
|
NULLIF(v_group.customer_name, ''),
|
||||||
|
NULLIF(v_invitation.customer_name, '')
|
||||||
|
);
|
||||||
|
v_customer_phone := COALESCE(
|
||||||
|
NULLIF(v_group.customer_phone, ''),
|
||||||
|
NULLIF(v_group.customer_phone_normalized, ''),
|
||||||
|
NULLIF(v_invitation.customer_phone, '')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Build orderItems: use source_orders for real product lines if available,
|
||||||
|
-- otherwise fall back to invoice numbers from order_numbers.
|
||||||
|
v_order_items := CASE
|
||||||
|
WHEN v_group.source_orders IS NOT NULL
|
||||||
|
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||||
|
AND jsonb_array_length(v_group.source_orders) > 0
|
||||||
|
THEN COALESCE(
|
||||||
|
(SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||||
|
'quantity', '',
|
||||||
|
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||||
|
)
|
||||||
|
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
ELSE COALESCE(
|
||||||
|
(SELECT jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||||
|
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),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
END;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
@ -90,7 +90,7 @@ describe("DeliveryChoiceFlow", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("OrderCompositionPanel", () => {
|
describe("OrderCompositionPanel", () => {
|
||||||
it("renders order items with quantities when they are provided", () => {
|
it("renders with position count and collapsed state", () => {
|
||||||
const markup = renderToStaticMarkup(
|
const markup = renderToStaticMarkup(
|
||||||
<OrderCompositionPanel
|
<OrderCompositionPanel
|
||||||
invitation={{
|
invitation={{
|
||||||
|
|
@ -105,42 +105,14 @@ describe("OrderCompositionPanel", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(markup).toContain("Состав заказа");
|
expect(markup).toContain("Состав заказа");
|
||||||
expect(markup).toContain("Кухонный гарнитур");
|
expect(markup).toContain("3 поз.");
|
||||||
expect(markup).toContain("1 комплект");
|
|
||||||
expect(markup).toContain("Фурнитура Blum");
|
|
||||||
expect(markup).toContain("12 шт");
|
|
||||||
expect(markup).toContain("Монтажный комплект");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders reference and customer info when there are no order items", () => {
|
it("renders nothing when there are no order items", () => {
|
||||||
const markup = renderToStaticMarkup(
|
const markup = renderToStaticMarkup(
|
||||||
<OrderCompositionPanel invitation={{ orderNumber: "CD-240031", customerName: "Мария Волкова" }} />,
|
<OrderCompositionPanel invitation={{ orderNumber: "CD-240031" }} />,
|
||||||
);
|
|
||||||
|
|
||||||
expect(markup).toContain("Состав заказа");
|
|
||||||
expect(markup).toContain("CD-240031");
|
|
||||||
expect(markup).toContain("Мария Волкова");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders nothing when there are no order items and no reference info", () => {
|
|
||||||
const markup = renderToStaticMarkup(
|
|
||||||
<OrderCompositionPanel invitation={{}} />,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(markup).toBe("");
|
expect(markup).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles string items with pipe separator", () => {
|
|
||||||
const markup = renderToStaticMarkup(
|
|
||||||
<OrderCompositionPanel
|
|
||||||
invitation={{
|
|
||||||
orderNumber: "CD-240031",
|
|
||||||
orderItems: ["Плинтус|5 шт"],
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(markup).toContain("Плинтус");
|
|
||||||
expect(markup).toContain("5 шт");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,109 +3,98 @@ import { Badge } from "../UI/Badge";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
import { getInvitationReferenceLabel } from "./invitationReference";
|
import { getInvitationReferenceLabel } from "./invitationReference";
|
||||||
|
|
||||||
const splitOrderItem = (item) => {
|
const flattenOrderProducts = (rawItems) => {
|
||||||
if (!item) return null;
|
const products = [];
|
||||||
|
|
||||||
if (typeof item === "string") {
|
for (const item of rawItems) {
|
||||||
const [name, quantity] = item.split("|").map((p) => p.trim());
|
if (!item || typeof item !== "object") continue;
|
||||||
return { name: name || item.trim(), quantity: quantity || "", items: [] };
|
|
||||||
|
const subOrders = Array.isArray(item.items) ? item.items : [];
|
||||||
|
|
||||||
|
if (subOrders.length === 0) {
|
||||||
|
const name = String(item.product_name || item.name || item.nom || "").trim();
|
||||||
|
const qty = String(item.product_quantity || item.quantity || item.count || item.amount || "").trim();
|
||||||
|
const unit = String(item.product_ed || item.unit || "").trim();
|
||||||
|
if (name) products.push({ name, quantity: qty, unit });
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof item === "object") {
|
const hasSubOrders = subOrders.some((s) => typeof s === "object" && ("nom" in s || ("items" in s && Array.isArray(s.items))));
|
||||||
const name = typeof item.name === "string"
|
|
||||||
? item.name.trim()
|
if (hasSubOrders) {
|
||||||
: typeof item.label === "string"
|
for (const sub of subOrders) {
|
||||||
? item.label.trim()
|
if (!sub || typeof sub !== "object") continue;
|
||||||
: "";
|
const productsList = Array.isArray(sub.items) ? sub.items : [];
|
||||||
const quantity = typeof item.quantity === "string"
|
for (const p of productsList) {
|
||||||
? item.quantity.trim()
|
if (!p || typeof p !== "object") continue;
|
||||||
: typeof item.quantity === "number"
|
const pName = String(p.product_name || p.name || "").trim();
|
||||||
? String(item.quantity)
|
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
||||||
: "";
|
const pUnit = String(p.product_ed || p.unit || "").trim();
|
||||||
const nestedItems = Array.isArray(item.items)
|
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
||||||
? item.items.map((sub) => {
|
}
|
||||||
if (!sub || typeof sub !== "object") return null;
|
}
|
||||||
return {
|
} else {
|
||||||
name: String(sub.product_name || sub.name || sub.title || "").trim(),
|
for (const p of subOrders) {
|
||||||
quantity: String(sub.product_quantity || sub.quantity || sub.count || sub.amount || "").trim(),
|
if (!p || typeof p !== "object") continue;
|
||||||
};
|
const pName = String(p.product_name || p.name || "").trim();
|
||||||
}).filter(Boolean)
|
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
||||||
: [];
|
const pUnit = String(p.product_ed || p.unit || "").trim();
|
||||||
return { name: name || "Позиция", quantity, items: nestedItems };
|
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return products;
|
||||||
};
|
|
||||||
|
|
||||||
const buildDetailRows = (invitation) => {
|
|
||||||
const rows = [];
|
|
||||||
const reference = getInvitationReferenceLabel(invitation);
|
|
||||||
if (reference && reference !== "Счет —") {
|
|
||||||
rows.push({ label: "Счет", value: reference.replace(/^Счета?:\s*/, "") });
|
|
||||||
}
|
|
||||||
if (invitation.customerName) {
|
|
||||||
rows.push({ label: "Клиент", value: invitation.customerName });
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrderCompositionPanel = ({ invitation = {} }) => {
|
export const OrderCompositionPanel = ({ invitation = {} }) => {
|
||||||
const orderItems = (invitation.orderItems || invitation.items || [])
|
const rawItems = invitation.orderItems || invitation.items || [];
|
||||||
.map(splitOrderItem)
|
const products = flattenOrderProducts(rawItems);
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const detailRows = buildDetailRows(invitation);
|
|
||||||
const hasContent = orderItems.length > 0 || detailRows.length > 0;
|
|
||||||
|
|
||||||
if (!hasContent) return null;
|
|
||||||
|
|
||||||
const reference = getInvitationReferenceLabel(invitation);
|
const reference = getInvitationReferenceLabel(invitation);
|
||||||
|
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
|
||||||
|
if (products.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
|
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center justify-between text-left"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
||||||
Состав заказа {reference !== "Счет —" ? reference : ""}
|
Состав заказа {reference !== "Счет —" ? reference : ""}
|
||||||
</p>
|
</p>
|
||||||
|
<span className="flex items-center gap-2 text-sm text-[var(--color-text-muted)]">
|
||||||
{detailRows.length > 0 && !orderItems.length ? (
|
{products.length > 0 ? `${products.length} поз.` : ""}
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 transition-transform"
|
||||||
|
style={{ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)" }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isExpanded && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{detailRows.map((row) => (
|
{products.map((product, idx) => (
|
||||||
<div
|
<div
|
||||||
key={row.label}
|
key={`${product.name}-${idx}`}
|
||||||
className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm"
|
className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm"
|
||||||
>
|
>
|
||||||
<span className="leading-6 text-[var(--color-text-muted)]">{row.label}</span>
|
<span className="leading-6">{product.name}</span>
|
||||||
<span className="font-medium leading-6">{row.value}</span>
|
{(product.quantity || product.unit) ? (
|
||||||
</div>
|
<Badge tone="neutral">{[product.quantity, product.unit].filter(Boolean).join(" ")}</Badge>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{orderItems.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{orderItems.map((item) => (
|
|
||||||
<div key={`${item.name}-${item.quantity || "item"}`}>
|
|
||||||
<div className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm">
|
|
||||||
<span className="font-medium leading-6">{item.name}</span>
|
|
||||||
{item.quantity ? <Badge tone="neutral">{item.quantity}</Badge> : null}
|
|
||||||
</div>
|
|
||||||
{item.items && item.items.length > 0 ? (
|
|
||||||
<div className="mt-1 space-y-1 pl-3">
|
|
||||||
{item.items.map((sub, idx) => (
|
|
||||||
<div
|
|
||||||
key={`${sub.name}-${idx}`}
|
|
||||||
className="flex items-center justify-between gap-3 rounded-[14px] border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm"
|
|
||||||
>
|
|
||||||
<span className="leading-6">{sub.name}</span>
|
|
||||||
{sub.quantity ? <Badge tone="neutral">{sub.quantity}</Badge> : null}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -183,60 +183,6 @@ export const __resetLocalDeliveryInvitationCache = () => {
|
||||||
localDeliveryInvitationCache.clear();
|
localDeliveryInvitationCache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
const enrichOrderItemsFromEdgeFunction = (invitation, token) => {
|
|
||||||
// Fire-and-forget: try the Edge Function in the background to get richer
|
|
||||||
// order item data (with source_orders). On success, update the cache.
|
|
||||||
// The initial RPC response is returned immediately so the page renders
|
|
||||||
// without delay; the richer data arrives shortly after.
|
|
||||||
if (!token || isLocalClientInvitationToken(token)) {
|
|
||||||
return invitation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasOnlyInvoiceItems = Array.isArray(invitation.orderItems)
|
|
||||||
&& invitation.orderItems.every(
|
|
||||||
(item) => typeof item === "object" && item !== null
|
|
||||||
&& (!Array.isArray(item.items) || item.items.length === 0)
|
|
||||||
&& (!item.quantity || item.quantity === ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasOnlyInvoiceItems) {
|
|
||||||
return invitation;
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeDeliveryFunction("get-delivery-invitation", { token })
|
|
||||||
.then((response) => {
|
|
||||||
if (response?.invitation?.orderItems) {
|
|
||||||
const enriched = {
|
|
||||||
...invitation,
|
|
||||||
orderItems: response.invitation.orderItems,
|
|
||||||
};
|
|
||||||
cacheInvitation(enriched);
|
|
||||||
const storage = getLocalStorage();
|
|
||||||
if (storage) {
|
|
||||||
try {
|
|
||||||
storage.setItem(
|
|
||||||
getLocalStorageKey(token),
|
|
||||||
JSON.stringify(enriched),
|
|
||||||
);
|
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key: getLocalStorageKey(token),
|
|
||||||
newValue: JSON.stringify(enriched),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Ignore storage errors.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Silently ignore — the initial RPC data is already shown.
|
|
||||||
});
|
|
||||||
|
|
||||||
return invitation;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchDeliveryInvitation = async (token) => {
|
export const fetchDeliveryInvitation = async (token) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Token is required");
|
throw new Error("Token is required");
|
||||||
|
|
@ -262,8 +208,7 @@ export const fetchDeliveryInvitation = async (token) => {
|
||||||
p_token: token,
|
p_token: token,
|
||||||
});
|
});
|
||||||
if (response?.invitation) {
|
if (response?.invitation) {
|
||||||
const enriched = enrichOrderItemsFromEdgeFunction(response.invitation, token);
|
return cacheInvitation(response.invitation);
|
||||||
return cacheInvitation(enriched);
|
|
||||||
}
|
}
|
||||||
if (isLocalClientInvitationToken(token)) {
|
if (isLocalClientInvitationToken(token)) {
|
||||||
return cacheInvitation(buildFallbackInvitation(token));
|
return cacheInvitation(buildFallbackInvitation(token));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue