Merge pull request #33708 from overleaf/jlm-stripe-stage5-fixes
Stripe migration improvements from stage 5 GitOrigin-RevId: 897941bf1a51f8f1639489fcd3f542af671b7ac7
This commit is contained in:
@@ -1561,7 +1561,31 @@ export async function compareAccountFields({
|
||||
const paymentMethodIsMissing =
|
||||
(!stripeDefaultPaymentMethod && expectedPaymentMethod) ||
|
||||
(stripeDefaultPaymentMethod && !expectedPaymentMethod)
|
||||
if (
|
||||
|
||||
const bothArePaypal =
|
||||
expectedPaymentMethod?.type === 'paypal' &&
|
||||
stripeDefaultPaymentMethod?.type === 'paypal'
|
||||
|
||||
if (bothArePaypal) {
|
||||
// Both are PayPal — the payment method itself matches, but flag if
|
||||
// Recurly billing info was updated after the Stripe payment method was
|
||||
// created.
|
||||
if (recurlyPaymentMethodIsNewer) {
|
||||
diffs.default_payment_method = {
|
||||
recurly: {
|
||||
type: 'paypal',
|
||||
updatedAt: account.billingInfo?.updatedAt?.toISOString() || null,
|
||||
},
|
||||
stripe: {
|
||||
type: 'paypal',
|
||||
created: stripeDefaultPaymentMethod?.created
|
||||
? new Date(stripeDefaultPaymentMethod.created * 1000).toISOString()
|
||||
: null,
|
||||
},
|
||||
bothPaypal: true,
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
paymentMethodIsMissing ||
|
||||
(!areStripeAndRecurlyCardDetailsEqual(
|
||||
stripeDefaultPaymentMethod,
|
||||
|
||||
+93
@@ -1053,3 +1053,96 @@ test('compareAccountFields detects tax_exempt drift', async () => {
|
||||
assert.equal(diffs.tax_exempt.recurly, 'exempt')
|
||||
assert.equal(diffs.tax_exempt.stripe, 'none')
|
||||
})
|
||||
|
||||
test('compareAccountFields reports bothPaypal diff when Recurly billingInfo.updatedAt is newer', async () => {
|
||||
const recurlyUpdatedAt = new Date('2025-06-01T12:00:00Z')
|
||||
const stripeCreatedEpoch = new Date('2025-05-01T12:00:00Z').getTime() / 1000
|
||||
|
||||
const account = {
|
||||
email: 'user@example.com',
|
||||
customFields: [],
|
||||
billingInfo: {
|
||||
updatedAt: recurlyUpdatedAt,
|
||||
paymentMethod: { object: 'paypal_billing_agreement' },
|
||||
},
|
||||
address: { country: 'GB' },
|
||||
}
|
||||
const stripeCustomer = {
|
||||
email: 'user@example.com',
|
||||
metadata: {
|
||||
recurlyAccountCode: '',
|
||||
userId: 'user123',
|
||||
taxInfoPending: '',
|
||||
},
|
||||
tax_exempt: 'none',
|
||||
invoice_settings: {
|
||||
default_payment_method: {
|
||||
type: 'paypal',
|
||||
created: stripeCreatedEpoch,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const diffs = await compareAccountFields({
|
||||
account,
|
||||
stripeCustomer,
|
||||
overleafUserId: 'user123',
|
||||
fetchCollectionMethod: async () => null,
|
||||
stripePaymentMethods: [],
|
||||
stripeServiceName: 'stripe-uk',
|
||||
})
|
||||
|
||||
assert.ok(diffs.default_payment_method)
|
||||
assert.equal(diffs.default_payment_method.bothPaypal, true)
|
||||
assert.equal(diffs.default_payment_method.recurly.type, 'paypal')
|
||||
assert.equal(
|
||||
diffs.default_payment_method.recurly.updatedAt,
|
||||
recurlyUpdatedAt.toISOString()
|
||||
)
|
||||
assert.equal(diffs.default_payment_method.stripe.type, 'paypal')
|
||||
assert.equal(
|
||||
diffs.default_payment_method.stripe.created,
|
||||
new Date(stripeCreatedEpoch * 1000).toISOString()
|
||||
)
|
||||
})
|
||||
|
||||
test('compareAccountFields reports no diff when both are PayPal and Recurly is not newer', async () => {
|
||||
const recurlyUpdatedAt = new Date('2025-04-01T12:00:00Z')
|
||||
const stripeCreatedEpoch = new Date('2025-05-01T12:00:00Z').getTime() / 1000
|
||||
|
||||
const account = {
|
||||
email: 'user@example.com',
|
||||
customFields: [],
|
||||
billingInfo: {
|
||||
updatedAt: recurlyUpdatedAt,
|
||||
paymentMethod: { object: 'paypal_billing_agreement' },
|
||||
},
|
||||
address: { country: 'GB' },
|
||||
}
|
||||
const stripeCustomer = {
|
||||
email: 'user@example.com',
|
||||
metadata: {
|
||||
recurlyAccountCode: '',
|
||||
userId: 'user123',
|
||||
taxInfoPending: '',
|
||||
},
|
||||
tax_exempt: 'none',
|
||||
invoice_settings: {
|
||||
default_payment_method: {
|
||||
type: 'paypal',
|
||||
created: stripeCreatedEpoch,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const diffs = await compareAccountFields({
|
||||
account,
|
||||
stripeCustomer,
|
||||
overleafUserId: 'user123',
|
||||
fetchCollectionMethod: async () => null,
|
||||
stripePaymentMethods: [],
|
||||
stripeServiceName: 'stripe-uk',
|
||||
})
|
||||
|
||||
assert.equal(diffs.default_payment_method, undefined)
|
||||
})
|
||||
|
||||
@@ -57,7 +57,6 @@ import PlansLocator from '../../app/src/Features/Subscription/PlansLocator.mjs'
|
||||
import UserAnalyticsIdCache from '../../app/src/Features/Analytics/UserAnalyticsIdCache.mjs'
|
||||
import CustomerIoHandler from '../../modules/customer-io/app/src/CustomerIoHandler.mjs'
|
||||
import { ReportError, convertToMinorUnits } from './helpers.mjs'
|
||||
import isEqual from 'lodash/isEqual.js'
|
||||
import { compareAccountFields } from '../helpers/migrate_recurly_customers_to_stripe.helpers.mjs'
|
||||
import {
|
||||
createRateLimitedApiWrappers,
|
||||
@@ -71,6 +70,9 @@ import {
|
||||
|
||||
const preloadedProductMetadata = new Map()
|
||||
|
||||
// Tolerance for comparing amounts (handles repeating decimals in per-seat prices)
|
||||
const AMOUNT_TOLERANCE = 1e-6
|
||||
|
||||
// rate limiters - initialized in main()
|
||||
let rateLimiters
|
||||
|
||||
@@ -556,13 +558,26 @@ function detectSubscriptionChanges(
|
||||
? productMetadata?.addOnCode
|
||||
: productMetadata?.planCode,
|
||||
quantity: item.quantity,
|
||||
amount: item.price.unit_amount,
|
||||
amount:
|
||||
item.price.unit_amount != null
|
||||
? item.price.unit_amount
|
||||
: parseFloat(item.price.unit_amount_decimal),
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.code.localeCompare(b.code))
|
||||
|
||||
// Compare items
|
||||
if (!isEqual(recurlyItems, stripeItems)) {
|
||||
// Compare items (use tolerance for amounts due to repeating decimals in per-seat prices)
|
||||
const itemsMatch =
|
||||
recurlyItems.length === stripeItems.length &&
|
||||
recurlyItems.every((rItem, i) => {
|
||||
const sItem = stripeItems[i]
|
||||
return (
|
||||
rItem.code === sItem.code &&
|
||||
rItem.quantity === sItem.quantity &&
|
||||
Math.abs(rItem.amount - sItem.amount) < AMOUNT_TOLERANCE
|
||||
)
|
||||
})
|
||||
if (!itemsMatch) {
|
||||
changes.push(
|
||||
`Items: Recurly=[${formatItems(recurlyItems)}], Stripe=[${formatItems(stripeItems)}]`
|
||||
)
|
||||
@@ -662,9 +677,15 @@ function formatDiffsAsChanges(diffs) {
|
||||
`Tax ID: Recurly={type:${diff.recurly.type}, value:${diff.recurly.value}}, Stripe=${stripeStr}`
|
||||
)
|
||||
} else if (field === 'default_payment_method') {
|
||||
changes.push(
|
||||
`Payment method: Recurly=${diff.recurly.type || diff.recurly.last4 || '(none)'}, Stripe=${diff.stripe.type || '(none)'}`
|
||||
)
|
||||
if (diff.bothPaypal) {
|
||||
changes.push(
|
||||
`Payment method: both PayPal, but Recurly billing info updatedAt (${diff.recurly.updatedAt}) is newer than Stripe payment method created (${diff.stripe.created})`
|
||||
)
|
||||
} else {
|
||||
changes.push(
|
||||
`Payment method: Recurly=${diff.recurly.type || diff.recurly.last4 || '(none)'}, Stripe=${diff.stripe.type || '(none)'}`
|
||||
)
|
||||
}
|
||||
} else if (field.startsWith('metadata.')) {
|
||||
const key = field.slice('metadata.'.length)
|
||||
changes.push(
|
||||
|
||||
Reference in New Issue
Block a user