Просмотр исходного кода

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

Stripe subscription ownership transfer

GitOrigin-RevId: 8285f635ecc220595782fbea6def74fdc9a92f36
Liangjun Song 8 месяцев назад
Родитель
Сommit
7effe4630f

+ 34 - 0
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<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,
   },
 }

+ 26 - 23
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'],

+ 75 - 0
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(