[web] handle duplicate customer accounts better (#31677)
* [web] extract card comparison function * [web] throw if more than 1 duplicate customer account * [web] attempt to pick the pre-existing account * [web] overwrite email of duplicate account * Update services/web/scripts/helpers/migrate_recurly_customers_to_stripe.helpers.mjs * [web] handle setting being undefined * [web] allow other account to be reused if migrating account is using paypal * [web] allow matching for customers with no payment method GitOrigin-RevId: 7d8eeb4734a20c09ce3e0d73095e28696e212549
This commit is contained in:
@@ -947,11 +947,8 @@ export function coalesceOrThrowPaymentMethod(
|
||||
)
|
||||
}
|
||||
|
||||
const matchingPaymentMethods = paymentMethods.filter(
|
||||
method =>
|
||||
method.card?.last4 === billingInfo?.paymentMethod?.lastFour &&
|
||||
method.card?.exp_month === billingInfo?.paymentMethod?.expMonth &&
|
||||
method.card?.exp_year === billingInfo?.paymentMethod?.expYear
|
||||
const matchingPaymentMethods = paymentMethods.filter(method =>
|
||||
areStripeAndRecurlyCardDetailsEqual(method, billingInfo?.paymentMethod)
|
||||
)
|
||||
|
||||
if (matchingPaymentMethods.length === 0) {
|
||||
@@ -962,3 +959,31 @@ export function coalesceOrThrowPaymentMethod(
|
||||
|
||||
return matchingPaymentMethods[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare Stripe card details with Recurly card details to determine if they likely represent the same card.
|
||||
*
|
||||
* Checks last4 and expiry month/year.
|
||||
*
|
||||
* @param {Stripe.PaymentMethod} stripePaymentMethod
|
||||
* @param {object} recurlyPaymentMethod - Recurly billingInfo.paymentMethod object
|
||||
* @returns {boolean} true if details match, false otherwise
|
||||
*/
|
||||
export function areStripeAndRecurlyCardDetailsEqual(
|
||||
stripePaymentMethod,
|
||||
recurlyPaymentMethod
|
||||
) {
|
||||
if (
|
||||
stripePaymentMethod.type !== 'card' ||
|
||||
!stripePaymentMethod?.card ||
|
||||
!recurlyPaymentMethod?.lastFour
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
stripePaymentMethod.card.last4 === recurlyPaymentMethod.lastFour &&
|
||||
stripePaymentMethod.card.exp_month === recurlyPaymentMethod.expMonth &&
|
||||
stripePaymentMethod.card.exp_year === recurlyPaymentMethod.expYear
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ import * as csv from 'csv'
|
||||
import { scriptRunner } from '../lib/ScriptRunner.mjs'
|
||||
|
||||
import {
|
||||
areStripeAndRecurlyCardDetailsEqual,
|
||||
coalesceOrEqualOrThrowAddress,
|
||||
coalesceOrEqualOrThrowName,
|
||||
coalesceOrThrowPaymentMethod,
|
||||
@@ -602,8 +603,50 @@ async function fetchOtherStripeCustomerByUserId(
|
||||
{ ...context, stripeApi: 'customers.search' }
|
||||
)
|
||||
|
||||
return (
|
||||
results.data?.find(customer => customer.id !== stripeCustomerId) || null
|
||||
const matchingCustomers = results.data?.filter(
|
||||
customer => customer.id !== stripeCustomerId
|
||||
)
|
||||
|
||||
if (matchingCustomers.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple Stripe customers found with userId metadata "${userId}": ${matchingCustomers.map(c => c.id).join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return matchingCustomers[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a Stripe customer as a duplicate of another customer.
|
||||
*
|
||||
* @param {Stripe} stripeClient
|
||||
* @param {string} stripeCustomerId
|
||||
* @param {string} recurlyAccountCode
|
||||
* @param {object} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function markCustomerAsDuplicate(
|
||||
stripeClient,
|
||||
stripeCustomerId,
|
||||
recurlyAccountCode,
|
||||
context
|
||||
) {
|
||||
const email =
|
||||
Settings.duplicateStripeCustomerAccountEmail?.replace(
|
||||
'@',
|
||||
`+${stripeCustomerId}@`
|
||||
) || ''
|
||||
await rateLimiters.requestWithRetries(
|
||||
stripeClient.serviceName,
|
||||
() =>
|
||||
stripeClient.customers.update(stripeCustomerId, {
|
||||
email,
|
||||
metadata: {
|
||||
userId: '',
|
||||
duplicateUserId: recurlyAccountCode,
|
||||
},
|
||||
}),
|
||||
{ ...context, stripeApi: 'customers.update' }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -937,9 +980,68 @@ async function resolveStripeCustomer({
|
||||
|
||||
if (otherMatchingCustomer) {
|
||||
if (stripeCustomerId) {
|
||||
throw new Error(
|
||||
`Found another Stripe customer with matching userId metadata: ${otherMatchingCustomer.id}`
|
||||
const otherCustomerPaymentMethods =
|
||||
await fetchTargetStripeCustomerPaymentMethods(
|
||||
stripeClient,
|
||||
otherMatchingCustomer.id,
|
||||
stripeContext
|
||||
)
|
||||
const isRecurlyPaymentMethodPaypal =
|
||||
account?.billingInfo?.paymentMethod?.object ===
|
||||
'paypal_billing_agreement'
|
||||
const isRecurlyPaymentMethodManual = !account?.billingInfo?.paymentMethod // billing info may be missing for manually billed customers
|
||||
const hasMatchingPaymentMethod = otherCustomerPaymentMethods.some(
|
||||
method =>
|
||||
areStripeAndRecurlyCardDetailsEqual(
|
||||
method,
|
||||
account?.billingInfo?.paymentMethod
|
||||
)
|
||||
)
|
||||
if (
|
||||
isRecurlyPaymentMethodPaypal ||
|
||||
isRecurlyPaymentMethodManual ||
|
||||
hasMatchingPaymentMethod
|
||||
) {
|
||||
logDebug(
|
||||
'Found another Stripe customer with matching userId metadata, reusing',
|
||||
{
|
||||
...context,
|
||||
nextStripeCustomerId: otherMatchingCustomer.id,
|
||||
},
|
||||
{ verboseOnly: true }
|
||||
)
|
||||
if (commit) {
|
||||
await markCustomerAsDuplicate(
|
||||
stripeClient,
|
||||
stripeCustomerId,
|
||||
recurlyAccountCode,
|
||||
stripeContext
|
||||
)
|
||||
logDebug(
|
||||
'Marked CSV customer as a duplicate of the existing Stripe customer',
|
||||
{
|
||||
...context,
|
||||
nextStripeCustomerId: otherMatchingCustomer.id,
|
||||
},
|
||||
{ verboseOnly: true }
|
||||
)
|
||||
} else {
|
||||
logDebug(
|
||||
'DRY RUN: Would mark CSV customer as a duplicate of the existing Stripe customer',
|
||||
{
|
||||
...context,
|
||||
nextStripeCustomerId: otherMatchingCustomer.id,
|
||||
step: 'mark_duplicate',
|
||||
},
|
||||
{ verboseOnly: true }
|
||||
)
|
||||
}
|
||||
return otherMatchingCustomer
|
||||
} else {
|
||||
throw new Error(
|
||||
`Found another Stripe customer with matching userId metadata but no matching payment method: ${otherMatchingCustomer.id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logDebug(
|
||||
|
||||
Reference in New Issue
Block a user