Merge pull request #29982 from overleaf/ls-group-ownership-transfer

Stripe subscription ownership transfer

GitOrigin-RevId: 8285f635ecc220595782fbea6def74fdc9a92f36
This commit is contained in:
Liangjun Song
2025-12-10 13:35:58 +00:00
committed by Copybot
parent 427419e6a5
commit 7effe4630f
3 changed files with 135 additions and 23 deletions

View File

@@ -21,6 +21,7 @@ import Modules from '../../infrastructure/Modules.mjs'
* @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider
* @typedef {import('../../../../types/group-management/group-audit-log').GroupAuditLog} GroupAuditLog
* @import { AddOn } from '../../../../types/subscription/plan'
* @typedef {InstanceType<Subscription>} MongoSubscription
*/
/**
@@ -528,6 +529,38 @@ async function setRestorePoint(subscriptionId, planCode, addOns, consumed) {
await Subscription.updateOne({ _id: subscriptionId }, update).exec()
}
/**
* Change the ownershiop of the given subscription.
* @param {MongoSubscription} subscription
* @param {string} adminId
* @param {boolean} clearPreviousPaymentProvider whether to clear the previousPaymentProvider field or set it to the current paymentProvider
*/
async function transferSubscriptionOwnership(
subscription,
adminId,
clearPreviousPaymentProvider
) {
const query = {
_id: new ObjectId(subscription._id),
}
const update = {
$set: { admin_id: new ObjectId(adminId) },
}
if (subscription.groupPlan) {
update.$addToSet = { manager_ids: new ObjectId(adminId) }
} else {
update.$set.manager_ids = [new ObjectId(adminId)]
}
if (clearPreviousPaymentProvider) {
update.$unset = { previousPaymentProvider: 1 }
} else {
update.$set.previousPaymentProvider = subscription.paymentProvider
}
await Subscription.updateOne(query, update).exec()
}
/**
* Clears the restore point for a given subscription, and signals that the subscription was sucessfully reverted.
*
@@ -588,5 +621,6 @@ export default {
setSubscriptionWasReverted,
voidRestorePoint,
handleExpiredSubscription,
transferSubscriptionOwnership,
},
}

View File

@@ -4,6 +4,30 @@ import { TeamInviteSchema } from './TeamInvite.mjs'
const { Schema } = mongoose
const { ObjectId } = Schema
const PaymentProvider = {
service: {
type: String,
},
subscriptionId: {
type: String,
},
state: {
type: String,
},
pausePeriodStart: {
type: Date,
},
pausePeriodEnd: {
type: Date,
},
trialStartedAt: {
type: Date,
},
trialEndsAt: {
type: Date,
},
}
export const SubscriptionSchema = new Schema(
{
admin_id: {
@@ -68,29 +92,8 @@ export const SubscriptionSchema = new Schema(
type: Date,
},
},
paymentProvider: {
service: {
type: String,
},
subscriptionId: {
type: String,
},
state: {
type: String,
},
pausePeriodStart: {
type: Date,
},
pausePeriodEnd: {
type: Date,
},
trialStartedAt: {
type: Date,
},
trialEndsAt: {
type: Date,
},
},
paymentProvider: PaymentProvider,
previousPaymentProvider: PaymentProvider,
collectionMethod: {
type: String,
enum: ['automatic', 'manual'],

View File

@@ -303,6 +303,81 @@ describe('SubscriptionUpdater', function () {
})
})
describe('transferSubscriptionOwnership', function () {
it('should transfer the subscription ownership for group subscriptions', async function (ctx) {
ctx.subscription.groupPlan = true
ctx.subscription.paymentProvider = {
id: 'stripe-123',
name: 'stripe-us',
}
await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
ctx.subscription,
ctx.otherUserId,
false
)
const query = {
_id: new ObjectId(ctx.subscription._id),
}
const update = {
$set: {
admin_id: new ObjectId(ctx.otherUserId),
previousPaymentProvider: ctx.subscription.paymentProvider,
},
$addToSet: { manager_ids: new ObjectId(ctx.otherUserId) },
}
ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
})
it('should transfer the subscription ownership for non-group subscriptions', async function (ctx) {
ctx.subscription.paymentProvider = {
id: 'stripe-123',
name: 'stripe-us',
}
await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
ctx.subscription,
ctx.otherUserId,
false
)
const query = {
_id: new ObjectId(ctx.subscription._id),
}
const update = {
$set: {
admin_id: new ObjectId(ctx.otherUserId),
manager_ids: [new ObjectId(ctx.otherUserId)],
previousPaymentProvider: ctx.subscription.paymentProvider,
},
}
ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
})
it('should clear previousPaymentProvider when clearPreviousPaymentProvider is true', async function (ctx) {
ctx.subscription.paymentProvider = {
id: 'stripe-123',
name: 'stripe-us',
}
await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
ctx.subscription,
ctx.otherUserId,
true
)
const query = {
_id: new ObjectId(ctx.subscription._id),
}
const update = {
$set: {
admin_id: new ObjectId(ctx.otherUserId),
manager_ids: [new ObjectId(ctx.otherUserId)],
},
$unset: { previousPaymentProvider: 1 },
}
ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
})
})
describe('syncSubscription', function () {
beforeEach(function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(