SubscriptionUpdater.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. const { db, ObjectId } = require('../../infrastructure/mongodb')
  2. const { callbackify } = require('../../util/promises')
  3. const { Subscription } = require('../../models/Subscription')
  4. const SubscriptionLocator = require('./SubscriptionLocator')
  5. const PlansLocator = require('./PlansLocator')
  6. const FeaturesUpdater = require('./FeaturesUpdater')
  7. const FeaturesHelper = require('./FeaturesHelper')
  8. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  9. const { DeletedSubscription } = require('../../models/DeletedSubscription')
  10. const logger = require('@overleaf/logger')
  11. const Features = require('../../infrastructure/Features')
  12. /**
  13. * Change the admin of the given subscription.
  14. *
  15. * If the subscription is a group, add the new admin as manager while keeping
  16. * the old admin. Otherwise, replace the manager.
  17. *
  18. * Validation checks are assumed to have been made:
  19. * * subscription exists
  20. * * user exists
  21. * * user does not have another subscription
  22. * * subscription is not a Recurly subscription
  23. *
  24. * If the subscription is Recurly, we silently do nothing.
  25. */
  26. async function updateAdmin(subscription, adminId) {
  27. const query = {
  28. _id: ObjectId(subscription._id),
  29. customAccount: true,
  30. }
  31. const update = {
  32. $set: { admin_id: ObjectId(adminId) },
  33. }
  34. if (subscription.groupPlan) {
  35. update.$addToSet = { manager_ids: ObjectId(adminId) }
  36. } else {
  37. update.$set.manager_ids = [ObjectId(adminId)]
  38. }
  39. await Subscription.updateOne(query, update).exec()
  40. }
  41. async function syncSubscription(
  42. recurlySubscription,
  43. adminUserId,
  44. requesterData = {}
  45. ) {
  46. let subscription = await SubscriptionLocator.promises.getUsersSubscription(
  47. adminUserId
  48. )
  49. if (subscription == null) {
  50. subscription = await _createNewSubscription(adminUserId)
  51. }
  52. await updateSubscriptionFromRecurly(
  53. recurlySubscription,
  54. subscription,
  55. requesterData
  56. )
  57. }
  58. async function addUserToGroup(subscriptionId, userId) {
  59. await Subscription.updateOne(
  60. { _id: subscriptionId },
  61. { $addToSet: { member_ids: userId } }
  62. ).exec()
  63. await FeaturesUpdater.promises.refreshFeatures(userId, 'add-to-group')
  64. await _sendUserGroupPlanCodeUserProperty(userId)
  65. await _sendSubscriptionEvent(
  66. userId,
  67. subscriptionId,
  68. 'group-subscription-joined'
  69. )
  70. }
  71. async function removeUserFromGroup(subscriptionId, userId) {
  72. await Subscription.updateOne(
  73. { _id: subscriptionId },
  74. { $pull: { member_ids: userId } }
  75. ).exec()
  76. await FeaturesUpdater.promises.refreshFeatures(
  77. userId,
  78. 'remove-user-from-group'
  79. )
  80. await _sendUserGroupPlanCodeUserProperty(userId)
  81. await _sendSubscriptionEvent(
  82. userId,
  83. subscriptionId,
  84. 'group-subscription-left'
  85. )
  86. }
  87. async function removeUserFromAllGroups(userId) {
  88. const subscriptions =
  89. await SubscriptionLocator.promises.getMemberSubscriptions(userId)
  90. if (subscriptions.length === 0) {
  91. return
  92. }
  93. const subscriptionIds = subscriptions.map(sub => sub._id)
  94. const removeOperation = { $pull: { member_ids: userId } }
  95. await Subscription.updateMany(
  96. { _id: subscriptionIds },
  97. removeOperation
  98. ).exec()
  99. await FeaturesUpdater.promises.refreshFeatures(
  100. userId,
  101. 'remove-user-from-groups'
  102. )
  103. for (const subscriptionId of subscriptionIds) {
  104. await _sendSubscriptionEvent(
  105. userId,
  106. subscriptionId,
  107. 'group-subscription-left'
  108. )
  109. }
  110. await _sendUserGroupPlanCodeUserProperty(userId)
  111. }
  112. async function deleteWithV1Id(v1TeamId) {
  113. await Subscription.deleteOne({ 'overleaf.id': v1TeamId }).exec()
  114. }
  115. async function deleteSubscription(subscription, deleterData) {
  116. // 1. create deletedSubscription
  117. await createDeletedSubscription(subscription, deleterData)
  118. // 2. notify analytics that members left the subscription
  119. await _sendSubscriptionEventForAllMembers(
  120. subscription._id,
  121. 'group-subscription-left'
  122. )
  123. // 3. remove subscription
  124. await Subscription.deleteOne({ _id: subscription._id }).exec()
  125. // 4. refresh users features
  126. await _scheduleRefreshFeatures(subscription)
  127. }
  128. async function restoreSubscription(subscriptionId) {
  129. const deletedSubscription =
  130. await SubscriptionLocator.promises.getDeletedSubscription(subscriptionId)
  131. const subscription = deletedSubscription.subscription
  132. // 1. upsert subscription
  133. await db.subscriptions.updateOne(
  134. { _id: subscription._id },
  135. { $set: subscription },
  136. { upsert: true }
  137. )
  138. // 2. refresh users features. Do this before removing the
  139. // subscription so the restore can be retried if this fails
  140. await refreshUsersFeatures(subscription)
  141. // 3. remove deleted subscription
  142. await DeletedSubscription.deleteOne({
  143. 'subscription._id': subscription._id,
  144. }).exec()
  145. // 4. notify analytics that members rejoined the subscription
  146. await _sendSubscriptionEventForAllMembers(
  147. subscriptionId,
  148. 'group-subscription-left'
  149. )
  150. }
  151. async function refreshUsersFeatures(subscription) {
  152. const userIds = [subscription.admin_id].concat(subscription.member_ids || [])
  153. for (const userId of userIds) {
  154. await FeaturesUpdater.promises.refreshFeatures(
  155. userId,
  156. 'subscription-updater'
  157. )
  158. }
  159. }
  160. async function _scheduleRefreshFeatures(subscription) {
  161. const userIds = [subscription.admin_id].concat(subscription.member_ids || [])
  162. for (const userId of userIds) {
  163. await FeaturesUpdater.promises.scheduleRefreshFeatures(
  164. userId,
  165. 'subscription-updater'
  166. )
  167. }
  168. }
  169. async function createDeletedSubscription(subscription, deleterData) {
  170. subscription.teamInvites = []
  171. subscription.invited_emails = []
  172. const filter = { 'subscription._id': subscription._id }
  173. const data = {
  174. deleterData: {
  175. deleterId: deleterData.id,
  176. deleterIpAddress: deleterData.ip,
  177. },
  178. subscription,
  179. }
  180. const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  181. await DeletedSubscription.findOneAndUpdate(filter, data, options).exec()
  182. }
  183. async function _createNewSubscription(adminUserId) {
  184. const subscription = new Subscription({
  185. admin_id: adminUserId,
  186. manager_ids: [adminUserId],
  187. })
  188. await subscription.save()
  189. return subscription
  190. }
  191. async function _deleteAndReplaceSubscriptionFromRecurly(
  192. recurlySubscription,
  193. subscription,
  194. requesterData
  195. ) {
  196. const adminUserId = subscription.admin_id
  197. await deleteSubscription(subscription, requesterData)
  198. const newSubscription = await _createNewSubscription(adminUserId)
  199. await updateSubscriptionFromRecurly(
  200. recurlySubscription,
  201. newSubscription,
  202. requesterData
  203. )
  204. }
  205. async function updateSubscriptionFromRecurly(
  206. recurlySubscription,
  207. subscription,
  208. requesterData
  209. ) {
  210. if (recurlySubscription.state === 'expired') {
  211. const hasManagedUsersFeature =
  212. Features.hasFeature('saas') && subscription?.groupPolicy != null
  213. if (hasManagedUsersFeature) {
  214. // If a payment lapses and if the group is managed, as a temporary measure we need to
  215. // make sure that the group continues as-is and no destructive actions are taken.
  216. logger.warn(
  217. { subscriptionId: subscription._id },
  218. 'expired subscription has managedUsers feature, skipping deletion'
  219. )
  220. } else {
  221. await deleteSubscription(subscription, requesterData)
  222. }
  223. return
  224. }
  225. const updatedPlanCode = recurlySubscription.plan.plan_code
  226. const plan = PlansLocator.findLocalPlanInSettings(updatedPlanCode)
  227. if (plan == null) {
  228. throw new Error(`plan code not found: ${updatedPlanCode}`)
  229. }
  230. if (!plan.groupPlan && subscription.groupPlan) {
  231. // If downgrading from group to individual plan, delete group sub and create a new one
  232. await _deleteAndReplaceSubscriptionFromRecurly(
  233. recurlySubscription,
  234. subscription,
  235. requesterData
  236. )
  237. return
  238. }
  239. subscription.recurlySubscription_id = recurlySubscription.uuid
  240. subscription.planCode = updatedPlanCode
  241. subscription.recurlyStatus = {
  242. state: recurlySubscription.state,
  243. trialStartedAt: recurlySubscription.trial_started_at,
  244. trialEndsAt: recurlySubscription.trial_ends_at,
  245. }
  246. if (plan.groupPlan) {
  247. if (!subscription.groupPlan) {
  248. subscription.member_ids = subscription.member_ids || []
  249. subscription.member_ids.push(subscription.admin_id)
  250. }
  251. subscription.groupPlan = true
  252. subscription.membersLimit = plan.membersLimit
  253. // Some plans allow adding more seats than the base plan provides.
  254. // This is recorded as a subscription add on.
  255. if (
  256. plan.membersLimitAddOn &&
  257. Array.isArray(recurlySubscription.subscription_add_ons)
  258. ) {
  259. recurlySubscription.subscription_add_ons.forEach(addOn => {
  260. if (addOn.add_on_code === plan.membersLimitAddOn) {
  261. subscription.membersLimit += addOn.quantity
  262. }
  263. })
  264. }
  265. }
  266. await subscription.save()
  267. await _scheduleRefreshFeatures(subscription)
  268. }
  269. async function _sendUserGroupPlanCodeUserProperty(userId) {
  270. try {
  271. const subscriptions =
  272. await SubscriptionLocator.promises.getMemberSubscriptions(userId)
  273. let bestPlanCode = null
  274. let bestFeatures = {}
  275. for (const subscription of subscriptions) {
  276. const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  277. if (
  278. plan &&
  279. FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures)
  280. ) {
  281. bestPlanCode = plan.planCode
  282. bestFeatures = plan.features
  283. }
  284. }
  285. AnalyticsManager.setUserPropertyForUser(
  286. userId,
  287. 'group-subscription-plan-code',
  288. bestPlanCode
  289. )
  290. } catch (error) {
  291. logger.error(
  292. { err: error },
  293. `Failed to update group-subscription-plan-code property for user ${userId}`
  294. )
  295. }
  296. }
  297. async function _sendSubscriptionEvent(userId, subscriptionId, event) {
  298. const subscription = await Subscription.findOne(
  299. { _id: subscriptionId },
  300. { recurlySubscription_id: 1, groupPlan: 1 }
  301. )
  302. if (!subscription || !subscription.groupPlan) {
  303. return
  304. }
  305. AnalyticsManager.recordEventForUser(userId, event, {
  306. groupId: subscription._id.toString(),
  307. subscriptionId: subscription.recurlySubscription_id,
  308. })
  309. }
  310. async function _sendSubscriptionEventForAllMembers(subscriptionId, event) {
  311. const subscription = await Subscription.findOne(
  312. { _id: subscriptionId },
  313. {
  314. recurlySubscription_id: 1,
  315. member_ids: 1,
  316. groupPlan: 1,
  317. }
  318. )
  319. if (!subscription) {
  320. return
  321. }
  322. const userIds = (subscription.member_ids || []).filter(Boolean)
  323. for (const userId of userIds) {
  324. if (userId) {
  325. AnalyticsManager.recordEventForUser(userId, event, {
  326. groupId: subscription._id.toString(),
  327. subscriptionId: subscription.recurlySubscription_id,
  328. })
  329. }
  330. }
  331. }
  332. module.exports = {
  333. updateAdmin: callbackify(updateAdmin),
  334. syncSubscription: callbackify(syncSubscription),
  335. deleteSubscription: callbackify(deleteSubscription),
  336. createDeletedSubscription: callbackify(createDeletedSubscription),
  337. addUserToGroup: callbackify(addUserToGroup),
  338. refreshUsersFeatures: callbackify(refreshUsersFeatures),
  339. removeUserFromGroup: callbackify(removeUserFromGroup),
  340. removeUserFromAllGroups: callbackify(removeUserFromAllGroups),
  341. deleteWithV1Id: callbackify(deleteWithV1Id),
  342. restoreSubscription: callbackify(restoreSubscription),
  343. updateSubscriptionFromRecurly: callbackify(updateSubscriptionFromRecurly),
  344. promises: {
  345. updateAdmin,
  346. syncSubscription,
  347. addUserToGroup,
  348. refreshUsersFeatures,
  349. removeUserFromGroup,
  350. removeUserFromAllGroups,
  351. deleteSubscription,
  352. createDeletedSubscription,
  353. deleteWithV1Id,
  354. restoreSubscription,
  355. updateSubscriptionFromRecurly,
  356. },
  357. }