CollectPayPalPastDueInvoiceTest.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = require('chai')
  4. chai.use(require('chai-as-promised'))
  5. chai.use(require('sinon-chai'))
  6. const {
  7. main,
  8. } = require('../../../scripts/recurly/collect_paypal_past_due_invoice')
  9. const RecurlyWrapper = require('../../../app/src/Features/Subscription/RecurlyWrapper')
  10. const OError = require('@overleaf/o-error')
  11. // from https://recurly.com/developers/api-v2/v2.21/#operation/listInvoices
  12. const invoicesXml = invoiceIds => `
  13. <invoices type="array">
  14. ${invoiceIds
  15. .map(
  16. invoiceId => `
  17. <invoice href="https://your-subdomain.recurly.com/v2/invoices/${invoiceId}">
  18. <account href="https://your-subdomain.recurly.com/v2/accounts/${invoiceId}"/>
  19. <subscriptions href="https://your-subdomain.recurly.com/v2/invoices/${invoiceId}/subscriptions"/>
  20. <address>
  21. <address1></address1>
  22. <address2></address2>
  23. <city></city>
  24. <state></state>
  25. <zip></zip>
  26. <country></country>
  27. <phone></phone>
  28. </address>
  29. <shipping_address>
  30. <name>Lon Doner</name>
  31. <address1>221B Baker St.</address1>
  32. <address2></address2>
  33. <city>London</city>
  34. <state></state>
  35. <zip>W1K 6AH</zip>
  36. <country>GB</country>
  37. <phone></phone>
  38. </shipping_address>
  39. <uuid>421f7b7d414e4c6792938e7c49d552e9</uuid>
  40. <state>paid</state>
  41. <invoice_number_prefix></invoice_number_prefix> <!-- Only populated for VAT Country Invoice Sequencing. Shows a country code. -->
  42. <invoice_number type="integer">${invoiceId}</invoice_number>
  43. <po_number nil="nil"></po_number>
  44. <vat_number nil="nil"></vat_number>
  45. <subtotal_in_cents type="integer">2000</subtotal_in_cents>
  46. <discount_in_cents type="integer">0</discount_in_cents>
  47. <due_on type="datetime">2018-01-30T21:11:50Z</due_on>
  48. <balance_in_cents type="integer">0</balance_in_cents>
  49. <type>charge</type>
  50. <origin>purchase</origin>
  51. <credit_invoices href="https://your-subdomain.recurly.com/v2/invoices/1325/credit_invoices"/>
  52. <refundable_total_in_cents type="integer">2000</refundable_total_in_cents>
  53. <credit_payments type="array">
  54. </credit_payments>
  55. <tax_in_cents type="integer">0</tax_in_cents>
  56. <total_in_cents type="integer">1200</total_in_cents>
  57. <currency>USD</currency>
  58. <created_at type="datetime">2016-06-25T12:00:00Z</created_at>
  59. <closed_at nil="nil"></closed_at>
  60. <terms_and_conditions></terms_and_conditions>
  61. <customer_notes></customer_notes>
  62. <vat_reverse_charge_notes></vat_reverse_charge_notes>
  63. <tax_type>usst</tax_type>
  64. <tax_region>CA</tax_region>
  65. <tax_rate type="float">0</tax_rate>
  66. <net_terms type="integer">0</net_terms>
  67. <collection_method>automatic</collection_method>
  68. <redemptions href="https://your-subdomain.recurly.com/v2/invoices/e3f0a9e084a2468480d00ee61b090d4d/redemptions"/>
  69. <line_items type="array">
  70. <adjustment href="https://your-subdomain.recurly.com/v2/adjustments/05a4bbdeda2a47348185270021e6087b">
  71. </adjustment>
  72. </line_items>
  73. <transactions type="array">
  74. </transactions>
  75. </invoice>`
  76. )
  77. .join('')}
  78. </invoices>
  79. `
  80. // from https://recurly.com/developers/api-v2/v2.21/#operation/lookupAccountsBillingInfo
  81. const billingInfoXml = `
  82. <billing_info href="https://your-subdomain.recurly.com/v2/accounts/1/billing_info" type="credit_card">
  83. <paypal_billing_agreement_id>PAYPAL_BILLING_AGREEMENT_ID</paypal_billing_agreement_id>
  84. <account href="https://your-subdomain.recurly.com/v2/accounts/1"/>
  85. <first_name>Verena</first_name>
  86. <last_name>Example</last_name>
  87. <company nil="nil"/>
  88. <address1>123 Main St.</address1>
  89. <address2 nil="nil"/>
  90. <city>San Francisco</city>
  91. <state>CA</state>
  92. <zip>94105</zip>
  93. <country>US</country>
  94. <phone nil="nil"/>
  95. <vat_number nil="nil"/>
  96. <ip_address>127.0.0.1</ip_address>
  97. <ip_address_country nil="nil"/>
  98. <card_type>Visa</card_type>
  99. <year type="integer">2019</year>
  100. <month type="integer">11</month>
  101. <first_six>411111</first_six>
  102. <last_four>1111</last_four>
  103. <updated_at type="datetime">2017-02-17T15:38:53Z</updated_at>
  104. </billing_info>
  105. `
  106. // from https://recurly.com/developers/api-v2/v2.21/#operation/collectAnInvoice
  107. const invoiceCollectXml = `
  108. <invoice href="https://your-subdomain.recurly.com/v2/invoices/1000">
  109. <account href="https://your-subdomain.recurly.com/v2/accounts/1"/>
  110. <subscriptions href="https://your-subdomain.recurly.com/v2/invoices/1000/subscriptions"/>
  111. <address>
  112. <address1>123 Main St.</address1>
  113. <address2 nil="nil"/>
  114. <city>San Francisco</city>
  115. <state>CA</state>
  116. <zip>94105</zip>
  117. <country>US</country>
  118. <phone nil="nil"/>
  119. </address>
  120. <uuid>374a37924f83c733b9c9814e9580496a</uuid>
  121. <state>pending</state>
  122. <invoice_number_prefix/>
  123. <invoice_number type="integer">1000</invoice_number>
  124. <po_number nil="nil"/>
  125. <vat_number nil="nil"/>
  126. <subtotal_in_cents type="integer">5000</subtotal_in_cents>
  127. <tax_in_cents type="integer">438</tax_in_cents>
  128. <total_in_cents type="integer">5438</total_in_cents>
  129. <currency>USD</currency>
  130. <created_at type="datetime">2016-07-11T19:25:57Z</created_at>
  131. <updated_at type="datetime">2016-07-11T19:25:57Z</updated_at>
  132. <closed_at nil="nil"/>
  133. <terms_and_conditions nil="nil"/>
  134. <customer_notes nil="nil"/>
  135. <tax_type>usst</tax_type>
  136. <tax_region>CA</tax_region>
  137. <tax_rate type="float">0.0875</tax_rate>
  138. <net_terms type="integer">0</net_terms>
  139. <collection_method>automatic</collection_method>
  140. <line_items type="array">
  141. <adjustment href="https://your-subdomain.recurly.com/v2/adjustments/374a2729397882fafbc82041a0a4dd0d" type="charge">
  142. <!-- Detail. -->
  143. </adjustment>
  144. </line_items>
  145. <transactions type="array">
  146. </transactions>
  147. <a name="mark_successful" href="https://your-subdomain.recurly.com/v2/invoices/1000/mark_successful" method="put"/>
  148. <a name="mark_failed" href="https://your-subdomain.recurly.com/v2/invoices/1000/mark_failed" method="put"/>
  149. </invoice>
  150. `
  151. const ITEMS_PER_PAGE = 3
  152. const getInvoicePage = fullInvoicesIds => queryOptions => {
  153. const cursor = queryOptions.qs.cursor
  154. const startEnd = cursor?.split(':').map(Number) || []
  155. const start = startEnd[0] || 0
  156. const end = startEnd[1] || ITEMS_PER_PAGE
  157. const body = invoicesXml(fullInvoicesIds.slice(start, end))
  158. const hasMore = end < fullInvoicesIds.length
  159. const nextPageCursor = hasMore ? `${end}%3A${end + ITEMS_PER_PAGE}&v=2` : null
  160. const response = {
  161. statusCode: 200,
  162. headers: {
  163. link: hasMore
  164. ? `https://fakerecurly.com/v2/invoices?cursor=${nextPageCursor}`
  165. : undefined,
  166. },
  167. }
  168. return { response, body }
  169. }
  170. describe('CollectPayPalPastDueInvoice', function () {
  171. let apiRequestStub
  172. const fakeApiRequests = invoiceIds => {
  173. apiRequestStub = sinon.stub(RecurlyWrapper.promises, 'apiRequest')
  174. apiRequestStub.callsFake(options => {
  175. if (options.url === 'invoices') {
  176. return getInvoicePage(invoiceIds)(options)
  177. }
  178. if (/accounts\/(\d+)\/billing_info/.test(options.url)) {
  179. return {
  180. response: { statusCode: 200, headers: {} },
  181. body: billingInfoXml,
  182. }
  183. }
  184. if (/invoices\/(\d+)\/collect/.test(options.url)) {
  185. const invoiceId = options.url.match(/invoices\/(\d+)\/collect/)[1]
  186. if (invoiceId < 400) {
  187. return {
  188. response: { statusCode: 200, headers: {} },
  189. body: invoiceCollectXml,
  190. }
  191. }
  192. throw new OError(`Recurly API returned with status code: 404`, {
  193. statusCode: 404,
  194. })
  195. }
  196. })
  197. }
  198. afterEach(function () {
  199. apiRequestStub?.restore()
  200. })
  201. it('collects one valid invoice', async function () {
  202. fakeApiRequests([200])
  203. const r = await main()
  204. expect(r).to.eql({
  205. INVOICES_COLLECTED: [200],
  206. INVOICES_COLLECTED_SUCCESS: [200],
  207. USERS_COLLECTED: ['200'],
  208. })
  209. })
  210. it('collects several pages', async function () {
  211. // 10 invoices, from 200 to 209
  212. fakeApiRequests([...Array(10).keys()].map(i => i + 200))
  213. const r = await main()
  214. expect(r).to.eql({
  215. INVOICES_COLLECTED: [200, 201, 202, 203, 204, 205, 206, 207, 208, 209],
  216. INVOICES_COLLECTED_SUCCESS: [
  217. 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
  218. ],
  219. USERS_COLLECTED: [
  220. '200',
  221. '201',
  222. '202',
  223. '203',
  224. '204',
  225. '205',
  226. '206',
  227. '207',
  228. '208',
  229. '209',
  230. ],
  231. })
  232. // 4 calls to get the invoices
  233. // 10 calls to get the billing info
  234. // 10 calls to collect the invoices
  235. expect(apiRequestStub.callCount).to.eql(24)
  236. })
  237. it('rejects when some invoice processing failed', async function () {
  238. fakeApiRequests([404])
  239. await expect(main()).to.be.rejectedWith(
  240. 'Invoices collection failed for 1 invoices'
  241. )
  242. })
  243. it('doesnt reject when there are no invoices', async function () {
  244. fakeApiRequests([])
  245. const r = await main()
  246. expect(r).to.eql({
  247. INVOICES_COLLECTED: [],
  248. INVOICES_COLLECTED_SUCCESS: [],
  249. USERS_COLLECTED: [],
  250. })
  251. })
  252. it('resolves when some invoices are partially successful', async function () {
  253. fakeApiRequests([200, 404])
  254. await expect(main()).to.be.rejectedWith(
  255. 'Invoices collection failed for 1 invoices'
  256. )
  257. })
  258. })