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

[web] display all Stripe accounts on admin panel (#29625)

* refactor PaymentService.getPaymentProviderAdminUrl to be more useful
* display all stripe customer accounts
* mv change segment modal to each account
* stripeSubscriptionData -> stripeCustomerData

GitOrigin-RevId: 4c1a277f5073ee7cb12f4596210ba4f8624451b8
Kristina 9 месяцев назад
Родитель
Сommit
3194434767

+ 1 - 0
services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs

@@ -653,6 +653,7 @@ export class PaymentProviderAccount {
    * @param {boolean} [props.hasPastDueInvoice]
    * @param {object} [props.metadata]
    * @param {string} [props.metadata.userId]
+   * @param {string} [props.metadata.segment]
    */
   constructor(props) {
     this.code = props.code

+ 0 - 19
services/web/app/src/Features/Subscription/RecurlyClient.mjs

@@ -799,24 +799,6 @@ async function terminateSubscriptionByUuid(subscriptionUuid) {
   return subscription
 }
 
-/**
- * Get the Recurly admin dashboard url for a user
- *
- * @param {string} userId
- * @returns {string}
- */
-function getCustomerAdminUrlFromUserId(userId) {
-  const isStagOrDev =
-    Settings.siteUrl.includes('dev-overleaf') ||
-    Settings.siteUrl.includes('stag-overleaf')
-
-  if (isStagOrDev) {
-    return `https://sharelatex-sandbox.recurly.com/accounts/${userId}`
-  }
-
-  return `https://sharelatex.recurly.com/accounts/${userId}`
-}
-
 export default {
   errors: recurly.errors,
 
@@ -841,7 +823,6 @@ export default {
   getPastDueInvoices: callbackify(getPastDueInvoices),
   failInvoice: callbackify(failInvoice),
   terminateSubscriptionByUuid: callbackify(terminateSubscriptionByUuid),
-  getCustomerAdminUrlFromUserId,
 
   promises: {
     getSubscription,

+ 59 - 0
services/web/app/src/Features/Subscription/SubscriptionHelper.js

@@ -1,3 +1,6 @@
+// @ts-check
+
+const Settings = require('@overleaf/settings')
 const { formatCurrency } = require('../../util/currency')
 const GroupPlansData = require('./GroupPlansData')
 const { isStandaloneAiAddOnPlanCode } = require('./AiHelper')
@@ -196,11 +199,67 @@ function isInTrial(trialEndsAt) {
   return trialEndsAt.getTime() > Date.now()
 }
 
+/**
+ * Get the Recurly customer admin URL
+ * @param {string | null} customerId - The customer ID in Recurly
+ * @returns {string | null}
+ */
+function getRecurlyCustomerAdminUrl(customerId) {
+  if (customerId == null) {
+    return null
+  }
+
+  const isStagOrDev =
+    Settings.siteUrl.includes('dev-overleaf') ||
+    Settings.siteUrl.includes('stag-overleaf')
+
+  const baseUrl = isStagOrDev
+    ? 'https://sharelatex-sandbox.recurly.com'
+    : 'https://sharelatex.recurly.com'
+
+  return `${baseUrl}/accounts/${customerId}`
+}
+
+/**
+ * Get the Stripe customer admin URL
+ * @param {string | null} customerId - The customer ID in Stripe
+ * @param {string} service - The Stripe service ('stripe-us' or 'stripe-uk')
+ * @returns {string | null}
+ */
+function getStripeCustomerAdminUrl(customerId, service) {
+  if (customerId == null || service == null) {
+    return null
+  }
+
+  let accountId = null
+  if (service === 'stripe-us') {
+    accountId = Settings.apis.stripeUS?.accountId
+  } else if (service === 'stripe-uk') {
+    accountId = Settings.apis.stripeUK?.accountId
+  }
+
+  if (accountId == null) {
+    return null
+  }
+
+  const isStagOrDev =
+    Settings.siteUrl.includes('dev-overleaf') ||
+    Settings.siteUrl.includes('stag-overleaf')
+
+  const baseUrl = isStagOrDev
+    ? `https://dashboard.stripe.com/${accountId}/test`
+    : `https://dashboard.stripe.com/${accountId}`
+
+  return `${baseUrl}/customers/${customerId}`
+}
+
 module.exports = {
   shouldPlanChangeAtTermEnd,
   generateInitialLocalizedGroupPrice,
   isPaidSubscription,
   isIndividualActivePaidSubscription,
+  getRecurlyCustomerAdminUrl,
+  getStripeCustomerAdminUrl,
   getPaymentProviderSubscriptionId,
   getPaidSubscriptionState,
   getSubscriptionTrialStartedAt,

+ 8 - 6
services/web/frontend/js/utils/meta.ts

@@ -275,15 +275,17 @@ export interface Meta {
     annual?: string
     monthlyTimesTwelve?: string
   }
-  'ol-stripeAccountId': string
-  'ol-stripePublicKeyUK': string
-  'ol-stripePublicKeyUS': string
-  'ol-stripeSubscriptionData': {
+  'ol-stripeCustomerData': Array<{
     customerId: string
+    subscriptionId: string
     subscriptionState: string | null
     paymentProviderService: StripePaymentProviderService | null
-    segment: string | null
-  }
+    managementUrl: string
+    segment?: string | null
+    error?: string
+  }>
+  'ol-stripePublicKeyUK': string
+  'ol-stripePublicKeyUS': string
   'ol-subscription': any // TODO: mixed types, split into two fields
   'ol-subscriptionChangePreview': SubscriptionChangePreview
   'ol-subscriptionCreationPreview': SubscriptionCreationPreview

+ 0 - 27
services/web/test/unit/src/Subscription/RecurlyClient.test.mjs

@@ -789,31 +789,4 @@ describe('RecurlyClient', function () {
       expect(invoices).to.deep.equal(pastDueInvoices)
     })
   })
-
-  describe('getCustomerAdminUrlFromUserId', function () {
-    it('should return staging URL for dev-overleaf sites', async function (ctx) {
-      ctx.settings.siteUrl = 'https://dev-overleaf.example.com'
-      const userId = 'user-123'
-      const url = await ctx.RecurlyClient.getCustomerAdminUrlFromUserId(userId)
-      expect(url).to.equal(
-        'https://sharelatex-sandbox.recurly.com/accounts/user-123'
-      )
-    })
-
-    it('should return staging URL for stag-overleaf sites', async function (ctx) {
-      ctx.settings.siteUrl = 'https://stag-overleaf.example.com'
-      const userId = 'user-456'
-      const url = await ctx.RecurlyClient.getCustomerAdminUrlFromUserId(userId)
-      expect(url).to.equal(
-        'https://sharelatex-sandbox.recurly.com/accounts/user-456'
-      )
-    })
-
-    it('should return production URL for production sites', async function (ctx) {
-      ctx.settings.siteUrl = 'https://www.overleaf.com'
-      const userId = 'user-789'
-      const url = await ctx.RecurlyClient.getCustomerAdminUrlFromUserId(userId)
-      expect(url).to.equal('https://sharelatex.recurly.com/accounts/user-789')
-    })
-  })
 })

+ 200 - 0
services/web/test/unit/src/Subscription/SubscriptionHelperTests.js

@@ -692,4 +692,204 @@ describe('SubscriptionHelper', function () {
       })
     })
   })
+
+  describe('getRecurlyCustomerAdminUrl', function () {
+    beforeEach(function () {
+      this.settings.siteUrl = 'https://www.overleaf.com'
+    })
+
+    it('should return production Recurly account URL', function () {
+      const result =
+        this.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
+      expect(result).to.equal(
+        'https://sharelatex.recurly.com/accounts/user_789'
+      )
+    })
+
+    it('should return sandbox Recurly account URL for dev environment', function () {
+      this.settings.siteUrl = 'https://dev-overleaf.com'
+      const result =
+        this.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
+      expect(result).to.equal(
+        'https://sharelatex-sandbox.recurly.com/accounts/user_789'
+      )
+    })
+
+    it('should return sandbox Recurly account URL for staging environment', function () {
+      this.settings.siteUrl = 'https://stag-overleaf.com'
+      const result =
+        this.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
+      expect(result).to.equal(
+        'https://sharelatex-sandbox.recurly.com/accounts/user_789'
+      )
+    })
+
+    it('should return null if customerId is null', function () {
+      const result = this.SubscriptionHelper.getRecurlyCustomerAdminUrl(null)
+      expect(result).to.be.null
+    })
+
+    it('should return null if customerId is undefined', function () {
+      const result =
+        this.SubscriptionHelper.getRecurlyCustomerAdminUrl(undefined)
+      expect(result).to.be.null
+    })
+
+    it('should handle empty string customerId', function () {
+      const result = this.SubscriptionHelper.getRecurlyCustomerAdminUrl('')
+      expect(result).to.equal('https://sharelatex.recurly.com/accounts/')
+    })
+  })
+
+  describe('getStripeCustomerAdminUrl', function () {
+    beforeEach(function () {
+      this.settings.siteUrl = 'https://www.overleaf.com'
+      this.settings.apis = {
+        stripeUS: { accountId: 'acct_us_123' },
+        stripeUK: { accountId: 'acct_uk_456' },
+      }
+    })
+
+    describe('stripe-us', function () {
+      it('should return production Stripe US customer URL', function () {
+        const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+          'cus_us_789',
+          'stripe-us'
+        )
+        expect(result).to.equal(
+          'https://dashboard.stripe.com/acct_us_123/customers/cus_us_789'
+        )
+      })
+
+      it('should return test Stripe US customer URL for dev environment', function () {
+        this.settings.siteUrl = 'https://dev-overleaf.com'
+        const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+          'cus_us_789',
+          'stripe-us'
+        )
+        expect(result).to.equal(
+          'https://dashboard.stripe.com/acct_us_123/test/customers/cus_us_789'
+        )
+      })
+
+      it('should return test Stripe US customer URL for staging environment', function () {
+        this.settings.siteUrl = 'https://stag-overleaf.com'
+        const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+          'cus_us_789',
+          'stripe-us'
+        )
+        expect(result).to.equal(
+          'https://dashboard.stripe.com/acct_us_123/test/customers/cus_us_789'
+        )
+      })
+    })
+
+    describe('stripe-uk', function () {
+      it('should return production Stripe UK customer URL', function () {
+        const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+          'cus_uk_123',
+          'stripe-uk'
+        )
+        expect(result).to.equal(
+          'https://dashboard.stripe.com/acct_uk_456/customers/cus_uk_123'
+        )
+      })
+
+      it('should return test Stripe UK customer URL for dev environment', function () {
+        this.settings.siteUrl = 'https://dev-overleaf.com'
+        const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+          'cus_uk_123',
+          'stripe-uk'
+        )
+        expect(result).to.equal(
+          'https://dashboard.stripe.com/acct_uk_456/test/customers/cus_uk_123'
+        )
+      })
+    })
+
+    it('should return null if accountId is missing', function () {
+      this.settings.apis.stripeUS = {}
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_us_789',
+        'stripe-us'
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if customerId is null', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        null,
+        'stripe-us'
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if service is null', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_us_789',
+        null
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if customerId is undefined', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        undefined,
+        'stripe-us'
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if service is undefined', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_us_789',
+        undefined
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if both customerId and service are null', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        null,
+        null
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if accountId is missing for UK', function () {
+      this.settings.apis.stripeUK = {}
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_uk_789',
+        'stripe-uk'
+      )
+      expect(result).to.be.null
+    })
+
+    it('should return null if apis object is missing', function () {
+      this.settings.apis = {}
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_us_789',
+        'stripe-us'
+      )
+      expect(result).to.be.null
+    })
+
+    it('should handle empty string customerId', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        '',
+        'stripe-us'
+      )
+      expect(result).to.equal(
+        'https://dashboard.stripe.com/acct_us_123/customers/'
+      )
+    })
+
+    it('should return null if service is not stripe-us or stripe-uk', function () {
+      const result = this.SubscriptionHelper.getStripeCustomerAdminUrl(
+        'cus_us_789',
+        'some-other-service'
+      )
+      expect(result).to.be.null
+    })
+  })
 })