-- 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 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_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(extensions.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; 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 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_group.delivery_status = 'pickup' THEN 'agreed' 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, ''), v_group.source_orders->0->>'customerName' ); 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(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_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', 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, '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 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, '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;