[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
This commit is contained in:
Kristina
2025-11-14 11:37:32 +01:00
committed by Copybot
parent 939efc7201
commit 3194434767
6 changed files with 268 additions and 52 deletions
@@ -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
@@ -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,
@@ -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
View File
@@ -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
@@ -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')
})
})
})
@@ -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
})
})
})