SubscriptionGroupHandler.js 8.0 KB

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