SubscriptionUpdater.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import { db, ObjectId } from '../../infrastructure/mongodb.mjs'
  2. import { callbackify } from '@overleaf/promise-utils'
  3. import { Subscription } from '../../models/Subscription.mjs'
  4. import SubscriptionLocator from './SubscriptionLocator.mjs'
  5. import PlansLocator from './PlansLocator.mjs'
  6. import FeaturesUpdater from './FeaturesUpdater.mjs'
  7. import FeaturesHelper from './FeaturesHelper.mjs'
  8. import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
  9. import { DeletedSubscription } from '../../models/DeletedSubscription.mjs'
  10. import logger from '@overleaf/logger'
  11. import Features from '../../infrastructure/Features.mjs'
  12. import UserAuditLogHandler from '../User/UserAuditLogHandler.mjs'
  13. import UserUpdater from '../User/UserUpdater.mjs'
  14. import AccountMappingHelper from '../Analytics/AccountMappingHelper.mjs'
  15. import { SSOConfig } from '../../models/SSOConfig.mjs'
  16. import mongoose from '../../infrastructure/Mongoose.mjs'
  17. import Modules from '../../infrastructure/Modules.mjs'
  18. /**
  19. * @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription
  20. * @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider
  21. * @typedef {import('../../../../types/group-management/group-audit-log').GroupAuditLog} GroupAuditLog
  22. * @typedef {InstanceType<Subscription>} MongoSubscription
  23. */
  24. /**
  25. *
  26. * @param {GroupAuditLog} auditLog
  27. */
  28. async function subscriptionUpdateWithAuditLog(dbFilter, dbUpdate, auditLog) {
  29. const session = await mongoose.startSession()
  30. try {
  31. await session.withTransaction(async () => {
  32. await Subscription.updateOne(dbFilter, dbUpdate, { session }).exec()
  33. await Modules.promises.hooks.fire(
  34. 'addGroupAuditLogEntry',
  35. auditLog,
  36. session
  37. )
  38. })
  39. } finally {
  40. await session.endSession()
  41. }
  42. }
  43. /**
  44. * Change the admin of the given subscription.
  45. *
  46. * If the subscription is a group, add the new admin as manager while keeping
  47. * the old admin. Otherwise, replace the manager.
  48. *
  49. * Validation checks are assumed to have been made:
  50. * * subscription exists
  51. * * user exists
  52. * * user does not have another subscription
  53. * * subscription is not a Recurly subscription
  54. *
  55. * If the subscription is Recurly, we silently do nothing.
  56. */
  57. async function updateAdmin(subscription, adminId) {
  58. const query = {
  59. _id: new ObjectId(subscription._id),
  60. customAccount: true,
  61. }
  62. const update = {
  63. $set: { admin_id: new ObjectId(adminId) },
  64. }
  65. if (subscription.groupPlan) {
  66. update.$addToSet = { manager_ids: new ObjectId(adminId) }
  67. } else {
  68. update.$set.manager_ids = [new ObjectId(adminId)]
  69. }
  70. await Subscription.updateOne(query, update).exec()
  71. }
  72. async function syncSubscription(
  73. recurlySubscription,
  74. adminUserId,
  75. requesterData = {}
  76. ) {
  77. let subscription =
  78. await SubscriptionLocator.promises.getUsersSubscription(adminUserId)
  79. if (subscription == null) {
  80. subscription = await createNewSubscription(adminUserId)
  81. }
  82. await updateSubscriptionFromRecurly(
  83. recurlySubscription,
  84. subscription,
  85. requesterData
  86. )
  87. }
  88. async function addUserToGroup(subscriptionId, userId, auditLog) {
  89. await UserAuditLogHandler.promises.addEntry(
  90. userId,
  91. 'join-group-subscription',
  92. undefined,
  93. undefined,
  94. { subscriptionId }
  95. )
  96. await subscriptionUpdateWithAuditLog(
  97. { _id: subscriptionId },
  98. { $addToSet: { member_ids: userId } },
  99. {
  100. initiatorId: auditLog?.initiatorId,
  101. ipAddress: auditLog?.ipAddress,
  102. groupId: subscriptionId,
  103. operation: 'join-group',
  104. }
  105. )
  106. await FeaturesUpdater.promises.refreshFeatures(userId, 'add-to-group')
  107. await _sendUserGroupPlanCodeUserProperty(userId)
  108. await _sendSubscriptionEvent(
  109. userId,
  110. subscriptionId,
  111. 'group-subscription-joined'
  112. )
  113. }
  114. async function removeUserFromGroup(subscriptionId, userId, auditLog) {
  115. await UserAuditLogHandler.promises.addEntry(
  116. userId,
  117. 'leave-group-subscription',
  118. undefined,
  119. undefined,
  120. { subscriptionId }
  121. )
  122. await subscriptionUpdateWithAuditLog(
  123. { _id: subscriptionId },
  124. { $pull: { member_ids: userId } },
  125. {
  126. initiatorId: auditLog?.initiatorId,
  127. ipAddress: auditLog?.ipAddress,
  128. groupId: subscriptionId,
  129. operation: 'leave-group',
  130. info: { userIdRemoved: userId },
  131. }
  132. )
  133. await Subscription.updateOne(
  134. { _id: subscriptionId },
  135. { $pull: { member_ids: userId } }
  136. ).exec()
  137. const subscription = await Subscription.findById(subscriptionId)
  138. if (subscription.managedUsersEnabled) {
  139. await UserUpdater.promises.updateUser(
  140. { _id: userId },
  141. {
  142. $unset: {
  143. 'enrollment.managedBy': 1,
  144. 'enrollment.enrolledAt': 1,
  145. },
  146. }
  147. )
  148. }
  149. await FeaturesUpdater.promises.refreshFeatures(
  150. userId,
  151. 'remove-user-from-group'
  152. )
  153. await _sendUserGroupPlanCodeUserProperty(userId)
  154. await _sendSubscriptionEvent(
  155. userId,
  156. subscriptionId,
  157. 'group-subscription-left'
  158. )
  159. }
  160. async function removeUserFromAllGroups(userId) {
  161. const subscriptions =
  162. await SubscriptionLocator.promises.getMemberSubscriptions(userId)
  163. if (subscriptions.length === 0) {
  164. return
  165. }
  166. const subscriptionIds = subscriptions.map(sub => sub._id)
  167. const removeOperation = { $pull: { member_ids: userId } }
  168. for (const subscriptionId of subscriptionIds) {
  169. await UserAuditLogHandler.promises.addEntry(
  170. userId,
  171. 'leave-group-subscription',
  172. undefined,
  173. undefined,
  174. { subscriptionId }
  175. )
  176. }
  177. await Subscription.updateMany(
  178. { _id: subscriptionIds },
  179. removeOperation
  180. ).exec()
  181. await FeaturesUpdater.promises.refreshFeatures(
  182. userId,
  183. 'remove-user-from-groups'
  184. )
  185. for (const subscriptionId of subscriptionIds) {
  186. await _sendSubscriptionEvent(
  187. userId,
  188. subscriptionId,
  189. 'group-subscription-left'
  190. )
  191. }
  192. await _sendUserGroupPlanCodeUserProperty(userId)
  193. }
  194. async function deleteWithV1Id(v1TeamId) {
  195. await Subscription.deleteOne({ 'overleaf.id': v1TeamId }).exec()
  196. }
  197. async function deleteSubscription(subscription, deleterData) {
  198. // 1. create deletedSubscription
  199. await createDeletedSubscription(subscription, deleterData)
  200. // 2. notify analytics that members left the subscription
  201. await _sendSubscriptionEventForAllMembers(
  202. subscription._id,
  203. 'group-subscription-left'
  204. )
  205. // 3. remove subscription
  206. await Subscription.deleteOne({ _id: subscription._id }).exec()
  207. // 4. refresh users features
  208. await scheduleRefreshFeatures(subscription)
  209. }
  210. async function restoreSubscription(subscriptionId) {
  211. const deletedSubscription =
  212. await SubscriptionLocator.promises.getDeletedSubscription(subscriptionId)
  213. const subscription = deletedSubscription.subscription
  214. // 1. upsert subscription
  215. await db.subscriptions.updateOne(
  216. { _id: subscription._id },
  217. { $set: subscription },
  218. { upsert: true }
  219. )
  220. // 2. refresh users features. Do this before removing the
  221. // subscription so the restore can be retried if this fails
  222. await refreshUsersFeatures(subscription)
  223. // 3. remove deleted subscription
  224. await DeletedSubscription.deleteOne({
  225. 'subscription._id': subscription._id,
  226. }).exec()
  227. // 4. notify analytics that members rejoined the subscription
  228. await _sendSubscriptionEventForAllMembers(
  229. subscriptionId,
  230. 'group-subscription-joined'
  231. )
  232. }
  233. async function refreshUsersFeatures(subscription) {
  234. const userIds = [subscription.admin_id].concat(subscription.member_ids || [])
  235. for (const userId of userIds) {
  236. await FeaturesUpdater.promises.refreshFeatures(
  237. userId,
  238. 'subscription-updater'
  239. )
  240. }
  241. }
  242. /**
  243. *
  244. * @param {Subscription} subscription
  245. */
  246. async function scheduleRefreshFeatures(subscription) {
  247. const userIds = [subscription.admin_id].concat(subscription.member_ids || [])
  248. for (const userId of userIds) {
  249. await FeaturesUpdater.promises.scheduleRefreshFeatures(
  250. userId,
  251. 'subscription-updater'
  252. )
  253. }
  254. }
  255. async function createDeletedSubscription(subscription, deleterData) {
  256. subscription.teamInvites = []
  257. subscription.invited_emails = []
  258. const filter = { 'subscription._id': subscription._id }
  259. const data = {
  260. deleterData: {
  261. deleterId: deleterData.id,
  262. deleterIpAddress: deleterData.ip,
  263. },
  264. subscription,
  265. }
  266. const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  267. await DeletedSubscription.findOneAndUpdate(filter, data, options).exec()
  268. }
  269. /**
  270. * Creates a new subscription for the given admin user.
  271. *
  272. * @param {string} adminUserId
  273. * @returns {Promise<Subscription>}
  274. */
  275. async function createNewSubscription(adminUserId) {
  276. const subscription = new Subscription({
  277. admin_id: adminUserId,
  278. manager_ids: [adminUserId],
  279. })
  280. await subscription.save()
  281. return subscription
  282. }
  283. async function _deleteAndReplaceSubscriptionFromRecurly(
  284. recurlySubscription,
  285. subscription,
  286. requesterData
  287. ) {
  288. const adminUserId = subscription.admin_id
  289. await deleteSubscription(subscription, requesterData)
  290. const newSubscription = await createNewSubscription(adminUserId)
  291. await updateSubscriptionFromRecurly(
  292. recurlySubscription,
  293. newSubscription,
  294. requesterData
  295. )
  296. }
  297. async function updateSubscriptionFromRecurly(
  298. recurlySubscription,
  299. subscription,
  300. requesterData
  301. ) {
  302. if (
  303. subscription?.paymentProvider?.service &&
  304. subscription.paymentProvider.service.includes('stripe')
  305. ) {
  306. logger.warn(
  307. { subscriptionId: subscription._id },
  308. 'attempted to update non-recurly subscription from Recurly data'
  309. )
  310. return
  311. }
  312. if (recurlySubscription.state === 'expired') {
  313. await handleExpiredSubscription(subscription, requesterData)
  314. return
  315. }
  316. const updatedPlanCode = recurlySubscription.plan.plan_code
  317. const plan = PlansLocator.findLocalPlanInSettings(updatedPlanCode)
  318. if (plan == null) {
  319. throw new Error(`plan code not found: ${updatedPlanCode}`)
  320. }
  321. if (!plan.groupPlan && subscription.groupPlan) {
  322. // If downgrading from group to individual plan, delete group sub and create a new one
  323. await _deleteAndReplaceSubscriptionFromRecurly(
  324. recurlySubscription,
  325. subscription,
  326. requesterData
  327. )
  328. return
  329. }
  330. const addOns = recurlySubscription?.subscription_add_ons?.map(addOn => {
  331. return {
  332. addOnCode: addOn.add_on_code,
  333. quantity: addOn.quantity,
  334. unitAmountInCents: addOn.unit_amount_in_cents,
  335. }
  336. })
  337. subscription.recurlySubscription_id = recurlySubscription.uuid
  338. subscription.planCode = updatedPlanCode
  339. subscription.addOns = addOns || []
  340. subscription.recurlyStatus = {
  341. state: recurlySubscription.state,
  342. trialStartedAt: recurlySubscription.trial_started_at,
  343. trialEndsAt: recurlySubscription.trial_ends_at,
  344. }
  345. if (plan.groupPlan) {
  346. if (!subscription.groupPlan) {
  347. subscription.member_ids = subscription.member_ids || []
  348. subscription.member_ids.push(subscription.admin_id)
  349. }
  350. subscription.groupPlan = true
  351. subscription.membersLimit = plan.membersLimit
  352. // Some plans allow adding more seats than the base plan provides.
  353. // This is recorded as a subscription add on.
  354. if (
  355. plan.membersLimitAddOn &&
  356. Array.isArray(recurlySubscription.subscription_add_ons)
  357. ) {
  358. recurlySubscription.subscription_add_ons.forEach(addOn => {
  359. if (addOn.add_on_code === plan.membersLimitAddOn) {
  360. subscription.membersLimit += addOn.quantity
  361. }
  362. })
  363. }
  364. }
  365. await subscription.save()
  366. const accountMapping =
  367. AccountMappingHelper.generateSubscriptionToRecurlyMapping(
  368. subscription._id,
  369. subscription.recurlySubscription_id
  370. )
  371. if (accountMapping) {
  372. AnalyticsManager.registerAccountMapping(accountMapping)
  373. }
  374. await scheduleRefreshFeatures(subscription)
  375. }
  376. async function _sendUserGroupPlanCodeUserProperty(userId) {
  377. try {
  378. const subscriptions =
  379. await SubscriptionLocator.promises.getMemberSubscriptions(userId)
  380. let bestPlanCode = null
  381. let bestFeatures = {}
  382. for (const subscription of subscriptions) {
  383. const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  384. if (
  385. plan &&
  386. FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures)
  387. ) {
  388. bestPlanCode = plan.planCode
  389. bestFeatures = plan.features
  390. }
  391. }
  392. AnalyticsManager.setUserPropertyForUserInBackground(
  393. userId,
  394. 'group-subscription-plan-code',
  395. bestPlanCode
  396. )
  397. } catch (error) {
  398. logger.error(
  399. { err: error },
  400. `Failed to update group-subscription-plan-code property for user ${userId}`
  401. )
  402. }
  403. }
  404. async function handleExpiredSubscription(subscription, requesterData) {
  405. const hasManagedUsersFeature =
  406. Features.hasFeature('saas') && subscription?.managedUsersEnabled
  407. // If a payment lapses and if the group is managed or has group SSO, as a temporary measure we need to
  408. // make sure that the group continues as-is and no destructive actions are taken.
  409. if (hasManagedUsersFeature) {
  410. logger.warn(
  411. { subscriptionId: subscription._id },
  412. 'expired subscription has managedUsers feature enabled, skipping deletion'
  413. )
  414. } else {
  415. let hasGroupSSOEnabled = false
  416. if (subscription?.ssoConfig) {
  417. const ssoConfig = await SSOConfig.findOne({
  418. _id: subscription.ssoConfig._id || subscription.ssoConfig,
  419. })
  420. .lean()
  421. .exec()
  422. if (ssoConfig.enabled) {
  423. hasGroupSSOEnabled = true
  424. }
  425. }
  426. if (hasGroupSSOEnabled) {
  427. logger.warn(
  428. { subscriptionId: subscription._id },
  429. 'expired subscription has groupSSO feature enabled, skipping deletion'
  430. )
  431. } else {
  432. await deleteSubscription(subscription, requesterData)
  433. }
  434. }
  435. }
  436. async function _sendSubscriptionEvent(userId, subscriptionId, event) {
  437. const subscription = await Subscription.findOne(
  438. { _id: subscriptionId },
  439. { recurlySubscription_id: 1, groupPlan: 1 }
  440. )
  441. if (!subscription || !subscription.groupPlan) {
  442. return
  443. }
  444. AnalyticsManager.recordEventForUserInBackground(userId, event, {
  445. groupId: subscription._id.toString(),
  446. subscriptionId: subscription.recurlySubscription_id,
  447. })
  448. }
  449. async function _sendSubscriptionEventForAllMembers(subscriptionId, event) {
  450. const subscription = await Subscription.findOne(
  451. { _id: subscriptionId },
  452. {
  453. recurlySubscription_id: 1,
  454. member_ids: 1,
  455. groupPlan: 1,
  456. }
  457. )
  458. if (!subscription) {
  459. return
  460. }
  461. const userIds = (subscription.member_ids || []).filter(Boolean)
  462. for (const userId of userIds) {
  463. if (userId) {
  464. AnalyticsManager.recordEventForUserInBackground(userId, event, {
  465. groupId: subscription._id.toString(),
  466. subscriptionId: subscription.recurlySubscription_id,
  467. })
  468. }
  469. }
  470. }
  471. /**
  472. * Change the ownershiop of the given subscription.
  473. * @param {MongoSubscription} subscription
  474. * @param {string} adminId
  475. * @param {boolean} clearPreviousPaymentProvider whether to clear the previousPaymentProvider field or set it to the current paymentProvider
  476. */
  477. async function transferSubscriptionOwnership(
  478. subscription,
  479. adminId,
  480. clearPreviousPaymentProvider
  481. ) {
  482. const query = {
  483. _id: new ObjectId(subscription._id),
  484. }
  485. const update = {
  486. $set: { admin_id: new ObjectId(adminId) },
  487. }
  488. if (subscription.groupPlan) {
  489. update.$addToSet = { manager_ids: new ObjectId(adminId) }
  490. } else {
  491. update.$set.manager_ids = [new ObjectId(adminId)]
  492. }
  493. if (clearPreviousPaymentProvider) {
  494. update.$unset = { previousPaymentProvider: 1 }
  495. } else {
  496. update.$set.previousPaymentProvider = subscription.paymentProvider
  497. }
  498. await Subscription.updateOne(query, update).exec()
  499. }
  500. export default {
  501. updateAdmin: callbackify(updateAdmin),
  502. syncSubscription: callbackify(syncSubscription),
  503. createNewSubscription: callbackify(createNewSubscription),
  504. deleteSubscription: callbackify(deleteSubscription),
  505. createDeletedSubscription: callbackify(createDeletedSubscription),
  506. addUserToGroup: callbackify(addUserToGroup),
  507. refreshUsersFeatures: callbackify(refreshUsersFeatures),
  508. removeUserFromGroup: callbackify(removeUserFromGroup),
  509. removeUserFromAllGroups: callbackify(removeUserFromAllGroups),
  510. deleteWithV1Id: callbackify(deleteWithV1Id),
  511. restoreSubscription: callbackify(restoreSubscription),
  512. updateSubscriptionFromRecurly: callbackify(updateSubscriptionFromRecurly),
  513. scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
  514. promises: {
  515. updateAdmin,
  516. syncSubscription,
  517. createNewSubscription,
  518. addUserToGroup,
  519. refreshUsersFeatures,
  520. removeUserFromGroup,
  521. removeUserFromAllGroups,
  522. deleteSubscription,
  523. createDeletedSubscription,
  524. deleteWithV1Id,
  525. restoreSubscription,
  526. updateSubscriptionFromRecurly,
  527. scheduleRefreshFeatures,
  528. handleExpiredSubscription,
  529. transferSubscriptionOwnership,
  530. },
  531. }