package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.exception.UserNotFoundException; import eu.ztsh.wymiana.model.Currency; import eu.ztsh.wymiana.model.Symbol; 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().equals(Symbol.PLN) && !request.to().equals(Symbol.PLN)) { throw new ExchangeFailedException("Either 'from' or 'to' has to be PLN"); } var from = user.currencies().get(request.from()); 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())).orElse(create(request.to())), Optional.ofNullable(request.toSell()).orElse(BigDecimal.ZERO), Optional.ofNullable(request.toBuy()).orElse(BigDecimal.ZERO)); user.currencies().putAll(exchanged); return userService.update(user); }) .orElseThrow(() -> new UserNotFoundException(request)); } private Currency create(Symbol symbol) { return new Currency(symbol, BigDecimal.ZERO); } private Map performExchange(Currency from, Currency to, BigDecimal toSell, BigDecimal toBuy) { BigDecimal exchangeRate; BigDecimal neededFromAmount; BigDecimal requestedToAmount; if (from.symbol().equals(Symbol.PLN)) { exchangeRate = nbpService.getSellRate(to.symbol()); neededFromAmount = round(toBuy.signum() != 0 ? toBuy.multiply(exchangeRate) : toSell); requestedToAmount = round(toBuy.signum() != 0 ? toBuy : divide(toSell, exchangeRate)); } else { exchangeRate = nbpService.getBuyRate(from.symbol()); neededFromAmount = round(toBuy.signum() != 0 ? divide(toBuy, exchangeRate) : toSell); requestedToAmount = round(toBuy.signum() != 0 ? toBuy : toSell.multiply(exchangeRate)); } if (neededFromAmount.compareTo(from.amount()) > 0) { throw new InsufficientFundsException(); } var newFrom = new Currency(from.symbol(), from.amount().subtract(neededFromAmount)); var newTo = new Currency(to.symbol(), to.amount().add(requestedToAmount)); return Stream.of(newFrom, newTo).collect(Collectors.toMap(Currency::symbol, currency -> currency)); } private BigDecimal round(BigDecimal input) { return input.setScale(2, RoundingMode.HALF_UP); } private BigDecimal divide(BigDecimal input, BigDecimal division) { return input.setScale(2, RoundingMode.HALF_UP).divide(division, RoundingMode.HALF_UP); } }