From 0cabc81d46bbbecfa6a1f2704534a93da9f48a4f Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:16:41 +0100 Subject: [PATCH] Merge pull request #31218 from overleaf/kh-update-assistant-prices * update Stripe AI assist prices * add soft archive option to the archiving prices script GitOrigin-RevId: 3f0b66cf227e31e03fb3337b3cb4c1b6a82bd1db --- .../Features/Subscription/PlansLocator.mjs | 2 +- .../stripe/archive_prices_by_version_key.mjs | 44 ++++++++++++------- .../src/Subscription/PlansLocator.test.mjs | 22 +++++----- services/web/types/subscription/plan.ts | 2 +- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/services/web/app/src/Features/Subscription/PlansLocator.mjs b/services/web/app/src/Features/Subscription/PlansLocator.mjs index 6bbfd9cb07..d2e45c033f 100644 --- a/services/web/app/src/Features/Subscription/PlansLocator.mjs +++ b/services/web/app/src/Features/Subscription/PlansLocator.mjs @@ -56,7 +56,7 @@ const recurlyCodeToStripeBaseLookupKey = { } // Keep in sync with StripeLookupKeyVersion in types/subscription/plan.ts -const LATEST_STRIPE_LOOKUP_KEY_VERSION = 'nov2025' +const LATEST_STRIPE_LOOKUP_KEY_VERSION = 'feb2026' /** * Build the Stripe lookup key, will be in this format: diff --git a/services/web/scripts/stripe/archive_prices_by_version_key.mjs b/services/web/scripts/stripe/archive_prices_by_version_key.mjs index 623f2dc661..e76257cc4a 100755 --- a/services/web/scripts/stripe/archive_prices_by_version_key.mjs +++ b/services/web/scripts/stripe/archive_prices_by_version_key.mjs @@ -10,7 +10,10 @@ * Options: * --region Required. Stripe region to process (us or uk) * --version Required. Version key to match in lookup keys (e.g., 'jul2025') - * --action Required. Action to perform: 'archive' or 'unarchive' + * --action Required. Action to perform: 'archive', 'soft-archive', or 'unarchive' + * - archive: Set prices as inactive (if not associated with any active subscriptions) and add [ARCHIVED] to nickname + * - soft-archive: Only add [ARCHIVED] to nickname, keep prices active + * - unarchive: Reactivate prices and remove [ARCHIVED] from nickname * --commit Actually perform the updates (default: dry-run mode) * * Examples: @@ -20,6 +23,9 @@ * # Commit archive prices with version 'jul2025' in UK region * node scripts/stripe/archive_prices_by_version_key.mjs --region uk --version jul2025 --action archive --commit * + * # Soft archive: only mark in nickname, keep prices active + * node scripts/stripe/archive_prices_by_version_key.mjs --region us --version jul2025 --action soft-archive --commit + * * # Unarchive prices with version 'jul2025' * node scripts/stripe/archive_prices_by_version_key.mjs --region us --version jul2025 --action unarchive --commit */ @@ -36,7 +42,7 @@ import { getRegionClient } from '../../modules/subscriptions/app/src/StripeClien const paramsSchema = z.object({ region: z.enum(['us', 'uk']), version: z.string(), - action: z.enum(['archive', 'unarchive']), + action: z.enum(['archive', 'soft-archive', 'unarchive']), commit: z.boolean().default(false), }) @@ -149,6 +155,8 @@ async function fetchPricesByVersion(stripe, version, trackProgress) { */ async function processPrices(prices, stripe, action, commit, trackProgress) { const targetActiveStatus = action === 'unarchive' + const isSoftArchive = action === 'soft-archive' + const isArchiving = action === 'archive' || action === 'soft-archive' const results = { processed: 0, skipped: 0, @@ -160,10 +168,9 @@ async function processPrices(prices, stripe, action, commit, trackProgress) { const pricesToProcess = [] for (const price of prices) { const hasArchivedNickname = price.nickname?.includes('[ARCHIVED]') - const alreadyInDesiredState = - action === 'archive' - ? hasArchivedNickname - : price.active && !hasArchivedNickname + const alreadyInDesiredState = isArchiving + ? hasArchivedNickname + : price.active && !hasArchivedNickname if (alreadyInDesiredState) { await trackProgress( @@ -194,11 +201,12 @@ async function processPrices(prices, stripe, action, commit, trackProgress) { if (commit) { const updateParams = {} - if (!hasActiveSubscriptions) { + // only update active status if not soft-archiving and price doesn't have active subscriptions + if (!isSoftArchive && !hasActiveSubscriptions) { updateParams.active = targetActiveStatus } - if (action === 'archive' && !price.nickname?.includes('[ARCHIVED]')) { + if (isArchiving && !price.nickname?.includes('[ARCHIVED]')) { updateParams.nickname = price.nickname ? `[ARCHIVED] ${price.nickname}` : '[ARCHIVED]' @@ -209,18 +217,24 @@ async function processPrices(prices, stripe, action, commit, trackProgress) { if (Object.keys(updateParams).length > 0) { await stripe.prices.update(price.id, updateParams) - const statusNote = hasActiveSubscriptions - ? '(nickname only - has active subscriptions)' - : '' + let statusNote = '' + if (hasActiveSubscriptions) { + statusNote = '(soft archived - has active subscriptions)' + } else if (isSoftArchive) { + statusNote = '(soft archived)' + } await trackProgress( - `${action === 'archive' ? 'Archived' : 'Unarchived'} price: ${price.id} (${price.lookup_key}) ${statusNote}` + `${isArchiving ? 'Archived' : 'Unarchived'} price: ${price.id} (${price.lookup_key}) ${statusNote}` ) await rateLimitSleep() } } else { - const statusNote = hasActiveSubscriptions - ? '(nickname only - has active subscriptions)' - : '' + let statusNote = '' + if (hasActiveSubscriptions) { + statusNote = '(soft archived - has active subscriptions)' + } else if (isSoftArchive) { + statusNote = '(soft archived)' + } await trackProgress( `[DRY RUN] Would ${action} price: ${price.id} (${price.lookup_key}) ${statusNote}` ) diff --git a/services/web/test/unit/src/Subscription/PlansLocator.test.mjs b/services/web/test/unit/src/Subscription/PlansLocator.test.mjs index bb3bf06d6a..7dc282cfbd 100644 --- a/services/web/test/unit/src/Subscription/PlansLocator.test.mjs +++ b/services/web/test/unit/src/Subscription/PlansLocator.test.mjs @@ -57,7 +57,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('standard_monthly_nov2025_eur') + expect(lookupKey).to.equal('standard_monthly_feb2026_eur') }) it('should map "collaborator_free_trial_7_days" plan code to stripe lookup keys', function (ctx) { @@ -67,7 +67,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('standard_monthly_nov2025_eur') + expect(lookupKey).to.equal('standard_monthly_feb2026_eur') }) it('should map "collaborator-annual" plan code to stripe lookup keys', function (ctx) { @@ -77,7 +77,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('standard_annual_nov2025_eur') + expect(lookupKey).to.equal('standard_annual_feb2026_eur') }) it('should map "professional" plan code to stripe lookup keys', function (ctx) { @@ -87,7 +87,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('professional_monthly_nov2025_eur') + expect(lookupKey).to.equal('professional_monthly_feb2026_eur') }) it('should map "professional_free_trial_7_days" plan code to stripe lookup keys', function (ctx) { @@ -97,7 +97,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('professional_monthly_nov2025_eur') + expect(lookupKey).to.equal('professional_monthly_feb2026_eur') }) it('should map "professional-annual" plan code to stripe lookup keys', function (ctx) { @@ -107,7 +107,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('professional_annual_nov2025_eur') + expect(lookupKey).to.equal('professional_annual_feb2026_eur') }) it('should map "student" plan code to stripe lookup keys', function (ctx) { @@ -117,7 +117,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('student_monthly_nov2025_eur') + expect(lookupKey).to.equal('student_monthly_feb2026_eur') }) it('shoult map "student_free_trial_7_days" plan code to stripe lookup keys', function (ctx) { @@ -127,7 +127,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('student_monthly_nov2025_eur') + expect(lookupKey).to.equal('student_monthly_feb2026_eur') }) it('should map "student-annual" plan code to stripe lookup keys', function (ctx) { @@ -137,7 +137,7 @@ describe('PlansLocator', function () { planCode, currency ) - expect(lookupKey).to.equal('student_annual_nov2025_eur') + expect(lookupKey).to.equal('student_annual_feb2026_eur') }) it('should return null for unknown add-on codes', function (ctx) { @@ -169,7 +169,7 @@ describe('PlansLocator', function () { currency, billingCycleInterval ) - expect(lookupKey).to.equal('assistant_monthly_nov2025_gbp') + expect(lookupKey).to.equal('assistant_monthly_feb2026_gbp') }) it('returns the key for an annual AI assist add-on', function (ctx) { @@ -181,7 +181,7 @@ describe('PlansLocator', function () { currency, billingCycleInterval ) - expect(lookupKey).to.equal('assistant_annual_nov2025_gbp') + expect(lookupKey).to.equal('assistant_annual_feb2026_gbp') }) }) diff --git a/services/web/types/subscription/plan.ts b/services/web/types/subscription/plan.ts index 6180371a71..6b632244ae 100644 --- a/services/web/types/subscription/plan.ts +++ b/services/web/types/subscription/plan.ts @@ -109,7 +109,7 @@ export type StripeBaseLookupKey = | 'group_professional_educational' // Keep in sync with LATEST_STRIPE_LOOKUP_KEY_VERSION in PlansLocator.mjs -export type StripeLookupKeyVersion = 'nov2025' +export type StripeLookupKeyVersion = 'feb2026' export type StripeLookupKey = `${StripeBaseLookupKey}_${StripeLookupKeyVersion}_${StripeCurrencyCode}`