RecurlyWrapperTests.js 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-return-assign,
  6. no-unused-vars,
  7. standard/no-callback-literal,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const should = require('chai').should()
  17. const { expect } = require('chai')
  18. const sinon = require('sinon')
  19. const crypto = require('crypto')
  20. const querystring = require('querystring')
  21. const modulePath = '../../../../app/src/Features/Subscription/RecurlyWrapper'
  22. const SandboxedModule = require('sandboxed-module')
  23. const tk = require('timekeeper')
  24. const Errors = require('../../../../app/src/Features/Errors/Errors')
  25. const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
  26. const fixtures = {
  27. 'subscriptions/44f83d7cba354d5b84812419f923ea96':
  28. '<?xml version="1.0" encoding="UTF-8"?>' +
  29. '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
  30. ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
  31. ' <plan href="https://api.recurly.com/v2/plans/gold">' +
  32. ' <plan_code>gold</plan_code>' +
  33. ' <name>Gold plan</name>' +
  34. ' </plan>' +
  35. ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
  36. ' <state>active</state>' +
  37. ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
  38. ' <currency>EUR</currency>' +
  39. ' <quantity type="integer">1</quantity>' +
  40. ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
  41. ' <canceled_at nil="nil"></canceled_at>' +
  42. ' <expires_at nil="nil"></expires_at>' +
  43. ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
  44. ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
  45. ' <trial_started_at nil="nil"></trial_started_at>' +
  46. ' <trial_ends_at nil="nil"></trial_ends_at>' +
  47. ' <subscription_add_ons type="array">' +
  48. ' <subscription_add_on>' +
  49. ' <add_on_code>ipaddresses</add_on_code>' +
  50. ' <quantity>10</quantity>' +
  51. ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
  52. ' </subscription_add_on>' +
  53. ' </subscription_add_ons>' +
  54. ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
  55. ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
  56. ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
  57. '</subscription>',
  58. 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5':
  59. '<?xml version="1.0" encoding="UTF-8"?>' +
  60. '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
  61. ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
  62. ' <plan href="https://api.recurly.com/v2/plans/gold">' +
  63. ' <plan_code>gold</plan_code>' +
  64. ' <name>Gold plan</name>' +
  65. ' </plan>' +
  66. ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
  67. ' <state>active</state>' +
  68. ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
  69. ' <currency>EUR</currency>' +
  70. ' <quantity type="integer">1</quantity>' +
  71. ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
  72. ' <canceled_at nil="nil"></canceled_at>' +
  73. ' <expires_at nil="nil"></expires_at>' +
  74. ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
  75. ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
  76. ' <trial_started_at nil="nil"></trial_started_at>' +
  77. ' <trial_ends_at nil="nil"></trial_ends_at>' +
  78. ' <subscription_add_ons type="array">' +
  79. ' <subscription_add_on>' +
  80. ' <add_on_code>ipaddresses</add_on_code>' +
  81. ' <quantity>10</quantity>' +
  82. ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
  83. ' </subscription_add_on>' +
  84. ' </subscription_add_ons>' +
  85. ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
  86. ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
  87. ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
  88. '</subscription>',
  89. 'accounts/104':
  90. '<?xml version="1.0" encoding="UTF-8"?>' +
  91. '<account href="https://api.recurly.com/v2/accounts/104">' +
  92. ' <adjustments href="https://api.recurly.com/v2/accounts/1/adjustments"/>' +
  93. ' <billing_info href="https://api.recurly.com/v2/accounts/1/billing_info"/>' +
  94. ' <invoices href="https://api.recurly.com/v2/accounts/1/invoices"/>' +
  95. ' <redemption href="https://api.recurly.com/v2/accounts/1/redemption"/>' +
  96. ' <subscriptions href="https://api.recurly.com/v2/accounts/1/subscriptions"/>' +
  97. ' <transactions href="https://api.recurly.com/v2/accounts/1/transactions"/>' +
  98. ' <account_code>104</account_code>' +
  99. ' <state>active</state>' +
  100. ' <username nil="nil"></username>' +
  101. ' <email>verena@example.com</email>' +
  102. ' <first_name>Verena</first_name>' +
  103. ' <last_name>Example</last_name>' +
  104. ' <accept_language nil="nil"></accept_language>' +
  105. ' <hosted_login_token>a92468579e9c4231a6c0031c4716c01d</hosted_login_token>' +
  106. ' <created_at type="datetime">2011-10-25T12:00:00</created_at>' +
  107. '</account>'
  108. }
  109. const mockApiRequest = function(options, callback) {
  110. if (fixtures[options.url]) {
  111. return callback(null, { statusCode: 200 }, fixtures[options.url])
  112. } else {
  113. return callback(new Error('Not found'), { statusCode: 404 })
  114. }
  115. }
  116. describe('RecurlyWrapper', function() {
  117. beforeEach(function() {
  118. let RecurlyWrapper
  119. this.settings = {
  120. plans: [
  121. {
  122. planCode: 'collaborator',
  123. name: 'Collaborator',
  124. features: {
  125. collaborators: -1,
  126. versioning: true
  127. }
  128. }
  129. ],
  130. defaultPlanCode: {
  131. collaborators: 0,
  132. versioning: false
  133. },
  134. apis: {
  135. recurly: {
  136. apiKey: 'nonsense',
  137. privateKey: 'private_nonsense'
  138. }
  139. }
  140. }
  141. tk.freeze(Date.now()) // freeze the time for these tests
  142. return (this.RecurlyWrapper = RecurlyWrapper = SandboxedModule.require(
  143. modulePath,
  144. {
  145. globals: {
  146. console: console
  147. },
  148. requires: {
  149. 'settings-sharelatex': this.settings,
  150. 'logger-sharelatex': {
  151. err: sinon.stub(),
  152. error: sinon.stub(),
  153. warn: sinon.stub(),
  154. log: sinon.stub()
  155. },
  156. request: sinon.stub(),
  157. xml2js: require('xml2js'),
  158. './Errors': SubscriptionErrors,
  159. '../Errors/Errors': Errors
  160. }
  161. }
  162. ))
  163. })
  164. afterEach(function() {
  165. return tk.reset()
  166. })
  167. describe('getSubscription', function() {
  168. describe('with proper subscription id', function() {
  169. beforeEach(function() {
  170. this.apiRequest = sinon
  171. .stub(this.RecurlyWrapper, 'apiRequest')
  172. .callsFake(mockApiRequest)
  173. return this.RecurlyWrapper.getSubscription(
  174. '44f83d7cba354d5b84812419f923ea96',
  175. (error, recurlySubscription) => {
  176. return (this.recurlySubscription = recurlySubscription)
  177. }
  178. )
  179. })
  180. afterEach(function() {
  181. return this.RecurlyWrapper.apiRequest.restore()
  182. })
  183. it('should look up the subscription at the normal API end point', function() {
  184. return this.apiRequest.args[0][0].url.should.equal(
  185. 'subscriptions/44f83d7cba354d5b84812419f923ea96'
  186. )
  187. })
  188. it('should return the subscription', function() {
  189. return this.recurlySubscription.uuid.should.equal(
  190. '44f83d7cba354d5b84812419f923ea96'
  191. )
  192. })
  193. })
  194. describe('with ReculyJS token', function() {
  195. beforeEach(function() {
  196. this.apiRequest = sinon
  197. .stub(this.RecurlyWrapper, 'apiRequest')
  198. .callsFake(mockApiRequest)
  199. return this.RecurlyWrapper.getSubscription(
  200. '70db44b10f5f4b238669480c9903f6f5',
  201. { recurlyJsResult: true },
  202. (error, recurlySubscription) => {
  203. return (this.recurlySubscription = recurlySubscription)
  204. }
  205. )
  206. })
  207. afterEach(function() {
  208. return this.RecurlyWrapper.apiRequest.restore()
  209. })
  210. it('should return the subscription', function() {
  211. return this.recurlySubscription.uuid.should.equal(
  212. '44f83d7cba354d5b84812419f923ea96'
  213. )
  214. })
  215. it('should look up the subscription at the RecurlyJS API end point', function() {
  216. return this.apiRequest.args[0][0].url.should.equal(
  217. 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5'
  218. )
  219. })
  220. })
  221. describe('with includeAccount', function() {
  222. beforeEach(function() {
  223. this.apiRequest = sinon
  224. .stub(this.RecurlyWrapper, 'apiRequest')
  225. .callsFake(mockApiRequest)
  226. return this.RecurlyWrapper.getSubscription(
  227. '44f83d7cba354d5b84812419f923ea96',
  228. { includeAccount: true },
  229. (error, recurlySubscription) => {
  230. return (this.recurlySubscription = recurlySubscription)
  231. }
  232. )
  233. })
  234. afterEach(function() {
  235. return this.RecurlyWrapper.apiRequest.restore()
  236. })
  237. it('should request the account from the API', function() {
  238. return this.apiRequest.args[1][0].url.should.equal('accounts/104')
  239. })
  240. it('should populate the account attribute', function() {
  241. return this.recurlySubscription.account.account_code.should.equal('104')
  242. })
  243. })
  244. })
  245. describe('updateAccountEmailAddress', function() {
  246. beforeEach(function(done) {
  247. this.recurlyAccountId = 'account-id-123'
  248. this.newEmail = 'example@overleaf.com'
  249. this.apiRequest = sinon
  250. .stub(this.RecurlyWrapper, 'apiRequest')
  251. .callsFake((options, callback) => {
  252. this.requestOptions = options
  253. callback(null, {}, fixtures['accounts/104'])
  254. })
  255. this.RecurlyWrapper.updateAccountEmailAddress(
  256. this.recurlyAccountId,
  257. this.newEmail,
  258. (error, recurlyAccount) => {
  259. this.recurlyAccount = recurlyAccount
  260. done()
  261. }
  262. )
  263. })
  264. afterEach(function() {
  265. return this.RecurlyWrapper.apiRequest.restore()
  266. })
  267. it('sends correct XML', function() {
  268. this.apiRequest.called.should.equal(true)
  269. const { body } = this.apiRequest.lastCall.args[0]
  270. expect(body).to.equal(`\
  271. <account>
  272. <email>example@overleaf.com</email>
  273. </account>\
  274. `)
  275. this.requestOptions.url.should.equal(`accounts/${this.recurlyAccountId}`)
  276. this.requestOptions.method.should.equal('PUT')
  277. })
  278. it('should return the updated account', function() {
  279. should.exist(this.recurlyAccount)
  280. this.recurlyAccount.account_code.should.equal('104')
  281. })
  282. })
  283. describe('updateSubscription', function() {
  284. beforeEach(function(done) {
  285. this.recurlySubscriptionId = 'subscription-id-123'
  286. this.apiRequest = sinon
  287. .stub(this.RecurlyWrapper, 'apiRequest')
  288. .callsFake((options, callback) => {
  289. this.requestOptions = options
  290. return callback(
  291. null,
  292. {},
  293. fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96']
  294. )
  295. })
  296. return this.RecurlyWrapper.updateSubscription(
  297. this.recurlySubscriptionId,
  298. { plan_code: 'silver', timeframe: 'now' },
  299. (error, recurlySubscription) => {
  300. this.recurlySubscription = recurlySubscription
  301. return done()
  302. }
  303. )
  304. })
  305. afterEach(function() {
  306. return this.RecurlyWrapper.apiRequest.restore()
  307. })
  308. it('sends correct XML', function() {
  309. this.apiRequest.called.should.equal(true)
  310. const { body } = this.apiRequest.lastCall.args[0]
  311. expect(body).to.equal(`\
  312. <subscription>
  313. <plan_code>silver</plan_code>
  314. <timeframe>now</timeframe>
  315. </subscription>\
  316. `)
  317. this.requestOptions.url.should.equal(
  318. `subscriptions/${this.recurlySubscriptionId}`
  319. )
  320. return this.requestOptions.method.should.equal('put')
  321. })
  322. it('should return the updated subscription', function() {
  323. should.exist(this.recurlySubscription)
  324. return this.recurlySubscription.plan.plan_code.should.equal('gold')
  325. })
  326. })
  327. describe('cancelSubscription', function() {
  328. beforeEach(function(done) {
  329. this.recurlySubscriptionId = 'subscription-id-123'
  330. this.apiRequest = sinon
  331. .stub(this.RecurlyWrapper, 'apiRequest')
  332. .callsFake((options, callback) => {
  333. options.url.should.equal(
  334. `subscriptions/${this.recurlySubscriptionId}/cancel`
  335. )
  336. options.method.should.equal('put')
  337. return callback()
  338. })
  339. return this.RecurlyWrapper.cancelSubscription(
  340. this.recurlySubscriptionId,
  341. done
  342. )
  343. })
  344. afterEach(function() {
  345. return this.RecurlyWrapper.apiRequest.restore()
  346. })
  347. it('should send a cancel request to the API', function() {
  348. return this.apiRequest.called.should.equal(true)
  349. })
  350. describe('when the subscription is already cancelled', function() {
  351. beforeEach(function() {
  352. this.RecurlyWrapper.apiRequest.restore()
  353. this.recurlySubscriptionId = 'subscription-id-123'
  354. return (this.apiRequest = sinon
  355. .stub(this.RecurlyWrapper, 'apiRequest')
  356. .callsFake((options, callback) => {
  357. return callback(
  358. new Error('woops'),
  359. {},
  360. "<error><description>A canceled subscription can't transition to canceled</description></error>"
  361. )
  362. }))
  363. })
  364. it('should not produce an error', function(done) {
  365. return this.RecurlyWrapper.cancelSubscription(
  366. this.recurlySubscriptionId,
  367. err => {
  368. expect(err).to.equal(null)
  369. return done()
  370. }
  371. )
  372. })
  373. })
  374. })
  375. describe('reactivateSubscription', function() {
  376. beforeEach(function(done) {
  377. this.recurlySubscriptionId = 'subscription-id-123'
  378. this.apiRequest = sinon
  379. .stub(this.RecurlyWrapper, 'apiRequest')
  380. .callsFake((options, callback) => {
  381. options.url.should.equal(
  382. `subscriptions/${this.recurlySubscriptionId}/reactivate`
  383. )
  384. options.method.should.equal('put')
  385. return callback()
  386. })
  387. return this.RecurlyWrapper.reactivateSubscription(
  388. this.recurlySubscriptionId,
  389. done
  390. )
  391. })
  392. afterEach(function() {
  393. return this.RecurlyWrapper.apiRequest.restore()
  394. })
  395. it('should send a cancel request to the API', function() {
  396. return this.apiRequest.called.should.equal(true)
  397. })
  398. })
  399. describe('redeemCoupon', function() {
  400. beforeEach(function(done) {
  401. this.recurlyAccountId = 'account-id-123'
  402. this.coupon_code = '312321312'
  403. this.apiRequest = sinon
  404. .stub(this.RecurlyWrapper, 'apiRequest')
  405. .callsFake((options, callback) => {
  406. options.url.should.equal(`coupons/${this.coupon_code}/redeem`)
  407. options.method.should.equal('post')
  408. return callback()
  409. })
  410. return this.RecurlyWrapper.redeemCoupon(
  411. this.recurlyAccountId,
  412. this.coupon_code,
  413. done
  414. )
  415. })
  416. afterEach(function() {
  417. return this.RecurlyWrapper.apiRequest.restore()
  418. })
  419. it('sends correct XML', function() {
  420. this.apiRequest.called.should.equal(true)
  421. const { body } = this.apiRequest.lastCall.args[0]
  422. return expect(body).to.equal(`\
  423. <redemption>
  424. <account_code>account-id-123</account_code>
  425. <currency>USD</currency>
  426. </redemption>\
  427. `)
  428. })
  429. })
  430. describe('createFixedAmmountCoupon', function() {
  431. beforeEach(function(done) {
  432. this.couponCode = 'a-coupon-code'
  433. this.couponName = 'a-coupon-name'
  434. this.currencyCode = 'EUR'
  435. this.discount = 1337
  436. this.planCode = 'a-plan-code'
  437. this.apiRequest = sinon
  438. .stub(this.RecurlyWrapper, 'apiRequest')
  439. .callsFake((options, callback) => {
  440. return callback()
  441. })
  442. return this.RecurlyWrapper.createFixedAmmountCoupon(
  443. this.couponCode,
  444. this.couponName,
  445. this.currencyCode,
  446. this.discount,
  447. this.planCode,
  448. done
  449. )
  450. })
  451. afterEach(function() {
  452. return this.RecurlyWrapper.apiRequest.restore()
  453. })
  454. it('sends correct XML', function() {
  455. this.apiRequest.called.should.equal(true)
  456. const { body } = this.apiRequest.lastCall.args[0]
  457. return expect(body).to.equal(`\
  458. <coupon>
  459. <coupon_code>a-coupon-code</coupon_code>
  460. <name>a-coupon-name</name>
  461. <discount_type>dollars</discount_type>
  462. <discount_in_cents>
  463. <EUR>1337</EUR>
  464. </discount_in_cents>
  465. <plan_codes>
  466. <plan_code>a-plan-code</plan_code>
  467. </plan_codes>
  468. <applies_to_all_plans>false</applies_to_all_plans>
  469. </coupon>\
  470. `)
  471. })
  472. })
  473. describe('createSubscription', function() {
  474. beforeEach(function() {
  475. this.user = {
  476. _id: 'some_id',
  477. email: 'user@example.com'
  478. }
  479. this.subscriptionDetails = {
  480. currencyCode: 'EUR',
  481. plan_code: 'some_plan_code',
  482. coupon_code: '',
  483. isPaypal: true,
  484. address: {
  485. address1: 'addr_one',
  486. address2: 'addr_two',
  487. country: 'some_country',
  488. state: 'some_state',
  489. zip: 'some_zip'
  490. }
  491. }
  492. this.subscription = {}
  493. this.recurlyTokenIds = {
  494. billing: 'a-token-id',
  495. threeDSecureActionResult: 'a-3d-token-id'
  496. }
  497. return (this.call = callback => {
  498. return this.RecurlyWrapper.createSubscription(
  499. this.user,
  500. this.subscriptionDetails,
  501. this.recurlyTokenIds,
  502. callback
  503. )
  504. })
  505. })
  506. describe('when paypal', function() {
  507. beforeEach(function() {
  508. this.subscriptionDetails.isPaypal = true
  509. this._createPaypalSubscription = sinon.stub(
  510. this.RecurlyWrapper,
  511. '_createPaypalSubscription'
  512. )
  513. return this._createPaypalSubscription.callsArgWith(
  514. 3,
  515. null,
  516. this.subscription
  517. )
  518. })
  519. afterEach(function() {
  520. return this._createPaypalSubscription.restore()
  521. })
  522. it('should not produce an error', function(done) {
  523. return this.call((err, sub) => {
  524. expect(err).to.equal(null)
  525. expect(err).to.not.be.instanceof(Error)
  526. return done()
  527. })
  528. })
  529. it('should produce a subscription object', function(done) {
  530. return this.call((err, sub) => {
  531. expect(sub).to.deep.equal(this.subscription)
  532. return done()
  533. })
  534. })
  535. it('should call _createPaypalSubscription', function(done) {
  536. return this.call((err, sub) => {
  537. this._createPaypalSubscription.callCount.should.equal(1)
  538. return done()
  539. })
  540. })
  541. describe('when _createPaypalSubscription produces an error', function() {
  542. beforeEach(function() {
  543. return this._createPaypalSubscription.callsArgWith(
  544. 3,
  545. new Error('woops')
  546. )
  547. })
  548. it('should produce an error', function(done) {
  549. return this.call((err, sub) => {
  550. expect(err).to.be.instanceof(Error)
  551. return done()
  552. })
  553. })
  554. })
  555. })
  556. describe('when not paypal', function() {
  557. beforeEach(function() {
  558. this.subscriptionDetails.isPaypal = false
  559. this._createCreditCardSubscription = sinon.stub(
  560. this.RecurlyWrapper,
  561. '_createCreditCardSubscription'
  562. )
  563. return this._createCreditCardSubscription.callsArgWith(
  564. 3,
  565. null,
  566. this.subscription
  567. )
  568. })
  569. afterEach(function() {
  570. return this._createCreditCardSubscription.restore()
  571. })
  572. it('should not produce an error', function(done) {
  573. return this.call((err, sub) => {
  574. expect(err).to.equal(null)
  575. expect(err).to.not.be.instanceof(Error)
  576. return done()
  577. })
  578. })
  579. it('should produce a subscription object', function(done) {
  580. return this.call((err, sub) => {
  581. expect(sub).to.deep.equal(this.subscription)
  582. return done()
  583. })
  584. })
  585. it('should call _createCreditCardSubscription', function(done) {
  586. return this.call((err, sub) => {
  587. this._createCreditCardSubscription.callCount.should.equal(1)
  588. return done()
  589. })
  590. })
  591. describe('when _createCreditCardSubscription produces an error', function() {
  592. beforeEach(function() {
  593. return this._createCreditCardSubscription.callsArgWith(
  594. 3,
  595. new Error('woops')
  596. )
  597. })
  598. it('should produce an error', function(done) {
  599. return this.call((err, sub) => {
  600. expect(err).to.be.instanceof(Error)
  601. return done()
  602. })
  603. })
  604. })
  605. })
  606. })
  607. describe('_createCreditCardSubscription', function() {
  608. beforeEach(function() {
  609. this.user = {
  610. _id: 'some_id',
  611. email: 'user@example.com',
  612. first_name: 'Foo',
  613. last_name: 'Johnson'
  614. }
  615. this.subscriptionDetails = {
  616. currencyCode: 'EUR',
  617. plan_code: 'some_plan_code',
  618. coupon_code: '',
  619. isPaypal: true,
  620. first_name: 'Prairie',
  621. address: {
  622. address1: 'addr_one',
  623. address2: 'addr_two',
  624. country: 'some_country',
  625. state: 'some_state',
  626. zip: 'some_zip'
  627. },
  628. ITMCampaign: 'itm-campaign-value',
  629. ITMContent: 'itm-content-value'
  630. }
  631. this.subscription = {}
  632. this.recurlyTokenIds = {
  633. billing: 'a-token-id',
  634. threeDSecureActionResult: 'a-3d-token-id'
  635. }
  636. this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest')
  637. this.response = { statusCode: 200 }
  638. this.body = '<xml>is_bad</xml>'
  639. this.apiRequest.callsArgWith(1, null, this.response, this.body)
  640. this._parseSubscriptionXml = sinon.stub(
  641. this.RecurlyWrapper,
  642. '_parseSubscriptionXml'
  643. )
  644. this._parseSubscriptionXml.callsArgWith(1, null, this.subscription)
  645. return (this.call = callback => {
  646. return this.RecurlyWrapper._createCreditCardSubscription(
  647. this.user,
  648. this.subscriptionDetails,
  649. this.recurlyTokenIds,
  650. callback
  651. )
  652. })
  653. })
  654. afterEach(function() {
  655. this.apiRequest.restore()
  656. return this._parseSubscriptionXml.restore()
  657. })
  658. it('sends correct XML', function(done) {
  659. return this.call((err, result) => {
  660. const { body } = this.apiRequest.lastCall.args[0]
  661. expect(body).to.equal(`\
  662. <subscription>
  663. <plan_code>some_plan_code</plan_code>
  664. <currency>EUR</currency>
  665. <coupon_code/>
  666. <account>
  667. <account_code>some_id</account_code>
  668. <email>user@example.com</email>
  669. <first_name>Prairie</first_name>
  670. <last_name>Johnson</last_name>
  671. <billing_info>
  672. <token_id>a-token-id</token_id>
  673. <three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
  674. </billing_info>
  675. </account>
  676. <custom_fields>
  677. <custom_field>
  678. <name>itm_campaign</name>
  679. <value>itm-campaign-value</value>
  680. </custom_field>
  681. <custom_field>
  682. <name>itm_content</name>
  683. <value>itm-content-value</value>
  684. </custom_field>
  685. </custom_fields>
  686. </subscription>\
  687. `)
  688. return done()
  689. })
  690. })
  691. it('should not produce an error', function(done) {
  692. return this.call((err, sub) => {
  693. expect(err).to.not.be.instanceof(Error)
  694. expect(err).to.equal(null)
  695. return done()
  696. })
  697. })
  698. it('should produce a subscription', function(done) {
  699. return this.call((err, sub) => {
  700. expect(sub).to.equal(this.subscription)
  701. return done()
  702. })
  703. })
  704. it('should call apiRequest', function(done) {
  705. return this.call((err, sub) => {
  706. this.apiRequest.callCount.should.equal(1)
  707. return done()
  708. })
  709. })
  710. it('should call _parseSubscriptionXml', function(done) {
  711. return this.call((err, sub) => {
  712. this._parseSubscriptionXml.callCount.should.equal(1)
  713. return done()
  714. })
  715. })
  716. describe('when api request returns 422', function() {
  717. beforeEach(function() {
  718. const body = `\
  719. <?xml version="1.0" encoding="UTF-8"?>
  720. <errors>
  721. <transaction_error>
  722. <error_code>three_d_secure_action_required</error_code>
  723. <error_category>3d_secure_action_required</error_category>
  724. <merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
  725. <customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
  726. <gateway_error_code nil="nil"></gateway_error_code>
  727. <three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
  728. </transaction_error>
  729. <error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
  730. </errors>
  731. `
  732. this.apiRequest.yields(null, { statusCode: 422 }, body)
  733. })
  734. it('should produce an error', function(done) {
  735. return this.call((err, sub) => {
  736. expect(err).to.be.instanceof(
  737. SubscriptionErrors.RecurlyTransactionError
  738. )
  739. expect(err.info.public.message).to.be.equal(
  740. 'Your card must be authenticated with 3D Secure before continuing.'
  741. )
  742. expect(err.info.public.threeDSecureActionTokenId).to.be.equal(
  743. 'mock_three_d_secure_action_token'
  744. )
  745. return done()
  746. })
  747. })
  748. })
  749. describe('when api request produces an error', function() {
  750. beforeEach(function() {
  751. return this.apiRequest.callsArgWith(1, new Error('woops'))
  752. })
  753. it('should produce an error', function(done) {
  754. return this.call((err, sub) => {
  755. expect(err).to.be.instanceof(Error)
  756. return done()
  757. })
  758. })
  759. it('should call apiRequest', function(done) {
  760. return this.call((err, sub) => {
  761. this.apiRequest.callCount.should.equal(1)
  762. return done()
  763. })
  764. })
  765. it('should not _parseSubscriptionXml', function(done) {
  766. return this.call((err, sub) => {
  767. this._parseSubscriptionXml.callCount.should.equal(0)
  768. return done()
  769. })
  770. })
  771. })
  772. describe('when parse xml produces an error', function() {
  773. beforeEach(function() {
  774. return this._parseSubscriptionXml.callsArgWith(1, new Error('woops'))
  775. })
  776. it('should produce an error', function(done) {
  777. return this.call((err, sub) => {
  778. expect(err).to.be.instanceof(Error)
  779. return done()
  780. })
  781. })
  782. })
  783. })
  784. describe('_createPaypalSubscription', function() {
  785. beforeEach(function() {
  786. this.checkAccountExists = sinon.stub(
  787. this.RecurlyWrapper._paypal,
  788. 'checkAccountExists'
  789. )
  790. this.createAccount = sinon.stub(
  791. this.RecurlyWrapper._paypal,
  792. 'createAccount'
  793. )
  794. this.createBillingInfo = sinon.stub(
  795. this.RecurlyWrapper._paypal,
  796. 'createBillingInfo'
  797. )
  798. this.setAddressAndCompanyBillingInfo = sinon.stub(
  799. this.RecurlyWrapper._paypal,
  800. 'setAddressAndCompanyBillingInfo'
  801. )
  802. this.createSubscription = sinon.stub(
  803. this.RecurlyWrapper._paypal,
  804. 'createSubscription'
  805. )
  806. this.user = {
  807. _id: 'some_id',
  808. email: 'user@example.com'
  809. }
  810. this.subscriptionDetails = {
  811. currencyCode: 'EUR',
  812. plan_code: 'some_plan_code',
  813. coupon_code: '',
  814. isPaypal: true,
  815. address: {
  816. address1: 'addr_one',
  817. address2: 'addr_two',
  818. country: 'some_country',
  819. state: 'some_state',
  820. zip: 'some_zip'
  821. }
  822. }
  823. this.subscription = {}
  824. this.recurlyTokenIds = {
  825. billing: 'a-token-id',
  826. threeDSecureActionResult: 'a-3d-token-id'
  827. }
  828. // set up data callbacks
  829. const { user } = this
  830. const { subscriptionDetails } = this
  831. const { recurlyTokenIds } = this
  832. this.checkAccountExists.callsArgWith(1, null, {
  833. user,
  834. subscriptionDetails,
  835. recurlyTokenIds,
  836. userExists: false,
  837. account: { accountCode: 'xx' }
  838. })
  839. this.createAccount.callsArgWith(1, null, {
  840. user,
  841. subscriptionDetails,
  842. recurlyTokenIds,
  843. userExists: false,
  844. account: { accountCode: 'xx' }
  845. })
  846. this.createBillingInfo.callsArgWith(1, null, {
  847. user,
  848. subscriptionDetails,
  849. recurlyTokenIds,
  850. userExists: false,
  851. account: { accountCode: 'xx' },
  852. billingInfo: { token_id: 'abc' }
  853. })
  854. this.setAddressAndCompanyBillingInfo.callsArgWith(1, null, {
  855. user,
  856. subscriptionDetails,
  857. recurlyTokenIds,
  858. userExists: false,
  859. account: { accountCode: 'xx' },
  860. billingInfo: { token_id: 'abc' }
  861. })
  862. this.createSubscription.callsArgWith(1, null, {
  863. user,
  864. subscriptionDetails,
  865. recurlyTokenIds,
  866. userExists: false,
  867. account: { accountCode: 'xx' },
  868. billingInfo: { token_id: 'abc' },
  869. subscription: this.subscription
  870. })
  871. return (this.call = callback => {
  872. return this.RecurlyWrapper._createPaypalSubscription(
  873. this.user,
  874. this.subscriptionDetails,
  875. this.recurlyTokenIds,
  876. callback
  877. )
  878. })
  879. })
  880. afterEach(function() {
  881. this.checkAccountExists.restore()
  882. this.createAccount.restore()
  883. this.createBillingInfo.restore()
  884. this.setAddressAndCompanyBillingInfo.restore()
  885. return this.createSubscription.restore()
  886. })
  887. it('should not produce an error', function(done) {
  888. return this.call((err, sub) => {
  889. expect(err).to.not.be.instanceof(Error)
  890. return done()
  891. })
  892. })
  893. it('should produce a subscription object', function(done) {
  894. return this.call((err, sub) => {
  895. expect(sub).to.not.equal(null)
  896. expect(sub).to.equal(this.subscription)
  897. return done()
  898. })
  899. })
  900. it('should call each of the paypal stages', function(done) {
  901. return this.call((err, sub) => {
  902. this.checkAccountExists.callCount.should.equal(1)
  903. this.createAccount.callCount.should.equal(1)
  904. this.createBillingInfo.callCount.should.equal(1)
  905. this.setAddressAndCompanyBillingInfo.callCount.should.equal(1)
  906. this.createSubscription.callCount.should.equal(1)
  907. return done()
  908. })
  909. })
  910. describe('when one of the paypal stages produces an error', function() {
  911. beforeEach(function() {
  912. return this.createAccount.callsArgWith(1, new Error('woops'))
  913. })
  914. it('should produce an error', function(done) {
  915. return this.call((err, sub) => {
  916. expect(err).to.be.instanceof(Error)
  917. return done()
  918. })
  919. })
  920. it('should stop calling the paypal stages after the error', function(done) {
  921. return this.call((err, sub) => {
  922. this.checkAccountExists.callCount.should.equal(1)
  923. this.createAccount.callCount.should.equal(1)
  924. this.createBillingInfo.callCount.should.equal(0)
  925. this.setAddressAndCompanyBillingInfo.callCount.should.equal(0)
  926. this.createSubscription.callCount.should.equal(0)
  927. return done()
  928. })
  929. })
  930. })
  931. })
  932. describe('paypal actions', function() {
  933. beforeEach(function() {
  934. this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest')
  935. this._parseAccountXml = sinon.spy(this.RecurlyWrapper, '_parseAccountXml')
  936. this._parseBillingInfoXml = sinon.spy(
  937. this.RecurlyWrapper,
  938. '_parseBillingInfoXml'
  939. )
  940. this._parseSubscriptionXml = sinon.spy(
  941. this.RecurlyWrapper,
  942. '_parseSubscriptionXml'
  943. )
  944. return (this.cache = {
  945. user: (this.user = {
  946. _id: 'some_id',
  947. email: 'foo@bar.com',
  948. first_name: 'Foo',
  949. last_name: 'Bar'
  950. }),
  951. recurlyTokenIds: (this.recurlyTokenIds = {
  952. billing: 'a-token-id',
  953. threeDSecureActionResult: 'a-3d-token-id'
  954. }),
  955. subscriptionDetails: (this.subscriptionDetails = {
  956. currencyCode: 'EUR',
  957. plan_code: 'some_plan_code',
  958. coupon_code: '',
  959. isPaypal: true,
  960. address: {
  961. address1: 'addr_one',
  962. address2: 'addr_two',
  963. city: 'some_city',
  964. country: 'some_country',
  965. state: 'some_state',
  966. zip: 'some_zip'
  967. },
  968. ITMCampaign: 'itm-campaign-value',
  969. ITMContent: 'itm-content-value'
  970. })
  971. })
  972. })
  973. afterEach(function() {
  974. this.apiRequest.restore()
  975. this._parseAccountXml.restore()
  976. this._parseBillingInfoXml.restore()
  977. return this._parseSubscriptionXml.restore()
  978. })
  979. describe('_paypal.checkAccountExists', function() {
  980. beforeEach(function() {
  981. return (this.call = callback => {
  982. return this.RecurlyWrapper._paypal.checkAccountExists(
  983. this.cache,
  984. callback
  985. )
  986. })
  987. })
  988. describe('when the account exists', function() {
  989. beforeEach(function() {
  990. const resultXml =
  991. '<account><account_code>abc</account_code></account>'
  992. return this.apiRequest.callsArgWith(
  993. 1,
  994. null,
  995. { statusCode: 200 },
  996. resultXml
  997. )
  998. })
  999. it('should not produce an error', function(done) {
  1000. return this.call((err, result) => {
  1001. expect(err).to.not.be.instanceof(Error)
  1002. return done()
  1003. })
  1004. })
  1005. it('should call apiRequest', function(done) {
  1006. return this.call((err, result) => {
  1007. this.apiRequest.callCount.should.equal(1)
  1008. return done()
  1009. })
  1010. })
  1011. it('should call _parseAccountXml', function(done) {
  1012. return this.call((err, result) => {
  1013. this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1)
  1014. return done()
  1015. })
  1016. })
  1017. it('should add the account to the cumulative result', function(done) {
  1018. return this.call((err, result) => {
  1019. expect(result.account).to.not.equal(null)
  1020. expect(result.account).to.not.equal(undefined)
  1021. expect(result.account).to.deep.equal({
  1022. account_code: 'abc'
  1023. })
  1024. return done()
  1025. })
  1026. })
  1027. it('should set userExists to true', function(done) {
  1028. return this.call((err, result) => {
  1029. expect(result.userExists).to.equal(true)
  1030. return done()
  1031. })
  1032. })
  1033. })
  1034. describe('when the account does not exist', function() {
  1035. beforeEach(function() {
  1036. return this.apiRequest.callsArgWith(1, null, { statusCode: 404 }, '')
  1037. })
  1038. it('should not produce an error', function(done) {
  1039. return this.call((err, result) => {
  1040. expect(err).to.not.be.instanceof(Error)
  1041. return done()
  1042. })
  1043. })
  1044. it('should call apiRequest', function(done) {
  1045. return this.call((err, result) => {
  1046. this.apiRequest.callCount.should.equal(1)
  1047. this.apiRequest.firstCall.args[0].method.should.equal('GET')
  1048. return done()
  1049. })
  1050. })
  1051. it('should not call _parseAccountXml', function(done) {
  1052. return this.call((err, result) => {
  1053. this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0)
  1054. return done()
  1055. })
  1056. })
  1057. it('should not add the account to result', function(done) {
  1058. return this.call((err, result) => {
  1059. expect(result.account).to.equal(undefined)
  1060. return done()
  1061. })
  1062. })
  1063. it('should set userExists to false', function(done) {
  1064. return this.call((err, result) => {
  1065. expect(result.userExists).to.equal(false)
  1066. return done()
  1067. })
  1068. })
  1069. })
  1070. describe('when apiRequest produces an error', function() {
  1071. beforeEach(function() {
  1072. return this.apiRequest.callsArgWith(1, new Error('woops'), {
  1073. statusCode: 500
  1074. })
  1075. })
  1076. it('should produce an error', function(done) {
  1077. return this.call((err, result) => {
  1078. expect(err).to.be.instanceof(Error)
  1079. return done()
  1080. })
  1081. })
  1082. })
  1083. })
  1084. describe('_paypal.createAccount', function() {
  1085. beforeEach(function() {
  1086. return (this.call = callback => {
  1087. return this.RecurlyWrapper._paypal.createAccount(this.cache, callback)
  1088. })
  1089. })
  1090. describe('when address is missing from subscriptionDetails', function() {
  1091. beforeEach(function() {
  1092. return (this.cache.subscriptionDetails.address = null)
  1093. })
  1094. it('should produce an error', function(done) {
  1095. return this.call((err, result) => {
  1096. expect(err).to.be.instanceof(Error)
  1097. return done()
  1098. })
  1099. })
  1100. })
  1101. describe('when country is missing from address', function() {
  1102. beforeEach(function() {
  1103. return (this.cache.subscriptionDetails.address = {})
  1104. })
  1105. it('should produce an error', function(done) {
  1106. return this.call((err, result) => {
  1107. expect(err).to.be.instanceof(Errors.InvalidError)
  1108. return done()
  1109. })
  1110. })
  1111. })
  1112. describe('when account already exists', function() {
  1113. beforeEach(function() {
  1114. this.cache.userExists = true
  1115. return (this.cache.account = { account_code: 'abc' })
  1116. })
  1117. it('should not produce an error', function(done) {
  1118. return this.call((err, result) => {
  1119. expect(err).to.not.be.instanceof(Error)
  1120. return done()
  1121. })
  1122. })
  1123. it('should produce cache object', function(done) {
  1124. return this.call((err, result) => {
  1125. expect(result).to.deep.equal(this.cache)
  1126. expect(result.account).to.deep.equal({
  1127. account_code: 'abc'
  1128. })
  1129. return done()
  1130. })
  1131. })
  1132. it('should not call apiRequest', function(done) {
  1133. return this.call((err, result) => {
  1134. this.apiRequest.callCount.should.equal(0)
  1135. return done()
  1136. })
  1137. })
  1138. it('should not call _parseAccountXml', function(done) {
  1139. return this.call((err, result) => {
  1140. this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0)
  1141. return done()
  1142. })
  1143. })
  1144. })
  1145. describe('when account does not exist', function() {
  1146. beforeEach(function() {
  1147. this.cache.userExists = false
  1148. const resultXml =
  1149. '<account><account_code>abc</account_code></account>'
  1150. return this.apiRequest.callsArgWith(
  1151. 1,
  1152. null,
  1153. { statusCode: 200 },
  1154. resultXml
  1155. )
  1156. })
  1157. it('sends correct XML', function(done) {
  1158. return this.call((err, result) => {
  1159. const { body } = this.apiRequest.lastCall.args[0]
  1160. expect(body).to.equal(`\
  1161. <account>
  1162. <account_code>some_id</account_code>
  1163. <email>foo@bar.com</email>
  1164. <first_name>Foo</first_name>
  1165. <last_name>Bar</last_name>
  1166. <address>
  1167. <address1>addr_one</address1>
  1168. <address2>addr_two</address2>
  1169. <city>some_city</city>
  1170. <state>some_state</state>
  1171. <zip>some_zip</zip>
  1172. <country>some_country</country>
  1173. </address>
  1174. </account>\
  1175. `)
  1176. return done()
  1177. })
  1178. })
  1179. it('should not produce an error', function(done) {
  1180. return this.call((err, result) => {
  1181. expect(err).to.not.be.instanceof(Error)
  1182. return done()
  1183. })
  1184. })
  1185. it('should call apiRequest', function(done) {
  1186. return this.call((err, result) => {
  1187. this.apiRequest.callCount.should.equal(1)
  1188. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1189. return done()
  1190. })
  1191. })
  1192. it('should call _parseAccountXml', function(done) {
  1193. return this.call((err, result) => {
  1194. this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1)
  1195. return done()
  1196. })
  1197. })
  1198. describe('when apiRequest produces an error', function() {
  1199. beforeEach(function() {
  1200. return this.apiRequest.callsArgWith(1, new Error('woops'), {
  1201. statusCode: 500
  1202. })
  1203. })
  1204. it('should produce an error', function(done) {
  1205. return this.call((err, result) => {
  1206. expect(err).to.be.instanceof(Error)
  1207. return done()
  1208. })
  1209. })
  1210. })
  1211. })
  1212. })
  1213. describe('_paypal.createBillingInfo', function() {
  1214. beforeEach(function() {
  1215. this.cache.account = { account_code: 'abc' }
  1216. return (this.call = callback => {
  1217. return this.RecurlyWrapper._paypal.createBillingInfo(
  1218. this.cache,
  1219. callback
  1220. )
  1221. })
  1222. })
  1223. describe('when account_code is missing from cache', function() {
  1224. beforeEach(function() {
  1225. return (this.cache.account.account_code = null)
  1226. })
  1227. it('should produce an error', function(done) {
  1228. return this.call((err, result) => {
  1229. expect(err).to.be.instanceof(Error)
  1230. return done()
  1231. })
  1232. })
  1233. })
  1234. describe('when all goes well', function() {
  1235. beforeEach(function() {
  1236. const resultXml = '<billing_info><a>1</a></billing_info>'
  1237. return this.apiRequest.callsArgWith(
  1238. 1,
  1239. null,
  1240. { statusCode: 200 },
  1241. resultXml
  1242. )
  1243. })
  1244. it('sends correct XML', function(done) {
  1245. return this.call((err, result) => {
  1246. const { body } = this.apiRequest.lastCall.args[0]
  1247. expect(body).to.equal(`\
  1248. <billing_info>
  1249. <token_id>a-token-id</token_id>
  1250. </billing_info>\
  1251. `)
  1252. return done()
  1253. })
  1254. })
  1255. it('should not produce an error', function(done) {
  1256. return this.call((err, result) => {
  1257. expect(err).to.not.be.instanceof(Error)
  1258. return done()
  1259. })
  1260. })
  1261. it('should call apiRequest', function(done) {
  1262. return this.call((err, result) => {
  1263. this.apiRequest.callCount.should.equal(1)
  1264. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1265. return done()
  1266. })
  1267. })
  1268. it('should call _parseBillingInfoXml', function(done) {
  1269. return this.call((err, result) => {
  1270. this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1)
  1271. return done()
  1272. })
  1273. })
  1274. it('should set billingInfo on cache', function(done) {
  1275. return this.call((err, result) => {
  1276. expect(result.billingInfo).to.deep.equal({
  1277. a: '1'
  1278. })
  1279. return done()
  1280. })
  1281. })
  1282. })
  1283. describe('when apiRequest produces an error', function() {
  1284. beforeEach(function() {
  1285. return this.apiRequest.callsArgWith(1, new Error('woops'), {
  1286. statusCode: 500
  1287. })
  1288. })
  1289. it('should produce an error', function(done) {
  1290. return this.call((err, result) => {
  1291. expect(err).to.be.instanceof(Error)
  1292. return done()
  1293. })
  1294. })
  1295. })
  1296. })
  1297. describe('_paypal.setAddressAndCompanyBillingInfo', function() {
  1298. beforeEach(function() {
  1299. this.cache.account = { account_code: 'abc' }
  1300. this.cache.billingInfo = {}
  1301. return (this.call = callback => {
  1302. return this.RecurlyWrapper._paypal.setAddressAndCompanyBillingInfo(
  1303. this.cache,
  1304. callback
  1305. )
  1306. })
  1307. })
  1308. describe('when account_code is missing from cache', function() {
  1309. beforeEach(function() {
  1310. return (this.cache.account.account_code = null)
  1311. })
  1312. it('should produce an error', function(done) {
  1313. return this.call((err, result) => {
  1314. expect(err).to.be.instanceof(Error)
  1315. return done()
  1316. })
  1317. })
  1318. })
  1319. describe('when country is missing', function() {
  1320. beforeEach(function() {
  1321. return (this.cache.subscriptionDetails.address = { country: '' })
  1322. })
  1323. it('should produce an error', function(done) {
  1324. return this.call((err, result) => {
  1325. expect(err).to.be.instanceof(Errors.InvalidError)
  1326. return done()
  1327. })
  1328. })
  1329. })
  1330. describe('when all goes well', function() {
  1331. beforeEach(function() {
  1332. const resultXml = '<billing_info><city>London</city></billing_info>'
  1333. return this.apiRequest.callsArgWith(
  1334. 1,
  1335. null,
  1336. { statusCode: 200 },
  1337. resultXml
  1338. )
  1339. })
  1340. it('sends correct XML', function(done) {
  1341. return this.call((err, result) => {
  1342. const { body } = this.apiRequest.lastCall.args[0]
  1343. expect(body).to.equal(`\
  1344. <billing_info>
  1345. <address1>addr_one</address1>
  1346. <address2>addr_two</address2>
  1347. <city>some_city</city>
  1348. <state>some_state</state>
  1349. <zip>some_zip</zip>
  1350. <country>some_country</country>
  1351. </billing_info>\
  1352. `)
  1353. return done()
  1354. })
  1355. })
  1356. it('should not produce an error', function(done) {
  1357. return this.call((err, result) => {
  1358. expect(err).to.not.be.instanceof(Error)
  1359. return done()
  1360. })
  1361. })
  1362. it('should call apiRequest', function(done) {
  1363. return this.call((err, result) => {
  1364. this.apiRequest.callCount.should.equal(1)
  1365. this.apiRequest.firstCall.args[0].method.should.equal('PUT')
  1366. return done()
  1367. })
  1368. })
  1369. it('should call _parseBillingInfoXml', function(done) {
  1370. return this.call((err, result) => {
  1371. this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1)
  1372. return done()
  1373. })
  1374. })
  1375. it('should set billingInfo on cache', function(done) {
  1376. return this.call((err, result) => {
  1377. expect(result.billingInfo).to.deep.equal({
  1378. city: 'London'
  1379. })
  1380. return done()
  1381. })
  1382. })
  1383. })
  1384. describe('when apiRequest produces an error', function() {
  1385. beforeEach(function() {
  1386. return this.apiRequest.callsArgWith(1, new Error('woops'), {
  1387. statusCode: 500
  1388. })
  1389. })
  1390. it('should produce an error', function(done) {
  1391. return this.call((err, result) => {
  1392. expect(err).to.be.instanceof(Error)
  1393. return done()
  1394. })
  1395. })
  1396. })
  1397. })
  1398. describe('_paypal.createSubscription', function() {
  1399. beforeEach(function() {
  1400. this.cache.account = { account_code: 'abc' }
  1401. this.cache.billingInfo = {}
  1402. return (this.call = callback => {
  1403. return this.RecurlyWrapper._paypal.createSubscription(
  1404. this.cache,
  1405. callback
  1406. )
  1407. })
  1408. })
  1409. describe('when all goes well', function() {
  1410. beforeEach(function() {
  1411. const resultXml = '<subscription><a>1</a></subscription>'
  1412. return this.apiRequest.callsArgWith(
  1413. 1,
  1414. null,
  1415. { statusCode: 200 },
  1416. resultXml
  1417. )
  1418. })
  1419. it('sends correct XML', function(done) {
  1420. return this.call((err, result) => {
  1421. const { body } = this.apiRequest.lastCall.args[0]
  1422. expect(body).to.equal(`\
  1423. <subscription>
  1424. <plan_code>some_plan_code</plan_code>
  1425. <currency>EUR</currency>
  1426. <coupon_code/>
  1427. <account>
  1428. <account_code>some_id</account_code>
  1429. </account>
  1430. <custom_fields>
  1431. <custom_field>
  1432. <name>itm_campaign</name>
  1433. <value>itm-campaign-value</value>
  1434. </custom_field>
  1435. <custom_field>
  1436. <name>itm_content</name>
  1437. <value>itm-content-value</value>
  1438. </custom_field>
  1439. </custom_fields>
  1440. </subscription>\
  1441. `)
  1442. return done()
  1443. })
  1444. })
  1445. it('should not produce an error', function(done) {
  1446. return this.call((err, result) => {
  1447. expect(err).to.not.be.instanceof(Error)
  1448. return done()
  1449. })
  1450. })
  1451. it('should call apiRequest', function(done) {
  1452. return this.call((err, result) => {
  1453. this.apiRequest.callCount.should.equal(1)
  1454. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1455. return done()
  1456. })
  1457. })
  1458. it('should call _parseSubscriptionXml', function(done) {
  1459. return this.call((err, result) => {
  1460. this.RecurlyWrapper._parseSubscriptionXml.callCount.should.equal(1)
  1461. return done()
  1462. })
  1463. })
  1464. it('should set subscription on cache', function(done) {
  1465. return this.call((err, result) => {
  1466. expect(result.subscription).to.deep.equal({
  1467. a: '1'
  1468. })
  1469. return done()
  1470. })
  1471. })
  1472. })
  1473. describe('when apiRequest produces an error', function() {
  1474. beforeEach(function() {
  1475. return this.apiRequest.callsArgWith(1, new Error('woops'), {
  1476. statusCode: 500
  1477. })
  1478. })
  1479. it('should produce an error', function(done) {
  1480. return this.call((err, result) => {
  1481. expect(err).to.be.instanceof(Error)
  1482. return done()
  1483. })
  1484. })
  1485. })
  1486. })
  1487. })
  1488. describe('listAccountActiveSubscriptions', function() {
  1489. beforeEach(function() {
  1490. this.user_id = 'mock-user-id'
  1491. this.callback = sinon.stub()
  1492. this.RecurlyWrapper.apiRequest = sinon
  1493. .stub()
  1494. .yields(
  1495. null,
  1496. (this.response = { mock: 'response' }),
  1497. (this.body = '<mock body/>')
  1498. )
  1499. return (this.RecurlyWrapper._parseSubscriptionsXml = sinon
  1500. .stub()
  1501. .yields(null, (this.subscriptions = ['mock', 'subscriptions'])))
  1502. })
  1503. describe('with an account', function() {
  1504. beforeEach(function() {
  1505. return this.RecurlyWrapper.listAccountActiveSubscriptions(
  1506. this.user_id,
  1507. this.callback
  1508. )
  1509. })
  1510. it('should send a request to Recurly', function() {
  1511. return this.RecurlyWrapper.apiRequest
  1512. .calledWith({
  1513. url: `accounts/${this.user_id}/subscriptions`,
  1514. qs: {
  1515. state: 'active'
  1516. },
  1517. expect404: true
  1518. })
  1519. .should.equal(true)
  1520. })
  1521. it('should return the subscriptions', function() {
  1522. return this.callback
  1523. .calledWith(null, this.subscriptions)
  1524. .should.equal(true)
  1525. })
  1526. })
  1527. describe('without an account', function() {
  1528. beforeEach(function() {
  1529. this.response.statusCode = 404
  1530. return this.RecurlyWrapper.listAccountActiveSubscriptions(
  1531. this.user_id,
  1532. this.callback
  1533. )
  1534. })
  1535. it('should return an empty array of subscriptions', function() {
  1536. return this.callback.calledWith(null, []).should.equal(true)
  1537. })
  1538. })
  1539. })
  1540. })