SubscriptionViewModelBuilderTests.js 35 KB

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