migrate_recurly_customers_to_stripe.helpers.node.test.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /* eslint-disable @overleaf/require-script-runner */
  2. import test from 'node:test'
  3. import assert from 'node:assert/strict'
  4. /*
  5. * This test can be run from the services/web directory with:
  6. *
  7. * node --test scripts/helpers/migrate_recurly_customers_to_stripe.helpers.node.test.mjs
  8. *
  9. * It can be deleted once the Recurly to Stripe migration is complete.
  10. */
  11. import {
  12. coalesceOrEqualOrThrow,
  13. coalesceOrEqualOrThrowAddress,
  14. coalesceOrEqualOrThrowName,
  15. coalesceOrThrowVATNumber,
  16. getCanadaTaxIdType,
  17. getAustraliaTaxIdType,
  18. getBrazilTaxIdType,
  19. getBulgariaTaxIdType,
  20. getCroatiaTaxIdType,
  21. getGermanyTaxIdType,
  22. getHungaryTaxIdType,
  23. getJapanTaxIdType,
  24. getLiechtensteinTaxIdType,
  25. getMalaysiaTaxIdType,
  26. getNorwayTaxIdType,
  27. getPolandTaxIdType,
  28. getRomaniaTaxIdType,
  29. getRussiaTaxIdType,
  30. getSingaporeTaxIdType,
  31. getSloveniaTaxIdType,
  32. getSpainTaxIdType,
  33. getSwitzerlandTaxIdType,
  34. getUkTaxIdType,
  35. getUzbekistanTaxIdType,
  36. getTaxIdType,
  37. coalesceOrThrowPaymentMethod,
  38. normalisedGBVATNumber,
  39. } from './migrate_recurly_customers_to_stripe.helpers.mjs'
  40. test('coalesceOrEqualOrThrow returns primary when set', () => {
  41. assert.equal(coalesceOrEqualOrThrow('a', undefined, 'field'), 'a')
  42. })
  43. test('coalesceOrEqualOrThrow returns fallback when primary is unset', () => {
  44. assert.equal(coalesceOrEqualOrThrow(undefined, 'b', 'field'), 'b')
  45. })
  46. test('coalesceOrEqualOrThrow returns value when both are set and equal', () => {
  47. assert.equal(coalesceOrEqualOrThrow('same', 'same', 'field'), 'same')
  48. })
  49. test('coalesceOrEqualOrThrow throws when both are set but differ', () => {
  50. assert.throws(
  51. () => coalesceOrEqualOrThrow('a', 'b', 'field'),
  52. /Primary and fallback values are both set but differ/
  53. )
  54. })
  55. test('coalesceOrEqualOrThrowAddress returns null when neither is valid', () => {
  56. assert.equal(coalesceOrEqualOrThrowAddress({}), null)
  57. assert.equal(
  58. coalesceOrEqualOrThrowAddress({
  59. address: { street1: '', postalCode: '', country: '' },
  60. billingInfo: { address: { street1: '', postalCode: '', country: '' } },
  61. }),
  62. null
  63. )
  64. assert.equal(
  65. coalesceOrEqualOrThrowAddress({
  66. address: { street1: ' ', postalCode: ' ', country: ' ' },
  67. }),
  68. null
  69. )
  70. })
  71. test('coalesceOrEqualOrThrowAddress returns account when billing invalid', () => {
  72. const account = {
  73. address: { street1: '1 Road', postalCode: 'ABC', country: 'GB' },
  74. billingInfo: {
  75. address: { street1: '', postalCode: 'ABC', country: 'GB' },
  76. },
  77. }
  78. assert.deepEqual(coalesceOrEqualOrThrowAddress(account), {
  79. line1: '1 Road',
  80. postal_code: 'ABC',
  81. country: 'GB',
  82. })
  83. })
  84. test('coalesceOrEqualOrThrowAddress returns billing when account invalid', () => {
  85. const account = {
  86. address: { street1: '', postalCode: 'ABC', country: 'GB' },
  87. billingInfo: {
  88. address: { street1: '1 Road', postalCode: 'ABC', country: 'GB' },
  89. },
  90. }
  91. assert.deepEqual(coalesceOrEqualOrThrowAddress(account), {
  92. line1: '1 Road',
  93. postal_code: 'ABC',
  94. country: 'GB',
  95. })
  96. })
  97. test('coalesceOrEqualOrThrowAddress returns billing when both valid+equal', () => {
  98. const addr = { street1: '1 Road', postalCode: 'ABC', country: 'GB' }
  99. assert.deepEqual(
  100. coalesceOrEqualOrThrowAddress({
  101. address: { ...addr },
  102. billingInfo: { address: addr },
  103. }),
  104. { line1: '1 Road', postal_code: 'ABC', country: 'GB' }
  105. )
  106. })
  107. test('coalesceOrEqualOrThrowAddress normalizes Recurly-style address fields', () => {
  108. const account = {
  109. billingInfo: {
  110. address: {
  111. street1: 'as',
  112. street2: '',
  113. city: '',
  114. region: '',
  115. postalCode: '12312',
  116. country: 'AI',
  117. },
  118. },
  119. }
  120. assert.deepEqual(coalesceOrEqualOrThrowAddress(account), {
  121. line1: 'as',
  122. postal_code: '12312',
  123. country: 'AI',
  124. })
  125. })
  126. test('coalesceOrEqualOrThrowAddress throws when both valid but differ', () => {
  127. const account = {
  128. address: { street1: '1 Road', postalCode: 'ABC', country: 'GB' },
  129. billingInfo: {
  130. address: { street1: '2 Road', postalCode: 'ABC', country: 'GB' },
  131. },
  132. }
  133. assert.throws(
  134. () => coalesceOrEqualOrThrowAddress(account),
  135. /Billing address and account address differ/
  136. )
  137. })
  138. test('coalesceOrEqualOrThrowName returns billingInfo name when both sources match', () => {
  139. const account = {
  140. firstName: 'Alice',
  141. lastName: 'Billing',
  142. billingInfo: { firstName: 'Alice', lastName: 'Billing' },
  143. }
  144. assert.equal(coalesceOrEqualOrThrowName(account), 'Alice Billing')
  145. })
  146. test('coalesceOrEqualOrThrowName prefers billingInfo when billingInfo is full but account is not', () => {
  147. const account = {
  148. firstName: 'Alice',
  149. lastName: '',
  150. billingInfo: { firstName: 'Alice', lastName: 'Billing' },
  151. }
  152. assert.equal(coalesceOrEqualOrThrowName(account), 'Alice Billing')
  153. })
  154. test('coalesceOrEqualOrThrowName falls back to account when billingInfo missing last name', () => {
  155. const account = {
  156. firstName: 'Alice',
  157. lastName: 'Account',
  158. billingInfo: { firstName: 'Alice', lastName: '' },
  159. }
  160. assert.equal(coalesceOrEqualOrThrowName(account), 'Alice Account')
  161. })
  162. test('coalesceOrEqualOrThrowName returns null when both sources are empty', () => {
  163. assert.equal(coalesceOrEqualOrThrowName({}), null)
  164. assert.equal(
  165. coalesceOrEqualOrThrowName({ firstName: '', lastName: '' }),
  166. null
  167. )
  168. })
  169. test('coalesceOrEqualOrThrowName throws when both full names are present but differ', () => {
  170. const account = {
  171. firstName: 'Alice',
  172. lastName: 'Account',
  173. billingInfo: { firstName: 'Alice', lastName: 'Billing' },
  174. }
  175. assert.throws(
  176. () => coalesceOrEqualOrThrowName(account),
  177. /Name differs between billingInfo and account/
  178. )
  179. })
  180. test('coalesceOrThrowVATNumber returns billingInfo VAT when set', () => {
  181. const account = {
  182. vatNumber: '',
  183. billingInfo: { vatNumber: 'BILL456' },
  184. }
  185. assert.equal(coalesceOrThrowVATNumber(account), 'BILL456')
  186. })
  187. test('coalesceOrThrowVATNumber returns account VAT when billingInfo VAT unset', () => {
  188. const account = {
  189. vatNumber: 'ACCT123',
  190. billingInfo: { vatNumber: '' },
  191. }
  192. assert.equal(coalesceOrThrowVATNumber(account), 'ACCT123')
  193. })
  194. test('coalesceOrThrowVATNumber returns null when neither is set', () => {
  195. assert.equal(coalesceOrThrowVATNumber({}), null)
  196. assert.equal(
  197. coalesceOrThrowVATNumber({ vatNumber: '', billingInfo: { vatNumber: '' } }),
  198. null
  199. )
  200. })
  201. test('coalesceOrThrowVATNumber treats trimmed values as equal', () => {
  202. const account = {
  203. vatNumber: ' GB123 ',
  204. billingInfo: { vatNumber: 'GB123' },
  205. }
  206. assert.equal(coalesceOrThrowVATNumber(account), 'GB123')
  207. })
  208. test('coalesceOrThrowVATNumber throws when both are set but differ', () => {
  209. const account = {
  210. vatNumber: 'GB123',
  211. billingInfo: { vatNumber: 'DE999' },
  212. }
  213. assert.throws(
  214. () => coalesceOrThrowVATNumber(account),
  215. /Field vatNumber: Primary and fallback values are both set but differ/
  216. )
  217. })
  218. test('getCanadaTaxIdType returns ca_gst_hst for GST/HST format', () => {
  219. assert.equal(getCanadaTaxIdType('123456789RT0002', null), 'ca_gst_hst')
  220. assert.equal(getCanadaTaxIdType(' 123456789rt0002 ', null), 'ca_gst_hst')
  221. })
  222. test('getCanadaTaxIdType returns ca_qst for Quebec QST format', () => {
  223. assert.equal(getCanadaTaxIdType('1234567890TQ1234', null), 'ca_qst')
  224. assert.equal(getCanadaTaxIdType('1234567890tq1234', null), 'ca_qst')
  225. })
  226. test('getCanadaTaxIdType returns PST types for documented provincial formats', () => {
  227. assert.equal(getCanadaTaxIdType('PST-1234-5678', null), 'ca_pst_bc')
  228. assert.equal(getCanadaTaxIdType('123456-7', 'R3C 4T3'), 'ca_pst_mb')
  229. assert.equal(getCanadaTaxIdType('1234567', 'S7K 3J8'), 'ca_pst_sk')
  230. })
  231. test('getCanadaTaxIdType returns ca_bn for 9-digit BN format', () => {
  232. // assert.equal(getCanadaTaxIdType('123456789', null), 'ca_bn')
  233. assert.equal(getCanadaTaxIdType('123456789', null), null) // TODO: improve function get definitive ca_bn vs ca_gst_hst
  234. })
  235. test('getCanadaTaxIdType returns null when format is unknown/ambiguous', () => {
  236. assert.equal(getCanadaTaxIdType('', null), null)
  237. assert.equal(getCanadaTaxIdType(null, null), null)
  238. assert.equal(getCanadaTaxIdType('RT0002', null), null)
  239. assert.equal(getCanadaTaxIdType('PST12345678', null), null)
  240. })
  241. test('getAustraliaTaxIdType distinguishes ABN vs ARN', () => {
  242. assert.equal(getAustraliaTaxIdType('12345678912'), 'au_abn')
  243. assert.equal(getAustraliaTaxIdType('123456789123'), 'au_arn')
  244. })
  245. test('getBrazilTaxIdType distinguishes CNPJ vs CPF', () => {
  246. assert.equal(getBrazilTaxIdType('01.234.456/5432-10'), 'br_cnpj')
  247. assert.equal(getBrazilTaxIdType('123.456.789-87'), 'br_cpf')
  248. })
  249. test('getBulgariaTaxIdType distinguishes EU VAT vs UIC', () => {
  250. assert.equal(getBulgariaTaxIdType('BG0123456789'), 'eu_vat')
  251. assert.equal(getBulgariaTaxIdType('123456789'), 'bg_uic')
  252. })
  253. test('getCroatiaTaxIdType distinguishes EU VAT vs OIB', () => {
  254. assert.equal(getCroatiaTaxIdType('HR12345678912'), 'eu_vat')
  255. assert.equal(getCroatiaTaxIdType('12345678901'), 'hr_oib')
  256. })
  257. test('getGermanyTaxIdType distinguishes EU VAT vs Steuer Nummer', () => {
  258. assert.equal(getGermanyTaxIdType('DE123456789'), 'eu_vat')
  259. assert.equal(getGermanyTaxIdType('1234567890'), 'de_stn')
  260. })
  261. test('getHungaryTaxIdType distinguishes EU VAT vs HU tax number', () => {
  262. assert.equal(getHungaryTaxIdType('HU12345678'), 'eu_vat')
  263. assert.equal(getHungaryTaxIdType('12345678-1-23'), 'hu_tin')
  264. })
  265. test('getJapanTaxIdType distinguishes TRN, CN, RN', () => {
  266. assert.equal(getJapanTaxIdType('T1234567891234'), 'jp_trn')
  267. assert.equal(getJapanTaxIdType('1234567891234'), 'jp_cn')
  268. assert.equal(getJapanTaxIdType('12345'), 'jp_rn')
  269. })
  270. test('getLiechtensteinTaxIdType distinguishes UID vs VAT', () => {
  271. assert.equal(getLiechtensteinTaxIdType('CHE123456789'), 'li_uid')
  272. assert.equal(getLiechtensteinTaxIdType('12345'), 'li_vat')
  273. })
  274. test('getMalaysiaTaxIdType distinguishes FRP, ITN, SST', () => {
  275. assert.equal(getMalaysiaTaxIdType('12345678'), 'my_frp')
  276. assert.equal(getMalaysiaTaxIdType('C 1234567890'), 'my_itn')
  277. assert.equal(getMalaysiaTaxIdType('A12-3456-78912345'), 'my_sst')
  278. })
  279. test('getNorwayTaxIdType distinguishes VAT vs VOEC', () => {
  280. assert.equal(getNorwayTaxIdType('123456789MVA'), 'no_vat')
  281. assert.equal(getNorwayTaxIdType('1234567'), 'no_voec')
  282. })
  283. test('getPolandTaxIdType distinguishes EU VAT vs NIP', () => {
  284. assert.equal(getPolandTaxIdType('PL1234567890'), 'eu_vat')
  285. assert.equal(getPolandTaxIdType('1234567890'), 'pl_nip')
  286. })
  287. test('getRomaniaTaxIdType distinguishes EU VAT vs TIN', () => {
  288. assert.equal(getRomaniaTaxIdType('RO1234567891'), 'eu_vat')
  289. assert.equal(getRomaniaTaxIdType('1234567890123'), 'ro_tin')
  290. })
  291. test('getRussiaTaxIdType distinguishes INN vs KPP', () => {
  292. assert.equal(getRussiaTaxIdType('1234567891'), 'ru_inn')
  293. assert.equal(getRussiaTaxIdType('123456789'), 'ru_kpp')
  294. })
  295. test('getSingaporeTaxIdType distinguishes GST vs UEN', () => {
  296. assert.equal(getSingaporeTaxIdType('M12345678X'), 'sg_gst')
  297. assert.equal(getSingaporeTaxIdType('123456789F'), 'sg_uen')
  298. })
  299. test('getSloveniaTaxIdType distinguishes EU VAT vs TIN', () => {
  300. assert.equal(getSloveniaTaxIdType('SI12345678'), 'eu_vat')
  301. assert.equal(getSloveniaTaxIdType('12345678'), 'si_tin')
  302. })
  303. test('getSpainTaxIdType distinguishes EU VAT vs CIF', () => {
  304. assert.equal(getSpainTaxIdType('ESA1234567Z'), 'eu_vat')
  305. assert.equal(getSpainTaxIdType('A12345678'), 'es_cif')
  306. })
  307. test('getSwitzerlandTaxIdType distinguishes UID vs VAT', () => {
  308. assert.equal(getSwitzerlandTaxIdType('CHE-123.456.789 HR'), 'ch_uid')
  309. assert.equal(getSwitzerlandTaxIdType('CHE-123.456.789 MWST'), 'ch_vat')
  310. })
  311. test('getUkTaxIdType distinguishes GB VAT vs EU VAT (NI)', () => {
  312. assert.equal(getUkTaxIdType('GB123456789'), 'gb_vat')
  313. assert.equal(getUkTaxIdType('XI123456789'), 'eu_vat')
  314. })
  315. test('getUzbekistanTaxIdType distinguishes TIN vs VAT', () => {
  316. assert.equal(getUzbekistanTaxIdType('123456789'), 'uz_tin')
  317. assert.equal(getUzbekistanTaxIdType('123456789012'), 'uz_vat')
  318. })
  319. test('getTaxIdType handles EU OSS VAT and EU VAT defaults', () => {
  320. assert.equal(getTaxIdType('EU', 'EU123456789').type, 'eu_oss_vat')
  321. assert.equal(getTaxIdType('AT', 'ATU12345678').type, 'eu_vat')
  322. })
  323. test('getTaxIdType includes failure reason', () => {
  324. const missingCountry = getTaxIdType(null, '12345', null)
  325. assert.equal(missingCountry.type, null)
  326. assert.equal(missingCountry.reason, 'missing country')
  327. const invalidEuOss = getTaxIdType('EU', 'INVALID', null)
  328. assert.equal(invalidEuOss.type, null)
  329. assert.equal(invalidEuOss.reason, 'invalid EU OSS VAT number')
  330. })
  331. test('getTaxIdType pre-validation is opt-in', () => {
  332. // Default behavior: no country-format pre-validation
  333. const noPreValidation = getTaxIdType('US', 'NOT_A_VALID_EIN', null)
  334. assert.equal(noPreValidation.type, 'us_ein')
  335. assert.equal(noPreValidation.reason, null)
  336. // Optional behavior: enable country-format pre-validation
  337. const withPreValidation = getTaxIdType('US', 'NOT_A_VALID_EIN', null, true)
  338. assert.equal(withPreValidation.type, null)
  339. assert.equal(withPreValidation.reason, 'invalid tax ID format for country US')
  340. })
  341. test('getTaxIdType pre-validates EU VAT-only countries when enabled', () => {
  342. const invalidWithPreValidation = getTaxIdType('AT', '12345678', null, true)
  343. assert.equal(invalidWithPreValidation.type, null)
  344. assert.equal(
  345. invalidWithPreValidation.reason,
  346. 'invalid tax ID format for country AT'
  347. )
  348. const validWithPreValidation = getTaxIdType('AT', 'ATU12345678', null, true)
  349. assert.equal(validWithPreValidation.type, 'eu_vat')
  350. assert.equal(validWithPreValidation.reason, null)
  351. })
  352. test('coalesceOrThrowPaymentMethod throws when payment methods array is empty', () => {
  353. assert.throws(
  354. () => coalesceOrThrowPaymentMethod([], 'cus_123', {}),
  355. /Stripe customer cus_123 has no usable payment method/
  356. )
  357. })
  358. test('coalesceOrThrowPaymentMethod throws when no payment methods match billing info', () => {
  359. const paymentMethods = [
  360. {
  361. id: 'pm_1',
  362. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  363. },
  364. ]
  365. const billingInfo = {
  366. paymentMethod: { lastFour: '5678', expMonth: 12, expYear: 2030 },
  367. }
  368. assert.throws(
  369. () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', billingInfo),
  370. /Stripe customer cus_123 has no usable payment method/
  371. )
  372. })
  373. test('coalesceOrThrowPaymentMethod returns matching payment method', () => {
  374. const paymentMethod = {
  375. id: 'pm_match',
  376. type: 'card',
  377. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  378. }
  379. const paymentMethods = [paymentMethod]
  380. const billingInfo = {
  381. paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
  382. }
  383. const result = coalesceOrThrowPaymentMethod(
  384. paymentMethods,
  385. 'cus_123',
  386. billingInfo
  387. )
  388. assert.equal(result, paymentMethod)
  389. })
  390. test('coalesceOrThrowPaymentMethod matches first of multiple matching methods', () => {
  391. const paymentMethod1 = {
  392. id: 'pm_1',
  393. type: 'card',
  394. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  395. }
  396. const paymentMethod2 = {
  397. id: 'pm_2',
  398. type: 'card',
  399. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  400. }
  401. const paymentMethods = [paymentMethod1, paymentMethod2]
  402. const billingInfo = {
  403. paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
  404. }
  405. const result = coalesceOrThrowPaymentMethod(
  406. paymentMethods,
  407. 'cus_123',
  408. billingInfo
  409. )
  410. assert.equal(result, paymentMethod1)
  411. })
  412. test('coalesceOrThrowPaymentMethod filters out non-matching methods', () => {
  413. const matchingMethod = {
  414. id: 'pm_match',
  415. type: 'card',
  416. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  417. }
  418. const nonMatchingMethod = {
  419. id: 'pm_no_match',
  420. type: 'card',
  421. card: { last4: '5678', exp_month: 12, exp_year: 2030 },
  422. }
  423. const paymentMethods = [nonMatchingMethod, matchingMethod]
  424. const billingInfo = {
  425. paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
  426. }
  427. const result = coalesceOrThrowPaymentMethod(
  428. paymentMethods,
  429. 'cus_123',
  430. billingInfo
  431. )
  432. assert.equal(result, matchingMethod)
  433. })
  434. test('coalesceOrThrowPaymentMethod handles null billingInfo', () => {
  435. const paymentMethods = [
  436. {
  437. id: 'pm_1',
  438. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  439. },
  440. ]
  441. assert.throws(
  442. () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', null),
  443. /Stripe customer cus_123 has no usable payment method/
  444. )
  445. })
  446. test('coalesceOrThrowPaymentMethod handles missing paymentMethod in billingInfo', () => {
  447. const paymentMethods = [
  448. {
  449. id: 'pm_1',
  450. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  451. },
  452. ]
  453. const billingInfo = {}
  454. assert.throws(
  455. () => coalesceOrThrowPaymentMethod(paymentMethods, 'cus_123', billingInfo),
  456. /Stripe customer cus_123 has no usable payment method/
  457. )
  458. })
  459. test('coalesceOrThrowPaymentMethod handles missing card in payment method', () => {
  460. const paymentMethods = [
  461. { id: 'pm_1' }, // no card property
  462. {
  463. id: 'pm_2',
  464. type: 'card',
  465. card: { last4: '1234', exp_month: 12, exp_year: 2030 },
  466. },
  467. ]
  468. const billingInfo = {
  469. paymentMethod: { lastFour: '1234', expMonth: 12, expYear: 2030 },
  470. }
  471. const result = coalesceOrThrowPaymentMethod(
  472. paymentMethods,
  473. 'cus_123',
  474. billingInfo
  475. )
  476. assert.equal(result.id, 'pm_2')
  477. })
  478. test('normalisedGBVATNumber strips spaces and punctuation', () => {
  479. assert.equal(normalisedGBVATNumber('123 456 789'), 'GB123456789')
  480. assert.equal(normalisedGBVATNumber('123-456-789'), 'GB123456789')
  481. assert.equal(normalisedGBVATNumber('123.456.789'), 'GB123456789')
  482. assert.equal(normalisedGBVATNumber(' 123 456 789 '), 'GB123456789')
  483. })
  484. test('normalisedGBVATNumber prepends GB if not present', () => {
  485. assert.equal(normalisedGBVATNumber('123456789'), 'GB123456789')
  486. assert.equal(normalisedGBVATNumber('123456789012'), 'GB123456789012')
  487. })
  488. test('normalisedGBVATNumber does not prepend GB if already present', () => {
  489. assert.equal(normalisedGBVATNumber('GB123456789'), 'GB123456789')
  490. assert.equal(normalisedGBVATNumber('GB123456789012'), 'GB123456789012')
  491. assert.equal(normalisedGBVATNumber('gb123456789'), 'GB123456789')
  492. })
  493. test('normalisedGBVATNumber removes trailing GB', () => {
  494. assert.equal(normalisedGBVATNumber('123456789GB'), 'GB123456789')
  495. assert.equal(normalisedGBVATNumber('GB123456789GB'), 'GB123456789')
  496. })
  497. test('normalisedGBVATNumber returns original if invalid after normalization', () => {
  498. // Invalid length (8 digits instead of 9 or 12)
  499. const invalid = '12345678'
  500. assert.equal(normalisedGBVATNumber(invalid), invalid)
  501. // Invalid length (10 digits instead of 9 or 12)
  502. const invalid2 = '1234567890'
  503. assert.equal(normalisedGBVATNumber(invalid2), invalid2)
  504. // Invalid format with letters
  505. const invalid3 = 'ABC123456'
  506. assert.equal(normalisedGBVATNumber(invalid3), invalid3)
  507. })
  508. test('normalisedGBVATNumber handles null and undefined', () => {
  509. assert.equal(normalisedGBVATNumber(null), null)
  510. assert.equal(normalisedGBVATNumber(undefined), undefined)
  511. assert.equal(normalisedGBVATNumber(''), '')
  512. })
  513. test('normalisedGBVATNumber handles valid 9-digit GB VAT numbers', () => {
  514. assert.equal(normalisedGBVATNumber('123456789'), 'GB123456789')
  515. assert.equal(normalisedGBVATNumber('GB123456789'), 'GB123456789')
  516. assert.equal(normalisedGBVATNumber(' GB 123 456 789 '), 'GB123456789')
  517. })
  518. test('normalisedGBVATNumber handles valid 12-digit GB VAT numbers', () => {
  519. assert.equal(normalisedGBVATNumber('123456789012'), 'GB123456789012')
  520. assert.equal(normalisedGBVATNumber('GB123456789012'), 'GB123456789012')
  521. assert.equal(normalisedGBVATNumber('GB 123 456 789 012'), 'GB123456789012')
  522. })
  523. test('normalisedGBVATNumber preserves Northern Ireland XI numbers', () => {
  524. // XI numbers should not be modified to GB
  525. assert.equal(normalisedGBVATNumber('XI123456789'), 'XI123456789')
  526. })