-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bf5fd3d
commit 8e57590
Showing
12 changed files
with
366 additions
and
44 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { priceDecorator } from "../../src/search/decorator" | ||
import { SearchProduct } from "@nosto/nosto-js/client" | ||
import { mockNostojs } from "@nosto/nosto-js/testing" | ||
|
||
mockNostojs({ | ||
internal: { | ||
getSettings: () => ({ currencySettings: {} }) | ||
} | ||
}) | ||
|
||
describe("priceDecorator", () => { | ||
const decorator = priceDecorator() | ||
|
||
it("should format prices and list prices", () => { | ||
const product = { | ||
price: 100, | ||
listPrice: 150, | ||
priceCurrencyCode: "USD", | ||
skus: [] | ||
} satisfies SearchProduct | ||
const result = decorator(product) | ||
expect(result.priceText).toBe("$100.00") | ||
expect(result.listPriceText).toBe("$150.00") | ||
}) | ||
|
||
it("should format prices for skus", () => { | ||
const product = { | ||
price: 100, | ||
listPrice: 150, | ||
priceCurrencyCode: "USD", | ||
skus: [ | ||
{ price: 50, listPrice: 75 }, | ||
{ price: 60, listPrice: 80 } | ||
] | ||
} satisfies SearchProduct | ||
const result = decorator(product) | ||
expect(result.skus?.[0].priceText).toBe("$50.00") | ||
expect(result.skus?.[0].listPriceText).toBe("$75.00") | ||
expect(result.skus?.[1].priceText).toBe("$60.00") | ||
expect(result.skus?.[1].listPriceText).toBe("$80.00") | ||
}) | ||
|
||
it("should return the original product if no prices are present", () => { | ||
const product = { | ||
priceCurrencyCode: "USD", | ||
skus: [] | ||
} satisfies SearchProduct | ||
const result = decorator(product) | ||
expect(result).toEqual(product) | ||
}) | ||
|
||
it("should handle products with undefined prices", () => { | ||
const product = { | ||
price: undefined, | ||
listPrice: undefined, | ||
priceCurrencyCode: "USD", | ||
skus: [] | ||
} satisfies SearchProduct | ||
const result = decorator(product) | ||
expect(result.priceText).toBeUndefined() | ||
expect(result.listPriceText).toBeUndefined() | ||
}) | ||
|
||
it("should use provided currency as fallback currency", () => { | ||
const product = { | ||
price: 100, | ||
listPrice: 150, | ||
skus: [] | ||
} satisfies SearchProduct | ||
const result = priceDecorator({ defaultCurrency: "EUR" })(product) | ||
expect(result.priceText).toBe("100,00 €") | ||
expect(result.listPriceText).toBe("150,00 €") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { getCurrencyFormatting } from "../../src/search/currencies" | ||
import { mockNostojs } from "@nosto/nosto-js/testing" | ||
|
||
const currencyFormatsMock = { | ||
GBP: { | ||
currencyBeforeAmount: true, | ||
currencyToken: "£", | ||
decimalCharacter: ".", | ||
groupingSeparator: "", | ||
decimalPlaces: 1 | ||
}, | ||
EUR: { | ||
currencyBeforeAmount: true, | ||
currencyToken: "€", | ||
decimalCharacter: ",", | ||
groupingSeparator: ".", | ||
decimalPlaces: 0 | ||
}, | ||
USD: { | ||
currencyBeforeAmount: false, | ||
currencyToken: "$", | ||
decimalCharacter: ".", | ||
groupingSeparator: ",", | ||
decimalPlaces: 2 | ||
}, | ||
INR: { | ||
currencyBeforeAmount: true, | ||
currencyToken: "₹", | ||
decimalCharacter: ".", | ||
groupingSeparator: ",", | ||
decimalPlaces: 2 | ||
} | ||
} | ||
|
||
describe("currency formatting", () => { | ||
it("should use currency formatting settings", () => { | ||
const { formatCurrency: format } = getCurrencyFormatting({ | ||
defaultCurrency: "GBP", | ||
currencySettings: currencyFormatsMock | ||
}) | ||
|
||
expect(format(12345.0)).toEqual("£12345.0") | ||
expect(format(123450.0)).toEqual("£123450.0") | ||
|
||
expect(format(12345.0, "EUR")).toEqual("€12.345") | ||
expect(format(123450.0, "EUR")).toEqual("€123.450") | ||
|
||
expect(format(12345.0, "USD")).toEqual("12,345.00$") | ||
expect(format(123450.0, "USD")).toEqual("123,450.00$") | ||
|
||
expect(format(12345.0, "INR")).toEqual("₹12,345.00") | ||
expect(format(123450.0, "INR")).toEqual("₹1,23,450.00") | ||
|
||
// uses fallback | ||
expect(format(12345.0, "AUD")).toEqual("$12,345.00") | ||
expect(format(123450.0, "AUD")).toEqual("$123,450.00") | ||
}) | ||
|
||
it("should have proper fallback behaviour", () => { | ||
const { formatCurrency: format } = getCurrencyFormatting({ | ||
defaultCurrency: "GBP", | ||
currencySettings: {} | ||
}) | ||
|
||
expect(format(12345.0)).toEqual("£12,345.00") | ||
expect(format(123450.0)).toEqual("£123,450.00") | ||
|
||
expect(format(12345.0, "EUR")).toEqual("12.345,00 €") | ||
expect(format(123450.0, "EUR")).toEqual("123.450,00 €") | ||
|
||
expect(format(12345.0, "USD")).toEqual("$12,345.00") | ||
expect(format(123450.0, "USD")).toEqual("$123,450.00") | ||
}) | ||
|
||
it("should load currency settings from nosto", async () => { | ||
const mockApi = { | ||
internal: { | ||
getSettings: jest.fn(() => ({ | ||
currencySettings: currencyFormatsMock | ||
})) | ||
} | ||
} | ||
mockNostojs(mockApi) | ||
|
||
getCurrencyFormatting() | ||
expect(mockApi.internal.getSettings).toHaveBeenCalledTimes(1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { CurrencyFormats } from "@nosto/nosto-js/client" | ||
import { nostojs } from "@nosto/nosto-js" | ||
|
||
const defaultConfig = { | ||
defaultCurrency: "EUR", | ||
defaultLocale: "en-US", | ||
/** @hidden */ | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
currencySettings: {} as CurrencyFormats | ||
} | ||
|
||
export type CurrencyConfig = typeof defaultConfig | ||
|
||
const currencyLocales: Record<string, string> = { | ||
EUR: "de-DE", | ||
GBP: "en-GB", | ||
USD: "en-US", | ||
AUD: "en-AU", | ||
CAD: "en-CA", | ||
//India, Afghanistan, Bangladesh, Bhutan, Myanmar, Nepal, and Pakistan uses lakhs and crores notation | ||
INR: "en-IN", | ||
AFN: "en-IN", | ||
BDT: "en-IN", | ||
BTN: "en-IN", | ||
MMK: "en-IN", | ||
NPR: "en-IN", | ||
PKR: "en-IN" | ||
} | ||
|
||
export function getCurrencyFormatting(overrides: Partial<CurrencyConfig> = {}) { | ||
const config = { | ||
...defaultConfig, | ||
...overrides | ||
} | ||
if (!overrides.currencySettings) { | ||
nostojs(api => { | ||
config.currencySettings = api.internal.getSettings().currencySettings | ||
}) | ||
} | ||
|
||
/** | ||
* Format the given monetary value using the Nosto currency settings. | ||
*/ | ||
function formatCurrency(value: number, currency?: string) { | ||
const { defaultCurrency, currencySettings, defaultLocale } = config | ||
const currencyCode = currency ?? defaultCurrency | ||
const locale = currencyLocales[currencyCode] ?? defaultLocale | ||
|
||
if (currencyCode in currencySettings) { | ||
// formatting using Nosto settings | ||
const settings = currencySettings[currencyCode]! | ||
const result = new Intl.NumberFormat(locale, { | ||
useGrouping: !!settings.groupingSeparator, | ||
minimumFractionDigits: settings.decimalPlaces, | ||
maximumFractionDigits: settings.decimalPlaces | ||
}).formatToParts(value) | ||
|
||
const normalised = result | ||
.map(it => { | ||
if (it.type === "group") return settings.groupingSeparator | ||
if (it.type === "decimal") return settings.decimalCharacter | ||
return it.value | ||
}) | ||
.join("") | ||
|
||
if (settings?.currencyBeforeAmount) { | ||
return `${settings.currencyToken}${normalised}` | ||
} | ||
return `${normalised}${settings?.currencyToken}` | ||
} | ||
|
||
// fallback logic | ||
const numberFormat = new Intl.NumberFormat(locale, { | ||
style: "currency", | ||
currency: currencyCode | ||
}) | ||
return numberFormat.format(value) | ||
} | ||
|
||
return { | ||
formatCurrency | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { SearchProduct, SearchProductSku } from "@nosto/nosto-js/client" | ||
import { CurrencyConfig, getCurrencyFormatting } from "./currencies" | ||
|
||
type Prices = Pick<SearchProduct, "price" | "listPrice"> | ||
|
||
type FormattedPrices = { | ||
priceText?: string | ||
listPriceText?: string | ||
} | ||
|
||
/** | ||
* Exposes currency formatting logic as a SearchProduct decorator | ||
*/ | ||
export function priceDecorator(config?: Partial<CurrencyConfig>) { | ||
const { formatCurrency } = getCurrencyFormatting(config) | ||
|
||
function formatPrices<T extends Prices>(obj: T, currency?: string) { | ||
const formatted: FormattedPrices = {} | ||
if (obj.price !== undefined) { | ||
formatted.priceText = formatCurrency(obj.price, currency) | ||
} | ||
if (obj.listPrice !== undefined) { | ||
formatted.listPriceText = formatCurrency(obj.listPrice, currency) | ||
} | ||
return Object.assign({}, obj, formatted) | ||
} | ||
|
||
function hasPrices(obj: Prices) { | ||
return obj.price !== undefined || obj.listPrice !== undefined | ||
} | ||
|
||
type Result = SearchProduct & | ||
FormattedPrices & { | ||
skus?: (SearchProductSku & FormattedPrices)[] | ||
} | ||
|
||
return function decorator(hit: SearchProduct): Result { | ||
if (hasPrices(hit)) { | ||
const copy = formatPrices(hit, hit.priceCurrencyCode) | ||
if (copy.skus && copy.skus.some(hasPrices)) { | ||
copy.skus = copy.skus.map(sku => { | ||
if (hasPrices(sku)) { | ||
return formatPrices(sku, hit.priceCurrencyCode) | ||
} | ||
return sku | ||
}) | ||
} | ||
return copy | ||
} | ||
return hit | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./currencies" | ||
export * from "./decorator" | ||
export * from "./search" |
Oops, something went wrong.