fix: prevent duplicate Dodo customers and handle multiple subscriptions per email

Comprehensive fixes to prevent customer ID mismatches:

1. **Checkout creation now passes customer_id** (lines 13669, 13064, 13503):
   - All checkouts (subscription, topup, PAYG) now call ensureDodoCustomer() first
   - Pass existing customer_id to checkout body to prevent Dodo from creating duplicates
   - Added customerId to metadata for tracking

2. **Subscription confirmation validates customer consistency** (line 13843):
   - Logs warning when checkout returns different customer_id than stored
   - Tracks which customer_id was used in checkout metadata
   - Prevents silent customer ID overwrites

3. **ensureDodoCustomer handles multiple customers per email** (line 6774):
   - Logs warning when multiple customers found for same email
   - Checks ALL customers for active subscriptions
   - Selects customer with active subscriptions if multiple exist
   - Returns first customer only if no active subscriptions found

4. **Added missing return statements** (lines 12417, 12448):
   - Prevents double response errors after successful plan changes

This ensures that:
- New subscriptions use existing customers instead of creating duplicates
- Plan changes work correctly even with multiple subscriptions
- Customer ID mismatches are detected and logged
- The correct customer (one with active subscriptions) is always used
This commit is contained in:
southseact-3d
2026-02-12 12:06:49 +00:00
parent d61fa3d621
commit 49747d08db

View File

@@ -6787,21 +6787,62 @@ async function ensureDodoCustomer(user) {
// Check if any customers were returned
if (existingCustomers?.items && existingCustomers.items.length > 0) {
const existingCustomer = existingCustomers.items[0];
const customerId = existingCustomer?.customer_id || existingCustomer?.id || '';
// Log warning if multiple customers found for same email
if (existingCustomers.items.length > 1) {
log('WARNING: Multiple Dodo customers found for same email', {
userId: user.id,
email: email,
customerCount: existingCustomers.items.length,
customerIds: existingCustomers.items.map(c => c.customer_id || c.id)
});
}
// Find the customer with active subscriptions, or use the first one
let selectedCustomer = existingCustomers.items[0];
// If multiple customers, try to find one with active subscriptions
if (existingCustomers.items.length > 1) {
for (const customer of existingCustomers.items) {
const customerId = customer?.customer_id || customer?.id;
if (customerId) {
try {
const subscriptions = await dodoRequest('/subscriptions', {
method: 'GET',
query: { customer_id: customerId, status: 'active' }
});
if (subscriptions?.items && subscriptions.items.length > 0) {
selectedCustomer = customer;
log('Selected customer with active subscriptions', {
userId: user.id,
email: email,
customerId: customerId,
activeSubscriptions: subscriptions.items.length
});
break;
}
} catch (err) {
// Continue to next customer
}
}
}
}
const customerId = selectedCustomer?.customer_id || selectedCustomer?.id || '';
log('DEBUG: Found customer in items', {
userId: user.id,
customerId: customerId,
customerKeys: Object.keys(existingCustomer || {}),
rawCustomer: JSON.stringify(existingCustomer).substring(0, 500)
totalCustomers: existingCustomers.items.length,
customerKeys: Object.keys(selectedCustomer || {})
});
if (customerId) {
user.dodoCustomerId = customerId;
await persistUsersDb();
log('Found existing Dodo customer by email', {
userId: user.id,
email: email,
customerId: customerId
customerId: customerId,
totalFound: existingCustomers.items.length
});
return customerId;
}
@@ -13004,6 +13045,14 @@ async function handleTopupCheckout(req, res) {
const unitAmount = applyTopupDiscount(baseAmount, discount);
const returnUrl = `${resolveBaseUrl(req)}/topup`;
const orderId = `topup_${randomUUID()}`;
// Ensure we have a Dodo customer ID before creating checkout
const customerId = await ensureDodoCustomer(user);
if (!customerId) {
log('Failed to get or create Dodo customer for topup', { userId: user.id, email: user.email });
return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
}
const checkoutBody = {
product_cart: [{
product_id: pack.productId,
@@ -13011,12 +13060,14 @@ async function handleTopupCheckout(req, res) {
amount: unitAmount,
}],
customer: {
customer_id: customerId, // Pass existing customer ID to prevent duplicates
email: user.billingEmail || user.email,
name: user.billingEmail || user.email,
},
metadata: {
type: 'topup',
orderId,
customerId,
userId: String(user.id),
tokens: String(pack.tokens),
tier: String(pack.tier),
@@ -13436,10 +13487,17 @@ async function handlePaygCheckout(req, res) {
}
const amount = Math.max(MIN_PAYMENT_AMOUNT, Math.ceil((payg.billableTokens * payg.pricePerUnit) / PAYG_UNIT_TOKENS));
const returnUrl = `${resolveBaseUrl(req)}/settings`;
const orderId = `payg_${randomUUID()}`;
const returnUrl = `${resolveBaseUrl(req)}/settings`;
const orderId = `payg_${randomUUID()}`;
try {
// Ensure we have a Dodo customer ID before creating checkout
const customerId = await ensureDodoCustomer(user);
if (!customerId) {
log('Failed to get or create Dodo customer for PAYG', { userId: user.id, email: user.email });
return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
}
try {
const checkoutSession = await dodoRequest('/checkouts', {
method: 'POST',
body: {
@@ -13449,6 +13507,7 @@ async function handlePaygCheckout(req, res) {
amount,
}],
customer: {
customer_id: customerId, // Pass existing customer ID to prevent duplicates
email: user.billingEmail || user.email,
name: user.billingEmail || user.email,
},
@@ -13462,6 +13521,7 @@ async function handlePaygCheckout(req, res) {
currency: String(currency),
amount: String(amount),
month: String(payg.month),
customerId,
},
},
});
@@ -13658,6 +13718,15 @@ async function handleSubscriptionCheckout(req, res) {
// Create a unique session ID for this checkout
const checkoutSessionId = randomUUID();
// Ensure we have a Dodo customer ID before creating checkout to prevent duplicate customers
const customerId = await ensureDodoCustomer(user);
if (!customerId) {
log('Failed to get or create Dodo customer for checkout', { userId: user.id, email: user.email });
return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
}
log('Using existing Dodo customer for checkout', { userId: user.id, customerId, email: user.email });
// Create checkout session with enhanced metadata for session tracking
const returnUrl = `${resolveBaseUrl(req)}/apps`;
@@ -13667,6 +13736,7 @@ async function handleSubscriptionCheckout(req, res) {
quantity: 1,
}],
customer: {
customer_id: customerId, // Pass existing customer ID to prevent duplicates
email: user.billingEmail || user.email,
name: user.billingEmail || user.email,
},
@@ -13680,6 +13750,7 @@ async function handleSubscriptionCheckout(req, res) {
currency: String(currency),
amount: String(product.price),
inline: String(isInline),
customerId: customerId, // Track which customer ID was used
},
settings: {
allow_payment_methods: ['card'],
@@ -13829,7 +13900,26 @@ async function handleSubscriptionConfirm(req, res, url) {
user.billingCycle = pending.billingCycle;
user.subscriptionCurrency = pending.currency;
user.subscriptionRenewsAt = computeRenewalDate(pending.billingCycle);
user.dodoCustomerId = checkout?.customer_id || user.dodoCustomerId;
// Validate customer ID consistency to prevent duplicate customer issues
const checkoutCustomerId = checkout?.customer_id;
const storedCustomerId = user.dodoCustomerId;
if (checkoutCustomerId && storedCustomerId && checkoutCustomerId !== storedCustomerId) {
log('WARNING: Checkout returned different customer_id than stored', {
userId: user.id,
email: user.email,
storedCustomerId: storedCustomerId,
checkoutCustomerId: checkoutCustomerId,
subscriptionId: checkout?.subscription_id,
pendingCustomerId: pending.metadata?.customerId
});
// Check if we should use the new customer ID (if it has subscriptions)
user.dodoCustomerId = checkoutCustomerId;
} else if (checkoutCustomerId) {
user.dodoCustomerId = checkoutCustomerId;
}
if (checkout?.subscription_id) {
user.dodoSubscriptionId = checkout.subscription_id;