chore: improve Supabase migrations via CLI after login and fix compatibility issues

- Update supabase/push-migrations.js:
  - Detect modern Supabase CLI link state (supabase/.temp/project-ref)
  - Prefer `supabase db query --linked --file` (works in this env where direct postgres connections fail with ENOTFOUND/network unreachable)
  - Updated docs/comments for `supabase login` + `supabase link --project-ref wnzkhezyhnfzhkhiflrp` workflow

- Add MEMORY.md capturing the Supabase login/link session, tooling changes, migration patches applied, and gotchas

- Patch migrations for current remote schema (column drift, syntax, idempotency, pre-existing tables):
  - 091_brand_plan_tier.sql: fix extra paren in get_brand_plan_info
  - 145_create_product_images_bucket.sql: allowed_mime_types + DROP POLICY IF EXISTS + safer bucket insert
  - 148_public_stops_rpc.sql: quote reserved "time" column in RETURNS TABLE + SELECT
  - 200_production_features.sql: add ALTER TABLE for user_activity_logs (originated in 036) so policies/indexes validate
  - 201_seed_data.sql: trim demo seeds with outdated column lists (products/stops/etc.); keep only compatible brands insert

- Include 202_fix_admin_create_stop.sql and related stop creation updates (src/actions/stops/create-stop.ts, 147_admin_create_stop_rpcs.sql, ADMIN_CREATE_STOP_FIX.sql)

- Update CLAUDE.md with pointer to MEMORY.md for recent migration work

Applied via the new flow (after supabase login + link): 084, 091, 142–148, 200–202 etc.

Refs: Supabase CLI now linked; use `node supabase/push-migrations.js <prefix>` or `npm run migrate:one NNN`
This commit is contained in:
2026-06-03 15:11:42 +00:00
parent f155bf6f5c
commit ba94d755fa
12 changed files with 653 additions and 296 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ BEGIN
-- Count current usage
SELECT COUNT(*) INTO v_user_count FROM admin_users WHERE brand_id = p_brand_id AND deleted_at IS NULL;
SELECT COUNT(*) INTO v_active_stops FROM stops WHERE brand_id = p_brand_id AND active = true AND date >= date_trunc('month', now()));
SELECT COUNT(*) INTO v_active_stops FROM stops WHERE brand_id = p_brand_id AND active = true AND date >= date_trunc('month', now());
SELECT COUNT(*) INTO v_product_count FROM products WHERE brand_id = p_brand_id AND deleted_at IS NULL;
SELECT jsonb_build_object(
@@ -1,13 +1,17 @@
-- Create product-images bucket for product images
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowedMimeTypes)
VALUES ('product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp'])
ON CONFLICT (id) DO NOTHING;
-- Create product-images bucket for product images (idempotent)
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
SELECT 'product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp']
WHERE NOT EXISTS (
SELECT 1 FROM storage.buckets WHERE id = 'product-images' OR name = 'product-images'
);
-- Enable public access to product-images bucket
-- Enable public access to product-images bucket (re-runnable)
DROP POLICY IF EXISTS "Public can view product-images" ON storage.objects;
CREATE POLICY "Public can view product-images"
ON storage.objects FOR SELECT
USING (bucket_id = 'product-images');
DROP POLICY IF EXISTS "Admins can upload product-images" ON storage.objects;
CREATE POLICY "Admins can upload product-images"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'product-images');
@@ -16,14 +16,36 @@
-- p_brand_id for explicit brand scoping. The application layer
-- (server actions) still validates the caller has can_manage_stops
-- via getAdminUser() before invoking the RPC.
--
-- Table schema (relevant columns):
-- id UUID PK
-- brand_id UUID FK brands
-- city TEXT
-- state TEXT
-- location TEXT
-- date TIMESTAMPTZ
-- time TEXT -- free-form ("8:00 AM 2:00 PM")
-- address TEXT
-- zip TEXT
-- cutoff_time TIMESTAMPTZ -- accepts ISO datetime OR "HH:MM"
-- slug TEXT
-- active BOOLEAN
-- status TEXT -- 'draft' | 'active' | ...
BEGIN;
-- ── 1. admin_create_stop ───────────────────────────────────────────────────
-- Inserts a single stop. Slug is derived from city + date; if a stop with
-- the same slug already exists for the brand, a numeric suffix is appended.
--
-- Robust type handling:
-- * p_date TEXT -> cast to TIMESTAMPTZ; NULLIF handles empty string
-- * p_cutoff_time TEXT -> accepts full ISO datetime ("2025-01-01T08:00")
-- OR a time-only value ("08:00" / "08:00:00") which is combined with
-- p_date to form a TIMESTAMPTZ. NULLIF handles empty string.
-- * p_time TEXT is passed through as-is (the column is free-form text).
CREATE OR REPLACE FUNCTION admin_create_stop(
CREATE OR REPLACE FUNCTION public.admin_create_stop(
p_brand_id UUID,
p_city TEXT,
p_state TEXT,
@@ -37,13 +59,16 @@ CREATE OR REPLACE FUNCTION admin_create_stop(
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = public
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_slug TEXT;
v_slug_base TEXT;
v_id UUID;
v_counter INT := 0;
v_date_value TIMESTAMPTZ;
v_cutoff_val TIMESTAMPTZ;
v_slug TEXT;
v_slug_base TEXT;
v_id UUID;
v_counter INT := 0;
BEGIN
IF p_brand_id IS NULL THEN
RAISE EXCEPTION 'brand_id is required';
@@ -52,57 +77,83 @@ BEGIN
RAISE EXCEPTION 'city is required';
END IF;
-- ── Parse p_date ─────────────────────────────────────────────────────────
v_date_value := NULLIF(trim(p_date), '')::TIMESTAMPTZ;
IF v_date_value IS NULL THEN
RAISE EXCEPTION 'date is required';
END IF;
-- ── Parse p_cutoff_time (accepts ISO datetime OR "HH:MM[:SS]") ───────────
IF p_cutoff_time IS NULL OR length(trim(p_cutoff_time)) = 0 THEN
v_cutoff_val := NULL;
ELSIF p_cutoff_time ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
-- Time-only value — combine with the stop's date
v_cutoff_val := (v_date_value::DATE + trim(p_cutoff_time)::TIME)::TIMESTAMPTZ;
ELSE
-- Assume a full ISO datetime string
v_cutoff_val := trim(p_cutoff_time)::TIMESTAMPTZ;
END IF;
-- ── Derive unique slug ───────────────────────────────────────────────────
v_slug_base := lower(regexp_replace(trim(p_city), '\s+', '-', 'g'))
|| '-' || COALESCE(NULLIF(trim(p_date), ''), CURRENT_DATE::TEXT);
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
v_slug := v_slug_base;
-- Ensure unique slug per brand by appending a counter if needed
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
v_counter := v_counter + 1;
v_slug := v_slug_base || '-' || v_counter;
END LOOP;
-- ── Insert ───────────────────────────────────────────────────────────────
INSERT INTO stops (
brand_id, city, state, location, date, time, slug,
address, zip, cutoff_time, active, status
) VALUES (
p_brand_id, p_city, p_state, p_location, p_date, p_time, v_slug,
p_address, p_zip, p_cutoff_time, p_active, 'draft'
p_brand_id, p_city, p_state, p_location,
v_date_value, p_time, v_slug,
p_address, p_zip, v_cutoff_val, p_active, 'draft'
)
RETURNING id INTO v_id;
RETURN jsonb_build_object('id', v_id, 'slug', v_slug);
EXCEPTION WHEN OTHERS THEN
-- Re-raise as a structured error so the client gets a useful message
RAISE EXCEPTION 'admin_create_stop failed: % (SQLSTATE %)', SQLERRM, SQLSTATE;
END;
$$;
-- ── 2. admin_create_stops_batch ────────────────────────────────────────────
-- Inserts many stops in one call. Returns JSONB with created IDs + slugs.
-- On any per-row failure the whole batch rolls back (transactional).
-- Same robust date / cutoff_time handling as the single-row RPC.
CREATE OR REPLACE FUNCTION admin_create_stops_batch(
CREATE OR REPLACE FUNCTION public.admin_create_stops_batch(
p_brand_id UUID,
p_stops JSONB -- array of {city, state, location, date, time, address?, zip?, cutoff_time?, active?}
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = public
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_row JSONB;
v_result JSONB := '[]'::JSONB;
v_slug_base TEXT;
v_slug TEXT;
v_counter INT;
v_id UUID;
v_city TEXT;
v_state TEXT;
v_location TEXT;
v_date TEXT;
v_time TEXT;
v_address TEXT;
v_zip TEXT;
v_cutoff TEXT;
v_active BOOLEAN;
v_row JSONB;
v_result JSONB := '[]'::JSONB;
v_slug_base TEXT;
v_slug TEXT;
v_counter INT;
v_id UUID;
v_city TEXT;
v_state TEXT;
v_location TEXT;
v_date_text TEXT;
v_time TEXT;
v_address TEXT;
v_zip TEXT;
v_cutoff_txt TEXT;
v_active BOOLEAN;
v_date_value TIMESTAMPTZ;
v_cutoff_val TIMESTAMPTZ;
BEGIN
IF p_brand_id IS NULL THEN
RAISE EXCEPTION 'brand_id is required';
@@ -113,22 +164,37 @@ BEGIN
END IF;
FOR v_row IN SELECT * FROM jsonb_array_elements(p_stops) LOOP
v_city := v_row->>'city';
v_state := v_row->>'state';
v_location := v_row->>'location';
v_date := v_row->>'date';
v_time := v_row->>'time';
v_address := v_row->>'address';
v_zip := v_row->>'zip';
v_cutoff := v_row->>'cutoff_time';
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
v_city := v_row->>'city';
v_state := v_row->>'state';
v_location := v_row->>'location';
v_date_text := v_row->>'date';
v_time := v_row->>'time';
v_address := v_row->>'address';
v_zip := v_row->>'zip';
v_cutoff_txt := v_row->>'cutoff_time';
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
IF v_city IS NULL OR length(trim(v_city)) = 0 THEN
RAISE EXCEPTION 'city is required for all stops';
END IF;
-- Parse date
v_date_value := NULLIF(trim(v_date_text), '')::TIMESTAMPTZ;
IF v_date_value IS NULL THEN
RAISE EXCEPTION 'date is required for all stops';
END IF;
-- Parse cutoff_time (accepts ISO datetime OR "HH:MM[:SS]")
IF v_cutoff_txt IS NULL OR length(trim(v_cutoff_txt)) = 0 THEN
v_cutoff_val := NULL;
ELSIF v_cutoff_txt ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
v_cutoff_val := (v_date_value::DATE + trim(v_cutoff_txt)::TIME)::TIMESTAMPTZ;
ELSE
v_cutoff_val := trim(v_cutoff_txt)::TIMESTAMPTZ;
END IF;
v_slug_base := lower(regexp_replace(trim(v_city), '\s+', '-', 'g'))
|| '-' || COALESCE(NULLIF(trim(v_date), ''), CURRENT_DATE::TEXT);
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
v_slug := v_slug_base;
v_counter := 0;
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
@@ -140,8 +206,9 @@ BEGIN
brand_id, city, state, location, date, time, slug,
address, zip, cutoff_time, active, status
) VALUES (
p_brand_id, v_city, v_state, v_location, v_date, v_time, v_slug,
v_address, v_zip, v_cutoff, v_active, 'draft'
p_brand_id, v_city, v_state, v_location,
v_date_value, v_time, v_slug,
v_address, v_zip, v_cutoff_val, v_active, 'draft'
)
RETURNING id INTO v_id;
@@ -149,7 +216,29 @@ BEGIN
END LOOP;
RETURN v_result;
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION 'admin_create_stops_batch failed: % (SQLSTATE %)', SQLERRM, SQLSTATE;
END;
$$;
-- ── 3. Grants ──────────────────────────────────────────────────────────────
-- SECURITY DEFINER runs as the function owner (postgres), so RLS is bypassed.
-- But PostgREST still needs explicit EXECUTE grants to expose the RPC to anon
-- (used by dev / unauthenticated public flows) and authenticated / service_role.
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN
) TO anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.admin_create_stops_batch(UUID, JSONB)
TO anon, authenticated, service_role;
-- ── 4. Reload PostgREST schema cache ───────────────────────────────────────
-- Without this, PostgREST will keep using its cached function list and return
-- PGRST202 ("function not found in schema") until the cache expires naturally.
-- Other migrations in this repo do this; 147 was missing it and was the
-- most common cause of PGRST202 right after applying the migration.
NOTIFY pgrst, 'reload schema';
COMMIT;
+2 -2
View File
@@ -13,7 +13,7 @@ RETURNS TABLE (
city TEXT,
state TEXT,
date TIMESTAMPTZ,
time TEXT,
"time" TEXT,
location TEXT,
address TEXT,
slug TEXT,
@@ -30,7 +30,7 @@ BEGIN
s.city,
s.state,
s.date,
s.time,
s."time",
s.location,
s.address,
s.slug,
@@ -103,6 +103,14 @@ CREATE TABLE IF NOT EXISTS user_activity_logs (
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- If table pre-existed from 036 (without brand_id etc), add the columns now so policies below succeed
ALTER TABLE user_activity_logs
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS action VARCHAR(100),
ADD COLUMN IF NOT EXISTS resource_type VARCHAR(50),
ADD COLUMN IF NOT EXISTS resource_id UUID,
ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
-- =============================================================================
-- API KEYS FOR EXTERNAL INTEGRATIONS
-- =============================================================================
+6 -215
View File
@@ -13,220 +13,11 @@ VALUES
('c3d4e5f6-a7b8-9012-cdef-123456789012', 'Orchard Fresh', 'orchard-fresh', 'starter', 2, 10, 50, 'cus_demo_orchard', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO PRODUCTS FOR SUNRISE FARMS
-- =============================================================================
INSERT INTO products (id, brand_id, name, description, price, unit, category, sku, is_active, is_taxable, image_url, created_at)
VALUES
('p0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Honeycrisp Apples', 'Premium organic Honeycrisp apples, sweet and crisp', 3.99, 'lb', 'Apples', 'APP-HC-001', true, true, NULL, NOW()),
('p0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Gala Apples', 'Sweet and mild Gala apples, perfect for snacking', 3.49, 'lb', 'Apples', 'APP-GA-001', true, true, NULL, NOW()),
('p0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Valencia Oranges', 'Fresh Valencia oranges, great for juicing', 2.99, 'lb', 'Oranges', 'ORG-VA-001', true, true, NULL, NOW()),
('p0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Navel Oranges', 'Seedless Navel oranges, sweet and easy to peel', 3.29, 'lb', 'Oranges', 'ORG-NA-001', true, true, NULL, NOW()),
('p0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Ruby Red Grapefruit', 'Juicy Ruby Red grapefruit, perfectly balanced sweet-tart', 3.79, 'lb', 'Citrus', 'GRF-RR-001', true, true, NULL, NOW()),
('p0010006-0000-0000-0000-000000000006', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Pink Lady Apples', 'Crisp and sweet Pink Lady apples', 4.29, 'lb', 'Apples', 'APP-PL-001', true, true, NULL, NOW()),
('p0010007-0000-0000-0000-000000000007', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Tangerines', 'Sweet and juicy tangerines, easy to peel', 4.49, 'lb', 'Citrus', 'CIT-TN-001', true, true, NULL, NOW()),
('p0010008-0000-0000-0000-000000000008', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Meyer Lemons', 'Fragrant Meyer lemons, sweeter than regular lemons', 4.99, 'lb', 'Lemons', 'LEM-ME-001', true, true, NULL, NOW()),
('p0010009-0000-0000-0000-000000000009', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Lime Mix Pack', 'Seasonal mix of Persian and Key limes', 5.99, 'lb', 'Lemons', 'LIM-MX-001', true, true, NULL, NOW()),
('p0010010-0000-0000-0000-000000000010', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Mixed Citrus Box', 'Seasonal assortment of 5+ citrus varieties, 20lb box', 54.99, 'box', 'Boxes', 'BOX-MC-001', true, true, NULL, NOW())
ON CONFLICT (id) DO NOTHING;
-- NOTE: The remainder of the original seed data (products, stops, orders, water logs, etc.)
-- has been removed from this migration because the column layouts in the live DB
-- (shaped by many prior migrations) no longer match the INSERTs here.
-- The 3 demo brands above will be inserted (now that plan_tier/max_* columns exist).
-- Use the admin UI or separate scripts to populate test data as needed.
-- =============================================================================
-- DEMO STOPS FOR SUNRISE FARMS
-- =============================================================================
INSERT INTO stops (id, brand_id, name, address, city, state, postal_code, scheduled_at, status, created_at)
VALUES
('s0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Downtown Farmers Market', '123 Main Street', 'Orlando', 'FL', '32801', NOW() + INTERVAL '2 days', 'scheduled', NOW()),
('s0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Winter Park Community', '456 Park Avenue', 'Winter Park', 'FL', '32789', NOW() + INTERVAL '3 days', 'scheduled', NOW()),
('s0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Maitland Office Park', '789 Commerce Blvd', 'Maitland', 'FL', '32751', NOW() + INTERVAL '5 days', 'scheduled', NOW()),
('s0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'UCF Campus Store', '4000 Central Florida Blvd', 'Orlando', 'FL', '32816', NOW() + INTERVAL '7 days', 'scheduled', NOW()),
('s0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Lake Mary Corporate', '1000 Business Center Dr', 'Lake Mary', 'FL', '32746', NOW() + INTERVAL '10 days', 'scheduled', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO CUSTOMERS
-- =============================================================================
INSERT INTO customers (id, brand_id, email, first_name, last_name, phone, company, created_at)
VALUES
('c0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'john.smith@email.com', 'John', 'Smith', '+1-407-555-0101', 'Smith Family Farm', NOW()),
('c0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'sarah.jones@email.com', 'Sarah', 'Jones', '+1-407-555-0102', 'Jones Organic Co', NOW()),
('c0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'mike.wilson@email.com', 'Mike', 'Wilson', '+1-407-555-0103', 'Wilson Grocers', NOW()),
('c0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'emily.brown@email.com', 'Emily', 'Brown', '+1-407-555-0104', 'Brown Cafe', NOW()),
('c0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'david.lee@email.com', 'David', 'Lee', '+1-407-555-0105', 'Lee Restaurant Group', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO WHOLESALE CUSTOMERS
-- =============================================================================
INSERT INTO wholesale_customers (id, brand_id, company_name, contact_name, email, phone, credit_limit, payment_terms, is_active, created_at)
VALUES
('w0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Whole Foods Market', 'Amanda Green', 'amanda.g@wholefoods.com', '+1-512-555-0201', 10000.00, 'net30', true, NOW()),
('w0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Publix Super Markets', 'Robert Baker', 'robert.b@publix.com', '+1-863-555-0202', 25000.00, 'net30', true, NOW()),
('w0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Fresh Market', 'Lisa Chen', 'lisa.c@freshmarket.com', '+1-919-555-0203', 15000.00, 'net15', true, NOW()),
('w0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'Trader Joes', 'Mark Johnson', 'mark.j@traderjoes.com', '+1-626-555-0204', 20000.00, 'net30', true, NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO ORDERS
-- =============================================================================
INSERT INTO orders (id, brand_id, customer_id, status, fulfillment, total_amount, customer_email, customer_name, customer_address, created_at)
VALUES
('o0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'c0010001-0000-0000-0000-000000000001', 'completed', 'pickup', 45.87, 'john.smith@email.com', 'John Smith', '123 Main St, Orlando FL 32801', NOW() - INTERVAL '7 days'),
('o0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'c0010002-0000-0000-0000-000000000002', 'completed', 'pickup', 62.34, 'sarah.jones@email.com', 'Sarah Jones', '456 Oak Ave, Winter Park FL 32789', NOW() - INTERVAL '5 days'),
('o0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'c0010003-0000-0000-0000-000000000003', 'confirmed', 'pickup', 38.92, 'mike.wilson@email.com', 'Mike Wilson', '789 Pine St, Maitland FL 32751', NOW() - INTERVAL '2 days'),
('o0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'c0010004-0000-0000-0000-000000000004', 'pending', 'ship', 89.55, 'emily.brown@email.com', 'Emily Brown', '321 Elm Blvd, Orlando FL 32816', NOW() - INTERVAL '1 day'),
('o0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'c0010005-0000-0000-0000-000000000005', 'preparing', 'mixed', 156.78, 'david.lee@email.com', 'David Lee', '555 Commerce Dr, Lake Mary FL 32746', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO ORDER ITEMS
-- =============================================================================
INSERT INTO order_items (id, order_id, product_id, quantity, price, fulfillment, created_at)
VALUES
('oi001001-0000-0000-0000-0000000001', 'o0010001-0000-0000-0000-000000000001', 'p0010001-0000-0000-0000-000000000001', 5, 3.99, 'pickup', NOW() - INTERVAL '7 days'),
('oi001002-0000-0000-0000-0000000002', 'o0010001-0000-0000-0000-000000000001', 'p0010003-0000-0000-0000-000000000003', 8, 2.99, 'pickup', NOW() - INTERVAL '7 days'),
('oi002001-0000-0000-0000-0000000003', 'o0010002-0000-0000-0000-000000000002', 'p0010002-0000-0000-0000-000000000002', 10, 3.49, 'pickup', NOW() - INTERVAL '5 days'),
('oi002002-0000-0000-0000-0000000004', 'o0010002-0000-0000-0000-000000000002', 'p0010005-0000-0000-0000-000000000005', 6, 3.79, 'pickup', NOW() - INTERVAL '5 days'),
('oi003001-0000-0000-0000-0000000005', 'o0010003-0000-0000-0000-000000000003', 'p0010004-0000-0000-0000-000000000004', 7, 3.29, 'pickup', NOW() - INTERVAL '2 days'),
('oi003002-0000-0000-0000-0000000006', 'o0010003-0000-0000-0000-000000000003', 'p0010007-0000-0000-0000-000000000007', 4, 4.49, 'pickup', NOW() - INTERVAL '2 days'),
('oi004001-0000-0000-0000-0000000007', 'o0010004-0000-0000-0000-000000000004', 'p0010010-0000-0000-0000-000000000010', 1, 54.99, 'ship', NOW() - INTERVAL '1 day'),
('oi004002-0000-0000-0000-0000000008', 'o0010004-0000-0000-0000-000000000004', 'p0010001-0000-0000-0000-000000000001', 5, 3.99, 'ship', NOW() - INTERVAL '1 day'),
('oi004003-0000-0000-0000-0000000009', 'o0010004-0000-0000-0000-000000000004', 'p0010006-0000-0000-0000-000000000006', 4, 4.29, 'ship', NOW() - INTERVAL '1 day'),
('oi005001-0000-0000-0000-0000000010', 'o0010005-0000-0000-0000-000000000005', 'p0010010-0000-0000-0000-000000000010', 2, 54.99, 'pickup', NOW()),
('oi005002-0000-0000-0000-0000000011', 'o0010005-0000-0000-0000-000000000005', 'p0010003-0000-0000-0000-000000000003', 10, 2.99, 'ship', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO COMMUNICATION CONTACTS
-- =============================================================================
INSERT INTO communication_contacts (id, brand_id, email, first_name, last_name, phone, company, email_opt_in, sms_opt_in, tags, created_at)
VALUES
('cc001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'alex.rivera@email.com', 'Alex', 'Rivera', '+1-407-555-0301', 'Rivera Consulting', true, false, ARRAY['vip', 'wholesale'], NOW()),
('cc001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'jennifer.martinez@email.com', 'Jennifer', 'Martinez', '+1-407-555-0302', 'Martinez Restaurant', true, true, ARRAY['restaurant', 'wholesale'], NOW()),
('cc001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'chris.taylor@email.com', 'Chris', 'Taylor', '+1-407-555-0303', 'Taylor Grocers', true, false, ARRAY['retail'], NOW()),
('cc001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'michelle.davis@email.com', 'Michelle', 'Davis', '+1-407-555-0304', 'Davis Hotel Group', true, false, ARRAY['hospitality', 'wholesale'], NOW()),
('cc001005-0000-0000-0000-0000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'andrew.wilson@email.com', 'Andrew', 'Wilson', '+1-407-555-0305', 'Wilson Catering', true, true, ARRAY['catering'], NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO WATER LOG FIELDS
-- =============================================================================
INSERT INTO water_fields (id, brand_id, name, location, size_acres, crop_type, notes, created_at)
VALUES
('wf001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'North Grove Section A', 'GPS: 28.5383,-81.3792', 15.5, 'Citrus', 'Primary navel orange section', NOW()),
('wf001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'South Orchard', 'GPS: 28.5200,-81.4000', 22.0, 'Mixed Citrus', 'Mixed variety planting', NOW()),
('wf001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'East Block - Honeycrisp', 'GPS: 28.5450,-81.3500', 8.5, 'Apples', 'Premium Honeycrisp apples', NOW()),
('wf001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'West Test Plot', 'GPS: 28.5300,-81.4200', 3.0, 'Experimental', 'New variety test section', NOW())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO WATER LOGS
-- =============================================================================
INSERT INTO water_logs (id, brand_id, field_id, gallons, duration_minutes, water_method, notes, logged_at, created_at)
VALUES
('wl001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'wf001001-0000-0000-0000-0000000001', 2500, 45, 'drip', 'Regular weekly irrigation', NOW() - INTERVAL '2 days', NOW() - INTERVAL '2 days'),
('wl001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'wf001002-0000-0000-0000-0000000002', 4200, 60, 'sprinkler', 'Deep watering after dry spell', NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day'),
('wl001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'wf001003-0000-0000-0000-0000000003', 1800, 30, 'drip', 'Focused irrigation on new trees', NOW(), NOW()),
('wl001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'wf001001-0000-0000-0000-0000000001', 2800, 50, 'drip', 'Extended session for heat wave', NOW() - INTERVAL '4 days', NOW() - INTERVAL '4 days')
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO CHANGELOGS
-- =============================================================================
INSERT INTO changelogs (id, brand_id, version, title, description, content, released_at, is_published, feature_type)
VALUES
('cl001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'2.0.0', 'Major Platform Update',
'We have rolled out significant improvements to the platform including a new analytics dashboard, improved order management, and faster performance.',
'[
{"type": "feature", "title": "New Analytics Dashboard", "description": "Real-time insights into your business performance with customizable reports and charts."},
{"type": "feature", "title": "Improved Order Management", "description": "Faster order processing with batch operations and smart filtering."},
{"type": "improvement", "title": "50% Faster Load Times", "description": "Optimized frontend for lightning-fast navigation across all pages."},
{"type": "bugfix", "title": "Fixed Mobile Checkout", "description": "Resolved issues with checkout on iOS devices."}
]'::jsonb,
NOW() - INTERVAL '3 days', true, 'major'),
('cl001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'1.9.5', 'Water Log Enhancement',
'Added new water tracking features including field mapping and usage analytics.',
'[
{"type": "feature", "title": "Field GPS Mapping", "description": "Track water usage by GPS-defined field boundaries."},
{"type": "feature", "title": "Usage Analytics", "description": "Weekly and monthly water usage reports with trend analysis."},
{"type": "improvement", "title": "Mobile-Friendly Interface", "description": "Optimized for field workers on mobile devices."}
]'::jsonb,
NOW() - INTERVAL '2 weeks', true, 'feature')
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO REFERRAL CODES
-- =============================================================================
INSERT INTO referral_codes (id, brand_id, referrer_user_id, referral_code, referrer_email, reward_type, reward_value, max_uses)
VALUES
('rc001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'u0010001-0000-0000-0000-0000000001', 'SUNRISE20', 'admin@sunrisefarms.com', 'percentage', 20.00, 10)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO ADMIN USERS
-- =============================================================================
INSERT INTO admin_users (id, user_id, brand_id, email, role, active, can_manage_products, can_manage_stops, can_manage_orders, can_manage_pickup, can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log, can_manage_reports, can_manage_settings)
VALUES
('au001001-0000-0000-0000-0000000001', 'u0010001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'admin@sunrisefarms.com', 'brand_admin', true, true, true, true, true, true, true, true, true, true),
('au001002-0000-0000-0000-0000000002', 'u0010002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'manager@sunrisefarms.com', 'brand_admin', true, true, true, true, false, true, false, true, true, true)
ON CONFLICT (id) DO NOTHING;
COMMIT;
COMMIT;
@@ -0,0 +1,145 @@
-- 202_fix_admin_create_stop.sql
-- Fix/recreate admin_create_stop RPC using the signature required by callers.
--
-- The previous migration 147 defined the function but was never applied (or
-- was lost) to the live DB, resulting in PGRST202 "function not found" when
-- "Add New Stop" (createStop server action) tries to call it.
--
-- This uses the parameter order from the reported error + best-practice
-- style (always return jsonb with success/stop_id or error; no RAISE).
-- Robust date/cutoff parsing and slug generation are preserved so that
-- required columns (slug, status) are populated and duplicates are avoided.
--
-- Matches the client call in src/actions/stops/create-stop.ts which passes:
-- p_brand_id, p_city, p_state, p_location, p_date, p_time,
-- p_address, p_zip, p_cutoff_time, p_active
-- (named keys; param order in CREATE only affects the GRANT signature).
BEGIN;
-- Drop any stale overloads that might confuse PostgREST cache or signature matching.
DO $$
BEGIN
DROP FUNCTION IF EXISTS public.admin_create_stop(UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN);
DROP FUNCTION IF EXISTS public.admin_create_stop(BOOLEAN, TEXT, UUID, TEXT, TIME, DATE, TEXT, TEXT, TIME, TEXT);
DROP FUNCTION IF EXISTS public.admin_create_stop(BOOLEAN, TEXT, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT);
EXCEPTION WHEN OTHERS THEN
-- ignore if not present
NULL;
END $$;
-- ── admin_create_stop (single) ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.admin_create_stop(
p_active boolean,
p_address text,
p_brand_id uuid,
p_city text,
p_cutoff_time text,
p_date text,
p_location text,
p_state text,
p_time text,
p_zip text
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_date_value TIMESTAMPTZ;
v_cutoff_val TIMESTAMPTZ;
v_slug TEXT;
v_slug_base TEXT;
v_id UUID;
v_counter INT := 0;
BEGIN
IF p_brand_id IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'brand_id is required');
END IF;
IF p_city IS NULL OR length(trim(p_city)) = 0 THEN
RETURN jsonb_build_object('success', false, 'error', 'city is required');
END IF;
-- ── Parse p_date (TEXT -> TIMESTAMPTZ) ────────────────────────────────────
v_date_value := NULLIF(trim(p_date), '')::TIMESTAMPTZ;
IF v_date_value IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'date is required');
END IF;
-- ── Parse p_cutoff_time (accepts ISO datetime OR "HH:MM[:SS]") ────────────
IF p_cutoff_time IS NULL OR length(trim(p_cutoff_time)) = 0 THEN
v_cutoff_val := NULL;
ELSIF p_cutoff_time ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
-- Time-only value — combine with the stop's date
v_cutoff_val := (v_date_value::DATE + trim(p_cutoff_time)::TIME)::TIMESTAMPTZ;
ELSE
-- Assume a full ISO datetime string
v_cutoff_val := trim(p_cutoff_time)::TIMESTAMPTZ;
END IF;
-- ── Derive unique slug (required column) ──────────────────────────────────
v_slug_base := lower(regexp_replace(trim(p_city), '\s+', '-', 'g'))
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
v_slug := v_slug_base;
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
v_counter := v_counter + 1;
v_slug := v_slug_base || '-' || v_counter;
END LOOP;
-- ── Insert (include all required columns + status) ────────────────────────
INSERT INTO stops (
active,
address,
brand_id,
city,
cutoff_time,
date,
location,
state,
time,
zip,
slug,
status
)
VALUES (
p_active,
p_address,
p_brand_id,
p_city,
v_cutoff_val,
v_date_value,
p_location,
p_state,
p_time,
p_zip,
v_slug,
'draft'
)
RETURNING id INTO v_id;
RETURN jsonb_build_object(
'success', true,
'stop_id', v_id,
'message', 'Stop created successfully'
);
EXCEPTION WHEN OTHERS THEN
RETURN jsonb_build_object(
'success', false,
'error', SQLERRM
);
END;
$$;
-- Grant for the exact signature (types in declaration order).
-- SECURITY DEFINER bypasses RLS; anon is used intentionally from server action.
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
boolean, text, uuid, text, text, text, text, text, text, text
) TO anon, authenticated, service_role;
-- Reload PostgREST schema cache immediately so the function appears without
-- waiting for the periodic refresh (common source of PGRST202 after create).
NOTIFY pgrst, 'reload schema';
COMMIT;