Files
avbeat-com/supabase/migrations/20260401000000_marketplace_phase1.sql
2026-05-07 16:39:17 +00:00

361 lines
14 KiB
PL/PgSQL

-- ============================================================
-- BROADCAST BEAT MARKETPLACE — PHASE 1 MVP MIGRATION
-- Additive only: all tables prefixed marketplace_
-- ============================================================
-- ── TYPES ────────────────────────────────────────────────────
DROP TYPE IF EXISTS public.marketplace_profile_type CASCADE;
CREATE TYPE public.marketplace_profile_type AS ENUM ('crew', 'vendor');
DROP TYPE IF EXISTS public.marketplace_profile_status CASCADE;
CREATE TYPE public.marketplace_profile_status AS ENUM ('pending', 'active', 'rejected');
DROP TYPE IF EXISTS public.marketplace_subscription_tier CASCADE;
CREATE TYPE public.marketplace_subscription_tier AS ENUM ('free', 'pro', 'featured');
DROP TYPE IF EXISTS public.marketplace_job_type CASCADE;
CREATE TYPE public.marketplace_job_type AS ENUM ('full_time', 'part_time', 'contract', 'temp');
DROP TYPE IF EXISTS public.marketplace_listing_status CASCADE;
CREATE TYPE public.marketplace_listing_status AS ENUM ('active', 'expired', 'draft');
-- ── CORE TABLES ───────────────────────────────────────────────
-- Marketplace profiles (one per user)
CREATE TABLE IF NOT EXISTS public.marketplace_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
profile_type public.marketplace_profile_type NOT NULL,
slug TEXT NOT NULL UNIQUE,
status public.marketplace_profile_status NOT NULL DEFAULT 'pending',
subscription_tier public.marketplace_subscription_tier NOT NULL DEFAULT 'free',
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
rejection_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Crew member details
CREATE TABLE IF NOT EXISTS public.marketplace_crew_details (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES public.marketplace_profiles(id) ON DELETE CASCADE,
name TEXT NOT NULL,
headshot_url TEXT,
location TEXT NOT NULL,
union_status TEXT DEFAULT 'non_union',
day_rate INTEGER,
bio TEXT,
skills TEXT[] DEFAULT ARRAY[]::TEXT[],
equipment TEXT[] DEFAULT ARRAY[]::TEXT[],
reel_url TEXT,
resume_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Vendor / company details
CREATE TABLE IF NOT EXISTS public.marketplace_vendor_details (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES public.marketplace_profiles(id) ON DELETE CASCADE,
company_name TEXT NOT NULL,
logo_url TEXT,
location TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
website TEXT,
contact_email TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Job board
CREATE TABLE IF NOT EXISTS public.marketplace_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES public.marketplace_profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL,
company TEXT NOT NULL,
location TEXT NOT NULL,
job_type public.marketplace_job_type NOT NULL DEFAULT 'full_time',
salary_min INTEGER,
salary_max INTEGER,
description TEXT NOT NULL,
apply_url TEXT,
apply_email TEXT,
expires_at TIMESTAMPTZ,
status public.marketplace_listing_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Gig board
CREATE TABLE IF NOT EXISTS public.marketplace_gigs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES public.marketplace_profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL,
company TEXT NOT NULL,
location TEXT NOT NULL,
gig_type TEXT NOT NULL,
rate TEXT,
start_date DATE,
end_date DATE,
description TEXT NOT NULL,
contact_method TEXT,
expires_at TIMESTAMPTZ,
status public.marketplace_listing_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ── INDEXES ───────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS idx_marketplace_profiles_user_id ON public.marketplace_profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_marketplace_profiles_slug ON public.marketplace_profiles(slug);
CREATE INDEX IF NOT EXISTS idx_marketplace_profiles_status ON public.marketplace_profiles(status);
CREATE INDEX IF NOT EXISTS idx_marketplace_crew_profile_id ON public.marketplace_crew_details(profile_id);
CREATE INDEX IF NOT EXISTS idx_marketplace_vendor_profile_id ON public.marketplace_vendor_details(profile_id);
CREATE INDEX IF NOT EXISTS idx_marketplace_jobs_profile_id ON public.marketplace_jobs(profile_id);
CREATE INDEX IF NOT EXISTS idx_marketplace_jobs_status ON public.marketplace_jobs(status);
CREATE INDEX IF NOT EXISTS idx_marketplace_gigs_profile_id ON public.marketplace_gigs(profile_id);
CREATE INDEX IF NOT EXISTS idx_marketplace_gigs_status ON public.marketplace_gigs(status);
-- ── FUNCTIONS ─────────────────────────────────────────────────
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION public.marketplace_set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
-- Check if user is admin (uses auth metadata to avoid recursion)
CREATE OR REPLACE FUNCTION public.marketplace_is_admin()
RETURNS BOOLEAN
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT EXISTS (
SELECT 1 FROM auth.users
WHERE id = auth.uid()
AND (
raw_user_meta_data->>'role' = 'admin'
OR raw_app_meta_data->>'role' = 'admin'
)
)
$$;
-- Check if user has active pro/featured subscription
CREATE OR REPLACE FUNCTION public.marketplace_has_active_subscription()
RETURNS BOOLEAN
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT EXISTS (
SELECT 1 FROM public.marketplace_profiles
WHERE user_id = auth.uid()
AND subscription_tier IN ('pro', 'featured')
AND status = 'active'
)
$$;
-- ── ENABLE RLS ────────────────────────────────────────────────
ALTER TABLE public.marketplace_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.marketplace_crew_details ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.marketplace_vendor_details ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.marketplace_jobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.marketplace_gigs ENABLE ROW LEVEL SECURITY;
-- ── RLS POLICIES ─────────────────────────────────────────────
-- marketplace_profiles
DROP POLICY IF EXISTS "mp_public_read_active" ON public.marketplace_profiles;
CREATE POLICY "mp_public_read_active" ON public.marketplace_profiles
FOR SELECT TO public
USING (status = 'active');
DROP POLICY IF EXISTS "mp_owner_read_own" ON public.marketplace_profiles;
CREATE POLICY "mp_owner_read_own" ON public.marketplace_profiles
FOR SELECT TO authenticated
USING (user_id = auth.uid());
DROP POLICY IF EXISTS "mp_owner_insert" ON public.marketplace_profiles;
CREATE POLICY "mp_owner_insert" ON public.marketplace_profiles
FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
DROP POLICY IF EXISTS "mp_owner_update" ON public.marketplace_profiles;
CREATE POLICY "mp_owner_update" ON public.marketplace_profiles
FOR UPDATE TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
DROP POLICY IF EXISTS "mp_admin_all" ON public.marketplace_profiles;
CREATE POLICY "mp_admin_all" ON public.marketplace_profiles
FOR ALL TO authenticated
USING (public.marketplace_is_admin())
WITH CHECK (public.marketplace_is_admin());
-- marketplace_crew_details
DROP POLICY IF EXISTS "mcd_public_read" ON public.marketplace_crew_details;
CREATE POLICY "mcd_public_read" ON public.marketplace_crew_details
FOR SELECT TO public
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.status = 'active'
));
DROP POLICY IF EXISTS "mcd_owner_all" ON public.marketplace_crew_details;
CREATE POLICY "mcd_owner_all" ON public.marketplace_crew_details
FOR ALL TO authenticated
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
))
WITH CHECK (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
));
DROP POLICY IF EXISTS "mcd_admin_all" ON public.marketplace_crew_details;
CREATE POLICY "mcd_admin_all" ON public.marketplace_crew_details
FOR ALL TO authenticated
USING (public.marketplace_is_admin())
WITH CHECK (public.marketplace_is_admin());
-- marketplace_vendor_details
DROP POLICY IF EXISTS "mvd_public_read" ON public.marketplace_vendor_details;
CREATE POLICY "mvd_public_read" ON public.marketplace_vendor_details
FOR SELECT TO public
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.status = 'active'
));
DROP POLICY IF EXISTS "mvd_owner_all" ON public.marketplace_vendor_details;
CREATE POLICY "mvd_owner_all" ON public.marketplace_vendor_details
FOR ALL TO authenticated
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
))
WITH CHECK (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
));
DROP POLICY IF EXISTS "mvd_admin_all" ON public.marketplace_vendor_details;
CREATE POLICY "mvd_admin_all" ON public.marketplace_vendor_details
FOR ALL TO authenticated
USING (public.marketplace_is_admin())
WITH CHECK (public.marketplace_is_admin());
-- marketplace_jobs
DROP POLICY IF EXISTS "mj_public_read_active" ON public.marketplace_jobs;
CREATE POLICY "mj_public_read_active" ON public.marketplace_jobs
FOR SELECT TO public
USING (status = 'active');
DROP POLICY IF EXISTS "mj_owner_all" ON public.marketplace_jobs;
CREATE POLICY "mj_owner_all" ON public.marketplace_jobs
FOR ALL TO authenticated
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
))
WITH CHECK (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
));
DROP POLICY IF EXISTS "mj_admin_all" ON public.marketplace_jobs;
CREATE POLICY "mj_admin_all" ON public.marketplace_jobs
FOR ALL TO authenticated
USING (public.marketplace_is_admin())
WITH CHECK (public.marketplace_is_admin());
-- marketplace_gigs
DROP POLICY IF EXISTS "mg_public_read_active" ON public.marketplace_gigs;
CREATE POLICY "mg_public_read_active" ON public.marketplace_gigs
FOR SELECT TO public
USING (status = 'active');
DROP POLICY IF EXISTS "mg_owner_all" ON public.marketplace_gigs;
CREATE POLICY "mg_owner_all" ON public.marketplace_gigs
FOR ALL TO authenticated
USING (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
))
WITH CHECK (EXISTS (
SELECT 1 FROM public.marketplace_profiles mp
WHERE mp.id = profile_id AND mp.user_id = auth.uid()
));
DROP POLICY IF EXISTS "mg_admin_all" ON public.marketplace_gigs;
CREATE POLICY "mg_admin_all" ON public.marketplace_gigs
FOR ALL TO authenticated
USING (public.marketplace_is_admin())
WITH CHECK (public.marketplace_is_admin());
-- ── TRIGGERS ─────────────────────────────────────────────────
DROP TRIGGER IF EXISTS marketplace_profiles_updated_at ON public.marketplace_profiles;
CREATE TRIGGER marketplace_profiles_updated_at
BEFORE UPDATE ON public.marketplace_profiles
FOR EACH ROW EXECUTE FUNCTION public.marketplace_set_updated_at();
DROP TRIGGER IF EXISTS marketplace_crew_updated_at ON public.marketplace_crew_details;
CREATE TRIGGER marketplace_crew_updated_at
BEFORE UPDATE ON public.marketplace_crew_details
FOR EACH ROW EXECUTE FUNCTION public.marketplace_set_updated_at();
DROP TRIGGER IF EXISTS marketplace_vendor_updated_at ON public.marketplace_vendor_details;
CREATE TRIGGER marketplace_vendor_updated_at
BEFORE UPDATE ON public.marketplace_vendor_details
FOR EACH ROW EXECUTE FUNCTION public.marketplace_set_updated_at();
-- ── STORAGE BUCKETS (via SQL) ─────────────────────────────────
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES
('marketplace-headshots', 'marketplace-headshots', true, 5242880,
ARRAY['image/jpeg','image/jpg','image/png','image/webp']::TEXT[]),
('marketplace-logos', 'marketplace-logos', true, 5242880,
ARRAY['image/jpeg','image/jpg','image/png','image/webp','image/svg+xml']::TEXT[]),
('marketplace-resumes', 'marketplace-resumes', false, 10485760,
ARRAY['application/pdf','application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document']::TEXT[])
ON CONFLICT (id) DO NOTHING;
-- Storage RLS
DROP POLICY IF EXISTS "marketplace_headshots_public_read" ON storage.objects;
CREATE POLICY "marketplace_headshots_public_read" ON storage.objects
FOR SELECT TO public
USING (bucket_id = 'marketplace-headshots');
DROP POLICY IF EXISTS "marketplace_headshots_auth_write" ON storage.objects;
CREATE POLICY "marketplace_headshots_auth_write" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'marketplace-headshots');
DROP POLICY IF EXISTS "marketplace_logos_public_read" ON storage.objects;
CREATE POLICY "marketplace_logos_public_read" ON storage.objects
FOR SELECT TO public
USING (bucket_id = 'marketplace-logos');
DROP POLICY IF EXISTS "marketplace_logos_auth_write" ON storage.objects;
CREATE POLICY "marketplace_logos_auth_write" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'marketplace-logos');
DROP POLICY IF EXISTS "marketplace_resumes_auth_read" ON storage.objects;
CREATE POLICY "marketplace_resumes_auth_read" ON storage.objects
FOR SELECT TO authenticated
USING (bucket_id = 'marketplace-resumes' AND auth.uid()::TEXT = (storage.foldername(name))[1]);
DROP POLICY IF EXISTS "marketplace_resumes_auth_write" ON storage.objects;
CREATE POLICY "marketplace_resumes_auth_write" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'marketplace-resumes');