Files
route-commerce/supabase/migrations/004_admin_order_fetch.sql

193 lines
6.1 KiB
PL/PgSQL

-- ============================================================
-- Admin Read Policies for Orders
-- ============================================================
-- The checkout hardening removed broad anon SELECT policies.
-- Admin pages need to read orders with the anon key (server-side via getAdminUser).
-- We use SECURITY DEFINER functions that bypass RLS so admin pages
-- can still fetch orders for their brand scope without auth.uid().
-- No direct public SELECT (already enforced by removal of anon policies)
-- Policy: platform_admin can SELECT all orders
CREATE POLICY "Platform admin can read orders"
ON orders FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE admin_users.user_id = auth.uid()
AND admin_users.role = 'platform_admin'
)
);
-- Policy: brand_admin can read orders for their brand
CREATE POLICY "Brand admin can read their brand orders"
ON orders FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE admin_users.user_id = auth.uid()
AND admin_users.role = 'brand_admin'
AND admin_users.brand_id = (
SELECT brand_id FROM stops WHERE stops.id = orders.stop_id
)
)
);
-- Policy: stops readable by admin users
CREATE POLICY "Admins can read stops"
ON stops FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE admin_users.user_id = auth.uid()
)
);
-- ============================================================
-- Admin Orders Fetch RPC
-- ============================================================
-- SECURITY DEFINER runs as postgres — bypasses RLS on orders/stops.
CREATE OR REPLACE FUNCTION get_admin_orders(p_brand_id UUID)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_orders JSONB;
v_stops JSONB;
BEGIN
IF p_brand_id IS NULL THEN
SELECT COALESCE(jsonb_agg(t ORDER BY t.created_at DESC), '[]'::JSONB)
INTO v_orders
FROM (
SELECT o.id, o.customer_name, o.customer_email, o.customer_phone,
o.stop_id, o.status, o.subtotal, o.pickup_complete,
o.pickup_completed_at, o.pickup_completed_by, o.created_at,
CASE WHEN o.stop_id IS NOT NULL THEN jsonb_build_object(
'id', s.id, 'city', s.city, 'state', s.state,
'date', s.date, 'time', s.time, 'location', s.location, 'brand_id', s.brand_id
) END as stops
FROM orders o LEFT JOIN stops s ON o.stop_id = s.id
WHERE o.stop_id IS NOT NULL
LIMIT 500
) t;
SELECT COALESCE(jsonb_agg(jsonb_build_object(
'id', id, 'city', city, 'state', state,
'date', date, 'time', time, 'location', location, 'brand_id', brand_id
) ORDER BY date), '[]'::JSONB)
INTO v_stops FROM stops WHERE active = true;
ELSE
SELECT COALESCE(jsonb_agg(t ORDER BY t.created_at DESC), '[]'::JSONB)
INTO v_orders
FROM (
SELECT o.id, o.customer_name, o.customer_email, o.customer_phone,
o.stop_id, o.status, o.subtotal, o.pickup_complete,
o.pickup_completed_at, o.pickup_completed_by, o.created_at,
jsonb_build_object(
'id', s.id, 'city', s.city, 'state', s.state,
'date', s.date, 'time', s.time, 'location', s.location, 'brand_id', s.brand_id
) as stops
FROM orders o JOIN stops s ON o.stop_id = s.id
WHERE s.brand_id = p_brand_id
LIMIT 500
) t;
SELECT COALESCE(jsonb_agg(jsonb_build_object(
'id', id, 'city', city, 'state', state,
'date', date, 'time', time, 'location', location, 'brand_id', brand_id
) ORDER BY date), '[]'::JSONB)
INTO v_stops FROM stops WHERE active = true AND brand_id = p_brand_id;
END IF;
RETURN jsonb_build_object('orders', v_orders, 'stops', v_stops);
END;
$$;
-- ============================================================
-- Admin Order Detail RPC
-- ============================================================
CREATE OR REPLACE FUNCTION get_admin_order_detail(p_order_id UUID)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_order JSONB;
BEGIN
SELECT jsonb_build_object(
'id', o.id,
'customer_name', o.customer_name,
'customer_email', o.customer_email,
'customer_phone', o.customer_phone,
'stop_id', o.stop_id,
'status', o.status,
'subtotal', o.subtotal,
'discount_amount', o.discount_amount,
'tax_amount', o.tax_amount,
'tax_rate', o.tax_rate,
'tax_location', o.tax_location,
'discount_reason', o.discount_reason,
'pickup_complete', o.pickup_complete,
'pickup_completed_at', o.pickup_completed_at,
'pickup_completed_by', o.pickup_completed_by,
'internal_notes', o.internal_notes,
'payment_processor', o.payment_processor,
'payment_status', o.payment_status,
'payment_transaction_id', o.payment_transaction_id,
'refunded_amount', o.refunded_amount,
'refund_reason', o.refund_reason,
'created_at', o.created_at,
'stops', CASE WHEN o.stop_id IS NOT NULL THEN jsonb_build_object(
'id', s.id,
'city', s.city,
'state', s.state,
'date', s.date,
'time', s.time,
'location', s.location,
'brand_id', s.brand_id
) END,
'order_items', COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'id', oi.id,
'product_id', oi.product_id,
'product_name', p.name,
'quantity', oi.quantity,
'price', oi.price,
'fulfillment', oi.fulfillment,
'products', jsonb_build_object('name', p.name)
))
FROM order_items oi
JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = o.id
), '[]'::JSONB),
'refunds', COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'id', r.id,
'order_id', r.order_id,
'amount', r.amount,
'reason', r.reason,
'processor', r.processor,
'processor_refund_id', r.processor_refund_id,
'status', r.status,
'created_at', r.created_at
))
FROM refunds r
WHERE r.order_id = o.id
), '[]'::JSONB)
)
INTO v_order
FROM orders o
LEFT JOIN stops s ON o.stop_id = s.id
WHERE o.id = p_order_id;
RETURN v_order;
END;
$$;