SubscriptionUpdater.js 14 KB

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