feat(billing): implement robust in-app subscription cancellation & fix CI/CD socket port typo
This commit is contained in:
66
memento-note/lib/billing/cancel-subscription.ts
Normal file
66
memento-note/lib/billing/cancel-subscription.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
|
||||
export interface CancelSubscriptionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an active user subscription by setting its auto-renew (cancel_at_period_end) status to true
|
||||
* in Stripe and synchronizing the status back into the local Prisma database.
|
||||
*
|
||||
* @param userId - Unique identifier of the user
|
||||
* @returns An object indicating the success or failure of the operation
|
||||
*/
|
||||
export async function cancelSubscription(userId: string): Promise<CancelSubscriptionResult> {
|
||||
if (!userId) {
|
||||
return { success: false, error: 'User ID is required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
return { success: false, error: 'No active subscription found' };
|
||||
}
|
||||
|
||||
if (subscription.stripeSubscriptionId) {
|
||||
// Direct call to Stripe API with period end cancellation set to true
|
||||
const updatedSub = await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
|
||||
// Local database synchronization
|
||||
await prisma.subscription.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
cancelAtPeriodEnd: true,
|
||||
canceledAt: updatedSub.canceled_at ? new Date(updatedSub.canceled_at * 1000) : new Date(),
|
||||
currentPeriodEnd: updatedSub.current_period_end ? new Date(updatedSub.current_period_end * 1000) : subscription.currentPeriodEnd,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Mock mode cancel (e.g. for development / local bypass testing)
|
||||
await prisma.subscription.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
cancelAtPeriodEnd: true,
|
||||
canceledAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error('[cancelSubscription] Error processing cancellation:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to cancel subscription',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,22 @@ export function resolvePriceId(tier: BillingTier, interval: BillingInterval): st
|
||||
};
|
||||
const priceId = map[tier][interval];
|
||||
if (!priceId) {
|
||||
const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder';
|
||||
if (isMock && process.env.NODE_ENV !== 'test') {
|
||||
return `price_mock_${tier.toLowerCase()}_${interval}`;
|
||||
}
|
||||
throw new Error(`No Stripe price ID configured for ${tier}/${interval}`);
|
||||
}
|
||||
return priceId;
|
||||
}
|
||||
|
||||
export function priceIdToTier(priceId: string): SubscriptionTier | null {
|
||||
if (priceId && priceId.startsWith('price_mock_')) {
|
||||
if (priceId.includes('pro')) return 'PRO';
|
||||
if (priceId.includes('business')) return 'BUSINESS';
|
||||
return 'BASIC';
|
||||
}
|
||||
|
||||
const entries: Array<[string | undefined, SubscriptionTier]> = [
|
||||
[process.env.STRIPE_PRICE_PRO_MONTHLY, 'PRO'],
|
||||
[process.env.STRIPE_PRICE_PRO_ANNUAL, 'PRO'],
|
||||
|
||||
@@ -44,8 +44,19 @@ export async function syncSubscriptionFromStripe(
|
||||
|
||||
const status = mapStripeStatus(subscription.status);
|
||||
|
||||
const currentPeriodStart = new Date(((subscription as any).current_period_start as number) * 1000);
|
||||
const currentPeriodEnd = new Date(((subscription as any).current_period_end as number) * 1000);
|
||||
const currentPeriodStartTimestamp =
|
||||
subscription.current_period_start ??
|
||||
(subscription as any).items?.data?.[0]?.current_period_start ??
|
||||
(subscription as any).start_date ??
|
||||
Math.floor(Date.now() / 1000);
|
||||
|
||||
const currentPeriodEndTimestamp =
|
||||
subscription.current_period_end ??
|
||||
(subscription as any).items?.data?.[0]?.current_period_end ??
|
||||
(currentPeriodStartTimestamp + 30 * 24 * 3600);
|
||||
|
||||
const currentPeriodStart = new Date(currentPeriodStartTimestamp * 1000);
|
||||
const currentPeriodEnd = new Date(currentPeriodEndTimestamp * 1000);
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
where: { stripeSubscriptionId: subscription.id },
|
||||
|
||||
Reference in New Issue
Block a user