From a879ad01c36d866c220a4b3954805a74c090f2f1 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 23 Jun 2026 05:12:00 +0000 Subject: [PATCH] fix: get_delivery_invitation_by_token returns full order composition with items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RPC was returning only top-level invoice numbers (nom) without the nested product items inside orderList[].items[]. Now returns {name, quantity, items} structure matching flattenOrderProducts() expectations in OrderCompositionPanel. Before: orderItems = [{name: 'ТП-008590', quantity: '1'}] After: orderItems = [{name: 'ТП-008590', items: [{name: 'СФ Т\ЕА-28944', items: [{product_name, product_quantity, product_ed}]}]}] Also includes stop words, delivery type, pickup code, payed_ship fields. --- docs/sql/public-delivery-choice-rpc.sql | 358 +++++++----------------- 1 file changed, 106 insertions(+), 252 deletions(-) diff --git a/docs/sql/public-delivery-choice-rpc.sql b/docs/sql/public-delivery-choice-rpc.sql index 5b1fc94..1a935a1 100644 --- a/docs/sql/public-delivery-choice-rpc.sql +++ b/docs/sql/public-delivery-choice-rpc.sql @@ -1,9 +1,6 @@ --- 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 +-- Fix: get_delivery_invitation_by_token — return full order composition with items +-- Problem: RPC returned only top-level nom (invoice number), not the product items inside orderList[].items[] +-- Solution: Return nested structure {name, quantity, items: [{name, quantity, unit}]} matching flattenOrderProducts() expectations CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text) RETURNS jsonb @@ -21,14 +18,22 @@ DECLARE v_customer_name text; v_customer_phone text; v_order_items jsonb; - v_order_numbers jsonb; + v_stop_words jsonb; + v_delivery_address text; + v_customer_address text; + v_delivery_type text; + v_pickup_code text; + v_is_payed_ship boolean; + v_payed_ship text; + v_pickup_date text; + v_pickup_time_slot text; 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'); + v_token_hash := encode(extensions.digest(p_token, 'sha256'), 'hex'); SELECT * INTO v_invitation @@ -47,6 +52,10 @@ BEGIN RAISE EXCEPTION 'Invitation expired'; END IF; + SELECT coalesce(jsonb_agg(word), '[]'::jsonb) + INTO v_stop_words + FROM public.stop_words; + IF v_invitation.order_group_id IS NOT NULL THEN SELECT * INTO v_group @@ -60,6 +69,7 @@ BEGIN v_state := CASE WHEN v_group.delivery_status = 'agreed' THEN 'agreed' WHEN v_group.delivery_status = 'delivered' THEN 'delivered' + WHEN v_group.delivery_status = 'pickup' THEN 'agreed' WHEN v_invitation.state IN ('awaiting_choice', 'opened', 'reminder_sent') THEN v_invitation.state ELSE 'default' END; @@ -80,60 +90,114 @@ BEGIN 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, '') + v_group.source_orders->0->>'customerName' ); - -- 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', ''), + v_customer_phone := COALESCE( + NULLIF(v_group.customer_phone, ''), + v_group.source_orders->0->>'customerPhone' + ); + + -- Build orderItems: preserve nested structure {name, quantity, items: [...]} + -- so flattenOrderProducts() in OrderCompositionPanel.jsx can extract individual products + IF v_group.source_orders IS NOT NULL AND jsonb_typeof(v_group.source_orders) = 'array' THEN + SELECT jsonb_agg(jsonb_build_object( + 'name', COALESCE( + NULLIF(src ->> 'nom', ''), + NULLIF(src ->> 'name', ''), + 'Товар' + ), + 'quantity', '', + 'items', COALESCE( + (SELECT jsonb_agg(jsonb_build_object( + 'name', COALESCE(ol ->> 'nom', ol ->> 'name', ''), 'quantity', '', - 'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb) - ) - ) FROM jsonb_array_elements(v_group.source_orders) AS src), - '[]'::jsonb - ) - ELSE COALESCE( + 'items', COALESCE(ol -> 'items', '[]'::jsonb) + )) + FROM jsonb_array_elements(src -> 'orderList') AS ol + WHERE jsonb_typeof(ol -> 'items') = 'array' + AND jsonb_array_length(ol -> 'items') > 0), + (SELECT jsonb_agg(jsonb_build_object( + 'name', COALESCE(ol ->> 'nom', ol ->> 'name', ''), + 'quantity', '' + )) + FROM jsonb_array_elements(src -> 'orderList') AS ol), + '[]'::jsonb + ) + )) + INTO v_order_items + FROM jsonb_array_elements(v_group.source_orders) AS src + WHERE jsonb_typeof(src -> 'orderList') = 'array'; + END IF; + + IF v_order_items IS NULL THEN + -- Fallback: use invoice numbers from order_numbers + SELECT 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 - ) + ) INTO v_order_items; + END IF; + + v_delivery_address := COALESCE( + NULLIF(v_group.delivery_address, ''), + v_group.source_orders->0->>'adress', + v_group.source_orders->0->>'address', + '' + ); + + v_customer_address := COALESCE( + NULLIF(v_group.customer_address, ''), + '' + ); + + v_delivery_address := CASE + WHEN v_delivery_address ILIKE '%САМОВЫВОЗ%' THEN '' + ELSE v_delivery_address END; + -- Pickup fields + v_delivery_type := COALESCE(NULLIF(v_group.delivery_type, ''), 'delivery'); + v_pickup_code := COALESCE(NULLIF(v_group.source_orders->0->>'rnd', ''), NULLIF(v_group.pickup_code, '')); + v_is_payed_ship := COALESCE(v_group.is_payed_ship, false); + v_payed_ship := COALESCE(NULLIF(v_group.payed_ship, ''), v_group.source_orders->0->>'payed_ship', ''); + v_pickup_date := CASE WHEN v_group.pickup_date IS NOT NULL THEN v_group.pickup_date::text ELSE NULL END; + v_pickup_time_slot := COALESCE(NULLIF(v_group.pickup_time_slot, ''), NULLIF(v_group.source_orders->0->>'pickup_time_slot', '')); + RETURN jsonb_build_object( 'ok', TRUE, 'invitation', jsonb_build_object( - 'orderId', COALESCE(v_invitation.order_group_id, v_group.id)::text, + 'orderId', COALESCE(v_invitation.order_group_id, v_invitation.order_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, + 'orderItems', COALESCE(v_order_items, '[]'::jsonb), 'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb), 'deliveryDate', v_invitation.delivery_date, 'deliveryTime', v_invitation.delivery_time, 'orderStatus', NULL, - 'deliveryAgreementStatus', NULL + 'deliveryAgreementStatus', NULL, + 'stopWords', COALESCE(v_stop_words, '[]'::jsonb), + 'deliveryType', v_delivery_type, + 'deliveryAddress', v_delivery_address, + 'customerAddress', v_customer_address, + 'isPayedShip', v_is_payed_ship, + 'payedShip', NULLIF(v_payed_ship, ''), + 'pickupCode', NULLIF(v_pickup_code, ''), + 'pickupDate', v_pickup_date, + 'pickupTimeSlot', NULLIF(v_pickup_time_slot, '') ) ); END IF; + -- Legacy single-order path SELECT id, order_number, status, delivery_agreement_status, customer INTO v_order FROM public.orders @@ -185,227 +249,17 @@ BEGIN 'deliveryDate', v_invitation.delivery_date, 'deliveryTime', v_invitation.delivery_time, 'orderStatus', v_order.status, - 'deliveryAgreementStatus', v_order.delivery_agreement_status + 'deliveryAgreementStatus', v_order.delivery_agreement_status, + 'stopWords', COALESCE(v_stop_words, '[]'::jsonb), + 'deliveryType', 'delivery', + 'deliveryAddress', '', + 'customerAddress', '', + 'isPayedShip', false, + 'payedShip', NULL ) ); 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; - -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; - - -- Business rule: never allow Sunday delivery (0 = Sunday in extract(dow)) - IF extract(dow from p_delivery_date) = 0 THEN - RAISE EXCEPTION 'Delivery is not available on Sunday'; - 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.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; +GRANT EXECUTE ON FUNCTION public.get_delivery_invitation_by_token(text) TO anon, authenticated; \ No newline at end of file