Merge pull request #25843 from overleaf/ii-managed-users-make-unmanaged-terminate-subscription

[web] Terminate subscription when joining a managed group

GitOrigin-RevId: 2a4f2fd57e1319970780043a633fb8027593e5d4
This commit is contained in:
ilkin-overleaf
2025-06-12 12:46:05 +03:00
committed by Copybot
parent 0fc229dfc0
commit d49a9e9e80
5 changed files with 108 additions and 8 deletions
@@ -736,6 +736,21 @@ async function failInvoice(invoiceId) {
await client.markInvoiceFailed(invoiceId)
}
async function terminateSubscriptionByUuid(subscriptionUuid) {
const subscription = await client.terminateSubscription(
'uuid-' + subscriptionUuid,
{
body: {
refund: 'none',
},
}
)
logger.debug({ subscriptionUuid }, 'subscription terminated')
return subscription
}
module.exports = {
errors: recurly.errors,
@@ -759,6 +774,7 @@ module.exports = {
resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid),
getPastDueInvoices: callbackify(getPastDueInvoices),
failInvoice: callbackify(failInvoice),
terminateSubscriptionByUuid: callbackify(terminateSubscriptionByUuid),
promises: {
getSubscription,
@@ -781,5 +797,6 @@ module.exports = {
getPlan,
getPastDueInvoices,
failInvoice,
terminateSubscriptionByUuid,
},
}
@@ -206,7 +206,7 @@ async function acceptInvite(req, res, next) {
const subscription = await TeamInvitesHandler.promises.acceptInvite(
token,
userId,
{ initiatorId: userId, ipAddress: req.ip }
req.ip
)
const groupSSOActive = (
await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
@@ -22,6 +22,7 @@ const {
callbackifyMultiResult,
} = require('@overleaf/promise-utils')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const RecurlyClient = require('./RecurlyClient')
async function getInvite(token) {
const subscription = await Subscription.findOne({
@@ -64,11 +65,50 @@ async function importInvite(subscription, inviterName, email, token, sentAt) {
return subscription.save()
}
async function acceptInvite(token, userId, auditLog) {
async function _deleteUserSubscription(userId, ipAddress) {
// Delete released user subscription to make it on a free plan
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
if (subscription) {
logger.debug(
{
subscriptionId: subscription._id,
},
'deleting user subscription'
)
const deleterData = {
id: userId,
ip: ipAddress,
}
await SubscriptionUpdater.promises.deleteSubscription(
subscription,
deleterData
)
// Terminate the subscription in Recurly
if (subscription.recurlySubscription_id) {
try {
await RecurlyClient.promises.terminateSubscriptionByUuid(
subscription.recurlySubscription_id
)
} catch (err) {
logger.error(
{ err, subscriptionId: subscription._id },
'terminating subscription failed'
)
}
}
}
}
async function acceptInvite(token, userId, ipAddress) {
const { invite, subscription } = await getInvite(token)
if (!invite) {
throw new Errors.NotFoundError('invite not found')
}
const auditLog = { initiatorId: userId, ipAddress }
await SubscriptionUpdater.promises.addUserToGroup(
subscription._id,
@@ -77,6 +117,7 @@ async function acceptInvite(token, userId, auditLog) {
)
if (subscription.managedUsersEnabled) {
await _deleteUserSubscription(userId, ipAddress)
await Modules.promises.hooks.fire(
'enrollInManagedSubscription',
userId,
@@ -692,4 +692,20 @@ describe('RecurlyClient', function () {
).to.be.rejectedWith(Error)
})
})
describe('terminateSubscriptionByUuid', function () {
it('should attempt to terminate the subscription', async function () {
this.client.terminateSubscription = sinon
.stub()
.resolves(this.recurlySubscription)
const subscription =
await this.RecurlyClient.promises.terminateSubscriptionByUuid(
this.subscription.uuid
)
expect(subscription).to.deep.equal(this.recurlySubscription)
expect(this.client.terminateSubscription).to.be.calledWith(
'uuid-' + this.subscription.uuid
)
})
})
})
@@ -29,6 +29,7 @@ describe('TeamInvitesHandler', function () {
this.subscription = {
id: '55153a8014829a865bbf700d',
_id: new ObjectId('55153a8014829a865bbf700d'),
recurlySubscription_id: '1a2b3c4d5e6f7g',
admin_id: this.manager._id,
groupPlan: true,
member_ids: [],
@@ -54,6 +55,7 @@ describe('TeamInvitesHandler', function () {
this.SubscriptionUpdater = {
promises: {
addUserToGroup: sinon.stub().resolves(),
deleteSubscription: sinon.stub().resolves(),
},
}
@@ -109,6 +111,12 @@ describe('TeamInvitesHandler', function () {
this.Subscription.findOne.resolves(this.subscription)
this.RecurlyClient = {
promises: {
terminateSubscriptionByUuid: sinon.stub().resolves(),
},
}
this.TeamInvitesHandler = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
@@ -126,6 +134,7 @@ describe('TeamInvitesHandler', function () {
'../../infrastructure/Modules': (this.Modules = {
promises: { hooks: { fire: sinon.stub().resolves() } },
}),
'./RecurlyClient': this.RecurlyClient,
},
})
})
@@ -335,6 +344,8 @@ describe('TeamInvitesHandler', function () {
email: 'tyrion@example.com',
}
this.ipAddress = '127.0.0.1'
this.UserGetter.promises.getUserByAnyEmail
.withArgs(this.user.email)
.resolves(this.user)
@@ -350,7 +361,8 @@ describe('TeamInvitesHandler', function () {
it('adds the user to the team', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
this.SubscriptionUpdater.promises.addUserToGroup
.calledWith(this.subscription._id, this.user.id)
@@ -360,7 +372,8 @@ describe('TeamInvitesHandler', function () {
it('removes the invite from the subscription', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
this.Subscription.updateOne
.calledWith(
@@ -375,7 +388,8 @@ describe('TeamInvitesHandler', function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
sinon.assert.called(
this.NotificationsBuilder.promises.groupInvitation(
@@ -389,7 +403,8 @@ describe('TeamInvitesHandler', function () {
it('should not schedule an SSO invite reminder', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
sinon.assert.notCalled(this.Modules.promises.hooks.fire)
})
@@ -401,7 +416,17 @@ describe('TeamInvitesHandler', function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
sinon.assert.calledWith(
this.SubscriptionUpdater.promises.deleteSubscription,
this.subscription,
{ id: this.user.id, ip: this.ipAddress }
)
sinon.assert.calledWith(
this.RecurlyClient.promises.terminateSubscriptionByUuid,
this.subscription.recurlySubscription_id
)
sinon.assert.calledWith(
this.Modules.promises.hooks.fire,
@@ -421,7 +446,8 @@ describe('TeamInvitesHandler', function () {
await this.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id
this.user.id,
this.ipAddress
)
sinon.assert.calledWith(
this.Modules.promises.hooks.fire,