mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Further improvements to Recurly -> Stripe Customer Upsert script
* handle billingInfo / account discrepancies * fix rate limiting behaviour of fetchRecurlyActiveSubscription * make force-invalid-tax aware of stripe tax validation errors * clear taxInfoPending if vatNumber is now valid or not present on second runs * re-instate pre-validation of tax ids in dry-run mode * handle tax exemption * fix tax pre-validation tests * fix comment typos * remove redundant hadConflict * re-use normalizeName function * throw if more than 1 active recurly subscription when resolving conflict --------- Co-authored-by: Simon Gardner <simon.gardner@overleaf.com> GitOrigin-RevId: a88cad87887bd9e043fb457091e999c1d555f546
This commit is contained in:
committed by
Copybot
co-authored by
Simon Gardner
parent
029c73adce
commit
89e8044a25
@@ -21,7 +21,7 @@ export function coalesceOrEqualOrThrow(a, b, fieldName) {
|
||||
return isSetA ? a : b
|
||||
}
|
||||
|
||||
function normalizeName(firstName, lastName) {
|
||||
export function normalizeName(firstName, lastName) {
|
||||
const first = (firstName || '').trim()
|
||||
const last = (lastName || '').trim()
|
||||
const full = `${first} ${last}`.trim()
|
||||
@@ -66,6 +66,34 @@ export function coalesceOrEqualOrThrowName(account) {
|
||||
return billingName ?? accountName
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract customer name from billing info only (no fallback to account).
|
||||
*
|
||||
* @param {object} account - Recurly account object
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function extractNameFromBillingInfo(account) {
|
||||
const billingHasFullName = !!(
|
||||
account.billingInfo?.firstName && account.billingInfo?.lastName
|
||||
)
|
||||
return billingHasFullName
|
||||
? normalizeName(account.billingInfo.firstName, account.billingInfo.lastName)
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract customer name from account level only (no fallback to billing info).
|
||||
*
|
||||
* @param {object} account - Recurly account object
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function extractNameFromAccount(account) {
|
||||
const accountHasFullName = !!(account.firstName && account.lastName)
|
||||
return accountHasFullName
|
||||
? normalizeName(account.firstName, account.lastName)
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and coalesce VAT number from Recurly data.
|
||||
*
|
||||
@@ -197,6 +225,11 @@ function normalizeTaxIdCompact(value) {
|
||||
return normalizeTaxId(value).replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function normalizeTaxIdAlnum(value) {
|
||||
// Keep alphanumerics and '&' (used by some MX RFC values), strip separators.
|
||||
return normalizeTaxId(value).replace(/[^A-Z0-9&]+/g, '')
|
||||
}
|
||||
|
||||
function digitsOnly(value) {
|
||||
return normalizeTaxId(value).replace(/\D+/g, '')
|
||||
}
|
||||
@@ -878,11 +911,17 @@ const COUNTRY_TAX_ID_FORMATS = {
|
||||
* @param {string|undefined|null} country - ISO 3166-1 alpha-2
|
||||
* @param {string|undefined|null} taxIdValue
|
||||
* @param {string|undefined|null} postalCode - used for Canada province inference
|
||||
* @param {boolean} [preValidateFormat=false] - Optional behavior flag to pre-validate format.
|
||||
* @returns {{type: string|null, reason: string|null}} - Stripe tax ID type + failure reason (when type is null)
|
||||
*/
|
||||
// TODO: this function is naive - we need more than just country to determine tax ID type
|
||||
// for example canada has multiple types (BN, QST, GST/HST) depending on province (inferred from postal code)
|
||||
export function getTaxIdType(country, taxIdValue, postalCode) {
|
||||
export function getTaxIdType(
|
||||
country,
|
||||
taxIdValue,
|
||||
postalCode,
|
||||
preValidateFormat = false
|
||||
) {
|
||||
if (!country) return { type: null, reason: 'missing country' }
|
||||
|
||||
const upperCountry = String(country).toUpperCase()
|
||||
@@ -923,6 +962,12 @@ export function getTaxIdType(country, taxIdValue, postalCode) {
|
||||
'SK',
|
||||
])
|
||||
if (euVatOnlyCountries.has(upperCountry)) {
|
||||
if (preValidateFormat && !hasEuVatPrefix(upperCountry, taxIdValue)) {
|
||||
return {
|
||||
type: null,
|
||||
reason: `invalid tax ID format for country ${upperCountry}`,
|
||||
}
|
||||
}
|
||||
return { type: 'eu_vat', reason: null }
|
||||
}
|
||||
}
|
||||
@@ -1051,6 +1096,15 @@ export function getTaxIdType(country, taxIdValue, postalCode) {
|
||||
|
||||
const countryFormat = COUNTRY_TAX_ID_FORMATS[upperCountry]
|
||||
if (countryFormat) {
|
||||
if (preValidateFormat) {
|
||||
const normalized = normalizeTaxIdAlnum(taxIdValue)
|
||||
if (!countryFormat.regex.test(normalized)) {
|
||||
return {
|
||||
type: null,
|
||||
reason: `invalid tax ID format for country ${upperCountry}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: countryFormat.taxIdType, reason: null }
|
||||
}
|
||||
|
||||
|
||||
+27
@@ -380,6 +380,33 @@ test('getTaxIdType includes failure reason', () => {
|
||||
assert.equal(invalidEuOss.reason, 'invalid EU OSS VAT number')
|
||||
})
|
||||
|
||||
test('getTaxIdType pre-validation is opt-in', () => {
|
||||
// Default behavior: no country-format pre-validation
|
||||
const noPreValidation = getTaxIdType('US', 'NOT_A_VALID_EIN', null)
|
||||
assert.equal(noPreValidation.type, 'us_ein')
|
||||
assert.equal(noPreValidation.reason, null)
|
||||
|
||||
// Optional behavior: enable country-format pre-validation
|
||||
const withPreValidation = getTaxIdType('US', 'NOT_A_VALID_EIN', null, true)
|
||||
assert.equal(withPreValidation.type, null)
|
||||
assert.equal(withPreValidation.reason, 'invalid tax ID format for country US')
|
||||
})
|
||||
|
||||
test('getTaxIdType pre-validates EU VAT-only countries when enabled', () => {
|
||||
const invalidWithPreValidation = getTaxIdType('AT', '12345678', null, true)
|
||||
|
||||
assert.equal(invalidWithPreValidation.type, null)
|
||||
assert.equal(
|
||||
invalidWithPreValidation.reason,
|
||||
'invalid tax ID format for country AT'
|
||||
)
|
||||
|
||||
const validWithPreValidation = getTaxIdType('AT', 'ATU12345678', null, true)
|
||||
|
||||
assert.equal(validWithPreValidation.type, 'eu_vat')
|
||||
assert.equal(validWithPreValidation.reason, null)
|
||||
})
|
||||
|
||||
test('coalesceOrThrowPaymentMethod throws when payment methods array is empty', () => {
|
||||
assert.throws(
|
||||
() => coalesceOrThrowPaymentMethod([], 'cus_123', {}),
|
||||
|
||||
Reference in New Issue
Block a user