chore: commit tour-seed scripts and delete_stop 400 fix from prior work

* inspect_xlsx.py — one-off xlsx dumper used to discover the Stop
  Directory sheet structure (14 lines, kept for reference when the
  tour schedule xlsx is updated next season).
* scripts/seed_tuxedo_tour.py — Python+openpyxl seed script that
  parsed the 3-week Tuxedo Corn 2026 tour schedule and inserted
  269 stops via admin_create_stops_batch. Also linked each stop to
  its Stop Directory venue (address/phone/contact) and self-published
  by flipping status='draft' -> 'active'.
* supabase/migrations/202_delete_stop_orders_guard_fix.sql — fixes a
  400 error on the admin Stop Table delete button. The delete_stop
  RPC referenced o.deleted_at on the orders table, but that column
  doesn't exist, so the SELECT raised 'column does not exist' and
  PostgREST surfaced it as HTTP 400. The fix drops the redundant
  o.deleted_at IS NULL clause (pickup_complete = false is sufficient
  on its own); re-add it here if/when orders grows a deleted_at
  column.
This commit is contained in:
2026-06-04 15:32:16 +00:00
parent f26d43ac96
commit 1feba25ced
3 changed files with 430 additions and 0 deletions
@@ -0,0 +1,70 @@
-- Migration 202: Fix delete_stop RPC — orders table has no deleted_at column
--
-- The delete_stop RPC added in migration 075 guards against deleting a stop
-- that has active orders:
--
-- SELECT COUNT(*) INTO v_active_order_count
-- FROM orders o JOIN stops s ON o.stop_id = s.id
-- WHERE s.id = p_stop_id
-- AND o.pickup_complete = false
-- AND o.deleted_at IS NULL; -- <-- column does not exist on orders
--
-- The orders table never got a deleted_at column, so the SELECT raises
-- "column o.deleted_at does not exist" and the RPC returns HTTP 400 from
-- PostgREST. Stop deletion has been silently broken since 075 landed.
--
-- Fix: drop the o.deleted_at IS NULL clause. The pickup_complete = false
-- guard is sufficient for now (no soft-deleted orders can exist).
-- If/when orders get a deleted_at column, re-add the clause.
--
-- This migration only touches the function. Schema is unchanged.
CREATE OR REPLACE FUNCTION public.delete_stop(p_stop_id UUID, p_brand_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_stop RECORD;
v_active_order_count INT;
BEGIN
-- Lock the stop row for update
SELECT * INTO v_stop FROM stops WHERE id = p_stop_id FOR UPDATE;
IF NOT FOUND THEN
RETURN jsonb_build_object('success', false, 'error', 'Stop not found');
END IF;
-- Brand scoping: brand_admin can only delete their own stops
IF p_brand_id IS NOT NULL AND v_stop.brand_id != p_brand_id THEN
RETURN jsonb_build_object('success', false, 'error', 'Stop not found');
ELSIF p_brand_id IS NULL AND v_stop.brand_id IS NOT NULL THEN
-- Caller didn't pass brand_id but stop has one — guard against platform admin accident
RETURN jsonb_build_object('success', false, 'error', 'Stop not found');
END IF;
-- Guard: refuse to delete a stop that still has active (un-picked-up) orders.
-- Note: orders has no deleted_at column (hard delete pattern) so we only
-- check pickup_complete. Re-add `AND o.deleted_at IS NULL` if/when orders
-- grows a soft-delete column.
SELECT COUNT(*) INTO v_active_order_count
FROM orders o
JOIN stops s ON o.stop_id = s.id
WHERE s.id = p_stop_id
AND o.pickup_complete = false;
IF v_active_order_count > 0 THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Cannot delete stop — it has ' || v_active_order_count || ' active order(s). Mark orders as picked up first.'
);
END IF;
-- Soft delete
UPDATE stops SET deleted_at = NOW() WHERE id = p_stop_id;
RETURN jsonb_build_object('success', true);
END;
$$;
-- Grants are inherited from 075; verify they're present (no-op if so).
GRANT EXECUTE ON FUNCTION public.delete_stop(UUID, UUID) TO anon, authenticated, service_role;