SubscriptionUpdater.js 13 KB

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