Pro/business
This commit is contained in:
163
manual-billing-sql.txt
Normal file
163
manual-billing-sql.txt
Normal file
@@ -0,0 +1,163 @@
|
||||
Manual billing SQL for QR Master
|
||||
================================
|
||||
|
||||
Use these statements for manual plan changes in the database.
|
||||
Replace USER_EMAIL_HERE before running.
|
||||
|
||||
Do not use a naked UPDATE like:
|
||||
UPDATE "User" SET "plan" = 'PRO' WHERE email = '...';
|
||||
|
||||
That changes the plan but creates no billing log. Always use the matching full
|
||||
transaction below so UserLifecycleLog gets the alert entry.
|
||||
|
||||
Upgrade user to PRO
|
||||
===================
|
||||
|
||||
BEGIN;
|
||||
|
||||
WITH old_user AS (
|
||||
SELECT *
|
||||
FROM "User"
|
||||
WHERE email = 'USER_EMAIL_HERE'
|
||||
FOR UPDATE
|
||||
),
|
||||
updated_user AS (
|
||||
UPDATE "User"
|
||||
SET
|
||||
"plan" = 'PRO',
|
||||
"lifecycleStage" = 'paid',
|
||||
"lastScoredAt" = NOW(),
|
||||
"lastQualifiedAt" = NOW(),
|
||||
"updatedAt" = NOW()
|
||||
WHERE "id" = (SELECT "id" FROM old_user)
|
||||
RETURNING *
|
||||
)
|
||||
INSERT INTO "UserLifecycleLog" (
|
||||
"id",
|
||||
"userId",
|
||||
"fromStage",
|
||||
"toStage",
|
||||
"fitScore",
|
||||
"intentScore",
|
||||
"leadScore",
|
||||
"reason",
|
||||
"createdAt"
|
||||
)
|
||||
SELECT
|
||||
CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)),
|
||||
updated_user."id",
|
||||
old_user."lifecycleStage",
|
||||
'paid',
|
||||
updated_user."fitScore",
|
||||
updated_user."intentScore",
|
||||
updated_user."leadScore",
|
||||
'subscription_created',
|
||||
NOW()
|
||||
FROM updated_user
|
||||
JOIN old_user ON old_user."id" = updated_user."id";
|
||||
|
||||
COMMIT;
|
||||
|
||||
Upgrade user to BUSINESS
|
||||
========================
|
||||
|
||||
BEGIN;
|
||||
|
||||
WITH old_user AS (
|
||||
SELECT *
|
||||
FROM "User"
|
||||
WHERE email = 'USER_EMAIL_HERE'
|
||||
FOR UPDATE
|
||||
),
|
||||
updated_user AS (
|
||||
UPDATE "User"
|
||||
SET
|
||||
"plan" = 'BUSINESS',
|
||||
"lifecycleStage" = 'paid',
|
||||
"lastScoredAt" = NOW(),
|
||||
"lastQualifiedAt" = NOW(),
|
||||
"updatedAt" = NOW()
|
||||
WHERE "id" = (SELECT "id" FROM old_user)
|
||||
RETURNING *
|
||||
)
|
||||
INSERT INTO "UserLifecycleLog" (
|
||||
"id",
|
||||
"userId",
|
||||
"fromStage",
|
||||
"toStage",
|
||||
"fitScore",
|
||||
"intentScore",
|
||||
"leadScore",
|
||||
"reason",
|
||||
"createdAt"
|
||||
)
|
||||
SELECT
|
||||
CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)),
|
||||
updated_user."id",
|
||||
old_user."lifecycleStage",
|
||||
'paid',
|
||||
updated_user."fitScore",
|
||||
updated_user."intentScore",
|
||||
updated_user."leadScore",
|
||||
'subscription_created',
|
||||
NOW()
|
||||
FROM updated_user
|
||||
JOIN old_user ON old_user."id" = updated_user."id";
|
||||
|
||||
COMMIT;
|
||||
|
||||
Set user to FREE after deabo / subscription ended
|
||||
=================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
WITH old_user AS (
|
||||
SELECT *
|
||||
FROM "User"
|
||||
WHERE email = 'USER_EMAIL_HERE'
|
||||
FOR UPDATE
|
||||
),
|
||||
updated_user AS (
|
||||
UPDATE "User"
|
||||
SET
|
||||
"plan" = 'FREE',
|
||||
"stripeSubscriptionId" = NULL,
|
||||
"stripePriceId" = NULL,
|
||||
"stripeCurrentPeriodEnd" = NULL,
|
||||
"lifecycleStage" = CASE
|
||||
WHEN COALESCE("leadScore", 0) >= 70 THEN 'upgrade_candidate'
|
||||
WHEN COALESCE("leadScore", 0) >= 55 THEN 'hot'
|
||||
WHEN COALESCE("leadScore", 0) >= 30 THEN 'warm'
|
||||
WHEN "activationAt" IS NOT NULL THEN 'activated'
|
||||
ELSE 'cold'
|
||||
END,
|
||||
"lastScoredAt" = NOW(),
|
||||
"updatedAt" = NOW()
|
||||
WHERE "id" = (SELECT "id" FROM old_user)
|
||||
RETURNING *
|
||||
)
|
||||
INSERT INTO "UserLifecycleLog" (
|
||||
"id",
|
||||
"userId",
|
||||
"fromStage",
|
||||
"toStage",
|
||||
"fitScore",
|
||||
"intentScore",
|
||||
"leadScore",
|
||||
"reason",
|
||||
"createdAt"
|
||||
)
|
||||
SELECT
|
||||
CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)),
|
||||
updated_user."id",
|
||||
old_user."lifecycleStage",
|
||||
updated_user."lifecycleStage",
|
||||
updated_user."fitScore",
|
||||
updated_user."intentScore",
|
||||
updated_user."leadScore",
|
||||
'subscription_deleted',
|
||||
NOW()
|
||||
FROM updated_user
|
||||
JOIN old_user ON old_user."id" = updated_user."id";
|
||||
|
||||
COMMIT;
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
BarChart3,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Download,
|
||||
Filter,
|
||||
Loader2,
|
||||
@@ -68,6 +69,12 @@ type SegmentRow = {
|
||||
qrCount: number;
|
||||
dynamicQrCount: number;
|
||||
scanCount: number;
|
||||
lifecycleLogs: Array<{
|
||||
fromStage: string | null;
|
||||
toStage: string;
|
||||
reason: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type DashboardData = {
|
||||
@@ -76,6 +83,7 @@ type DashboardData = {
|
||||
mismatchCount: number;
|
||||
activatedUsers: number;
|
||||
paidUsers: number;
|
||||
recentBillingEvents: number;
|
||||
};
|
||||
acquisition: {
|
||||
bySource: Array<{
|
||||
@@ -119,6 +127,16 @@ type DashboardData = {
|
||||
byTeamSize: Array<any>;
|
||||
};
|
||||
lifecycleSummary: Record<string, number>;
|
||||
recentBillingActivity: Array<{
|
||||
userId: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
plan: string;
|
||||
reason: string | null;
|
||||
fromStage: string | null;
|
||||
toStage: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
campaignSourceQuality: Array<any>;
|
||||
upgradeCandidates: Array<SegmentRow>;
|
||||
filterOptions: {
|
||||
@@ -184,6 +202,27 @@ function buildQuery(filters: Filters) {
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function formatStage(value: string | null) {
|
||||
return value ? value.replace(/_/g, ' ') : 'none';
|
||||
}
|
||||
|
||||
function formatBillingReason(value: string | null) {
|
||||
switch (value) {
|
||||
case 'subscription_created':
|
||||
return 'Started paid plan';
|
||||
case 'subscription_updated':
|
||||
return 'Updated subscription';
|
||||
case 'subscription_canceled_at_period_end':
|
||||
return 'Cancels at period end';
|
||||
case 'subscription_deleted':
|
||||
return 'Ended subscription';
|
||||
case 'subscription_synced':
|
||||
return 'Synced subscription';
|
||||
default:
|
||||
return 'Subscription changed';
|
||||
}
|
||||
}
|
||||
|
||||
function LifecycleCard({
|
||||
label,
|
||||
value,
|
||||
@@ -475,6 +514,50 @@ export default function NewsletterClient() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<CreditCard className="h-5 w-5 text-slate-500" />
|
||||
<CardTitle>Recent Billing Activity</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.recentBillingActivity.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
<TableHead>Lifecycle change</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.recentBillingActivity.map((event) => (
|
||||
<TableRow key={`${event.userId}-${event.createdAt}`}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">{event.name || event.email}</div>
|
||||
<div className="text-xs text-slate-500">{event.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{event.plan}</TableCell>
|
||||
<TableCell>{formatBillingReason(event.reason)}</TableCell>
|
||||
<TableCell>
|
||||
{formatStage(event.fromStage)} -> {formatStage(event.toStage)}
|
||||
</TableCell>
|
||||
<TableCell>{new Date(event.createdAt).toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">No billing activity logged yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mb-8 grid gap-6 xl:grid-cols-[1.4fr_1fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function PricingPage() {
|
||||
const handleDowngrade = async () => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
|
||||
'Are you sure you want to cancel your paid plan? You will keep premium features until the end of your current billing period.'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -99,7 +99,7 @@ export default function PricingPage() {
|
||||
throw new Error(error.error || 'Failed to cancel subscription');
|
||||
}
|
||||
|
||||
showToast('Successfully downgraded to Free plan', 'success');
|
||||
showToast('Subscription will end at the end of your current billing period.', 'success');
|
||||
|
||||
// Refresh to update the plan
|
||||
setTimeout(() => {
|
||||
@@ -148,7 +148,7 @@ export default function PricingPage() {
|
||||
'Standard QR design templates',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Cancel paid plan',
|
||||
buttonVariant: 'outline' as const,
|
||||
disabled: currentPlan === 'FREE',
|
||||
popular: false,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ type ScoreReason =
|
||||
| 'onboarding_update'
|
||||
| 'qr_created'
|
||||
| 'scan_recorded'
|
||||
| 'subscription_changed';
|
||||
| 'subscription_changed'
|
||||
| 'subscription_created'
|
||||
| 'subscription_updated'
|
||||
| 'subscription_canceled_at_period_end'
|
||||
| 'subscription_deleted'
|
||||
| 'subscription_synced';
|
||||
|
||||
type UserForScoring = {
|
||||
id: string;
|
||||
@@ -159,7 +164,27 @@ export async function scoreUserLifecycle(userId: string, reason: ScoreReason) {
|
||||
},
|
||||
});
|
||||
|
||||
if (user.lifecycleStage !== nextStage) {
|
||||
const isSubscriptionReason = reason.startsWith('subscription_');
|
||||
const recentSubscriptionLog = isSubscriptionReason
|
||||
? await db.userLifecycleLog.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
reason,
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 10 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
const shouldLogLifecycleEvent =
|
||||
user.lifecycleStage !== nextStage ||
|
||||
(isSubscriptionReason && !recentSubscriptionLog);
|
||||
|
||||
if (shouldLogLifecycleEvent) {
|
||||
await db.userLifecycleLog.create({
|
||||
data: {
|
||||
userId,
|
||||
|
||||
@@ -20,7 +20,7 @@ export function validateStripeKey() {
|
||||
}
|
||||
}
|
||||
|
||||
export const STRIPE_PLANS = {
|
||||
export const STRIPE_PLANS = {
|
||||
FREE: {
|
||||
name: 'Free / Starter',
|
||||
price: 0,
|
||||
@@ -80,6 +80,28 @@ export const STRIPE_PLANS = {
|
||||
priceId: process.env.STRIPE_PRICE_ID_BUSINESS_MONTHLY,
|
||||
priceIdYearly: process.env.STRIPE_PRICE_ID_BUSINESS_YEARLY,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PlanType = keyof typeof STRIPE_PLANS;
|
||||
} as const;
|
||||
|
||||
export type PlanType = keyof typeof STRIPE_PLANS;
|
||||
|
||||
export function getPlanFromStripePriceId(priceId?: string | null): Exclude<PlanType, 'FREE'> | null {
|
||||
if (!priceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
priceId === process.env.STRIPE_PRICE_ID_BUSINESS_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_BUSINESS_YEARLY
|
||||
) {
|
||||
return 'BUSINESS';
|
||||
}
|
||||
|
||||
if (
|
||||
priceId === process.env.STRIPE_PRICE_ID_PRO_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_PRO_YEARLY
|
||||
) {
|
||||
return 'PRO';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user