From 6b38336c7b52ae14ce9b91023bbe43a3e31c769f Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:36:33 +0200 Subject: [PATCH] Merge pull request #26397 from overleaf/kh-use-new-price-lookups [web] use new price lookup keys GitOrigin-RevId: f4c077d946100862aaea0288d5035a34d6188e83 --- .../src/Features/Subscription/PlansLocator.js | 85 +++++++++------ .../Subscription/SubscriptionController.js | 2 +- .../backfill_mixpanel_user_properties.mjs | 1 + .../src/Subscription/PlansLocatorTests.js | 103 ++++++++++++------ services/web/types/subscription/plan.ts | 26 +++-- 5 files changed, 135 insertions(+), 82 deletions(-) diff --git a/services/web/app/src/Features/Subscription/PlansLocator.js b/services/web/app/src/Features/Subscription/PlansLocator.js index 1d4fe210d5..67d2f31c52 100644 --- a/services/web/app/src/Features/Subscription/PlansLocator.js +++ b/services/web/app/src/Features/Subscription/PlansLocator.js @@ -1,11 +1,14 @@ -// TODO: This file may be deleted when Stripe is fully implemented to all users, so, consider deleting it +// @ts-check + const Settings = require('@overleaf/settings') const logger = require('@overleaf/logger') /** * @typedef {import('../../../../types/subscription/plan').RecurlyPlanCode} RecurlyPlanCode - * @typedef {import('../../../../types/subscription/plan').RecurlyAddOnCode} RecurlyAddOnCode * @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 */ @@ -26,18 +29,21 @@ function ensurePlansAreSetupCorrectly() { }) } -const recurlyPlanCodeToStripeLookupKey = { - collaborator: 'collaborator_may2025', - 'collaborator-annual': 'collaborator_annual_may2025', - collaborator_free_trial_7_days: 'collaborator_may2025', +/** + * @type {Record} + */ +const recurlyCodeToStripeBaseLookupKey = { + collaborator: 'standard_monthly', + 'collaborator-annual': 'standard_annual', + collaborator_free_trial_7_days: 'standard_monthly', - professional: 'professional_may2025', - 'professional-annual': 'professional_annual_may2025', - professional_free_trial_7_days: 'professional_may2025', + professional: 'professional_monthly', + 'professional-annual': 'professional_annual', + professional_free_trial_7_days: 'professional_monthly', - student: 'student_may2025', - 'student-annual': 'student_annual_may2025', - student_free_trial_7_days: 'student_may2025', + 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', @@ -45,41 +51,46 @@ const recurlyPlanCodeToStripeLookupKey = { group_professional: 'group_professional_enterprise', group_professional_educational: 'group_professional_educational', - assistant: 'assistant_may2025', - 'assistant-annual': 'assistant_annual_may2025', + assistant: 'assistant_monthly', + 'assistant-annual': 'assistant_annual', } -/** - * - * @param {RecurlyPlanCode} recurlyPlanCode - * @returns {StripeLookupKey} - */ -function mapRecurlyPlanCodeToStripeLookupKey(recurlyPlanCode) { - return recurlyPlanCodeToStripeLookupKey[recurlyPlanCode] -} +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 {RecurlyAddOnCode} recurlyAddOnCode - * @param {BillingCycleInterval} billingCycleInterval + * @param {RecurlyPlanCode} recurlyCode + * @param {StripeCurrencyCode} currency + * @param {BillingCycleInterval} [billingCycleInterval] -- needed for handling 'assistant' add-on * @returns {StripeLookupKey|null} */ -function mapRecurlyAddOnCodeToStripeLookupKey( - recurlyAddOnCode, - billingCycleInterval -) { +function buildStripeLookupKey(recurlyCode, currency, billingCycleInterval) { + let stripeBaseLookupKey = recurlyCodeToStripeBaseLookupKey[recurlyCode] + // Recurly always uses 'assistant' as the code regardless of the subscription duration - if (recurlyAddOnCode === 'assistant') { + if (recurlyCode === 'assistant' && billingCycleInterval) { if (billingCycleInterval === 'month') { - return 'assistant_may2025' + stripeBaseLookupKey = 'assistant_monthly' } if (billingCycleInterval === 'year') { - return 'assistant_annual_may2025' + stripeBaseLookupKey = 'assistant_annual' } } - return null + + 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} + */ const recurlyPlanCodeToPlanTypeAndPeriod = { collaborator: { planType: 'individual', period: 'monthly' }, 'collaborator-annual': { planType: 'individual', period: 'annual' }, @@ -106,14 +117,17 @@ const recurlyPlanCodeToPlanTypeAndPeriod = { } /** - * * @param {RecurlyPlanCode} recurlyPlanCode - * @returns {{ planType: 'individual' | 'group' | 'student' | null, period: 'annual' | 'monthly'}} + * @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) { @@ -126,7 +140,6 @@ function findLocalPlanInSettings(planCode) { module.exports = { ensurePlansAreSetupCorrectly, findLocalPlanInSettings, - mapRecurlyPlanCodeToStripeLookupKey, - mapRecurlyAddOnCodeToStripeLookupKey, + buildStripeLookupKey, getPlanTypeAndPeriodFromRecurlyPlanCode, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 3d70fd1d3f..4159ab5b93 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -839,7 +839,7 @@ function makeChangePreview( paymentMethod: paymentMethod?.toString(), netTerms: subscription.netTerms, nextPlan: { - annual: nextPlan.annual ?? false, + annual: nextPlan?.annual ?? false, }, nextInvoice: { date: subscription.periodEnd.toISOString(), diff --git a/services/web/scripts/backfill_mixpanel_user_properties.mjs b/services/web/scripts/backfill_mixpanel_user_properties.mjs index 3e966a3a62..ef8b8e0565 100644 --- a/services/web/scripts/backfill_mixpanel_user_properties.mjs +++ b/services/web/scripts/backfill_mixpanel_user_properties.mjs @@ -66,6 +66,7 @@ async function _getGroupSubscriptionPlanCode(userId) { const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) if ( plan && + plan.features && FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures) ) { bestPlanCode = plan.planCode diff --git a/services/web/test/unit/src/Subscription/PlansLocatorTests.js b/services/web/test/unit/src/Subscription/PlansLocatorTests.js index e0db2e825d..bd15f5cfaa 100644 --- a/services/web/test/unit/src/Subscription/PlansLocatorTests.js +++ b/services/web/test/unit/src/Subscription/PlansLocatorTests.js @@ -50,84 +50,111 @@ describe('PlansLocator', function () { }) }) - describe('mapRecurlyPlanCodeToStripeLookupKey', function () { + describe('buildStripeLookupKey', function () { it('should map "collaborator" plan code to stripe lookup keys', function () { const planCode = 'collaborator' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('collaborator_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('standard_monthly_jun2025_eur') }) it('should map "collaborator_free_trial_7_days" plan code to stripe lookup keys', function () { const planCode = 'collaborator_free_trial_7_days' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('collaborator_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('standard_monthly_jun2025_eur') }) it('should map "collaborator-annual" plan code to stripe lookup keys', function () { const planCode = 'collaborator-annual' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('collaborator_annual_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('standard_annual_jun2025_eur') }) it('should map "professional" plan code to stripe lookup keys', function () { const planCode = 'professional' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('professional_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('professional_monthly_jun2025_eur') }) it('should map "professional_free_trial_7_days" plan code to stripe lookup keys', function () { const planCode = 'professional_free_trial_7_days' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('professional_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('professional_monthly_jun2025_eur') }) it('should map "professional-annual" plan code to stripe lookup keys', function () { const planCode = 'professional-annual' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('professional_annual_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('professional_annual_jun2025_eur') }) it('should map "student" plan code to stripe lookup keys', function () { const planCode = 'student' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('student_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('student_monthly_jun2025_eur') }) it('shoult map "student_free_trial_7_days" plan code to stripe lookup keys', function () { const planCode = 'student_free_trial_7_days' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('student_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('student_monthly_jun2025_eur') }) it('should map "student-annual" plan code to stripe lookup keys', function () { const planCode = 'student-annual' - const lookupKey = - this.PlansLocator.mapRecurlyPlanCodeToStripeLookupKey(planCode) - expect(lookupKey).to.equal('student_annual_may2025') + const currency = 'eur' + const lookupKey = this.PlansLocator.buildStripeLookupKey( + planCode, + currency + ) + expect(lookupKey).to.equal('student_annual_jun2025_eur') }) - }) - describe('mapRecurlyAddOnCodeToStripeLookupKey', function () { it('should return null for unknown add-on codes', function () { const billingCycleInterval = 'month' const addOnCode = 'unknown_addon' - const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey( + const currency = 'gbp' + const lookupKey = this.PlansLocator.buildStripeLookupKey( addOnCode, + currency, billingCycleInterval ) expect(lookupKey).to.equal(null) }) it('should handle missing input', function () { - const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey( + const lookupKey = this.PlansLocator.buildStripeLookupKey( undefined, undefined ) @@ -137,21 +164,25 @@ describe('PlansLocator', function () { it('returns the key for a monthly AI assist add-on', function () { const billingCycleInterval = 'month' const addOnCode = this.AI_ADD_ON_CODE - const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey( + const currency = 'gbp' + const lookupKey = this.PlansLocator.buildStripeLookupKey( addOnCode, + currency, billingCycleInterval ) - expect(lookupKey).to.equal('assistant_may2025') + expect(lookupKey).to.equal('assistant_monthly_jun2025_gbp') }) it('returns the key for an annual AI assist add-on', function () { const billingCycleInterval = 'year' const addOnCode = this.AI_ADD_ON_CODE - const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey( + const currency = 'gbp' + const lookupKey = this.PlansLocator.buildStripeLookupKey( addOnCode, + currency, billingCycleInterval ) - expect(lookupKey).to.equal('assistant_annual_may2025') + expect(lookupKey).to.equal('assistant_annual_jun2025_gbp') }) }) diff --git a/services/web/types/subscription/plan.ts b/services/web/types/subscription/plan.ts index 5a0d40a695..d6f3008a19 100644 --- a/services/web/types/subscription/plan.ts +++ b/services/web/types/subscription/plan.ts @@ -1,3 +1,5 @@ +import { StripeCurrencyCode } from './currency' + type Features = { collaborators: number compileGroup: string @@ -60,6 +62,7 @@ export type Plan = { name: string planCode: string price_in_cents: number + canUseFlexibleLicensing?: boolean } export type PriceForDisplayData = { @@ -90,17 +93,22 @@ export type RecurlyPlanCode = export type RecurlyAddOnCode = 'assistant' -export type StripeLookupKey = - | 'collaborator_may2025' - | 'collaborator_annual_may2025' - | 'professional_may2025' - | 'professional_annual_may2025' - | 'student_may2025' - | 'student_annual_may2025' +export type StripeBaseLookupKey = + | 'standard_monthly' + | 'standard_annual' + | 'professional_monthly' + | 'professional_annual' + | 'student_monthly' + | 'student_annual' + | 'assistant_annual' + | 'assistant_monthly' // TODO: change all group plans' lookup_keys to match the UK account after they have been added | 'group_standard_enterprise' | 'group_professional_enterprise' | 'group_standard_educational' | 'group_professional_educational' - | 'assistant_annual_may2025' - | 'assistant_may2025' + +export type StripeLookupKeyVersion = 'jun2025' + +export type StripeLookupKey = + `${StripeBaseLookupKey}_${StripeLookupKeyVersion}_${StripeCurrencyCode}`