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
27 changes: 27 additions & 0 deletions src/main/kotlin/dev/vality/rateboss/client/nbaz/NbazApiClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.vality.rateboss.client.nbaz

import dev.vality.rateboss.config.properties.RatesProperties
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Component
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.exchange
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Component
class NbazApiClient(
private val restTemplate: RestTemplate,
private val ratesProperties: RatesProperties,
) {
fun getExchangeRates(date: LocalDate): String {
val url = buildUrl(date)
return restTemplate.exchange<String>(url, HttpMethod.GET, HttpEntity.EMPTY).body!!
}

private fun buildUrl(date: LocalDate): String {
val rootUrl = ratesProperties.source.nbaz.rootUrl
val dateFormat = ratesProperties.source.nbaz.dateFormat
return "${rootUrl}${date.format(DateTimeFormatter.ofPattern(dateFormat))}.xml"
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.vality.rateboss.config
import dev.vality.rateboss.config.properties.RatesProperties
import dev.vality.rateboss.job.CbrExchangeGrabberMasterJob
import dev.vality.rateboss.job.FixerExchangeGrabberMasterJob
import dev.vality.rateboss.job.NbazExchangeGrabberMasterJob
import dev.vality.rateboss.job.NbkrExchangeGrabberMasterJob
import dev.vality.rateboss.job.NbkzExchangeGrabberMasterJob
import dev.vality.rateboss.job.NbuzExchangeGrabberMasterJob
Expand Down Expand Up @@ -76,6 +77,16 @@ class JobConfig {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.nbuzJob.jobKey))
}
}
val nbazJobTriggerName = ratesProperties.nbazJob.jobTriggerName
if (nbazJobTriggerName.isNotEmpty()) {
schedulerFactoryBean.addJob(nbazExchangeRateGrabberMasterJob(), true, true)
if (!schedulerFactoryBean.checkExists(TriggerKey(ratesProperties.nbazJob.jobTriggerName))) {
schedulerFactoryBean.scheduleJob(nbazExchangeRateGrabberMasterTrigger())
}
if (runOnStartup) {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.nbazJob.jobKey))
}
}
}

fun fixerExchangeRateGrabberMasterJob(): JobDetailImpl {
Expand Down Expand Up @@ -152,4 +163,19 @@ class JobConfig {
.withIdentity(ratesProperties.nbuzJob.jobTriggerName)
.withSchedule(CronScheduleBuilder.cronSchedule(ratesProperties.nbuzJob.jobCron))
.build()

fun nbazExchangeRateGrabberMasterJob(): JobDetailImpl {
val jobDetail = JobDetailImpl()
jobDetail.key = JobKey(ratesProperties.nbazJob.jobKey)
jobDetail.jobClass = NbazExchangeGrabberMasterJob::class.java
return jobDetail
}

fun nbazExchangeRateGrabberMasterTrigger(): CronTrigger =
TriggerBuilder
.newTrigger()
.forJob(nbazExchangeRateGrabberMasterJob())
.withIdentity(ratesProperties.nbazJob.jobTriggerName)
.withSchedule(CronScheduleBuilder.cronSchedule(ratesProperties.nbazJob.jobCron))
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class RatesProperties(
val nbkzJob: JobDescription,
val nbkrJob: JobDescription,
val nbuzJob: JobDescription,
val nbazJob: JobDescription,
val source: RatesSourceProperties,
)

Expand All @@ -33,6 +34,7 @@ data class RatesSourceProperties(
val nbkz: NbkzProperties,
val nbkr: NbkrProperties,
val nbuz: NbuzProperties,
val nbaz: NbazProperties,
)

data class FixerProperties(
Expand Down Expand Up @@ -61,3 +63,9 @@ data class NbuzProperties(
val locale: String,
val timeZone: ZoneId,
)

data class NbazProperties(
val rootUrl: String,
val dateFormat: String,
val timeZone: ZoneId,
)
37 changes: 37 additions & 0 deletions src/main/kotlin/dev/vality/rateboss/job/NbazExchangeGrabberJob.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dev.vality.rateboss.job

import dev.vality.rateboss.extensions.getApplicationContext
import dev.vality.rateboss.source.ExchangeRateSource
import dev.vality.rateboss.source.ExchangeRateSourceException
import dev.vality.rateboss.source.impl.NbazExchangeRateSource
import dev.vality.rateboss.source.model.ExchangeRates
import mu.KotlinLogging
import org.quartz.JobExecutionContext
import org.springframework.context.ApplicationContext
import org.springframework.retry.support.RetryTemplate

private val log = KotlinLogging.logger {}

class NbazExchangeGrabberJob : AbstractExchangeGrabberJob() {
override fun executeInternal(context: JobExecutionContext) {
val applicationContext = context.getApplicationContext()
val currencySymbolCode = context.jobDetail.jobDataMap["currencySymbolCode"] as String
val currencyExponent = context.jobDetail.jobDataMap["currencyExponent"] as Int
val exchangeRateSource = applicationContext.getBean(NbazExchangeRateSource::class.java)
val sourceId = exchangeRateSource.getSourceId()
log.info { "Process NbazExchangeGrabberJob for $sourceId" }
val exchangeRates = getExchangeRates(applicationContext, exchangeRateSource, currencySymbolCode)
saveExchangeRates(applicationContext, currencySymbolCode, currencyExponent, exchangeRates, sourceId)
}

private fun getExchangeRates(
applicationContext: ApplicationContext,
exchangeRateSource: ExchangeRateSource,
currencySymbolCode: String,
): ExchangeRates {
val retryTemplate = applicationContext.getBean(RetryTemplate::class.java)
return retryTemplate.execute<ExchangeRates, ExchangeRateSourceException> {
exchangeRateSource.getExchangeRate(currencySymbolCode)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.vality.rateboss.job

import dev.vality.rateboss.config.properties.RatesProperties
import dev.vality.rateboss.extensions.getApplicationContext
import org.quartz.JobExecutionContext
import org.quartz.Scheduler

class NbazExchangeGrabberMasterJob : AbstractExchangeGrabberMasterJob() {
override fun executeInternal(context: JobExecutionContext) {
val applicationContext = context.getApplicationContext()
val ratesProperties = applicationContext.getBean(RatesProperties::class.java)
val currencies = ratesProperties.nbazJob.currencies
val schedulerFactoryBean = applicationContext.getBean(Scheduler::class.java)
launchJob(currencies, schedulerFactoryBean, NbazExchangeGrabberJob::class.java, getJobName())
}

override fun getJobName(): String = "nbazJob"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ object ExRateSources {
const val NBKZ = "nbkz"
const val NBKR = "nbkr"
const val NBUZ = "nbuz"
const val NBAZ = "nbaz"
}
35 changes: 35 additions & 0 deletions src/main/kotlin/dev/vality/rateboss/model/NbazDailyRatesXml.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.vality.rateboss.model

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement

@JacksonXmlRootElement(localName = "ValCurs")
@JsonIgnoreProperties(ignoreUnknown = true)
data class NbazDailyRatesXml(
@field:JacksonXmlProperty(isAttribute = true, localName = "Date")
val date: String? = null,
@field:JacksonXmlElementWrapper(useWrapping = false)
@field:JacksonXmlProperty(localName = "ValType")
val valTypes: List<NbazValTypeXml>? = null,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class NbazValTypeXml(
@field:JacksonXmlProperty(isAttribute = true, localName = "Type")
val type: String? = null,
@field:JacksonXmlElementWrapper(useWrapping = false)
@field:JacksonXmlProperty(localName = "Valute")
val valutes: List<NbazValuteXml>? = null,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class NbazValuteXml(
@field:JacksonXmlProperty(isAttribute = true, localName = "Code")
val code: String? = null,
@field:JacksonXmlProperty(localName = "Nominal")
val nominal: String? = null,
@field:JacksonXmlProperty(localName = "Value")
val value: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package dev.vality.rateboss.source.impl

import com.fasterxml.jackson.dataformat.xml.XmlMapper
import dev.vality.rateboss.client.nbaz.NbazApiClient
import dev.vality.rateboss.config.properties.RatesProperties
import dev.vality.rateboss.job.constant.ExRateSources
import dev.vality.rateboss.model.NbazDailyRatesXml
import dev.vality.rateboss.source.ExchangeRateSource
import dev.vality.rateboss.source.ExchangeRateSourceException
import dev.vality.rateboss.source.model.ExchangeRates
import mu.KotlinLogging
import org.springframework.stereotype.Component
import java.math.BigDecimal
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

private val log = KotlinLogging.logger {}

@Component
class NbazExchangeRateSource(
private val nbazApiClient: NbazApiClient,
private val ratesProperties: RatesProperties,
private val xmlMapper: XmlMapper,
) : ExchangeRateSource {
override fun getExchangeRate(currencySymbolCode: String): ExchangeRates {
val timeZone = ratesProperties.source.nbaz.timeZone
val date = LocalDate.now(timeZone)
log.info { "Trying to get exchange rates from nbaz for currency=$currencySymbolCode, date=$date" }
val parsed = fetchDailyRates(date)
val rates = buildRatesMap(parsed)
if (rates.isEmpty()) {
throw ExchangeRateSourceException("Unsuccessful response from NbazApi")
}
val dayTimestamp = date.atStartOfDay().toEpochSecond(ZoneOffset.UTC)
Comment thread
ggmaleva marked this conversation as resolved.
log.info {
"Exchange rates from nbaz have been retrieved, date=$date, " +
"exchangeRates=$rates, targetTimestamp=$dayTimestamp"
}
return ExchangeRates(
rates = rates,
timestamp = dayTimestamp,
)
}

override fun getSourceId(): String = ExRateSources.NBAZ

private fun fetchDailyRates(date: LocalDate): NbazDailyRatesXml =
try {
xmlMapper.readValue(nbazApiClient.getExchangeRates(date), NbazDailyRatesXml::class.java)
} catch (e: Exception) {
throw ExchangeRateSourceException("Failed to get daily rates", e)
}

private fun buildRatesMap(root: NbazDailyRatesXml): Map<String, BigDecimal> {
val rates = mutableMapOf<String, BigDecimal>()
val currencies =
root.valTypes
.orEmpty()
.firstOrNull { it.type?.trim { ch -> ch <= ' ' } == FOREIGN_CURRENCIES_TYPE }
?.valutes
.orEmpty()
for (item in currencies) {
val code = item.code?.trim { it <= ' ' }.orEmpty()
val nominal = item.nominal?.extractDecimal()?.toBigDecimalOrNull()
val value = item.value?.extractDecimal()?.toBigDecimalOrNull()
if (code.isBlank() || nominal == null || value == null || nominal.compareTo(BigDecimal.ZERO) == 0) {
log.debug { "Skip malformed NBAZ currency record: $item" }
continue
}
rates[code] = value.divide(nominal)
}
return rates
}

private fun String.extractDecimal(): String =
trim()
.replace(",", ".")
.substringBefore(' ')

companion object {
private const val FOREIGN_CURRENCIES_TYPE = "Xarici valyutalar"
private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
}
}
11 changes: 11 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ rates:
currencies:
- symbolCode: "UZS"
exponent: 2
nbazJob:
jobCron: '0 0 0/1 * * ?'
jobKey: 'nbaz-exchange-rate-grabber-master-job'
jobTriggerName: 'nbaz-exchange-rate-grabber-master-job-trigger'
currencies:
- symbolCode: "AZN"
exponent: 2
source:
fixer:
rootUrl: https://api.apilayer.com/fixer/
Expand All @@ -129,3 +136,7 @@ rates:
rootUrl: https://nbu.uz/api/collections/individuals_exchange_rates/entries
locale: ru
timeZone: Asia/Tashkent
nbaz:
rootUrl: https://www.cbar.az/currencies/
dateFormat: dd.MM.yyyy
timeZone: Asia/Baku
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.vality.rateboss.client.nbaz

import dev.vality.rateboss.config.TestConfig
import dev.vality.rateboss.source.ExchangeRateSource
import dev.vality.rateboss.source.impl.NbazExchangeRateSource
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Import
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.time.LocalDate

@Disabled("integration test")
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [NbazApiClient::class, NbazExchangeRateSource::class])
@Import(TestConfig::class)
class NbazApiClientTest {
@Autowired
lateinit var nbazApiClient: NbazApiClient

@Autowired
lateinit var nbazExchangeRateSource: ExchangeRateSource

@Test
fun getExchangeRates() {
val response = nbazApiClient.getExchangeRates(LocalDate.now())

assertNotNull(response)
assertTrue(response.isNotBlank())
}

@Test
fun getExchangeRatesViaSource() {
val exchangeRates = nbazExchangeRateSource.getExchangeRate("AZN")

assertNotNull(exchangeRates)
assertTrue(exchangeRates.rates.isNotEmpty())
}
}
11 changes: 11 additions & 0 deletions src/test/kotlin/dev/vality/rateboss/config/TestConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ class TestConfig {
"nbuz-name",
listOf(CurrencyProperties("UZS", 2)),
),
JobDescription(
"nbaz-cron",
"nbaz-key",
"nbaz-name",
listOf(CurrencyProperties("AZN", 2)),
),
RatesSourceProperties(
FixerProperties("url", "key"),
CbrProperties("https://www.cbr.ru/scripts/XML_daily.asp", ZoneId.of("Europe/Moscow")),
Expand All @@ -69,6 +75,11 @@ class TestConfig {
"ru",
ZoneId.of("Asia/Tashkent"),
),
NbazProperties(
"https://www.cbar.az/currencies/",
"dd.MM.yyyy",
ZoneId.of("Asia/Baku"),
),
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class CbrExchangeGrabberJobTest : ContainerConfiguration() {
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkzJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkrJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbuzJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbazJob.jobTriggerName))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class FixerExchangeGrabberJobTest : ContainerConfiguration() {
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkzJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkrJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbuzJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.nbazJob.jobTriggerName))
}

@Test
Expand Down
Loading
Loading