Files
overleaf-cep/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
Alexandre Bourdin 44a8883b6d Merge pull request #4639 from overleaf/ab-web-mono-analytics-id
Analytics ID support

GitOrigin-RevId: 820a6c0f4d19f046f6c791ce4dc64dbc80748924
2021-09-10 08:04:31 +00:00

402 lines
11 KiB
JavaScript

const { db, ObjectId } = require('../../infrastructure/mongodb')
const OError = require('@overleaf/o-error')
const async = require('async')
const { promisify, callbackify } = 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 AnalyticsManager = require('../Analytics/AnalyticsManager')
const { DeletedSubscription } = require('../../models/DeletedSubscription')
const logger = require('logger-sharelatex')
/**
* 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.
*/
function 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)
}
function 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) {
updateSubscriptionFromRecurly(
recurlySubscription,
subscription,
requesterData,
callback
)
} else {
_createNewSubscription(adminUserId, function (err, subscription) {
if (err != null) {
return callback(err)
}
updateSubscriptionFromRecurly(
recurlySubscription,
subscription,
requesterData,
callback
)
})
}
}
)
}
function addUserToGroup(subscriptionId, userId, callback) {
Subscription.updateOne(
{ _id: subscriptionId },
{ $addToSet: { member_ids: userId } },
function (err) {
if (err != null) {
return callback(err)
}
FeaturesUpdater.refreshFeatures(userId, 'add-to-group', function () {
callbackify(_sendUserGroupPlanCodeUserProperty)(userId, callback)
})
}
)
}
function removeUserFromGroup(subscriptionId, userId, callback) {
Subscription.updateOne(
{ _id: subscriptionId },
{ $pull: { member_ids: userId } },
function (error) {
if (error) {
OError.tag(error, 'error removing user from group', {
subscriptionId,
userId,
})
return callback(error)
}
UserGetter.getUser(userId, function (error, user) {
if (error) {
return callback(error)
}
FeaturesUpdater.refreshFeatures(
userId,
'remove-user-from-group',
function () {
callbackify(_sendUserGroupPlanCodeUserProperty)(userId, callback)
}
)
})
}
)
}
function 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)
const removeOperation = { $pull: { member_ids: userId } }
Subscription.updateMany(
{ _id: subscriptionIds },
removeOperation,
function (error) {
if (error) {
OError.tag(error, 'error removing user from groups', {
userId,
subscriptionIds,
})
return callback(error)
}
UserGetter.getUser(userId, function (error, user) {
if (error) {
return callback(error)
}
FeaturesUpdater.refreshFeatures(
userId,
'remove-user-from-groups',
function () {
callbackify(_sendUserGroupPlanCodeUserProperty)(
userId,
callback
)
}
)
})
}
)
}
)
}
function deleteWithV1Id(v1TeamId, callback) {
Subscription.deleteOne({ 'overleaf.id': v1TeamId }, callback)
}
function deleteSubscription(subscription, deleterData, callback) {
if (callback == null) {
callback = function () {}
}
async.series(
[
cb =>
// 1. create deletedSubscription
createDeletedSubscription(subscription, deleterData, cb),
cb =>
// 2. remove subscription
Subscription.deleteOne({ _id: subscription._id }, cb),
cb =>
// 3. refresh users features
refreshUsersFeatures(subscription, cb),
],
callback
)
}
function restoreSubscription(subscriptionId, callback) {
SubscriptionLocator.getDeletedSubscription(
subscriptionId,
function (err, deletedSubscription) {
if (err) {
return callback(err)
}
const 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
refreshUsersFeatures(subscription, cb),
cb =>
// 3. remove deleted subscription
DeletedSubscription.deleteOne(
{ 'subscription._id': subscription._id },
callback
),
],
callback
)
}
)
}
function refreshUsersFeatures(subscription, callback) {
const userIds = [subscription.admin_id].concat(subscription.member_ids || [])
async.mapSeries(
userIds,
function (userId, cb) {
FeaturesUpdater.refreshFeatures(userId, 'subscription-updater', cb)
},
callback
)
}
function 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)
}
function _createNewSubscription(adminUserId, callback) {
const subscription = new Subscription({
admin_id: adminUserId,
manager_ids: [adminUserId],
})
subscription.save(err => callback(err, subscription))
}
function _deleteAndReplaceSubscriptionFromRecurly(
recurlySubscription,
subscription,
requesterData,
callback
) {
const adminUserId = subscription.admin_id
deleteSubscription(subscription, requesterData, err => {
if (err) {
return callback(err)
}
_createNewSubscription(adminUserId, (err, newSubscription) => {
if (err) {
return callback(err)
}
updateSubscriptionFromRecurly(
recurlySubscription,
newSubscription,
requesterData,
callback
)
})
})
}
function updateSubscriptionFromRecurly(
recurlySubscription,
subscription,
requesterData,
callback
) {
if (recurlySubscription.state === 'expired') {
return deleteSubscription(subscription, requesterData, callback)
}
const updatedPlanCode = recurlySubscription.plan.plan_code
const plan = PlansLocator.findLocalPlanInSettings(updatedPlanCode)
if (plan == null) {
return callback(new Error(`plan code not found: ${updatedPlanCode}`))
}
if (!plan.groupPlan && subscription.groupPlan) {
// If downgrading from group to individual plan, delete group sub and create a new one
return _deleteAndReplaceSubscriptionFromRecurly(
recurlySubscription,
subscription,
requesterData,
callback
)
}
subscription.recurlySubscription_id = recurlySubscription.uuid
subscription.planCode = updatedPlanCode
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)
}
refreshUsersFeatures(subscription, callback)
})
}
async function _sendUserGroupPlanCodeUserProperty(userId) {
try {
const subscriptions =
(await SubscriptionLocator.promises.getMemberSubscriptions(userId)) || []
let bestPlanCode = null
let bestFeatures = {}
for (const subscription of subscriptions) {
const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
if (
plan &&
FeaturesUpdater.isFeatureSetBetter(plan.features, bestFeatures)
) {
bestPlanCode = plan.planCode
bestFeatures = plan.features
}
}
AnalyticsManager.setUserPropertyForUser(
userId,
'group-subscription-plan-code',
bestPlanCode
)
} catch (error) {
logger.error(
{ err: error },
`Failed to update group-subscription-plan-code property for user ${userId}`
)
}
}
module.exports = {
updateAdmin,
syncSubscription,
deleteSubscription,
createDeletedSubscription,
addUserToGroup,
refreshUsersFeatures,
removeUserFromGroup,
removeUserFromAllGroups,
deleteWithV1Id,
restoreSubscription,
updateSubscriptionFromRecurly,
promises: {
updateAdmin: promisify(updateAdmin),
syncSubscription: promisify(syncSubscription),
addUserToGroup: promisify(addUserToGroup),
refreshUsersFeatures: promisify(refreshUsersFeatures),
removeUserFromGroup: promisify(removeUserFromGroup),
removeUserFromAllGroups: promisify(removeUserFromAllGroups),
deleteSubscription: promisify(deleteSubscription),
createDeletedSubscription: promisify(createDeletedSubscription),
deleteWithV1Id: promisify(deleteWithV1Id),
restoreSubscription: promisify(restoreSubscription),
updateSubscriptionFromRecurly: promisify(updateSubscriptionFromRecurly),
},
}