From fdffa41d1c171309551322fcdf713bc5a400c69f Mon Sep 17 00:00:00 2001 From: M Fahru Date: Mon, 24 Mar 2025 07:03:21 -0700 Subject: [PATCH] Merge pull request #24417 from overleaf/mf-implement-stripe-hosted-checkout-split-test [web] Implement stripe hosted checkout with split test GitOrigin-RevId: 25e5ff2a46135f402cdf479623ab38c858c5640c --- .../src/Features/Subscription/PlansLocator.js | 53 +++++++ .../Subscription/SubscriptionController.js | 9 +- .../src/Subscription/PlansLocatorTests.js | 146 ++++++++++++++++++ services/web/types/subscription/plan.ts | 19 +++ 4 files changed, 226 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Subscription/PlansLocator.js b/services/web/app/src/Features/Subscription/PlansLocator.js index 7497c78c91..937d2d3ccb 100644 --- a/services/web/app/src/Features/Subscription/PlansLocator.js +++ b/services/web/app/src/Features/Subscription/PlansLocator.js @@ -1,6 +1,12 @@ +// TODO: This file may be deleted when Stripe is fully implemented to all users, so, consider deleting it const Settings = require('@overleaf/settings') const logger = require('@overleaf/logger') +/** + * @typedef {import('../../../../types/subscription/plan').RecurlyPlanCode} RecurlyPlanCode + * @typedef {import('../../../../types/subscription/plan').StripeLookupKey} StripeLookupKey + */ + function ensurePlansAreSetupCorrectly() { Settings.plans.forEach(plan => { if (typeof plan.price_in_cents !== 'number') { @@ -18,6 +24,51 @@ function ensurePlansAreSetupCorrectly() { }) } +const recurlyPlanCodeToStripeLookupKey = { + 'professional-annual': 'professional_annual', + professional: 'professional_monthly', + professional_free_trial_7_days: 'professional_monthly', + 'collaborator-annual': 'standard_annual', + collaborator: 'standard_monthly', + collaborator_free_trial_7_days: 'standard_monthly', + 'student-annual': 'student_annual', + student: 'student_monthly', + student_free_trial_7_days: 'student_monthly', +} + +/** + * + * @param {RecurlyPlanCode} recurlyPlanCode + * @returns {StripeLookupKey} + */ +function mapRecurlyPlanCodeToStripeLookupKey(recurlyPlanCode) { + return recurlyPlanCodeToStripeLookupKey[recurlyPlanCode] +} + +const recurlyPlanCodeToPlanTypeAndPeriod = { + collaborator: { planType: 'standard', period: 'monthly' }, + collaborator_free_trial_7_days: { planType: 'standard', period: 'monthly' }, + 'collaborator-annual': { planType: 'standard', period: 'annual' }, + professional: { planType: 'professional', period: 'monthly' }, + professional_free_trial_7_days: { + planType: 'professional', + period: 'monthly', + }, + 'professional-annual': { planType: 'professional', period: 'annual' }, + student: { planType: 'student', period: 'monthly' }, + student_free_trial_7_days: { planType: 'student', period: 'monthly' }, + 'student-annual': { planType: 'student', period: 'annual' }, +} + +/** + * + * @param {RecurlyPlanCode} recurlyPlanCode + * @returns {{ planType: 'standard' | 'professional' | 'student', period: 'annual' | 'monthly'}} + */ +function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) { + return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode] +} + function findLocalPlanInSettings(planCode) { for (const plan of Settings.plans) { if (plan.planCode === planCode) { @@ -30,4 +81,6 @@ function findLocalPlanInSettings(planCode) { module.exports = { ensurePlansAreSetupCorrectly, findLocalPlanInSettings, + mapRecurlyPlanCodeToStripeLookupKey, + getPlanTypeAndPeriodFromRecurlyPlanCode, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index cfd10a08ba..784083fdd8 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -629,7 +629,7 @@ async function getRecommendedCurrency(req, res) { ip = req.query.ip } const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip) - const countryCode = currencyLookup.countryCode + let countryCode = currencyLookup.countryCode const recommendedCurrency = currencyLookup.currencyCode let currency = null @@ -640,6 +640,13 @@ async function getRecommendedCurrency(req, res) { currency = recommendedCurrency } + const queryCountryCode = req.query.countryCode?.toUpperCase() + + // only enable countryCode testing flag on staging or dev environments + if (queryCountryCode && process.env.NODE_ENV !== 'production') { + countryCode = queryCountryCode + } + return { currency, recommendedCurrency, diff --git a/services/web/test/unit/src/Subscription/PlansLocatorTests.js b/services/web/test/unit/src/Subscription/PlansLocatorTests.js index e0807d4247..9373c02b89 100644 --- a/services/web/test/unit/src/Subscription/PlansLocatorTests.js +++ b/services/web/test/unit/src/Subscription/PlansLocatorTests.js @@ -48,4 +48,150 @@ describe('PlansLocator', function () { expect(plan).to.be.a('null') }) }) + + describe('mapRecurlyPlanCodeToStripeLookupKey', 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('standard_monthly') + }) + + 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('standard_monthly') + }) + + 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('standard_annual') + }) + + 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_monthly') + }) + + 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_monthly') + }) + + 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') + }) + + 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_monthly') + }) + + 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_monthly') + }) + + 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') + }) + }) + + describe('getPlanTypeAndPeriodFromRecurlyPlanCode', function () { + it('should return the plan type and period for "collaborator"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'collaborator' + ) + expect(planType).to.equal('standard') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "collaborator_free_trial_7_days"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'collaborator_free_trial_7_days' + ) + expect(planType).to.equal('standard') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "collaborator-annual"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'collaborator-annual' + ) + expect(planType).to.equal('standard') + expect(period).to.equal('annual') + }) + + it('should return the plan type and period for "professional"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'professional' + ) + expect(planType).to.equal('professional') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "professional_free_trial_7_days"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'professional_free_trial_7_days' + ) + expect(planType).to.equal('professional') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "professional-annual"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'professional-annual' + ) + expect(planType).to.equal('professional') + expect(period).to.equal('annual') + }) + + it('should return the plan type and period for "student"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode('student') + expect(planType).to.equal('student') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "student_free_trial_7_days"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'student_free_trial_7_days' + ) + expect(planType).to.equal('student') + expect(period).to.equal('monthly') + }) + + it('should return the plan type and period for "student-annual"', function () { + const { planType, period } = + this.PlansLocator.getPlanTypeAndPeriodFromRecurlyPlanCode( + 'student-annual' + ) + expect(planType).to.equal('student') + expect(period).to.equal('annual') + }) + }) }) diff --git a/services/web/types/subscription/plan.ts b/services/web/types/subscription/plan.ts index e97a354b5a..8747663575 100644 --- a/services/web/types/subscription/plan.ts +++ b/services/web/types/subscription/plan.ts @@ -68,3 +68,22 @@ export type PriceForDisplayData = { includesTax: boolean perUserDisplayPrice?: string } + +export type RecurlyPlanCode = + | 'collaborator' + | 'collaborator-annual' + | 'collaborator_free_trial_7_days' + | 'professional' + | 'professional-annual' + | 'professional_free_trial_7_days' + | 'student' + | 'student-annual' + | 'student_free_trial_7_days' + +export type StripeLookupKey = + | 'collaborator_monthly' + | 'collaborator_annual' + | 'professional_monthly' + | 'professional_annual' + | 'student_monthly' + | 'student_annual'