Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.vality.rateboss.client.cbr

import dev.vality.rateboss.client.cbr.model.CbrExchangeRateData
Comment thread
karle0wne marked this conversation as resolved.
import dev.vality.rateboss.config.properties.RatesProperties
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
Expand All @@ -17,12 +16,12 @@ class CbrApiClient(
private val restTemplate: RestTemplate,
private val ratesProperties: RatesProperties,
) {
fun getExchangeRates(time: Instant): CbrExchangeRateData {
fun getExchangeRates(time: Instant): String {
val baseUrl = ratesProperties.source.cbr.rootUrl
val timezone = ratesProperties.source.cbr.timeZone
val date = time.atZone(timezone).toLocalDate()
val url = buildUrl(baseUrl, date)
return restTemplate.exchange<CbrExchangeRateData>(url, HttpMethod.GET, HttpEntity(null, null)).body!!
return restTemplate.exchange<String>(url, HttpMethod.GET, HttpEntity(null, null)).body!!
Comment thread
karle0wne marked this conversation as resolved.
}

private fun buildUrl(
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

13 changes: 13 additions & 0 deletions src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dev.vality.rateboss.job.NbkzExchangeGrabberMasterJob
import org.quartz.*
import org.quartz.impl.JobDetailImpl
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct

Expand All @@ -18,6 +19,9 @@ class JobConfig {
@Autowired
private lateinit var ratesProperties: RatesProperties

@Value("\${rates.run-on-startup:true}")
private var runOnStartup: Boolean = true

@PostConstruct
fun init() {
val fixerJobTriggerName = ratesProperties.fixerJob.jobTriggerName
Expand All @@ -26,20 +30,29 @@ class JobConfig {
if (!schedulerFactoryBean.checkExists(TriggerKey(fixerJobTriggerName))) {
schedulerFactoryBean.scheduleJob(fixerExchangeRateGrabberMasterTrigger())
}
if (runOnStartup) {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.fixerJob.jobKey))
}
}
val cbrJobTriggerName = ratesProperties.cbrJob.jobTriggerName
if (cbrJobTriggerName.isNotEmpty()) {
schedulerFactoryBean.addJob(cbrExchangeRateGrabberMasterJob(), true, true)
if (!schedulerFactoryBean.checkExists(TriggerKey(ratesProperties.cbrJob.jobTriggerName))) {
schedulerFactoryBean.scheduleJob(cbrExchangeRateGrabberMasterTrigger())
}
if (runOnStartup) {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.cbrJob.jobKey))
}
Comment thread
karle0wne marked this conversation as resolved.
}
val nbkzJobTriggerName = ratesProperties.nbkzJob.jobTriggerName
if (nbkzJobTriggerName.isNotEmpty()) {
schedulerFactoryBean.addJob(nbkzExchangeRateGrabberMasterJob(), true, true)
if (!schedulerFactoryBean.checkExists(TriggerKey(ratesProperties.nbkzJob.jobTriggerName))) {
schedulerFactoryBean.scheduleJob(nbkzExchangeRateGrabberMasterTrigger())
}
if (runOnStartup) {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.nbkzJob.jobKey))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class ExRateConverter {
DEFAULT_EXPONENT
}
return ExRate().apply {
destinationCurrencySymbolicCode = baseCurrencySymbolCode
destinationCurrencyExponent = baseCurrencyExponent
sourceCurrencySymbolicCode = exchangeRateMapEntry.key
sourceCurrencyExponent = exponent.toShort()
sourceCurrencySymbolicCode = baseCurrencySymbolCode
sourceCurrencyExponent = baseCurrencyExponent
destinationCurrencySymbolicCode = exchangeRateMapEntry.key
destinationCurrencyExponent = exponent.toShort()
val rational = exchangeRateMapEntry.value.toRational()
rationalP = rational.numerator
rationalQ = rational.denominator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import dev.vality.rateboss.source.ExchangeRateSourceException
import dev.vality.rateboss.source.model.ExchangeRates
import mu.KotlinLogging
import org.springframework.stereotype.Component
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.ByteArrayInputStream
import java.math.BigDecimal
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import javax.xml.parsers.DocumentBuilderFactory

private val log = KotlinLogging.logger {}

Expand All @@ -26,15 +33,21 @@ class CbrExchangeRateSource(
} catch (e: Exception) {
throw ExchangeRateSourceException("Remote client exception", e)
}
if (response.currencies.isNullOrEmpty()) {
val rates =
try {
parseRates(response)
} catch (e: Exception) {
throw ExchangeRateSourceException("Failed to parse response from CbrApi", e)
}
if (rates.isEmpty()) {
throw ExchangeRateSourceException("Unsuccessful response from CbrApi")
}

val rates: Map<String, BigDecimal> =
response.currencies!!.associate {
it.charCode!! to it.value!!.divide(it.nominal!!.toBigDecimal())
val responseDate =
try {
parseDate(response)
} catch (e: Exception) {
throw ExchangeRateSourceException("Failed to parse date from CbrApi", e)
}
val responseDate = response.date!!
val nextDayTimestamp = responseDate.plusDays(1).atStartOfDay().toEpochSecond(ZoneOffset.UTC)
log.info("Exchange rates from cbr have been retrieved, date=$responseDate, exchangeRates=$rates, targetTimestamp=$nextDayTimestamp")
return ExchangeRates(
Expand All @@ -44,4 +57,59 @@ class CbrExchangeRateSource(
}

override fun getSourceId(): String = ExRateSources.CBR

private fun parseRates(xmlContent: String): Map<String, BigDecimal> {
val document = parseXml(xmlContent)
val items = document.getElementsByTagName("Valute")
val rates = mutableMapOf<String, BigDecimal>()
for (i in 0 until items.length) {
val itemNode = items.item(i)
if (itemNode.nodeType != Node.ELEMENT_NODE) {
continue
}
val item = itemNode as Element
val charCode =
item
.getElementsByTagName("CharCode")
.item(0)
?.textContent
?.trim()
val nominalStr =
item
.getElementsByTagName("Nominal")
.item(0)
?.textContent
?.trim()
val valueStr =
item
.getElementsByTagName("Value")
.item(0)
?.textContent
?.trim()
if (charCode.isNullOrBlank() || nominalStr.isNullOrBlank() || valueStr.isNullOrBlank()) {
continue
}
val nominal = nominalStr.toBigDecimal()
val value = valueStr.replace(",", ".").toBigDecimal()
rates[charCode] = value.divide(nominal)
}
return rates
}

private fun parseDate(xmlContent: String): LocalDate {
val document = parseXml(xmlContent)
val root = document.documentElement
val dateAttr = root.getAttribute("Date")
return LocalDate.parse(dateAttr, DATE_FORMATTER)
}

private fun parseXml(xmlContent: String) =
DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(ByteArrayInputStream(xmlContent.toByteArray(StandardCharsets.UTF_8)))

companion object {
private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ContainerConfiguration {
registry.add("spring.flyway.url", postgresql::getJdbcUrl)
registry.add("spring.flyway.user", postgresql::getUsername)
registry.add("spring.flyway.password", postgresql::getPassword)
registry.add("rates.run-on-startup") { "false" }
}
}
}
17 changes: 15 additions & 2 deletions src/test/kotlin/dev/vality/rateboss/client/cbr/CbrApiClientTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.vality.rateboss.client.cbr

import dev.vality.rateboss.config.TestConfig
import dev.vality.rateboss.source.ExchangeRateSource
import dev.vality.rateboss.source.impl.CbrExchangeRateSource
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
Expand All @@ -14,17 +16,28 @@ import java.time.Instant

@Disabled("integration test")
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [CbrApiClient::class])
@ContextConfiguration(classes = [CbrApiClient::class, CbrExchangeRateSource::class])
@Import(TestConfig::class)
class CbrApiClientTest {
@Autowired
lateinit var cbrApiClient: CbrApiClient

@Autowired
lateinit var cbrExchangeRateSource: ExchangeRateSource

@Test
fun getExchangeRates() {
val response = cbrApiClient.getExchangeRates(Instant.now())

assertNotNull(response)
assertTrue(response.currencies!!.isNotEmpty())
assertTrue(response.isNotBlank())
}

@Test
fun getExchangeRatesViaSource() {
val exchangeRates = cbrExchangeRateSource.getExchangeRate("RUB")

assertNotNull(exchangeRates)
assertTrue(exchangeRates.rates.isNotEmpty())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ExchangeDaoServiceTest : ContainerConfiguration() {
val codes =
records
.stream()
.map(ExRateRecord::getSourceCurrencySymbolicCode)
.map(ExRateRecord::getDestinationCurrencySymbolicCode)
.toList()
assertThat(codes, contains("RUB", "AED", "AMD"))
}
Expand Down
Loading
Loading