-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #47 from psuzn/forex-rate
Forex api
- Loading branch information
Showing
12 changed files
with
244 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package me.sujanpoudel.playdeals.api | ||
|
||
import io.vertx.ext.web.Router | ||
import me.sujanpoudel.playdeals.common.coHandler | ||
import me.sujanpoudel.playdeals.common.jsonResponse | ||
import me.sujanpoudel.playdeals.usecases.GetForexUseCase | ||
import me.sujanpoudel.playdeals.usecases.executeUseCase | ||
import org.kodein.di.DirectDI | ||
import org.kodein.di.instance | ||
|
||
fun forexRateApi( | ||
di: DirectDI, | ||
vertx: io.vertx.core.Vertx | ||
): Router = Router.router(vertx).apply { | ||
get() | ||
.coHandler { ctx -> | ||
ctx.executeUseCase( | ||
useCase = di.instance<GetForexUseCase>(), | ||
toContext = { }, | ||
toInput = { } | ||
) { | ||
ctx.json(jsonResponse(data = it)) | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package me.sujanpoudel.playdeals.domain | ||
|
||
import java.time.OffsetDateTime | ||
|
||
// Rates are USD based | ||
data class ForexRate( | ||
val timestamp: OffsetDateTime, | ||
val rates: List<ConversionRate> | ||
) | ||
|
||
data class ConversionRate(val currency: String, val rate: Float) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package me.sujanpoudel.playdeals.jobs | ||
|
||
import io.vertx.core.json.Json | ||
import io.vertx.ext.web.client.WebClient | ||
import io.vertx.ext.web.client.WebClientOptions | ||
import io.vertx.kotlin.coroutines.await | ||
import me.sujanpoudel.playdeals.Conf | ||
import me.sujanpoudel.playdeals.common.SIMPLE_NAME | ||
import me.sujanpoudel.playdeals.common.loggingExecutionTime | ||
import me.sujanpoudel.playdeals.domain.ConversionRate | ||
import me.sujanpoudel.playdeals.domain.ForexRate | ||
import me.sujanpoudel.playdeals.logger | ||
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository | ||
import org.jobrunr.jobs.lambdas.JobRequest | ||
import org.jobrunr.scheduling.RecurringJobBuilder | ||
import org.kodein.di.DI | ||
import org.kodein.di.DIAware | ||
import org.kodein.di.instance | ||
import java.time.Duration | ||
import java.time.OffsetDateTime | ||
import java.time.ZoneOffset | ||
import java.util.UUID | ||
|
||
class ForexFetcher( | ||
override val di: DI, | ||
private val conf: Conf | ||
) : CoJobRequestHandler<ForexFetcher.Request>(), DIAware { | ||
|
||
private val backgroundJobsVerticle by instance<BackgroundJobsVerticle>() | ||
|
||
private val webClient by lazy { | ||
WebClient.create( | ||
backgroundJobsVerticle.vertx, | ||
WebClientOptions().setSsl(false).setDefaultHost("api.exchangeratesapi.io") | ||
) | ||
} | ||
|
||
private val repository by instance<KeyValuesRepository>() | ||
|
||
override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( | ||
"$SIMPLE_NAME:: handleRequest" | ||
) { | ||
val rates = getForexRates() | ||
logger.info("got ${rates.rates.size} forex rate") | ||
repository.saveForexRate(rates) | ||
} | ||
|
||
private suspend fun getForexRates(): ForexRate { | ||
val response = webClient.get("/v1/latest?access_key=${conf.forexApiKey}&format=1&base=EUR") | ||
.send() | ||
.await() | ||
.bodyAsString() | ||
.let { | ||
Json.decodeValue(it) as io.vertx.core.json.JsonObject | ||
} | ||
|
||
val epochSeconds = response.getLong("timestamp") | ||
val usdRate = response.getJsonObject("rates").getNumber("USD").toFloat() | ||
|
||
return ForexRate( | ||
timestamp = OffsetDateTime.ofInstant(java.time.Instant.ofEpochSecond(epochSeconds), ZoneOffset.UTC), | ||
rates = response.getJsonObject("rates").map { | ||
ConversionRate(it.key, (it.value as Number).toFloat() / usdRate) | ||
} | ||
) | ||
} | ||
|
||
class Request private constructor() : JobRequest { | ||
override fun getJobRequestHandler() = ForexFetcher::class.java | ||
|
||
companion object { | ||
private val JOB_ID: UUID = UUID.nameUUIDFromBytes("ForexFetch".toByteArray()) | ||
operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() | ||
.withJobRequest(Request()) | ||
.withName("ForexFetch") | ||
.withId(JOB_ID.toString()) | ||
.withDuration(Duration.ofDays(1)) | ||
.withAmountOfRetries(3) | ||
|
||
fun immediate(): JobRequest = Request() | ||
} | ||
} | ||
} | ||
|
||
private const val KEY_FOREX_RATE = "FOREX_RATE" | ||
|
||
suspend fun KeyValuesRepository.getForexRate(): ForexRate? = get(KEY_FOREX_RATE)?.let { | ||
Json.decodeValue(it, ForexRate::class.java) | ||
} | ||
|
||
suspend fun KeyValuesRepository.saveForexRate(forexRate: ForexRate) = set(KEY_FOREX_RATE, Json.encode(forexRate)) |
16 changes: 16 additions & 0 deletions
16
backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package me.sujanpoudel.playdeals.usecases | ||
|
||
import me.sujanpoudel.playdeals.domain.ForexRate | ||
import me.sujanpoudel.playdeals.jobs.getForexRate | ||
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository | ||
import org.kodein.di.DI | ||
import org.kodein.di.instance | ||
|
||
class GetForexUseCase(di: DI) : UseCase<Unit, ForexRate?> { | ||
|
||
private val appDealsRepository by di.instance<KeyValuesRepository>() | ||
|
||
override suspend fun doExecute(input: Unit): ForexRate? { | ||
return appDealsRepository.getForexRate() | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package me.sujanpoudel.playdeals.api.forex | ||
|
||
import io.kotest.matchers.shouldBe | ||
import io.vertx.core.Vertx | ||
import io.vertx.core.json.JsonObject | ||
import io.vertx.kotlin.coroutines.await | ||
import me.sujanpoudel.playdeals.IntegrationTest | ||
import me.sujanpoudel.playdeals.domain.ConversionRate | ||
import me.sujanpoudel.playdeals.domain.ForexRate | ||
import me.sujanpoudel.playdeals.get | ||
import me.sujanpoudel.playdeals.jobs.saveForexRate | ||
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository | ||
import org.junit.jupiter.api.Test | ||
import java.time.OffsetDateTime | ||
|
||
class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { | ||
@Test | ||
fun `should return forex if there is data`() = runTest { | ||
val repository = di.get<KeyValuesRepository>() | ||
|
||
val forexRate = ForexRate(OffsetDateTime.now(), listOf(ConversionRate("USD", 1.1f))) | ||
|
||
repository.saveForexRate(forexRate) | ||
|
||
val response = httpClient.get("/api/forex") | ||
.send() | ||
.await() | ||
.bodyAsJsonObject() | ||
|
||
response.getJsonObject("data").also { data -> | ||
OffsetDateTime.parse(data.getString("timestamp")).toEpochSecond() shouldBe forexRate.timestamp.toEpochSecond() | ||
data.getJsonArray("rates").also { rates -> | ||
rates.size() shouldBe 1 | ||
(rates.first() as JsonObject).also { rate -> | ||
rate.getString("currency") shouldBe "USD" | ||
rate.getFloat("rate") shouldBe 1.1f | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun `should return null if there is no data`() = runTest { | ||
val response = httpClient.get("/api/forex") | ||
.send() | ||
.await() | ||
.bodyAsJsonObject() | ||
|
||
response.getJsonObject("data") shouldBe null | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters