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
+2
View File
@@ -26,6 +26,8 @@ npx playwright test # Run E2E tests (Playwright)
> For direct PG mode: `pg` and `dotenv` are already in devDependencies.
> If `get_brand_settings` migration fails with "cannot change return type", the function signature changed — drop and recreate it first.
**Recent migration work is documented in `MEMORY.md`** (Supabase login + link process, updates to `push-migrations.js` for modern CLI, specific SQL patches made to 091/145/148/200/201 so they would apply cleanly, and which migrations were pushed in the session). Cat `MEMORY.md` for details.
No test suite currently exists. E2E tests use Playwright (`tests/` or `test-e2e.ts`).
---
+158
View File
@@ -0,0 +1,158 @@
# MEMORY.md — Persistent Session Context
This file captures key context, decisions, fixes, and state from recent work so it survives across conversations.
**Last updated:** 2026-06-03 (during Supabase migration apply session)
---
## Supabase CLI + Migrations Tooling
### Login + Link (done in this session)
- User ran `supabase login`
- We ran: `supabase link --project-ref wnzkhezyhnfzhkhiflrp`
- Link succeeded. Project appears as **LINKED** (●) in `supabase projects list`.
- Link state lives under `supabase/.temp/` (project-ref, linked-project.json, pooler-url, etc.). **Not** the legacy `.supabase/config.toml` at project root.
- This enables `supabase db query --linked`, `supabase migration list`, etc.
### Important Environment Note
- Direct Postgres connections (`db.wnzkhezyhnfzhkhiflrp.supabase.co:5432` using service role key as password) **do not work** from this workspace:
- `getaddrinfo ENOTFOUND`
- IPv6 "network is unreachable" in some cases
- HTTPS / Supabase REST / Management API work fine.
- Therefore the `push-migrations.js` **CLI path** (using linked project) is the only reliable way to apply migrations here.
### Updated Migration Script
File: `supabase/push-migrations.js`
Key changes:
- Detection logic now recognizes modern link: `supabase/.temp/project-ref` (in addition to old `.supabase/config.toml`).
- `pushWithCli()` rewritten to apply files one-by-one using:
```bash
supabase db query --linked --file "supabase/migrations/NNN_foo.sql"
```
(Previously tried `db push --db-url ...` which used direct connection and failed here.)
- Falls back to direct `pg` only if CLI path fails.
- Header comments updated with current recommended workflow.
**Recommended commands now:**
```bash
supabase login
supabase link --project-ref wnzkhezyhnfzhkhiflrp
node supabase/push-migrations.js 148 # or any prefix
# or
npm run migrate:one 148
```
`npm run migrate` (no arg) will push every `*.sql` in order (use with caution).
---
## Migration Files Patched in This Session
These changes were required to make the files actually apply successfully against the live remote schema (column drift, syntax errors, non-idempotent statements, pre-existing tables).
### 091_brand_plan_tier.sql
- Fixed syntax error: extra `)` in `get_brand_plan_info`:
```sql
... date >= date_trunc('month', now())); -- was broken
```
→ `now());`
### 145_create_product_images_bucket.sql
- Column name: `allowedMimeTypes` → `allowed_mime_types` (current Supabase storage.buckets schema uses snake_case).
- Made idempotent:
- Bucket insert now uses `WHERE NOT EXISTS (...)` (handles both id and name unique constraints).
- Added `DROP POLICY IF EXISTS ...` before each `CREATE POLICY` (so re-runs don't fail).
Policies created:
- "Public can view product-images"
- "Admins can upload product-images"
### 148_public_stops_rpc.sql
- `time` is a reserved word in Postgres.
- Quoted the output column and the source reference:
```sql
RETURNS TABLE ( ..., "time" TEXT, ... )
...
s."time",
```
- GRANTs to anon/authenticated/service_role added at bottom.
### 200_production_features.sql
- `user_activity_logs` table originated in `036_user_activity_logs.sql` (no `brand_id`, different column names: `activity_type` + `details`).
- 200's `CREATE TABLE IF NOT EXISTS` was a no-op.
- Its policies + indexes assumed `brand_id`, `action`, `resource_type`, `resource_id`, `metadata`.
- Added after the CREATE TABLE block:
```sql
ALTER TABLE user_activity_logs
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
... (other columns) ...
```
- This unblocked policy creation and indexes.
Also added many new tables (referral_*, changelogs, onboarding_progress, api_keys, notification_preferences) + RLS policies + indexes.
### 201_seed_data.sql
- Heavily truncated.
- Original contained large demo INSERTs for products, stops, orders, order_items, wholesale_customers, communication_contacts, water_*, admin_users, etc.
- These used outdated column lists vs. the live DB (e.g. products: `unit`/`category`/`sku`/`is_active` vs current `type`/`active`/`pickup_type`/`is_taxable`; stops used `name`/`scheduled_at`/`postal_code` vs `location`/`date`+`time`/`zip`/`slug`).
- Kept only the 3 demo brands INSERT (now works because 091 added `plan_tier` + limit columns).
- Added explanatory comment.
- File now ends cleanly after `COMMIT;`.
(The demo brands with enterprise/farm/starter + plan limits were successfully inserted.)
### Other notes on 147 / 202
- `147_admin_create_stop_rpcs.sql` and `202_fix_admin_create_stop.sql` were also present/modified in the working tree (related to admin stop creation fixes mentioned in CLAUDE.md).
---
## Successfully Applied (this session, via updated script)
Batches included (not exhaustive):
- 084 (shipments)
- 091 (plan tier + get_brand_plan_info)
- 142 (integration_credentials / resend + twilio RPCs)
- 143 (enable_route_trace via set_brand_feature)
- 144 (time_tracking_worker_number_rls)
- 145 (product-images bucket + policies)
- 146 (sitemap stops RPC)
- 147 (admin create stop RPCs)
- 148 (public stops RPC) — verified working
- 200 (production features)
- 201 (brands seed only)
- 202 (admin create stop fix)
Verification queries (post-apply) confirmed:
- shipments table exists
- get_resend_credentials / get_public_stops_for_brand / get_active_stops_with_brand functions exist
- product-images bucket + its two policies exist
- demo brand (sunrise-farms) has plan_tier = 'enterprise'
- etc.
---
## Current State / Gotchas
- `supabase migration list` will show a lot of "pending" because the project's custom numeric migrations (00x_*.sql) were historically applied via the raw-SQL push script, not registered in Supabase's `schema_migrations` tracking table. Only early ones (001/002) + many timestamped migrations from other activity are tracked.
- Many migrations are intentionally written to be re-runnable. Re-pushing a prefix is the supported workflow for this project.
- When adding **new** migrations in the future, prefer the standard `supabase migration new` flow if possible, but the custom `push-migrations.js` + numeric prefix style is still the established pattern here.
- Storage policies, RLS, SECURITY DEFINER functions, and brand-scoped data are all over the place — test carefully after big applies.
- CLAUDE.md already documents the overall migrate story and the `get_brand_settings` gotcha.
---
## How to Use This Memory
- Cat this file at the start of future sessions if context is needed: `cat MEMORY.md`
- Update this file with new key facts, applied migrations, or new gotchas.
- Feel free to add dated sections.
---
## Unrelated / Other Changes in Tree (as of last git status)
(See `git status --short` and `git diff` for full picture. Includes stop-related action + RPC fixes, various debug scripts in `scripts/`, `supabase/ADMIN_CREATE_STOP_FIX.sql`, a pricing assessment doc, etc.)
This MEMORY.md focuses on the Supabase login / link / migration tooling + SQL repair work from the immediate request.
+69 -15
View File
@@ -1,5 +1,6 @@
"use server";
import { revalidateTag } from "next/cache";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
import { getMockTableData } from "@/lib/mock-data";
@@ -38,33 +39,86 @@ export async function createStop(
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
// Use anon key — the RPC is SECURITY DEFINER so it bypasses RLS regardless
// of caller. This also means a missing SUPABASE_SERVICE_ROLE_KEY in
// production no longer breaks stop creation.
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Preferred path: anon key + SECURITY DEFINER RPC (bypasses RLS, works even if
// service role key is absent at runtime). See:
// supabase/migrations/202_fix_admin_create_stop.sql
// scripts/apply-admin-create-stop.js (run this locally with the keys you have)
// supabase/ADMIN_CREATE_STOP_FIX.sql (exact prompt SQL for SQL editor)
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
headers: { ...svcHeaders(anonKey), "Content-Type": "application/json" },
body: JSON.stringify({
p_active: data.active ?? false,
p_address: data.address || null,
p_brand_id: brandId,
p_city: data.city,
p_state: data.state,
p_location: data.location,
p_cutoff_time: data.cutoff_time || null,
p_date: data.date,
p_location: data.location,
p_state: data.state,
p_time: data.time,
p_address: data.address ?? null,
p_zip: data.zip ?? null,
p_cutoff_time: data.cutoff_time ?? null,
p_active: data.active ?? false,
p_zip: data.zip || null,
}),
});
let usedFallback = false;
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
const errText = await res.text();
const lower = errText.toLowerCase();
const looksLikeMissingFn =
lower.includes("pgrst202") ||
lower.includes("admin_create_stop") ||
lower.includes("could not find the function") ||
lower.includes("function not found");
if (looksLikeMissingFn) {
usedFallback = true;
} else {
return { success: false, error: `Failed: ${errText}` };
}
} else {
const inserted = await res.json().catch(() => ({} as any));
if (inserted && inserted.success === false) {
// Our RPC returns structured errors as 200 + {success:false}
const errMsg = inserted.error || "Failed to create stop";
const lower = errMsg.toLowerCase();
if (lower.includes("function") || lower.includes("not found") || lower.includes("pgrst")) {
usedFallback = true;
} else {
return { success: false, error: errMsg };
}
} else {
// Happy path from RPC (either old {id,slug} or new {success, stop_id})
const stopId = inserted?.stop_id || inserted?.id || "";
const effectiveBrandId = brandId || adminUser.brand_id;
if (effectiveBrandId) {
revalidateTag("stops", "default");
revalidateTag(`brand:${effectiveBrandId}:stops`, "default");
}
return { success: true, id: stopId };
}
}
const inserted = await res.json();
return { success: true, id: inserted?.id ?? "" };
if (usedFallback) {
// We cannot bypass the block_stops_mutations RLS policy via REST even with the service key.
// The only reliable path is the SECURITY DEFINER RPC created by migration 202.
// Tell the user exactly how to install it using only the keys they already have.
return {
success: false,
error:
"The admin_create_stop database function is missing (this is why Add New Stop fails with function not found).\n\n" +
"Fix using only the keys in your .env.local (no Supabase dashboard needed):\n" +
" 1. On a machine that can reach your database (normal laptop usually works):\n" +
" node scripts/apply-admin-create-stop.js\n" +
" 2. Or: npm run migrate:one 202\n" +
" 3. Or apply supabase/migrations/202_fix_admin_create_stop.sql via psql / Supabase CLI with --db-url.\n\n" +
"After it succeeds, restart your dev server and Add New Stop will work.",
};
}
// Should not reach here
return { success: false, error: "Unexpected state creating stop" };
}
+92
View File
@@ -0,0 +1,92 @@
-- ============================================================================
-- Grok Fix Prompt: admin_create_stop (copy-paste ready for Supabase SQL Editor)
-- ============================================================================
-- This is the exact "Best Practice Version" from the fix prompt, with
-- additional comments. Run this directly in the Supabase SQL Editor
-- (https://supabase.com/dashboard/project/wnzkhezyhnfzhkhiflrp/sql) if the
-- migration push cannot be used.
--
-- After running, PostgREST may need a schema reload:
-- NOTIFY pgrst, 'reload schema';
--
-- Then test "Add New Stop" again.
--
-- The calling code (JS/TS) is in:
-- src/actions/stops/create-stop.ts (the fetch to /rest/v1/rpc/admin_create_stop)
-- src/components/admin/AddStopModal.tsx
-- src/components/admin/NewStopForm.tsx
-- (all go through the server action createStop which builds the p_* args)
-- ============================================================================
CREATE OR REPLACE FUNCTION public.admin_create_stop(
p_active boolean,
p_address text,
p_brand_id uuid,
p_city text,
p_cutoff_time time,
p_date date,
p_location text,
p_state text,
p_time time,
p_zip text
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
new_stop_id uuid;
BEGIN
INSERT INTO stops (
active,
address,
brand_id,
city,
cutoff_time,
date,
location,
state,
time,
zip
)
VALUES (
p_active,
p_address,
p_brand_id,
p_city,
p_cutoff_time,
p_date,
p_location,
p_state,
p_time,
p_zip
)
RETURNING id INTO new_stop_id;
RETURN jsonb_build_object(
'success', true,
'stop_id', new_stop_id,
'message', 'Stop created successfully'
);
EXCEPTION WHEN OTHERS THEN
RETURN jsonb_build_object(
'success', false,
'error', SQLERRM
);
END;
$$;
-- Important: grant so PostgREST (anon key from server action) can invoke it.
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
boolean, text, uuid, text, time, date, text, text, time, text
) TO anon, authenticated, service_role;
-- Reload cache so the function is immediately visible (avoids PGRST202).
NOTIFY pgrst, 'reload schema';
-- NOTE: The above simple version may fail on INSERT because the stops table
-- requires "slug" and "status" (NOT NULL, no defaults in all cases).
-- Prefer the enhanced version in migrations/202_fix_admin_create_stop.sql
-- which includes slug generation and status='draft' while using a compatible
-- signature and the success/stop_id return shape.
+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;
+33 -19
View File
@@ -3,19 +3,21 @@
* push-migrations.js
*
* Pushes migration files to the linked Supabase project.
* Uses the Supabase CLI if available (supabase db push),
* Uses the Supabase CLI (supabase db query --linked --file) if the project
* is linked (after `supabase login` + `supabase link --project-ref <ref>`),
* otherwise falls back to direct PostgreSQL connection via `pg`.
*
* Usage:
* node supabase/push-migrations.js # push all pending migrations
* node supabase/push-migrations.js 082 # push only migration 082
* node supabase/push-migrations.js # push all migration files
* node supabase/push-migrations.js 148 # push only migrations matching 148_*.sql
*
* Prerequisites for CLI mode:
* brew install supabase/tap/supabase # install CLI
* supabase link --project-ref <ref> # link project once
* Prerequisites for CLI mode (preferred, works even without direct 5432 access):
* supabase login
* supabase link --project-ref wnzkhezyhnfzhkhiflrp
*
* Prerequisites for direct PG mode:
* npm install # pg and dotenv already in devDependencies
* .env.local with NEXT_PUBLIC_SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY
* (pg and dotenv are in devDependencies)
*/
const { Client } = require("pg");
@@ -35,17 +37,25 @@ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
async function pushWithCli(files) {
console.log("Using Supabase CLI (supabase db push)...\n");
const joined = files.map((f) => `supabase/migrations/${f}`).join(" ");
try {
execSync(
`supabase db push --db-url "postgresql://postgres:${serviceRoleKey}@db.wnzkhezyhnfzhkhiflrp.supabase.co:5432/postgres" ${joined}`,
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
);
return true;
} catch (err) {
return false;
console.log("Using Supabase CLI (supabase db query --linked --file)...\n");
for (const f of files) {
const filePath = `supabase/migrations/${f}`;
process.stdout.write(` Applying ${f}... `);
try {
execSync(
`supabase db query --linked --file "${filePath}"`,
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
);
console.log("✓");
} catch (err) {
console.log("✗");
// execSync with inherit already printed stderr/stdout; surface a short message
const msg = (err && err.message) ? err.message.replace(/\n/g, " ").slice(0, 200) : "";
if (msg) console.error(" " + msg);
return false;
}
}
return true;
}
async function pushWithPg(sql, migrationFile) {
@@ -102,14 +112,18 @@ async function main() {
let ok = false;
// Try CLI first if supabase is linked (has .supabase/config.toml)
// Try CLI first if supabase is linked.
// Legacy link: .supabase/config.toml at project root
// Modern link (post `supabase link`): supabase/.temp/project-ref
let hasSupabaseCli = false;
try {
hasSupabaseCli = !!execSync("which supabase", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
} catch {
hasSupabaseCli = false;
}
const isLinked = fs.existsSync(path.resolve(__dirname, "../.supabase/config.toml"));
const hasLegacyLink = fs.existsSync(path.resolve(__dirname, "../.supabase/config.toml"));
const hasModernLink = fs.existsSync(path.resolve(__dirname, ".temp/project-ref"));
const isLinked = hasLegacyLink || hasModernLink;
if (hasSupabaseCli && isLinked) {
console.log("Supabase CLI detected and project is linked.\n");