SubscriptionViewModelBuilderTests.js 32 KB

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