const { db, ObjectId } = require('../../infrastructure/mongodb') const OError = require('@overleaf/o-error') const async = require('async') const { promisifyAll } = require('../../util/promises') const { Subscription } = require('../../models/Subscription') const SubscriptionLocator = require('./SubscriptionLocator') const UserGetter = require('../User/UserGetter') const PlansLocator = require('./PlansLocator') const FeaturesUpdater = require('./FeaturesUpdater') const { DeletedSubscription } = require('../../models/DeletedSubscription') const SubscriptionUpdater = { /** * Change the admin of the given subscription. * * If the subscription is a group, add the new admin as manager while keeping * the old admin. Otherwise, replace the manager. * * Validation checks are assumed to have been made: * * subscription exists * * user exists * * user does not have another subscription * * subscription is not a Recurly subscription * * If the subscription is Recurly, we silently do nothing. */ updateAdmin(subscription, adminId, callback) { const query = { _id: ObjectId(subscription._id), customAccount: true } const update = { $set: { admin_id: ObjectId(adminId) } } if (subscription.groupPlan) { update.$addToSet = { manager_ids: ObjectId(adminId) } } else { update.$set.manager_ids = [ObjectId(adminId)] } Subscription.updateOne(query, update, callback) }, syncSubscription(recurlySubscription, adminUserId, requesterData, callback) { if (!callback) { callback = requesterData requesterData = {} } SubscriptionLocator.getUsersSubscription(adminUserId, function( err, subscription ) { if (err != null) { return callback(err) } if (subscription != null) { SubscriptionUpdater._updateSubscriptionFromRecurly( recurlySubscription, subscription, requesterData, callback ) } else { SubscriptionUpdater._createNewSubscription(adminUserId, function( err, subscription ) { if (err != null) { return callback(err) } SubscriptionUpdater._updateSubscriptionFromRecurly( recurlySubscription, subscription, requesterData, callback ) }) } }) }, addUserToGroup(subscriptionId, userId, callback) { SubscriptionUpdater.addUsersToGroup(subscriptionId, [userId], callback) }, addUsersToGroup(subscriptionId, memberIds, callback) { SubscriptionUpdater.addUsersToGroupWithoutFeaturesRefresh( subscriptionId, memberIds, function(err) { if (err != null) { return callback(err) } async.map(memberIds, FeaturesUpdater.refreshFeatures, callback) } ) }, addUsersToGroupWithoutFeaturesRefresh(subscriptionId, memberIds, callback) { const searchOps = { _id: subscriptionId } const insertOperation = { $addToSet: { member_ids: { $each: memberIds } } } Subscription.updateOne(searchOps, insertOperation, callback) }, removeUserFromGroups(filter, userId, callback) { const removeOperation = { $pull: { member_ids: userId } } Subscription.updateMany(filter, removeOperation, function(err) { if (err != null) { OError.tag(err, 'error removing user from groups', { filter, removeOperation }) return callback(err) } UserGetter.getUser(userId, function(error, user) { if (error) { return callback(error) } FeaturesUpdater.refreshFeatures(userId, callback) }) }) }, removeUserFromGroup(subscriptionId, userId, callback) { SubscriptionUpdater.removeUserFromGroups( { _id: subscriptionId }, userId, callback ) }, removeUserFromAllGroups(userId, callback) { SubscriptionLocator.getMemberSubscriptions(userId, function( error, subscriptions ) { if (error) { return callback(error) } if (!subscriptions) { return callback() } const subscriptionIds = subscriptions.map(sub => sub._id) SubscriptionUpdater.removeUserFromGroups( { _id: subscriptionIds }, userId, callback ) }) }, deleteWithV1Id(v1TeamId, callback) { Subscription.deleteOne({ 'overleaf.id': v1TeamId }, callback) }, deleteSubscription(subscription, deleterData, callback) { if (callback == null) { callback = function() {} } async.series( [ cb => // 1. create deletedSubscription SubscriptionUpdater._createDeletedSubscription( subscription, deleterData, cb ), cb => // 2. remove subscription Subscription.deleteOne({ _id: subscription._id }, cb), cb => // 3. refresh users features SubscriptionUpdater._refreshUsersFeatures(subscription, cb) ], callback ) }, restoreSubscription(subscriptionId, callback) { SubscriptionLocator.getDeletedSubscription(subscriptionId, function( err, deletedSubscription ) { if (err) { return callback(err) } let subscription = deletedSubscription.subscription async.series( [ cb => // 1. upsert subscription db.subscriptions.updateOne( { _id: subscription._id }, subscription, { upsert: true }, cb ), cb => // 2. refresh users features. Do this before removing the // subscription so the restore can be retried if this fails SubscriptionUpdater._refreshUsersFeatures(subscription, cb), cb => // 3. remove deleted subscription DeletedSubscription.deleteOne( { 'subscription._id': subscription._id }, callback ) ], callback ) }) }, _refreshUsersFeatures(subscription, callback) { const userIds = [subscription.admin_id].concat( subscription.member_ids || [] ) async.mapSeries(userIds, FeaturesUpdater.refreshFeatures, callback) }, _createDeletedSubscription(subscription, deleterData, callback) { subscription.teamInvites = [] subscription.invited_emails = [] const filter = { 'subscription._id': subscription._id } const data = { deleterData: { deleterId: deleterData.id, deleterIpAddress: deleterData.ip }, subscription: subscription } const options = { upsert: true, new: true, setDefaultsOnInsert: true } DeletedSubscription.findOneAndUpdate(filter, data, options, callback) }, _createNewSubscription(adminUserId, callback) { const subscription = new Subscription({ admin_id: adminUserId, manager_ids: [adminUserId] }) subscription.save(err => callback(err, subscription)) }, _updateSubscriptionFromRecurly( recurlySubscription, subscription, requesterData, callback ) { if (recurlySubscription.state === 'expired') { return SubscriptionUpdater.deleteSubscription( subscription, requesterData, callback ) } subscription.recurlySubscription_id = recurlySubscription.uuid subscription.planCode = recurlySubscription.plan.plan_code const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) if (plan == null) { return callback( new Error(`plan code not found: ${subscription.planCode}`) ) } if (plan.groupPlan) { if (!subscription.groupPlan) { subscription.member_ids = subscription.member_ids || [] subscription.member_ids.push(subscription.admin_id) } subscription.groupPlan = true subscription.membersLimit = plan.membersLimit // Some plans allow adding more seats than the base plan provides. // This is recorded as a subscription add on. if ( plan.membersLimitAddOn && Array.isArray(recurlySubscription.subscription_add_ons) ) { recurlySubscription.subscription_add_ons.forEach(addOn => { if (addOn.add_on_code === plan.membersLimitAddOn) { subscription.membersLimit += addOn.quantity } }) } } subscription.save(function(error) { if (error) { return callback(error) } SubscriptionUpdater._refreshUsersFeatures(subscription, callback) }) } } SubscriptionUpdater.promises = promisifyAll(SubscriptionUpdater) module.exports = SubscriptionUpdater