| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055 |
- /* eslint-disable @overleaf/require-script-runner */
- import test from 'node:test'
- import assert from 'node:assert/strict'
- /*
- * This test can be run from the services/web directory with:
- *
- * node --test scripts/helpers/migrate_recurly_customers_to_stripe.helpers.node.test.mjs
- *
- * It can be deleted once the Recurly to Stripe migration is complete.
- */
- import {
- coalesceOrEqualOrThrow,
- coalesceOrThrowVATNumber,
- extractNameFromAccount,
- getCanadaTaxIdType,
- getAustraliaTaxIdType,
- getBrazilTaxIdType,
- getBulgariaTaxIdType,
- getCroatiaTaxIdType,
- getGermanyTaxIdType,
- getHungaryTaxIdType,
- getJapanTaxIdType,
- getLiechtensteinTaxIdType,
- getMalaysiaTaxIdType,
- getNorwayTaxIdType,
- getPolandTaxIdType,
- getRomaniaTaxIdType,
- getRussiaTaxIdType,
- getSingaporeTaxIdType,
- getSloveniaTaxIdType,
- getSpainTaxIdType,
- getSwitzerlandTaxIdType,
- getUkTaxIdType,
- getUzbekistanTaxIdType,
- getTaxIdType,
- coalesceOrThrowPaymentMethod,
- normalisedGBVATNumber,
- normalizeComparableString,
- hasAnyAddressValue,
- ccEmailsToArray,
- normalizeTaxIdValue,
- extractRecurlyCustomFieldMetadata,
- addressesEqual,
- resolveCustomerIdentity,
- compareAccountFields,
- areStripeAndRecurlyCardDetailsEqual,
- } from './migrate_recurly_customers_to_stripe.helpers.mjs'
- test('extractNameFromAccount returns normalized full name when first and last are present', () => {
- const account = {
- firstName: ' Alice ',
- lastName: ' Example ',
- }
- assert.equal(extractNameFromAccount(account), 'Alice Example')
- })
- test('extractNameFromAccount falls back to firstName when lastName is missing', () => {
- const account = {
- firstName: 'Alice Example',
- lastName: '',
- }
- assert.equal(extractNameFromAccount(account), 'Alice Example')
- })
- test('coalesceOrEqualOrThrow returns primary when set', () => {
- assert.equal(coalesceOrEqualOrThrow('a', undefined, 'field'), 'a')
- })
- test('coalesceOrEqualOrThrow returns fallback when primary is unset', () => {
- assert.equal(coalesceOrEqualOrThrow(undefined, 'b', 'field'), 'b')
- })
- test('coalesceOrEqualOrThrow returns value when both are set and equal', () => {
- assert.equal(coalesceOrEqualOrThrow('same', 'same', 'field'), 'same')
- })
- test('coalesceOrEqualOrThrow throws when both are set but differ', () => {
- assert.throws(
- () => coalesceOrEqualOrThrow('a', 'b', 'field'),
- /Primary and fallback values are both set but differ/
- )
- })
- test('coalesceOrThrowVATNumber returns billingInfo VAT when set', () => {
- const account = {
- vatNumber: '',
- billingInfo: { vatNumber: 'BILL456' },
- }
- assert.equal(coalesceOrThrowVATNumber(account), 'BILL456')
- })
- test('coalesceOrThrowVATNumber returns account VAT when billingInfo VAT unset', () => {
- const account = {
- vatNumber: 'ACCT123',
- billingInfo: { vatNumber: '' },
- }
- assert.equal(coalesceOrThrowVATNumber(account), 'ACCT123')
- })
- test('coalesceOrThrowVATNumber returns null when neither is set', () => {
- assert.equal(coalesceOrThrowVATNumber({}), null)
- assert.equal(
- coalesceOrThrowVATNumber({ vatNumber: '', billingInfo: { vatNumber: '' } }),
- null
- )
- })
- test('coalesceOrThrowVATNumber treats trimmed values as equal', () => {
- const account = {
- vatNumber: ' GB123 ',
- billingInfo: { vatNumber: 'GB123' },
- }
- assert.equal(coalesceOrThrowVATNumber(account), 'GB123')
- })
- test('coalesceOrThrowVATNumber throws when both are set but differ', () => {
- const account = {
- vatNumber: 'GB123',
- billingInfo: { vatNumber: 'DE999' },
- }
- assert.throws(
- () => coalesceOrThrowVATNumber(account),
- /Field vatNumber: Primary and fallback values are both set but differ/
- )
- })
- test('getCanadaTaxIdType returns ca_gst_hst for GST/HST format', () => {
- assert.equal(getCanadaTaxIdType('123456789RT0002', null), 'ca_gst_hst')
- assert.equal(getCanadaTaxIdType(' 123456789rt0002 ', null), 'ca_gst_hst')
- })
- test('getCanadaTaxIdType returns ca_qst for Quebec QST format', () => {
- assert.equal(getCanadaTaxIdType('1234567890TQ1234', null), 'ca_qst')
- assert.equal(getCanadaTaxIdType('1234567890tq1234', null), 'ca_qst')
- })
- test('getCanadaTaxIdType returns PST types for documented provincial formats', () => {
- assert.equal(getCanadaTaxIdType('PST-1234-5678', null), 'ca_pst_bc')
- assert.equal(getCanadaTaxIdType('123456-7', 'R3C 4T3'), 'ca_pst_mb')
- assert.equal(getCanadaTaxIdType('1234567', 'S7K 3J8'), 'ca_pst_sk')
- })
- test('getCanadaTaxIdType returns ca_bn for 9-digit BN format', () => {
- // assert.equal(getCanadaTaxIdType('123456789', null), 'ca_bn')
- assert.equal(getCanadaTaxIdType('123456789', null), null) // TODO: improve function get definitive ca_bn vs ca_gst_hst
- })
- test('getCanadaTaxIdType returns null when format is unknown/ambiguous', () => {
- assert.equal(getCanadaTaxIdType('', null), null)
- assert.equal(getCanadaTaxIdType(null, null), null)
- assert.equal(getCanadaTaxIdType('RT0002', null), null)
- assert.equal(getCanadaTaxIdType('PST12345678', null), null)
- })
- test('getAustraliaTaxIdType distinguishes ABN vs ARN', () => {
- assert.equal(getAustraliaTaxIdType('12345678912'), 'au_abn')
- assert.equal(getAustraliaTaxIdType('123456789123'), 'au_arn')
- })
- test('getBrazilTaxIdType distinguishes CNPJ vs CPF', () => {
- assert.equal(getBrazilTaxIdType('01.234.456/5432-10'), 'br_cnpj')
- assert.equal(getBrazilTaxIdType('123.456.789-87'), 'br_cpf')
- })
- test('getBulgariaTaxIdType distinguishes EU VAT vs UIC', () => {
- assert.equal(getBulgariaTaxIdType('BG0123456789'), 'eu_vat')
- assert.equal(getBulgariaTaxIdType('123456789'), 'bg_uic')
- })
- test('getCroatiaTaxIdType distinguishes EU VAT vs OIB', () => {
- assert.equal(getCroatiaTaxIdType('HR12345678912'), 'eu_vat')
- assert.equal(getCroatiaTaxIdType('12345678901'), 'hr_oib')
- })
- test('getGermanyTaxIdType distinguishes EU VAT vs Steuer Nummer', () => {
- assert.equal(getGermanyTaxIdType('DE123456789'), 'eu_vat')
- assert.equal(getGermanyTaxIdType('1234567890'), 'de_stn')
- })
- test('getHungaryTaxIdType distinguishes EU VAT vs HU tax number', () => {
- assert.equal(getHungaryTaxIdType('HU12345678'), 'eu_vat')
- assert.equal(getHungaryTaxIdType('12345678-1-23'), 'hu_tin')
- })
- test('getJapanTaxIdType distinguishes TRN, CN, RN', () => {
- assert.equal(getJapanTaxIdType('T1234567891234'), 'jp_trn')
- assert.equal(getJapanTaxIdType('1234567891234'), 'jp_cn')
- assert.equal(getJapanTaxIdType('12345'), 'jp_rn')
- })
- test('getLiechtensteinTaxIdType distinguishes UID vs VAT', () => {
- assert.equal(getLiechtensteinTaxIdType('CHE123456789'), 'li_uid')
- assert.equal(getLiechtensteinTaxIdType('12345'), 'li_vat')
- })
- test('getMalaysiaTaxIdType distinguishes FRP, ITN, SST', () => {
- assert.equal(getMalaysiaTaxIdType('12345678'), 'my_frp')
- assert.equal(getMalaysiaTaxIdType('C 1234567890'), 'my_itn')
- assert.equal(getMalaysiaTaxIdType('A12-3456-78912345'), 'my_sst')
- })
- test('getNorwayTaxIdType distinguishes VAT vs VOEC', () => {
- assert.equal(getNorwayTaxIdType('123456789MVA'), 'no_vat')
- assert.equal(getNorwayTaxIdType('1234567'), 'no_voec')
- })
- test('getPolandTaxIdType distinguishes EU VAT vs NIP', () => {
- assert.equal(getPolandTaxIdType('PL1234567890'), 'eu_vat')
- assert.equal(getPolandTaxIdType('1234567890'), 'pl_nip')
- })
- test('getRomaniaTaxIdType distinguishes EU VAT vs TIN', () => {
- assert.equal(getRomaniaTaxIdType('RO1234567891'), 'eu_vat')
- assert.equal(getRomaniaTaxIdType('1234567890123'), 'ro_tin')
- })
- test('getRussiaTaxIdType distinguishes INN vs KPP', () => {
- assert.equal(getRussiaTaxIdType('1234567891'), 'ru_inn')
- assert.equal(getRussiaTaxIdType('123456789'), 'ru_kpp')
- })
- test('getSingaporeTaxIdType distinguishes GST vs UEN', () => {
- assert.equal(getSingaporeTaxIdType('M12345678X'), 'sg_gst')
- assert.equal(getSingaporeTaxIdType('123456789F'), 'sg_uen')
- })
- test('getSloveniaTaxIdType distinguishes EU VAT vs TIN', () => {
- assert.equal(getSloveniaTaxIdType('SI12345678'), 'eu_vat')
- assert.equal(getSloveniaTaxIdType('12345678'), 'si_tin')
- })
- test('getSpainTaxIdType distinguishes EU VAT vs CIF', () => {
- assert.equal(getSpainTaxIdType('ESA1234567Z'), 'eu_vat')
- assert.equal(getSpainTaxIdType('A12345678'), 'es_cif')
- })
- test('getSwitzerlandTaxIdType distinguishes UID vs VAT', () => {
- assert.equal(getSwitzerlandTaxIdType('CHE-123.456.789 HR'), 'ch_uid')
- assert.equal(getSwitzerlandTaxIdType('CHE-123.456.789 MWST'), 'ch_vat')
- })
- test('getUkTaxIdType distinguishes GB VAT vs EU VAT (NI)', () => {
- assert.equal(getUkTaxIdType('GB123456789'), 'gb_vat')
- assert.equal(getUkTaxIdType('XI123456789'), 'eu_vat')
- })
- test('getUzbekistanTaxIdType distinguishes TIN vs VAT', () => {
- assert.equal(getUzbekistanTaxIdType('123456789'), 'uz_tin')
- assert.equal(getUzbekistanTaxIdType('123456789012'), 'uz_vat')
- })
- test('getTaxIdType handles EU OSS VAT and EU VAT defaults', () => {
- assert.equal(getTaxIdType('EU', 'EU123456789').type, 'eu_oss_vat')
- assert.equal(getTaxIdType('AT', 'ATU12345678').type, 'eu_vat')
- })
- test('getTaxIdType includes failure reason', () => {
- const missingCountry = getTaxIdType(null, '12345', null)
- assert.equal(missingCountry.type, null)
- assert.equal(missingCountry.reason, 'missing country')
- const invalidEuOss = getTaxIdType('EU', 'INVALID', null)
- assert.equal(invalidEuOss.type, null)
- 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', {}),
- /Stripe customer cus_123 has no usable payment method/
- )
- })
- test('coalesceOrThrowPaymentMethod throws when no payment methods match billing info', () => {
- const paymentMethods = [
- {
- id: 'pm_1',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- },
- ]
- const billingInfo = {
- paymentMethod: { lastFour: '5678', expMonth: 12, expYear: 2030 },
- }
- assert.throws(
- () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', billingInfo),
- /Stripe customer cus_123 has no usable payment method/
- )
- })
- test('coalesceOrThrowPaymentMethod returns matching payment method', () => {
- const paymentMethod = {
- id: 'pm_match',
- type: 'card',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- }
- const paymentMethods = [paymentMethod]
- const billingInfo = {
- paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
- }
- const result = coalesceOrThrowPaymentMethod(
- paymentMethods,
- 'cus_123',
- billingInfo
- )
- assert.equal(result, paymentMethod)
- })
- test('coalesceOrThrowPaymentMethod matches first of multiple matching methods', () => {
- const paymentMethod1 = {
- id: 'pm_1',
- type: 'card',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- }
- const paymentMethod2 = {
- id: 'pm_2',
- type: 'card',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- }
- const paymentMethods = [paymentMethod1, paymentMethod2]
- const billingInfo = {
- paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
- }
- const result = coalesceOrThrowPaymentMethod(
- paymentMethods,
- 'cus_123',
- billingInfo
- )
- assert.equal(result, paymentMethod1)
- })
- test('coalesceOrThrowPaymentMethod filters out non-matching methods', () => {
- const matchingMethod = {
- id: 'pm_match',
- type: 'card',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- }
- const nonMatchingMethod = {
- id: 'pm_no_match',
- type: 'card',
- card: { last4: '5678', exp_month: 12, exp_year: 2030 },
- }
- const paymentMethods = [nonMatchingMethod, matchingMethod]
- const billingInfo = {
- paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
- }
- const result = coalesceOrThrowPaymentMethod(
- paymentMethods,
- 'cus_123',
- billingInfo
- )
- assert.equal(result, matchingMethod)
- })
- test('coalesceOrThrowPaymentMethod handles null billingInfo', () => {
- const paymentMethods = [
- {
- id: 'pm_1',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- },
- ]
- assert.throws(
- () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', null),
- /Stripe customer cus_123 has no usable payment method/
- )
- })
- test('coalesceOrThrowPaymentMethod handles missing paymentMethod in billingInfo', () => {
- const paymentMethods = [
- {
- id: 'pm_1',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- },
- ]
- const billingInfo = {}
- assert.throws(
- () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', billingInfo),
- /Stripe customer cus_123 has no usable payment method/
- )
- })
- test('coalesceOrThrowPaymentMethod handles missing card in payment method', () => {
- const paymentMethods = [
- { id: 'pm_1' }, // no card property
- {
- id: 'pm_2',
- type: 'card',
- card: { last4: '1234', exp_month: 12, exp_year: 2030 },
- },
- ]
- const billingInfo = {
- paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
- }
- const result = coalesceOrThrowPaymentMethod(
- paymentMethods,
- 'cus_123',
- billingInfo
- )
- assert.equal(result.id, 'pm_2')
- })
- test('normalisedGBVATNumber strips spaces and punctuation', () => {
- assert.equal(normalisedGBVATNumber('123 456 789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('123-456-789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('123.456.789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber(' 123 456 789 '), 'GB123456789')
- })
- test('normalisedGBVATNumber prepends GB if not present', () => {
- assert.equal(normalisedGBVATNumber('123456789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('123456789012'), 'GB123456789012')
- })
- test('normalisedGBVATNumber does not prepend GB if already present', () => {
- assert.equal(normalisedGBVATNumber('GB123456789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('GB123456789012'), 'GB123456789012')
- assert.equal(normalisedGBVATNumber('gb123456789'), 'GB123456789')
- })
- test('normalisedGBVATNumber removes trailing GB', () => {
- assert.equal(normalisedGBVATNumber('123456789GB'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('GB123456789GB'), 'GB123456789')
- })
- test('normalisedGBVATNumber returns original if invalid after normalization', () => {
- // Invalid length (8 digits instead of 9 or 12)
- const invalid = '12345678'
- assert.equal(normalisedGBVATNumber(invalid), invalid)
- // Invalid length (10 digits instead of 9 or 12)
- const invalid2 = '1234567890'
- assert.equal(normalisedGBVATNumber(invalid2), invalid2)
- // Invalid format with letters
- const invalid3 = 'ABC123456'
- assert.equal(normalisedGBVATNumber(invalid3), invalid3)
- })
- test('normalisedGBVATNumber handles null and undefined', () => {
- assert.equal(normalisedGBVATNumber(null), null)
- assert.equal(normalisedGBVATNumber(undefined), undefined)
- assert.equal(normalisedGBVATNumber(''), '')
- })
- test('normalisedGBVATNumber handles valid 9-digit GB VAT numbers', () => {
- assert.equal(normalisedGBVATNumber('123456789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber('GB123456789'), 'GB123456789')
- assert.equal(normalisedGBVATNumber(' GB 123 456 789 '), 'GB123456789')
- })
- test('normalisedGBVATNumber handles valid 12-digit GB VAT numbers', () => {
- assert.equal(normalisedGBVATNumber('123456789012'), 'GB123456789012')
- assert.equal(normalisedGBVATNumber('GB123456789012'), 'GB123456789012')
- assert.equal(normalisedGBVATNumber('GB 123 456 789 012'), 'GB123456789012')
- })
- test('normalisedGBVATNumber preserves Northern Ireland XI numbers', () => {
- // XI numbers should not be modified to GB
- assert.equal(normalisedGBVATNumber('XI123456789'), 'XI123456789')
- })
- test('normalizeComparableString returns empty string for null/undefined', () => {
- assert.equal(normalizeComparableString(null), '')
- assert.equal(normalizeComparableString(undefined), '')
- })
- test('normalizeComparableString trims whitespace', () => {
- assert.equal(normalizeComparableString(' hello '), 'hello')
- assert.equal(normalizeComparableString(' '), '')
- })
- test('normalizeComparableString converts non-strings', () => {
- assert.equal(normalizeComparableString(42), '42')
- assert.equal(normalizeComparableString(0), '0')
- })
- test('hasAnyAddressValue returns false for null/undefined/non-object', () => {
- assert.equal(hasAnyAddressValue(null), false)
- assert.equal(hasAnyAddressValue(undefined), false)
- assert.equal(hasAnyAddressValue('string'), false)
- })
- test('hasAnyAddressValue returns false for all-empty address', () => {
- assert.equal(hasAnyAddressValue({ line1: '', line2: '', city: '' }), false)
- assert.equal(hasAnyAddressValue({ line1: ' ', city: null }), false)
- })
- test('hasAnyAddressValue returns true when any field is non-empty', () => {
- assert.equal(hasAnyAddressValue({ line1: '123 Main St' }), true)
- assert.equal(hasAnyAddressValue({ country: 'US' }), true)
- })
- test('ccEmailsToArray returns empty array for null/undefined', () => {
- assert.deepEqual(ccEmailsToArray(null), [])
- assert.deepEqual(ccEmailsToArray(undefined), [])
- })
- test('ccEmailsToArray splits on commas, semicolons, and whitespace', () => {
- assert.deepEqual(ccEmailsToArray('a@b.com,c@d.com'), ['a@b.com', 'c@d.com'])
- assert.deepEqual(ccEmailsToArray('a@b.com;c@d.com'), ['a@b.com', 'c@d.com'])
- assert.deepEqual(ccEmailsToArray('a@b.com c@d.com'), ['a@b.com', 'c@d.com'])
- })
- test('ccEmailsToArray deduplicates', () => {
- assert.deepEqual(ccEmailsToArray('a@b.com,a@b.com,c@d.com'), [
- 'a@b.com',
- 'c@d.com',
- ])
- })
- test('ccEmailsToArray handles empty string', () => {
- assert.deepEqual(ccEmailsToArray(''), [])
- })
- test('normalizeTaxIdValue strips punctuation and lowercases', () => {
- assert.equal(normalizeTaxIdValue('GB.123-456_789'), 'gb123456789')
- assert.equal(normalizeTaxIdValue(' DE:123/456 '), 'de123456')
- })
- test('normalizeTaxIdValue handles null/undefined', () => {
- assert.equal(normalizeTaxIdValue(null), '')
- assert.equal(normalizeTaxIdValue(undefined), '')
- })
- test('extractRecurlyCustomFieldMetadata extracts known fields', () => {
- const account = {
- customFields: [
- { name: 'channel', value: 'web' },
- { name: 'Industry', value: 'Education' },
- { name: 'unknown_field', value: 'ignored' },
- ],
- }
- const { metadata, counts } = extractRecurlyCustomFieldMetadata(account)
- assert.deepEqual(metadata, { channel: 'web', Industry: 'Education' })
- assert.equal(counts.channel, 1)
- assert.equal(counts.Industry, 1)
- assert.equal(counts.ol_sales_person, 0)
- })
- test('extractRecurlyCustomFieldMetadata returns empty for no custom fields', () => {
- const { metadata, counts } = extractRecurlyCustomFieldMetadata({
- customFields: [],
- })
- assert.deepEqual(metadata, {})
- assert.equal(counts.noCustomFields, 1)
- })
- test('extractRecurlyCustomFieldMetadata throws for missing customFields', () => {
- assert.throws(
- () => extractRecurlyCustomFieldMetadata({}),
- /account.customFields is missing or not an array/
- )
- })
- test('extractRecurlyCustomFieldMetadata skips null/empty values', () => {
- const account = {
- customFields: [
- { name: 'channel', value: null },
- { name: 'Industry', value: '' },
- { name: 'ol_sales_person', value: ' ' },
- ],
- }
- const { metadata } = extractRecurlyCustomFieldMetadata(account)
- assert.deepEqual(metadata, {})
- })
- test('addressesEqual returns true for two null addresses', () => {
- assert.equal(addressesEqual(null, null), true)
- })
- test('addressesEqual returns false when one is null', () => {
- assert.equal(
- addressesEqual({ line1: '123 Main St', country: 'US' }, null),
- false
- )
- assert.equal(
- addressesEqual(null, { line1: '123 Main St', country: 'US' }),
- false
- )
- })
- test('addressesEqual returns true for matching addresses', () => {
- const a = {
- line1: '123 Main St',
- line2: '',
- city: 'London',
- state: '',
- postal_code: 'SW1A 1AA',
- country: 'GB',
- }
- const b = {
- line1: '123 Main St',
- line2: '',
- city: 'London',
- state: '',
- postal_code: 'SW1A 1AA',
- country: 'gb',
- }
- assert.equal(addressesEqual(a, b), true)
- })
- test('addressesEqual returns false for differing addresses', () => {
- const a = {
- line1: '123 Main St',
- city: 'London',
- country: 'GB',
- }
- const b = {
- line1: '456 High St',
- city: 'London',
- country: 'GB',
- }
- assert.equal(addressesEqual(a, b), false)
- })
- test('addressesEqual normalizes whitespace', () => {
- const a = { line1: ' 123 Main St ', country: ' GB ' }
- const b = { line1: '123 Main St', country: 'GB' }
- assert.equal(addressesEqual(a, b), true)
- })
- test('areStripeAndRecurlyCardDetailsEqual returns false for null inputs', () => {
- assert.equal(areStripeAndRecurlyCardDetailsEqual(null, null), false)
- assert.equal(
- areStripeAndRecurlyCardDetailsEqual(null, { lastFour: '1234' }),
- false
- )
- assert.equal(
- areStripeAndRecurlyCardDetailsEqual(
- { type: 'card', card: { last4: '1234', exp_month: 12, exp_year: 2030 } },
- null
- ),
- false
- )
- })
- test('areStripeAndRecurlyCardDetailsEqual returns false for non-card type', () => {
- assert.equal(
- areStripeAndRecurlyCardDetailsEqual(
- { type: 'paypal' },
- { lastFour: '1234' }
- ),
- false
- )
- })
- test('areStripeAndRecurlyCardDetailsEqual returns true for matching cards', () => {
- assert.equal(
- areStripeAndRecurlyCardDetailsEqual(
- { type: 'card', card: { last4: '4242', exp_month: 6, exp_year: 2025 } },
- { lastFour: '4242', expMonth: 6, expYear: 2025 }
- ),
- true
- )
- })
- test('areStripeAndRecurlyCardDetailsEqual returns false for mismatched last4', () => {
- assert.equal(
- areStripeAndRecurlyCardDetailsEqual(
- { type: 'card', card: { last4: '4242', exp_month: 6, exp_year: 2025 } },
- { lastFour: '1111', expMonth: 6, expYear: 2025 }
- ),
- false
- )
- })
- test('resolveCustomerIdentity uses billing info when no conflict', async () => {
- const account = {
- billingInfo: {
- firstName: 'Bill',
- lastName: 'User',
- address: { street1: '123 Bill St', country: 'US' },
- company: 'Bill Corp',
- vatNumber: 'US123',
- },
- firstName: 'Account',
- lastName: 'User',
- address: { street1: '456 Acct St', country: 'US' },
- company: 'Acct Corp',
- vatNumber: 'US123',
- }
- // No conflict on vatNumber (same), but name differs -> conflict
- const result = await resolveCustomerIdentity(account, async () => 'automatic')
- assert.equal(result.name, 'Bill User')
- })
- test('resolveCustomerIdentity uses account info for manual collection', async () => {
- const account = {
- billingInfo: {
- firstName: 'Bill',
- lastName: 'User',
- address: { street1: '123 Bill St', country: 'US' },
- },
- firstName: 'Account',
- lastName: 'User',
- address: { street1: '456 Acct St', country: 'US' },
- }
- const result = await resolveCustomerIdentity(account, async () => 'manual')
- assert.equal(result.name, 'Account User')
- assert.notEqual(result.billingInfoForPaymentMethod, null)
- })
- test('resolveCustomerIdentity returns billing info preferred when no conflict', async () => {
- const account = {
- billingInfo: {
- firstName: 'Same',
- lastName: 'Name',
- vatNumber: 'GB123456789',
- },
- firstName: 'Same',
- lastName: 'Name',
- vatNumber: 'GB123456789',
- }
- const result = await resolveCustomerIdentity(account, async () => {
- throw new Error('should not be called')
- })
- assert.equal(result.name, 'Same Name')
- assert.equal(result.collectionMethod, null)
- })
- test('resolveCustomerIdentity does not call fetchCollectionMethod when no conflict', async () => {
- let called = false
- const account = {
- billingInfo: { firstName: 'Only', lastName: 'Billing' },
- }
- await resolveCustomerIdentity(account, async () => {
- called = true
- return 'automatic'
- })
- assert.equal(called, false)
- })
- test('resolveCustomerIdentity throws when conflict but no collection method', async () => {
- const account = {
- billingInfo: {
- firstName: 'Bill',
- lastName: 'Name',
- },
- firstName: 'Acct',
- lastName: 'Name',
- }
- await assert.rejects(
- () => resolveCustomerIdentity(account, async () => null),
- /no subscription found to determine collection method/
- )
- })
- test('resolveCustomerIdentity normalises GB VAT', async () => {
- const account = {
- billingInfo: {
- firstName: 'Test',
- lastName: 'User',
- address: { country: 'GB' },
- vatNumber: '123456789',
- },
- }
- const result = await resolveCustomerIdentity(account, async () => 'automatic')
- assert.equal(result.vatNumber, 'GB123456789')
- })
- test('compareAccountFields returns empty diffs when everything matches', async () => {
- const account = {
- email: 'user@example.com',
- firstName: 'Test',
- lastName: 'User',
- customFields: [],
- billingInfo: {
- firstName: 'Test',
- lastName: 'User',
- },
- }
- const stripeCustomer = {
- email: 'user@example.com',
- name: 'Test User',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.deepEqual(diffs, {})
- })
- test('compareAccountFields detects email drift', async () => {
- const account = {
- email: 'new@example.com',
- customFields: [],
- billingInfo: {},
- }
- const stripeCustomer = {
- email: 'old@example.com',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.ok(diffs.email)
- assert.equal(diffs.email.recurly, 'new@example.com')
- assert.equal(diffs.email.stripe, 'old@example.com')
- })
- test('compareAccountFields detects name drift', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [],
- billingInfo: {
- firstName: 'New',
- lastName: 'Name',
- },
- }
- const stripeCustomer = {
- email: 'user@example.com',
- name: 'Old Name',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.ok(diffs.name)
- assert.equal(diffs.name.recurly, 'New Name')
- assert.equal(diffs.name.stripe, 'Old Name')
- })
- test('compareAccountFields detects tax ID drift', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [],
- billingInfo: {
- firstName: 'Test',
- lastName: 'User',
- address: { country: 'DE' },
- vatNumber: 'DE123456789',
- },
- }
- const stripeCustomer = {
- email: 'user@example.com',
- name: 'Test User',
- address: { country: 'DE' },
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- tax_ids: { data: [] },
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.ok(diffs.tax_id)
- assert.equal(diffs.tax_id.recurly.type, 'eu_vat')
- assert.equal(diffs.tax_id.recurly.value, 'DE123456789')
- })
- test('compareAccountFields does not report tax ID diff when values match after normalization', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [],
- billingInfo: {
- firstName: 'Test',
- lastName: 'User',
- address: { country: 'DE' },
- vatNumber: 'DE-123.456.789',
- },
- }
- const stripeCustomer = {
- email: 'user@example.com',
- name: 'Test User',
- address: { country: 'DE' },
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- tax_ids: {
- data: [{ type: 'eu_vat', value: 'DE123456789' }],
- },
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.equal(diffs.tax_id, undefined)
- })
- test('compareAccountFields handles no payment methods on either side without crashing', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [],
- billingInfo: {},
- }
- const stripeCustomer = {
- email: 'user@example.com',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- invoice_settings: { default_payment_method: null },
- }
- // This should not throw (regression test for null payment method crash)
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.equal(diffs.default_payment_method, undefined)
- })
- test('compareAccountFields detects metadata drift', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [{ name: 'channel', value: 'web' }],
- billingInfo: {},
- }
- const stripeCustomer = {
- email: 'user@example.com',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- channel: 'api',
- },
- tax_exempt: 'none',
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.ok(diffs['metadata.channel'])
- assert.equal(diffs['metadata.channel'].recurly, 'web')
- assert.equal(diffs['metadata.channel'].stripe, 'api')
- })
- test('compareAccountFields detects tax_exempt drift', async () => {
- const account = {
- email: 'user@example.com',
- customFields: [],
- billingInfo: {},
- taxExempt: true,
- }
- const stripeCustomer = {
- email: 'user@example.com',
- metadata: {
- recurlyAccountCode: '',
- userId: 'user123',
- taxInfoPending: '',
- },
- tax_exempt: 'none',
- }
- const diffs = await compareAccountFields({
- account,
- stripeCustomer,
- overleafUserId: 'user123',
- fetchCollectionMethod: async () => null,
- stripePaymentMethods: [],
- stripeServiceName: 'stripe-uk',
- })
- assert.ok(diffs.tax_exempt)
- assert.equal(diffs.tax_exempt.recurly, 'exempt')
- assert.equal(diffs.tax_exempt.stripe, 'none')
- })
|