SubscriptionViewModelBuilder.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. // ts-check
  2. const Settings = require('@overleaf/settings')
  3. const RecurlyWrapper = require('./RecurlyWrapper')
  4. const PlansLocator = require('./PlansLocator')
  5. const {
  6. isStandaloneAiAddOnPlanCode,
  7. MEMBERS_LIMIT_ADD_ON_CODE,
  8. } = require('./RecurlyEntities')
  9. const SubscriptionFormatters = require('./SubscriptionFormatters')
  10. const SubscriptionLocator = require('./SubscriptionLocator')
  11. const SubscriptionUpdater = require('./SubscriptionUpdater')
  12. const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
  13. const InstitutionsManager = require('../Institutions/InstitutionsManager')
  14. const PublishersGetter = require('../Publishers/PublishersGetter')
  15. const sanitizeHtml = require('sanitize-html')
  16. const _ = require('lodash')
  17. const async = require('async')
  18. const SubscriptionHelper = require('./SubscriptionHelper')
  19. const { callbackify } = require('@overleaf/promise-utils')
  20. const {
  21. InvalidError,
  22. NotFoundError,
  23. V1ConnectionError,
  24. } = require('../Errors/Errors')
  25. const FeaturesHelper = require('./FeaturesHelper')
  26. /**
  27. * @import { Subscription } from "../../../../types/project/dashboard/subscription"
  28. */
  29. function buildHostedLink(type) {
  30. return `/user/subscription/recurly/${type}`
  31. }
  32. // Downgrade from Mongoose object, so we can add custom attributes to object
  33. function serializeMongooseObject(object) {
  34. return object && typeof object.toObject === 'function'
  35. ? object.toObject()
  36. : object
  37. }
  38. async function getRedirectToHostedPage(userId, pageType) {
  39. if (!['billing-details', 'account-management'].includes(pageType)) {
  40. throw new InvalidError('unexpected page type')
  41. }
  42. const personalSubscription =
  43. await SubscriptionLocator.promises.getUsersSubscription(userId)
  44. const recurlySubscriptionId = personalSubscription?.recurlySubscription_id
  45. if (!recurlySubscriptionId) {
  46. throw new NotFoundError('not a recurly subscription')
  47. }
  48. const recurlySubscription = await RecurlyWrapper.promises.getSubscription(
  49. recurlySubscriptionId,
  50. { includeAccount: true }
  51. )
  52. const recurlySubdomain = Settings.apis.recurly.subdomain
  53. const hostedLoginToken = recurlySubscription.account.hosted_login_token
  54. if (!hostedLoginToken) {
  55. throw new Error('recurly account does not have hosted login token')
  56. }
  57. let path = ''
  58. if (pageType === 'billing-details') {
  59. path = 'billing_info/edit?ht='
  60. }
  61. return [
  62. 'https://',
  63. recurlySubdomain,
  64. '.recurly.com/account/',
  65. path,
  66. hostedLoginToken,
  67. ].join('')
  68. }
  69. async function buildUsersSubscriptionViewModel(user, locale = 'en') {
  70. let {
  71. personalSubscription,
  72. memberGroupSubscriptions,
  73. managedGroupSubscriptions,
  74. currentInstitutionsWithLicence,
  75. managedInstitutions,
  76. managedPublishers,
  77. recurlySubscription,
  78. recurlyCoupons,
  79. plan,
  80. } = await async.auto({
  81. personalSubscription(cb) {
  82. SubscriptionLocator.getUsersSubscription(user, cb)
  83. },
  84. recurlySubscription: [
  85. 'personalSubscription',
  86. ({ personalSubscription }, cb) => {
  87. if (
  88. personalSubscription == null ||
  89. personalSubscription.recurlySubscription_id == null ||
  90. personalSubscription.recurlySubscription_id === ''
  91. ) {
  92. return cb(null, null)
  93. }
  94. RecurlyWrapper.getSubscription(
  95. personalSubscription.recurlySubscription_id,
  96. { includeAccount: true },
  97. cb
  98. )
  99. },
  100. ],
  101. recurlyCoupons: [
  102. 'recurlySubscription',
  103. ({ recurlySubscription }, cb) => {
  104. if (!recurlySubscription) {
  105. return cb(null, null)
  106. }
  107. const accountId = recurlySubscription.account.account_code
  108. RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
  109. },
  110. ],
  111. plan: [
  112. 'personalSubscription',
  113. ({ personalSubscription }, cb) => {
  114. if (personalSubscription == null) {
  115. return cb()
  116. }
  117. const plan = PlansLocator.findLocalPlanInSettings(
  118. personalSubscription.planCode
  119. )
  120. if (plan == null) {
  121. return cb(
  122. new Error(
  123. `No plan found for planCode '${personalSubscription.planCode}'`
  124. )
  125. )
  126. }
  127. cb(null, plan)
  128. },
  129. ],
  130. memberGroupSubscriptions(cb) {
  131. SubscriptionLocator.getMemberSubscriptions(user, cb)
  132. },
  133. managedGroupSubscriptions(cb) {
  134. SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
  135. },
  136. currentInstitutionsWithLicence(cb) {
  137. InstitutionsGetter.getCurrentInstitutionsWithLicence(
  138. user._id,
  139. (error, institutions) => {
  140. if (error instanceof V1ConnectionError) {
  141. return cb(null, false)
  142. }
  143. cb(null, institutions)
  144. }
  145. )
  146. },
  147. managedInstitutions(cb) {
  148. InstitutionsGetter.getManagedInstitutions(user._id, cb)
  149. },
  150. managedPublishers(cb) {
  151. PublishersGetter.getManagedPublishers(user._id, cb)
  152. },
  153. })
  154. if (memberGroupSubscriptions == null) {
  155. memberGroupSubscriptions = []
  156. } else {
  157. memberGroupSubscriptions = memberGroupSubscriptions.map(group => {
  158. const userIsGroupManager = group.manager_ids?.some(
  159. id => id.toString() === user._id.toString()
  160. )
  161. const groupDataForView = {
  162. _id: group._id,
  163. planCode: group.planCode,
  164. teamName: group.teamName,
  165. admin_id: {
  166. email: group.admin_id.email,
  167. },
  168. userIsGroupManager,
  169. }
  170. if (group.teamNotice) {
  171. groupDataForView.teamNotice = sanitizeHtml(group.teamNotice)
  172. }
  173. buildGroupSubscriptionForView(groupDataForView)
  174. return groupDataForView
  175. })
  176. }
  177. if (managedGroupSubscriptions == null) {
  178. managedGroupSubscriptions = []
  179. } else {
  180. managedGroupSubscriptions = managedGroupSubscriptions.map(group => {
  181. const userIsGroupMember = group.member_ids?.some(
  182. id => id.toString() === user._id.toString()
  183. )
  184. const groupDataForView = {
  185. _id: group._id,
  186. planCode: group.planCode,
  187. groupPlan: group.groupPlan,
  188. teamName: group.teamName,
  189. admin_id: {
  190. _id: group.admin_id._id,
  191. email: group.admin_id.email,
  192. },
  193. features: group.features,
  194. userIsGroupMember,
  195. }
  196. buildGroupSubscriptionForView(groupDataForView)
  197. return groupDataForView
  198. })
  199. }
  200. if (managedInstitutions == null) {
  201. managedInstitutions = []
  202. }
  203. if (recurlyCoupons == null) {
  204. recurlyCoupons = []
  205. }
  206. personalSubscription = serializeMongooseObject(personalSubscription)
  207. managedInstitutions = managedInstitutions.map(serializeMongooseObject)
  208. await Promise.all(
  209. managedInstitutions.map(InstitutionsManager.promises.fetchV1Data)
  210. )
  211. managedPublishers = managedPublishers.map(serializeMongooseObject)
  212. await Promise.all(
  213. managedPublishers.map(PublishersGetter.promises.fetchV1Data)
  214. )
  215. if (plan != null) {
  216. personalSubscription.plan = plan
  217. }
  218. // Subscription DB object contains a recurly property, used to cache trial info
  219. // on the project-list. However, this can cause the wrong template to render,
  220. // if we do not have any subscription data from Recurly (recurlySubscription)
  221. // TODO: Delete this workaround once recurly cache property name migration rolled out.
  222. if (personalSubscription) {
  223. delete personalSubscription.recurly
  224. }
  225. function getPlanOnlyDisplayPrice(
  226. totalPlanPriceInCents,
  227. taxRate,
  228. addOns = []
  229. ) {
  230. // The MEMBERS_LIMIT_ADD_ON_CODE is considered as part of the new plan model
  231. const allAddOnsPriceInCentsExceptAdditionalLicensePrice = addOns.reduce(
  232. (prev, curr) => {
  233. return curr.add_on_code !== MEMBERS_LIMIT_ADD_ON_CODE
  234. ? curr.quantity * curr.unit_amount_in_cents + prev
  235. : prev
  236. },
  237. 0
  238. )
  239. const allAddOnsTotalPriceInCentsExceptAdditionalLicensePrice =
  240. allAddOnsPriceInCentsExceptAdditionalLicensePrice +
  241. allAddOnsPriceInCentsExceptAdditionalLicensePrice * taxRate
  242. return SubscriptionFormatters.formatPriceLocalized(
  243. totalPlanPriceInCents -
  244. allAddOnsTotalPriceInCentsExceptAdditionalLicensePrice,
  245. recurlySubscription.currency,
  246. locale
  247. )
  248. }
  249. function getAddOnDisplayPricesWithoutAdditionalLicense(taxRate, addOns = []) {
  250. return addOns.reduce((prev, curr) => {
  251. if (curr.add_on_code !== MEMBERS_LIMIT_ADD_ON_CODE) {
  252. const priceInCents = curr.quantity * curr.unit_amount_in_cents
  253. const totalPriceInCents = priceInCents + priceInCents * taxRate
  254. if (totalPriceInCents > 0) {
  255. prev[curr.add_on_code] = SubscriptionFormatters.formatPriceLocalized(
  256. totalPriceInCents,
  257. recurlySubscription.currency,
  258. locale
  259. )
  260. }
  261. }
  262. return prev
  263. }, {})
  264. }
  265. if (personalSubscription && recurlySubscription) {
  266. const tax = recurlySubscription.tax_in_cents || 0
  267. // Some plans allow adding more seats than the base plan provides.
  268. // This is recorded as a subscription add on.
  269. // Note: tax_in_cents already includes the tax for any addon.
  270. let addOnPrice = 0
  271. let additionalLicenses = 0
  272. const addOns = recurlySubscription.subscription_add_ons || []
  273. const taxRate = recurlySubscription.tax_rate
  274. ? parseFloat(recurlySubscription.tax_rate._)
  275. : 0
  276. addOns.forEach(addOn => {
  277. addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
  278. if (addOn.add_on_code === plan.membersLimitAddOn) {
  279. additionalLicenses += addOn.quantity
  280. }
  281. })
  282. const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
  283. personalSubscription.recurly = {
  284. tax,
  285. taxRate,
  286. billingDetailsLink: buildHostedLink('billing-details'),
  287. accountManagementLink: buildHostedLink('account-management'),
  288. additionalLicenses,
  289. addOns,
  290. totalLicenses,
  291. nextPaymentDueAt: SubscriptionFormatters.formatDateTime(
  292. recurlySubscription.current_period_ends_at
  293. ),
  294. nextPaymentDueDate: SubscriptionFormatters.formatDate(
  295. recurlySubscription.current_period_ends_at
  296. ),
  297. currency: recurlySubscription.currency,
  298. state: recurlySubscription.state,
  299. trialEndsAtFormatted: SubscriptionFormatters.formatDateTime(
  300. recurlySubscription.trial_ends_at
  301. ),
  302. trial_ends_at: recurlySubscription.trial_ends_at,
  303. activeCoupons: recurlyCoupons,
  304. account: recurlySubscription.account,
  305. pausedAt: recurlySubscription.paused_at,
  306. remainingPauseCycles: recurlySubscription.remaining_pause_cycles,
  307. }
  308. if (recurlySubscription.pending_subscription) {
  309. const pendingPlan = PlansLocator.findLocalPlanInSettings(
  310. recurlySubscription.pending_subscription.plan.plan_code
  311. )
  312. if (pendingPlan == null) {
  313. throw new Error(
  314. `No plan found for planCode '${personalSubscription.planCode}'`
  315. )
  316. }
  317. let pendingAdditionalLicenses = 0
  318. let pendingAddOnTax = 0
  319. let pendingAddOnPrice = 0
  320. if (recurlySubscription.pending_subscription.subscription_add_ons) {
  321. const pendingRecurlyAddons =
  322. recurlySubscription.pending_subscription.subscription_add_ons
  323. pendingRecurlyAddons.forEach(addOn => {
  324. pendingAddOnPrice += addOn.quantity * addOn.unit_amount_in_cents
  325. if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
  326. pendingAdditionalLicenses += addOn.quantity
  327. }
  328. })
  329. // Need to calculate tax ourselves as we don't get tax amounts for pending subs
  330. pendingAddOnTax =
  331. personalSubscription.recurly.taxRate * pendingAddOnPrice
  332. pendingPlan.addOns = pendingRecurlyAddons
  333. }
  334. const pendingSubscriptionTax =
  335. personalSubscription.recurly.taxRate *
  336. recurlySubscription.pending_subscription.unit_amount_in_cents
  337. const totalPriceInCents =
  338. recurlySubscription.pending_subscription.unit_amount_in_cents +
  339. pendingAddOnPrice +
  340. pendingAddOnTax +
  341. pendingSubscriptionTax
  342. personalSubscription.recurly.displayPrice =
  343. SubscriptionFormatters.formatPriceLocalized(
  344. totalPriceInCents,
  345. recurlySubscription.currency,
  346. locale
  347. )
  348. personalSubscription.recurly.currentPlanDisplayPrice =
  349. SubscriptionFormatters.formatPriceLocalized(
  350. recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
  351. recurlySubscription.currency,
  352. locale
  353. )
  354. personalSubscription.recurly.planOnlyDisplayPrice =
  355. getPlanOnlyDisplayPrice(
  356. totalPriceInCents,
  357. taxRate,
  358. recurlySubscription.pending_subscription.subscription_add_ons
  359. )
  360. personalSubscription.recurly.addOnDisplayPricesWithoutAdditionalLicense =
  361. getAddOnDisplayPricesWithoutAdditionalLicense(
  362. taxRate,
  363. recurlySubscription.pending_subscription.subscription_add_ons
  364. )
  365. const pendingTotalLicenses =
  366. (pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
  367. personalSubscription.recurly.pendingAdditionalLicenses =
  368. pendingAdditionalLicenses
  369. personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
  370. personalSubscription.pendingPlan = pendingPlan
  371. } else {
  372. const totalPriceInCents =
  373. recurlySubscription.unit_amount_in_cents + addOnPrice + tax
  374. personalSubscription.recurly.displayPrice =
  375. SubscriptionFormatters.formatPriceLocalized(
  376. totalPriceInCents,
  377. recurlySubscription.currency,
  378. locale
  379. )
  380. personalSubscription.recurly.planOnlyDisplayPrice =
  381. getPlanOnlyDisplayPrice(totalPriceInCents, taxRate, addOns)
  382. personalSubscription.recurly.addOnDisplayPricesWithoutAdditionalLicense =
  383. getAddOnDisplayPricesWithoutAdditionalLicense(taxRate, addOns)
  384. }
  385. }
  386. return {
  387. personalSubscription,
  388. managedGroupSubscriptions,
  389. memberGroupSubscriptions,
  390. currentInstitutionsWithLicence,
  391. managedInstitutions,
  392. managedPublishers,
  393. }
  394. }
  395. /**
  396. * @param {{_id: string}} user
  397. * @returns {Promise<Subscription>}
  398. */
  399. async function getBestSubscription(user) {
  400. let [
  401. individualSubscription,
  402. memberGroupSubscriptions,
  403. currentInstitutionsWithLicence,
  404. ] = await Promise.all([
  405. SubscriptionLocator.promises.getUsersSubscription(user),
  406. SubscriptionLocator.promises.getMemberSubscriptions(user),
  407. InstitutionsGetter.promises.getCurrentInstitutionsWithLicence(user._id),
  408. ])
  409. if (
  410. individualSubscription &&
  411. !individualSubscription.customAccount &&
  412. individualSubscription.recurlySubscription_id &&
  413. !individualSubscription.recurlyStatus?.state
  414. ) {
  415. const recurlySubscription = await RecurlyWrapper.promises.getSubscription(
  416. individualSubscription.recurlySubscription_id,
  417. { includeAccount: true }
  418. )
  419. await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  420. recurlySubscription,
  421. individualSubscription
  422. )
  423. individualSubscription =
  424. await SubscriptionLocator.promises.getUsersSubscription(user)
  425. }
  426. let bestSubscription = {
  427. type: 'free',
  428. }
  429. if (currentInstitutionsWithLicence?.length) {
  430. for (const institutionMembership of currentInstitutionsWithLicence) {
  431. const plan = PlansLocator.findLocalPlanInSettings(
  432. Settings.institutionPlanCode
  433. )
  434. if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
  435. bestSubscription = {
  436. type: 'commons',
  437. subscription: institutionMembership,
  438. plan,
  439. }
  440. }
  441. }
  442. }
  443. if (memberGroupSubscriptions?.length) {
  444. for (const groupSubscription of memberGroupSubscriptions) {
  445. const plan = PlansLocator.findLocalPlanInSettings(
  446. groupSubscription.planCode
  447. )
  448. if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
  449. const groupDataForView = {}
  450. if (groupSubscription.teamName) {
  451. groupDataForView.teamName = groupSubscription.teamName
  452. }
  453. const remainingTrialDays = _getRemainingTrialDays(groupSubscription)
  454. bestSubscription = {
  455. type: 'group',
  456. subscription: groupDataForView,
  457. plan,
  458. remainingTrialDays,
  459. }
  460. }
  461. }
  462. }
  463. if (individualSubscription && !individualSubscription.groupPlan) {
  464. if (
  465. isStandaloneAiAddOnPlanCode(individualSubscription.planCode) &&
  466. bestSubscription.type === 'free'
  467. ) {
  468. bestSubscription = { type: 'standalone-ai-add-on' }
  469. } else {
  470. const plan = PlansLocator.findLocalPlanInSettings(
  471. individualSubscription.planCode
  472. )
  473. if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
  474. const remainingTrialDays = _getRemainingTrialDays(
  475. individualSubscription
  476. )
  477. bestSubscription = {
  478. type: 'individual',
  479. subscription: individualSubscription,
  480. plan,
  481. remainingTrialDays,
  482. }
  483. }
  484. }
  485. }
  486. return bestSubscription
  487. }
  488. function buildPlansList(currentPlan) {
  489. const { plans } = Settings
  490. const allPlans = {}
  491. plans.forEach(plan => {
  492. allPlans[plan.planCode] = plan
  493. })
  494. const result = { allPlans }
  495. if (currentPlan) {
  496. result.planCodesChangingAtTermEnd = _.map(
  497. _.filter(plans, plan => {
  498. if (!plan.hideFromUsers) {
  499. return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
  500. }
  501. }),
  502. 'planCode'
  503. )
  504. }
  505. result.studentAccounts = _.filter(
  506. plans,
  507. plan => plan.planCode.indexOf('student') !== -1
  508. )
  509. result.groupMonthlyPlans = _.filter(
  510. plans,
  511. plan => plan.groupPlan && !plan.annual
  512. )
  513. result.groupAnnualPlans = _.filter(
  514. plans,
  515. plan => plan.groupPlan && plan.annual
  516. )
  517. result.individualMonthlyPlans = _.filter(
  518. plans,
  519. plan =>
  520. !plan.groupPlan &&
  521. !plan.annual &&
  522. plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
  523. plan.planCode.indexOf('student') === -1
  524. )
  525. result.individualAnnualPlans = _.filter(
  526. plans,
  527. plan =>
  528. !plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1
  529. )
  530. return result
  531. }
  532. function _isPlanEqualOrBetter(planA, planB) {
  533. return FeaturesHelper.isFeatureSetBetter(
  534. planA?.features || {},
  535. planB?.features || {}
  536. )
  537. }
  538. function _getRemainingTrialDays(subscription) {
  539. const now = new Date()
  540. const trialEndDate = subscription.recurlyStatus?.trialEndsAt
  541. return trialEndDate && trialEndDate > now
  542. ? Math.ceil(
  543. (trialEndDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
  544. )
  545. : -1
  546. }
  547. function buildGroupSubscriptionForView(groupSubscription) {
  548. // most group plans in Recurly should be in form "group_plancode_size_usage"
  549. const planLevelFromGroupPlanCode = groupSubscription.planCode.substr(6, 12)
  550. if (planLevelFromGroupPlanCode === 'professional') {
  551. groupSubscription.planLevelName = 'Professional'
  552. } else if (planLevelFromGroupPlanCode === 'collaborator') {
  553. groupSubscription.planLevelName = 'Standard'
  554. }
  555. // there are some group subscription entries that have the personal plancodes...
  556. // this fallback tries to still show the right thing in these cases:
  557. if (!groupSubscription.planLevelName) {
  558. if (groupSubscription.planCode.startsWith('professional')) {
  559. groupSubscription.planLevelName = 'Professional'
  560. } else if (groupSubscription.planCode.startsWith('collaborator')) {
  561. groupSubscription.planLevelName = 'Standard'
  562. } else {
  563. // if we still don't have anything, we can show the plan name (eg, v1 Pro):
  564. const plan = PlansLocator.findLocalPlanInSettings(
  565. groupSubscription.planCode
  566. )
  567. groupSubscription.planLevelName = plan
  568. ? plan.name
  569. : groupSubscription.planCode
  570. }
  571. }
  572. }
  573. function buildPlansListForSubscriptionDash(currentPlan) {
  574. const allPlansData = buildPlansList(currentPlan)
  575. const plans = []
  576. // only list individual and visible plans for "change plans" UI
  577. if (allPlansData.studentAccounts) {
  578. plans.push(
  579. ...allPlansData.studentAccounts.filter(plan => !plan.hideFromUsers)
  580. )
  581. }
  582. if (allPlansData.individualMonthlyPlans) {
  583. plans.push(
  584. ...allPlansData.individualMonthlyPlans.filter(plan => !plan.hideFromUsers)
  585. )
  586. }
  587. if (allPlansData.individualAnnualPlans) {
  588. plans.push(
  589. ...allPlansData.individualAnnualPlans.filter(plan => !plan.hideFromUsers)
  590. )
  591. }
  592. return {
  593. plans,
  594. planCodesChangingAtTermEnd: allPlansData.planCodesChangingAtTermEnd,
  595. }
  596. }
  597. module.exports = {
  598. buildUsersSubscriptionViewModel: callbackify(buildUsersSubscriptionViewModel),
  599. buildPlansList,
  600. buildPlansListForSubscriptionDash,
  601. getBestSubscription: callbackify(getBestSubscription),
  602. promises: {
  603. buildUsersSubscriptionViewModel,
  604. getRedirectToHostedPage,
  605. getBestSubscription,
  606. },
  607. }