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

@@ -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;