SubscriptionViewModelBuilder.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. const Settings = require('@overleaf/settings')
  2. const RecurlyWrapper = require('./RecurlyWrapper')
  3. const PlansLocator = require('./PlansLocator')
  4. const SubscriptionFormatters = require('./SubscriptionFormatters')
  5. const SubscriptionLocator = require('./SubscriptionLocator')
  6. const V1SubscriptionManager = require('./V1SubscriptionManager')
  7. const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
  8. const PublishersGetter = require('../Publishers/PublishersGetter')
  9. const sanitizeHtml = require('sanitize-html')
  10. const _ = require('underscore')
  11. const async = require('async')
  12. const SubscriptionHelper = require('./SubscriptionHelper')
  13. const { promisify } = require('../../util/promises')
  14. function buildHostedLink(recurlySubscription, type) {
  15. const recurlySubdomain = Settings.apis.recurly.subdomain
  16. const hostedLoginToken = recurlySubscription.account.hosted_login_token
  17. let path = ''
  18. if (type === 'billingDetails') {
  19. path = 'billing_info/edit?ht='
  20. }
  21. if (hostedLoginToken && recurlySubdomain) {
  22. return [
  23. 'https://',
  24. recurlySubdomain,
  25. '.recurly.com/account/',
  26. path,
  27. hostedLoginToken,
  28. ].join('')
  29. }
  30. }
  31. function buildUsersSubscriptionViewModel(user, callback) {
  32. async.auto(
  33. {
  34. personalSubscription(cb) {
  35. SubscriptionLocator.getUsersSubscription(user, cb)
  36. },
  37. recurlySubscription: [
  38. 'personalSubscription',
  39. (cb, { personalSubscription }) => {
  40. if (
  41. personalSubscription == null ||
  42. personalSubscription.recurlySubscription_id == null ||
  43. personalSubscription.recurlySubscription_id === ''
  44. ) {
  45. return cb(null, null)
  46. }
  47. RecurlyWrapper.getSubscription(
  48. personalSubscription.recurlySubscription_id,
  49. { includeAccount: true },
  50. cb
  51. )
  52. },
  53. ],
  54. recurlyCoupons: [
  55. 'recurlySubscription',
  56. (cb, { recurlySubscription }) => {
  57. if (!recurlySubscription) {
  58. return cb(null, null)
  59. }
  60. const accountId = recurlySubscription.account.account_code
  61. RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
  62. },
  63. ],
  64. plan: [
  65. 'personalSubscription',
  66. (cb, { personalSubscription }) => {
  67. if (personalSubscription == null) {
  68. return cb()
  69. }
  70. const plan = PlansLocator.findLocalPlanInSettings(
  71. personalSubscription.planCode
  72. )
  73. if (plan == null) {
  74. return cb(
  75. new Error(
  76. `No plan found for planCode '${personalSubscription.planCode}'`
  77. )
  78. )
  79. }
  80. cb(null, plan)
  81. },
  82. ],
  83. memberGroupSubscriptions(cb) {
  84. SubscriptionLocator.getMemberSubscriptions(user, cb)
  85. },
  86. managedGroupSubscriptions(cb) {
  87. SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
  88. },
  89. confirmedMemberAffiliations(cb) {
  90. InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
  91. },
  92. managedInstitutions(cb) {
  93. InstitutionsGetter.getManagedInstitutions(user._id, cb)
  94. },
  95. managedPublishers(cb) {
  96. PublishersGetter.getManagedPublishers(user._id, cb)
  97. },
  98. v1SubscriptionStatus(cb) {
  99. V1SubscriptionManager.getSubscriptionStatusFromV1(
  100. user._id,
  101. (error, status, v1Id) => {
  102. if (error) {
  103. return cb(error)
  104. }
  105. cb(null, status)
  106. }
  107. )
  108. },
  109. },
  110. (err, results) => {
  111. if (err) {
  112. return callback(err)
  113. }
  114. let {
  115. personalSubscription,
  116. memberGroupSubscriptions,
  117. managedGroupSubscriptions,
  118. confirmedMemberAffiliations,
  119. managedInstitutions,
  120. managedPublishers,
  121. v1SubscriptionStatus,
  122. recurlySubscription,
  123. recurlyCoupons,
  124. plan,
  125. } = results
  126. if (memberGroupSubscriptions == null) {
  127. memberGroupSubscriptions = []
  128. }
  129. if (managedGroupSubscriptions == null) {
  130. managedGroupSubscriptions = []
  131. }
  132. if (confirmedMemberAffiliations == null) {
  133. confirmedMemberAffiliations = []
  134. }
  135. if (managedInstitutions == null) {
  136. managedInstitutions = []
  137. }
  138. if (v1SubscriptionStatus == null) {
  139. v1SubscriptionStatus = {}
  140. }
  141. if (recurlyCoupons == null) {
  142. recurlyCoupons = []
  143. }
  144. if (
  145. personalSubscription &&
  146. typeof personalSubscription.toObject === 'function'
  147. ) {
  148. // Downgrade from Mongoose object, so we can add a recurly and plan attribute
  149. personalSubscription = personalSubscription.toObject()
  150. }
  151. if (plan != null) {
  152. personalSubscription.plan = plan
  153. }
  154. if (personalSubscription && recurlySubscription) {
  155. const tax = recurlySubscription.tax_in_cents || 0
  156. // Some plans allow adding more seats than the base plan provides.
  157. // This is recorded as a subscription add on.
  158. // Note: tax_in_cents already includes the tax for any addon.
  159. let addOnPrice = 0
  160. let additionalLicenses = 0
  161. if (
  162. plan.membersLimitAddOn &&
  163. Array.isArray(recurlySubscription.subscription_add_ons)
  164. ) {
  165. recurlySubscription.subscription_add_ons.forEach(addOn => {
  166. if (addOn.add_on_code === plan.membersLimitAddOn) {
  167. addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
  168. additionalLicenses += addOn.quantity
  169. }
  170. })
  171. }
  172. const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
  173. personalSubscription.recurly = {
  174. tax,
  175. taxRate: recurlySubscription.tax_rate
  176. ? parseFloat(recurlySubscription.tax_rate._)
  177. : 0,
  178. billingDetailsLink: buildHostedLink(
  179. recurlySubscription,
  180. 'billingDetails'
  181. ),
  182. accountManagementLink: buildHostedLink(recurlySubscription),
  183. additionalLicenses,
  184. totalLicenses,
  185. nextPaymentDueAt: SubscriptionFormatters.formatDate(
  186. recurlySubscription.current_period_ends_at
  187. ),
  188. currency: recurlySubscription.currency,
  189. state: recurlySubscription.state,
  190. trialEndsAtFormatted: SubscriptionFormatters.formatDate(
  191. recurlySubscription.trial_ends_at
  192. ),
  193. trial_ends_at: recurlySubscription.trial_ends_at,
  194. activeCoupons: recurlyCoupons,
  195. account: recurlySubscription.account,
  196. }
  197. if (recurlySubscription.pending_subscription) {
  198. const pendingPlan = PlansLocator.findLocalPlanInSettings(
  199. recurlySubscription.pending_subscription.plan.plan_code
  200. )
  201. if (pendingPlan == null) {
  202. return callback(
  203. new Error(
  204. `No plan found for planCode '${personalSubscription.planCode}'`
  205. )
  206. )
  207. }
  208. let pendingAdditionalLicenses = 0
  209. let pendingAddOnTax = 0
  210. let pendingAddOnPrice = 0
  211. if (recurlySubscription.pending_subscription.subscription_add_ons) {
  212. if (
  213. pendingPlan.membersLimitAddOn &&
  214. Array.isArray(
  215. recurlySubscription.pending_subscription.subscription_add_ons
  216. )
  217. ) {
  218. recurlySubscription.pending_subscription.subscription_add_ons.forEach(
  219. addOn => {
  220. if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
  221. pendingAddOnPrice +=
  222. addOn.quantity * addOn.unit_amount_in_cents
  223. pendingAdditionalLicenses += addOn.quantity
  224. }
  225. }
  226. )
  227. }
  228. // Need to calculate tax ourselves as we don't get tax amounts for pending subs
  229. pendingAddOnTax =
  230. personalSubscription.recurly.taxRate * pendingAddOnPrice
  231. }
  232. const pendingSubscriptionTax =
  233. personalSubscription.recurly.taxRate *
  234. recurlySubscription.pending_subscription.unit_amount_in_cents
  235. personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
  236. recurlySubscription.pending_subscription.unit_amount_in_cents +
  237. pendingAddOnPrice +
  238. pendingAddOnTax +
  239. pendingSubscriptionTax,
  240. recurlySubscription.currency
  241. )
  242. const pendingTotalLicenses =
  243. (pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
  244. personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
  245. personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
  246. personalSubscription.pendingPlan = pendingPlan
  247. } else {
  248. personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
  249. recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
  250. recurlySubscription.currency
  251. )
  252. }
  253. }
  254. for (const memberGroupSubscription of memberGroupSubscriptions) {
  255. if (memberGroupSubscription.teamNotice) {
  256. memberGroupSubscription.teamNotice = sanitizeHtml(
  257. memberGroupSubscription.teamNotice
  258. )
  259. }
  260. }
  261. callback(null, {
  262. personalSubscription,
  263. managedGroupSubscriptions,
  264. memberGroupSubscriptions,
  265. confirmedMemberAffiliations,
  266. managedInstitutions,
  267. managedPublishers,
  268. v1SubscriptionStatus,
  269. })
  270. }
  271. )
  272. }
  273. function buildPlansList(currentPlan) {
  274. const { plans } = Settings
  275. const allPlans = {}
  276. plans.forEach(plan => {
  277. allPlans[plan.planCode] = plan
  278. })
  279. const result = { allPlans }
  280. if (currentPlan) {
  281. result.planCodesChangingAtTermEnd = _.pluck(
  282. _.filter(plans, plan => {
  283. if (!plan.hideFromUsers) {
  284. return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
  285. }
  286. }),
  287. 'planCode'
  288. )
  289. }
  290. result.studentAccounts = _.filter(
  291. plans,
  292. plan => plan.planCode.indexOf('student') !== -1
  293. )
  294. result.groupMonthlyPlans = _.filter(
  295. plans,
  296. plan => plan.groupPlan && !plan.annual
  297. )
  298. result.groupAnnualPlans = _.filter(
  299. plans,
  300. plan => plan.groupPlan && plan.annual
  301. )
  302. result.individualMonthlyPlans = _.filter(
  303. plans,
  304. plan =>
  305. !plan.groupPlan &&
  306. !plan.annual &&
  307. plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
  308. plan.planCode.indexOf('student') === -1
  309. )
  310. result.individualAnnualPlans = _.filter(
  311. plans,
  312. plan =>
  313. !plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1
  314. )
  315. return result
  316. }
  317. module.exports = {
  318. buildUsersSubscriptionViewModel,
  319. buildPlansList,
  320. promises: {
  321. buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),
  322. },
  323. }