mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
[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:
1
services/web/scripts/stripe/.gitignore
vendored
Normal file
1
services/web/scripts/stripe/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
output
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user