refactor: add support for webhook event of invoice.payment_succeeded & optimize the whole process

This commit is contained in:
javayhu 2025-09-08 00:36:01 +08:00
parent 5cebf2ef00
commit b8d3d09d9e

View File

@ -8,12 +8,8 @@ import {
import { getCreditPackageById } from '@/credits/server'; import { getCreditPackageById } from '@/credits/server';
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
import { getDb } from '@/db'; import { getDb } from '@/db';
import { creditTransaction, payment, user } from '@/db/schema'; import { payment, user } from '@/db/schema';
import { import { findPlanByPlanId, findPriceInPlan } from '@/lib/price-plan';
findPlanByPlanId,
findPlanByPriceId,
findPriceInPlan,
} from '@/lib/price-plan';
import { sendNotification } from '@/notification/notification'; import { sendNotification } from '@/notification/notification';
import { desc, eq } from 'drizzle-orm'; import { desc, eq } from 'drizzle-orm';
import { Stripe } from 'stripe'; import { Stripe } from 'stripe';
@ -523,88 +519,162 @@ export class StripeProvider implements PaymentProvider {
} }
/** /**
* Handle successful invoice payment * Handle successful invoice payment - NEW ARCHITECTURE
* Only create payment records here after payment is confirmed
* @param invoice Stripe invoice * @param invoice Stripe invoice
*/ */
private async onInvoicePaymentSucceeded( private async onInvoicePaymentSucceeded(
invoice: Stripe.Invoice invoice: Stripe.Invoice
): Promise<void> { ): Promise<void> {
console.log('>> Handle invoice payment succeeded'); console.log('>> Handle invoice payment succeeded');
const subscriptionId = invoice.subscription as string | null;
try { try {
const subscriptionId = invoice.subscription as string | null;
if (subscriptionId) { if (subscriptionId) {
// This is a subscription payment // This is a subscription payment
await this.handleSubscriptionPayment(invoice, subscriptionId); await this.createSubscriptionPayment(invoice, subscriptionId);
} else { } else {
// This is a one-time payment with invoice // This is a one-time payment
await this.handleOneTimeInvoicePayment(invoice); await this.createOneTimePayment(invoice);
} }
console.log('<< Successfully processed invoice payment');
} catch (error) { } catch (error) {
console.error('<< Handle invoice payment succeeded error:', error); console.error('<< Handle invoice payment succeeded error:', error);
// Check if it's a duplicate invoice error (database constraint violation)
if (
error instanceof Error &&
error.message.includes('unique constraint')
) {
console.log('<< Invoice already processed:', invoice.id);
return; // Don't throw, this is expected for duplicate processing
}
// For other errors, let Stripe retry
throw error; throw error;
} }
} }
/** /**
* Handle subscription payment success * Create subscription payment record and process benefits - NEW ARCHITECTURE
*
* The order of events may be:
* checkout.session.completed
* customer.subscription.created
* customer.subscription.updated
* invoice.payment_succeeded
*
* @param invoice Stripe invoice * @param invoice Stripe invoice
* @param subscriptionId Subscription ID * @param subscriptionId Subscription ID
*/ */
private async handleSubscriptionPayment( private async createSubscriptionPayment(
invoice: Stripe.Invoice, invoice: Stripe.Invoice,
subscriptionId: string subscriptionId: string
): Promise<void> { ): Promise<void> {
console.log( console.log(
'>> Handle subscription payment for subscription:', '>> Create subscription payment record for subscription:',
subscriptionId subscriptionId
); );
try { try {
const db = await getDb(); // Get subscription details from Stripe
const subscription =
await this.stripe.subscriptions.retrieve(subscriptionId);
const customerId = subscription.customer as string;
// Find the payment record for this subscription // Get priceId from subscription items
const payments = await db const priceId = subscription.items.data[0]?.price.id;
.select({ if (!priceId) {
id: payment.id, console.warn('<< No priceId found for subscription');
userId: payment.userId,
priceId: payment.priceId,
})
.from(payment)
.where(eq(payment.subscriptionId, subscriptionId))
.limit(1);
if (payments.length === 0) {
console.warn(
'<< No payment record found for subscription:',
subscriptionId
);
return; return;
} }
const { userId, priceId } = payments[0]; // Get userId from subscription metadata or fallback to customerId lookup
let userId: string | undefined = subscription.metadata.userId;
// If no userId in metadata (common in renewals), find by customerId
if (!userId) {
console.log('No userId in metadata, finding by customerId');
userId = await this.findUserIdByCustomerId(customerId);
if (!userId) {
console.error('<< No userId found, this should not happen');
return;
}
}
const periodStart = this.getPeriodStart(subscription);
const periodEnd = this.getPeriodEnd(subscription);
const trialStart = subscription.trial_start
? new Date(subscription.trial_start * 1000)
: null;
const trialEnd = subscription.trial_end
? new Date(subscription.trial_end * 1000)
: null;
const currentDate = new Date();
// Create payment record with subscription status
const db = await getDb();
const paymentResult = await db
.insert(payment)
.values({
id: randomUUID(),
priceId,
type: PaymentTypes.SUBSCRIPTION,
userId,
customerId,
subscriptionId,
invoiceId: invoice.id,
interval: this.mapStripeIntervalToPlanInterval(subscription),
status: this.mapSubscriptionStatusToPaymentStatus(
subscription.status
), // Use actual subscription status
periodStart,
periodEnd,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
trialStart,
trialEnd,
createdAt: currentDate,
updatedAt: currentDate,
})
.returning({ id: payment.id });
if (paymentResult.length === 0) {
console.warn('<< Failed to create subscription payment record');
return;
}
// Add subscription credits if enabled // Add subscription credits if enabled
if (userId && priceId && websiteConfig.credits?.enableCredits) { if (websiteConfig.credits?.enableCredits) {
await addSubscriptionCredits(userId, priceId); await addSubscriptionCredits(userId, priceId);
console.log('Added subscription credits for invoice:', invoice.id); console.log('Added subscription credits for invoice:', invoice.id);
} }
console.log('<< Successfully processed subscription payment'); console.log('<< Successfully processed subscription payment');
} catch (error) { } catch (error) {
console.error('<< Handle subscription payment error:', error); console.error('<< Create subscription payment error:', error);
// Don't throw error if it's already processed
if (
error instanceof Error &&
error.message.includes('unique constraint')
) {
console.log('<< Subscription payment already processed:', invoice.id);
return;
}
throw error; throw error;
} }
} }
/** /**
* Handle one-time payment with invoice * Create one-time payment record and process benefits - NEW ARCHITECTURE
* @param invoice Stripe invoice * @param invoice Stripe invoice
*/ */
private async handleOneTimeInvoicePayment( private async createOneTimePayment(invoice: Stripe.Invoice): Promise<void> {
invoice: Stripe.Invoice console.log('>> Create one-time payment record for invoice:', invoice.id);
): Promise<void> {
console.log('>> Handle one-time invoice payment:', invoice.id);
try { try {
const customerId = invoice.customer as string; const customerId = invoice.customer as string;
@ -620,67 +690,61 @@ export class StripeProvider implements PaymentProvider {
await this.stripe.paymentIntents.retrieve(paymentIntentId); await this.stripe.paymentIntents.retrieve(paymentIntentId);
const metadata = paymentIntent.metadata; const metadata = paymentIntent.metadata;
// Get userId from payment intent metadata // Get userId from payment intent metadata or fallback to customerId lookup
const userId = metadata?.userId; let userId: string | undefined = metadata?.userId;
if (!userId) { if (!userId) {
console.warn('<< No userId found in payment intent metadata'); console.log('No userId in metadata, finding by customerId');
return; userId = await this.findUserIdByCustomerId(customerId);
if (!userId) {
console.error('<< No userId found, this should not happen');
return;
}
} }
// Check if this is a credit purchase // Check if this is a credit purchase
const isCreditPurchase = metadata?.type === 'credit_purchase'; const isCreditPurchase = metadata?.type === 'credit_purchase';
if (isCreditPurchase) { if (isCreditPurchase) {
// Process credit purchase using metadata // Process credit purchase
await this.processCreditPurchaseFromMetadata(metadata, userId, invoice); await this.createCreditPurchasePayment(invoice, metadata, userId);
} else { } else {
// This is a lifetime plan purchase // Process lifetime plan purchase
const priceId = metadata?.priceId; await this.createLifetimePlanPayment(
if (!priceId) { invoice,
console.warn('<< No priceId found in payment intent metadata'); metadata,
return; userId,
} customerId
);
// Add lifetime credits if enabled
if (websiteConfig.credits?.enableCredits) {
await addLifetimeMonthlyCredits(userId, priceId);
console.log('Added lifetime credits for invoice:', invoice.id);
}
// Send notification
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
await sendNotification(invoice.id, customerId, userId, amount);
} }
console.log('<< Successfully processed one-time invoice payment'); console.log('<< Successfully created one-time payment record');
} catch (error) { } catch (error) {
console.error('<< Handle one-time invoice payment error:', error); console.error('<< Create one-time payment error:', error);
throw error; throw error;
} }
} }
/** /**
* Process credit purchase payment using payment intent metadata * Create payment record for credit purchase - NEW ARCHITECTURE
* @param invoice Stripe invoice
* @param metadata Payment intent metadata * @param metadata Payment intent metadata
* @param userId User ID * @param userId User ID
* @param invoice Stripe invoice
*/ */
private async processCreditPurchaseFromMetadata( private async createCreditPurchasePayment(
invoice: Stripe.Invoice,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
userId: string, userId: string
invoice: Stripe.Invoice
): Promise<void> { ): Promise<void> {
console.log('>> Process credit purchase payment from metadata'); console.log('>> Create credit purchase payment record');
try { try {
const packageId = metadata.packageId; const packageId = metadata.packageId;
const credits = metadata.credits; const credits = metadata.credits;
const paymentIntentId = invoice.payment_intent as string; const customerId = invoice.customer as string;
if (!packageId || !credits) { if (!packageId || !credits) {
console.warn( console.warn('<< Missing packageId or credits in metadata');
'<< Missing packageId or credits in payment intent metadata'
);
return; return;
} }
@ -691,19 +755,27 @@ export class StripeProvider implements PaymentProvider {
return; return;
} }
// Check if credits have already been added to prevent duplicates // Create payment record
const db = await getDb(); const db = await getDb();
const existingCreditTransaction = await db const currentDate = new Date();
.select({ id: creditTransaction.id }) const paymentResult = await db
.from(creditTransaction) .insert(payment)
.where(eq(creditTransaction.paymentId, paymentIntentId)) .values({
.limit(1); id: randomUUID(),
priceId: metadata.priceId || '',
type: PaymentTypes.ONE_TIME,
userId,
customerId,
invoiceId: invoice.id,
status: 'completed',
periodStart: currentDate,
createdAt: currentDate,
updatedAt: currentDate,
})
.returning({ id: payment.id });
if (existingCreditTransaction.length > 0) { if (paymentResult.length === 0) {
console.log( console.warn('<< Failed to create credit purchase payment record');
'<< Credits already added for payment intent:',
paymentIntentId
);
return; return;
} }
@ -714,98 +786,109 @@ export class StripeProvider implements PaymentProvider {
amount: Number.parseInt(credits), amount: Number.parseInt(credits),
type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE, type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE,
description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`, description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`,
paymentId: paymentIntentId, // Use payment intent ID as payment ID paymentId: invoice.id, // Use invoice ID as payment ID
expireDays: creditPackage.expireDays, expireDays: creditPackage.expireDays,
}); });
console.log('Added', credits, 'credits to user for confirmed payment'); console.log('<< Successfully added credits to user for credit purchase');
} catch (error) { } catch (error) {
console.error('<< Process credit purchase from metadata error:', error); console.error('<< Create credit purchase payment error:', error);
// Don't throw error if it's already processed
if (
error instanceof Error &&
error.message.includes('unique constraint')
) {
console.log('<< Credit purchase already processed:', invoice.id);
return;
}
throw error; throw error;
} }
} }
/** /**
* Create payment record * Create payment record for lifetime plan purchase - NEW ARCHITECTURE
* @param invoice Stripe invoice
* @param metadata Payment intent metadata
* @param userId User ID
* @param customerId Customer ID
*/
private async createLifetimePlanPayment(
invoice: Stripe.Invoice,
metadata: { [key: string]: string },
userId: string,
customerId: string
): Promise<void> {
console.log('>> Create lifetime plan payment record');
try {
const priceId = metadata?.priceId;
if (!priceId) {
console.warn('<< No priceId found in payment intent metadata');
return;
}
// Create payment record
const db = await getDb();
const currentDate = new Date();
const paymentResult = await db
.insert(payment)
.values({
id: randomUUID(),
priceId,
type: PaymentTypes.ONE_TIME,
userId,
customerId,
invoiceId: invoice.id,
status: 'completed',
periodStart: currentDate,
createdAt: currentDate,
updatedAt: currentDate,
})
.returning({ id: payment.id });
if (paymentResult.length === 0) {
console.warn('<< Failed to create lifetime plan payment record');
return;
}
// Add lifetime credits if enabled
if (websiteConfig.credits?.enableCredits) {
await addLifetimeMonthlyCredits(userId, priceId);
console.log('Added lifetime credits for invoice:', invoice.id);
}
// Send notification
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
await sendNotification(invoice.id, customerId, userId, amount);
console.log('<< Successfully created lifetime plan payment record');
} catch (error) {
console.error('<< Create lifetime plan payment error:', error);
// Don't throw error if it's already processed
if (
error instanceof Error &&
error.message.includes('unique constraint')
) {
console.log('<< Lifetime plan payment already processed:', invoice.id);
return;
}
throw error;
}
}
/**
* Handle subscription creation - NEW ARCHITECTURE
* Only log the event, payment records created in invoice.payment_succeeded
* @param stripeSubscription Stripe subscription * @param stripeSubscription Stripe subscription
*/ */
private async onCreateSubscription( private async onCreateSubscription(
stripeSubscription: Stripe.Subscription stripeSubscription: Stripe.Subscription
): Promise<void> { ): Promise<void> {
console.log('>> Create payment record for Stripe subscription'); console.log('Handle subscription creation:', stripeSubscription.id);
const customerId = stripeSubscription.customer as string;
// Check if subscription record already exists to prevent duplicates
const db = await getDb();
const existingPayment = await db
.select({ id: payment.id })
.from(payment)
.where(eq(payment.subscriptionId, stripeSubscription.id))
.limit(1);
if (existingPayment.length > 0) {
console.log(
'<< Subscription already has payment record:',
stripeSubscription.id
);
return;
}
// get priceId from subscription items (this is always available)
const priceId = stripeSubscription.items.data[0]?.price.id;
if (!priceId) {
console.warn('No priceId found for subscription');
return;
}
// get userId from metadata, we add it in the createCheckout session
const userId = stripeSubscription.metadata.userId;
if (!userId) {
console.warn('No userId found for subscription');
return;
}
const periodStart = this.getPeriodStart(stripeSubscription);
const periodEnd = this.getPeriodEnd(stripeSubscription);
// create fields
const createFields: any = {
id: randomUUID(),
priceId: priceId,
type: PaymentTypes.SUBSCRIPTION,
userId: userId,
customerId: customerId,
subscriptionId: stripeSubscription.id,
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
status: this.mapSubscriptionStatusToPaymentStatus(
stripeSubscription.status
),
periodStart: periodStart,
periodEnd: periodEnd,
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
trialStart: stripeSubscription.trial_start
? new Date(stripeSubscription.trial_start * 1000)
: null,
trialEnd: stripeSubscription.trial_end
? new Date(stripeSubscription.trial_end * 1000)
: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await db
.insert(payment)
.values(createFields)
.returning({ id: payment.id });
if (result.length > 0) {
console.log('<< Created new payment record for Stripe subscription');
} else {
console.warn('<< No payment record created for Stripe subscription');
}
// Note: Credits will be added when invoice.payment_succeeded event is received
// This ensures credits are only added for successful payments
} }
/** /**
@ -815,48 +898,24 @@ export class StripeProvider implements PaymentProvider {
private async onUpdateSubscription( private async onUpdateSubscription(
stripeSubscription: Stripe.Subscription stripeSubscription: Stripe.Subscription
): Promise<void> { ): Promise<void> {
console.log('>> Update payment record for Stripe subscription'); console.log('>> Handle subscription update');
// get priceId from subscription items (this is always available) // get priceId from subscription items (this is always available)
const priceId = stripeSubscription.items.data[0]?.price.id; const priceId = stripeSubscription.items.data[0]?.price.id;
if (!priceId) { if (!priceId) {
console.warn('No priceId found for subscription'); console.warn('<< No priceId found for subscription');
return; return;
} }
// Single query to check existence and get current payment data
const db = await getDb();
const existingPayments = await db
.select({
id: payment.id,
userId: payment.userId,
periodStart: payment.periodStart,
periodEnd: payment.periodEnd,
})
.from(payment)
.where(eq(payment.subscriptionId, stripeSubscription.id))
.limit(1);
if (existingPayments.length === 0) {
console.warn(
'<< No payment record found for subscription update:',
stripeSubscription.id
);
return;
}
const currentPayment = existingPayments[0];
// get new period start and end // get new period start and end
const newPeriodStart = this.getPeriodStart(stripeSubscription); const newPeriodStart = this.getPeriodStart(stripeSubscription);
const newPeriodEnd = this.getPeriodEnd(stripeSubscription); const newPeriodEnd = this.getPeriodEnd(stripeSubscription);
const trialStart = stripeSubscription.trial_start
// Check if this is a renewal (period has changed and subscription is active) ? new Date(stripeSubscription.trial_start * 1000)
const isRenewal = : undefined;
stripeSubscription.status === 'active' && const trialEnd = stripeSubscription.trial_end
currentPayment.periodStart && ? new Date(stripeSubscription.trial_end * 1000)
newPeriodStart && : undefined;
currentPayment.periodStart.getTime() !== newPeriodStart.getTime();
// update fields // update fields
const updateFields: any = { const updateFields: any = {
@ -868,15 +927,12 @@ export class StripeProvider implements PaymentProvider {
periodStart: newPeriodStart, periodStart: newPeriodStart,
periodEnd: newPeriodEnd, periodEnd: newPeriodEnd,
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end, cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
trialStart: stripeSubscription.trial_start trialStart: trialStart,
? new Date(stripeSubscription.trial_start * 1000) trialEnd: trialEnd,
: undefined,
trialEnd: stripeSubscription.trial_end
? new Date(stripeSubscription.trial_end * 1000)
: undefined,
updatedAt: new Date(), updatedAt: new Date(),
}; };
const db = await getDb();
const result = await db const result = await db
.update(payment) .update(payment)
.set(updateFields) .set(updateFields)
@ -884,12 +940,9 @@ export class StripeProvider implements PaymentProvider {
.returning({ id: payment.id }); .returning({ id: payment.id });
if (result.length > 0) { if (result.length > 0) {
console.log('<< Updated payment record for Stripe subscription'); console.log('<< Updated payment record for subscription');
// Note: Credits for subscription renewals will be added when invoice.payment_succeeded event is received
// This ensures credits are only added for successful payments, not just subscription updates
} else { } else {
console.warn('<< No payment record found for Stripe subscription'); console.warn('<< No payment record found for subscription update');
} }
} }
@ -900,25 +953,9 @@ export class StripeProvider implements PaymentProvider {
private async onDeleteSubscription( private async onDeleteSubscription(
stripeSubscription: Stripe.Subscription stripeSubscription: Stripe.Subscription
): Promise<void> { ): Promise<void> {
console.log('>> Mark payment record for Stripe subscription as canceled'); console.log('>> Handle subscription deletion');
const db = await getDb(); const db = await getDb();
// Check if subscription record exists first
const existingPayment = await db
.select({ id: payment.id })
.from(payment)
.where(eq(payment.subscriptionId, stripeSubscription.id))
.limit(1);
if (existingPayment.length === 0) {
console.warn(
'<< No payment record found for subscription deletion:',
stripeSubscription.id
);
return;
}
const result = await db const result = await db
.update(payment) .update(payment)
.set({ .set({
@ -933,162 +970,30 @@ export class StripeProvider implements PaymentProvider {
if (result.length > 0) { if (result.length > 0) {
console.log('<< Marked payment record for subscription as canceled'); console.log('<< Marked payment record for subscription as canceled');
} else { } else {
console.warn( console.warn('<< No payment record found for subscription deletion');
'<< No payment record found to cancel for Stripe subscription'
);
} }
} }
/** /**
* Handle one-time payment checkout completion * Handle checkout session completion - NEW ARCHITECTURE
* Note: This creates the initial payment record. Credits and final processing * Only log the event, payment records created in invoice.payment_succeeded
* are handled by invoice.payment_succeeded event for better reliability
* @param session Stripe checkout session * @param session Stripe checkout session
*/ */
private async onOnetimePayment( private async onOnetimePayment(
session: Stripe.Checkout.Session session: Stripe.Checkout.Session
): Promise<void> { ): Promise<void> {
const customerId = session.customer as string; console.log('Handle checkout session completion:', session.id);
console.log('>> Handle onetime payment checkout completion for customer');
// get userId from session metadata, we add it in the createCheckout session
const userId = session.metadata?.userId;
if (!userId) {
console.warn('No userId found for checkout session');
return;
}
// get priceId from session metadata, not from line items
const priceId = session.metadata?.priceId;
if (!priceId) {
console.warn('No priceId found for checkout session');
return;
}
try {
const db = await getDb();
// Check if this session has already been processed to prevent duplicate processing
const existingPayment = await db
.select({ id: payment.id })
.from(payment)
.where(eq(payment.sessionId, session.id))
.limit(1);
if (existingPayment.length > 0) {
console.log(
'One-time payment session already processed: ' + session.id
);
return;
}
// Create a one-time payment record
const now = new Date();
const result = await db
.insert(payment)
.values({
id: randomUUID(),
priceId: priceId,
type: PaymentTypes.ONE_TIME,
userId: userId,
customerId: customerId,
sessionId: session.id, // Track the session ID
status: 'completed', // One-time payments are considered completed when checkout succeeds
periodStart: now,
createdAt: now,
updatedAt: now,
})
.returning({ id: payment.id });
if (result.length === 0) {
console.warn('<< Failed to create one-time payment record for user');
return;
}
console.log(
'Created one-time payment record for checkout session:',
session.id
);
// Note: Credits and notifications will be handled by invoice.payment_succeeded event
// This ensures credits are only added when payment is actually confirmed successful
console.log(
'<< One-time payment checkout recorded, awaiting payment confirmation via invoice webhook'
);
} catch (error) {
console.error('onOnetimePayment error for session: ' + session.id, error);
throw error;
}
} }
/** /**
* Handle credit purchase checkout completion * Handle credit purchase checkout completion - NEW ARCHITECTURE
* Note: This creates the initial payment record. Credits are processed * Only log the event, payment records created in invoice.payment_succeeded
* when invoice.payment_succeeded event is received for payment confirmation.
* @param session Stripe checkout session * @param session Stripe checkout session
*/ */
private async onCreditPurchase( private async onCreditPurchase(
session: Stripe.Checkout.Session session: Stripe.Checkout.Session
): Promise<void> { ): Promise<void> {
const customerId = session.customer as string; console.log('Handle credit purchase checkout completion:', session.id);
console.log('>> Handle credit purchase checkout completion for customer');
// get userId from session metadata, we add it in the createCheckout session
const userId = session.metadata?.userId;
if (!userId) {
console.warn('No userId found for checkout session');
return;
}
// get packageId from session metadata
const packageId = session.metadata?.packageId;
if (!packageId) {
console.warn('No packageId found for checkout session');
return;
}
try {
// Check if this session has already been processed to prevent duplicate processing
const db = await getDb();
const existingPayment = await db
.select({ id: payment.id })
.from(payment)
.where(eq(payment.sessionId, session.id))
.limit(1);
if (existingPayment.length > 0) {
console.log('Credit purchase session already processed: ' + session.id);
return;
}
// Create payment record first to mark this session as processed
const now = new Date();
await db.insert(payment).values({
id: randomUUID(),
priceId: session.metadata?.priceId || '',
type: PaymentTypes.ONE_TIME,
userId: userId,
customerId: customerId,
sessionId: session.id, // Use sessionId to track processed sessions
status: 'completed', // Checkout completed, but waiting for payment confirmation
periodStart: now,
createdAt: now,
updatedAt: now,
});
console.log(
'Created credit purchase payment record for checkout session:',
session.id
);
// Note: Credits will be added when invoice.payment_succeeded event is received
// This ensures credits are only added when payment is actually confirmed successful
console.log(
'<< Credit purchase checkout recorded, awaiting payment confirmation via invoice webhook'
);
} catch (error) {
console.error('onCreditPurchase error for session: ' + session.id, error);
throw error;
}
} }
/** /**