mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-05 15:19:02 +02:00
9f78291e94
Support individual to group plan upgrade in Stripe GitOrigin-RevId: 24cbe7bd6de86a4d9410e1abc49b6457e0871f40
184 lines
6.0 KiB
JavaScript
184 lines
6.0 KiB
JavaScript
// @ts-check
|
|
|
|
const Settings = require('@overleaf/settings')
|
|
const logger = require('@overleaf/logger')
|
|
|
|
/**
|
|
* @typedef {import('../../../../types/subscription/plan').RecurlyPlanCode} RecurlyPlanCode
|
|
* @typedef {import('../../../../types/subscription/plan').StripeLookupKey} StripeLookupKey
|
|
* @typedef {import('../../../../types/subscription/plan').StripeBaseLookupKey} StripeBaseLookupKey
|
|
* @typedef {import('../../../../types/subscription/plan').Plan} Plan
|
|
* @typedef {import('../../../../types/subscription/currency').StripeCurrencyCode} StripeCurrencyCode
|
|
* @typedef {import('stripe').Stripe.Price.Recurring.Interval} BillingCycleInterval
|
|
*/
|
|
|
|
function ensurePlansAreSetupCorrectly() {
|
|
Settings.plans.forEach(plan => {
|
|
if (typeof plan.price_in_cents !== 'number') {
|
|
logger.fatal({ plan }, 'missing price on plan')
|
|
process.exit(1)
|
|
}
|
|
if (plan.price) {
|
|
logger.fatal({ plan }, 'unclear price attribute on plan')
|
|
process.exit(1)
|
|
}
|
|
if (plan.price_in_unit) {
|
|
logger.fatal({ plan }, 'deprecated price_in_unit attribute on plan')
|
|
process.exit(1)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @type {Record<RecurlyPlanCode, StripeBaseLookupKey>}
|
|
*/
|
|
const recurlyCodeToStripeBaseLookupKey = {
|
|
collaborator: 'standard_monthly',
|
|
'collaborator-annual': 'standard_annual',
|
|
collaborator_free_trial_7_days: 'standard_monthly',
|
|
|
|
professional: 'professional_monthly',
|
|
'professional-annual': 'professional_annual',
|
|
professional_free_trial_7_days: 'professional_monthly',
|
|
|
|
student: 'student_monthly',
|
|
'student-annual': 'student_annual',
|
|
student_free_trial_7_days: 'student_monthly',
|
|
|
|
// TODO: change all group plans' lookup_keys to match the UK account after they have been added
|
|
group_collaborator: 'group_standard_enterprise',
|
|
group_collaborator_educational: 'group_standard_educational',
|
|
group_professional: 'group_professional_enterprise',
|
|
group_professional_educational: 'group_professional_educational',
|
|
|
|
assistant: 'assistant_monthly',
|
|
'assistant-annual': 'assistant_annual',
|
|
}
|
|
|
|
const LATEST_STRIPE_LOOKUP_KEY_VERSION = 'jun2025'
|
|
|
|
/**
|
|
* Build the Stripe lookup key, will be in this format:
|
|
* `${productCode}_${billingInterval}_${latestVersion}_${currency}`
|
|
* (for example: 'assistant_annual_jun2025_clp')
|
|
*
|
|
* @param {RecurlyPlanCode} recurlyCode
|
|
* @param {StripeCurrencyCode} currency
|
|
* @param {BillingCycleInterval} [billingCycleInterval] -- needed for handling 'assistant' add-on
|
|
* @returns {StripeLookupKey|null}
|
|
*/
|
|
function buildStripeLookupKey(recurlyCode, currency, billingCycleInterval) {
|
|
let stripeBaseLookupKey = recurlyCodeToStripeBaseLookupKey[recurlyCode]
|
|
|
|
// Recurly always uses 'assistant' as the code regardless of the subscription duration
|
|
if (recurlyCode === 'assistant' && billingCycleInterval) {
|
|
if (billingCycleInterval === 'month') {
|
|
stripeBaseLookupKey = 'assistant_monthly'
|
|
}
|
|
if (billingCycleInterval === 'year') {
|
|
stripeBaseLookupKey = 'assistant_annual'
|
|
}
|
|
}
|
|
|
|
if (stripeBaseLookupKey == null) {
|
|
return null
|
|
}
|
|
|
|
return `${stripeBaseLookupKey}_${LATEST_STRIPE_LOOKUP_KEY_VERSION}_${currency}`
|
|
}
|
|
|
|
/**
|
|
* @typedef {{ planType: 'individual' | 'group' | 'student' | null, period: 'annual' | 'monthly' }} PlanTypeAndPeriod
|
|
* @type {Record<RecurlyPlanCode, PlanTypeAndPeriod>}
|
|
*/
|
|
const recurlyPlanCodeToPlanTypeAndPeriod = {
|
|
collaborator: { planType: 'individual', period: 'monthly' },
|
|
'collaborator-annual': { planType: 'individual', period: 'annual' },
|
|
collaborator_free_trial_7_days: { planType: 'individual', period: 'monthly' },
|
|
|
|
professional: { planType: 'individual', period: 'monthly' },
|
|
'professional-annual': { planType: 'individual', period: 'annual' },
|
|
professional_free_trial_7_days: {
|
|
planType: 'individual',
|
|
period: 'monthly',
|
|
},
|
|
|
|
student: { planType: 'student', period: 'monthly' },
|
|
'student-annual': { planType: 'student', period: 'annual' },
|
|
student_free_trial_7_days: { planType: 'student', period: 'monthly' },
|
|
|
|
group_collaborator: { planType: 'group', period: 'annual' },
|
|
group_collaborator_educational: { planType: 'group', period: 'annual' },
|
|
group_professional: { planType: 'group', period: 'annual' },
|
|
group_professional_educational: { planType: 'group', period: 'annual' },
|
|
|
|
assistant: { planType: null, period: 'monthly' },
|
|
'assistant-annual': { planType: null, period: 'annual' },
|
|
}
|
|
|
|
/**
|
|
* @param {RecurlyPlanCode} recurlyPlanCode
|
|
* @returns {PlanTypeAndPeriod}
|
|
*/
|
|
function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) {
|
|
return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode]
|
|
}
|
|
|
|
/**
|
|
* @param {string|null} [planCode]
|
|
* @returns {Plan|null}
|
|
*/
|
|
function findLocalPlanInSettings(planCode) {
|
|
for (const plan of Settings.plans) {
|
|
if (plan.planCode === planCode) {
|
|
return plan
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given plan code is a group plan
|
|
*
|
|
* @param {string} planCode
|
|
*/
|
|
function isGroupPlanCode(planCode) {
|
|
return planCode.includes('group')
|
|
}
|
|
|
|
/**
|
|
* Adapts a legacy Recurly group plan code (e.g., `group_professional_5_educational`)
|
|
* into its corresponding Stripe-compatible plan code (e.g., `group_professional_educational`),
|
|
* extracting the license quantity where applicable.
|
|
*
|
|
* @param {RecurlyPlanCode} planCode
|
|
* @returns {{ planCode: RecurlyPlanCode, quantity: number }}
|
|
*/
|
|
function convertLegacyGroupPlanCodeToConsolidatedGroupPlanCodeIfNeeded(
|
|
planCode
|
|
) {
|
|
const pattern =
|
|
/^group_(collaborator|professional)_(2|3|4|5|10|20|50)_(educational|enterprise)$/
|
|
|
|
const match = planCode.match(pattern)
|
|
if (match == null) {
|
|
return { planCode, quantity: 1 }
|
|
}
|
|
|
|
const [, tier, size, usage] = match
|
|
const newPlanCode = /** @type {RecurlyPlanCode} */ (
|
|
usage === 'enterprise' ? `group_${tier}` : `group_${tier}_${usage}`
|
|
)
|
|
|
|
return { planCode: newPlanCode, quantity: Number(size) }
|
|
}
|
|
|
|
module.exports = {
|
|
ensurePlansAreSetupCorrectly,
|
|
findLocalPlanInSettings,
|
|
buildStripeLookupKey,
|
|
getPlanTypeAndPeriodFromRecurlyPlanCode,
|
|
isGroupPlanCode,
|
|
convertLegacyGroupPlanCodeToConsolidatedGroupPlanCodeIfNeeded,
|
|
}
|