feat: add subscription_status column to user table and update related schema
- Introduced a new column `subscription_status` to the user table for improved subscription management. - Updated the user schema in the database to reflect this change. - Added a new migration file and snapshot for versioning. - Updated the StripeProvider to handle subscription status updates in webhook events.
This commit is contained in:
parent
ab8d85be00
commit
2ab25a402b
1
drizzle/0008_curious_pepper_potts.sql
Normal file
1
drizzle/0008_curious_pepper_potts.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "subscription_status" text;
|
386
drizzle/meta/0008_snapshot.json
Normal file
386
drizzle/meta/0008_snapshot.json
Normal file
@ -0,0 +1,386 @@
|
||||
{
|
||||
"id": "4542a435-02d6-4849-9970-c11721171a00",
|
||||
"prevId": "95005672-7c98-424f-b955-b44ddeaf6903",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"impersonated_by": {
|
||||
"name": "impersonated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"banned": {
|
||||
"name": "banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_reason": {
|
||||
"name": "ban_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_expires": {
|
||||
"name": "ban_expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"subscription_status": {
|
||||
"name": "subscription_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lifetime_member": {
|
||||
"name": "lifetime_member",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
@ -57,6 +57,13 @@
|
||||
"when": 1743693630071,
|
||||
"tag": "0007_overconfident_adam_destine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1743743427477,
|
||||
"tag": "0008_curious_pepper_potts",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -1,15 +1,6 @@
|
||||
import { handleWebhookEvent } from '@/payment';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* Disable body parsing as we need the raw body for signature verification
|
||||
*/
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Stripe webhook handler
|
||||
* This endpoint receives webhook events from Stripe and processes them
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { drizzle } from 'drizzle-orm/neon-http';
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
|
||||
/**
|
||||
* https://orm.drizzle.team/docs/get-started/neon-new
|
||||
@ -7,8 +6,17 @@ import { neon } from '@neondatabase/serverless';
|
||||
*
|
||||
* Using the browser-compatible Neon HTTP driver for better compatibility with Next.js
|
||||
* This avoids the Node.js-specific modules that cause build issues
|
||||
*
|
||||
* With the neon-http and neon-websockets drivers, you can access a Neon database from serverless environments over HTTP or WebSockets instead of TCP.
|
||||
* Querying over HTTP is faster for single, non-interactive transactions.
|
||||
*/
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
const db = drizzle({ client: sql });
|
||||
|
||||
// If you need to provide your existing drivers:
|
||||
// import { neon } from '@neondatabase/serverless';
|
||||
// const sql = neon(process.env.DATABASE_URL!);
|
||||
// const db = drizzle({ client: sql });
|
||||
|
||||
// https://orm.drizzle.team/docs/connect-neon
|
||||
const db = drizzle(process.env.DATABASE_URL!);
|
||||
|
||||
export default db;
|
||||
|
@ -15,6 +15,7 @@ export const user = pgTable("user", {
|
||||
banExpires: timestamp('ban_expires'),
|
||||
customerId: text('customer_id'),
|
||||
subscriptionId: text('subscription_id'),
|
||||
subscriptionStatus: text('subscription_status'),
|
||||
lifetimeMember: boolean('lifetime_member')
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PaymentProvider, PricePlan, PaymentConfig, Customer, Subscription, Payment, PaymentStatus, PlanInterval, PaymentType, Price, CreateCheckoutParams, CheckoutResult, CreatePortalParams, PortalResult, GetCustomerParams, GetSubscriptionParams, WebhookEventHandler, ListCustomerSubscriptionsParams } from "./types";
|
||||
import { StripeProvider } from "./provider/stripe";
|
||||
import { paymentConfig } from "./config/payment-config";
|
||||
|
||||
/**
|
||||
* Default payment configuration
|
||||
*/
|
||||
|
@ -1,14 +1,18 @@
|
||||
import Stripe from 'stripe';
|
||||
import { PaymentProvider, CreateCheckoutParams, CheckoutResult, CreatePortalParams, PortalResult, GetCustomerParams, Customer, GetSubscriptionParams, Subscription, PaymentStatus, PlanInterval, WebhookEventHandler, PaymentType, PaymentTypes, ListCustomerSubscriptionsParams } from '../types';
|
||||
import { getPlanById, findPriceInPlan } from '../index';
|
||||
import db from '@/db/index';
|
||||
import { user } from '@/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
/**
|
||||
* Stripe payment provider implementation
|
||||
*/
|
||||
export class StripeProvider implements PaymentProvider {
|
||||
|
||||
private stripe: Stripe;
|
||||
private webhookSecret: string;
|
||||
private webhookHandlers: Map<string, WebhookEventHandler[]>;
|
||||
private webhookSecret: string | undefined;
|
||||
|
||||
/**
|
||||
* Initialize Stripe provider with API key
|
||||
@ -19,14 +23,14 @@ export class StripeProvider implements PaymentProvider {
|
||||
throw new Error('STRIPE_SECRET_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
this.webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
if (!this.webhookSecret) {
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
if (!webhookSecret) {
|
||||
throw new Error('STRIPE_WEBHOOK_SECRET environment variable is not set.');
|
||||
}
|
||||
|
||||
// Initialize Stripe without specifying apiVersion to use default/latest version
|
||||
this.stripe = new Stripe(apiKey);
|
||||
|
||||
this.webhookSecret = webhookSecret;
|
||||
this.webhookHandlers = new Map();
|
||||
}
|
||||
|
||||
@ -121,12 +125,6 @@ export class StripeProvider implements PaymentProvider {
|
||||
*/
|
||||
private async updateUserWithCustomerId(customerId: string, email: string): Promise<void> {
|
||||
try {
|
||||
// Dynamic import to avoid circular dependencies
|
||||
// TODO: can we avoid using dynamic import?
|
||||
const { default: db } = await import('@/db/index');
|
||||
const { user } = await import('@/db/schema');
|
||||
const { eq } = await import('drizzle-orm');
|
||||
|
||||
// Update user record with customer ID if email matches
|
||||
const result = await db
|
||||
.update(user)
|
||||
@ -405,26 +403,21 @@ export class StripeProvider implements PaymentProvider {
|
||||
*/
|
||||
public async handleWebhookEvent(payload: string, signature: string): Promise<void> {
|
||||
let event: Stripe.Event;
|
||||
console.log('handle webhook event, hook secret:', this.webhookSecret);
|
||||
|
||||
try {
|
||||
// Verify the event signature if webhook secret is available
|
||||
if (this.webhookSecret) {
|
||||
event = this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
this.webhookSecret
|
||||
);
|
||||
} else {
|
||||
// Parse the event payload without verification
|
||||
event = JSON.parse(payload) as Stripe.Event;
|
||||
}
|
||||
event = this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
this.webhookSecret
|
||||
);
|
||||
|
||||
console.log(`Received Stripe webhook event: ${event.type}`);
|
||||
|
||||
// Process the event based on type
|
||||
const handlers = this.webhookHandlers.get(event.type) || [];
|
||||
const defaultHandlers = this.webhookHandlers.get('*') || [];
|
||||
|
||||
const allHandlers = [...handlers, ...defaultHandlers];
|
||||
|
||||
// If no custom handlers are registered, use default handling logic
|
||||
@ -435,7 +428,7 @@ export class StripeProvider implements PaymentProvider {
|
||||
await Promise.all(allHandlers.map(handler => handler(event)));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Handle webhook event failed:', error);
|
||||
console.error('handle webhook event error:', error);
|
||||
throw new Error('Failed to handle webhook event');
|
||||
}
|
||||
}
|
||||
@ -451,58 +444,102 @@ export class StripeProvider implements PaymentProvider {
|
||||
// Handle subscription events
|
||||
if (eventType.startsWith('customer.subscription.')) {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
console.log(`Subscription ${subscription.id} is ${subscription.status}`);
|
||||
|
||||
// Process based on subscription status
|
||||
console.log(`Processing subscription ${subscription.id}, status: ${subscription.status}`);
|
||||
|
||||
// Get customerId from subscription
|
||||
const customerId = subscription.customer as string;
|
||||
|
||||
// Process based on subscription status and event type
|
||||
switch (eventType) {
|
||||
case 'customer.subscription.created':
|
||||
// Handle subscription creation
|
||||
case 'customer.subscription.created': {
|
||||
// New subscription created - update user record with subscription ID and status
|
||||
const result = await db
|
||||
.update(user)
|
||||
.set({
|
||||
subscriptionId: subscription.id,
|
||||
subscriptionStatus: subscription.status,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId))
|
||||
.returning({ id: user.id });
|
||||
|
||||
if (result.length > 0) {
|
||||
console.log(`Updated user ${customerId} with subscription ${subscription.id}`);
|
||||
} else {
|
||||
console.warn(`Update operation performed but no rows were updated for customerId ${customerId}`);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'customer.subscription.updated':
|
||||
// Handle subscription update
|
||||
}
|
||||
case 'customer.subscription.updated': {
|
||||
// Subscription was updated - update status
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
subscriptionStatus: subscription.status,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId) && eq(user.subscriptionId, subscription.id));
|
||||
|
||||
console.log(`Updated subscription status for user ${customerId} to ${subscription.status}`);
|
||||
break;
|
||||
case 'customer.subscription.deleted':
|
||||
// Handle subscription cancellation
|
||||
}
|
||||
case 'customer.subscription.deleted': {
|
||||
// Subscription was cancelled/deleted - remove from user
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
subscriptionId: null,
|
||||
subscriptionStatus: 'canceled',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId) && eq(user.subscriptionId, subscription.id));
|
||||
|
||||
console.log(`Removed subscription from user ${customerId}`);
|
||||
break;
|
||||
case 'customer.subscription.trial_will_end':
|
||||
// Handle trial ending soon
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Handle payment events
|
||||
else if (eventType.startsWith('payment_intent.')) {
|
||||
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
||||
console.log(`Payment ${paymentIntent.id} is ${paymentIntent.status}`);
|
||||
|
||||
switch (eventType) {
|
||||
case 'payment_intent.succeeded':
|
||||
// Handle successful payment
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
// Handle failed payment
|
||||
}
|
||||
case 'customer.subscription.trial_will_end': {
|
||||
// Trial ending soon - we could trigger an email notification here
|
||||
console.log(`Trial ending soon for subscription ${subscription.id}, customerId ${customerId}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle checkout events
|
||||
else if (eventType.startsWith('checkout.')) {
|
||||
if (eventType === 'checkout.session.completed') {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
console.log(`Checkout session ${session.id} completed`);
|
||||
|
||||
// Handle completed checkout
|
||||
if (session.mode === 'subscription') {
|
||||
// Handle subscription checkout
|
||||
const subscriptionId = session.subscription as string;
|
||||
console.log(`New subscription: ${subscriptionId}`);
|
||||
} else if (session.mode === 'payment') {
|
||||
// Handle one-time payment checkout
|
||||
const paymentIntentId = session.payment_intent as string;
|
||||
console.log(`One-time payment: ${paymentIntentId}`);
|
||||
|
||||
// Only process one-time payments (likely for lifetime plan)
|
||||
if (session.mode === 'payment') {
|
||||
const customerId = session.customer as string;
|
||||
console.log(`Processing one-time payment for customer ${customerId}`);
|
||||
|
||||
// Check if this was for a lifetime plan (via metadata)
|
||||
const metadata = session.metadata || {};
|
||||
const planId = metadata.planId;
|
||||
|
||||
if (planId === 'lifetime') {
|
||||
// Mark user as lifetime member
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
lifetimeMember: true,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId));
|
||||
|
||||
console.log(`Marked user ${customerId} as lifetime member`);
|
||||
} else {
|
||||
// Handle other one-time payments if needed, like increase user credits
|
||||
console.log(`One-time payment for non-lifetime plan: ${planId}, customerId: ${customerId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Default webhook handler failed:', error);
|
||||
console.error('default webhook handler error:', error);
|
||||
throw new Error('Failed to handle webhook event');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user