SubscriptionGroupHandler.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. const { callbackify } = require('util')
  2. const SubscriptionUpdater = require('./SubscriptionUpdater')
  3. const SubscriptionLocator = require('./SubscriptionLocator')
  4. const SubscriptionController = require('./SubscriptionController')
  5. const { Subscription } = require('../../models/Subscription')
  6. const SessionManager = require('../Authentication/SessionManager')
  7. const RecurlyClient = require('./RecurlyClient')
  8. const PlansLocator = require('./PlansLocator')
  9. const SubscriptionHandler = require('./SubscriptionHandler')
  10. const GroupPlansData = require('./GroupPlansData')
  11. const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./RecurlyEntities')
  12. async function removeUserFromGroup(subscriptionId, userIdToRemove) {
  13. await SubscriptionUpdater.promises.removeUserFromGroup(
  14. subscriptionId,
  15. userIdToRemove
  16. )
  17. }
  18. async function replaceUserReferencesInGroups(oldId, newId) {
  19. await Subscription.updateOne({ admin_id: oldId }, { admin_id: newId }).exec()
  20. await _replaceInArray(Subscription, 'manager_ids', oldId, newId)
  21. await _replaceInArray(Subscription, 'member_ids', oldId, newId)
  22. }
  23. async function isUserPartOfGroup(userId, subscriptionId) {
  24. const subscription =
  25. await SubscriptionLocator.promises.getSubscriptionByMemberIdAndId(
  26. userId,
  27. subscriptionId
  28. )
  29. return !!subscription
  30. }
  31. async function getTotalConfirmedUsersInGroup(subscriptionId) {
  32. const subscription =
  33. await SubscriptionLocator.promises.getSubscription(subscriptionId)
  34. return subscription?.member_ids?.length
  35. }
  36. async function _replaceInArray(model, property, oldValue, newValue) {
  37. // Mongo won't let us pull and addToSet in the same query, so do it in
  38. // two. Note we need to add first, since the query is based on the old user.
  39. const query = {}
  40. query[property] = oldValue
  41. const setNewValue = {}
  42. setNewValue[property] = newValue
  43. const setOldValue = {}
  44. setOldValue[property] = oldValue
  45. await model.updateMany(query, { $addToSet: setNewValue })
  46. await model.updateMany(query, { $pull: setOldValue })
  47. }
  48. async function ensureFlexibleLicensingEnabled(plan) {
  49. if (!plan?.canUseFlexibleLicensing) {
  50. throw new Error('The group plan does not support flexible licensing')
  51. }
  52. }
  53. async function ensureAddSeatsEnabled(plan) {
  54. if (!plan?.membersLimitAddOn) {
  55. throw new Error('The group plan does not support adding seats')
  56. }
  57. }
  58. async function getUsersGroupSubscriptionDetails(req) {
  59. const userId = SessionManager.getLoggedInUserId(req.session)
  60. const subscription =
  61. await SubscriptionLocator.promises.getUsersSubscription(userId)
  62. if (!subscription.groupPlan) {
  63. throw new Error('User subscription is not a group plan')
  64. }
  65. const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  66. const recurlySubscription = await RecurlyClient.promises.getSubscription(
  67. subscription.recurlySubscription_id
  68. )
  69. return {
  70. subscription,
  71. recurlySubscription,
  72. plan,
  73. }
  74. }
  75. async function _addSeatsSubscriptionChange(req) {
  76. const adding = req.body.adding
  77. const { recurlySubscription, plan } =
  78. await getUsersGroupSubscriptionDetails(req)
  79. await ensureFlexibleLicensingEnabled(plan)
  80. await ensureAddSeatsEnabled(plan)
  81. const userId = SessionManager.getLoggedInUserId(req.session)
  82. const currentAddonQuantity =
  83. recurlySubscription.addOns.find(
  84. addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
  85. )?.quantity ?? 0
  86. // Keeps only the new total quantity of addon
  87. const nextAddonQuantity = currentAddonQuantity + adding
  88. let changeRequest
  89. if (recurlySubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)) {
  90. // Not providing a custom price as once the subscription is locked
  91. // to an add-on at a given price, it will use it for subsequent payments
  92. changeRequest = recurlySubscription.getRequestForAddOnUpdate(
  93. MEMBERS_LIMIT_ADD_ON_CODE,
  94. nextAddonQuantity
  95. )
  96. } else {
  97. let unitPrice
  98. const newPlanPricesAppliedAt = new Date('2025-01-08T14:00:00Z')
  99. const isLegacyPriceApplicable =
  100. new Date(recurlySubscription.createdAt) < newPlanPricesAppliedAt
  101. if (isLegacyPriceApplicable) {
  102. const pattern =
  103. /^group_(collaborator|professional)_(5|10|20|50)_(educational|enterprise)$/
  104. const [, planCode, size, usage] = plan.planCode.match(pattern)
  105. const currency = recurlySubscription.currency
  106. const legacyPriceInCents =
  107. GroupPlansData[usage][planCode][currency][size]
  108. .additional_license_legacy_price_in_cents
  109. if (legacyPriceInCents > 0) {
  110. unitPrice = legacyPriceInCents / 100
  111. }
  112. }
  113. changeRequest = recurlySubscription.getRequestForAddOnPurchase(
  114. MEMBERS_LIMIT_ADD_ON_CODE,
  115. nextAddonQuantity,
  116. unitPrice
  117. )
  118. }
  119. return {
  120. changeRequest,
  121. userId,
  122. currentAddonQuantity,
  123. recurlySubscription,
  124. }
  125. }
  126. async function previewAddSeatsSubscriptionChange(req) {
  127. const { changeRequest, userId, currentAddonQuantity } =
  128. await _addSeatsSubscriptionChange(req)
  129. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
  130. const subscriptionChange =
  131. await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
  132. const subscriptionChangePreview =
  133. await SubscriptionController.makeChangePreview(
  134. {
  135. type: 'add-on-update',
  136. addOn: {
  137. code: MEMBERS_LIMIT_ADD_ON_CODE,
  138. quantity: subscriptionChange.nextAddOns.find(
  139. addon => addon.code === MEMBERS_LIMIT_ADD_ON_CODE
  140. ).quantity,
  141. prevQuantity: currentAddonQuantity,
  142. },
  143. },
  144. subscriptionChange,
  145. paymentMethod
  146. )
  147. return subscriptionChangePreview
  148. }
  149. async function createAddSeatsSubscriptionChange(req) {
  150. const { changeRequest, userId, recurlySubscription } =
  151. await _addSeatsSubscriptionChange(req)
  152. await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
  153. await SubscriptionHandler.promises.syncSubscription(
  154. { uuid: recurlySubscription.id },
  155. userId
  156. )
  157. return { adding: req.body.adding }
  158. }
  159. async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
  160. if (
  161. subscription.planCode.includes('professional') ||
  162. !subscription.groupPlan
  163. ) {
  164. throw new Error('Not eligible for group plan upgrade')
  165. }
  166. return subscription.planCode.includes('educational')
  167. ? 'group_professional_educational'
  168. : 'group_professional'
  169. }
  170. async function _getGroupPlanUpgradeChangeRequest(ownerId) {
  171. const olSubscription =
  172. await SubscriptionLocator.promises.getUsersSubscription(ownerId)
  173. const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
  174. const recurlySubscription = await RecurlyClient.promises.getSubscription(
  175. olSubscription.recurlySubscription_id
  176. )
  177. return recurlySubscription.getRequestForGroupPlanUpgrade(
  178. newPlanCode,
  179. olSubscription.membersLimit
  180. )
  181. }
  182. function _getPlanNameForDisplay(subscription) {
  183. if (/^group_collaborator_\d+_enterprise$/.test(subscription.planCode)) {
  184. return 'Overleaf Standard Group'
  185. }
  186. if (/^group_collaborator_\d+_educational$/.test(subscription.planCode)) {
  187. return 'Overleaf Standard Group Educational'
  188. }
  189. return subscription.planName
  190. }
  191. async function getGroupPlanUpgradePreview(ownerId) {
  192. const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
  193. const subscriptionChange =
  194. await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
  195. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
  196. return SubscriptionController.makeChangePreview(
  197. {
  198. type: 'group-plan-upgrade',
  199. prevPlan: {
  200. name: _getPlanNameForDisplay(subscriptionChange.subscription),
  201. },
  202. },
  203. subscriptionChange,
  204. paymentMethod
  205. )
  206. }
  207. async function upgradeGroupPlan(ownerId) {
  208. const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
  209. await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
  210. await SubscriptionHandler.promises.syncSubscription(
  211. { uuid: changeRequest.subscription.id },
  212. ownerId
  213. )
  214. }
  215. module.exports = {
  216. removeUserFromGroup: callbackify(removeUserFromGroup),
  217. replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
  218. ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
  219. ensureAddSeatsEnabled: callbackify(ensureAddSeatsEnabled),
  220. getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
  221. isUserPartOfGroup: callbackify(isUserPartOfGroup),
  222. getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
  223. upgradeGroupPlan: callbackify(upgradeGroupPlan),
  224. promises: {
  225. removeUserFromGroup,
  226. replaceUserReferencesInGroups,
  227. ensureFlexibleLicensingEnabled,
  228. ensureAddSeatsEnabled,
  229. getTotalConfirmedUsersInGroup,
  230. isUserPartOfGroup,
  231. getUsersGroupSubscriptionDetails,
  232. previewAddSeatsSubscriptionChange,
  233. createAddSeatsSubscriptionChange,
  234. getGroupPlanUpgradePreview,
  235. upgradeGroupPlan,
  236. },
  237. }