diff --git a/pom.xml b/pom.xml
index d134209..9a57e3d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
dev.vality
service-parent-pom
- 3.1.4
+ 3.1.7
rate-boss
0.0.1-SNAPSHOT
@@ -16,7 +16,7 @@
8023
21
2.1.21
- 1.17.3
+ 1.21.4
${server.port} ${management.port}
5432
postgres
@@ -57,6 +57,10 @@
com.fasterxml.jackson.module
jackson-module-kotlin
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
org.jetbrains.kotlin
kotlin-reflect
diff --git a/src/main/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClient.kt b/src/main/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClient.kt
new file mode 100644
index 0000000..87a50cb
--- /dev/null
+++ b/src/main/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClient.kt
@@ -0,0 +1,31 @@
+package dev.vality.rateboss.client.nbkr
+
+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 java.nio.charset.Charset
+
+@Component
+class NbkrApiClient(
+ private val restTemplate: RestTemplate,
+ private val ratesProperties: RatesProperties,
+) {
+ fun getExchangeRates(): String {
+ val url = ratesProperties.source.nbkr.rootUrl
+ val bytes =
+ restTemplate
+ .exchange(
+ url,
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ ByteArray::class.java,
+ ).body ?: byteArrayOf()
+ return String(bytes, NBKR_XML_CHARSET)
+ }
+
+ companion object {
+ private val NBKR_XML_CHARSET: Charset = Charset.forName("Windows-1251")
+ }
+}
diff --git a/src/main/kotlin/dev/vality/rateboss/config/AppConfig.kt b/src/main/kotlin/dev/vality/rateboss/config/AppConfig.kt
index bed1d29..50c19fa 100644
--- a/src/main/kotlin/dev/vality/rateboss/config/AppConfig.kt
+++ b/src/main/kotlin/dev/vality/rateboss/config/AppConfig.kt
@@ -1,5 +1,8 @@
package dev.vality.rateboss.config
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.dataformat.xml.XmlMapper
+import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -13,6 +16,14 @@ class AppConfig {
@Bean
fun restTemplate() = RestTemplate()
+ @Bean
+ fun nbkrXmlMapper(): XmlMapper =
+ XmlMapper
+ .builder()
+ .addModule(kotlinModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .build()
+
@Bean
fun retryTemplate(
@Value("\${retryTemplate.backOffPeriod}") backOffPeriod: Long,
diff --git a/src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt b/src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt
index 5dc65c6..8db168d 100644
--- a/src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt
+++ b/src/main/kotlin/dev/vality/rateboss/config/JobConfig.kt
@@ -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.NbkrExchangeGrabberMasterJob
import dev.vality.rateboss.job.NbkzExchangeGrabberMasterJob
import org.quartz.*
import org.quartz.impl.JobDetailImpl
@@ -54,6 +55,16 @@ class JobConfig {
schedulerFactoryBean.triggerJob(JobKey(ratesProperties.nbkzJob.jobKey))
}
}
+ val nbkrJobTriggerName = ratesProperties.nbkrJob.jobTriggerName
+ if (nbkrJobTriggerName.isNotEmpty()) {
+ schedulerFactoryBean.addJob(nbkrExchangeRateGrabberMasterJob(), true, true)
+ if (!schedulerFactoryBean.checkExists(TriggerKey(ratesProperties.nbkrJob.jobTriggerName))) {
+ schedulerFactoryBean.scheduleJob(nbkrExchangeRateGrabberMasterTrigger())
+ }
+ if (runOnStartup) {
+ schedulerFactoryBean.triggerJob(JobKey(ratesProperties.nbkrJob.jobKey))
+ }
+ }
}
fun fixerExchangeRateGrabberMasterJob(): JobDetailImpl {
@@ -100,4 +111,19 @@ class JobConfig {
.withIdentity(ratesProperties.nbkzJob.jobTriggerName)
.withSchedule(CronScheduleBuilder.cronSchedule(ratesProperties.nbkzJob.jobCron))
.build()
+
+ fun nbkrExchangeRateGrabberMasterJob(): JobDetailImpl {
+ val jobDetail = JobDetailImpl()
+ jobDetail.key = JobKey(ratesProperties.nbkrJob.jobKey)
+ jobDetail.jobClass = NbkrExchangeGrabberMasterJob::class.java
+ return jobDetail
+ }
+
+ fun nbkrExchangeRateGrabberMasterTrigger(): CronTrigger =
+ TriggerBuilder
+ .newTrigger()
+ .forJob(nbkrExchangeRateGrabberMasterJob())
+ .withIdentity(ratesProperties.nbkrJob.jobTriggerName)
+ .withSchedule(CronScheduleBuilder.cronSchedule(ratesProperties.nbkrJob.jobCron))
+ .build()
}
diff --git a/src/main/kotlin/dev/vality/rateboss/config/properties/RatesProperties.kt b/src/main/kotlin/dev/vality/rateboss/config/properties/RatesProperties.kt
index 1030d3f..95e9bab 100644
--- a/src/main/kotlin/dev/vality/rateboss/config/properties/RatesProperties.kt
+++ b/src/main/kotlin/dev/vality/rateboss/config/properties/RatesProperties.kt
@@ -10,6 +10,7 @@ data class RatesProperties(
val fixerJob: JobDescription,
val cbrJob: JobDescription,
val nbkzJob: JobDescription,
+ val nbkrJob: JobDescription,
val source: RatesSourceProperties,
)
@@ -29,6 +30,7 @@ data class RatesSourceProperties(
val fixer: FixerProperties,
val cbr: CbrProperties,
val nbkz: NbkzProperties,
+ val nbkr: NbkrProperties,
)
data class FixerProperties(
@@ -46,3 +48,8 @@ data class NbkzProperties(
val dateFormat: String,
val timeZone: ZoneId,
)
+
+data class NbkrProperties(
+ val rootUrl: String,
+ val timeZone: ZoneId,
+)
diff --git a/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJob.kt b/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJob.kt
new file mode 100644
index 0000000..5e0b4be
--- /dev/null
+++ b/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJob.kt
@@ -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.NbkrExchangeRateSource
+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 NbkrExchangeGrabberJob : 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(NbkrExchangeRateSource::class.java)
+ val sourceId = exchangeRateSource.getSourceId()
+ log.info { "Process NbkrExchangeGrabberJob 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 {
+ exchangeRateSource.getExchangeRate(currencySymbolCode)
+ }
+ }
+}
diff --git a/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberMasterJob.kt b/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberMasterJob.kt
new file mode 100644
index 0000000..7125485
--- /dev/null
+++ b/src/main/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberMasterJob.kt
@@ -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 NbkrExchangeGrabberMasterJob : AbstractExchangeGrabberMasterJob() {
+ override fun executeInternal(context: JobExecutionContext) {
+ val applicationContext = context.getApplicationContext()
+ val ratesProperties = applicationContext.getBean(RatesProperties::class.java)
+ val currencies = ratesProperties.nbkrJob.currencies
+ val schedulerFactoryBean = applicationContext.getBean(Scheduler::class.java)
+ launchJob(currencies, schedulerFactoryBean, NbkrExchangeGrabberJob::class.java, getJobName())
+ }
+
+ override fun getJobName(): String = "nbkrJob"
+}
diff --git a/src/main/kotlin/dev/vality/rateboss/job/constant/ExRateSources.kt b/src/main/kotlin/dev/vality/rateboss/job/constant/ExRateSources.kt
index b9c6915..19968ae 100644
--- a/src/main/kotlin/dev/vality/rateboss/job/constant/ExRateSources.kt
+++ b/src/main/kotlin/dev/vality/rateboss/job/constant/ExRateSources.kt
@@ -4,4 +4,5 @@ object ExRateSources {
const val FIXER = "fixer"
const val CBR = "cbr"
const val NBKZ = "nbkz"
+ const val NBKR = "nbkr"
}
diff --git a/src/main/kotlin/dev/vality/rateboss/model/NbkrDailyRatesXml.kt b/src/main/kotlin/dev/vality/rateboss/model/NbkrDailyRatesXml.kt
new file mode 100644
index 0000000..e6a81ca
--- /dev/null
+++ b/src/main/kotlin/dev/vality/rateboss/model/NbkrDailyRatesXml.kt
@@ -0,0 +1,26 @@
+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 = "CurrencyRates")
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class NbkrDailyRatesXml(
+ @field:JacksonXmlProperty(isAttribute = true, localName = "Date")
+ val date: String? = null,
+ @field:JacksonXmlElementWrapper(useWrapping = false)
+ @field:JacksonXmlProperty(localName = "Currency")
+ val currencies: List? = null,
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class NbkrCurrencyXml(
+ @field:JacksonXmlProperty(isAttribute = true, localName = "ISOCode")
+ val isoCode: String? = null,
+ @field:JacksonXmlProperty(localName = "Nominal")
+ val nominal: String? = null,
+ @field:JacksonXmlProperty(localName = "Value")
+ val value: String? = null,
+)
diff --git a/src/main/kotlin/dev/vality/rateboss/source/impl/NbkrExchangeRateSource.kt b/src/main/kotlin/dev/vality/rateboss/source/impl/NbkrExchangeRateSource.kt
new file mode 100644
index 0000000..a0ab43c
--- /dev/null
+++ b/src/main/kotlin/dev/vality/rateboss/source/impl/NbkrExchangeRateSource.kt
@@ -0,0 +1,89 @@
+package dev.vality.rateboss.source.impl
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper
+import dev.vality.rateboss.client.nbkr.NbkrApiClient
+import dev.vality.rateboss.config.properties.RatesProperties
+import dev.vality.rateboss.job.constant.ExRateSources
+import dev.vality.rateboss.model.NbkrDailyRatesXml
+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 NbkrExchangeRateSource(
+ private val nbkrApiClient: NbkrApiClient,
+ private val ratesProperties: RatesProperties,
+ private val xmlMapper: XmlMapper,
+) : ExchangeRateSource {
+ override fun getExchangeRate(currencySymbolCode: String): ExchangeRates {
+ val timeZone = ratesProperties.source.nbkr.timeZone
+ val date = LocalDate.now(timeZone)
+ log.info { "Trying to get exchange rates from nbkr for currency=$currencySymbolCode, date=$date" }
+ val parsed = fetchDailyRates()
+ val rates = buildRatesMap(parsed)
+ if (rates.isEmpty()) {
+ throw ExchangeRateSourceException("Unsuccessful response from NbkrApi")
+ }
+ val responseDate =
+ try {
+ parseDate(parsed)
+ } catch (e: Exception) {
+ throw ExchangeRateSourceException("Failed to parse date from NbkrApi", e)
+ }
+ val nextDayTimestamp = responseDate.atStartOfDay().toEpochSecond(ZoneOffset.UTC)
+ log.info {
+ "Exchange rates from nbkr have been retrieved, date=$responseDate, " +
+ "exchangeRates=$rates, targetTimestamp=$nextDayTimestamp"
+ }
+ return ExchangeRates(
+ rates = rates,
+ timestamp = nextDayTimestamp,
+ )
+ }
+
+ override fun getSourceId(): String = ExRateSources.NBKR
+
+ private fun fetchDailyRates(): NbkrDailyRatesXml =
+ try {
+ xmlMapper.readValue(nbkrApiClient.getExchangeRates(), NbkrDailyRatesXml::class.java)
+ } catch (e: Exception) {
+ throw ExchangeRateSourceException("Failed to get daily rates", e)
+ }
+
+ private fun buildRatesMap(root: NbkrDailyRatesXml): Map {
+ val rates = mutableMapOf()
+ for (item in root.currencies.orEmpty()) {
+ val isoCode = item.isoCode?.trim { it <= ' ' }.orEmpty()
+ val nominal = item.nominal?.trim { it <= ' ' }?.toBigDecimalOrNull()
+ val value =
+ item.value
+ ?.trim { it <= ' ' }
+ ?.replace(",", ".")
+ ?.toBigDecimalOrNull()
+ if (isoCode.isBlank() || nominal == null || value == null || nominal.compareTo(BigDecimal.ZERO) == 0) {
+ log.debug { "Skip malformed NBKR currency record: $item" }
+ continue
+ }
+ rates[isoCode] = value.divide(nominal)
+ }
+ return rates
+ }
+
+ private fun parseDate(root: NbkrDailyRatesXml): LocalDate {
+ val dateAttr = root.date?.trim { it <= ' ' }.orEmpty()
+ require(dateAttr.isNotBlank()) { "Missing Date on CurrencyRates" }
+ return LocalDate.parse(dateAttr, DATE_FORMATTER)
+ }
+
+ companion object {
+ private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 1248cd3..536a63a 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -97,6 +97,13 @@ rates:
currencies:
- symbolCode: "KZT"
exponent: 2
+ nbkrJob:
+ jobCron: '0 0 0/1 * * ?'
+ jobKey: 'nbkr-exchange-rate-grabber-master-job'
+ jobTriggerName: 'nbkr-exchange-rate-grabber-master-job-trigger'
+ currencies:
+ - symbolCode: "KGS"
+ exponent: 2
source:
fixer:
rootUrl: https://api.apilayer.com/fixer/
@@ -108,3 +115,6 @@ rates:
rootUrl: https://nationalbank.kz/rss/get_rates.cfm
dateFormat: dd.MM.yyyy
timeZone: Asia/Almaty
+ nbkr:
+ rootUrl: https://www.nbkr.kg/XML/daily.xml
+ timeZone: Asia/Bishkek
diff --git a/src/test/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClientTest.kt b/src/test/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClientTest.kt
new file mode 100644
index 0000000..2ed1a69
--- /dev/null
+++ b/src/test/kotlin/dev/vality/rateboss/client/nbkr/NbkrApiClientTest.kt
@@ -0,0 +1,42 @@
+package dev.vality.rateboss.client.nbkr
+
+import dev.vality.rateboss.config.TestConfig
+import dev.vality.rateboss.source.ExchangeRateSource
+import dev.vality.rateboss.source.impl.NbkrExchangeRateSource
+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
+
+@Disabled("integration test")
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [NbkrApiClient::class, NbkrExchangeRateSource::class])
+@Import(TestConfig::class)
+class NbkrApiClientTest {
+ @Autowired
+ lateinit var nbkrApiClient: NbkrApiClient
+
+ @Autowired
+ lateinit var nbkrExchangeRateSource: ExchangeRateSource
+
+ @Test
+ fun getExchangeRates() {
+ val response = nbkrApiClient.getExchangeRates()
+
+ assertNotNull(response)
+ assertTrue(response.isNotBlank())
+ }
+
+ @Test
+ fun getExchangeRatesViaSource() {
+ val exchangeRates = nbkrExchangeRateSource.getExchangeRate("KGS")
+
+ assertNotNull(exchangeRates)
+ assertTrue(exchangeRates.rates.isNotEmpty())
+ }
+}
diff --git a/src/test/kotlin/dev/vality/rateboss/config/TestConfig.kt b/src/test/kotlin/dev/vality/rateboss/config/TestConfig.kt
index 691e729..6e38747 100644
--- a/src/test/kotlin/dev/vality/rateboss/config/TestConfig.kt
+++ b/src/test/kotlin/dev/vality/rateboss/config/TestConfig.kt
@@ -1,5 +1,8 @@
package dev.vality.rateboss.config
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.dataformat.xml.XmlMapper
+import com.fasterxml.jackson.module.kotlin.kotlinModule
import dev.vality.rateboss.config.properties.*
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
@@ -11,6 +14,14 @@ class TestConfig {
@Bean
fun testRestTemplate() = RestTemplate()
+ @Bean
+ fun nbkrXmlMapper(): XmlMapper =
+ XmlMapper
+ .builder()
+ .addModule(kotlinModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .build()
+
@Bean
fun testRatesProperties(): RatesProperties =
RatesProperties(
@@ -32,10 +43,17 @@ class TestConfig {
"nbkz-name",
listOf(CurrencyProperties("KZT", 2)),
),
+ JobDescription(
+ "nbkr-cron",
+ "nbkr-key",
+ "nbkr-name",
+ listOf(CurrencyProperties("KGS", 2)),
+ ),
RatesSourceProperties(
FixerProperties("url", "key"),
CbrProperties("https://www.cbr.ru/scripts/XML_daily.asp", ZoneId.of("Europe/Moscow")),
NbkzProperties("https://nationalbank.kz/rss/get_rates.cfm", "dd.MM.yyyy", ZoneId.of("Asia/Almaty")),
+ NbkrProperties("https://www.nbkr.kg/XML/daily.xml", ZoneId.of("Asia/Bishkek")),
),
)
}
diff --git a/src/test/kotlin/dev/vality/rateboss/job/CbrExchangeGrabberJobTest.kt b/src/test/kotlin/dev/vality/rateboss/job/CbrExchangeGrabberJobTest.kt
index df34df5..a23b52a 100644
--- a/src/test/kotlin/dev/vality/rateboss/job/CbrExchangeGrabberJobTest.kt
+++ b/src/test/kotlin/dev/vality/rateboss/job/CbrExchangeGrabberJobTest.kt
@@ -12,6 +12,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import org.quartz.JobKey
import org.quartz.Scheduler
import org.quartz.TriggerKey
import org.springframework.beans.factory.annotation.Autowired
@@ -47,6 +48,8 @@ class CbrExchangeGrabberJobTest : ContainerConfiguration() {
@BeforeEach
fun setUp() {
scheduler.unscheduleJob(TriggerKey(ratesProperties.fixerJob.jobTriggerName))
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkzJob.jobTriggerName))
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkrJob.jobTriggerName))
}
@Test
@@ -62,7 +65,8 @@ class CbrExchangeGrabberJobTest : ContainerConfiguration() {
timestamp = Instant.now().epochSecond,
)
}
- await().atMost(5, TimeUnit.SECONDS).untilAsserted {
+ scheduler.triggerJob(JobKey(ratesProperties.cbrJob.jobKey))
+ await().atMost(30, TimeUnit.SECONDS).untilAsserted {
verify(exchangeDaoService, atLeastOnce()).saveExchangeRates(any())
}
}
diff --git a/src/test/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJobTest.kt b/src/test/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJobTest.kt
new file mode 100644
index 0000000..c15fc17
--- /dev/null
+++ b/src/test/kotlin/dev/vality/rateboss/job/NbkrExchangeGrabberJobTest.kt
@@ -0,0 +1,73 @@
+package dev.vality.rateboss.job
+
+import dev.vality.rateboss.ContainerConfiguration
+import dev.vality.rateboss.config.properties.RatesProperties
+import dev.vality.rateboss.service.ExchangeDaoService
+import dev.vality.rateboss.source.impl.NbkrExchangeRateSource
+import dev.vality.rateboss.source.model.ExchangeRates
+import org.awaitility.Awaitility.await
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.quartz.JobKey
+import org.quartz.Scheduler
+import org.quartz.TriggerKey
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.bean.override.mockito.MockitoBean
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean
+import java.math.BigDecimal
+import java.time.Instant
+import java.util.concurrent.TimeUnit
+
+@SpringBootTest(
+ properties = [
+ "rates.nbkr-job.jobCron=0/5 * * * * ?",
+ "rates.nbkr-job.currencies.[0].symbolCode=KGS",
+ "rates.nbkr-job.currencies.[0].exponent=2",
+ ],
+)
+class NbkrExchangeGrabberJobTest : ContainerConfiguration() {
+ @MockitoSpyBean
+ lateinit var exchangeDaoService: ExchangeDaoService
+
+ @MockitoBean
+ lateinit var nbkrExchangeRateSource: NbkrExchangeRateSource
+
+ @Autowired
+ lateinit var scheduler: Scheduler
+
+ @Autowired
+ @Qualifier("rates-dev.vality.rateboss.config.properties.RatesProperties")
+ lateinit var ratesProperties: RatesProperties
+
+ @BeforeEach
+ fun setUp() {
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.fixerJob.jobTriggerName))
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.cbrJob.jobTriggerName))
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkzJob.jobTriggerName))
+ }
+
+ @Test
+ fun `test grabber job`() {
+ whenever(nbkrExchangeRateSource.getSourceId()).thenReturn("sourceId")
+ whenever(nbkrExchangeRateSource.getExchangeRate(any())).then {
+ ExchangeRates(
+ rates =
+ mapOf(
+ "USD" to BigDecimal.valueOf(87.42),
+ "EUR" to BigDecimal.valueOf(102.10),
+ ),
+ timestamp = Instant.now().epochSecond,
+ )
+ }
+ scheduler.triggerJob(JobKey(ratesProperties.nbkrJob.jobKey))
+ await().atMost(30, TimeUnit.SECONDS).untilAsserted {
+ verify(exchangeDaoService, atLeastOnce()).saveExchangeRates(any())
+ }
+ }
+}
diff --git a/src/test/kotlin/dev/vality/rateboss/job/NbkzExchangeGrabberJobTest.kt b/src/test/kotlin/dev/vality/rateboss/job/NbkzExchangeGrabberJobTest.kt
index 1a60e41..c70696b 100644
--- a/src/test/kotlin/dev/vality/rateboss/job/NbkzExchangeGrabberJobTest.kt
+++ b/src/test/kotlin/dev/vality/rateboss/job/NbkzExchangeGrabberJobTest.kt
@@ -12,6 +12,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import org.quartz.JobKey
import org.quartz.Scheduler
import org.quartz.TriggerKey
import org.springframework.beans.factory.annotation.Autowired
@@ -48,6 +49,7 @@ class NbkzExchangeGrabberJobTest : ContainerConfiguration() {
fun setUp() {
scheduler.unscheduleJob(TriggerKey(ratesProperties.fixerJob.jobTriggerName))
scheduler.unscheduleJob(TriggerKey(ratesProperties.cbrJob.jobTriggerName))
+ scheduler.unscheduleJob(TriggerKey(ratesProperties.nbkrJob.jobTriggerName))
}
@Test
@@ -63,7 +65,8 @@ class NbkzExchangeGrabberJobTest : ContainerConfiguration() {
timestamp = Instant.now().epochSecond,
)
}
- await().atMost(5, TimeUnit.SECONDS).untilAsserted {
+ scheduler.triggerJob(JobKey(ratesProperties.nbkzJob.jobKey))
+ await().atMost(30, TimeUnit.SECONDS).untilAsserted {
verify(exchangeDaoService, atLeastOnce()).saveExchangeRates(any())
}
}
diff --git a/src/test/kotlin/dev/vality/rateboss/source/NbkrExchangeRateSourceTest.kt b/src/test/kotlin/dev/vality/rateboss/source/NbkrExchangeRateSourceTest.kt
new file mode 100644
index 0000000..f44e32c
--- /dev/null
+++ b/src/test/kotlin/dev/vality/rateboss/source/NbkrExchangeRateSourceTest.kt
@@ -0,0 +1,114 @@
+package dev.vality.rateboss.source
+
+import dev.vality.rateboss.client.nbkr.NbkrApiClient
+import dev.vality.rateboss.config.TestConfig
+import dev.vality.rateboss.source.impl.NbkrExchangeRateSource
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.kotlin.whenever
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.bean.override.mockito.MockitoBean
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.web.client.ResourceAccessException
+import java.math.BigDecimal
+
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [NbkrApiClient::class, NbkrExchangeRateSource::class])
+@Import(TestConfig::class)
+class NbkrExchangeRateSourceTest {
+ @Autowired
+ lateinit var exchangeRateSource: ExchangeRateSource
+
+ @MockitoBean
+ lateinit var nbkrApiClient: NbkrApiClient
+
+ @Test
+ fun getFailedExchangeRate() {
+ val currencySymbolCode = "KGS"
+ whenever(nbkrApiClient.getExchangeRates()).thenThrow(ResourceAccessException("Error"))
+
+ val exception =
+ org.junit.jupiter.api.assertThrows {
+ exchangeRateSource.getExchangeRate(currencySymbolCode)
+ }
+
+ assertEquals("Failed to get daily rates", exception.message)
+ }
+
+ @Test
+ fun getEmptyExchangeRate() {
+ val currencySymbolCode = "KGS"
+ whenever(nbkrApiClient.getExchangeRates()).thenReturn(
+ """
+
+
+
+ """.trimIndent(),
+ )
+
+ val exception =
+ org.junit.jupiter.api.assertThrows {
+ exchangeRateSource.getExchangeRate(currencySymbolCode)
+ }
+
+ assertEquals("Unsuccessful response from NbkrApi", exception.message)
+ }
+
+ @Test
+ fun getSuccessExchangeRate() {
+ val currencySymbolCode = "KGS"
+ whenever(nbkrApiClient.getExchangeRates()).thenReturn(
+ """
+
+
+
+ 1
+ 87,4205
+
+
+ 10
+ 1021,028
+
+
+ """.trimIndent(),
+ )
+
+ val exchangeRate = exchangeRateSource.getExchangeRate(currencySymbolCode)
+
+ assertNotNull(exchangeRate)
+ assertTrue(exchangeRate.rates.isNotEmpty())
+ assertTrue(exchangeRate.rates.containsKey("USD"))
+ assertEquals(BigDecimal("87.4205"), exchangeRate.rates["USD"])
+ assertEquals(BigDecimal("102.1028"), exchangeRate.rates["EUR"])
+ }
+
+ @Test
+ fun getSuccessExchangeRateWithMalformedCurrencyRecord() {
+ val currencySymbolCode = "KGS"
+ whenever(nbkrApiClient.getExchangeRates()).thenReturn(
+ """
+
+
+
+ 1
+ 87,4205
+
+
+ 0
+ 100,0000
+
+
+ """.trimIndent(),
+ )
+
+ val exchangeRate = exchangeRateSource.getExchangeRate(currencySymbolCode)
+
+ assertEquals(BigDecimal("87.4205"), exchangeRate.rates["USD"])
+ assertTrue(!exchangeRate.rates.containsKey("BROKEN"))
+ }
+}