Merge pull request #22340 from overleaf/mf-clean-up-currency-format-test

[web] Clean up localized currency format test (`local-ccy-format-v2`)

GitOrigin-RevId: 30d671479522b87ee9205994508b745d2b0ae4c3
This commit is contained in:
M Fahru
2025-01-14 09:05:49 +00:00
committed by Copybot
parent 640cc8fc79
commit 8f21f58ad5
27 changed files with 105 additions and 504 deletions
@@ -3,11 +3,8 @@ import _ from 'lodash'
import { SplitTestContext } from '../../frontend/js/shared/context/split-test-context'
export const splitTestsArgTypes = {
'local-ccy-format-v2': {
description: 'Use local currency formatting',
control: { type: 'radio' as const },
options: ['default', 'enabled'],
},
// to be able to use this utility, you need to add the argTypes for each split test in this object
// Check the original implementation for an example: https://github.com/overleaf/internal/pull/17809
}
export const withSplitTests = (
@@ -23,8 +23,7 @@ const SubscriptionHelper = require('./SubscriptionHelper')
const AuthorizationManager = require('../Authorization/AuthorizationManager')
const Modules = require('../../infrastructure/Modules')
const async = require('async')
const { formatCurrencyLocalized } = require('../../util/currency')
const SubscriptionFormatters = require('./SubscriptionFormatters')
const { formatCurrency } = require('../../util/currency')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const { URLSearchParams } = require('url')
const RecurlyClient = require('./RecurlyClient')
@@ -113,16 +112,6 @@ async function plansPage(req, res) {
const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } =
_plansBanners(countryCode)
const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'local-ccy-format-v2'
)
const formatCurrency =
localCcyAssignment.variant === 'enabled'
? formatCurrencyLocalized
: SubscriptionHelper.formatCurrencyDefault
const shouldLoadHotjar = await getShouldLoadHotjar(req, res)
res.render('subscriptions/plans', {
@@ -142,8 +131,7 @@ async function plansPage(req, res) {
initialLocalizedGroupPrice:
SubscriptionHelper.generateInitialLocalizedGroupPrice(
currency ?? 'USD',
language,
formatCurrency
language
),
showInrGeoBanner,
showBrlGeoBanner,
@@ -163,16 +151,6 @@ async function plansPageLightDesign(req, res) {
const plans = SubscriptionViewModelBuilder.buildPlansList()
const groupPlanModalDefaults = _getGroupPlanModalDefaults(req, currency)
const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'local-ccy-format-v2'
)
const formatCurrency =
localCcyAssignment.variant === 'enabled'
? formatCurrencyLocalized
: SubscriptionHelper.formatCurrencyDefault
const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } =
_plansBanners(countryCode)
@@ -197,8 +175,7 @@ async function plansPageLightDesign(req, res) {
initialLocalizedGroupPrice:
SubscriptionHelper.generateInitialLocalizedGroupPrice(
currency ?? 'USD',
language,
formatCurrency
language
),
showLATAMBanner,
showInrGeoBanner,
@@ -222,11 +199,6 @@ function formatGroupPlansDataForDash() {
async function userSubscriptionPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'local-ccy-format-v2'
)
await SplitTestHandler.promises.getAssignment(req, res, 'ai-add-on')
// Populates splitTestVariants with a value for the split test name and allows
@@ -241,10 +213,7 @@ async function userSubscriptionPage(req, res) {
const results =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user,
req.i18n.language,
localCcyAssignment.variant === 'enabled'
? SubscriptionFormatters.formatPriceLocalized
: SubscriptionFormatters.formatPriceDefault
req.i18n.language
)
const {
personalSubscription,
@@ -364,12 +333,6 @@ async function interstitialPaymentPage(req, res) {
const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } =
_plansBanners(countryCode)
const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'local-ccy-format-v2'
)
const shouldLoadHotjar = await getShouldLoadHotjar(req, res)
res.render(template, {
@@ -380,11 +343,8 @@ async function interstitialPaymentPage(req, res) {
recommendedCurrency,
interstitialPaymentConfig,
showSkipLink,
formatCurrency:
localCcyAssignment.variant === 'enabled'
? formatCurrencyLocalized
: SubscriptionHelper.formatCurrencyDefault,
showCurrencyAndPaymentMethods: localCcyAssignment.variant === 'enabled',
formatCurrency,
showCurrencyAndPaymentMethods: true, // TODO: remove hardcode
showInrGeoBanner,
showBrlGeoBanner,
showLATAMBanner,
@@ -399,18 +359,11 @@ async function interstitialPaymentPage(req, res) {
async function successfulSubscription(req, res) {
const user = SessionManager.getSessionUser(req.session)
const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'local-ccy-format-v2'
)
const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user,
req.i18n.language,
localCcyAssignment.variant === 'enabled'
? SubscriptionFormatters.formatPriceLocalized
: SubscriptionFormatters.formatPriceDefault
req.i18n.language
)
const postCheckoutRedirect = req.session?.postCheckoutRedirect
@@ -1,56 +1,5 @@
const dateformat = require('dateformat')
const { formatCurrencyLocalized } = require('../../util/currency')
/**
* @import { CurrencyCode } from '../../../../types/currency-code'
*/
const currencySymbols = {
EUR: '€',
USD: '$',
GBP: '£',
SEK: 'kr',
CAD: '$',
NOK: 'kr',
DKK: 'kr',
AUD: '$',
NZD: '$',
CHF: 'Fr',
SGD: '$',
INR: '₹',
BRL: 'R$',
MXN: '$',
COP: '$',
CLP: '$',
PEN: 'S/',
}
function formatPriceDefault(priceInCents, currency) {
if (!currency) {
currency = 'USD'
} else if (currency === 'CLP') {
// CLP doesn't have minor units, recurly stores the whole major unit without cents
return priceInCents.toLocaleString('es-CL', {
style: 'currency',
currency,
minimumFractionDigits: 0,
})
}
let string = String(Math.round(priceInCents))
if (string.length === 2) {
string = `0${string}`
}
if (string.length === 1) {
string = `00${string}`
}
if (string.length === 0) {
string = '000'
}
const cents = string.slice(-2)
const dollars = string.slice(0, -2)
const symbol = currencySymbols[currency]
return `${symbol}${dollars}.${cents}`
}
const { formatCurrency } = require('../../util/currency')
/**
* @param {number} priceInCents - price in the smallest currency unit (e.g. dollar cents, CLP units, ...)
@@ -65,7 +14,7 @@ function formatPriceLocalized(priceInCents, currency = 'USD', locale) {
? priceInCents
: priceInCents / 100
return formatCurrencyLocalized(priceInCurrencyUnit, currency, locale)
return formatCurrency(priceInCurrencyUnit, currency, locale)
}
function formatDateTime(date) {
@@ -83,7 +32,6 @@ function formatDate(date) {
}
module.exports = {
formatPriceDefault,
formatPriceLocalized,
formatDateTime,
formatDate,
@@ -1,3 +1,4 @@
const { formatCurrency } = require('../../util/currency')
const GroupPlansData = require('./GroupPlansData')
/**
@@ -9,20 +10,15 @@ function shouldPlanChangeAtTermEnd(oldPlan, newPlan) {
}
/**
* @import { CurrencyCode } from '../../../../types/currency-code'
* @import { CurrencyCode } from '../../../../types/subscription/currency'
*/
/**
* @param {CurrencyCode} recommendedCurrency
* @param {string} locale
* @param {(amount: number, currency: CurrencyCode, locale: string, stripIfInteger: boolean) => string} formatCurrency
* @returns {{ price: { collaborator: string, professional: string }, pricePerUser: { collaborator: string, professional: string } }} - localized group price
*/
function generateInitialLocalizedGroupPrice(
recommendedCurrency,
locale,
formatCurrency
) {
function generateInitialLocalizedGroupPrice(recommendedCurrency, locale) {
const INITIAL_LICENSE_SIZE = 2
// the price is in cents, so divide by 100 to get the value
@@ -56,106 +52,7 @@ function generateInitialLocalizedGroupPrice(
}
}
const currencies = {
USD: {
symbol: '$',
placement: 'before',
},
EUR: {
symbol: '€',
placement: 'before',
},
GBP: {
symbol: '£',
placement: 'before',
},
SEK: {
symbol: ' kr',
placement: 'after',
},
CAD: {
symbol: '$',
placement: 'before',
},
NOK: {
symbol: ' kr',
placement: 'after',
},
DKK: {
symbol: ' kr',
placement: 'after',
},
AUD: {
symbol: '$',
placement: 'before',
},
NZD: {
symbol: '$',
placement: 'before',
},
CHF: {
symbol: 'Fr ',
placement: 'before',
},
SGD: {
symbol: '$',
placement: 'before',
},
INR: {
symbol: '₹',
placement: 'before',
},
BRL: {
code: 'BRL',
locale: 'pt-BR',
symbol: 'R$ ',
placement: 'before',
},
MXN: {
code: 'MXN',
locale: 'es-MX',
symbol: '$ ',
placement: 'before',
},
COP: {
code: 'COP',
locale: 'es-CO',
symbol: '$ ',
placement: 'before',
},
CLP: {
code: 'CLP',
locale: 'es-CL',
symbol: '$ ',
placement: 'before',
},
PEN: {
code: 'PEN',
locale: 'es-PE',
symbol: 'S/ ',
placement: 'before',
},
}
function formatCurrencyDefault(amount, recommendedCurrency) {
const currency = currencies[recommendedCurrency]
// Test using toLocaleString to format currencies for new LATAM regions
if (currency.locale && currency.code) {
return amount.toLocaleString(currency.locale, {
style: 'currency',
currency: currency.code,
minimumFractionDigits: 0,
})
}
return currency.placement === 'before'
? `${currency.symbol}${amount}`
: `${amount}${currency.symbol}`
}
module.exports = {
formatCurrencyDefault,
shouldPlanChangeAtTermEnd,
generateInitialLocalizedGroupPrice,
}
@@ -69,11 +69,7 @@ async function getRedirectToHostedPage(userId, pageType) {
].join('')
}
async function buildUsersSubscriptionViewModel(
user,
locale = 'en',
formatPrice = SubscriptionFormatters.formatPriceDefault
) {
async function buildUsersSubscriptionViewModel(user, locale = 'en') {
let {
personalSubscription,
memberGroupSubscriptions,
@@ -313,19 +309,21 @@ async function buildUsersSubscriptionViewModel(
const pendingSubscriptionTax =
personalSubscription.recurly.taxRate *
recurlySubscription.pending_subscription.unit_amount_in_cents
personalSubscription.recurly.displayPrice = formatPrice(
recurlySubscription.pending_subscription.unit_amount_in_cents +
pendingAddOnPrice +
pendingAddOnTax +
pendingSubscriptionTax,
recurlySubscription.currency,
locale
)
personalSubscription.recurly.currentPlanDisplayPrice = formatPrice(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency,
locale
)
personalSubscription.recurly.displayPrice =
SubscriptionFormatters.formatPriceLocalized(
recurlySubscription.pending_subscription.unit_amount_in_cents +
pendingAddOnPrice +
pendingAddOnTax +
pendingSubscriptionTax,
recurlySubscription.currency,
locale
)
personalSubscription.recurly.currentPlanDisplayPrice =
SubscriptionFormatters.formatPriceLocalized(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency,
locale
)
const pendingTotalLicenses =
(pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
personalSubscription.recurly.pendingAdditionalLicenses =
@@ -333,11 +331,12 @@ async function buildUsersSubscriptionViewModel(
personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
personalSubscription.pendingPlan = pendingPlan
} else {
personalSubscription.recurly.displayPrice = formatPrice(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency,
locale
)
personalSubscription.recurly.displayPrice =
SubscriptionFormatters.formatPriceLocalized(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency,
locale
)
}
}
+3 -3
View File
@@ -3,7 +3,7 @@
*/
/**
* @import { CurrencyCode } from '../../../types/currency-code'
* @import { CurrencyCode } from '../../../types/subscription/currency'
*/
/**
@@ -13,7 +13,7 @@
* @param {boolean} stripIfInteger
* @returns {string}
*/
function formatCurrencyLocalized(amount, currency, locale, stripIfInteger) {
function formatCurrency(amount, currency, locale, stripIfInteger) {
const options = { style: 'currency', currency }
if (stripIfInteger && Number.isInteger(amount)) {
options.minimumFractionDigits = 0
@@ -34,5 +34,5 @@ function formatCurrencyLocalized(amount, currency, locale, stripIfInteger) {
}
module.exports = {
formatCurrencyLocalized,
formatCurrency,
}
@@ -10,7 +10,6 @@ block vars
block append meta
meta(name="ol-recommendedCurrency" content=recommendedCurrency)
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
meta(name="ol-currencySymbols" data-type="json" content=groupPlanModalOptions.currencySymbols)
meta(name="ol-itm_content" content=itm_content)
meta(name="ol-currentView" content=currentView)
meta(name="ol-countryCode" content=countryCode)
@@ -6,7 +6,6 @@ block entrypointVar
block append meta
meta(name="ol-recommendedCurrency" content=recommendedCurrency)
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
meta(name="ol-currencySymbols" data-type="json" content=groupPlanModalOptions.currencySymbols)
meta(name="ol-itm_content" content=itm_content)
meta(name="ol-currentView" content=currentView)
meta(name="ol-countryCode" content=countryCode)
@@ -1,6 +1,6 @@
import { Trans, useTranslation } from 'react-i18next'
import { Card, ListGroup } from 'react-bootstrap-5'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { formatCurrency } from '@/shared/utils/currency'
import { formatTime } from '@/features/utils/format-date'
import {
AddOnUpdate,
@@ -67,7 +67,7 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) {
{t('seats')}
</span>
<span data-testid="price">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.subtotal,
subscriptionChange.currency
)}
@@ -82,7 +82,7 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) {
{subscriptionChange.nextInvoice.tax.rate * 100}%
</span>
<span data-testid="price">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.tax,
subscriptionChange.currency
)}
@@ -94,7 +94,7 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) {
>
<strong className="me-auto">{t('total_due_today')}</strong>
<strong data-testid="price">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.total,
subscriptionChange.currency
)}
@@ -112,7 +112,7 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) {
{t(
'after_that_well_bill_you_x_annually_on_date_unless_you_cancel',
{
subtotal: formatCurrencyLocalized(
subtotal: formatCurrency(
subscriptionChange.nextInvoice.total,
subscriptionChange.currency
),
@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Card, Row, Col } from 'react-bootstrap-5'
import MaterialIcon from '@/shared/components/material-icon'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { formatCurrency } from '@/shared/utils/currency'
const LICENSE_ADD_ON = 'additional-license'
@@ -30,7 +30,7 @@ function UpgradeSubscriptionPlanDetails() {
<Col>
<span className="per-user-price" data-testid="per-user-price">
<b>
{formatCurrencyLocalized(
{formatCurrency(
licenseUnitPrice,
preview.currency,
getMeta('ol-i18n')?.currentLangCode ?? 'en',
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import { Card, ListGroup } from 'react-bootstrap-5'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { formatCurrency } from '@/shared/utils/currency'
import { formatTime } from '@/features/utils/format-date'
import {
GroupPlanUpgrade,
@@ -39,7 +39,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) {
{t('users')}
</span>
<span data-testid="subtotal">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.subtotal,
subscriptionChange.currency
)}
@@ -48,7 +48,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) {
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
<span className="me-auto">{t('sales_tax')}</span>
<span data-testid="tax">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.tax,
subscriptionChange.currency
)}
@@ -57,7 +57,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) {
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
<strong className="me-auto">{t('total_due_today')}</strong>
<strong data-testid="total">
{formatCurrencyLocalized(
{formatCurrency(
subscriptionChange.immediateCharge.total,
subscriptionChange.currency
)}
@@ -73,7 +73,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) {
</div>
<div>
{t('after_that_well_bill_you_x_annually_on_date_unless_you_cancel', {
subtotal: formatCurrencyLocalized(
subtotal: formatCurrency(
subscriptionChange.nextInvoice.subtotal,
subscriptionChange.currency
),
@@ -1,12 +1,7 @@
import getMeta from '../../../utils/meta'
import { swapModal } from '../../utils/swapModal'
import * as eventTracking from '../../../infrastructure/event-tracking'
import {
createLocalizedGroupPlanPrice,
formatCurrencyDefault,
} from '../utils/group-plan-pricing'
import { getSplitTestVariant } from '@/utils/splitTestUtils'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { createLocalizedGroupPlanPrice } from '../utils/group-plan-pricing'
export const GROUP_PLAN_MODAL_HASH = '#groups'
@@ -27,18 +22,12 @@ export function updateGroupModalPlanPricing() {
const modalEl = document.querySelector('[data-ol-group-plan-modal]')
const { planCode, size, currency, usage } = getFormValues()
const localCcyVariant = getSplitTestVariant('local-ccy-format-v2')
const { localizedPrice, localizedPerUserPrice } =
createLocalizedGroupPlanPrice({
plan: planCode,
licenseSize: size,
currency,
usage,
formatCurrency:
localCcyVariant === 'enabled'
? formatCurrencyLocalized
: formatCurrencyDefault,
})
modalEl.querySelectorAll('[data-ol-group-plan-plan-code]').forEach(el => {
@@ -1,7 +1,8 @@
import { formatCurrency } from '@/shared/utils/currency'
import getMeta from '../../../utils/meta'
/**
* @import { CurrencyCode } from '../../../../../types/currency-code'
* @import { CurrencyCode } from '../../../../../types/subscription/currency'
*/
// plan: 'collaborator' or 'professional'
@@ -13,7 +14,6 @@ import getMeta from '../../../utils/meta'
* @param {CurrencyCode} opts.currency
* @param {'enterprise' | 'educational'} opts.usage
* @param {string} [opts.locale]
* @param {(amount: number, currency: CurrencyCode, locale: string, includeSymbol: boolean) => string} opts.formatCurrency
* @returns {{localizedPrice: string, localizedPerUserPrice: string}}
*/
export function createLocalizedGroupPlanPrice({
@@ -22,7 +22,6 @@ export function createLocalizedGroupPlanPrice({
currency,
usage,
locale = getMeta('ol-i18n').currentLangCode || 'en',
formatCurrency,
}) {
const groupPlans = getMeta('ol-groupPlans')
const priceInCents =
@@ -42,44 +41,3 @@ export function createLocalizedGroupPlanPrice({
localizedPerUserPrice: formatPrice(perUserPrice),
}
}
const LOCALES = {
BRL: 'pt-BR',
MXN: 'es-MX',
COP: 'es-CO',
CLP: 'es-CL',
PEN: 'es-PE',
}
/**
* @param {number} amount
* @param {string} currency
*/
export function formatCurrencyDefault(amount, currency) {
const currencySymbols = getMeta('ol-currencySymbols')
const currencySymbol = currencySymbols[currency]
switch (currency) {
case 'BRL':
case 'MXN':
case 'COP':
case 'CLP':
case 'PEN':
// Test using toLocaleString to format currencies for new LATAM regions
return amount.toLocaleString(LOCALES[currency], {
style: 'currency',
currency,
minimumFractionDigits: Number.isInteger(amount) ? 0 : null,
})
case 'CHF':
return `${currencySymbol} ${amount}`
case 'DKK':
case 'SEK':
case 'NOK':
return `${amount} ${currencySymbol}`
default: {
return `${currencySymbol}${amount}`
}
}
}
@@ -8,7 +8,7 @@ import {
PremiumSubscriptionChange,
} from '../../../../../../types/subscription/subscription-change-preview'
import getMeta from '@/utils/meta'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { formatCurrency } from '@/shared/utils/currency'
import useAsync from '@/shared/hooks/use-async'
import { useLocation } from '@/shared/hooks/use-location'
import { debugConsole } from '@/utils/debugging'
@@ -101,7 +101,7 @@ function PreviewSubscriptionChange() {
<Col xs={9}>{changeName}</Col>
<Col xs={3} className="text-right">
<strong>
{formatCurrencyLocalized(
{formatCurrency(
preview.immediateCharge.subtotal,
preview.currency
)}
@@ -115,7 +115,7 @@ function PreviewSubscriptionChange() {
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
</Col>
<Col xs={3} className="text-right">
{formatCurrencyLocalized(
{formatCurrency(
preview.immediateCharge.tax,
preview.currency
)}
@@ -127,7 +127,7 @@ function PreviewSubscriptionChange() {
<Col xs={9}>{t('total_today')}</Col>
<Col xs={3} className="text-right">
<strong>
{formatCurrencyLocalized(
{formatCurrency(
preview.immediateCharge.total,
preview.currency
)}
@@ -169,7 +169,7 @@ function PreviewSubscriptionChange() {
<Row className="mt-1">
<Col xs={9}>{preview.nextInvoice.plan.name}</Col>
<Col xs={3} className="text-right">
{formatCurrencyLocalized(
{formatCurrency(
preview.nextInvoice.plan.amount,
preview.currency
)}
@@ -183,7 +183,7 @@ function PreviewSubscriptionChange() {
{addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''}
</Col>
<Col xs={3} className="text-right">
{formatCurrencyLocalized(addOn.amount, preview.currency)}
{formatCurrency(addOn.amount, preview.currency)}
</Col>
</Row>
))}
@@ -194,7 +194,7 @@ function PreviewSubscriptionChange() {
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
</Col>
<Col xs={3} className="text-right">
{formatCurrencyLocalized(
{formatCurrency(
preview.nextInvoice.tax.amount,
preview.currency
)}
@@ -209,10 +209,7 @@ function PreviewSubscriptionChange() {
: t('total_per_month')}
</Col>
<Col xs={3} className="text-right">
{formatCurrencyLocalized(
preview.nextInvoice.total,
preview.currency
)}
{formatCurrency(preview.nextInvoice.total, preview.currency)}
</Col>
</Row>
</div>
@@ -21,15 +21,13 @@ import {
import { Institution } from '../../../../../types/institution'
import getMeta from '../../../utils/meta'
import {
formatCurrencyDefault,
loadDisplayPriceWithTaxPromise,
loadGroupDisplayPriceWithTaxPromise,
} from '../util/recurly-pricing'
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
import { SubscriptionDashModalIds } from '../../../../../types/subscription/dashboard/modal-ids'
import { debugConsole } from '@/utils/debugging'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { formatCurrency } from '@/shared/utils/currency'
import { ManagedInstitution } from '../../../../../types/subscription/dashboard/managed-institution'
import { Publisher } from '../../../../../types/subscription/dashboard/publisher'
@@ -141,10 +139,6 @@ export function SubscriptionDashboardProvider({
memberGroupSubscriptions?.length > 0
)
const formatCurrency = useFeatureFlag('local-ccy-format-v2')
? formatCurrencyLocalized
: formatCurrencyDefault
useEffect(() => {
if (!isRecurlyLoaded()) {
setRecurlyLoadError(true)
@@ -167,8 +161,7 @@ export function SubscriptionDashboardProvider({
plan.planCode,
currency,
taxRate,
i18n.language,
formatCurrency
i18n.language
)
if (priceData?.totalAsNumber !== undefined) {
plan.displayPrice = formatCurrency(
@@ -186,12 +179,7 @@ export function SubscriptionDashboardProvider({
}
fetchPlansDisplayPrices().catch(debugConsole.error)
}
}, [
personalSubscription,
plansWithoutDisplayPrice,
i18n.language,
formatCurrency,
])
}, [personalSubscription, plansWithoutDisplayPrice, i18n.language])
useEffect(() => {
if (
@@ -214,8 +202,7 @@ export function SubscriptionDashboardProvider({
taxRate,
groupPlanToChangeToSize,
groupPlanToChangeToUsage,
i18n.language,
formatCurrency
i18n.language
)
} catch (e) {
debugConsole.error(e)
@@ -231,7 +218,6 @@ export function SubscriptionDashboardProvider({
groupPlanToChangeToSize,
personalSubscription,
groupPlanToChangeToCode,
formatCurrency,
i18n.language,
])
@@ -1,11 +1,9 @@
import { SubscriptionPricingState } from '@recurly/recurly-js'
import { PriceForDisplayData } from '../../../../../types/subscription/plan'
import {
currencies,
CurrencyCode,
} from '../../../../../types/subscription/currency'
import { CurrencyCode } from '../../../../../types/subscription/currency'
import { getRecurlyGroupPlanCode } from './recurly-group-plan-code'
import { debugConsole } from '@/utils/debugging'
import { formatCurrency } from '@/shared/utils/currency'
function queryRecurlyPlanPrice(planCode: string, currency: CurrencyCode) {
return new Promise(resolve => {
@@ -23,31 +21,11 @@ function queryRecurlyPlanPrice(planCode: string, currency: CurrencyCode) {
})
}
type FormatCurrency = (
price: number,
currency: CurrencyCode,
locale: string,
stripIfInteger?: boolean
) => string
export const formatCurrencyDefault: FormatCurrency = (
price: number,
currency: CurrencyCode,
_locale: string,
stripIfInteger = false
) => {
const currencySymbol = currencies[currency]
const number =
stripIfInteger && price % 1 === 0 ? Number(price) : price.toFixed(2)
return `${currencySymbol}${number}`
}
export function formatPriceForDisplayData(
price: string,
taxRate: number,
currencyCode: CurrencyCode,
locale: string,
formatCurrency: FormatCurrency
locale: string
): PriceForDisplayData {
const totalPriceExTax = parseFloat(price)
let taxAmount = totalPriceExTax * taxRate
@@ -69,8 +47,7 @@ function getPerUserDisplayPrice(
totalPrice: number,
currency: CurrencyCode,
size: string,
locale: string,
formatCurrency: FormatCurrency
locale: string
): string {
return formatCurrency(totalPrice / parseInt(size), currency, locale, true)
}
@@ -79,8 +56,7 @@ export async function loadDisplayPriceWithTaxPromise(
planCode: string,
currencyCode: CurrencyCode,
taxRate: number,
locale: string,
formatCurrency: FormatCurrency
locale: string
) {
if (!recurly) return
@@ -93,8 +69,7 @@ export async function loadDisplayPriceWithTaxPromise(
price.next.total,
taxRate,
currencyCode,
locale,
formatCurrency
locale
)
}
@@ -104,8 +79,7 @@ export async function loadGroupDisplayPriceWithTaxPromise(
taxRate: number,
size: string,
usage: string,
locale: string,
formatCurrency: FormatCurrency
locale: string
) {
if (!recurly) return
@@ -114,8 +88,7 @@ export async function loadGroupDisplayPriceWithTaxPromise(
planCode,
currencyCode,
taxRate,
locale,
formatCurrency
locale
)
if (price) {
@@ -123,8 +96,7 @@ export async function loadGroupDisplayPriceWithTaxPromise(
price.totalAsNumber,
currencyCode,
size,
locale,
formatCurrency
locale
)
}
@@ -1,12 +1,8 @@
import { updateGroupModalPlanPricing } from '../../../../features/plans/group-plan-modal'
import '../../../../features/plans/plans-v2-group-plan-modal'
import {
createLocalizedGroupPlanPrice,
formatCurrencyDefault,
} from '../../../../features/plans/utils/group-plan-pricing'
import getMeta from '../../../../utils/meta'
import { getSplitTestVariant } from '@/utils/splitTestUtils'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
import { updateGroupModalPlanPricing } from '../../../../features/plans/group-plan-modal'
import { createLocalizedGroupPlanPrice } from '../../../../features/plans/utils/group-plan-pricing'
const MINIMUM_LICENSE_SIZE_EDUCATIONAL_DISCOUNT = 10
@@ -26,11 +22,6 @@ export function updateMainGroupPlanPricing() {
? 'educational'
: 'enterprise'
const localCcyVariant = getSplitTestVariant('local-ccy-format-v2')
const formatCurrency =
localCcyVariant === 'enabled'
? formatCurrencyLocalized
: formatCurrencyDefault
const {
localizedPrice: localizedPriceProfessional,
localizedPerUserPrice: localizedPerUserPriceProfessional,
@@ -39,7 +30,6 @@ export function updateMainGroupPlanPricing() {
licenseSize,
currency,
usage,
formatCurrency,
})
const {
@@ -50,7 +40,6 @@ export function updateMainGroupPlanPricing() {
licenseSize,
currency,
usage,
formatCurrency,
})
document.querySelector(
@@ -13,10 +13,6 @@ export function sendPlansViewEvent() {
'group-tab-improvements'
)
const websiteRedesignPlansTestVariant = getMeta(
'ol-websiteRedesignPlansVariant'
)
const periodToggleTestVariant = getSplitTestVariant(
'period-toggle-improvements'
)
@@ -32,7 +28,6 @@ export function sendPlansViewEvent() {
currency,
countryCode,
device,
'website-redesign-plans': websiteRedesignPlansTestVariant,
'group-tab-improvements': groupTabImprovementsVariant,
plan: planTabParam,
'period-toggle-improvements': periodToggleTestVariant,
@@ -2,7 +2,7 @@ import getMeta from '@/utils/meta'
const DEFAULT_LOCALE = getMeta('ol-i18n')?.currentLangCode ?? 'en'
export function formatCurrencyLocalized(
export function formatCurrency(
amount: number,
currency: string,
locale: string = DEFAULT_LOCALE,
@@ -344,7 +344,7 @@ describe('<ChangePlanModal />', function () {
within(modal).getByText('Customize your group subscription')
within(modal).getByText('Save 30% or more')
within(modal).getByText('$1290 per year')
within(modal).getByText('$1,290 per year')
expect(within(modal).getAllByText('$129 per user').length).to.equal(2)
within(modal).getByText('Each user will have access to:')
@@ -432,9 +432,9 @@ describe('<ChangePlanModal />', function () {
within(modal).getByText('Total:', { exact: false })
expect(
within(modal).getAllByText('€1438.40', { exact: false }).length
within(modal).getAllByText('€1,438.40', { exact: false }).length
).to.equal(3)
within(modal).getByText('(€1160.00 + €278.40 tax) per year', {
within(modal).getByText('(€1,160.00 + €278.40 tax) per year', {
exact: false,
})
})
@@ -444,7 +444,7 @@ describe('<ChangePlanModal />', function () {
await openModal()
within(modal).getByText('$1290 per year')
within(modal).getByText('$1,290 per year')
within(modal).getAllByText('$129 per user')
// plan type (pro collab)
@@ -468,7 +468,7 @@ describe('<ChangePlanModal />', function () {
) as HTMLInputElement
expect(professionalPlanRadioInput.checked).to.be.true
await within(modal).findByText('$2590 per year')
await within(modal).findByText('$2,590 per year')
await within(modal).findAllByText('$259 per user')
// user count
@@ -478,7 +478,7 @@ describe('<ChangePlanModal />', function () {
sizeSelect = within(modal).getByRole('combobox') as HTMLInputElement
expect(sizeSelect.value).to.equal('5')
await within(modal).findByText('$1395 per year')
await within(modal).findByText('$1,395 per year')
await within(modal).findAllByText('$279 per user')
// usage (enterprise or educational)
@@ -493,12 +493,12 @@ describe('<ChangePlanModal />', function () {
expect(educationInput.checked).to.be.true
// make sure doesn't change price until back at min user to qualify
await within(modal).findByText('$1395 per year')
await within(modal).findByText('$1,395 per year')
await within(modal).findAllByText('$279 per user')
await userEvent.selectOptions(sizeSelect, [screen.getByText('10')])
await within(modal).findByText('$1550 per year')
await within(modal).findByText('$1,550 per year')
await within(modal).findAllByText('$155 per user')
})
@@ -1,16 +1,9 @@
import { expect } from 'chai'
import { formatPriceForDisplayData } from '../../../../../frontend/js/features/subscription/util/recurly-pricing'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
describe('formatPriceForDisplayData', function () {
it('should handle no tax rate', function () {
const data = formatPriceForDisplayData(
'1000',
0,
'USD',
'en',
formatCurrencyLocalized
)
const data = formatPriceForDisplayData('1000', 0, 'USD', 'en')
expect(data).to.deep.equal({
totalForDisplay: '$1,000',
totalAsNumber: 1000,
@@ -21,13 +14,7 @@ describe('formatPriceForDisplayData', function () {
})
it('should handle a tax rate', function () {
const data = formatPriceForDisplayData(
'380',
0.2,
'EUR',
'en',
formatCurrencyLocalized
)
const data = formatPriceForDisplayData('380', 0.2, 'EUR', 'en')
expect(data).to.deep.equal({
totalForDisplay: '€456',
totalAsNumber: 456,
@@ -38,13 +25,7 @@ describe('formatPriceForDisplayData', function () {
})
it('should handle total with cents', function () {
const data = formatPriceForDisplayData(
'8',
0.2,
'EUR',
'en',
formatCurrencyLocalized
)
const data = formatPriceForDisplayData('8', 0.2, 'EUR', 'en')
expect(data).to.deep.equal({
totalForDisplay: '€9.60',
totalAsNumber: 9.6,
@@ -1,6 +1,5 @@
import { expect } from 'chai'
import { createLocalizedGroupPlanPrice } from '../../../../frontend/js/features/plans/utils/group-plan-pricing'
import { formatCurrencyLocalized } from '@/shared/utils/currency'
describe('group-plan-pricing', function () {
beforeEach(function () {
@@ -25,11 +24,6 @@ describe('group-plan-pricing', function () {
},
},
})
window.metaAttributesCache.set('ol-currencySymbols', {
CHF: 'Fr',
DKK: 'kr',
USD: '$',
})
window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' })
})
@@ -41,7 +35,6 @@ describe('group-plan-pricing', function () {
currency: 'CHF',
licenseSize: '2',
usage: 'enterprise',
formatCurrency: formatCurrencyLocalized,
})
expect(localizedGroupPlanPrice).to.deep.equal({
@@ -57,7 +50,6 @@ describe('group-plan-pricing', function () {
currency: 'DKK',
licenseSize: '2',
usage: 'enterprise',
formatCurrency: formatCurrencyLocalized,
})
expect(localizedGroupPlanPrice).to.deep.equal({
@@ -73,7 +65,6 @@ describe('group-plan-pricing', function () {
currency: 'USD',
licenseSize: '2',
usage: 'enterprise',
formatCurrency: formatCurrencyLocalized,
})
expect(localizedGroupPlanPrice).to.deep.equal({
@@ -176,7 +176,7 @@ describe('SubscriptionController', function () {
},
'../../infrastructure/Features': this.Features,
'../../util/currency': (this.currency = {
formatCurrencyLocalized: sinon.stub(),
formatCurrency: sinon.stub(),
}),
},
})
@@ -380,26 +380,10 @@ describe('SubscriptionController', function () {
})
})
describe('localCcyAssignment', function () {
it('uses formatCurrencyLocalized when variant is enabled', function (done) {
this.SplitTestV2Hander.promises.getAssignment
.withArgs(this.req, this.res, 'local-ccy-format-v2')
.resolves({
variant: 'enabled',
})
describe('formatCurrency data', function () {
it('return correct formatCurrency function', function (done) {
this.res.render = (page, opts) => {
expect(opts.formatCurrency).to.equal(
this.currency.formatCurrencyLocalized
)
done()
}
this.SubscriptionController.plansPage(this.req, this.res)
})
it('uses formatCurrencyDefault when variant is default', function (done) {
this.res.render = (page, opts) => {
expect(opts.formatCurrency).to.equal(
this.SubscriptionHelper.formatCurrencyDefault
)
expect(opts.formatCurrency).to.equal(this.currency.formatCurrency)
done()
}
this.SubscriptionController.plansPage(this.req, this.res)
@@ -698,26 +682,10 @@ describe('SubscriptionController', function () {
})
})
describe('localCcyAssignment', function () {
it('uses formatCurrencyLocalized when variant is enabled', function (done) {
this.SplitTestV2Hander.promises.getAssignment
.withArgs(this.req, this.res, 'local-ccy-format-v2')
.resolves({
variant: 'enabled',
})
describe('formatCurrency data', function () {
it('return correct formatCurrency function', function (done) {
this.res.render = (page, opts) => {
expect(opts.formatCurrency).to.equal(
this.currency.formatCurrencyLocalized
)
done()
}
this.SubscriptionController.plansPageLightDesign(this.req, this.res)
})
it('uses formatCurrencyDefault when variant is default', function (done) {
this.res.render = (page, opts) => {
expect(opts.formatCurrency).to.equal(
this.SubscriptionHelper.formatCurrencyDefault
)
expect(opts.formatCurrency).to.equal(this.currency.formatCurrency)
done()
}
this.SubscriptionController.plansPageLightDesign(this.req, this.res)
@@ -1,6 +1,5 @@
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const { formatCurrencyLocalized } = require('../../../../app/src/util/currency')
const modulePath =
'../../../../app/src/Features/Subscription/SubscriptionHelper'
@@ -34,15 +33,7 @@ describe('SubscriptionHelper', function () {
beforeEach(function () {
this.INITIAL_LICENSE_SIZE = 2
this.settings = {
groupPlanModalOptions: {
currencySymbols: {
USD: '$',
CHF: 'Fr',
DKK: 'kr',
NOK: 'kr',
SEK: 'kr',
},
},
groupPlanModalOptions: {},
}
this.GroupPlansData = {
enterprise: {
@@ -154,8 +145,7 @@ describe('SubscriptionHelper', function () {
const localizedPrice =
this.SubscriptionHelper.generateInitialLocalizedGroupPrice(
'CHF',
'fr',
formatCurrencyLocalized
'fr'
)
expect(localizedPrice).to.deep.equal({
@@ -176,8 +166,7 @@ describe('SubscriptionHelper', function () {
const localizedPrice =
this.SubscriptionHelper.generateInitialLocalizedGroupPrice(
'DKK',
'da',
formatCurrencyLocalized
'da'
)
expect(localizedPrice).to.deep.equal({
@@ -198,8 +187,7 @@ describe('SubscriptionHelper', function () {
const localizedPrice =
this.SubscriptionHelper.generateInitialLocalizedGroupPrice(
'SEK',
'sv',
formatCurrencyLocalized
'sv'
)
expect(localizedPrice).to.deep.equal({
@@ -222,8 +210,7 @@ describe('SubscriptionHelper', function () {
'NOK',
// there seem to be possible inconsistencies with the CI
// maybe it depends on what languages are installed on the server?
'en',
formatCurrencyLocalized
'en'
)
expect(localizedPrice).to.deep.equal({
@@ -244,8 +231,7 @@ describe('SubscriptionHelper', function () {
const localizedPrice =
this.SubscriptionHelper.generateInitialLocalizedGroupPrice(
'USD',
'en',
formatCurrencyLocalized
'en'
)
expect(localizedPrice).to.deep.equal({
-1
View File
@@ -1 +0,0 @@
export type { CurrencyCode } from './subscription/currency'
@@ -20,4 +20,3 @@ export const currencies = <const>{
type Currency = typeof currencies
export type CurrencyCode = keyof Currency
export type CurrencySymbol = Currency[CurrencyCode]
@@ -2,7 +2,7 @@ import countries from '@/features/subscription/data/countries'
import { Plan } from './plan'
import { SubscriptionPricingStateTax } from 'recurly__recurly-js'
import { SubscriptionPricingInstanceCustom } from '../recurly/pricing/subscription'
import { currencies, CurrencyCode, CurrencySymbol } from './currency'
import { currencies, CurrencyCode } from './currency'
export type PricingFormState = {
first_name: string
@@ -23,7 +23,6 @@ export type PaymentContextValue = {
setCurrencyCode: React.Dispatch<
React.SetStateAction<PaymentContextValue['currencyCode']>
>
currencySymbol: CurrencySymbol
limitedCurrencies: Partial<typeof currencies>
pricingFormState: PricingFormState
setPricingFormState: React.Dispatch<