revops + onboarding
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { scoreUserLifecycle } from '@/lib/revops-server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -53,32 +54,35 @@ export async function POST(request: NextRequest) {
|
||||
// No active subscription
|
||||
if (!user.stripeSubscriptionId) {
|
||||
// Just update plan to FREE if somehow plan is not FREE but no subscription
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
});
|
||||
await scoreUserLifecycle(userId, 'subscription_changed');
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// Cancel the Stripe subscription
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
|
||||
// Update user plan to FREE
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(userId, 'subscription_changed');
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -65,13 +65,29 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (customerId) {
|
||||
try {
|
||||
const existingCustomer = await stripe.customers.retrieve(customerId);
|
||||
|
||||
if ('deleted' in existingCustomer && existingCustomer.deleted) {
|
||||
customerId = null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'resource_missing' || error?.type === 'StripeInvalidRequestError') {
|
||||
customerId = null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
@@ -79,30 +95,33 @@ export async function POST(request: NextRequest) {
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin;
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
},
|
||||
],
|
||||
success_url: `${appUrl}/dashboard?success=true&session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${appUrl}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
billingInterval,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { scoreUserLifecycle } from '@/lib/revops-server';
|
||||
|
||||
/**
|
||||
* Manual sync endpoint to update user subscription from Stripe
|
||||
@@ -37,17 +38,19 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (subscriptions.data.length === 0) {
|
||||
// No active subscription - set to FREE
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan: 'FREE',
|
||||
message: 'No active subscription found, set to FREE plan',
|
||||
@@ -87,18 +90,20 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: priceId,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
currentPeriodEnd,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { scoreUserLifecycle } from '@/lib/revops-server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -20,26 +21,30 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the most recent checkout session for this customer
|
||||
const checkoutSessions = await stripe.checkout.sessions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (checkoutSessions.data.length === 0) {
|
||||
return NextResponse.json({ error: 'No checkout session found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const checkoutSession = checkoutSessions.data[0];
|
||||
|
||||
// Only process if payment was successful
|
||||
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
|
||||
const subscriptionId = typeof checkoutSession.subscription === 'string'
|
||||
? checkoutSession.subscription
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { sessionId } = await request.json().catch(() => ({ sessionId: null }));
|
||||
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
return NextResponse.json({ error: 'Missing checkout session ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId);
|
||||
|
||||
const sessionBelongsToUser =
|
||||
checkoutSession.metadata?.userId === user.id ||
|
||||
checkoutSession.customer === user.stripeCustomerId;
|
||||
|
||||
if (!sessionBelongsToUser) {
|
||||
return NextResponse.json({ error: 'Checkout session does not belong to user' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Only process if payment was successful
|
||||
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
|
||||
const subscriptionId = typeof checkoutSession.subscription === 'string'
|
||||
? checkoutSession.subscription
|
||||
: checkoutSession.subscription.id;
|
||||
|
||||
// Retrieve the full subscription object
|
||||
@@ -48,41 +53,32 @@ export async function POST(request: NextRequest) {
|
||||
// Determine plan from metadata or price ID
|
||||
const plan = checkoutSession.metadata?.plan || 'PRO';
|
||||
|
||||
// Debug log to see the subscription structure
|
||||
console.log('Full subscription object:', JSON.stringify(subscription, null, 2));
|
||||
|
||||
// Get current_period_end - Stripe returns it as a Unix timestamp
|
||||
// Try different possible field names
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
// Get current_period_end - Stripe returns it as a Unix timestamp
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // Default to 30 days from now
|
||||
|
||||
console.log('Subscription data:', {
|
||||
id: subscription.id,
|
||||
periodEndTimestamp,
|
||||
currentPeriodEnd,
|
||||
priceId: subscription.items?.data?.[0]?.price?.id,
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
import { sendConversionEvent } from '@/lib/meta';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
import { sendConversionEvent } from '@/lib/meta';
|
||||
import { scoreUserLifecycle } from '@/lib/revops-server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.text();
|
||||
@@ -50,17 +51,19 @@ export async function POST(request: NextRequest) {
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const updatedUser = await db.user.update({
|
||||
where: {
|
||||
stripeCustomerId: session.customer as string,
|
||||
const updatedUser = await db.user.update({
|
||||
where: {
|
||||
stripeCustomerId: session.customer as string,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: (session.metadata?.plan || 'FREE') as any,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_changed');
|
||||
|
||||
// Meta CAPI — Purchase event
|
||||
const amountCents = session.amount_total ?? 0;
|
||||
@@ -92,34 +95,43 @@ export async function POST(request: NextRequest) {
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
const updated = await db.user.findUnique({
|
||||
where: { stripeSubscriptionId: subscription.id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (updated?.id) {
|
||||
await scoreUserLifecycle(updated.id, 'subscription_changed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
const updatedUser = await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_changed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
|
||||
Reference in New Issue
Block a user