diff --git a/app/webpacker/js/services/price_parser.js b/app/webpacker/js/services/price_parser.js new file mode 100644 index 0000000000..873f665c9a --- /dev/null +++ b/app/webpacker/js/services/price_parser.js @@ -0,0 +1,45 @@ +export default class PriceParser { + parse(price) { + if (!price) { + return null; + } + + // used decimal and thousands separators from currency configuration + const decimalSeparator = I18n.toCurrency(0.1, { precision: 1, unit: "" }).substring(1, 2); + const thousandsSeparator = I18n.toCurrency(1000, { precision: 1, unit: "" }).substring(1, 2); + + // Replace comma used as a decimal separator and remplace by "." + price = this.replaceCommaByFinalPoint(price); + + // Remove configured thousands separator if it is actually a thousands separator + price = this.removeThousandsSeparator(price, thousandsSeparator); + + if (decimalSeparator === ",") { + price = price.replace(",", "."); + } + + price = parseFloat(price); + + if (isNaN(price)) { + return null; + } + + return price; + } + + replaceCommaByFinalPoint(price) { + if (price.match(/^[0-9]*(,{1})[0-9]{1,2}$/g)) { + return price.replace(",", "."); + } else { + return price; + } + } + + removeThousandsSeparator(price, thousandsSeparator) { + if (new RegExp(`^([0-9]*(${thousandsSeparator}{1})[0-9]{3}[0-9\.,]*)*$`, "g").test(price)) { + return price.replaceAll(thousandsSeparator, ""); + } else { + return price; + } + } +} diff --git a/app/webpacker/js/services/unit_prices.js b/app/webpacker/js/services/unit_prices.js new file mode 100644 index 0000000000..6344a2e073 --- /dev/null +++ b/app/webpacker/js/services/unit_prices.js @@ -0,0 +1,51 @@ +import PriceParser from "js/services/price_parser"; +import VariantUnitManager from "js/services/variant_unit_manager"; +import localizeCurrency from "js/services/localize_currency"; + +export default class UnitPrices { + constructor() { + this.variantUnitManager = new VariantUnitManager(); + this.priceParser = new PriceParser(); + } + + displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) { + price = this.priceParser.parse(price); + if (price && !isNaN(price) && unit_type && unit_value) { + const value = localizeCurrency( + this.price(price, scale, unit_type, unit_value, variant_unit_name), + ); + const unit = this.unit(scale, unit_type, variant_unit_name); + return `${value} / ${unit}`; + } + return null; + } + + price(price, scale, unit_type, unit_value) { + return price / this.denominator(scale, unit_type, unit_value); + } + + denominator(scale, unit_type, unit_value) { + const unit = this.unit(scale, unit_type); + if (unit === "lb") { + return unit_value / 453.6; + } else if (unit === "kg") { + return unit_value / 1000; + } else { + return unit_value; + } + } + + unit(scale, unit_type, variant_unit_name = "") { + if (variant_unit_name.length > 0 && unit_type === "items") { + return variant_unit_name; + } else if (unit_type === "items") { + return "item"; + } else if (this.variantUnitManager.systemOfMeasurement(scale, unit_type) === "imperial") { + return "lb"; + } else if (unit_type === "weight") { + return "kg"; + } else if (unit_type === "volume") { + return "L"; + } + } +} diff --git a/spec/javascripts/services/price_parser_test.js b/spec/javascripts/services/price_parser_test.js new file mode 100644 index 0000000000..b8d77948ca --- /dev/null +++ b/spec/javascripts/services/price_parser_test.js @@ -0,0 +1,150 @@ +/** + * @jest-environment jsdom + */ + +import PriceParse from "js/services/price_parser"; + +describe("PriceParser service", function () { + let priceParser = null; + + beforeEach(() => { + priceParser = new PriceParse(); + }); + + describe("test internal method with Regexp", function () { + describe("test replaceCommaByFinalPoint() method", function () { + it("handle the default case (with two numbers after comma)", function () { + expect(priceParser.replaceCommaByFinalPoint("1,00")).toEqual("1.00"); + }); + it("doesn't confuse with thousands separator", function () { + expect(priceParser.replaceCommaByFinalPoint("1,000")).toEqual("1,000"); + }); + it("handle also when there is only one number after the decimal separator", function () { + expect(priceParser.replaceCommaByFinalPoint("1,0")).toEqual("1.0"); + }); + }); + + describe("test removeThousandsSeparator() method", function () { + it("handle the default case", function () { + expect(priceParser.removeThousandsSeparator("1,000", ",")).toEqual("1000"); + expect(priceParser.removeThousandsSeparator("1,000,000", ",")).toEqual("1000000"); + }); + it("handle the case with decimal separator", function () { + expect(priceParser.removeThousandsSeparator("1,000,000.00", ",")).toEqual("1000000.00"); + }); + it("handle the case when it is actually a decimal separator (and not a thousands one)", function () { + expect(priceParser.removeThousandsSeparator("1,00", ",")).toEqual("1,00"); + }); + }); + }); + + describe("with point as decimal separator and comma as thousands separator for I18n service", function () { + beforeAll(() => { + const mockedToCurrency = jest.fn(); + mockedToCurrency.mockImplementation((arg) => { + if (arg == 0.1) { + return "0.1"; + } else if (arg == 1000) { + return "1,000"; + } + }); + + global.I18n = { toCurrency: mockedToCurrency }; + }); + // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) + afterAll(() => { + delete global.I18n; + }); + + it("handle point as decimal separator", function () { + expect(priceParser.parse("1.00")).toEqual(1.0); + }); + + it("handle point as decimal separator", function () { + expect(priceParser.parse("1.000")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("1,0")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("1,00")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("11,00")).toEqual(11.0); + }); + + it("handle comma as decimal separator but not confusing with thousands separator", function () { + expect(priceParser.parse("11,000")).toEqual(11000); + }); + + it("handle point as decimal separator and comma as thousands separator", function () { + expect(priceParser.parse("1,000,000.00")).toEqual(1000000); + }); + + it("handle integer number", function () { + expect(priceParser.parse("10")).toEqual(10); + }); + + it("handle integer number with comma as thousands separator", function () { + expect(priceParser.parse("1,000")).toEqual(1000); + }); + + it("handle integer number with no thousands separator", function () { + expect(priceParser.parse("1000")).toEqual(1000); + }); + }); + + describe("with comma as decimal separator and final point as thousands separator for I18n service", function () { + beforeAll(() => { + const mockedToCurrency = jest.fn(); + mockedToCurrency.mockImplementation((arg) => { + if (arg == 0.1) { + return "0,1"; + } else if (arg == 1000) { + return "1.000"; + } + }); + + global.I18n = { toCurrency: mockedToCurrency }; + }); + // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) + afterAll(() => { + delete global.I18n; + }); + + it("handle comma as decimal separator", function () { + expect(priceParser.parse("1,00")).toEqual(1.0); + }); + + it("handle comma as decimal separator with one digit after the comma", function () { + expect(priceParser.parse("11,0")).toEqual(11.0); + }); + + it("handle comma as decimal separator with two digit after the comma", function () { + expect(priceParser.parse("11,00")).toEqual(11.0); + }); + + it("handle comma as decimal separator with three digit after the comma", function () { + expect(priceParser.parse("11,000")).toEqual(11.0); + }); + + it("also handle point as decimal separator", function () { + expect(priceParser.parse("1.00")).toEqual(1.0); + }); + + it("also handle point as decimal separator with integer part with two digits", function () { + expect(priceParser.parse("11.00")).toEqual(11.0); + }); + + it("handle point as decimal separator and final point as thousands separator", function () { + expect(priceParser.parse("1.000.000,00")).toEqual(1000000); + }); + + it("handle integer number", function () { + expect(priceParser.parse("10")).toEqual(10); + }); + }); +}); diff --git a/spec/javascripts/services/unit_prices_test.js b/spec/javascripts/services/unit_prices_test.js new file mode 100644 index 0000000000..6df9eb791f --- /dev/null +++ b/spec/javascripts/services/unit_prices_test.js @@ -0,0 +1,170 @@ +/** + * @jest-environment jsdom + */ + +import UnitPrices from "js/services/unit_prices"; + +describe("UnitPrices service", function () { + let unitPrices = null; + + beforeAll(() => { + // Requires global var from page for VariantUnitManager + global.ofn_available_units_sorted = { + weight: { + "1.0": { name: "g", system: "metric" }, + 28.35: { name: "oz", system: "imperial" }, + 453.6: { name: "lb", system: "imperial" }, + "1000.0": { name: "kg", system: "metric" }, + "1000000.0": { name: "T", system: "metric" }, + }, + volume: { + 0.001: { name: "mL", system: "metric" }, + "1.0": { name: "L", system: "metric" }, + "1000.0": { name: "kL", system: "metric" }, + }, + }; + }); + + beforeEach(() => { + unitPrices = new UnitPrices(); + }); + + describe("get correct unit price duo unit/value for weight", function () { + const unit_type = "weight"; + + it("with scale: 1", function () { + const price = 1; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1000); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with scale and unit_value: 1000", function () { + const price = 1; + const scale = 1000; + const unit_value = 1000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with scale: 1000 and unit_value: 2000", function () { + const price = 1; + const scale = 1000; + const unit_value = 2000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.5); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2", function () { + const price = 2; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2000); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale and unit_value: 1000", function () { + const price = 2; + const scale = 1000; + const unit_value = 1000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale: 1000 and unit_value: 2000", function () { + const price = 2; + const scale = 1000; + const unit_value = 2000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale: 1000 and unit_value: 500", function () { + const price = 2; + const scale = 1000; + const unit_value = 500; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + }); + + describe("get correct unit price duo unit/value for volume", function () { + const unit_type = "volume"; + + it("with scale: 1", function () { + const price = 1; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2 and unit_value: 0.5", function () { + const price = 2; + const scale = 1; + const unit_value = 0.5; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2, scale: 0.001 and unit_value: 0.01", function () { + const price = 2; + const scale = 0.001; + const unit_value = 0.01; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(200); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 20000, scale: 1000 and unit_value: 10000", function () { + const price = 20000; + const scale = 1000; + const unit_value = 10000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2, scale: 1000 and unit_value: 10000 and variant_unit_name: box", function () { + const price = 20000; + const scale = 1000; + const unit_value = 10000; + const variant_unit_name = "Box"; + expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(2); + expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("L"); + }); + }); + + describe("get correct unit price duo unit/value for items", function () { + const unit_type = "items"; + const scale = null; + + it("with price: 1 and unit_value: 1", function () { + const price = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 1 and unit_value: 10", function () { + const price = 1; + const unit_value = 10; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.1); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 10 and unit_value: 1", function () { + const price = 10; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(10); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 10 and unit_value: 1 and variant_unit_name: box", function () { + const price = 10; + const unit_value = 1; + const variant_unit_name = "Box"; + expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(10); + expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("Box"); + }); + }); +});