SubscriptionHandlerTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /* eslint-disable
  2. max-len,
  3. no-return-assign,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const SandboxedModule = require('sandboxed-module')
  14. const should = require('chai').should()
  15. const sinon = require('sinon')
  16. const querystring = require('querystring')
  17. const modulePath =
  18. '../../../../app/src/Features/Subscription/SubscriptionHandler'
  19. const mockRecurlySubscriptions = {
  20. 'subscription-123-active': {
  21. uuid: 'subscription-123-active',
  22. plan: {
  23. name: 'Gold',
  24. plan_code: 'gold'
  25. },
  26. current_period_ends_at: new Date(),
  27. state: 'active',
  28. unit_amount_in_cents: 999,
  29. account: {
  30. account_code: 'user-123'
  31. }
  32. }
  33. }
  34. describe('SubscriptionHandler', function() {
  35. beforeEach(function() {
  36. this.Settings = {
  37. plans: [
  38. {
  39. planCode: 'collaborator',
  40. name: 'Collaborator',
  41. features: {
  42. collaborators: -1,
  43. versioning: true
  44. }
  45. }
  46. ],
  47. defaultPlanCode: {
  48. collaborators: 0,
  49. versioning: false
  50. }
  51. }
  52. this.activeRecurlySubscription =
  53. mockRecurlySubscriptions['subscription-123-active']
  54. this.User = {}
  55. this.user = { _id: (this.user_id = 'user_id_here_') }
  56. this.subscription = {
  57. recurlySubscription_id: this.activeRecurlySubscription.uuid
  58. }
  59. this.RecurlyWrapper = {
  60. getSubscription: sinon
  61. .stub()
  62. .callsArgWith(2, null, this.activeRecurlySubscription),
  63. updateSubscription: sinon
  64. .stub()
  65. .callsArgWith(2, null, this.activeRecurlySubscription),
  66. cancelSubscription: sinon.stub().callsArgWith(1),
  67. reactivateSubscription: sinon.stub().callsArgWith(1),
  68. redeemCoupon: sinon.stub().callsArgWith(2),
  69. createSubscription: sinon
  70. .stub()
  71. .callsArgWith(3, null, this.activeRecurlySubscription),
  72. getBillingInfo: sinon.stub().yields(),
  73. getAccountPastDueInvoices: sinon.stub().yields(),
  74. attemptInvoiceCollection: sinon.stub().yields()
  75. }
  76. this.DropboxHandler = { unlinkAccount: sinon.stub().callsArgWith(1) }
  77. this.SubscriptionUpdater = {
  78. syncSubscription: sinon.stub().yields(),
  79. startFreeTrial: sinon.stub().callsArgWith(1)
  80. }
  81. this.LimitationsManager = { userHasV2Subscription: sinon.stub() }
  82. this.EmailHandler = { sendEmail: sinon.stub() }
  83. this.AnalyticsManager = { recordEvent: sinon.stub() }
  84. this.SubscriptionHandler = SandboxedModule.require(modulePath, {
  85. globals: {
  86. console: console
  87. },
  88. requires: {
  89. './RecurlyWrapper': this.RecurlyWrapper,
  90. 'settings-sharelatex': this.Settings,
  91. '../../models/User': {
  92. User: this.User
  93. },
  94. './SubscriptionUpdater': this.SubscriptionUpdater,
  95. 'logger-sharelatex': { log() {} },
  96. './LimitationsManager': this.LimitationsManager,
  97. '../Email/EmailHandler': this.EmailHandler,
  98. '../Dropbox/DropboxHandler': this.DropboxHandler,
  99. '../../infrastructure/Events': (this.Events = { emit: sinon.stub() }),
  100. '../Analytics/AnalyticsManager': this.AnalyticsManager
  101. }
  102. })
  103. return (this.SubscriptionHandler.syncSubscriptionToUser = sinon
  104. .stub()
  105. .callsArgWith(2))
  106. })
  107. describe('createSubscription', function() {
  108. beforeEach(function() {
  109. this.callback = sinon.stub()
  110. this.subscriptionDetails = {
  111. cvv: '123',
  112. number: '12345'
  113. }
  114. this.recurlyTokenIds = { billing: '45555666' }
  115. return (this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  116. .stub()
  117. .yields(null, true))
  118. })
  119. describe('successfully', function() {
  120. beforeEach(function() {
  121. return this.SubscriptionHandler.createSubscription(
  122. this.user,
  123. this.subscriptionDetails,
  124. this.recurlyTokenIds,
  125. this.callback
  126. )
  127. })
  128. it('should create the subscription with the wrapper', function() {
  129. return this.RecurlyWrapper.createSubscription
  130. .calledWith(this.user, this.subscriptionDetails, this.recurlyTokenIds)
  131. .should.equal(true)
  132. })
  133. it('should sync the subscription to the user', function() {
  134. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
  135. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  136. this.activeRecurlySubscription
  137. )
  138. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  139. this.user._id
  140. )
  141. })
  142. })
  143. describe('when there is already a subscription in Recurly', function() {
  144. beforeEach(function() {
  145. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  146. .stub()
  147. .yields(null, false)
  148. return this.SubscriptionHandler.createSubscription(
  149. this.user,
  150. this.subscriptionDetails,
  151. this.recurlyTokenIds,
  152. this.callback
  153. )
  154. })
  155. it('should return an error', function() {
  156. return this.callback.calledWith(
  157. new Error('user already has subscription in recurly')
  158. )
  159. })
  160. })
  161. })
  162. describe('updateSubscription', function() {
  163. describe('with a user with a subscription', function() {
  164. describe('with a valid plan code', function() {
  165. beforeEach(function(done) {
  166. this.plan_code = 'collaborator'
  167. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  168. 1,
  169. null,
  170. true,
  171. this.subscription
  172. )
  173. return this.SubscriptionHandler.updateSubscription(
  174. this.user,
  175. this.plan_code,
  176. null,
  177. done
  178. )
  179. })
  180. it('should update the subscription', function() {
  181. this.RecurlyWrapper.updateSubscription
  182. .calledWith(this.subscription.recurlySubscription_id)
  183. .should.equal(true)
  184. const updateOptions = this.RecurlyWrapper.updateSubscription
  185. .args[0][1]
  186. return updateOptions.plan_code.should.equal(this.plan_code)
  187. })
  188. it('should update immediately', function() {
  189. const updateOptions = this.RecurlyWrapper.updateSubscription
  190. .args[0][1]
  191. return updateOptions.timeframe.should.equal('now')
  192. })
  193. it('should sync the new subscription to the user', function() {
  194. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(
  195. true
  196. )
  197. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  198. this.activeRecurlySubscription
  199. )
  200. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  201. this.user._id
  202. )
  203. })
  204. })
  205. })
  206. describe('with a user without a subscription', function() {
  207. beforeEach(function(done) {
  208. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  209. 1,
  210. null,
  211. false
  212. )
  213. return this.SubscriptionHandler.updateSubscription(
  214. this.user,
  215. this.plan_code,
  216. null,
  217. done
  218. )
  219. })
  220. it('should redirect to the subscription dashboard', function() {
  221. this.RecurlyWrapper.updateSubscription.called.should.equal(false)
  222. return this.SubscriptionHandler.syncSubscriptionToUser.called.should.equal(
  223. false
  224. )
  225. })
  226. })
  227. describe('with a coupon code', function() {
  228. beforeEach(function(done) {
  229. this.plan_code = 'collaborator'
  230. this.coupon_code = '1231312'
  231. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  232. 1,
  233. null,
  234. true,
  235. this.subscription
  236. )
  237. return this.SubscriptionHandler.updateSubscription(
  238. this.user,
  239. this.plan_code,
  240. this.coupon_code,
  241. done
  242. )
  243. })
  244. it('should get the users account', function() {
  245. return this.RecurlyWrapper.getSubscription
  246. .calledWith(this.activeRecurlySubscription.uuid)
  247. .should.equal(true)
  248. })
  249. it('should redeme the coupon', function(done) {
  250. this.RecurlyWrapper.redeemCoupon
  251. .calledWith(
  252. this.activeRecurlySubscription.account.account_code,
  253. this.coupon_code
  254. )
  255. .should.equal(true)
  256. return done()
  257. })
  258. it('should update the subscription', function() {
  259. this.RecurlyWrapper.updateSubscription
  260. .calledWith(this.subscription.recurlySubscription_id)
  261. .should.equal(true)
  262. const updateOptions = this.RecurlyWrapper.updateSubscription.args[0][1]
  263. return updateOptions.plan_code.should.equal(this.plan_code)
  264. })
  265. })
  266. })
  267. describe('cancelSubscription', function() {
  268. describe('with a user without a subscription', function() {
  269. beforeEach(function(done) {
  270. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  271. 1,
  272. null,
  273. false,
  274. this.subscription
  275. )
  276. return this.SubscriptionHandler.cancelSubscription(this.user, done)
  277. })
  278. it('should redirect to the subscription dashboard', function() {
  279. return this.RecurlyWrapper.cancelSubscription.called.should.equal(false)
  280. })
  281. })
  282. describe('with a user with a subscription', function() {
  283. beforeEach(function(done) {
  284. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  285. 1,
  286. null,
  287. true,
  288. this.subscription
  289. )
  290. return this.SubscriptionHandler.cancelSubscription(this.user, done)
  291. })
  292. it('should cancel the subscription', function() {
  293. this.RecurlyWrapper.cancelSubscription.called.should.equal(true)
  294. return this.RecurlyWrapper.cancelSubscription
  295. .calledWith(this.subscription.recurlySubscription_id)
  296. .should.equal(true)
  297. })
  298. it('should trigger the cancel subscription event', function() {
  299. return this.Events.emit
  300. .calledWith('cancelSubscription', this.user._id)
  301. .should.equal(true)
  302. })
  303. })
  304. })
  305. describe('reactiveRecurlySubscription', function() {
  306. describe('with a user without a subscription', function() {
  307. beforeEach(function(done) {
  308. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  309. 1,
  310. null,
  311. false,
  312. this.subscription
  313. )
  314. return this.SubscriptionHandler.reactivateSubscription(this.user, done)
  315. })
  316. it('should redirect to the subscription dashboard', function() {
  317. return this.RecurlyWrapper.reactivateSubscription.called.should.equal(
  318. false
  319. )
  320. })
  321. it('should not send a notification email', function() {
  322. return sinon.assert.notCalled(this.EmailHandler.sendEmail)
  323. })
  324. })
  325. describe('with a user with a subscription', function() {
  326. beforeEach(function(done) {
  327. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  328. 1,
  329. null,
  330. true,
  331. this.subscription
  332. )
  333. return this.SubscriptionHandler.reactivateSubscription(this.user, done)
  334. })
  335. it('should reactivate the subscription', function() {
  336. this.RecurlyWrapper.reactivateSubscription.called.should.equal(true)
  337. return this.RecurlyWrapper.reactivateSubscription
  338. .calledWith(this.subscription.recurlySubscription_id)
  339. .should.equal(true)
  340. })
  341. it('should send a notification email', function() {
  342. return sinon.assert.calledWith(
  343. this.EmailHandler.sendEmail,
  344. 'reactivatedSubscription'
  345. )
  346. })
  347. })
  348. })
  349. describe('syncSubscription', function() {
  350. describe('with an actionable request', function() {
  351. beforeEach(function(done) {
  352. this.user.id = this.activeRecurlySubscription.account.account_code
  353. this.User.findById = (userId, callback) => {
  354. userId.should.equal(this.user.id)
  355. return callback(null, this.user)
  356. }
  357. return this.SubscriptionHandler.syncSubscription(
  358. this.activeRecurlySubscription,
  359. {},
  360. done
  361. )
  362. })
  363. it('should request the affected subscription from the API', function() {
  364. return this.RecurlyWrapper.getSubscription
  365. .calledWith(this.activeRecurlySubscription.uuid)
  366. .should.equal(true)
  367. })
  368. it('should request the account details of the subscription', function() {
  369. const options = this.RecurlyWrapper.getSubscription.args[0][1]
  370. return options.includeAccount.should.equal(true)
  371. })
  372. it('should sync the subscription to the user', function() {
  373. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
  374. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  375. this.activeRecurlySubscription
  376. )
  377. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  378. this.user._id
  379. )
  380. })
  381. })
  382. })
  383. describe('attemptPaypalInvoiceCollection', function() {
  384. describe('for credit card users', function() {
  385. beforeEach(function(done) {
  386. this.RecurlyWrapper.getBillingInfo.yields(null, {
  387. paypal_billing_agreement_id: null
  388. })
  389. this.SubscriptionHandler.attemptPaypalInvoiceCollection(
  390. this.activeRecurlySubscription.account.account_code,
  391. done
  392. )
  393. })
  394. it('gets billing infos', function() {
  395. sinon.assert.calledWith(
  396. this.RecurlyWrapper.getBillingInfo,
  397. this.activeRecurlySubscription.account.account_code
  398. )
  399. })
  400. it('skips user', function() {
  401. sinon.assert.notCalled(this.RecurlyWrapper.getAccountPastDueInvoices)
  402. })
  403. })
  404. describe('for paypal users', function() {
  405. beforeEach(function(done) {
  406. this.RecurlyWrapper.getBillingInfo.yields(null, {
  407. paypal_billing_agreement_id: 'mock-billing-agreement'
  408. })
  409. this.RecurlyWrapper.getAccountPastDueInvoices.yields(null, [
  410. { invoice_number: 'mock-invoice-number' }
  411. ])
  412. this.SubscriptionHandler.attemptPaypalInvoiceCollection(
  413. this.activeRecurlySubscription.account.account_code,
  414. done
  415. )
  416. })
  417. it('gets past due invoices', function() {
  418. sinon.assert.calledWith(
  419. this.RecurlyWrapper.getAccountPastDueInvoices,
  420. this.activeRecurlySubscription.account.account_code
  421. )
  422. })
  423. it('calls attemptInvoiceCollection', function() {
  424. sinon.assert.calledWith(
  425. this.RecurlyWrapper.attemptInvoiceCollection,
  426. 'mock-invoice-number'
  427. )
  428. })
  429. })
  430. })
  431. describe('validateNoSubscriptionInRecurly', function() {
  432. beforeEach(function() {
  433. this.subscriptions = []
  434. this.RecurlyWrapper.listAccountActiveSubscriptions = sinon
  435. .stub()
  436. .yields(null, this.subscriptions)
  437. this.SubscriptionUpdater.syncSubscription = sinon.stub().yields()
  438. return (this.callback = sinon.stub())
  439. })
  440. describe('with no subscription in recurly', function() {
  441. beforeEach(function() {
  442. this.subscriptions.push((this.subscription = { mock: 'subscription' }))
  443. return this.SubscriptionHandler.validateNoSubscriptionInRecurly(
  444. this.user_id,
  445. this.callback
  446. )
  447. })
  448. it('should call RecurlyWrapper.listAccountActiveSubscriptions with the user id', function() {
  449. return this.RecurlyWrapper.listAccountActiveSubscriptions
  450. .calledWith(this.user_id)
  451. .should.equal(true)
  452. })
  453. it('should sync the subscription', function() {
  454. return this.SubscriptionUpdater.syncSubscription
  455. .calledWith(this.subscription, this.user_id)
  456. .should.equal(true)
  457. })
  458. it('should call the callback with valid == false', function() {
  459. return this.callback.calledWith(null, false).should.equal(true)
  460. })
  461. })
  462. describe('with a subscription in recurly', function() {
  463. beforeEach(function() {
  464. return this.SubscriptionHandler.validateNoSubscriptionInRecurly(
  465. this.user_id,
  466. this.callback
  467. )
  468. })
  469. it('should not sync the subscription', function() {
  470. return this.SubscriptionUpdater.syncSubscription.called.should.equal(
  471. false
  472. )
  473. })
  474. it('should call the callback with valid == true', function() {
  475. return this.callback.calledWith(null, true).should.equal(true)
  476. })
  477. })
  478. })
  479. })