package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.exception.NoDataException; import eu.ztsh.wymiana.model.Rates; import eu.ztsh.wymiana.model.Symbol; import lombok.RequiredArgsConstructor; import org.assertj.core.util.VisibleForTesting; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import java.math.BigDecimal; import java.time.Clock; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * NBP exchange rates service */ @Service @RequiredArgsConstructor public class NbpService { private final Clock clock; private final RestClient restClient; private static final String URI_PATTERN = "/api/exchangerates/rates/c/{code}/{date}/"; private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final ConcurrentMap cache = new ConcurrentHashMap<>(1); public BigDecimal getSellRate(Symbol currency) { return getCurrency(currency).sell(); } public BigDecimal getBuyRate(Symbol currency) { return getCurrency(currency).buy(); } private synchronized RatesCache getCurrency(Symbol currency) { var today = getFetchDate(); var cacheObject = cache.get(currency); if (cacheObject == null || cacheObject.date().isBefore(today)) { var fresh = fetchData(currency, dtf.format(today)); var rate = fresh.getRates().getFirst(); cacheObject = new RatesCache( LocalDate.parse(rate.getEffectiveDate(), dtf), rate.getBid(), rate.getAsk() ); cache.put(Symbol.valueOf(fresh.getCode().toUpperCase()), cacheObject); } return cacheObject; } /** * Calculates date for data fetch. *

* Usually this would be today, but as rates are set only Mon-Fri, during weekends it is needed to fetch last friday rates. * * @return date for data fetch */ @VisibleForTesting LocalDate getFetchDate() { var today = LocalDate.now(clock); return isWeekend(today) ? today.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)) : today; } @VisibleForTesting Rates fetchData(Symbol code, String date) { return restClient.get().uri(URI_PATTERN, code.name().toLowerCase(), date) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> { throw new NoDataException(code, date); })) .body(Rates.class); } private static boolean isWeekend(LocalDate today) { return today.getDayOfWeek() == DayOfWeek.SATURDAY || today.getDayOfWeek() == DayOfWeek.SUNDAY; } private record RatesCache(LocalDate date, BigDecimal buy, BigDecimal sell) { } }