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

Merge pull request #26397 from overleaf/kh-use-new-price-lookups

[web] use new price lookup keys

GitOrigin-RevId: f4c077d946100862aaea0288d5035a34d6188e83
Kristina 1 год назад
Родитель
Сommit
6b38336c7b

+ 49 - 36
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 Settings = require('@overleaf/settings')
 const logger = require('@overleaf/logger')
 const logger = require('@overleaf/logger')
 
 
 /**
 /**
  * @typedef {import('../../../../types/subscription/plan').RecurlyPlanCode} RecurlyPlanCode
  * @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').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
  * @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<RecurlyPlanCode, StripeBaseLookupKey>}
+ */
+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
   // TODO: change all group plans' lookup_keys to match the UK account after they have been added
   group_collaborator: 'group_standard_enterprise',
   group_collaborator: 'group_standard_enterprise',
@@ -45,41 +51,46 @@ const recurlyPlanCodeToStripeLookupKey = {
   group_professional: 'group_professional_enterprise',
   group_professional: 'group_professional_enterprise',
   group_professional_educational: 'group_professional_educational',
   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}
  * @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
   // Recurly always uses 'assistant' as the code regardless of the subscription duration
-  if (recurlyAddOnCode === 'assistant') {
+  if (recurlyCode === 'assistant' && billingCycleInterval) {
     if (billingCycleInterval === 'month') {
     if (billingCycleInterval === 'month') {
-      return 'assistant_may2025'
+      stripeBaseLookupKey = 'assistant_monthly'
     }
     }
     if (billingCycleInterval === 'year') {
     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<RecurlyPlanCode, PlanTypeAndPeriod>}
+ */
 const recurlyPlanCodeToPlanTypeAndPeriod = {
 const recurlyPlanCodeToPlanTypeAndPeriod = {
   collaborator: { planType: 'individual', period: 'monthly' },
   collaborator: { planType: 'individual', period: 'monthly' },
   'collaborator-annual': { planType: 'individual', period: 'annual' },
   'collaborator-annual': { planType: 'individual', period: 'annual' },
@@ -106,14 +117,17 @@ const recurlyPlanCodeToPlanTypeAndPeriod = {
 }
 }
 
 
 /**
 /**
- *
  * @param {RecurlyPlanCode} recurlyPlanCode
  * @param {RecurlyPlanCode} recurlyPlanCode
- * @returns {{ planType: 'individual' | 'group' | 'student' | null, period: 'annual' | 'monthly'}}
+ * @returns {PlanTypeAndPeriod}
  */
  */
 function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) {
 function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) {
   return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode]
   return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode]
 }
 }
 
 
+/**
+ * @param {string|null} [planCode]
+ * @returns {Plan|null}
+ */
 function findLocalPlanInSettings(planCode) {
 function findLocalPlanInSettings(planCode) {
   for (const plan of Settings.plans) {
   for (const plan of Settings.plans) {
     if (plan.planCode === planCode) {
     if (plan.planCode === planCode) {
@@ -126,7 +140,6 @@ function findLocalPlanInSettings(planCode) {
 module.exports = {
 module.exports = {
   ensurePlansAreSetupCorrectly,
   ensurePlansAreSetupCorrectly,
   findLocalPlanInSettings,
   findLocalPlanInSettings,
-  mapRecurlyPlanCodeToStripeLookupKey,
-  mapRecurlyAddOnCodeToStripeLookupKey,
+  buildStripeLookupKey,
   getPlanTypeAndPeriodFromRecurlyPlanCode,
   getPlanTypeAndPeriodFromRecurlyPlanCode,
 }
 }

+ 1 - 1
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -839,7 +839,7 @@ function makeChangePreview(
     paymentMethod: paymentMethod?.toString(),
     paymentMethod: paymentMethod?.toString(),
     netTerms: subscription.netTerms,
     netTerms: subscription.netTerms,
     nextPlan: {
     nextPlan: {
-      annual: nextPlan.annual ?? false,
+      annual: nextPlan?.annual ?? false,
     },
     },
     nextInvoice: {
     nextInvoice: {
       date: subscription.periodEnd.toISOString(),
       date: subscription.periodEnd.toISOString(),

+ 1 - 0
services/web/scripts/backfill_mixpanel_user_properties.mjs

@@ -66,6 +66,7 @@ async function _getGroupSubscriptionPlanCode(userId) {
     const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
     const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
     if (
     if (
       plan &&
       plan &&
+      plan.features &&
       FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures)
       FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures)
     ) {
     ) {
       bestPlanCode = plan.planCode
       bestPlanCode = plan.planCode

+ 67 - 36
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 () {
     it('should map "collaborator" plan code to stripe lookup keys', function () {
       const planCode = 'collaborator'
       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 () {
     it('should map "collaborator_free_trial_7_days" plan code to stripe lookup keys', function () {
       const planCode = 'collaborator_free_trial_7_days'
       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 () {
     it('should map "collaborator-annual" plan code to stripe lookup keys', function () {
       const planCode = 'collaborator-annual'
       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 () {
     it('should map "professional" plan code to stripe lookup keys', function () {
       const planCode = 'professional'
       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 () {
     it('should map "professional_free_trial_7_days" plan code to stripe lookup keys', function () {
       const planCode = 'professional_free_trial_7_days'
       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 () {
     it('should map "professional-annual" plan code to stripe lookup keys', function () {
       const planCode = 'professional-annual'
       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 () {
     it('should map "student" plan code to stripe lookup keys', function () {
       const planCode = 'student'
       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 () {
     it('shoult map "student_free_trial_7_days" plan code to stripe lookup keys', function () {
       const planCode = 'student_free_trial_7_days'
       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 () {
     it('should map "student-annual" plan code to stripe lookup keys', function () {
       const planCode = 'student-annual'
       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 () {
     it('should return null for unknown add-on codes', function () {
       const billingCycleInterval = 'month'
       const billingCycleInterval = 'month'
       const addOnCode = 'unknown_addon'
       const addOnCode = 'unknown_addon'
-      const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey(
+      const currency = 'gbp'
+      const lookupKey = this.PlansLocator.buildStripeLookupKey(
         addOnCode,
         addOnCode,
+        currency,
         billingCycleInterval
         billingCycleInterval
       )
       )
       expect(lookupKey).to.equal(null)
       expect(lookupKey).to.equal(null)
     })
     })
 
 
     it('should handle missing input', function () {
     it('should handle missing input', function () {
-      const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey(
+      const lookupKey = this.PlansLocator.buildStripeLookupKey(
         undefined,
         undefined,
         undefined
         undefined
       )
       )
@@ -137,21 +164,25 @@ describe('PlansLocator', function () {
     it('returns the key for a monthly AI assist add-on', function () {
     it('returns the key for a monthly AI assist add-on', function () {
       const billingCycleInterval = 'month'
       const billingCycleInterval = 'month'
       const addOnCode = this.AI_ADD_ON_CODE
       const addOnCode = this.AI_ADD_ON_CODE
-      const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey(
+      const currency = 'gbp'
+      const lookupKey = this.PlansLocator.buildStripeLookupKey(
         addOnCode,
         addOnCode,
+        currency,
         billingCycleInterval
         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 () {
     it('returns the key for an annual AI assist add-on', function () {
       const billingCycleInterval = 'year'
       const billingCycleInterval = 'year'
       const addOnCode = this.AI_ADD_ON_CODE
       const addOnCode = this.AI_ADD_ON_CODE
-      const lookupKey = this.PlansLocator.mapRecurlyAddOnCodeToStripeLookupKey(
+      const currency = 'gbp'
+      const lookupKey = this.PlansLocator.buildStripeLookupKey(
         addOnCode,
         addOnCode,
+        currency,
         billingCycleInterval
         billingCycleInterval
       )
       )
-      expect(lookupKey).to.equal('assistant_annual_may2025')
+      expect(lookupKey).to.equal('assistant_annual_jun2025_gbp')
     })
     })
   })
   })
 
 

+ 17 - 9
services/web/types/subscription/plan.ts

@@ -1,3 +1,5 @@
+import { StripeCurrencyCode } from './currency'
+
 type Features = {
 type Features = {
   collaborators: number
   collaborators: number
   compileGroup: string
   compileGroup: string
@@ -60,6 +62,7 @@ export type Plan = {
   name: string
   name: string
   planCode: string
   planCode: string
   price_in_cents: number
   price_in_cents: number
+  canUseFlexibleLicensing?: boolean
 }
 }
 
 
 export type PriceForDisplayData = {
 export type PriceForDisplayData = {
@@ -90,17 +93,22 @@ export type RecurlyPlanCode =
 
 
 export type RecurlyAddOnCode = 'assistant'
 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
   // TODO: change all group plans' lookup_keys to match the UK account after they have been added
   | 'group_standard_enterprise'
   | 'group_standard_enterprise'
   | 'group_professional_enterprise'
   | 'group_professional_enterprise'
   | 'group_standard_educational'
   | 'group_standard_educational'
   | 'group_professional_educational'
   | 'group_professional_educational'
-  | 'assistant_annual_may2025'
-  | 'assistant_may2025'
+
+export type StripeLookupKeyVersion = 'jun2025'
+
+export type StripeLookupKey =
+  `${StripeBaseLookupKey}_${StripeLookupKeyVersion}_${StripeCurrencyCode}`