diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.mjs b/services/web/app/src/Features/Subscription/SubscriptionUpdater.mjs index a6ae37103b..99042b9566 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.mjs @@ -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} 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, }, } diff --git a/services/web/app/src/models/Subscription.mjs b/services/web/app/src/models/Subscription.mjs index 9134541192..c7d1deaaa0 100644 --- a/services/web/app/src/models/Subscription.mjs +++ b/services/web/app/src/models/Subscription.mjs @@ -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'], diff --git a/services/web/test/unit/src/Subscription/SubscriptionUpdater.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionUpdater.test.mjs index d85edfa24b..0ba33a592b 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionUpdater.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionUpdater.test.mjs @@ -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(