SubscriptionViewModelBuilderTests.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { assert } = require('chai')
  4. const {
  5. PaymentProviderAccount,
  6. PaymentProviderSubscription,
  7. PaymentProviderSubscriptionAddOn,
  8. PaymentProviderSubscriptionChange,
  9. } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
  10. const modulePath =
  11. '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder'
  12. describe('SubscriptionViewModelBuilder', function () {
  13. beforeEach(function () {
  14. this.user = { _id: '5208dd34438842e2db333333' }
  15. this.recurlySubscription_id = '123abc456def'
  16. this.planCode = 'collaborator_monthly'
  17. this.planFeatures = {
  18. compileGroup: 'priority',
  19. collaborators: -1,
  20. compileTimeout: 240,
  21. }
  22. this.plan = {
  23. planCode: this.planCode,
  24. features: this.planFeatures,
  25. }
  26. this.annualPlanCode = 'collaborator_annual'
  27. this.annualPlan = {
  28. planCode: this.annualPlanCode,
  29. features: this.planFeatures,
  30. }
  31. this.individualSubscription = {
  32. planCode: this.planCode,
  33. plan: this.plan,
  34. recurlySubscription_id: this.recurlySubscription_id,
  35. recurlyStatus: {
  36. state: 'active',
  37. },
  38. }
  39. this.paymentRecord = new PaymentProviderSubscription({
  40. id: this.recurlySubscription_id,
  41. userId: this.user._id,
  42. currency: 'EUR',
  43. planCode: 'plan-code',
  44. planName: 'plan-name',
  45. planPrice: 13,
  46. addOns: [
  47. new PaymentProviderSubscriptionAddOn({
  48. code: 'addon-code',
  49. name: 'addon name',
  50. quantity: 1,
  51. unitPrice: 2,
  52. }),
  53. ],
  54. subtotal: 15,
  55. taxRate: 0.1,
  56. taxAmount: 1.5,
  57. total: 16.5,
  58. periodStart: new Date('2025-01-20T12:00:00.000Z'),
  59. periodEnd: new Date('2025-02-20T12:00:00.000Z'),
  60. collectionMethod: 'automatic',
  61. })
  62. this.individualCustomSubscription = {
  63. planCode: this.planCode,
  64. plan: this.plan,
  65. recurlySubscription_id: this.recurlySubscription_id,
  66. }
  67. this.groupPlanCode = 'group_collaborator_monthly'
  68. this.groupPlanFeatures = {
  69. compileGroup: 'priority',
  70. collaborators: 10,
  71. compileTimeout: 240,
  72. }
  73. this.groupPlan = {
  74. planCode: this.groupPlanCode,
  75. features: this.groupPlanFeatures,
  76. membersLimit: 4,
  77. membersLimitAddOn: 'additional-license',
  78. groupPlan: true,
  79. }
  80. this.groupSubscription = {
  81. planCode: this.groupPlanCode,
  82. plan: this.plan,
  83. recurlyStatus: {
  84. state: 'active',
  85. },
  86. }
  87. this.commonsPlanCode = 'commons_license'
  88. this.commonsPlanFeatures = {
  89. compileGroup: 'priority',
  90. collaborators: '-1',
  91. compileTimeout: 240,
  92. }
  93. this.commonsPlan = {
  94. planCode: this.commonsPlanCode,
  95. features: this.commonsPlanFeatures,
  96. }
  97. this.commonsSubscription = {
  98. planCode: this.commonsPlanCode,
  99. plan: this.commonsPlan,
  100. name: 'Digital Science',
  101. }
  102. this.Settings = {
  103. institutionPlanCode: this.commonsPlanCode,
  104. }
  105. this.SubscriptionLocator = {
  106. promises: {
  107. getUsersSubscription: sinon.stub().resolves(),
  108. getMemberSubscriptions: sinon.stub().resolves(),
  109. },
  110. getUsersSubscription: sinon.stub().yields(),
  111. getMemberSubscriptions: sinon.stub().yields(null, []),
  112. getManagedGroupSubscriptions: sinon.stub().yields(null, []),
  113. findLocalPlanInSettings: sinon.stub(),
  114. }
  115. this.InstitutionsGetter = {
  116. promises: {
  117. getCurrentInstitutionsWithLicence: sinon.stub().resolves(),
  118. },
  119. getCurrentInstitutionsWithLicence: sinon.stub().yields(null, []),
  120. getManagedInstitutions: sinon.stub().yields(null, []),
  121. }
  122. this.InstitutionsManager = {
  123. promises: {
  124. fetchV1Data: sinon.stub().resolves(),
  125. },
  126. }
  127. this.PublishersGetter = {
  128. promises: {
  129. fetchV1Data: sinon.stub().resolves(),
  130. },
  131. getManagedPublishers: sinon.stub().yields(null, []),
  132. }
  133. this.RecurlyWrapper = {
  134. promises: {
  135. getSubscription: sinon.stub().resolves(),
  136. },
  137. }
  138. this.SubscriptionUpdater = {
  139. promises: {
  140. updateSubscriptionFromRecurly: sinon.stub().resolves(),
  141. },
  142. }
  143. this.PlansLocator = {
  144. findLocalPlanInSettings: sinon.stub(),
  145. }
  146. this.SubscriptionViewModelBuilder = SandboxedModule.require(modulePath, {
  147. requires: {
  148. '@overleaf/settings': this.Settings,
  149. './SubscriptionLocator': this.SubscriptionLocator,
  150. '../Institutions/InstitutionsGetter': this.InstitutionsGetter,
  151. '../Institutions/InstitutionsManager': this.InstitutionsManager,
  152. './RecurlyWrapper': this.RecurlyWrapper,
  153. './SubscriptionUpdater': this.SubscriptionUpdater,
  154. './PlansLocator': this.PlansLocator,
  155. '../../infrastructure/Modules': (this.Modules = {
  156. hooks: {
  157. fire: sinon.stub().yields(null, []),
  158. },
  159. }),
  160. './V1SubscriptionManager': {},
  161. '../Publishers/PublishersGetter': this.PublishersGetter,
  162. './SubscriptionHelper': {},
  163. },
  164. })
  165. this.PlansLocator.findLocalPlanInSettings
  166. .withArgs(this.planCode)
  167. .returns(this.plan)
  168. .withArgs(this.annualPlanCode)
  169. .returns(this.annualPlan)
  170. .withArgs(this.groupPlanCode)
  171. .returns(this.groupPlan)
  172. .withArgs(this.commonsPlanCode)
  173. .returns(this.commonsPlan)
  174. })
  175. describe('getBestSubscription', function () {
  176. it('should return a free plan when user has no subscription or affiliation', async function () {
  177. const usersBestSubscription =
  178. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  179. this.user
  180. )
  181. assert.deepEqual(usersBestSubscription, { type: 'free' })
  182. })
  183. describe('with a individual subscription only', function () {
  184. it('should return a individual subscription when user has non-Recurly one', async function () {
  185. this.SubscriptionLocator.promises.getUsersSubscription
  186. .withArgs(this.user)
  187. .resolves(this.individualCustomSubscription)
  188. const usersBestSubscription =
  189. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  190. this.user
  191. )
  192. assert.deepEqual(usersBestSubscription, {
  193. type: 'individual',
  194. subscription: this.individualCustomSubscription,
  195. plan: this.plan,
  196. remainingTrialDays: -1,
  197. })
  198. })
  199. it('should return a individual subscription when user has an active one', async function () {
  200. this.SubscriptionLocator.promises.getUsersSubscription
  201. .withArgs(this.user)
  202. .resolves(this.individualSubscription)
  203. const usersBestSubscription =
  204. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  205. this.user
  206. )
  207. assert.deepEqual(usersBestSubscription, {
  208. type: 'individual',
  209. subscription: this.individualSubscription,
  210. plan: this.plan,
  211. remainingTrialDays: -1,
  212. })
  213. })
  214. it('should return a individual subscription with remaining free trial days', async function () {
  215. const threeDaysLater = new Date()
  216. threeDaysLater.setDate(threeDaysLater.getDate() + 3)
  217. this.individualSubscription.recurlyStatus.trialEndsAt = threeDaysLater
  218. this.SubscriptionLocator.promises.getUsersSubscription
  219. .withArgs(this.user)
  220. .resolves(this.individualSubscription)
  221. const usersBestSubscription =
  222. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  223. this.user
  224. )
  225. assert.deepEqual(usersBestSubscription, {
  226. type: 'individual',
  227. subscription: this.individualSubscription,
  228. plan: this.plan,
  229. remainingTrialDays: 3,
  230. })
  231. })
  232. it('should return a individual subscription with free trial on last day', async function () {
  233. const threeHoursLater = new Date()
  234. threeHoursLater.setTime(threeHoursLater.getTime() + 3 * 60 * 60 * 1000)
  235. this.individualSubscription.recurlyStatus.trialEndsAt = threeHoursLater
  236. this.SubscriptionLocator.promises.getUsersSubscription
  237. .withArgs(this.user)
  238. .resolves(this.individualSubscription)
  239. const usersBestSubscription =
  240. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  241. this.user
  242. )
  243. assert.deepEqual(usersBestSubscription, {
  244. type: 'individual',
  245. subscription: this.individualSubscription,
  246. plan: this.plan,
  247. remainingTrialDays: 1,
  248. })
  249. })
  250. it('should update subscription if recurly data is missing', async function () {
  251. this.individualSubscriptionWithoutRecurly = {
  252. planCode: this.planCode,
  253. plan: this.plan,
  254. recurlySubscription_id: this.recurlySubscription_id,
  255. }
  256. this.paymentRecord = {
  257. state: 'active',
  258. }
  259. this.SubscriptionLocator.promises.getUsersSubscription
  260. .withArgs(this.user)
  261. .onCall(0)
  262. .resolves(this.individualSubscriptionWithoutRecurly)
  263. .withArgs(this.user)
  264. .onCall(1)
  265. .resolves(this.individualSubscription)
  266. this.RecurlyWrapper.promises.getSubscription
  267. .withArgs(this.individualSubscription.recurlySubscription_id, {
  268. includeAccount: true,
  269. })
  270. .resolves(this.paymentRecord)
  271. const usersBestSubscription =
  272. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  273. this.user
  274. )
  275. sinon.assert.calledWith(
  276. this.RecurlyWrapper.promises.getSubscription,
  277. this.individualSubscriptionWithoutRecurly.recurlySubscription_id,
  278. { includeAccount: true }
  279. )
  280. sinon.assert.calledWith(
  281. this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly,
  282. this.paymentRecord,
  283. this.individualSubscriptionWithoutRecurly
  284. )
  285. assert.deepEqual(usersBestSubscription, {
  286. type: 'individual',
  287. subscription: this.individualSubscription,
  288. plan: this.plan,
  289. remainingTrialDays: -1,
  290. })
  291. })
  292. })
  293. it('should return a group subscription when user has one', async function () {
  294. this.SubscriptionLocator.promises.getMemberSubscriptions
  295. .withArgs(this.user)
  296. .resolves([this.groupSubscription])
  297. const usersBestSubscription =
  298. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  299. this.user
  300. )
  301. assert.deepEqual(usersBestSubscription, {
  302. type: 'group',
  303. subscription: {},
  304. plan: this.groupPlan,
  305. remainingTrialDays: -1,
  306. })
  307. })
  308. it('should return a group subscription with team name when user has one', async function () {
  309. this.SubscriptionLocator.promises.getMemberSubscriptions
  310. .withArgs(this.user)
  311. .resolves([
  312. Object.assign({}, this.groupSubscription, { teamName: 'test team' }),
  313. ])
  314. const usersBestSubscription =
  315. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  316. this.user
  317. )
  318. assert.deepEqual(usersBestSubscription, {
  319. type: 'group',
  320. subscription: { teamName: 'test team' },
  321. plan: this.groupPlan,
  322. remainingTrialDays: -1,
  323. })
  324. })
  325. it('should return a commons subscription when user has an institution affiliation', async function () {
  326. this.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence
  327. .withArgs(this.user._id)
  328. .resolves([this.commonsSubscription])
  329. const usersBestSubscription =
  330. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  331. this.user
  332. )
  333. assert.deepEqual(usersBestSubscription, {
  334. type: 'commons',
  335. subscription: this.commonsSubscription,
  336. plan: this.commonsPlan,
  337. })
  338. })
  339. describe('with multiple subscriptions', function () {
  340. beforeEach(function () {
  341. this.SubscriptionLocator.promises.getUsersSubscription
  342. .withArgs(this.user)
  343. .resolves(this.individualSubscription)
  344. this.SubscriptionLocator.promises.getMemberSubscriptions
  345. .withArgs(this.user)
  346. .resolves([this.groupSubscription])
  347. this.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence
  348. .withArgs(this.user._id)
  349. .resolves([this.commonsSubscription])
  350. })
  351. it('should return individual when the individual subscription has the best feature set', async function () {
  352. this.commonsPlan.features = {
  353. compileGroup: 'standard',
  354. collaborators: 1,
  355. compileTimeout: 60,
  356. }
  357. const usersBestSubscription =
  358. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  359. this.user
  360. )
  361. assert.deepEqual(usersBestSubscription, {
  362. type: 'individual',
  363. subscription: this.individualSubscription,
  364. plan: this.plan,
  365. remainingTrialDays: -1,
  366. })
  367. })
  368. it('should return group when the group subscription has the best feature set', async function () {
  369. this.plan.features = {
  370. compileGroup: 'standard',
  371. collaborators: 1,
  372. compileTimeout: 60,
  373. }
  374. this.commonsPlan.features = {
  375. compileGroup: 'standard',
  376. collaborators: 1,
  377. compileTimeout: 60,
  378. }
  379. const usersBestSubscription =
  380. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  381. this.user
  382. )
  383. assert.deepEqual(usersBestSubscription, {
  384. type: 'group',
  385. subscription: {},
  386. plan: this.groupPlan,
  387. remainingTrialDays: -1,
  388. })
  389. })
  390. it('should return commons when the commons affiliation has the best feature set', async function () {
  391. this.plan.features = {
  392. compileGroup: 'priority',
  393. collaborators: 5,
  394. compileTimeout: 240,
  395. }
  396. this.groupPlan.features = {
  397. compileGroup: 'standard',
  398. collaborators: 1,
  399. compileTimeout: 60,
  400. }
  401. this.commonsPlan.features = {
  402. compileGroup: 'priority',
  403. collaborators: -1,
  404. compileTimeout: 240,
  405. }
  406. const usersBestSubscription =
  407. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  408. this.user
  409. )
  410. assert.deepEqual(usersBestSubscription, {
  411. type: 'commons',
  412. subscription: this.commonsSubscription,
  413. plan: this.commonsPlan,
  414. })
  415. })
  416. it('should return individual with equal feature sets', async function () {
  417. this.plan.features = {
  418. compileGroup: 'priority',
  419. collaborators: -1,
  420. compileTimeout: 240,
  421. }
  422. this.groupPlan.features = {
  423. compileGroup: 'priority',
  424. collaborators: -1,
  425. compileTimeout: 240,
  426. }
  427. this.commonsPlan.features = {
  428. compileGroup: 'priority',
  429. collaborators: -1,
  430. compileTimeout: 240,
  431. }
  432. const usersBestSubscription =
  433. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  434. this.user
  435. )
  436. assert.deepEqual(usersBestSubscription, {
  437. type: 'individual',
  438. subscription: this.individualSubscription,
  439. plan: this.plan,
  440. remainingTrialDays: -1,
  441. })
  442. })
  443. it('should return group over commons with equal feature sets', async function () {
  444. this.plan.features = {
  445. compileGroup: 'standard',
  446. collaborators: 1,
  447. compileTimeout: 60,
  448. }
  449. this.groupPlan.features = {
  450. compileGroup: 'priority',
  451. collaborators: -1,
  452. compileTimeout: 240,
  453. }
  454. this.commonsPlan.features = {
  455. compileGroup: 'priority',
  456. collaborators: -1,
  457. compileTimeout: 240,
  458. }
  459. const usersBestSubscription =
  460. await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
  461. this.user
  462. )
  463. assert.deepEqual(usersBestSubscription, {
  464. type: 'group',
  465. subscription: {},
  466. plan: this.groupPlan,
  467. remainingTrialDays: -1,
  468. })
  469. })
  470. })
  471. })
  472. describe('buildUsersSubscriptionViewModel', function () {
  473. beforeEach(function () {
  474. this.SubscriptionLocator.getUsersSubscription.yields(
  475. null,
  476. this.individualSubscription
  477. )
  478. this.Modules.hooks.fire
  479. .withArgs('getPaymentFromRecord', this.individualSubscription)
  480. .yields(null, [
  481. {
  482. subscription: this.paymentRecord,
  483. account: new PaymentProviderAccount({}),
  484. coupons: [],
  485. },
  486. ])
  487. })
  488. describe('with a paid subscription', function () {
  489. it('adds payment data to the personal subscription', async function () {
  490. this.Modules.hooks.fire
  491. .withArgs('getPaymentFromRecord', this.individualSubscription)
  492. .yields(null, [
  493. {
  494. subscription: this.paymentRecord,
  495. account: new PaymentProviderAccount({
  496. email: 'example@example.com',
  497. hasPastDueInvoice: false,
  498. }),
  499. coupons: [],
  500. },
  501. ])
  502. const result =
  503. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  504. this.user
  505. )
  506. assert.deepEqual(result.personalSubscription.payment, {
  507. taxRate: 0.1,
  508. billingDetailsLink: '/user/subscription/payment/billing-details',
  509. accountManagementLink:
  510. '/user/subscription/payment/account-management',
  511. additionalLicenses: 0,
  512. addOns: [
  513. {
  514. code: 'addon-code',
  515. name: 'addon name',
  516. quantity: 1,
  517. unitPrice: 2,
  518. preTaxTotal: 2,
  519. },
  520. ],
  521. totalLicenses: 0,
  522. nextPaymentDueAt: 'February 20th, 2025 12:00 PM UTC',
  523. nextPaymentDueDate: 'February 20th, 2025',
  524. currency: 'EUR',
  525. state: 'active',
  526. trialEndsAtFormatted: null,
  527. trialEndsAt: null,
  528. activeCoupons: [],
  529. accountEmail: 'example@example.com',
  530. hasPastDueInvoice: false,
  531. pausedAt: null,
  532. remainingPauseCycles: null,
  533. displayPrice: '€16.50',
  534. planOnlyDisplayPrice: '€14.30',
  535. addOnDisplayPricesWithoutAdditionalLicense: {
  536. 'addon-code': '€2.20',
  537. },
  538. isEligibleForGroupPlan: true,
  539. isEligibleForPause: false,
  540. isEligibleForDowngradeUpsell: true,
  541. })
  542. })
  543. describe('isEligibleForGroupPlan', function () {
  544. it('is false for Stripe subscriptions', async function () {
  545. this.paymentRecord.service = 'stripe'
  546. const result =
  547. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  548. this.user
  549. )
  550. assert.isFalse(
  551. result.personalSubscription.payment.isEligibleForGroupPlan
  552. )
  553. })
  554. it('is false when in trial', async function () {
  555. const msIn24Hours = 24 * 60 * 60 * 1000
  556. const tomorrow = new Date(Date.now() + msIn24Hours)
  557. this.paymentRecord.trialPeriodEnd = tomorrow
  558. this.paymentRecord.service = 'recurly'
  559. const result =
  560. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  561. this.user
  562. )
  563. assert.isFalse(
  564. result.personalSubscription.payment.isEligibleForGroupPlan
  565. )
  566. })
  567. it('is true when not in trial and for a Recurly subscription', async function () {
  568. this.paymentRecord.service = 'recurly'
  569. const result =
  570. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  571. this.user
  572. )
  573. assert.isTrue(
  574. result.personalSubscription.payment.isEligibleForGroupPlan
  575. )
  576. })
  577. })
  578. describe('isEligibleForPause', function () {
  579. it('is false for Stripe subscriptions', async function () {
  580. this.paymentRecord.service = 'stripe'
  581. const result =
  582. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  583. this.user
  584. )
  585. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  586. })
  587. it('is false for subscriptions with pending plan', async function () {
  588. this.paymentRecord.service = 'recurly'
  589. this.individualSubscription.pendingPlan = {} // anything
  590. const result =
  591. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  592. this.user
  593. )
  594. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  595. })
  596. it('is false for a group subscription', async function () {
  597. this.paymentRecord.service = 'recurly'
  598. this.individualSubscription.groupPlan = true
  599. const result =
  600. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  601. this.user
  602. )
  603. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  604. })
  605. it('is false when in trial', async function () {
  606. this.paymentRecord.service = 'recurly'
  607. const msIn24Hours = 24 * 60 * 60 * 1000
  608. const tomorrow = new Date(Date.now() + msIn24Hours)
  609. this.paymentRecord.trialPeriodEnd = tomorrow
  610. const result =
  611. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  612. this.user
  613. )
  614. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  615. })
  616. it('is false for annual subscriptions', async function () {
  617. this.paymentRecord.service = 'recurly'
  618. this.paymentRecord.planCode = 'collaborator-annual'
  619. const result =
  620. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  621. this.user
  622. )
  623. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  624. })
  625. it('is false for subscriptions with add-ons', async function () {
  626. this.paymentRecord.service = 'recurly'
  627. this.paymentRecord.addOns = [{}] // anything
  628. const result =
  629. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  630. this.user
  631. )
  632. assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
  633. })
  634. it('is true when conditions are met', async function () {
  635. this.paymentRecord.service = 'recurly'
  636. this.paymentRecord.addOns = []
  637. const result =
  638. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  639. this.user
  640. )
  641. assert.isTrue(result.personalSubscription.payment.isEligibleForPause)
  642. })
  643. })
  644. describe('isEligibleForDowngradeUpsell', function () {
  645. it('is true for eligible individual subscriptions', async function () {
  646. this.paymentRecord.pausePeriodStart = null
  647. this.paymentRecord.remainingPauseCycles = null
  648. this.paymentRecord.trialPeriodEnd = null
  649. this.paymentRecord.service = 'recurly'
  650. const result =
  651. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  652. this.user
  653. )
  654. assert.isTrue(
  655. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  656. )
  657. })
  658. it('is false for group plans', async function () {
  659. this.individualSubscription.planCode = this.groupPlanCode
  660. this.paymentRecord.pausePeriodStart = null
  661. this.paymentRecord.remainingPauseCycles = null
  662. this.paymentRecord.trialPeriodEnd = null
  663. this.paymentRecord.service = 'recurly'
  664. const result =
  665. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  666. this.user
  667. )
  668. assert.isFalse(
  669. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  670. )
  671. })
  672. it('is false for annual individual plans', async function () {
  673. this.individualSubscription.planCode = this.annualPlanCode
  674. this.paymentRecord.pausePeriodStart = null
  675. this.paymentRecord.remainingPauseCycles = null
  676. this.paymentRecord.trialPeriodEnd = null
  677. this.paymentRecord.service = 'recurly'
  678. const result =
  679. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  680. this.user
  681. )
  682. assert.isFalse(
  683. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  684. )
  685. })
  686. it('is false for paused plans', async function () {
  687. this.paymentRecord.pausePeriodStart = new Date()
  688. this.paymentRecord.remainingPauseCycles = 1
  689. this.paymentRecord.trialPeriodEnd = null
  690. this.paymentRecord.service = 'recurly'
  691. const result =
  692. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  693. this.user
  694. )
  695. assert.isFalse(
  696. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  697. )
  698. })
  699. it('is false for plans in free trial period', async function () {
  700. this.paymentRecord.pausePeriodStart = null
  701. this.paymentRecord.remainingPauseCycles = null
  702. this.paymentRecord.trialPeriodEnd = new Date(
  703. Date.now() + 24 * 60 * 60 * 1000 // tomorrow
  704. )
  705. this.paymentRecord.service = 'recurly'
  706. const result =
  707. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  708. this.user
  709. )
  710. assert.isFalse(
  711. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  712. )
  713. })
  714. it('is false for Stripe subscriptions', async function () {
  715. this.paymentRecord.pausePeriodStart = null
  716. this.paymentRecord.remainingPauseCycles = null
  717. this.paymentRecord.trialPeriodEnd = null
  718. this.paymentRecord.service = 'stripe'
  719. const result =
  720. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  721. this.user
  722. )
  723. assert.isFalse(
  724. result.personalSubscription.payment.isEligibleForDowngradeUpsell
  725. )
  726. })
  727. })
  728. it('includes pending changes', async function () {
  729. this.paymentRecord.pendingChange =
  730. new PaymentProviderSubscriptionChange({
  731. subscription: this.paymentRecord,
  732. nextPlanCode: this.groupPlanCode,
  733. nextPlanName: 'Group Collaborator (Annual) 4 licenses',
  734. nextPlanPrice: 1400,
  735. nextAddOns: [
  736. new PaymentProviderSubscriptionAddOn({
  737. code: 'additional-license',
  738. name: 'additional license',
  739. quantity: 8,
  740. unitPrice: 24.4,
  741. }),
  742. new PaymentProviderSubscriptionAddOn({
  743. code: 'addon-code',
  744. name: 'addon name',
  745. quantity: 1,
  746. unitPrice: 2,
  747. }),
  748. ],
  749. })
  750. this.Modules.hooks.fire
  751. .withArgs('getPaymentFromRecord', this.individualSubscription)
  752. .yields(null, [
  753. {
  754. subscription: this.paymentRecord,
  755. account: {},
  756. coupons: [],
  757. },
  758. ])
  759. const result =
  760. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  761. this.user
  762. )
  763. assert.equal(
  764. result.personalSubscription.payment.displayPrice,
  765. '€1,756.92'
  766. )
  767. assert.equal(
  768. result.personalSubscription.payment.planOnlyDisplayPrice,
  769. '€1,754.72'
  770. )
  771. assert.deepEqual(
  772. result.personalSubscription.payment
  773. .addOnDisplayPricesWithoutAdditionalLicense,
  774. { 'addon-code': '€2.20' }
  775. )
  776. assert.equal(
  777. result.personalSubscription.payment.pendingAdditionalLicenses,
  778. 8
  779. )
  780. assert.equal(
  781. result.personalSubscription.payment.pendingTotalLicenses,
  782. 12
  783. )
  784. })
  785. it('does not add a billing details link for a Stripe subscription', async function () {
  786. this.paymentRecord.service = 'stripe'
  787. this.Modules.hooks.fire
  788. .withArgs('getPaymentFromRecord', this.individualSubscription)
  789. .yields(null, [
  790. {
  791. subscription: this.paymentRecord,
  792. account: new PaymentProviderAccount({}),
  793. coupons: [],
  794. },
  795. ])
  796. const result =
  797. await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  798. this.user
  799. )
  800. assert.equal(
  801. result.personalSubscription.payment.billingDetailsLink,
  802. undefined
  803. )
  804. assert.equal(
  805. result.personalSubscription.payment.accountManagementLink,
  806. '/user/subscription/payment/account-management'
  807. )
  808. })
  809. })
  810. })
  811. })