SubscriptionControllerTests.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /* eslint-disable
  2. max-len,
  3. mocha/handle-done-callback,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const SandboxedModule = require('sandboxed-module')
  15. const sinon = require('sinon')
  16. const should = require('chai').should()
  17. const { expect } = require('chai')
  18. const MockRequest = require('../helpers/MockRequest')
  19. const MockResponse = require('../helpers/MockResponse')
  20. const modulePath =
  21. '../../../../app/src/Features/Subscription/SubscriptionController'
  22. const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
  23. const OError = require('@overleaf/o-error')
  24. const HttpErrors = require('@overleaf/o-error/http')
  25. const mockSubscriptions = {
  26. 'subscription-123-active': {
  27. uuid: 'subscription-123-active',
  28. plan: {
  29. name: 'Gold',
  30. plan_code: 'gold'
  31. },
  32. current_period_ends_at: new Date(),
  33. state: 'active',
  34. unit_amount_in_cents: 999,
  35. account: {
  36. account_code: 'user-123'
  37. }
  38. }
  39. }
  40. describe('SubscriptionController', function() {
  41. beforeEach(function() {
  42. this.user = {
  43. email: 'tom@yahoo.com',
  44. _id: 'one',
  45. signUpDate: new Date('2000-10-01')
  46. }
  47. this.activeRecurlySubscription =
  48. mockSubscriptions['subscription-123-active']
  49. this.AuthenticationController = {
  50. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  51. getLoggedInUserId: sinon.stub().returns(this.user._id),
  52. getSessionUser: sinon.stub().returns(this.user),
  53. isUserLoggedIn: sinon.stub().returns(true)
  54. }
  55. this.SubscriptionHandler = {
  56. createSubscription: sinon.stub().callsArgWith(3),
  57. updateSubscription: sinon.stub().callsArgWith(3),
  58. reactivateSubscription: sinon.stub().callsArgWith(1),
  59. cancelSubscription: sinon.stub().callsArgWith(1),
  60. syncSubscription: sinon.stub().yields(),
  61. attemptPaypalInvoiceCollection: sinon.stub().yields(),
  62. startFreeTrial: sinon.stub()
  63. }
  64. this.PlansLocator = { findLocalPlanInSettings: sinon.stub() }
  65. this.LimitationsManager = {
  66. hasPaidSubscription: sinon.stub(),
  67. userHasV1OrV2Subscription: sinon.stub(),
  68. userHasV2Subscription: sinon.stub()
  69. }
  70. this.SubscriptionViewModelBuilder = {
  71. buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
  72. buildViewModel: sinon.stub()
  73. }
  74. this.settings = {
  75. coupon_codes: {
  76. upgradeToAnnualPromo: {
  77. student: 'STUDENTCODEHERE',
  78. collaborator: 'COLLABORATORCODEHERE'
  79. }
  80. },
  81. apis: {
  82. recurly: {
  83. subdomain: 'sl'
  84. }
  85. },
  86. siteUrl: 'http://de.sharelatex.dev:3000',
  87. gaExperiments: {}
  88. }
  89. this.GeoIpLookup = { getCurrencyCode: sinon.stub() }
  90. this.UserGetter = {
  91. getUser: sinon.stub().callsArgWith(2, null, this.user)
  92. }
  93. this.SubscriptionController = SandboxedModule.require(modulePath, {
  94. globals: {
  95. console: console
  96. },
  97. requires: {
  98. '../Authentication/AuthenticationController': this
  99. .AuthenticationController,
  100. './SubscriptionHandler': this.SubscriptionHandler,
  101. './PlansLocator': this.PlansLocator,
  102. './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder,
  103. './LimitationsManager': this.LimitationsManager,
  104. '../../infrastructure/GeoIpLookup': this.GeoIpLookup,
  105. 'logger-sharelatex': {
  106. log() {},
  107. warn() {}
  108. },
  109. 'settings-sharelatex': this.settings,
  110. '../User/UserGetter': this.UserGetter,
  111. './RecurlyWrapper': (this.RecurlyWrapper = {}),
  112. './FeaturesUpdater': (this.FeaturesUpdater = {}),
  113. './GroupPlansData': (this.GroupPlansData = {}),
  114. './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
  115. './Errors': SubscriptionErrors,
  116. '@overleaf/o-error/http': HttpErrors
  117. }
  118. })
  119. this.res = new MockResponse()
  120. this.req = new MockRequest()
  121. this.req.body = {}
  122. this.req.query = { planCode: '123123' }
  123. return (this.stubbedCurrencyCode = 'GBP')
  124. })
  125. describe('plansPage', function() {
  126. beforeEach(function() {
  127. this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
  128. return this.GeoIpLookup.getCurrencyCode.callsArgWith(
  129. 1,
  130. null,
  131. this.stubbedCurrencyCode
  132. )
  133. })
  134. describe('when user is logged in', function(done) {
  135. beforeEach(function(done) {
  136. this.res.callback = done
  137. return this.SubscriptionController.plansPage(this.req, this.res)
  138. })
  139. it('should fetch the current user', function(done) {
  140. this.UserGetter.getUser.callCount.should.equal(1)
  141. return done()
  142. })
  143. describe('not dependant on logged in state', function(done) {
  144. // these could have been put in 'when user is not logged in' too
  145. it('should set the recommended currency from the geoiplookup', function(done) {
  146. this.res.renderedVariables.recomendedCurrency.should.equal(
  147. this.stubbedCurrencyCode
  148. )
  149. this.GeoIpLookup.getCurrencyCode
  150. .calledWith(this.req.ip)
  151. .should.equal(true)
  152. return done()
  153. })
  154. it('should include data for features table', function(done) {
  155. this.res.renderedVariables.planFeatures.length.should.not.equal(0)
  156. return done()
  157. })
  158. })
  159. })
  160. describe('when user is not logged in', function(done) {
  161. beforeEach(function(done) {
  162. this.res.callback = done
  163. this.AuthenticationController.getLoggedInUserId = sinon
  164. .stub()
  165. .returns(null)
  166. return this.SubscriptionController.plansPage(this.req, this.res)
  167. })
  168. it('should not fetch the current user', function(done) {
  169. this.UserGetter.getUser.callCount.should.equal(0)
  170. return done()
  171. })
  172. })
  173. })
  174. describe('paymentPage', function() {
  175. beforeEach(function() {
  176. this.req.headers = {}
  177. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  178. .stub()
  179. .yields(null, true)
  180. return this.GeoIpLookup.getCurrencyCode.callsArgWith(
  181. 1,
  182. null,
  183. this.stubbedCurrencyCode
  184. )
  185. })
  186. describe('with a user without a subscription', function() {
  187. beforeEach(function() {
  188. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  189. 1,
  190. null,
  191. false
  192. )
  193. return this.PlansLocator.findLocalPlanInSettings.returns({})
  194. })
  195. describe('with a valid plan code', function() {
  196. it('should render the new subscription page', function(done) {
  197. this.res.render = (page, opts) => {
  198. page.should.equal('subscriptions/new')
  199. return done()
  200. }
  201. return this.SubscriptionController.paymentPage(this.req, this.res)
  202. })
  203. })
  204. })
  205. describe('with a user with subscription', function() {
  206. it('should redirect to the subscription dashboard', function(done) {
  207. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  208. 1,
  209. null,
  210. true
  211. )
  212. this.res.redirect = url => {
  213. url.should.equal('/user/subscription?hasSubscription=true')
  214. return done()
  215. }
  216. return this.SubscriptionController.paymentPage(this.req, this.res)
  217. })
  218. })
  219. describe('with an invalid plan code', function() {
  220. it('should redirect to the subscription dashboard', function(done) {
  221. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  222. 1,
  223. null,
  224. false
  225. )
  226. this.PlansLocator.findLocalPlanInSettings.returns(null)
  227. this.res.redirect = url => {
  228. url.should.equal('/user/subscription?hasSubscription=true')
  229. return done()
  230. }
  231. return this.SubscriptionController.paymentPage(this.req, this.res)
  232. })
  233. })
  234. describe('which currency to use', function() {
  235. beforeEach(function() {
  236. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  237. 1,
  238. null,
  239. false
  240. )
  241. return this.PlansLocator.findLocalPlanInSettings.returns({})
  242. })
  243. it('should use the set currency from the query string', function(done) {
  244. this.req.query.currency = 'EUR'
  245. this.res.render = (page, opts) => {
  246. opts.currency.should.equal('EUR')
  247. opts.currency.should.not.equal(this.stubbedCurrencyCode)
  248. return done()
  249. }
  250. return this.SubscriptionController.paymentPage(this.req, this.res)
  251. })
  252. it('should upercase the currency code', function(done) {
  253. this.req.query.currency = 'eur'
  254. this.res.render = (page, opts) => {
  255. opts.currency.should.equal('EUR')
  256. return done()
  257. }
  258. return this.SubscriptionController.paymentPage(this.req, this.res)
  259. })
  260. it('should use the geo ip currency if non is provided', function(done) {
  261. this.req.query.currency = null
  262. this.res.render = (page, opts) => {
  263. opts.currency.should.equal(this.stubbedCurrencyCode)
  264. return done()
  265. }
  266. return this.SubscriptionController.paymentPage(this.req, this.res)
  267. })
  268. })
  269. describe('with a recurly subscription already', function() {
  270. it('should redirect to the subscription dashboard', function(done) {
  271. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  272. 1,
  273. null,
  274. false
  275. )
  276. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  277. .stub()
  278. .yields(null, false)
  279. this.res.redirect = url => {
  280. url.should.equal('/user/subscription?hasSubscription=true')
  281. return done()
  282. }
  283. return this.SubscriptionController.paymentPage(this.req, this.res)
  284. })
  285. })
  286. })
  287. describe('successful_subscription', function() {
  288. beforeEach(function(done) {
  289. this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
  290. 1,
  291. null,
  292. {}
  293. )
  294. this.res.callback = done
  295. return this.SubscriptionController.successful_subscription(
  296. this.req,
  297. this.res
  298. )
  299. })
  300. })
  301. describe('userSubscriptionPage', function() {
  302. beforeEach(function(done) {
  303. this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
  304. 1,
  305. null,
  306. {
  307. personalSubscription: (this.personalSubscription = {
  308. 'personal-subscription': 'mock'
  309. }),
  310. memberGroupSubscriptions: (this.memberGroupSubscriptions = {
  311. 'group-subscriptions': 'mock'
  312. })
  313. }
  314. )
  315. this.SubscriptionViewModelBuilder.buildViewModel.returns(
  316. (this.plans = { plans: 'mock' })
  317. )
  318. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  319. 1,
  320. null,
  321. false
  322. )
  323. this.res.render = (view, data) => {
  324. this.data = data
  325. expect(view).to.equal('subscriptions/dashboard')
  326. return done()
  327. }
  328. return this.SubscriptionController.userSubscriptionPage(
  329. this.req,
  330. this.res
  331. )
  332. })
  333. it('should load the personal, groups and v1 subscriptions', function() {
  334. expect(this.data.personalSubscription).to.deep.equal(
  335. this.personalSubscription
  336. )
  337. return expect(this.data.memberGroupSubscriptions).to.deep.equal(
  338. this.memberGroupSubscriptions
  339. )
  340. })
  341. it('should load the user', function() {
  342. return expect(this.data.user).to.deep.equal(this.user)
  343. })
  344. it('should load the plans', function() {
  345. return expect(this.data.plans).to.deep.equal(this.plans)
  346. })
  347. })
  348. describe('createSubscription', function() {
  349. beforeEach(function(done) {
  350. this.res = {
  351. sendStatus() {
  352. return done()
  353. }
  354. }
  355. sinon.spy(this.res, 'sendStatus')
  356. this.subscriptionDetails = {
  357. card: '1234',
  358. cvv: '123'
  359. }
  360. this.recurlyTokenIds = {
  361. billing: '1234',
  362. threeDSecureActionResult: '5678'
  363. }
  364. this.req.body.recurly_token_id = this.recurlyTokenIds.billing
  365. this.req.body.recurly_three_d_secure_action_result_token_id = this.recurlyTokenIds.threeDSecureActionResult
  366. this.req.body.subscriptionDetails = this.subscriptionDetails
  367. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  368. return this.SubscriptionController.createSubscription(this.req, this.res)
  369. })
  370. it('should send the user and subscriptionId to the handler', function(done) {
  371. this.SubscriptionHandler.createSubscription
  372. .calledWithMatch(
  373. this.user,
  374. this.subscriptionDetails,
  375. this.recurlyTokenIds
  376. )
  377. .should.equal(true)
  378. return done()
  379. })
  380. it('should redurect to the subscription page', function(done) {
  381. this.res.sendStatus.calledWith(201).should.equal(true)
  382. return done()
  383. })
  384. })
  385. describe('createSubscription with errors', function() {
  386. it('should handle 3DSecure errors', function(done) {
  387. this.next = sinon.stub()
  388. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  389. this.SubscriptionHandler.createSubscription.yields(
  390. new SubscriptionErrors.RecurlyTransactionError({})
  391. )
  392. this.SubscriptionController.createSubscription(this.req, null, error => {
  393. expect(error).to.exist
  394. expect(error).to.be.instanceof(HttpErrors.UnprocessableEntityError)
  395. expect(
  396. OError.hasCauseInstanceOf(
  397. error,
  398. SubscriptionErrors.RecurlyTransactionError
  399. )
  400. ).to.be.true
  401. })
  402. return done()
  403. })
  404. })
  405. describe('updateSubscription via post', function() {
  406. beforeEach(function(done) {
  407. this.res = {
  408. redirect() {
  409. return done()
  410. }
  411. }
  412. sinon.spy(this.res, 'redirect')
  413. this.plan_code = '1234'
  414. this.req.body.plan_code = this.plan_code
  415. return this.SubscriptionController.updateSubscription(this.req, this.res)
  416. })
  417. it('should send the user and subscriptionId to the handler', function(done) {
  418. this.SubscriptionHandler.updateSubscription
  419. .calledWith(this.user, this.plan_code)
  420. .should.equal(true)
  421. return done()
  422. })
  423. it('should redurect to the subscription page', function(done) {
  424. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  425. return done()
  426. })
  427. })
  428. describe('reactivateSubscription', function() {
  429. beforeEach(function(done) {
  430. this.res = {
  431. redirect() {
  432. return done()
  433. }
  434. }
  435. sinon.spy(this.res, 'redirect')
  436. return this.SubscriptionController.reactivateSubscription(
  437. this.req,
  438. this.res
  439. )
  440. })
  441. it('should tell the handler to reactivate this user', function(done) {
  442. this.SubscriptionHandler.reactivateSubscription
  443. .calledWith(this.user)
  444. .should.equal(true)
  445. return done()
  446. })
  447. it('should redurect to the subscription page', function(done) {
  448. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  449. return done()
  450. })
  451. })
  452. describe('cancelSubscription', function() {
  453. beforeEach(function(done) {
  454. this.res = {
  455. redirect() {
  456. return done()
  457. }
  458. }
  459. sinon.spy(this.res, 'redirect')
  460. return this.SubscriptionController.cancelSubscription(this.req, this.res)
  461. })
  462. it('should tell the handler to cancel this user', function(done) {
  463. this.SubscriptionHandler.cancelSubscription
  464. .calledWith(this.user)
  465. .should.equal(true)
  466. return done()
  467. })
  468. it('should redurect to the subscription page', function(done) {
  469. this.res.redirect
  470. .calledWith('/user/subscription/canceled')
  471. .should.equal(true)
  472. return done()
  473. })
  474. })
  475. describe('recurly callback', function() {
  476. describe('with a sync subscription request', function() {
  477. beforeEach(function(done) {
  478. this.req = {
  479. body: {
  480. expired_subscription_notification: {
  481. subscription: {
  482. uuid: this.activeRecurlySubscription.uuid
  483. }
  484. }
  485. }
  486. }
  487. this.res = {
  488. sendStatus() {
  489. return done()
  490. }
  491. }
  492. sinon.spy(this.res, 'sendStatus')
  493. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  494. })
  495. it('should tell the SubscriptionHandler to process the recurly callback', function(done) {
  496. this.SubscriptionHandler.syncSubscription.called.should.equal(true)
  497. return done()
  498. })
  499. it('should send a 200', function(done) {
  500. this.res.sendStatus.calledWith(200)
  501. return done()
  502. })
  503. })
  504. describe('with a billing info updated request', function() {
  505. beforeEach(function(done) {
  506. this.req = {
  507. body: {
  508. billing_info_updated_notification: {
  509. account: {
  510. account_code: 'mock-account-code'
  511. }
  512. }
  513. }
  514. }
  515. this.res = {
  516. sendStatus() {
  517. done()
  518. }
  519. }
  520. sinon.spy(this.res, 'sendStatus')
  521. this.SubscriptionController.recurlyCallback(this.req, this.res)
  522. })
  523. it('should call attemptPaypalInvoiceCollection', function(done) {
  524. this.SubscriptionHandler.attemptPaypalInvoiceCollection
  525. .calledWith('mock-account-code')
  526. .should.equal(true)
  527. done()
  528. })
  529. it('should send a 200', function(done) {
  530. this.res.sendStatus.calledWith(200)
  531. done()
  532. })
  533. })
  534. describe('with a non-actionable request', function() {
  535. beforeEach(function(done) {
  536. this.user.id = this.activeRecurlySubscription.account.account_code
  537. this.req = {
  538. body: {
  539. renewed_subscription_notification: {
  540. subscription: {
  541. uuid: this.activeRecurlySubscription.uuid
  542. }
  543. }
  544. }
  545. }
  546. this.res = {
  547. sendStatus() {
  548. return done()
  549. }
  550. }
  551. sinon.spy(this.res, 'sendStatus')
  552. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  553. })
  554. it('should not call the subscriptionshandler', function() {
  555. this.SubscriptionHandler.syncSubscription.called.should.equal(false)
  556. this.SubscriptionHandler.attemptPaypalInvoiceCollection.called.should.equal(
  557. false
  558. )
  559. })
  560. it('should respond with a 200 status', function() {
  561. return this.res.sendStatus.calledWith(200)
  562. })
  563. })
  564. })
  565. describe('renderUpgradeToAnnualPlanPage', function() {
  566. it('should redirect to the plans page if the user does not have a subscription', function(done) {
  567. this.LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false)
  568. this.res.redirect = function(url) {
  569. url.should.equal('/user/subscription/plans')
  570. return done()
  571. }
  572. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  573. this.req,
  574. this.res
  575. )
  576. })
  577. it('should pass the plan code to the view - student', function(done) {
  578. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  579. 1,
  580. null,
  581. true,
  582. { planCode: 'Student free trial 14 days' }
  583. )
  584. this.res.render = function(view, opts) {
  585. view.should.equal('subscriptions/upgradeToAnnual')
  586. opts.planName.should.equal('student')
  587. return done()
  588. }
  589. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  590. this.req,
  591. this.res
  592. )
  593. })
  594. it('should pass the plan code to the view - collaborator', function(done) {
  595. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  596. 1,
  597. null,
  598. true,
  599. { planCode: 'free trial for Collaborator free trial 14 days' }
  600. )
  601. this.res.render = function(view, opts) {
  602. opts.planName.should.equal('collaborator')
  603. return done()
  604. }
  605. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  606. this.req,
  607. this.res
  608. )
  609. })
  610. it('should pass annual as the plan name if the user is already on an annual plan', function(done) {
  611. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  612. 1,
  613. null,
  614. true,
  615. { planCode: 'student annual with free trial' }
  616. )
  617. this.res.render = function(view, opts) {
  618. opts.planName.should.equal('annual')
  619. return done()
  620. }
  621. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  622. this.req,
  623. this.res
  624. )
  625. })
  626. })
  627. describe('processUpgradeToAnnualPlan', function() {
  628. beforeEach(function() {})
  629. it('should tell the subscription handler to update the subscription with the annual plan and apply a coupon code', function(done) {
  630. this.req.body = { planName: 'student' }
  631. this.res.sendStatus = () => {
  632. this.SubscriptionHandler.updateSubscription
  633. .calledWith(this.user, 'student-annual', 'STUDENTCODEHERE')
  634. .should.equal(true)
  635. return done()
  636. }
  637. return this.SubscriptionController.processUpgradeToAnnualPlan(
  638. this.req,
  639. this.res
  640. )
  641. })
  642. it('should get the collaborator coupon code', function(done) {
  643. this.req.body = { planName: 'collaborator' }
  644. this.res.sendStatus = url => {
  645. this.SubscriptionHandler.updateSubscription
  646. .calledWith(this.user, 'collaborator-annual', 'COLLABORATORCODEHERE')
  647. .should.equal(true)
  648. return done()
  649. }
  650. return this.SubscriptionController.processUpgradeToAnnualPlan(
  651. this.req,
  652. this.res
  653. )
  654. })
  655. })
  656. })