SubscriptionGroupHandler.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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 getUsersGroupSubscriptionDetails(req) {
  54. const userId = SessionManager.getLoggedInUserId(req.session)
  55. const subscription =
  56. await SubscriptionLocator.promises.getUsersSubscription(userId)
  57. if (!subscription.groupPlan) {
  58. throw new Error('User subscription is not a group plan')
  59. }
  60. const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  61. const recurlySubscription = await RecurlyClient.promises.getSubscription(
  62. subscription.recurlySubscription_id
  63. )
  64. return {
  65. subscription,
  66. recurlySubscription,
  67. plan,
  68. }
  69. }
  70. async function _addSeatsSubscriptionChange(req) {
  71. const adding = req.body.adding
  72. const { recurlySubscription, plan } =
  73. await getUsersGroupSubscriptionDetails(req)
  74. await ensureFlexibleLicensingEnabled(plan)
  75. const userId = SessionManager.getLoggedInUserId(req.session)
  76. const currentAddonQuantity =
  77. recurlySubscription.addOns.find(
  78. addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
  79. )?.quantity ?? 0
  80. // Keeps only the new total quantity of addon
  81. const nextAddonQuantity = currentAddonQuantity + adding
  82. let changeRequest
  83. if (recurlySubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)) {
  84. // Not providing a custom price as once the subscription is locked
  85. // to an add-on at a given price, it will use it for subsequent payments
  86. changeRequest = recurlySubscription.getRequestForAddOnUpdate(
  87. MEMBERS_LIMIT_ADD_ON_CODE,
  88. nextAddonQuantity
  89. )
  90. } else {
  91. let unitPrice
  92. const newPlanPricesAppliedAt = new Date('2025-01-08T14:00:00Z')
  93. const isLegacyPriceApplicable =
  94. new Date(recurlySubscription.createdAt) < newPlanPricesAppliedAt
  95. if (isLegacyPriceApplicable) {
  96. const pattern =
  97. /^group_(collaborator|professional)_(2|3|4|5|10|20|50)_(educational|enterprise)$/
  98. const [, planCode, size, usage] = plan.planCode.match(pattern)
  99. const currency = recurlySubscription.currency
  100. const legacyPriceInCents =
  101. GroupPlansData[usage][planCode][currency][size]
  102. .additional_license_legacy_price_in_cents
  103. if (legacyPriceInCents > 0) {
  104. unitPrice = legacyPriceInCents / 100
  105. }
  106. }
  107. changeRequest = recurlySubscription.getRequestForAddOnPurchase(
  108. MEMBERS_LIMIT_ADD_ON_CODE,
  109. nextAddonQuantity,
  110. unitPrice
  111. )
  112. }
  113. return {
  114. changeRequest,
  115. userId,
  116. currentAddonQuantity,
  117. recurlySubscription,
  118. }
  119. }
  120. async function previewAddSeatsSubscriptionChange(req) {
  121. const { changeRequest, userId, currentAddonQuantity } =
  122. await _addSeatsSubscriptionChange(req)
  123. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
  124. const subscriptionChange =
  125. await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
  126. const subscriptionChangePreview =
  127. await SubscriptionController.makeChangePreview(
  128. {
  129. type: 'add-on-update',
  130. addOn: {
  131. code: MEMBERS_LIMIT_ADD_ON_CODE,
  132. quantity: subscriptionChange.nextAddOns.find(
  133. addon => addon.code === MEMBERS_LIMIT_ADD_ON_CODE
  134. ).quantity,
  135. prevQuantity: currentAddonQuantity,
  136. },
  137. },
  138. subscriptionChange,
  139. paymentMethod
  140. )
  141. return subscriptionChangePreview
  142. }
  143. async function createAddSeatsSubscriptionChange(req) {
  144. const { changeRequest, userId, recurlySubscription } =
  145. await _addSeatsSubscriptionChange(req)
  146. await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
  147. await SubscriptionHandler.promises.syncSubscription(
  148. { uuid: recurlySubscription.id },
  149. userId
  150. )
  151. return { adding: req.body.adding }
  152. }
  153. async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
  154. if (
  155. subscription.planCode.includes('professional') ||
  156. !subscription.groupPlan
  157. ) {
  158. throw new Error('Not eligible for group plan upgrade')
  159. }
  160. return subscription.planCode.replace('collaborator', 'professional')
  161. }
  162. async function _getGroupPlanUpgradeChangeRequest(ownerId) {
  163. const olSubscription =
  164. await SubscriptionLocator.promises.getUsersSubscription(ownerId)
  165. const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
  166. const recurlySubscription = await RecurlyClient.promises.getSubscription(
  167. olSubscription.recurlySubscription_id
  168. )
  169. return recurlySubscription.getRequestForGroupPlanUpgrade(newPlanCode)
  170. }
  171. async function getGroupPlanUpgradePreview(ownerId) {
  172. const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
  173. const subscriptionChange =
  174. await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
  175. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
  176. return SubscriptionController.makeChangePreview(
  177. {
  178. type: 'group-plan-upgrade',
  179. prevPlan: {
  180. name: SubscriptionController.getPlanNameForDisplay(
  181. subscriptionChange.subscription.planName,
  182. subscriptionChange.subscription.planCode
  183. ),
  184. },
  185. },
  186. subscriptionChange,
  187. paymentMethod
  188. )
  189. }
  190. async function upgradeGroupPlan(ownerId) {
  191. const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
  192. await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
  193. await SubscriptionHandler.promises.syncSubscription(
  194. { uuid: changeRequest.subscription.id },
  195. ownerId
  196. )
  197. }
  198. module.exports = {
  199. removeUserFromGroup: callbackify(removeUserFromGroup),
  200. replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
  201. ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
  202. getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
  203. isUserPartOfGroup: callbackify(isUserPartOfGroup),
  204. getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
  205. upgradeGroupPlan: callbackify(upgradeGroupPlan),
  206. promises: {
  207. removeUserFromGroup,
  208. replaceUserReferencesInGroups,
  209. ensureFlexibleLicensingEnabled,
  210. getTotalConfirmedUsersInGroup,
  211. isUserPartOfGroup,
  212. getUsersGroupSubscriptionDetails,
  213. previewAddSeatsSubscriptionChange,
  214. createAddSeatsSubscriptionChange,
  215. getGroupPlanUpgradePreview,
  216. upgradeGroupPlan,
  217. },
  218. }