SubscriptionHandlerTests.js 16 KB

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