overleaf/services/web/scripts/plan-prices/plans.js
Antoine Clausse b2ef7a935f [web] Use localized number formatting for currencies (#17622)
* Add a unit test on `SubscriptionFormatters.formatPrice`

* Add JSDoc to `formatPrice`

Also: Name the functions before exporting:
This fixes my IDE (WebStorm) navigation

* Make `'USD'` the default param instead of reassigning

* Create `formatCurrency` function

* Use `formatCurrency` in SubscriptionFormatters

* Use an `isNoCentsCurrency` logic for `CLP` `JPY` `KRW` `VND`

And remove custom `CLP` logic and locale

* Add `locale` param to `formatPrice`

* Generate `groups.json` and `localizedPlanPricing.json`

```
bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json
bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir
```

* Update scripts/plan-prices/plans.js to generate numbers instead of localized amounts

* Generate `groups.json` and `localizedPlanPricing.json`

```
bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json
bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir
```

* Remove generation of `plans.json`

As /services/web/frontend/js/main/plans.js was removed in https://github.com/overleaf/internal/pull/12593

* Sort currencies in alphabetical order in scripts/plan-prices/plans.js

* Generate `groups.json` and `localizedPlanPricing.json`

```
bin/exec web node ./scripts/recurly/recurly_prices.js --download -o prices.json
bin/exec web node ./scripts/plan-prices/plans.js -f ../../prices.json -o dir
```

* Use `formatCurrency` in price-summary.tsx

* Use `formatCurrency` in Subscription Pug files

* Fix unit tests SubscriptionHelperTests.js

* Remove unused `currencySymbol`

* Change to `formatCurrency` in other React components

* Add `CurrencyCode` JSDoc types

* Duplicate `formatCurrency` into services/web/app/src/util

* Wrap tests in a top-level describe block

* Use `narrowSymbol`

* Fix tests with `narrowSymbol` expects

* Revert deletion of old `formatPrice` in SubscriptionFormatters.js

* Rename `formatCurrency` -> `formatCurrencyLocalized`

* Revert deletion of `CurrencySymbol`

* Add split-test in SubscriptionController.js

* Add split-test in SubscriptionViewModelBuilder.js

* Add split-test in plans

* Add split-test in subscription-dashboard-context.tsx

* Add split-test in 4 more components

* Update tests

* Show currency and payment methods in interstitial page

* Fix `–` being printed. Use `–` instead

* Fix test with NOK

* Storybook: Fix missing `SplitTestProvider`

* Storybook: Revert "Remove unused `currencySymbol`"

This reverts commit e55387d4753f97bbf8e39e0fdc3ad17312122aaa.

* Replace `getSplitTestVariant` by `useSplitTestContext`

* Use parameterize currencyFormat in `generateInitialLocalizedGroupPrice`

* Fixup import paths of `formatCurrencyLocalized`

* Replace `% 1 === 0` by `Number.isInteger`

* Add comment explaining that any combinations of languages/currencies could happen

* Fixup after rebase: import `useSplitTestContext`

* Revert "Remove SplitTestProvider from subscription root"

This reverts commit be9f378fda715b86589ab0759737581c72321d87.

* Revert "Remove split test provider from some tests"

This reverts commit 985522932b550cfd38fa6a4f4c3d2ebaee6ff7df.

GitOrigin-RevId: 59a83cbbe0f7cc7e45f189c654e23fcf9bfa37af
2024-04-19 08:03:54 +00:00

194 lines
5.1 KiB
JavaScript

// Creates data for localizedPlanPricing object in settings.overrides.saas.js
// and plans object in main/plans.js
const csv = require('csv/sync')
const fs = require('fs')
const path = require('path')
const minimist = require('minimist')
function readCSVFile(fileName) {
// Pick the csv file
const filePath = path.resolve(__dirname, fileName)
const input = fs.readFileSync(filePath, 'utf8')
const rawRecords = csv.parse(input, { columns: true })
return rawRecords
}
function readJSONFile(fileName) {
const filePath = path.resolve(__dirname, fileName)
const file = fs.readFileSync(filePath)
const plans = JSON.parse(file)
// convert the plans JSON from recurly to an array of
// objects matching the spreadsheet format
const result = []
for (const plan of plans) {
const newRow = { plan_code: plan.code }
for (const price of plan.currencies) {
newRow[price.currency] = price.unitAmount
}
result.push(newRow)
}
return result
}
// Mapping of [output_keys]:[actual_keys]
const plansMap = {
student: 'student',
personal: 'paid-personal',
collaborator: 'collaborator',
professional: 'professional',
}
const currencies = [
'AUD',
'BRL',
'CAD',
'CHF',
'CLP',
'COP',
'DKK',
'EUR',
'GBP',
'INR',
'MXN',
'NOK',
'NZD',
'PEN',
'SEK',
'SGD',
'USD',
]
function generatePlans(workSheetJSON) {
// localizedPlanPricing object for settings.overrides.saas.js
const localizedPlanPricing = {}
// plans object for main/plans.js
for (const currency of currencies) {
localizedPlanPricing[currency] = {
free: {
monthly: 0,
annual: 0,
},
}
for (const [outputKey, actualKey] of Object.entries(plansMap)) {
const monthlyPlan = workSheetJSON.find(
data => data.plan_code === actualKey
)
if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
if (!(currency in monthlyPlan))
throw new Error(
`Missing currency "${currency}" for plan "${actualKey}"`
)
const actualKeyAnnual = `${actualKey}-annual`
const annualPlan = workSheetJSON.find(
data => data.plan_code === actualKeyAnnual
)
if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
if (!(currency in annualPlan))
throw new Error(
`Missing currency "${currency}" for plan "${actualKeyAnnual}"`
)
const monthly = Number(monthlyPlan[currency])
const monthlyTimesTwelve = Number(monthlyPlan[currency] * 12)
const annual = Number(annualPlan[currency])
localizedPlanPricing[currency] = {
...localizedPlanPricing[currency],
[outputKey]: { monthly, monthlyTimesTwelve, annual },
}
}
}
return { localizedPlanPricing }
}
function generateGroupPlans(workSheetJSON) {
const groupPlans = workSheetJSON.filter(data =>
data.plan_code.startsWith('group')
)
const sizes = ['2', '3', '4', '5', '10', '20', '50']
const result = {}
for (const type1 of ['educational', 'enterprise']) {
result[type1] = {}
for (const type2 of ['professional', 'collaborator']) {
result[type1][type2] = {}
for (const currency of currencies) {
result[type1][type2][currency] = {}
for (const size of sizes) {
const planCode = `group_${type2}_${size}_${type1}`
const plan = groupPlans.find(data => data.plan_code === planCode)
if (!plan) throw new Error(`Missing plan: ${planCode}`)
result[type1][type2][currency][size] = {
price_in_cents: plan[currency] * 100,
}
}
}
}
}
return result
}
const argv = minimist(process.argv.slice(2), {
string: ['output', 'file'],
alias: { o: 'output', f: 'file' },
})
let input
if (argv.file) {
const ext = path.extname(argv.file)
switch (ext) {
case '.csv':
input = readCSVFile(argv.file)
break
case '.json':
input = readJSONFile(argv.file)
break
default:
console.log('Invalid file type: must be csv or json')
}
} else {
console.log('usage: node plans.js -f <file.csv|file.json> -o <dir>')
process.exit(1)
}
// removes quotes from object keys
const formatJS = obj =>
JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
const formatJSON = obj => JSON.stringify(obj, null, 2)
function writeFile(outputFile, data) {
console.log(`Writing ${outputFile}`)
fs.writeFileSync(outputFile, data)
}
const { localizedPlanPricing } = generatePlans(input)
const groupPlans = generateGroupPlans(input)
if (argv.output) {
const dir = argv.output
// check if output directory exists
if (!fs.existsSync(dir)) {
console.log(`Creating output directory ${dir}`)
fs.mkdirSync(dir)
}
// check if output directory is a directory and report error if not
if (!fs.lstatSync(dir).isDirectory()) {
console.error(`Error: output dir ${dir} is not a directory`)
process.exit(1)
}
writeFile(`${dir}/localizedPlanPricing.json`, formatJS(localizedPlanPricing))
writeFile(`${dir}/groups.json`, formatJSON(groupPlans))
} else {
console.log('LOCALIZED', localizedPlanPricing)
console.log('GROUP PLANS', JSON.stringify(groupPlans, null, 2))
}
console.log('Completed!')