From b07fa4cb65c20ba30919459c6a2dbf690a3cf8b8 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 01:22:34 +0200 Subject: [PATCH 1/7] feat: CurrencyService outline & tests --- .../exception/InsufficientFundsException.java | 9 ++++ .../ztsh/wymiana/service/CurrencyService.java | 21 ++++++++ .../wymiana/service/CurrencyServiceTest.java | 50 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java create mode 100644 src/main/java/eu/ztsh/wymiana/service/CurrencyService.java create mode 100644 src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java diff --git a/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java b/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java new file mode 100644 index 0000000..4c8f314 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java @@ -0,0 +1,9 @@ +package eu.ztsh.wymiana.exception; + +public class InsufficientFundsException extends RuntimeException { + + public InsufficientFundsException() { + super("Insufficient funds"); + } + +} diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java new file mode 100644 index 0000000..53d401e --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -0,0 +1,21 @@ +package eu.ztsh.wymiana.service; + +import eu.ztsh.wymiana.model.User; +import eu.ztsh.wymiana.validation.InstanceValidator; +import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class CurrencyService { + + private final UserService userService; + private final NbpService nbpService; + private final InstanceValidator validator; + + public User exchange(CurrencyExchangeRequest request) { + throw new UnsupportedOperationException("Not implemented yet"); + } + +} diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java new file mode 100644 index 0000000..c49396c --- /dev/null +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -0,0 +1,50 @@ +package eu.ztsh.wymiana.service; + +import eu.ztsh.wymiana.EntityCreator; +import eu.ztsh.wymiana.RepositoryBasedTest; +import eu.ztsh.wymiana.data.repository.UserRepository; +import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.validation.InstanceValidator; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +import static eu.ztsh.wymiana.EntityCreator.Constants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CurrencyServiceTest extends RepositoryBasedTest { + + private final CurrencyService currencyService; + + @Autowired + public CurrencyServiceTest(UserRepository userRepository, InstanceValidator instanceValidator) { + super(userRepository); + var nbp = Mockito.mock(NbpService.class); + Mockito.when(nbp.getSellRate("USD")).thenReturn(USD_SELL); + Mockito.when(nbp.getBuyRate("USD")).thenReturn(USD_BUY); + currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator); + } + + @Test + void successTest() { + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()); + assertThat(result.currencies()) + .matches(map -> map.get(PLN_SYMBOL).amount() == 0 && map.get(USD_SYMBOL).amount() == USD_BUY); + } + + @Test + void insufficientFundsTest() { + assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toBuy(PLN) + .build())) + .isInstanceOf(InsufficientFundsException.class); + } + +} From af187e3daa7fbf314c5a378698943914c5297a71 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 08:31:23 +0200 Subject: [PATCH 2/7] test: Moved disabled tests from validator to service tests --- .../wymiana/service/CurrencyServiceTest.java | 61 ++++++++++++++++++- .../ValidExchangeRequestValidatorTest.java | 37 ----------- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index c49396c..c64e9b2 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -5,10 +5,17 @@ import eu.ztsh.wymiana.RepositoryBasedTest; import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.validation.InstanceValidator; +import eu.ztsh.wymiana.validation.ValidationFailedException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import java.util.stream.Stream; + import static eu.ztsh.wymiana.EntityCreator.Constants.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -27,7 +34,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { } @Test - void successTest() { + void toSellSuccessTest() { var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) @@ -35,6 +42,21 @@ class CurrencyServiceTest extends RepositoryBasedTest { .build()); assertThat(result.currencies()) .matches(map -> map.get(PLN_SYMBOL).amount() == 0 && map.get(USD_SYMBOL).amount() == USD_BUY); + var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); + expect(expected); + } + + @Test + void toBuySuccessTest() { + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toBuy(USD_BUY) + .build()); + assertThat(result.currencies()) + .matches(map -> map.get(PLN_SYMBOL).amount() == 0 && map.get(USD_SYMBOL).amount() == USD_BUY); + var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); + expect(expected); } @Test @@ -47,4 +69,41 @@ class CurrencyServiceTest extends RepositoryBasedTest { .isInstanceOf(InsufficientFundsException.class); } + @DisplayName("Invalid PESEL value") + @ParameterizedTest + @MethodSource + void invalidPeselTest(String pesel) { + assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + .pesel(pesel) + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(USD_SELL) + .build())) + .isInstanceOf(ValidationFailedException.class); + } + + @Test + @DisplayName("Empty 'from' value") + void emptyFromTest() { + assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + .to(USD_SYMBOL) + .toSell(USD_SELL) + .build())) + .isInstanceOf(ValidationFailedException.class); + } + + @Test + @DisplayName("Empty 'to' value") + void emptyToTest() { + assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .toSell(USD_SELL) + .build())) + .isInstanceOf(ValidationFailedException.class); + } + + private static Stream invalidPeselTest() { + return Stream.of("INVALID", PESEL.replace('6', '7')); + } + } diff --git a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java index 85161e6..3a3a4d1 100644 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java +++ b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java @@ -38,19 +38,6 @@ class ValidExchangeRequestValidatorTest extends ValidatorTest invalidPeselTest() { - return Stream.of("INVALID", PESEL.replace('6', '7')); - } - } From 6d8a2093b91f48af9246ae55bde362c1fada44ee Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 08:33:57 +0200 Subject: [PATCH 3/7] test: SonarLint fixes --- .../wymiana/service/CurrencyServiceTest.java | 21 +++++++++++-------- .../ValidExchangeRequestValidatorTest.java | 10 ++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index c64e9b2..c26f5ad 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -6,7 +6,6 @@ import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.validation.ValidationFailedException; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -61,11 +60,12 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Test void insufficientFundsTest() { - assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + var entity = EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) .toBuy(PLN) - .build())) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) .isInstanceOf(InsufficientFundsException.class); } @@ -73,32 +73,35 @@ class CurrencyServiceTest extends RepositoryBasedTest { @ParameterizedTest @MethodSource void invalidPeselTest(String pesel) { - assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + var entity = EntityCreator.exchangeRequest() .pesel(pesel) .from(PLN_SYMBOL) .to(USD_SYMBOL) .toSell(USD_SELL) - .build())) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) .isInstanceOf(ValidationFailedException.class); } @Test @DisplayName("Empty 'from' value") void emptyFromTest() { - assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + var entity = EntityCreator.exchangeRequest() .to(USD_SYMBOL) .toSell(USD_SELL) - .build())) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) .isInstanceOf(ValidationFailedException.class); } @Test @DisplayName("Empty 'to' value") void emptyToTest() { - assertThatThrownBy(() -> currencyService.exchange(EntityCreator.exchangeRequest() + var entity = EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .toSell(USD_SELL) - .build())) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) .isInstanceOf(ValidationFailedException.class); } diff --git a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java index 3a3a4d1..5226e14 100644 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java +++ b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java @@ -2,15 +2,13 @@ package eu.ztsh.wymiana.validation; import eu.ztsh.wymiana.EntityCreator; import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - -import static eu.ztsh.wymiana.EntityCreator.Constants.*; +import static eu.ztsh.wymiana.EntityCreator.Constants.PLN_SYMBOL; +import static eu.ztsh.wymiana.EntityCreator.Constants.USD_BUY; +import static eu.ztsh.wymiana.EntityCreator.Constants.USD_SELL; +import static eu.ztsh.wymiana.EntityCreator.Constants.USD_SYMBOL; class ValidExchangeRequestValidatorTest extends ValidatorTest { From c87bcc8b54af6c9658c61b252a8adee7a0a40203 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 16:25:19 +0200 Subject: [PATCH 4/7] test: CurrencyServiceTest completed? --- .../exception/ExchangeFailedException.java | 13 ++++ .../ztsh/wymiana/service/CurrencyService.java | 50 ++++++++++++++- .../eu/ztsh/wymiana/service/NbpService.java | 4 +- .../eu/ztsh/wymiana/service/UserService.java | 4 ++ .../ValidExchangeRequestValidator.java | 3 +- .../wymiana/service/CurrencyServiceTest.java | 63 +++++++++++++++++-- 6 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java diff --git a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java new file mode 100644 index 0000000..481d330 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java @@ -0,0 +1,13 @@ +package eu.ztsh.wymiana.exception; + +public class ExchangeFailedException extends RuntimeException{ + + public ExchangeFailedException() { + this("An exchange error has occurred"); + } + + public ExchangeFailedException(String message) { + super(message); + } + +} diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java index 53d401e..a26b0bc 100644 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -1,11 +1,19 @@ package eu.ztsh.wymiana.service; +import eu.ztsh.wymiana.exception.ExchangeFailedException; +import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.model.Currency; import eu.ztsh.wymiana.model.User; import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + @RequiredArgsConstructor @Service public class CurrencyService { @@ -15,7 +23,47 @@ public class CurrencyService { private final InstanceValidator validator; public User exchange(CurrencyExchangeRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + validator.validate(request); + return userService.get(request.pesel()).map(user -> { + if (!request.from().equalsIgnoreCase("PLN") && !request.to().equalsIgnoreCase("PLN")) { + throw new ExchangeFailedException("Either 'from' or 'to' has to be PLN"); + } + // As we support only USD now, we need to limit second parameter too + // Begin: unlock other currencies + if (!request.from().equalsIgnoreCase("USD") && !request.to().equalsIgnoreCase("USD")) { + throw new ExchangeFailedException("Either 'from' or 'to' has to be USD"); + } + // End: unlock other currencies + + var exchanged = performExchange(user.currencies().get(request.from().toUpperCase()), + user.currencies().get(request.to().toUpperCase()), + Optional.ofNullable(request.toSell()).orElse(0D), + Optional.ofNullable(request.toBuy()).orElse(0D)); + user.currencies().putAll(exchanged); + return userService.update(user); + }) + .orElseThrow(ExchangeFailedException::new); + } + + private Map performExchange(Currency from, Currency to, double toSell, double toBuy) { + double exchangeRate; + double neededFromAmount; + double requestedToAmount; + if (from.symbol().equalsIgnoreCase("PLN")) { + exchangeRate = nbpService.getSellRate(to.symbol()); + neededFromAmount = toBuy != 0 ? toBuy * exchangeRate : toSell; + requestedToAmount = toBuy != 0 ? toBuy : toSell / exchangeRate; + } else { + exchangeRate = nbpService.getSellRate(from.symbol()); + neededFromAmount = toBuy != 0 ? toBuy / exchangeRate : toSell; + requestedToAmount = toBuy != 0 ? toBuy : toSell * exchangeRate; + } + if (neededFromAmount > from.amount()) { + throw new InsufficientFundsException(); + } + var newFrom = new Currency(from.symbol(), from.amount() - neededFromAmount); + var newTo = new Currency(to.symbol(), to.amount() + requestedToAmount); + return Stream.of(newFrom, newTo).collect(Collectors.toMap(Currency::symbol, currency -> currency)); } } diff --git a/src/main/java/eu/ztsh/wymiana/service/NbpService.java b/src/main/java/eu/ztsh/wymiana/service/NbpService.java index da26238..e612e0a 100644 --- a/src/main/java/eu/ztsh/wymiana/service/NbpService.java +++ b/src/main/java/eu/ztsh/wymiana/service/NbpService.java @@ -31,11 +31,11 @@ public class NbpService { private final ConcurrentMap cache = new ConcurrentHashMap<>(1); public double getSellRate(String currency) { - return getCurrency(currency).sell(); + return getCurrency(currency.toUpperCase()).sell(); } public double getBuyRate(String currency) { - return getCurrency(currency).buy(); + return getCurrency(currency.toUpperCase()).buy(); } private synchronized RatesCache getCurrency(String currency) { diff --git a/src/main/java/eu/ztsh/wymiana/service/UserService.java b/src/main/java/eu/ztsh/wymiana/service/UserService.java index d23dc2c..e5b26f6 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -30,4 +30,8 @@ public class UserService { return userRepository.findById(pesel).map(UserMapper::entityToPojo); } + public User update(User user) { + throw new UnsupportedOperationException("Not implemented yet"); + } + } diff --git a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java index 0895d92..143876b 100644 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java +++ b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java @@ -14,7 +14,8 @@ public class ValidExchangeRequestValidator implements return false; } - return !request.from().equals(request.to()) + // Apart from @NotNull annotation we need to check if request.from() != null to avoid NPE in equals + return (request.from() != null && !request.from().equals(request.to())) && !((request.toBuy() == null && request.toSell() == null) || (request.toBuy() != null && request.toSell() != null)) && ((request.toBuy() != null && request.toBuy() >= 0) diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index c26f5ad..13ae4ec 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -3,9 +3,11 @@ package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.EntityCreator; import eu.ztsh.wymiana.RepositoryBasedTest; import eu.ztsh.wymiana.data.repository.UserRepository; +import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.validation.ValidationFailedException; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -32,8 +34,11 @@ class CurrencyServiceTest extends RepositoryBasedTest { currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator); } + @Transactional @Test - void toSellSuccessTest() { + void plnToUsdToSellSuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) @@ -45,8 +50,11 @@ class CurrencyServiceTest extends RepositoryBasedTest { expect(expected); } + @Transactional @Test - void toBuySuccessTest() { + void plnToUsdToBuySuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) @@ -57,6 +65,37 @@ class CurrencyServiceTest extends RepositoryBasedTest { var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); expect(expected); } + @Transactional + @Test + void usdToPlnToSellSuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toSell(USD_SELL) + .build()); + assertThat(result.currencies()) + .matches(map -> map.get(PLN_SYMBOL).amount() == PLN && map.get(USD_SYMBOL).amount() == 0); + var expected = EntityCreator.user().pln(PLN).usd(0).build(); + expect(expected); + } + + @Transactional + @Test + void usdToPlnToBuySuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toBuy(PLN) + .build()); + assertThat(result.currencies()) + .matches(map -> map.get(PLN_SYMBOL).amount() == PLN && map.get(USD_SYMBOL).amount() == 0); + var expected = EntityCreator.user().pln(PLN).usd(0).build(); + expect(expected); + } @Test void insufficientFundsTest() { @@ -80,7 +119,19 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("PESEL"); + } + + @Test + void notExistingUserTest() { + var entity = EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(USD_SELL) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) + .isInstanceOf(ExchangeFailedException.class); } @Test @@ -91,7 +142,8 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("null"); } @Test @@ -102,7 +154,8 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("null"); } private static Stream invalidPeselTest() { From 71109174f7dc5dcfbf5723d1967302c325fbfbfc Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 17:19:30 +0200 Subject: [PATCH 5/7] fix: exchange method fixes --- .../ztsh/wymiana/service/CurrencyService.java | 30 +++++++--- .../wymiana/service/CurrencyServiceTest.java | 56 +++++++++++++++++-- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java index a26b0bc..f43d6b6 100644 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -9,6 +9,8 @@ import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -35,8 +37,13 @@ public class CurrencyService { } // End: unlock other currencies - var exchanged = performExchange(user.currencies().get(request.from().toUpperCase()), - user.currencies().get(request.to().toUpperCase()), + var from = user.currencies().get(request.from().toUpperCase()); + if (from == null) { + // There is no currency 'from' opened so no need to check if user has funds to exchange + throw new InsufficientFundsException(); + } + var exchanged = performExchange(from, + Optional.ofNullable(user.currencies().get(request.to().toUpperCase())).orElse(create(request.to())), Optional.ofNullable(request.toSell()).orElse(0D), Optional.ofNullable(request.toBuy()).orElse(0D)); user.currencies().putAll(exchanged); @@ -45,18 +52,23 @@ public class CurrencyService { .orElseThrow(ExchangeFailedException::new); } + private Currency create(String symbol) { + // TODO: check if supported + return new Currency(symbol.toUpperCase(), 0D); + } + private Map performExchange(Currency from, Currency to, double toSell, double toBuy) { double exchangeRate; double neededFromAmount; double requestedToAmount; if (from.symbol().equalsIgnoreCase("PLN")) { exchangeRate = nbpService.getSellRate(to.symbol()); - neededFromAmount = toBuy != 0 ? toBuy * exchangeRate : toSell; - requestedToAmount = toBuy != 0 ? toBuy : toSell / exchangeRate; + neededFromAmount = round(toBuy != 0 ? toBuy * exchangeRate : toSell); + requestedToAmount = round(toBuy != 0 ? toBuy : toSell / exchangeRate); } else { - exchangeRate = nbpService.getSellRate(from.symbol()); - neededFromAmount = toBuy != 0 ? toBuy / exchangeRate : toSell; - requestedToAmount = toBuy != 0 ? toBuy : toSell * exchangeRate; + exchangeRate = nbpService.getBuyRate(from.symbol()); + neededFromAmount = round(toBuy != 0 ? toBuy / exchangeRate : toSell); + requestedToAmount = round(toBuy != 0 ? toBuy : toSell * exchangeRate); } if (neededFromAmount > from.amount()) { throw new InsufficientFundsException(); @@ -66,4 +78,8 @@ public class CurrencyService { return Stream.of(newFrom, newTo).collect(Collectors.toMap(Currency::symbol, currency -> currency)); } + private double round(double input) { + return BigDecimal.valueOf(input).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + } diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index 13ae4ec..cc640a0 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -29,8 +29,8 @@ class CurrencyServiceTest extends RepositoryBasedTest { public CurrencyServiceTest(UserRepository userRepository, InstanceValidator instanceValidator) { super(userRepository); var nbp = Mockito.mock(NbpService.class); - Mockito.when(nbp.getSellRate("USD")).thenReturn(USD_SELL); - Mockito.when(nbp.getBuyRate("USD")).thenReturn(USD_BUY); + Mockito.when(nbp.getSellRate("USD")).thenReturn(SELL_RATE); + Mockito.when(nbp.getBuyRate("USD")).thenReturn(BUY_RATE); currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator); } @@ -65,10 +65,11 @@ class CurrencyServiceTest extends RepositoryBasedTest { var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); expect(expected); } + @Transactional @Test void usdToPlnToSellSuccessTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.user().pln(0).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -84,7 +85,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Transactional @Test void usdToPlnToBuySuccessTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.user().pln(0).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -97,14 +98,57 @@ class CurrencyServiceTest extends RepositoryBasedTest { expect(expected); } + @Transactional + @Test + void usdToPlnNoUsdCurrencyTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var request = EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toBuy(PLN) + .build(); + assertThatThrownBy(() -> currencyService.exchange(request)) + .isInstanceOf(InsufficientFundsException.class); + } + + @Transactional + @Test + void doubleExchangeTest() { + var initialValue = 100; + var entity = EntityCreator.user().pln(initialValue).build(); + userRepository.save(entity); + var result1 = currencyService.exchange(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toBuy(USD_BUY) + .build()); + var result2 = currencyService.exchange(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toSell(USD_BUY) + .build()); + var resultOptional = userRepository.findById(entity.getPesel()); + assertThat(resultOptional) + .isNotEmpty(); + var resultEntity = resultOptional.get(); + assertThat(resultEntity.getCurrencies() + .stream() + .filter(c -> c.getSymbol().equalsIgnoreCase("PLN")) + .findFirst()).isNotEmpty().get().matches(currencyEntity -> currencyEntity.getAmount() < initialValue); + } + + @Transactional @Test void insufficientFundsTest() { - var entity = EntityCreator.exchangeRequest() + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var request = EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) .toBuy(PLN) .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) + assertThatThrownBy(() -> currencyService.exchange(request)) .isInstanceOf(InsufficientFundsException.class); } From 94dd43c138b58febe48512577286d746c9bd6391 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 17:44:51 +0200 Subject: [PATCH 6/7] fix: Fixed user persistence after exchange --- .../java/eu/ztsh/wymiana/service/UserService.java | 2 +- .../java/eu/ztsh/wymiana/util/CurrencyMapper.java | 8 ++++++++ src/main/java/eu/ztsh/wymiana/util/UserMapper.java | 5 +++++ src/test/java/eu/ztsh/wymiana/EntityCreator.java | 8 ++++---- .../ztsh/wymiana/service/CurrencyServiceTest.java | 4 ++-- .../java/eu/ztsh/wymiana/util/UserMapperTest.java | 14 ++++++++++++++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/ztsh/wymiana/service/UserService.java b/src/main/java/eu/ztsh/wymiana/service/UserService.java index e5b26f6..f9e7cde 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -31,7 +31,7 @@ public class UserService { } public User update(User user) { - throw new UnsupportedOperationException("Not implemented yet"); + return UserMapper.entityToPojo(userRepository.save(UserMapper.pojoToEntity(user))); } } diff --git a/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java b/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java index 7542187..b962ddc 100644 --- a/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java @@ -13,11 +13,19 @@ public class CurrencyMapper { return new Currency(entity.getSymbol(), entity.getAmount()); } + public static CurrencyEntity pojoToEntity(Currency pojo, String pesel) { + return new CurrencyEntity(pesel, pojo.symbol(), pojo.amount()); + } + public static Map entitiesToPojoMap(List values) { return values.stream().map(CurrencyMapper::entityToPojo) .collect(Collectors.toMap(Currency::symbol, pojo -> pojo)); } + public static List pojoMapToEntities(Map currencies, String pesel) { + return currencies.values().stream().map(entry -> pojoToEntity(entry, pesel)).toList(); + } + private CurrencyMapper() { } diff --git a/src/main/java/eu/ztsh/wymiana/util/UserMapper.java b/src/main/java/eu/ztsh/wymiana/util/UserMapper.java index b4aff44..39985fb 100644 --- a/src/main/java/eu/ztsh/wymiana/util/UserMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/UserMapper.java @@ -14,6 +14,11 @@ public class UserMapper { CurrencyMapper.entitiesToPojoMap(entity.getCurrencies())); } + public static UserEntity pojoToEntity(User pojo) { + return new UserEntity(pojo.pesel(), pojo.name(), pojo.surname(), + CurrencyMapper.pojoMapToEntities(pojo.currencies(), pojo.pesel())); + } + public static UserEntity requestToEntity(UserCreateRequest request) { return new UserEntity(request.pesel(), request.name(), request.surname(), List.of(new CurrencyEntity(request.pesel(), "PLN", request.pln()))); diff --git a/src/test/java/eu/ztsh/wymiana/EntityCreator.java b/src/test/java/eu/ztsh/wymiana/EntityCreator.java index df78b20..c0bb186 100644 --- a/src/test/java/eu/ztsh/wymiana/EntityCreator.java +++ b/src/test/java/eu/ztsh/wymiana/EntityCreator.java @@ -62,8 +62,8 @@ public class EntityCreator { String name; String surname; String pesel; - double pln; - double usd; + double pln = -1; + double usd = -1; public UserEntityBuilder name(String name) { this.name = name; @@ -93,10 +93,10 @@ public class EntityCreator { public UserEntity build() { var nonnulPesel = Optional.ofNullable(pesel).orElse(Constants.PESEL); List currencies = new ArrayList<>(); - if (pln > 0) { + if (pln > -1) { currencies.add(new CurrencyEntity(nonnulPesel, "PLN", pln)); } - if (usd > 0) { + if (usd > -1) { currencies.add(new CurrencyEntity(nonnulPesel, "USD", usd)); } if (currencies.isEmpty()) { diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index cc640a0..95b1442 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -69,7 +69,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Transactional @Test void usdToPlnToSellSuccessTest() { - var entity = EntityCreator.user().pln(0).usd(USD_SELL).build(); + var entity = EntityCreator.user().pln(-1).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -85,7 +85,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Transactional @Test void usdToPlnToBuySuccessTest() { - var entity = EntityCreator.user().pln(0).usd(USD_SELL).build(); + var entity = EntityCreator.user().pln(-1).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) diff --git a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java index 0479332..0b4a7d9 100644 --- a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java +++ b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java @@ -25,6 +25,20 @@ class UserMapperTest { .isEqualTo(expected); } + @Test + void pojoToEntityTest() { + var entity = new User( + EntityCreator.Constants.NAME, + EntityCreator.Constants.SURNAME, + EntityCreator.Constants.PESEL, + Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) + ); + var expected = EntityCreator.user().build(); + assertThat(UserMapper.pojoToEntity(entity)) + .usingRecursiveComparison() + .isEqualTo(expected); + } + @Test void requestToEntityTest() { var request = EntityCreator.userRequest().build(); From 4fcc395a61717c87a7b4add13c298a8bc36b61b1 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 17:50:39 +0200 Subject: [PATCH 7/7] chore: Formatting fixes --- .../exception/ExchangeFailedException.java | 2 +- .../ztsh/wymiana/service/CurrencyService.java | 2 +- .../eu/ztsh/wymiana/util/UserMapperTest.java | 28 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java index 481d330..b32bb5e 100644 --- a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java +++ b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java @@ -1,6 +1,6 @@ package eu.ztsh.wymiana.exception; -public class ExchangeFailedException extends RuntimeException{ +public class ExchangeFailedException extends RuntimeException { public ExchangeFailedException() { this("An exchange error has occurred"); diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java index f43d6b6..b00f761 100644 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -53,7 +53,7 @@ public class CurrencyService { } private Currency create(String symbol) { - // TODO: check if supported + // TODO: check if supported - now limited to PLN <-> USD return new Currency(symbol.toUpperCase(), 0D); } diff --git a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java index 0b4a7d9..dc1eeb8 100644 --- a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java +++ b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java @@ -15,28 +15,28 @@ class UserMapperTest { void entityToPojoTest() { var entity = EntityCreator.user().build(); var expected = new User( - EntityCreator.Constants.NAME, - EntityCreator.Constants.SURNAME, - EntityCreator.Constants.PESEL, - Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) + EntityCreator.Constants.NAME, + EntityCreator.Constants.SURNAME, + EntityCreator.Constants.PESEL, + Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) ); assertThat(UserMapper.entityToPojo(entity)) - .usingRecursiveComparison() - .isEqualTo(expected); + .usingRecursiveComparison() + .isEqualTo(expected); } @Test void pojoToEntityTest() { var entity = new User( - EntityCreator.Constants.NAME, - EntityCreator.Constants.SURNAME, - EntityCreator.Constants.PESEL, - Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) + EntityCreator.Constants.NAME, + EntityCreator.Constants.SURNAME, + EntityCreator.Constants.PESEL, + Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) ); var expected = EntityCreator.user().build(); assertThat(UserMapper.pojoToEntity(entity)) - .usingRecursiveComparison() - .isEqualTo(expected); + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -44,8 +44,8 @@ class UserMapperTest { var request = EntityCreator.userRequest().build(); var expected = EntityCreator.user().build(); assertThat(UserMapper.requestToEntity(request)) - .usingRecursiveComparison() - .isEqualTo(expected); + .usingRecursiveComparison() + .isEqualTo(expected); } }