diff --git a/services/web/scripts/recurly/get_manually_billed_users_details.mjs b/services/web/scripts/recurly/get_manually_billed_users_details.mjs new file mode 100644 index 0000000000..dafec9f8d6 --- /dev/null +++ b/services/web/scripts/recurly/get_manually_billed_users_details.mjs @@ -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 Path to CSV file containing subscription_id, period_end, currency (can be exported from Recurly)' + ) + console.error(' --output, -o 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) +} diff --git a/services/web/scripts/recurly/update_terms_and_conditions_for_manually_billed_users.mjs b/services/web/scripts/recurly/update_terms_and_conditions_for_manually_billed_users.mjs new file mode 100644 index 0000000000..f47ee6a2f0 --- /dev/null +++ b/services/web/scripts/recurly/update_terms_and_conditions_for_manually_billed_users.mjs @@ -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 Path to CSV file containing subscription IDs (can be exported from Recurly)' + ) + console.error( + ' --termsAndConditions, -t 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) +}