Browse Source

Merge pull request #10402 from overleaf/bg-sync-plan-prices-to-recurly

script to sync prices to recurly

GitOrigin-RevId: c6659f749136a9aeb2f90631470e144e9d1180a8
Eric Mc Sween 3 năm trước cách đây
mục cha
commit
8be8dd4cc0
1 tập tin đã thay đổi với 213 bổ sung0 xóa
  1. 213 0
      services/web/scripts/recurly/recurly_prices.js

+ 213 - 0
services/web/scripts/recurly/recurly_prices.js

@@ -0,0 +1,213 @@
+// script to sync plan prices to/from recurly
+//
+// Usage:
+//
+// Save current plan and addon prices to file
+// $ node scripts/recurly/sync_plan_prices_to_recurly.js --download -o prices.json
+//
+// Upload new plan and addon prices (change --dry-run to --commit to make the change)
+// $ node scripts/recurly/sync_plan_prices_to_recurly.js --upload -f prices.json --dry-run
+//
+// File format is JSON of the plans returned by recurly, with an extra _addOns property for the
+// addOns associated with that plan.
+//
+// The idea is to download the current prices to a file, update them locally (e.g. via a script)
+// and then upload them to recurly.
+
+const recurly = require('recurly')
+const Settings = require('@overleaf/settings')
+const minimist = require('minimist')
+const _ = require('lodash')
+const fs = require('fs')
+
+const recurlySettings = Settings.apis.recurly
+const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined
+
+const client = new recurly.Client(recurlyApiKey)
+
+async function getRecurlyPlans() {
+  const plans = client.listPlans({ params: { limit: 200, state: 'active' } })
+  const result = []
+  for await (const plan of plans.each()) {
+    plan._addOns = await getRecurlyPlanAddOns(plan) // store the addOns in a private property
+    if (VERBOSE) {
+      console.error('plan', plan.code, 'found', plan._addOns.length, 'addons')
+    }
+    result.push(plan)
+  }
+  return result
+}
+
+async function getRecurlyPlanAddOns(plan) {
+  // also store the addons for each plan
+  const addOns = await client.listPlanAddOns(plan.id, {
+    params: { limit: 200, state: 'active' },
+  })
+  const result = []
+  for await (const addOn of addOns.each()) {
+    result.push(addOn)
+  }
+  return result
+}
+
+async function download(outputFile) {
+  const plans = await getRecurlyPlans()
+  console.error('retrieved', plans.length, 'plans')
+  fs.writeFileSync(outputFile, JSON.stringify(plans, null, 2))
+}
+
+async function upload(inputFile) {
+  const localPlans = JSON.parse(fs.readFileSync(inputFile))
+  console.error('local plans', localPlans.length)
+  console.error('checking remote plans for consistency')
+  const remotePlans = await getRecurlyPlans() // includes addOns
+  // compare local with remote
+  console.error('remote plans', remotePlans.length)
+  const matching = _.intersectionBy(localPlans, remotePlans, 'code')
+  const localOnly = _.differenceBy(localPlans, remotePlans, 'code')
+  const remoteOnly = _.differenceBy(remotePlans, localPlans, 'code')
+  console.error(
+    'plan status:',
+    matching.length,
+    'matching,',
+    localOnly.length,
+    'local only,',
+    remoteOnly.length,
+    'remote only.'
+  )
+  if (localOnly.length > 0) {
+    const localOnlyPlanCodes = localOnly.map(p => p.code)
+    throw new Error(
+      `plans not found in Recurly: ${localOnlyPlanCodes.join(', ')}`
+    )
+  }
+  // update remote plan pricing with local version
+  for (const localPlan of localPlans) {
+    console.error(`=== ${localPlan.code} ===`)
+    await updatePlan(localPlan)
+    if (!localPlan._addOns?.length) {
+      console.error('no addons for this plan')
+      continue
+    }
+    for (const localPlanAddOn of localPlan._addOns) {
+      await updatePlanAddOn(localPlan, localPlanAddOn)
+    }
+    process.stderr.write('\n')
+  }
+}
+
+async function updatePlan(localPlan) {
+  const planCodeId = `code-${localPlan.code}`
+  const originalPlan = await client.getPlan(planCodeId)
+  const changes = _.differenceWith(
+    localPlan.currencies,
+    originalPlan.currencies,
+    (a, b) => _.isEqual(a, _.assign({}, b))
+  )
+  if (changes.length === 0) {
+    console.error('no changes to plan currencies')
+    return
+  } else {
+    console.error('changes', changes)
+  }
+  const planUpdate = { currencies: localPlan.currencies }
+  try {
+    if (DRY_RUN) {
+      console.error('skipping update to', planCodeId)
+      return
+    }
+    const newPlan = await client.updatePlan(planCodeId, planUpdate)
+    if (VERBOSE) {
+      console.error('new plan', newPlan)
+    }
+  } catch (err) {
+    console.error('failed to update', localPlan.code, 'error', err)
+  }
+}
+
+async function updatePlanAddOn(plan, localAddOn) {
+  if (localAddOn.code != null && localAddOn.code !== 'additional-license') {
+    return
+  }
+  const planCodeId = `code-${plan.code}`
+  const addOnId = 'code-additional-license'
+  const originalPlanAddOn = await client.getPlanAddOn(planCodeId, addOnId)
+  const changes = _.differenceWith(
+    localAddOn.currencies,
+    originalPlanAddOn.currencies,
+    (a, b) => _.isEqual(a, _.assign({}, b))
+  )
+  if (changes.length === 0) {
+    console.error('no changes to addon currencies')
+    return
+  } else {
+    console.error('changes', changes)
+  }
+  const planAddOnUpdate = { currencies: localAddOn.currencies }
+  try {
+    if (DRY_RUN) {
+      console.error('skipping update to additional licencse for', planCodeId)
+      return
+    }
+    const newPlanAddOn = await client.updatePlanAddOn(
+      planCodeId,
+      addOnId,
+      planAddOnUpdate
+    )
+    if (VERBOSE) {
+      console.error('new plan addon', newPlanAddOn)
+    }
+  } catch (err) {
+    console.error(
+      'failed to update plan addon',
+      plan.code,
+      '=>',
+      localAddOn.code
+    )
+  }
+}
+
+const argv = minimist(process.argv.slice(2), {
+  boolean: ['download', 'upload', 'dry-run', 'commit', 'verbose'],
+  string: ['output', 'file'],
+  alias: { o: 'output', f: 'file', v: 'verbose' },
+  default: { output: '/dev/stdout' },
+})
+
+const DRY_RUN = argv['dry-run']
+const COMMIT = argv.commit
+const VERBOSE = argv.verbose
+
+if (argv.download === argv.upload) {
+  console.error('specify one of --download or --upload')
+  process.exit(1)
+}
+
+if (argv.upload && DRY_RUN === COMMIT) {
+  console.error('specify one of --dry-run or --commit when uploading prices')
+  process.exit(1)
+}
+
+if (argv.download) {
+  download(argv.output)
+    .then(() => {
+      process.exit(0)
+    })
+    .catch(error => {
+      console.error({ error })
+      process.exit(1)
+    })
+} else if (argv.upload) {
+  upload(argv.file)
+    .then(() => {
+      process.exit(0)
+    })
+    .catch(error => {
+      console.error({ error })
+      process.exit(1)
+    })
+} else {
+  console.log(
+    'usage:\n' + '  --save -o file.json\n' + '  --load -f file.json\n'
+  )
+}