diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml
index fe460c1df..aef0d1d48 100644
--- a/enrollment-server/pom.xml
+++ b/enrollment-server/pom.xml
@@ -145,6 +145,11 @@
swagger-annotations
+
+ org.javamoney.moneta
+ moneta-core
+
+
io.netty
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
index 6b7201e63..3f4a7f7a5 100644
--- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
@@ -31,15 +31,13 @@
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import static java.util.stream.Collectors.toMap;
@@ -228,15 +226,22 @@ private static Optional buildAmountAttribute(final OperationTemplateP
if (amount.isEmpty()) {
return Optional.empty();
}
- final String currency = fetchTemplateParamValue(templateParam, params, "currency").orElse(null);
+ final Optional currency = fetchTemplateParamValue(templateParam, params, "currency");
+ if (currency.isEmpty()) {
+ return Optional.empty();
+ }
+ final BigDecimal amountRaw;
try {
- final BigDecimal amountValue = new BigDecimal(amount.get());
- // TODO (racansky, 2023-02-07, #657) implement formatting based on locale
- return Optional.of(new AmountAttribute(id, text, amountValue, currency, amount.get(), currency));
+ amountRaw = new BigDecimal(amount.get());
} catch (NumberFormatException ex) {
logger.warn("Invalid number format: {}, skipping the AMOUNT attribute!", amount);
return Optional.empty();
}
+ final Locale locale = LocaleContextHolder.getLocale();
+ final String currencyRaw = currency.get();
+ final String currencyFormatted = MonetaryConverter.formatCurrency(currencyRaw, locale);
+ final String amountFormatted = MonetaryConverter.formatAmount(amountRaw, currencyRaw, locale);
+ return Optional.of(new AmountAttribute(id, text, amountRaw, currencyRaw, amountFormatted, currencyFormatted));
}
private static Optional buildAmountConversionAttribute(final OperationTemplateParam templateParam, final Map params) {
@@ -247,33 +252,46 @@ private static Optional buildAmountConversionAttribute(final Operatio
if (sourceAmount.isEmpty() || targetAmount.isEmpty()) {
return Optional.empty();
}
- final String sourceCurrency = fetchTemplateParamValue(templateParam, params, "sourceCurrency").orElse(null);
- final String targetCurrency = fetchTemplateParamValue(templateParam, params, "targetCurrency").orElse(null);
+ final Optional sourceCurrency = fetchTemplateParamValue(templateParam, params, "sourceCurrency");
+ final Optional targetCurrency = fetchTemplateParamValue(templateParam, params, "targetCurrency");
+ if (sourceCurrency.isEmpty() || targetCurrency.isEmpty()) {
+ return Optional.empty();
+ }
+
final boolean dynamic = fetchTemplateParamValue(templateParam, params, "dynamic")
.map(Boolean::parseBoolean)
.orElse(false);
+ final BigDecimal sourceAmountRaw;
+ final BigDecimal targetAmountRaw;
try {
- final BigDecimal sourceAmountValue = new BigDecimal(sourceAmount.get());
- final BigDecimal targetAmountValue = new BigDecimal(targetAmount.get());
- return Optional.of(AmountConversionAttribute.builder()
- .id(id)
- .label(text)
- .dynamic(dynamic)
- .sourceAmount(sourceAmountValue)
- // TODO (racansky, 2023-02-07, #657) implement formatting based on locale
- .sourceAmountFormatted(sourceAmount.get())
- .sourceCurrency(sourceCurrency)
- .sourceCurrencyFormatted(sourceCurrency)
- .targetAmount(targetAmountValue)
- .targetAmountFormatted(targetAmount.get())
- .targetCurrency(targetCurrency)
- .targetCurrencyFormatted(targetCurrency)
- .build());
+ sourceAmountRaw = new BigDecimal(sourceAmount.get());
+ targetAmountRaw = new BigDecimal(targetAmount.get());
} catch (NumberFormatException ex) {
logger.warn("Invalid number format: {}, skipping the AMOUNT_CONVERSION attribute!", sourceAmount);
return Optional.empty();
}
+
+ final Locale locale = LocaleContextHolder.getLocale();
+ final String sourceCurrencyRaw = sourceCurrency.get();
+ final String targetCurrencyRaw = targetCurrency.get();
+ final String sourceCurrencyFormatted = MonetaryConverter.formatCurrency(sourceCurrencyRaw, locale);
+ final String targetCurrencyFormatted = MonetaryConverter.formatCurrency(targetCurrencyRaw, locale);
+ final String sourceAmountFormatted = MonetaryConverter.formatAmount(sourceAmountRaw, sourceCurrencyRaw, locale);
+ final String targetAmountFormatted = MonetaryConverter.formatAmount(targetAmountRaw, targetCurrencyRaw, locale);
+ return Optional.of(AmountConversionAttribute.builder()
+ .id(id)
+ .label(text)
+ .dynamic(dynamic)
+ .sourceAmount(sourceAmountRaw)
+ .sourceAmountFormatted(sourceAmountFormatted)
+ .sourceCurrency(sourceCurrencyRaw)
+ .sourceCurrencyFormatted(sourceCurrencyFormatted)
+ .targetAmount(targetAmountRaw)
+ .targetAmountFormatted(targetAmountFormatted)
+ .targetCurrency(targetCurrencyRaw)
+ .targetCurrencyFormatted(targetCurrencyFormatted)
+ .build());
}
private static Optional buildImageAttribute(final OperationTemplateParam templateParam, final Map params) {
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java
new file mode 100644
index 000000000..74990bb45
--- /dev/null
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java
@@ -0,0 +1,95 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.enrollmentserver.impl.service.converter;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.money.CurrencyUnit;
+import javax.money.Monetary;
+import javax.money.UnknownCurrencyException;
+import java.math.RoundingMode;
+import java.text.NumberFormat;
+import java.util.Currency;
+import java.util.Locale;
+
+/**
+ * Convert currency and amount.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Slf4j
+class MonetaryConverter {
+
+ private static final int DEFAULT_MINIMAL_FRACTION_DIGITS = 2;
+ private static final int MAXIMAL_FRACTION_DIGITS = 18;
+
+
+ private MonetaryConverter() {
+ // hidden constructor
+ }
+
+ /**
+ * Convert the given currency code and locale.
+ * If there is no specific representation for the given currency and locale, original {@code code} is returned.
+ *
+ * @param code code to format
+ * @param locale locale to be used for the conversion
+ * @return localized currency or original code if there is no mapping available
+ */
+ static String formatCurrency(final String code, final Locale locale) {
+ try {
+ // TODO (racansky, 2023-02-16) we should rely on javax.money.CurrencyUnit instead of java.util.Currency, but there is no support for display name yet
+ // https://github.com/JavaMoney/jsr354-api/issues/58
+ return Currency.getInstance(code).getSymbol(locale);
+ } catch (final IllegalArgumentException e) {
+ logger.debug("No currency mapping for code={}, locale={}", code, locale);
+ logger.trace("No currency mapping for code={}, locale={}", code, locale, e);
+ return code;
+ }
+ }
+
+ /**
+ * Convert the given amount according to the given code and locale.
+ * Amount is rounded down if limit of maximum fraction digits reached.
+ *
+ * @param amount amount to format
+ * @param code currency code
+ * @param locale locale to be used for the conversion
+ * @return formatted amount
+ */
+ static String formatAmount(final Number amount, final String code, final Locale locale) {
+ final int fractionDigits = getFractionDigits(code);
+
+ final NumberFormat format = NumberFormat.getInstance(locale);
+ format.setMinimumFractionDigits(fractionDigits);
+ format.setMaximumFractionDigits(MAXIMAL_FRACTION_DIGITS);
+ format.setRoundingMode(RoundingMode.DOWN);
+ return format.format(amount);
+ }
+
+ private static int getFractionDigits(String code) {
+ try {
+ final CurrencyUnit currencyUnit = Monetary.getCurrency(code);
+ return currencyUnit.getDefaultFractionDigits();
+ } catch (UnknownCurrencyException e) {
+ logger.debug("No currency mapping for code={}, most probably not FIAT", code);
+ logger.trace("No currency mapping for code={}", code, e);
+ return DEFAULT_MINIMAL_FRACTION_DIGITS;
+ }
+ }
+}
diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
index d7266f7b4..e0d675c94 100644
--- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
+++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
@@ -26,9 +26,11 @@
import com.wultra.security.powerauth.lib.mtoken.model.entity.*;
import com.wultra.security.powerauth.lib.mtoken.model.entity.attributes.*;
import org.junit.jupiter.api.Test;
+import org.springframework.context.i18n.LocaleContextHolder;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -360,7 +362,7 @@ void testConvertAttributes() throws Exception {
.put("thumbnailUrl", "https://example.com/123_thumb.jpeg")
.put("originalUrl", "https://example.com/123.jpeg")
.put("sourceAmount", "1.26")
- .put("sourceCurrency", "ETC")
+ .put("sourceCurrency", "ETH")
.put("targetAmount", "1710.98")
.put("targetCurrency", "USD")
.put("dynamic", "true")
@@ -436,13 +438,14 @@ void testConvertAttributes() throws Exception {
" }\n" +
"]");
+ LocaleContextHolder.setLocale(new Locale("en"));
final Operation result = tested.convert(operationDetail, operationTemplate);
final List attributes = result.getFormData().getAttributes();
assertEquals(7, attributes.size());
final var atributesIterator = attributes.iterator();
- assertEquals(new AmountAttribute("operation.amount", "Amount", new BigDecimal("13.7"), "EUR", "13.7", "EUR"), atributesIterator.next());
+ assertEquals(new AmountAttribute("operation.amount", "Amount", new BigDecimal("13.7"), "EUR", "13.70", "€"), atributesIterator.next());
assertEquals(new KeyValueAttribute("operation.account", "To Account", "AT483200000012345864"), atributesIterator.next());
assertEquals(new NoteAttribute("operation.note", "Note", "Remember me"), atributesIterator.next());
assertEquals(new HeadingAttribute("operation.heading", "Heading"), atributesIterator.next());
@@ -453,12 +456,12 @@ void testConvertAttributes() throws Exception {
.dynamic(true)
.sourceAmount(new BigDecimal("1.26"))
.sourceAmountFormatted("1.26")
- .sourceCurrency("ETC")
- .sourceCurrencyFormatted("ETC")
+ .sourceCurrency("ETH")
+ .sourceCurrencyFormatted("ETH")
.targetAmount(new BigDecimal("1710.98"))
- .targetAmountFormatted("1710.98")
+ .targetAmountFormatted("1,710.98")
.targetCurrency("USD")
- .targetCurrencyFormatted("USD")
+ .targetCurrencyFormatted("$")
.build(), atributesIterator.next());
assertEquals(new PartyAttribute("operation.partyInfo", "Party Info", PartyInfo.builder()
.logoUrl("https://example.com/img/logo/logo.svg")
diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java
new file mode 100644
index 000000000..18b1e49dc
--- /dev/null
+++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java
@@ -0,0 +1,77 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.enrollmentserver.impl.service.converter;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test for {@link MonetaryConverter}.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+class MonetaryConverterTest {
+
+ @ParameterizedTest
+ @CsvSource({
+ "CZK, en, CZK",
+ "CZK, cs, Kč",
+ "USD, en, $",
+ "USD, cs, US$",
+ "UYU, cs, UYU",
+ "UYU, en, UYU",
+ "UYU, uy, UYU",
+ "CAD, cs, CA$",
+ "CAD, ca, CAD",
+ "CAD, en, CA$",
+ "NZD, en, NZ$",
+ "NZD, cs, NZ$",
+ "NZD, nz, NZ$",
+ "BTC, cs, BTC"
+ })
+ void testFormatCurrency(final String source, final String locale, final String expected) {
+ final String result = MonetaryConverter.formatCurrency(source, new Locale(locale));
+ assertEquals(expected, result);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "1710.9817, CZK, en, '1,710.9817'",
+ "1710.9817, CZK, cs, '1 710,9817'",
+ "1710, CZK, cs, '1 710,00'",
+ "1710, USD, en, '1,710.00'",
+ "1710.1, CZK, cs, '1 710,10'",
+ "1710.1, USD, en, '1,710.10'",
+ "1710, JPY, jp, '1,710'",
+ "1710, JPY, en, '1,710'",
+ "1710, JPY, cs, '1 710'",
+ "1, BTC, en, '1.00'",
+ "1.1, BTC, en, '1.10'",
+ "0.123456789, BTC, en, '0.123456789'",
+ "0.567567567567567567567, BTC, en, '0.567567567567567567'"
+ })
+ void testFormatAmount(final String amount, final String code, final String locale, final String expected) {
+ final String result = MonetaryConverter.formatAmount(new BigDecimal(amount), code, new Locale(locale));
+ assertEquals(expected, result);
+ }
+}
diff --git a/pom.xml b/pom.xml
index ced5f8da2..dbb371c80 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,7 @@
3.2.0
2.2.8
1.6.14
+ 1.4.2
1.7.0-SNAPSHOT
1.5.0-SNAPSHOT
@@ -194,6 +195,12 @@
${powerauth-push.version}
+
+ org.javamoney.moneta
+ moneta-core
+ ${moneta.version}
+
+
io.swagger.core.v3
swagger-annotations