Coverage Summary for Class: ExchangeRateRepositoryImpl (com.georgeci.moneysurfer.data.repository)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ExchangeRateRepositoryImpl |
100%
(7/7)
|
87.5%
(14/16)
|
100%
(29/29)
|
97.3%
(248/255)
|
| ExchangeRateRepositoryImpl$Companion |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/1)
|
| ExchangeRateRepositoryImpl$isStale$1 |
|
| ExchangeRateRepositoryImpl$observe-MpYtP3o$$inlined$map$1 |
0%
(0/2)
|
|
| ExchangeRateRepositoryImpl$observe-MpYtP3o$$inlined$map$1$1 |
|
| ExchangeRateRepositoryImpl$observe-MpYtP3o$$inlined$map$1$2 |
0%
(0/1)
|
|
| ExchangeRateRepositoryImpl$observe-MpYtP3o$$inlined$map$1$2$1 |
|
| ExchangeRateRepositoryImpl$refreshIfStale$1 |
|
| Total |
63.6%
(7/11)
|
87.5%
(14/16)
|
96.7%
(29/30)
|
96.9%
(248/256)
|
package com.georgeci.moneysurfer.data.repository
import co.touchlab.kermit.Logger
import com.georgeci.moneysurfer.data.db.dao.ExchangeRateDao
import com.georgeci.moneysurfer.data.db.entity.ExchangeRateEntity
import com.georgeci.moneysurfer.domain.model.ExchangeRateTable
import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
import com.georgeci.moneysurfer.domain.repositories.ExchangeRateRemoteSource
import com.georgeci.moneysurfer.domain.repositories.ExchangeRateRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Instant
/**
* Room-backed FX cache in front of [ExchangeRateRemoteSource].
*
* Refresh policy: at most one fetch per [REFRESH_INTERVAL] per base currency, triggered by whoever
* observes the rates (in practice the dashboard, on open). Daily is the natural cadence — the
* reference feeds this reads publish once a business day — and it keeps a backgrounded app that
* gets resumed twenty times from making twenty requests.
*
* Everything else about failure is deliberately quiet: an unreachable provider leaves the previous
* table in place, and the dashboard keeps showing it with its own "as of" date.
*/
@Single(binds = [ExchangeRateRepository::class])
class ExchangeRateRepositoryImpl(
private val dao: ExchangeRateDao,
private val remoteSource: ExchangeRateRemoteSource,
private val clock: ClockUseCase,
) : ExchangeRateRepository {
private val log = Logger.withTag(TAG)
override fun observe(base: CurrencyCode): Flow<ExchangeRateTable?> =
dao.observeByBase(base.value).map { rows -> rows.toTable(base) }
override suspend fun refreshIfStale(base: CurrencyCode) {
if (!isStale(base)) return
val fetched = remoteSource.fetchLatest(base)
if (fetched == null) {
log.i { "[refresh] provider gave nothing for ${base.value} — keeping cache" }
return
}
val fetchedAt = clock.now().toEpochMilliseconds()
dao.replaceForBase(base.value, fetched.toEntities(fetchedAt))
log.i { "[refresh] cached ${fetched.rates.size} rates for ${base.value} asOf=${fetched.asOf}" }
}
private suspend fun isStale(base: CurrencyCode): Boolean {
val lastFetchedAt = dao.lastFetchedAt(base.value) ?: return true
return clock.now() - Instant.fromEpochMilliseconds(lastFetchedAt) >= REFRESH_INTERVAL
}
private fun List<ExchangeRateEntity>.toTable(base: CurrencyCode): ExchangeRateTable? {
if (isEmpty()) return null
return ExchangeRateTable(
baseCurrency = base,
rates = associate { CurrencyCode(it.currency) to it.rate },
// Every row of one fetch carries the same publication moment; the newest wins so a
// half-written table (an interrupted replace) still reports the date it can back up.
asOf = Instant.fromEpochMilliseconds(maxOf { it.asOf }),
)
}
private fun ExchangeRateTable.toEntities(fetchedAt: Long): List<ExchangeRateEntity> =
rates.map { (currency, rate) ->
ExchangeRateEntity(
baseCurrency = baseCurrency.value,
currency = currency.value,
rate = rate,
asOf = asOf.toEpochMilliseconds(),
fetchedAt = fetchedAt,
)
}
private companion object {
const val TAG = "ExchangeRateRepository"
/** Staleness threshold: the reference feeds publish daily, so anything younger is current. */
val REFRESH_INTERVAL: Duration = 24.hours
}
}