test: CurrencyServiceTest completed?
This commit is contained in:
parent
6d8a2093b9
commit
c87bcc8b54
6 changed files with 128 additions and 9 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,11 +1,19 @@
|
||||||
package eu.ztsh.wymiana.service;
|
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.model.User;
|
||||||
import eu.ztsh.wymiana.validation.InstanceValidator;
|
import eu.ztsh.wymiana.validation.InstanceValidator;
|
||||||
import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
|
import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
public class CurrencyService {
|
public class CurrencyService {
|
||||||
|
@ -15,7 +23,47 @@ public class CurrencyService {
|
||||||
private final InstanceValidator validator;
|
private final InstanceValidator validator;
|
||||||
|
|
||||||
public User exchange(CurrencyExchangeRequest request) {
|
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<String, Currency> 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,11 @@ public class NbpService {
|
||||||
private final ConcurrentMap<String, RatesCache> cache = new ConcurrentHashMap<>(1);
|
private final ConcurrentMap<String, RatesCache> cache = new ConcurrentHashMap<>(1);
|
||||||
|
|
||||||
public double getSellRate(String currency) {
|
public double getSellRate(String currency) {
|
||||||
return getCurrency(currency).sell();
|
return getCurrency(currency.toUpperCase()).sell();
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getBuyRate(String currency) {
|
public double getBuyRate(String currency) {
|
||||||
return getCurrency(currency).buy();
|
return getCurrency(currency.toUpperCase()).buy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized RatesCache getCurrency(String currency) {
|
private synchronized RatesCache getCurrency(String currency) {
|
||||||
|
|
|
@ -30,4 +30,8 @@ public class UserService {
|
||||||
return userRepository.findById(pesel).map(UserMapper::entityToPojo);
|
return userRepository.findById(pesel).map(UserMapper::entityToPojo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public User update(User user) {
|
||||||
|
throw new UnsupportedOperationException("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ public class ValidExchangeRequestValidator implements
|
||||||
return false;
|
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.toSell() != null))
|
|| (request.toBuy() != null && request.toSell() != null))
|
||||||
&& ((request.toBuy() != null && request.toBuy() >= 0)
|
&& ((request.toBuy() != null && request.toBuy() >= 0)
|
||||||
|
|
|
@ -3,9 +3,11 @@ package eu.ztsh.wymiana.service;
|
||||||
import eu.ztsh.wymiana.EntityCreator;
|
import eu.ztsh.wymiana.EntityCreator;
|
||||||
import eu.ztsh.wymiana.RepositoryBasedTest;
|
import eu.ztsh.wymiana.RepositoryBasedTest;
|
||||||
import eu.ztsh.wymiana.data.repository.UserRepository;
|
import eu.ztsh.wymiana.data.repository.UserRepository;
|
||||||
|
import eu.ztsh.wymiana.exception.ExchangeFailedException;
|
||||||
import eu.ztsh.wymiana.exception.InsufficientFundsException;
|
import eu.ztsh.wymiana.exception.InsufficientFundsException;
|
||||||
import eu.ztsh.wymiana.validation.InstanceValidator;
|
import eu.ztsh.wymiana.validation.InstanceValidator;
|
||||||
import eu.ztsh.wymiana.validation.ValidationFailedException;
|
import eu.ztsh.wymiana.validation.ValidationFailedException;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
@ -32,8 +34,11 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator);
|
currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Test
|
@Test
|
||||||
void toSellSuccessTest() {
|
void plnToUsdToSellSuccessTest() {
|
||||||
|
var entity = EntityCreator.user().build();
|
||||||
|
userRepository.save(entity);
|
||||||
var result = currencyService.exchange(EntityCreator.exchangeRequest()
|
var result = currencyService.exchange(EntityCreator.exchangeRequest()
|
||||||
.from(PLN_SYMBOL)
|
.from(PLN_SYMBOL)
|
||||||
.to(USD_SYMBOL)
|
.to(USD_SYMBOL)
|
||||||
|
@ -45,8 +50,11 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
expect(expected);
|
expect(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Test
|
@Test
|
||||||
void toBuySuccessTest() {
|
void plnToUsdToBuySuccessTest() {
|
||||||
|
var entity = EntityCreator.user().build();
|
||||||
|
userRepository.save(entity);
|
||||||
var result = currencyService.exchange(EntityCreator.exchangeRequest()
|
var result = currencyService.exchange(EntityCreator.exchangeRequest()
|
||||||
.from(PLN_SYMBOL)
|
.from(PLN_SYMBOL)
|
||||||
.to(USD_SYMBOL)
|
.to(USD_SYMBOL)
|
||||||
|
@ -57,6 +65,37 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
var expected = EntityCreator.user().pln(0).usd(USD_BUY).build();
|
var expected = EntityCreator.user().pln(0).usd(USD_BUY).build();
|
||||||
expect(expected);
|
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
|
@Test
|
||||||
void insufficientFundsTest() {
|
void insufficientFundsTest() {
|
||||||
|
@ -80,7 +119,19 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
.toSell(USD_SELL)
|
.toSell(USD_SELL)
|
||||||
.build();
|
.build();
|
||||||
assertThatThrownBy(() -> currencyService.exchange(entity))
|
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
|
@Test
|
||||||
|
@ -91,7 +142,8 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
.toSell(USD_SELL)
|
.toSell(USD_SELL)
|
||||||
.build();
|
.build();
|
||||||
assertThatThrownBy(() -> currencyService.exchange(entity))
|
assertThatThrownBy(() -> currencyService.exchange(entity))
|
||||||
.isInstanceOf(ValidationFailedException.class);
|
.isInstanceOf(ValidationFailedException.class)
|
||||||
|
.hasMessageContaining("null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -102,7 +154,8 @@ class CurrencyServiceTest extends RepositoryBasedTest {
|
||||||
.toSell(USD_SELL)
|
.toSell(USD_SELL)
|
||||||
.build();
|
.build();
|
||||||
assertThatThrownBy(() -> currencyService.exchange(entity))
|
assertThatThrownBy(() -> currencyService.exchange(entity))
|
||||||
.isInstanceOf(ValidationFailedException.class);
|
.isInstanceOf(ValidationFailedException.class)
|
||||||
|
.hasMessageContaining("null");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<String> invalidPeselTest() {
|
private static Stream<String> invalidPeselTest() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue