Pro/business
This commit is contained in:
@@ -293,6 +293,32 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
const recentBillingLogs = await db.userLifecycleLog.findMany({
|
||||
where: {
|
||||
reason: {
|
||||
startsWith: 'subscription_',
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 10,
|
||||
select: {
|
||||
fromStage: true,
|
||||
toStage: true,
|
||||
reason: true,
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const users: HydratedUser[] = rawUsers.map((user) => {
|
||||
const metrics = getMetricSnapshot(user.qrCodes);
|
||||
|
||||
@@ -371,6 +397,17 @@ export async function GET(request: NextRequest) {
|
||||
.sort((a, b) => b.leadScore - a.leadScore)
|
||||
.slice(0, 25);
|
||||
|
||||
const recentBillingActivity = recentBillingLogs.map((log) => ({
|
||||
userId: log.user.id,
|
||||
name: log.user.name,
|
||||
email: log.user.email,
|
||||
plan: log.user.plan,
|
||||
reason: log.reason,
|
||||
fromStage: log.fromStage,
|
||||
toStage: log.toStage,
|
||||
createdAt: log.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
const filterOptions = {
|
||||
stages: ['cold', 'activated', 'warm', 'hot', 'upgrade_candidate', 'paid'],
|
||||
sources: Array.from(new Set(users.map((user) => user.signupSource).filter((value): value is string => Boolean(value)))),
|
||||
@@ -389,6 +426,7 @@ export async function GET(request: NextRequest) {
|
||||
mismatchCount,
|
||||
activatedUsers: funnel.activated,
|
||||
paidUsers: lifecycleSummary.paid,
|
||||
recentBillingEvents: recentBillingActivity.length,
|
||||
},
|
||||
acquisition: {
|
||||
bySource: acquisitionBySource,
|
||||
@@ -403,6 +441,7 @@ export async function GET(request: NextRequest) {
|
||||
byTeamSize: buildGroupedRows(users, 'teamSizeBucket').map((row) => ({ ...row, label: getTeamSizeLabel(row.key) })),
|
||||
},
|
||||
lifecycleSummary,
|
||||
recentBillingActivity,
|
||||
campaignSourceQuality: acquisitionBySource,
|
||||
upgradeCandidates,
|
||||
filterOptions,
|
||||
|
||||
@@ -34,13 +34,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Get user with subscription info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeSubscriptionId: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeSubscriptionId: true,
|
||||
stripeCurrentPeriodEnd: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
@@ -62,27 +63,34 @@ export async function POST(request: NextRequest) {
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
await scoreUserLifecycle(userId, 'subscription_changed');
|
||||
await scoreUserLifecycle(userId, 'subscription_deleted');
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// Cancel the Stripe subscription
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
|
||||
// Update user plan to FREE
|
||||
// Schedule cancellation at the end of the paid period so paid features stay active.
|
||||
const subscription: any = await stripe.subscriptions.update(user.stripeSubscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: user.stripeCurrentPeriodEnd;
|
||||
|
||||
// Keep the paid plan locally until Stripe sends customer.subscription.deleted.
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(userId, 'subscription_changed');
|
||||
await scoreUserLifecycle(userId, 'subscription_canceled_at_period_end');
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
return NextResponse.json({ success: true, currentPeriodEnd });
|
||||
} catch (error) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(user.id, 'subscription_deleted');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -100,7 +100,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(user.id, 'subscription_synced');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(user.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(user.id, 'subscription_created');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { getPlanFromStripePriceId, stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
import { sendConversionEvent } from '@/lib/metaConversions';
|
||||
@@ -63,7 +63,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_created');
|
||||
|
||||
// Meta CAPI — Purchase event
|
||||
const amountCents = session.amount_total ?? 0;
|
||||
@@ -98,10 +98,11 @@ export async function POST(request: NextRequest) {
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: getPlanFromStripePriceId(subscription.items.data[0].price.id) ?? undefined,
|
||||
},
|
||||
});
|
||||
const updated = await db.user.findUnique({
|
||||
@@ -109,7 +110,10 @@ export async function POST(request: NextRequest) {
|
||||
select: { id: true },
|
||||
});
|
||||
if (updated?.id) {
|
||||
await scoreUserLifecycle(updated.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(
|
||||
updated.id,
|
||||
subscription.cancel_at_period_end ? 'subscription_canceled_at_period_end' : 'subscription_updated'
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -129,7 +133,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_changed');
|
||||
await scoreUserLifecycle(updatedUser.id, 'subscription_deleted');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user