sysrot-hub
Version:
CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026
449 lines (385 loc) • 13.8 kB
text/typescript
import { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';
import { getSession } from 'next-auth/react';
import { z } from 'zod';
import Stripe from 'stripe';
const prisma = new PrismaClient();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16'
});
// Validation schemas
const createSubscriptionSchema = z.object({
organizationId: z.string().min(1, 'Organization ID is required'),
planId: z.string().min(1, 'Plan ID is required'),
billingInterval: z.enum(['month', 'year']).default('month')
});
const updateSubscriptionSchema = z.object({
planId: z.string().min(1, 'Plan ID is required'),
billingInterval: z.enum(['month', 'year']).optional()
});
async function hasPermission(userId: string, organizationId: string, requiredRole: string[] = ['OWNER', 'ADMIN']) {
const membership = await prisma.organizationMember.findUnique({
where: {
organizationId_userId: {
organizationId,
userId
}
}
});
return membership && requiredRole.includes(membership.role);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const session = await getSession({ req });
if (!session?.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { method, query } = req;
const { organizationId } = query;
switch (method) {
case 'GET':
return await getSubscription(req, res);
case 'POST':
return await createSubscription(req, res);
case 'PUT':
return await updateSubscription(req, res);
case 'DELETE':
return await cancelSubscription(req, res);
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']);
return res.status(405).end('Method Not Allowed');
}
} catch (error) {
console.error('Subscriptions API error:', error);
return res.status(500).json({ error: 'Internal server error' });
} finally {
await prisma.$disconnect();
}
}
async function getSubscription(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const { organizationId } = req.query;
if (!organizationId || typeof organizationId !== 'string') {
return res.status(400).json({ error: 'Organization ID is required' });
}
try {
// Check permissions
const hasAccess = await hasPermission(session.user.id, organizationId);
if (!hasAccess) {
return res.status(403).json({ error: 'Access denied' });
}
const subscription = await prisma.subscription.findUnique({
where: { organizationId },
include: {
plan: true,
organization: {
select: {
name: true,
status: true
}
}
}
});
if (!subscription) {
return res.status(404).json({ error: 'Subscription not found' });
}
// Get Stripe subscription details if available
let stripeSubscription = null;
if (subscription.stripeSubscriptionId) {
try {
stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId);
} catch (error) {
console.warn('Failed to fetch Stripe subscription:', error);
}
}
res.status(200).json({
subscription: {
...subscription,
stripeDetails: stripeSubscription
}
});
} catch (error) {
console.error('Error fetching subscription:', error);
res.status(500).json({ error: 'Failed to fetch subscription' });
}
}
async function createSubscription(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
try {
const validatedData = createSubscriptionSchema.parse(req.body);
const { organizationId, planId, billingInterval } = validatedData;
// Check permissions
const hasAccess = await hasPermission(session.user.id, organizationId, ['OWNER']);
if (!hasAccess) {
return res.status(403).json({ error: 'Only organization owners can manage subscriptions' });
}
// Check if organization already has a subscription
const existingSubscription = await prisma.subscription.findUnique({
where: { organizationId }
});
if (existingSubscription) {
return res.status(400).json({ error: 'Organization already has a subscription' });
}
// Get organization and plan details
const [organization, plan] = await Promise.all([
prisma.organization.findUnique({
where: { id: organizationId },
include: {
members: {
where: { role: 'OWNER' },
include: { user: true }
}
}
}),
prisma.plan.findUnique({
where: { id: planId }
})
]);
if (!organization || !plan) {
return res.status(404).json({ error: 'Organization or plan not found' });
}
const owner = organization.members[0]?.user;
if (!owner) {
return res.status(400).json({ error: 'Organization owner not found' });
}
// Create or retrieve Stripe customer
let stripeCustomer;
const existingCustomers = await stripe.customers.list({
email: owner.email,
limit: 1
});
if (existingCustomers.data.length > 0) {
stripeCustomer = existingCustomers.data[0];
} else {
stripeCustomer = await stripe.customers.create({
email: owner.email,
name: owner.name || undefined,
metadata: {
organizationId: organization.id,
organizationName: organization.name
}
});
}
// Determine the price based on billing interval
const stripePriceId = billingInterval === 'year' && plan.stripePriceId
? plan.stripePriceId // Assume yearly price ID is stored differently or calculated
: plan.stripePriceId;
if (!stripePriceId) {
return res.status(400).json({ error: 'Plan does not have a valid Stripe price ID' });
}
// Create Stripe subscription
const stripeSubscription = await stripe.subscriptions.create({
customer: stripeCustomer.id,
items: [{ price: stripePriceId }],
trial_period_days: organization.trialEndsAt && organization.trialEndsAt > new Date() ? 14 : undefined,
metadata: {
organizationId: organization.id,
planId: plan.id
}
});
// Create subscription in database
const subscription = await prisma.subscription.create({
data: {
organizationId,
planId,
stripeCustomerId: stripeCustomer.id,
stripeSubscriptionId: stripeSubscription.id,
stripePriceId,
status: stripeSubscription.status === 'active' ? 'ACTIVE' :
stripeSubscription.status === 'trialing' ? 'TRIALING' : 'ACTIVE',
currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000),
currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000),
trialStart: stripeSubscription.trial_start ? new Date(stripeSubscription.trial_start * 1000) : null,
trialEnd: stripeSubscription.trial_end ? new Date(stripeSubscription.trial_end * 1000) : null,
stripeCurrentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000)
},
include: {
plan: true
}
});
// Update organization with new plan
await prisma.organization.update({
where: { id: organizationId },
data: {
planId,
userLimit: plan.userLimit,
storageLimit: plan.storageLimit,
apiLimit: plan.apiLimit
}
});
// Log activity
await prisma.activity.create({
data: {
organizationId,
userId: session.user.id,
action: 'subscription_created',
metadata: {
planName: plan.name,
stripeSubscriptionId: stripeSubscription.id
}
}
});
res.status(201).json({
subscription,
checkoutUrl: null, // Could add checkout session URL if needed
message: 'Subscription created successfully'
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error creating subscription:', error);
res.status(500).json({ error: 'Failed to create subscription' });
}
}
async function updateSubscription(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const { organizationId } = req.query;
if (!organizationId || typeof organizationId !== 'string') {
return res.status(400).json({ error: 'Organization ID is required' });
}
try {
const validatedData = updateSubscriptionSchema.parse(req.body);
const { planId } = validatedData;
// Check permissions
const hasAccess = await hasPermission(session.user.id, organizationId, ['OWNER']);
if (!hasAccess) {
return res.status(403).json({ error: 'Only organization owners can manage subscriptions' });
}
// Get current subscription and new plan
const [currentSubscription, newPlan] = await Promise.all([
prisma.subscription.findUnique({
where: { organizationId },
include: { plan: true }
}),
prisma.plan.findUnique({
where: { id: planId }
})
]);
if (!currentSubscription || !newPlan) {
return res.status(404).json({ error: 'Subscription or plan not found' });
}
if (currentSubscription.planId === planId) {
return res.status(400).json({ error: 'Already subscribed to this plan' });
}
// Update Stripe subscription
if (currentSubscription.stripeSubscriptionId && newPlan.stripePriceId) {
const stripeSubscription = await stripe.subscriptions.retrieve(
currentSubscription.stripeSubscriptionId
);
await stripe.subscriptions.update(currentSubscription.stripeSubscriptionId, {
items: [{
id: stripeSubscription.items.data[0].id,
price: newPlan.stripePriceId
}],
proration_behavior: 'create_prorations'
});
}
// Update subscription in database
const updatedSubscription = await prisma.subscription.update({
where: { organizationId },
data: {
planId,
stripePriceId: newPlan.stripePriceId
},
include: {
plan: true
}
});
// Update organization limits
await prisma.organization.update({
where: { id: organizationId },
data: {
planId,
userLimit: newPlan.userLimit,
storageLimit: newPlan.storageLimit,
apiLimit: newPlan.apiLimit
}
});
// Log activity
await prisma.activity.create({
data: {
organizationId,
userId: session.user.id,
action: 'subscription_updated',
metadata: {
oldPlan: currentSubscription.plan.name,
newPlan: newPlan.name
}
}
});
res.status(200).json({
subscription: updatedSubscription,
message: 'Subscription updated successfully'
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error updating subscription:', error);
res.status(500).json({ error: 'Failed to update subscription' });
}
}
async function cancelSubscription(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const { organizationId } = req.query;
if (!organizationId || typeof organizationId !== 'string') {
return res.status(400).json({ error: 'Organization ID is required' });
}
try {
// Check permissions
const hasAccess = await hasPermission(session.user.id, organizationId, ['OWNER']);
if (!hasAccess) {
return res.status(403).json({ error: 'Only organization owners can cancel subscriptions' });
}
const subscription = await prisma.subscription.findUnique({
where: { organizationId },
include: { plan: true }
});
if (!subscription) {
return res.status(404).json({ error: 'Subscription not found' });
}
// Cancel Stripe subscription at period end
if (subscription.stripeSubscriptionId) {
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
cancel_at_period_end: true
});
}
// Update subscription in database
const updatedSubscription = await prisma.subscription.update({
where: { organizationId },
data: {
cancelAtPeriodEnd: true,
canceledAt: new Date()
},
include: {
plan: true
}
});
// Log activity
await prisma.activity.create({
data: {
organizationId,
userId: session.user.id,
action: 'subscription_cancelled',
metadata: {
planName: subscription.plan.name,
cancelAtPeriodEnd: true
}
}
});
res.status(200).json({
subscription: updatedSubscription,
message: 'Subscription will be cancelled at the end of the current billing period'
});
} catch (error) {
console.error('Error cancelling subscription:', error);
res.status(500).json({ error: 'Failed to cancel subscription' });
}
}