Merge pull request #24271 from overleaf/ls-script-to-update-manually-billed-users

Scripts to update terms and conditions for manually billed users

GitOrigin-RevId: 5efe43a42c3ed21779c9de698268817e2cbb5249
This commit is contained in:
Liangjun Song
2025-03-19 14:32:18 +01:00
committed by Copybot
parent 6932b3deb7
commit e44f892cb0
2 changed files with 206 additions and 0 deletions
@@ -0,0 +1,103 @@
import Settings from '@overleaf/settings'
import recurly from 'recurly'
import fs from 'node:fs'
import { setTimeout } from 'node:timers/promises'
import minimist from 'minimist'
import * as csv from 'csv'
import Stream from 'node:stream/promises'
const recurlyApiKey = Settings.apis.recurly.apiKey
if (!recurlyApiKey) {
throw new Error('Recurly API key is not set in the settings')
}
const client = new recurly.Client(recurlyApiKey)
function usage() {
console.error(
'Script to retrieve details of manually billed users from Recurly'
)
console.error('')
console.error('Usage:')
console.error(
' node scripts/recurly/get_manually_billed_users_details.mjs [options]'
)
console.error('')
console.error('Options:')
console.error(
' --input, -i <file> Path to CSV file containing subscription_id, period_end, currency (can be exported from Recurly)'
)
console.error(' --output, -o <file> Path to output CSV file')
console.error('')
console.error('Input format:')
console.error(
' CSV file with the following columns: subscription_id, period_end, currency (header row is skipped)'
)
}
function parseArgs() {
return minimist(process.argv.slice(2), {
alias: { i: 'input', o: 'output' },
string: ['input', 'output'],
})
}
async function enrichRow(row) {
const account = await client.getAccount(`code-${row.account_code}`)
return {
...row,
email: account.email,
first_name: account.firstName,
last_name: account.lastName,
cc_emails: account.ccEmails,
}
}
async function main() {
const { input: inputPath, output: outputPath, h, help } = parseArgs()
if (help || h || !inputPath || !outputPath) {
usage()
process.exit(0)
}
let processedCount = 0
await Stream.pipeline([
fs.createReadStream(inputPath),
csv.parse({ columns: true }),
async function* (rows) {
for await (const row of rows) {
try {
yield await enrichRow(row)
} catch (error) {
console.error(`Error processing subscription ${row.subscription_id}`)
}
processedCount++
if (processedCount % 1 === 0) {
console.log(`Processed ${processedCount} subscriptions`)
}
await setTimeout(1000)
}
},
csv.stringify({
header: true,
columns: {
subscription_id: 'subscription_id',
current_period_ends_at: 'period_end',
currency: 'currency',
email: 'email',
first_name: 'first_name',
last_name: 'last_name',
cc_emails: 'cc_emails',
},
}),
fs.createWriteStream(outputPath),
])
console.log(`Processed ${processedCount} subscriptions in total`)
}
try {
await main()
process.exit(0)
} catch (error) {
console.error(error)
process.exit(1)
}
@@ -0,0 +1,103 @@
import recurly from 'recurly'
import Settings from '@overleaf/settings'
import fs from 'node:fs'
import minimist from 'minimist'
import * as csv from 'csv'
import { setTimeout } from 'node:timers/promises'
const recurlyApiKey = Settings.apis.recurly.apiKey
if (!recurlyApiKey) {
throw new Error('Recurly API key is not set in the settings')
}
const client = new recurly.Client(recurlyApiKey)
function usage() {
console.error(
'Script to update terms and conditions for manually billed Recurly subscriptions'
)
console.error('')
console.error('Usage:')
console.error(
' node scripts/recurly/update_terms_and_conditions_for_manually_billed_users.mjs [options]'
)
console.error('')
console.error('Options:')
console.error(
' --input, -i <file> Path to CSV file containing subscription IDs (can be exported from Recurly)'
)
console.error(
' --termsAndConditions, -t <file> Path to text file containing terms and conditions'
)
console.error('')
console.error('Input format:')
console.error(
' - Subscription IDs CSV: First column contains subscription IDs (header row is skipped)'
)
console.error(
' - Terms and conditions: Plain text file with the terms and conditions content'
)
}
function parseArgs() {
return minimist(process.argv.slice(2), {
string: ['input', 'termsAndConditions'],
alias: {
i: 'input',
t: 'termsAndConditions',
},
})
}
async function updateTermsAndConditionsForSubscription(
subscriptionId,
termsAndConditions
) {
try {
await client.updateSubscription(`uuid-${subscriptionId}`, {
terms_and_conditions: termsAndConditions,
})
} catch (error) {
console.error(
`Error updating subscription ${subscriptionId}: ${error.message}`
)
}
}
async function main() {
const {
termsAndConditions: termsAndConditionsPath,
input: inputPath,
h,
help,
} = parseArgs()
if (help || h || !termsAndConditionsPath || !inputPath) {
usage()
process.exit(0)
}
const termsAndConditions = fs.readFileSync(termsAndConditionsPath, 'utf8')
const parser = csv.parse({ columns: true })
fs.createReadStream(inputPath).pipe(parser)
let processedCount = 0
for await (const row of parser) {
const subscriptionId = row.subscription_id
await updateTermsAndConditionsForSubscription(
subscriptionId,
termsAndConditions
)
processedCount++
if (processedCount % 10 === 0) {
console.log(`Processed ${processedCount} subscriptions`)
}
await setTimeout(1000)
}
console.log(`Processed ${processedCount} subscriptions in total`)
}
try {
await main()
process.exit(0)
} catch (error) {
console.error(error)
process.exit(1)
}