-
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.
Merge pull request #138 from Nosto/hit-decorators
Add formatting hit decorators
- Loading branch information
Showing
13 changed files
with
426 additions
and
15 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-js/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-js/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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { mockNostojs } from "@nosto/nosto-js/testing" | ||
import { search, HitDecorator } from "../../src/search-js" | ||
import { | ||
SearchQuery, | ||
SearchProduct, | ||
} from "@nosto/nosto-js/client" | ||
|
||
describe("search", () => { | ||
beforeEach(() => { | ||
mockNostojs({ | ||
search: jest.fn().mockResolvedValue({ | ||
products: { | ||
hits: [ | ||
{ id: "1", name: "Product 1" }, | ||
{ id: "2", name: "Product 2" }, | ||
], | ||
}, | ||
}), | ||
}) | ||
}) | ||
|
||
const query: SearchQuery = { query: "test" } | ||
|
||
it("should perform a search without decorators", async () => { | ||
const result = await search(query) | ||
expect(result.products?.hits).toEqual([ | ||
{ id: "1", name: "Product 1" }, | ||
{ id: "2", name: "Product 2" }, | ||
]) | ||
}) | ||
|
||
it("should apply hit decorators to the search results", async () => { | ||
const decorator: HitDecorator = (hit: SearchProduct) => ({ | ||
...hit, | ||
name: `${hit.name} Decorated`, | ||
}) | ||
const result = await search(query, { hitDecorators: [decorator] }) | ||
expect(result.products?.hits).toEqual([ | ||
{ id: "1", name: "Product 1 Decorated" }, | ||
{ id: "2", name: "Product 2 Decorated" }, | ||
]) | ||
}) | ||
|
||
it("should handle empty search results", async () => { | ||
mockNostojs({ | ||
search: jest.fn().mockResolvedValue({ products: { hits: [] } }), | ||
}) | ||
const result = await search(query) | ||
expect(result.products?.hits).toEqual([]) | ||
}) | ||
}) |
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 | ||
} | ||
} |
Oops, something went wrong.