overleaf/services/web/scripts/recurly/generate_recurly_prices.mjs

122 lines
3.5 KiB
JavaScript
Raw Normal View History

// script to generate plan prices for recurly from a csv file
//
// Usage:
//
// $ node scripts/recurly/generate_recurly_prices.mjs -f input.csv -o prices.json
//
// The input csv file has the following format:
//
// plan_code,USD,EUR,GBP,...
// student,9,8,7,...
// student-annual,89,79,69,...
// group_professional_2_educational,558,516,446,...
//
// The output file format is the JSON of the plans returned by recurly, with an
// extra _addOns property for the addOns associated with that plan.
//
// The output can be used as input for the upload script `recurly_prices.js`.
import minimist from 'minimist'
// https://github.com/import-js/eslint-plugin-import/issues/1810
// eslint-disable-next-line import/no-unresolved
import * as csv from 'csv/sync'
import _ from 'lodash'
import fs from 'node:fs'
const argv = minimist(process.argv.slice(2), {
string: ['output', 'file'],
alias: { o: 'output', f: 'file' },
default: { output: '/dev/stdout' },
})
// All currency codes are 3 uppercase letters
const CURRENCY_CODE_REGEX = /^[A-Z]{3}$/
// Group plans have a plan code of the form group_name_size_type, e.g.
const GROUP_SIZE_REGEX = /group_\w+_([0-9]+)_\w+/
// Only group plans with more than 4 users can have additional licenses
const SINGLE_LICENSE_MAX_GROUP_SIZE = 4
// Compute prices for the base plan
function computePrices(plan) {
const prices = _.pickBy(plan, (value, key) => CURRENCY_CODE_REGEX.test(key))
const result = []
for (const currency in prices) {
result.push({
currency,
setupFee: 0,
unitAmount: parseInt(prices[currency], 10),
})
}
return _.sortBy(result, 'currency')
}
// Handle prices for license add-ons associated with group plans
function isGroupPlan(plan) {
return plan.plan_code.startsWith('group_')
}
function getGroupSize(plan) {
// extract the group size from the plan code group_name_size_type using a regex
const match = plan.plan_code.match(GROUP_SIZE_REGEX)
if (!match) {
throw new Error(`cannot find group size in plan code: ${plan.plan_code}`)
}
const size = parseInt(match[1], 10)
return size
}
function computeAddOnPrices(prices, size) {
// The price of an additional license is the per-user cost of the base plan,
// i.e. the price of the plan divided by the group size of the plan
return prices.map(price => {
return {
currency: price.currency,
unitAmount: Math.round((100 * price.unitAmount) / size) / 100,
unitAmountDecimal: null,
}
})
}
// Convert the raw records into the output format
function transformRecordToPlan(record) {
const prices = computePrices(record)
// The base plan has no add-ons
const plan = {
code: record.plan_code,
currencies: prices,
}
// Large group plans have an add-on for additional licenses
if (isGroupPlan(record)) {
const size = getGroupSize(record)
if (size > SINGLE_LICENSE_MAX_GROUP_SIZE) {
const addOnPrices = computeAddOnPrices(prices, size)
plan._addOns = [
{
code: 'additional-license',
currencies: addOnPrices,
},
]
}
}
return plan
}
function generate(inputFile, outputFile) {
const input = fs.readFileSync(inputFile, 'utf8')
const rawRecords = csv.parse(input, { columns: true })
// transform the raw records into the output format
const plans = _.sortBy(rawRecords, 'plan_code').map(transformRecordToPlan)
const output = JSON.stringify(plans, null, 2)
fs.writeFileSync(outputFile, output)
}
if (argv.file) {
generate(argv.file, argv.output)
} else {
console.log('usage:\n' + ' --file input.csv -o file.json\n')
}