feat: implement subscription plan system with monthly credits

- Database schema:
  * subscription_plans table - stores plan tiers (Starter, Bronze, Silver, Gold)
  * user_subscriptions table - assigns users to plans
  * Default plans created (10, 50, 100, 250 credits/month)
  * All existing users assigned to Starter plan

- Subscription service (subscription-service.ts):
  * getAllPlans() - fetch all plans
  * getActivePlans() - fetch active plans for users
  * createPlan() - admin creates new plan
  * updatePlan() - admin edits plan
  * deletePlan() - admin removes plan
  * getUserSubscription() - get user's current plan
  * setUserPlan() - admin assigns user to plan
  * grantMonthlyCredits() - distribute credits to all users

- Plan management interface (/admin/plans):
  * Visual plan cards with credits, price, description
  * Create/Edit/Delete plans
  * Toggle active/inactive status
  * Sort order management
  * Grant monthly credits button (manual trigger)

- Features:
  * Monthly credit allocation based on plan
  * Prevents duplicate credit grants (tracks last_credit_grant_at)
  * Admin can manually trigger monthly credit distribution
  * Plans can be activated/deactivated
  * Custom pricing and credit amounts per plan

- UI:
  * Beautiful plan cards with color coding
  * Modal for create/edit with validation
  * Success/error messages
  * Manage Plans button in admin dashboard

Ready for future automation (cron job for monthly credits)
and payment integration (Stripe/PayPal).
This commit is contained in:
2025-12-18 15:16:44 +01:00
parent f83243fd90
commit 42b4b2b2e1
5 changed files with 773 additions and 0 deletions

View File

@@ -83,6 +83,12 @@ export default async function AdminPage() {
<p className="text-zinc-500 mt-1">API Usage Monitoring & Statistics</p>
</div>
<div className="flex gap-3">
<Link
href="/admin/plans"
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl font-bold transition-colors"
>
Manage Plans
</Link>
<Link
href="/admin/users"
className="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-bold transition-colors"

View File

@@ -0,0 +1,49 @@
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { checkIsAdmin } from '@/services/track-api-usage';
import { getAllPlans } from '@/services/subscription-service';
import Link from 'next/link';
import { ChevronLeft, Package } from 'lucide-react';
import PlanManagementClient from '@/components/PlanManagementClient';
export default async function AdminPlansPage() {
const supabase = createServerComponentClient({ cookies });
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/');
}
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) {
redirect('/');
}
// Fetch all plans
const plans = await getAllPlans();
return (
<main className="min-h-screen bg-zinc-50 dark:bg-black p-4 md:p-12">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<Link
href="/admin"
className="inline-flex items-center gap-2 text-zinc-500 hover:text-amber-600 transition-colors font-medium mb-2"
>
<ChevronLeft size={20} />
Back to Dashboard
</Link>
<h1 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tight">Subscription Plans</h1>
<p className="text-zinc-500 mt-1">Manage subscription tiers and monthly credits</p>
</div>
</div>
{/* Plan Management */}
<PlanManagementClient initialPlans={plans} />
</div>
</main>
);
}

View File

@@ -0,0 +1,321 @@
'use client';
import { useState } from 'react';
import { Plus, Edit, Trash2, X, Check, AlertCircle, Zap } from 'lucide-react';
import { createPlan, updatePlan, deletePlan, grantMonthlyCredits, type SubscriptionPlan } from '@/services/subscription-service';
interface PlanManagementClientProps {
initialPlans: SubscriptionPlan[];
}
export default function PlanManagementClient({ initialPlans }: PlanManagementClientProps) {
const [plans, setPlans] = useState<SubscriptionPlan[]>(initialPlans);
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [formData, setFormData] = useState({
name: '',
display_name: '',
monthly_credits: 0,
price: 0,
description: '',
is_active: true,
sort_order: 0
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const handleEdit = (plan: SubscriptionPlan) => {
setEditingPlan(plan);
setFormData({
name: plan.name,
display_name: plan.display_name,
monthly_credits: plan.monthly_credits,
price: plan.price,
description: plan.description || '',
is_active: plan.is_active,
sort_order: plan.sort_order
});
setIsCreating(false);
setMessage(null);
};
const handleCreate = () => {
setEditingPlan(null);
setFormData({
name: '',
display_name: '',
monthly_credits: 0,
price: 0,
description: '',
is_active: true,
sort_order: plans.length + 1
});
setIsCreating(true);
setMessage(null);
};
const handleSave = async () => {
if (!formData.name || !formData.display_name || formData.monthly_credits <= 0) {
setMessage({ type: 'error', text: 'Please fill in all required fields' });
return;
}
setLoading(true);
setMessage(null);
if (isCreating) {
const result = await createPlan(formData);
if (result.success && result.plan) {
setPlans([...plans, result.plan]);
setMessage({ type: 'success', text: 'Plan created successfully' });
setIsCreating(false);
} else {
setMessage({ type: 'error', text: result.error || 'Failed to create plan' });
}
} else if (editingPlan) {
const result = await updatePlan(editingPlan.id, formData);
if (result.success) {
setPlans(plans.map(p => p.id === editingPlan.id ? { ...p, ...formData } : p));
setMessage({ type: 'success', text: 'Plan updated successfully' });
setEditingPlan(null);
} else {
setMessage({ type: 'error', text: result.error || 'Failed to update plan' });
}
}
setLoading(false);
};
const handleDelete = async (planId: string) => {
if (!confirm('Are you sure you want to delete this plan? Users on this plan will be unassigned.')) {
return;
}
setLoading(true);
const result = await deletePlan(planId);
if (result.success) {
setPlans(plans.filter(p => p.id !== planId));
setMessage({ type: 'success', text: 'Plan deleted successfully' });
} else {
setMessage({ type: 'error', text: result.error || 'Failed to delete plan' });
}
setLoading(false);
};
const handleGrantCredits = async () => {
if (!confirm('Grant monthly credits to all users based on their subscription plan?')) {
return;
}
setLoading(true);
setMessage(null);
const result = await grantMonthlyCredits();
if (result.success) {
setMessage({
type: 'success',
text: `Credits granted! Processed: ${result.processed}, Failed: ${result.failed}`
});
} else {
setMessage({ type: 'error', text: result.error || 'Failed to grant credits' });
}
setLoading(false);
};
return (
<div className="space-y-6">
{/* Actions Bar */}
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
<div className="flex gap-3">
<button
onClick={handleCreate}
className="flex-1 py-3 bg-amber-600 hover:bg-amber-700 text-white font-bold rounded-xl transition-colors flex items-center justify-center gap-2"
>
<Plus size={18} />
Create New Plan
</button>
<button
onClick={handleGrantCredits}
disabled={loading}
className="flex-1 py-3 bg-green-600 hover:bg-green-700 text-white font-bold rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
<Zap size={18} />
Grant Monthly Credits
</button>
</div>
</div>
{message && (
<div className={`p-4 rounded-xl flex items-center gap-3 ${message.type === 'success'
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-500'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-500'
}`}>
{message.type === 'success' ? <Check size={20} /> : <AlertCircle size={20} />}
{message.text}
</div>
)}
{/* Plans Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{plans.map((plan) => (
<div
key={plan.id}
className={`bg-white dark:bg-zinc-900 rounded-2xl p-6 border-2 ${plan.is_active
? 'border-amber-200 dark:border-amber-800'
: 'border-zinc-200 dark:border-zinc-800 opacity-60'
} shadow-sm relative`}
>
{!plan.is_active && (
<div className="absolute top-4 right-4 px-2 py-1 bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400 text-xs font-bold rounded">
Inactive
</div>
)}
<div className="mb-4">
<h3 className="text-2xl font-black text-zinc-900 dark:text-white">{plan.display_name}</h3>
<p className="text-xs text-zinc-500 mt-1">{plan.name}</p>
</div>
<div className="mb-4">
<div className="text-3xl font-black text-amber-600">{plan.monthly_credits}</div>
<div className="text-xs text-zinc-500">Credits/Month</div>
</div>
<div className="mb-4">
<div className="text-2xl font-bold text-zinc-900 dark:text-white">{plan.price.toFixed(2)}</div>
<div className="text-xs text-zinc-500">per month</div>
</div>
{plan.description && (
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-4">{plan.description}</p>
)}
<div className="flex gap-2">
<button
onClick={() => handleEdit(plan)}
className="flex-1 py-2 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 font-bold rounded-lg text-sm transition-colors flex items-center justify-center gap-2"
>
<Edit size={14} />
Edit
</button>
<button
onClick={() => handleDelete(plan.id)}
className="px-3 py-2 bg-red-600 hover:bg-red-700 text-white font-bold rounded-lg text-sm transition-colors"
>
<Trash2 size={14} />
</button>
</div>
</div>
))}
</div>
{/* Edit/Create Modal */}
{(editingPlan || isCreating) && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 max-w-2xl w-full border border-zinc-200 dark:border-zinc-800">
<div className="flex items-center justify-between mb-6">
<h3 className="text-2xl font-black text-zinc-900 dark:text-white">
{isCreating ? 'Create Plan' : 'Edit Plan'}
</h3>
<button
onClick={() => { setEditingPlan(null); setIsCreating(false); }}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
>
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Name (ID)</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g. starter"
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Display Name</label>
<input
type="text"
value={formData.display_name}
onChange={(e) => setFormData({ ...formData, display_name: e.target.value })}
placeholder="e.g. Starter"
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Monthly Credits</label>
<input
type="number"
value={formData.monthly_credits}
onChange={(e) => setFormData({ ...formData, monthly_credits: parseInt(e.target.value) || 0 })}
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Price ()</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })}
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Brief description of the plan"
rows={3}
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Sort Order</label>
<input
type="number"
value={formData.sort_order}
onChange={(e) => setFormData({ ...formData, sort_order: parseInt(e.target.value) || 0 })}
className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
/>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
className="w-5 h-5 rounded border-zinc-300 text-amber-600 focus:ring-amber-500"
/>
<span className="text-sm font-bold text-zinc-900 dark:text-white">Active</span>
</label>
</div>
</div>
<button
onClick={handleSave}
disabled={loading}
className="w-full py-3 bg-amber-600 hover:bg-amber-700 text-white font-bold rounded-xl transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<Check size={18} />
{isCreating ? 'Create Plan' : 'Save Changes'}
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,322 @@
'use server';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { checkIsAdmin } from './track-api-usage';
import { addCredits } from './credit-service';
export interface SubscriptionPlan {
id: string;
name: string;
display_name: string;
monthly_credits: number;
price: number;
description: string | null;
is_active: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
export interface UserSubscription {
user_id: string;
plan_id: string | null;
started_at: string;
last_credit_grant_at: string;
updated_at: string;
}
/**
* Get all subscription plans
*/
export async function getAllPlans(): Promise<SubscriptionPlan[]> {
try {
const supabase = createServerComponentClient({ cookies });
const { data, error } = await supabase
.from('subscription_plans')
.select('*')
.order('sort_order', { ascending: true });
if (error) {
console.error('Error fetching plans:', error);
return [];
}
return data || [];
} catch (err) {
console.error('Error in getAllPlans:', err);
return [];
}
}
/**
* Get active subscription plans (for users to see)
*/
export async function getActivePlans(): Promise<SubscriptionPlan[]> {
try {
const supabase = createServerComponentClient({ cookies });
const { data, error } = await supabase
.from('subscription_plans')
.select('*')
.eq('is_active', true)
.order('sort_order', { ascending: true });
if (error) {
console.error('Error fetching active plans:', error);
return [];
}
return data || [];
} catch (err) {
console.error('Error in getActivePlans:', err);
return [];
}
}
/**
* Create a new subscription plan (admin only)
*/
export async function createPlan(plan: Omit<SubscriptionPlan, 'id' | 'created_at' | 'updated_at'>): Promise<{ success: boolean; error?: string; plan?: SubscriptionPlan }> {
try {
const supabase = createServerComponentClient({ cookies });
// Check if current user is admin
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { success: false, error: 'Not authenticated' };
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) return { success: false, error: 'Not authorized' };
const { data, error } = await supabase
.from('subscription_plans')
.insert(plan)
.select()
.single();
if (error) {
console.error('Error creating plan:', error);
return { success: false, error: error.message };
}
return { success: true, plan: data };
} catch (err) {
console.error('Error in createPlan:', err);
return { success: false, error: 'Failed to create plan' };
}
}
/**
* Update a subscription plan (admin only)
*/
export async function updatePlan(planId: string, updates: Partial<SubscriptionPlan>): Promise<{ success: boolean; error?: string }> {
try {
const supabase = createServerComponentClient({ cookies });
// Check if current user is admin
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { success: false, error: 'Not authenticated' };
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) return { success: false, error: 'Not authorized' };
const { error } = await supabase
.from('subscription_plans')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', planId);
if (error) {
console.error('Error updating plan:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (err) {
console.error('Error in updatePlan:', err);
return { success: false, error: 'Failed to update plan' };
}
}
/**
* Delete a subscription plan (admin only)
*/
export async function deletePlan(planId: string): Promise<{ success: boolean; error?: string }> {
try {
const supabase = createServerComponentClient({ cookies });
// Check if current user is admin
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { success: false, error: 'Not authenticated' };
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) return { success: false, error: 'Not authorized' };
const { error } = await supabase
.from('subscription_plans')
.delete()
.eq('id', planId);
if (error) {
console.error('Error deleting plan:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (err) {
console.error('Error in deletePlan:', err);
return { success: false, error: 'Failed to delete plan' };
}
}
/**
* Get user's current subscription
*/
export async function getUserSubscription(userId: string): Promise<{ subscription: UserSubscription | null; plan: SubscriptionPlan | null }> {
try {
const supabase = createServerComponentClient({ cookies });
const { data: subscription, error: subError } = await supabase
.from('user_subscriptions')
.select('*')
.eq('user_id', userId)
.single();
if (subError || !subscription) {
return { subscription: null, plan: null };
}
if (!subscription.plan_id) {
return { subscription, plan: null };
}
const { data: plan, error: planError } = await supabase
.from('subscription_plans')
.select('*')
.eq('id', subscription.plan_id)
.single();
if (planError) {
return { subscription, plan: null };
}
return { subscription, plan };
} catch (err) {
console.error('Error in getUserSubscription:', err);
return { subscription: null, plan: null };
}
}
/**
* Set user's subscription plan (admin only)
*/
export async function setUserPlan(userId: string, planId: string): Promise<{ success: boolean; error?: string }> {
try {
const supabase = createServerComponentClient({ cookies });
// Check if current user is admin
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { success: false, error: 'Not authenticated' };
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) return { success: false, error: 'Not authorized' };
const { error } = await supabase
.from('user_subscriptions')
.upsert({
user_id: userId,
plan_id: planId,
updated_at: new Date().toISOString()
});
if (error) {
console.error('Error setting user plan:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (err) {
console.error('Error in setUserPlan:', err);
return { success: false, error: 'Failed to set user plan' };
}
}
/**
* Grant monthly credits to all users based on their subscription plan
* This should be called by a cron job or manually by admin
*/
export async function grantMonthlyCredits(): Promise<{ success: boolean; processed: number; failed: number; error?: string }> {
try {
const supabase = createServerComponentClient({ cookies });
// Check if current user is admin
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { success: false, processed: 0, failed: 0, error: 'Not authenticated' };
const isAdmin = await checkIsAdmin(user.id);
if (!isAdmin) return { success: false, processed: 0, failed: 0, error: 'Not authorized' };
// Get all user subscriptions with their plans
const { data: subscriptions, error: subError } = await supabase
.from('user_subscriptions')
.select(`
*,
plan:plan_id (
id,
display_name,
monthly_credits
)
`);
if (subError) {
console.error('Error fetching subscriptions:', subError);
return { success: false, processed: 0, failed: 0, error: 'Failed to fetch subscriptions' };
}
let processed = 0;
let failed = 0;
const now = new Date();
const oneMonthAgo = new Date(now);
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
for (const sub of subscriptions || []) {
// Check if credits were already granted this month
const lastGrant = new Date(sub.last_credit_grant_at);
if (lastGrant > oneMonthAgo) {
continue; // Already granted this month
}
const plan = sub.plan as any;
if (!plan || !plan.monthly_credits) {
failed++;
continue;
}
// Grant credits
const result = await addCredits(
sub.user_id,
plan.monthly_credits,
`Monthly credits for ${plan.display_name} plan`,
user.id
);
if (result.success) {
// Update last_credit_grant_at
await supabase
.from('user_subscriptions')
.update({ last_credit_grant_at: now.toISOString() })
.eq('user_id', sub.user_id);
processed++;
} else {
failed++;
}
}
return { success: true, processed, failed };
} catch (err) {
console.error('Error in grantMonthlyCredits:', err);
return { success: false, processed: 0, failed: 0, error: 'Failed to grant monthly credits' };
}
}

View File

@@ -346,3 +346,78 @@ FOR UPDATE USING (
-- FROM auth.users
-- ON CONFLICT (user_id) DO UPDATE SET balance = EXCLUDED.balance
-- WHERE user_credits.balance = 0;
-- ============================================
-- Subscription Plans System
-- ============================================
-- Subscription plans table
CREATE TABLE IF NOT EXISTS subscription_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
monthly_credits INTEGER NOT NULL,
price DECIMAL(10, 2) DEFAULT 0,
description TEXT,
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
);
CREATE INDEX idx_subscription_plans_active ON subscription_plans(is_active);
CREATE INDEX idx_subscription_plans_sort_order ON subscription_plans(sort_order);
-- User subscriptions table
CREATE TABLE IF NOT EXISTS user_subscriptions (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
plan_id UUID REFERENCES subscription_plans(id) ON DELETE SET NULL,
started_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
last_credit_grant_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
);
CREATE INDEX idx_user_subscriptions_plan_id ON user_subscriptions(plan_id);
-- Enable RLS
ALTER TABLE subscription_plans ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_subscriptions ENABLE ROW LEVEL SECURITY;
-- Policies for subscription_plans (everyone can view active plans)
CREATE POLICY "Anyone can view active plans" ON subscription_plans
FOR SELECT USING (is_active = true);
CREATE POLICY "Admins can manage plans" ON subscription_plans
FOR ALL USING (
auth.uid() IN (SELECT user_id FROM admin_users)
);
-- Policies for user_subscriptions
CREATE POLICY "Users can view their own subscription" ON user_subscriptions
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Admins can view all subscriptions" ON user_subscriptions
FOR SELECT USING (
auth.uid() IN (SELECT user_id FROM admin_users)
);
CREATE POLICY "Admins can manage subscriptions" ON user_subscriptions
FOR ALL USING (
auth.uid() IN (SELECT user_id FROM admin_users)
);
-- Insert default plans
INSERT INTO subscription_plans (name, display_name, monthly_credits, price, description, sort_order) VALUES
('starter', 'Starter', 10, 0.00, 'Perfect for occasional use', 1),
('bronze', 'Bronze', 50, 4.99, 'Great for regular users', 2),
('silver', 'Silver', 100, 8.99, 'Best value for power users', 3),
('gold', 'Gold', 250, 19.99, 'Unlimited searches for professionals', 4)
ON CONFLICT (name) DO NOTHING;
-- Set all existing users to Starter plan
INSERT INTO user_subscriptions (user_id, plan_id)
SELECT
u.id,
(SELECT id FROM subscription_plans WHERE name = 'starter' LIMIT 1)
FROM auth.users u
ON CONFLICT (user_id) DO NOTHING;