diff --git a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java deleted file mode 100644 index b32bb5e..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java +++ /dev/null @@ -1,13 +0,0 @@ -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/exception/InsufficientFundsException.java b/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java deleted file mode 100644 index 4c8f314..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index b00f761..0000000 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ /dev/null @@ -1,85 +0,0 @@ -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.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@RequiredArgsConstructor -@Service -public class CurrencyService { - - private final UserService userService; - private final NbpService nbpService; - private final InstanceValidator validator; - - public User exchange(CurrencyExchangeRequest request) { - 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 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); - return userService.update(user); - }) - .orElseThrow(ExchangeFailedException::new); - } - - private Currency create(String symbol) { - // TODO: check if supported - now limited to PLN <-> USD - 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 = round(toBuy != 0 ? toBuy * exchangeRate : toSell); - requestedToAmount = round(toBuy != 0 ? toBuy : toSell / exchangeRate); - } else { - 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(); - } - 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)); - } - - private double round(double input) { - return BigDecimal.valueOf(input).setScale(2, RoundingMode.HALF_UP).doubleValue(); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/service/NbpService.java b/src/main/java/eu/ztsh/wymiana/service/NbpService.java index e612e0a..da26238 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.toUpperCase()).sell(); + return getCurrency(currency).sell(); } public double getBuyRate(String currency) { - return getCurrency(currency.toUpperCase()).buy(); + return getCurrency(currency).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 f9e7cde..d23dc2c 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -30,8 +30,4 @@ public class UserService { return userRepository.findById(pesel).map(UserMapper::entityToPojo); } - public User update(User user) { - 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 b962ddc..7542187 100644 --- a/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java @@ -13,19 +13,11 @@ 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 39985fb..b4aff44 100644 --- a/src/main/java/eu/ztsh/wymiana/util/UserMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/UserMapper.java @@ -14,11 +14,6 @@ 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/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java index 143876b..0895d92 100644 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java +++ b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java @@ -14,8 +14,7 @@ public class ValidExchangeRequestValidator implements return false; } - // 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())) + return !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/EntityCreator.java b/src/test/java/eu/ztsh/wymiana/EntityCreator.java index c0bb186..df78b20 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 = -1; - double usd = -1; + double pln; + double usd; 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 > -1) { + if (pln > 0) { currencies.add(new CurrencyEntity(nonnulPesel, "PLN", pln)); } - if (usd > -1) { + if (usd > 0) { 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 deleted file mode 100644 index 95b1442..0000000 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ /dev/null @@ -1,209 +0,0 @@ -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; -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; - -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(SELL_RATE); - Mockito.when(nbp.getBuyRate("USD")).thenReturn(BUY_RATE); - currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator); - } - - @Transactional - @Test - void plnToUsdToSellSuccessTest() { - var entity = EntityCreator.user().build(); - userRepository.save(entity); - 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); - var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); - expect(expected); - } - - @Transactional - @Test - void plnToUsdToBuySuccessTest() { - var entity = EntityCreator.user().build(); - userRepository.save(entity); - 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); - } - - @Transactional - @Test - void usdToPlnToSellSuccessTest() { - var entity = EntityCreator.user().pln(-1).usd(USD_SELL).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().pln(-1).usd(USD_SELL).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); - } - - @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.user().build(); - userRepository.save(entity); - var request = EntityCreator.exchangeRequest() - .from(PLN_SYMBOL) - .to(USD_SYMBOL) - .toBuy(PLN) - .build(); - assertThatThrownBy(() -> currencyService.exchange(request)) - .isInstanceOf(InsufficientFundsException.class); - } - - @DisplayName("Invalid PESEL value") - @ParameterizedTest - @MethodSource - void invalidPeselTest(String pesel) { - var entity = EntityCreator.exchangeRequest() - .pesel(pesel) - .from(PLN_SYMBOL) - .to(USD_SYMBOL) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .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 - @DisplayName("Empty 'from' value") - void emptyFromTest() { - var entity = EntityCreator.exchangeRequest() - .to(USD_SYMBOL) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining("null"); - } - - @Test - @DisplayName("Empty 'to' value") - void emptyToTest() { - var entity = EntityCreator.exchangeRequest() - .from(PLN_SYMBOL) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining("null"); - } - - private static Stream invalidPeselTest() { - return Stream.of("INVALID", PESEL.replace('6', '7')); - } - -} diff --git a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java index dc1eeb8..0479332 100644 --- a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java +++ b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java @@ -15,28 +15,14 @@ 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); - } - - @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); + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -44,8 +30,8 @@ class UserMapperTest { var request = EntityCreator.userRequest().build(); var expected = EntityCreator.user().build(); assertThat(UserMapper.requestToEntity(request)) - .usingRecursiveComparison() - .isEqualTo(expected); + .usingRecursiveComparison() + .isEqualTo(expected); } } diff --git a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java index 5226e14..85161e6 100644 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java +++ b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java @@ -2,13 +2,15 @@ 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 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; +import java.util.stream.Stream; + +import static eu.ztsh.wymiana.EntityCreator.Constants.*; class ValidExchangeRequestValidatorTest extends ValidatorTest { @@ -36,6 +38,19 @@ class ValidExchangeRequestValidatorTest extends ValidatorTest invalidPeselTest() { + return Stream.of("INVALID", PESEL.replace('6', '7')); + } + }