-- ============================================================ -- 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');