[web] update Stripe archive script to avoid archiving actively used prices (#29978)

* make Stripe export script only export active products and prices
* add a gitignore for the scripts/stripe/output

GitOrigin-RevId: 975d84077c3940d4f5af518e5f8292ea455e1c3a
This commit is contained in:
Kristina
2025-12-02 10:53:00 +01:00
committed by Copybot
parent 5982eed3fa
commit eafef60b75
3 changed files with 132 additions and 20 deletions

View File

@@ -0,0 +1 @@
output

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* This script archives or unarchives all prices with a matching version key in their lookup key
* This script marks as archived all prices with a matching version key in their lookup key
*
* Usage:
* node scripts/stripe/archive_prices_by_version_key.mjs --region us --version versionKey [options]
@@ -47,6 +47,56 @@ async function rateLimitSleep() {
return new Promise(resolve => setTimeout(resolve, 50))
}
/**
* Check if a price has active subscriptions (if an active subscription has
* archived prices, those customers will run into issues modifying their
* subscriptions)
*
* @param {Stripe} stripe
* @param {string} priceId
* @returns {Promise<boolean>}
*/
async function getHasActiveSubscriptions(stripe, priceId) {
const potentiallyActiveStatuses = [
'active',
'trialing',
'past_due',
'unpaid',
'paused',
'incomplete',
]
let hasMore = true
let startingAfter
while (hasMore) {
const params = {
price: priceId,
limit: 100,
}
if (startingAfter) {
params.starting_after = startingAfter
}
const subscriptions = await stripe.subscriptions.list(params)
await rateLimitSleep()
const hasActiveInBatch = subscriptions.data.some(subscription =>
potentiallyActiveStatuses.includes(subscription.status)
)
if (hasActiveInBatch) {
return true
}
hasMore = subscriptions.has_more
if (hasMore && subscriptions.data.length > 0) {
startingAfter = subscriptions.data[subscriptions.data.length - 1].id
}
}
return false
}
/**
* Fetch all prices matching the version key from Stripe
*
@@ -102,29 +152,77 @@ async function processPrices(prices, stripe, action, commit, trackProgress) {
const results = {
processed: 0,
skipped: 0,
hasSubscriptions: 0,
errored: 0,
}
// pre-filter prices already in the desired state to avoid unnecessary API calls
const pricesToProcess = []
for (const price of prices) {
const hasArchivedNickname = price.nickname?.includes('[ARCHIVED]')
const alreadyInDesiredState =
action === 'archive'
? hasArchivedNickname
: price.active && !hasArchivedNickname
if (alreadyInDesiredState) {
await trackProgress(
`Skipping price ${price.id} (${price.lookup_key}) - already ${price.active ? 'active' : 'archived'}`
)
results.skipped++
} else {
pricesToProcess.push(price)
}
}
if (pricesToProcess.length === 0) {
return results
}
await trackProgress(`Processing ${pricesToProcess.length} prices...`)
for (const price of pricesToProcess) {
try {
// Skip if already in the desired state
if (price.active === targetActiveStatus) {
await trackProgress(
`Skipping price ${price.id} (${price.lookup_key}) - already ${price.active ? 'active' : 'archived'}`
)
results.skipped++
continue
const hasActiveSubscriptions =
action === 'archive'
? await getHasActiveSubscriptions(stripe, price.id)
: false
if (hasActiveSubscriptions) {
results.hasSubscriptions++
}
if (commit) {
await stripe.prices.update(price.id, { active: targetActiveStatus })
await trackProgress(
`${action === 'archive' ? 'Archived' : 'Unarchived'} price: ${price.id} (${price.lookup_key})`
)
await rateLimitSleep()
const updateParams = {}
if (!hasActiveSubscriptions) {
updateParams.active = targetActiveStatus
}
if (action === 'archive' && !price.nickname?.includes('[ARCHIVED]')) {
updateParams.nickname = price.nickname
? `[ARCHIVED] ${price.nickname}`
: '[ARCHIVED]'
}
if (action === 'unarchive' && price.nickname?.includes('[ARCHIVED]')) {
updateParams.nickname = price.nickname.replace(/^\[ARCHIVED\]\s*/, '')
}
if (Object.keys(updateParams).length > 0) {
await stripe.prices.update(price.id, updateParams)
const statusNote = hasActiveSubscriptions
? '(nickname only - has active subscriptions)'
: ''
await trackProgress(
`${action === 'archive' ? 'Archived' : 'Unarchived'} price: ${price.id} (${price.lookup_key}) ${statusNote}`
)
await rateLimitSleep()
}
} else {
const statusNote = hasActiveSubscriptions
? '(nickname only - has active subscriptions)'
: ''
await trackProgress(
`[DRY RUN] Would ${action} price: ${price.id} (${price.lookup_key})`
`[DRY RUN] Would ${action} price: ${price.id} (${price.lookup_key}) ${statusNote}`
)
}
@@ -183,6 +281,9 @@ async function main(trackProgress) {
await trackProgress(
`Prices skipped (already in desired state): ${results.skipped}`
)
await trackProgress(
`Prices skipped (has active subscriptions): ${results.hasSubscriptions}`
)
await trackProgress(`Prices errored: ${results.errored}`)
if (results.errored > 0) {

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* This script exports all products and their prices from a Stripe environment to a JSON file
* This script exports active products and their active prices from a Stripe environment to a JSON file
*
* Usage:
* node scripts/stripe/export_products_from_environment.mjs --region us -o fileName [options]
@@ -21,6 +21,7 @@
import minimist from 'minimist'
import fs from 'node:fs'
import path from 'node:path'
import { z } from '../../app/src/infrastructure/Validation.mjs'
import { scriptRunner } from '../lib/ScriptRunner.mjs'
import { getRegionClient } from '../../modules/subscriptions/app/src/StripeClient.mjs'
@@ -93,15 +94,22 @@ function buildExportData(prices) {
const product = price.product
const productId = typeof product === 'string' ? product : product.id
// Store the product object if it's expanded
if (typeof product !== 'string' && !productMap.has(productId)) {
// Store the product object if it's expanded and active
if (
typeof product !== 'string' &&
product.active &&
!productMap.has(productId)
) {
productMap.set(productId, product)
}
if (!pricesByProduct.has(productId)) {
pricesByProduct.set(productId, [])
// Only include prices for active products
if (typeof product !== 'string' && product.active) {
if (!pricesByProduct.has(productId)) {
pricesByProduct.set(productId, [])
}
pricesByProduct.get(productId).push(price)
}
pricesByProduct.get(productId).push(price)
}
const products = Array.from(productMap.values())
@@ -141,6 +149,8 @@ async function main(trackProgress) {
const exportData = buildExportData(prices)
await trackProgress(`Writing to file: ${outputFile}`)
const outputDir = path.dirname(outputFile)
fs.mkdirSync(outputDir, { recursive: true })
fs.writeFileSync(outputFile, JSON.stringify(exportData, null, 2))
await trackProgress('EXPORT COMPLETE')