Skip to content

Commit 14698d3

Browse files
DavidDavid
authored andcommitted
Add preferred api
1 parent 09ec786 commit 14698d3

16 files changed

Lines changed: 215 additions & 379 deletions

File tree

app/detekt-baseline.xml

Lines changed: 4 additions & 303 deletions
Large diffs are not rendered by default.

app/src/main/java/com/davidcrespo/onewallet/data/local/cache/SymbolCache.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import com.davidcrespo.onewallet.data.local.database.portfolio.entities.Investme
55
interface SymbolCache {
66

77
fun getCachedInvestmentIfValid(symbol: String, validCacheHours: Long): InvestmentEntity?
8+
fun getCachedInvestment(symbol: String): InvestmentEntity?
89
fun setCachedInvestment(investmentEntity: InvestmentEntity)
910
}

app/src/main/java/com/davidcrespo/onewallet/data/local/cache/SymbolCacheImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class SymbolCacheImpl(
1515
override fun getCachedInvestmentIfValid(symbol: String, validCacheHours: Long): InvestmentEntity? {
1616
if (!isValid(symbol, validCacheHours)) return null
1717

18+
return getCachedInvestment(symbol)
19+
}
20+
21+
override fun getCachedInvestment(symbol: String): InvestmentEntity? {
1822
val raw = sharedPreferences.getString(valueKey(symbol), null) ?: return null
1923
return runCatching { raw.toInvestmentEntity() }.getOrNull()
2024
}

app/src/main/java/com/davidcrespo/onewallet/data/local/database/converters/RoomConverters.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.davidcrespo.onewallet.data.local.database.converters
22

33
import androidx.room.TypeConverter
44
import com.davidcrespo.onewallet.data.local.database.portfolio.entities.CurrencyEntity
5+
import com.davidcrespo.onewallet.domain.model.investment.DataSource
56
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
67

78
class RoomConverters {
@@ -17,4 +18,10 @@ class RoomConverters {
1718

1819
@TypeConverter
1920
fun stringToCurrency(value: String): CurrencyEntity = CurrencyEntity(value)
21+
22+
@TypeConverter
23+
fun dataSourceToString(value: DataSource?): String? = value?.name
24+
25+
@TypeConverter
26+
fun stringToDataSource(value: String?): DataSource? = value?.let { runCatching { DataSource.valueOf(it) }.getOrNull() }
2027
}

app/src/main/java/com/davidcrespo/onewallet/data/local/database/portfolio/entities/InvestmentEntity.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.davidcrespo.onewallet.data.local.database.portfolio.entities
22

33
import androidx.room.Entity
4+
import com.davidcrespo.onewallet.domain.model.investment.DataSource
45
import com.davidcrespo.onewallet.domain.model.investment.Investment
56
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
67
import kotlinx.serialization.Serializable
@@ -16,10 +17,11 @@ data class InvestmentEntity(
1617
val currency: CurrencyEntity,
1718
val type: InvestmentType,
1819
val year: Int,
19-
val month: Int
20+
val month: Int,
21+
val preferredApi: DataSource? = null
2022
) {
2123
override fun toString(): String {
22-
return "$symbol|$name|$quantity|$price|$previousPrice|${currency.code}|$type|$year|$month"
24+
return "$symbol|$name|$quantity|$price|$previousPrice|${currency.code}|$type|$year|$month|${preferredApi?.name}"
2325
}
2426
}
2527

@@ -32,7 +34,8 @@ fun Investment.toEntity(): InvestmentEntity = InvestmentEntity(
3234
currency = currency.toEntity(),
3335
type = type,
3436
year = year,
35-
month = month
37+
month = month,
38+
preferredApi = preferredApi
3639
)
3740

3841
fun InvestmentEntity.toDomain(): Investment = Investment(
@@ -44,7 +47,8 @@ fun InvestmentEntity.toDomain(): Investment = Investment(
4447
currency = currency.toDomain(),
4548
type = type,
4649
year = year,
47-
month = month
50+
month = month,
51+
preferredApi = preferredApi
4852
)
4953

5054
fun String.toInvestmentEntity(): InvestmentEntity {
@@ -59,5 +63,6 @@ fun String.toInvestmentEntity(): InvestmentEntity {
5963
type = InvestmentType.valueOf(parts[6]),
6064
year = parts[7].toIntOrNull() ?: 0,
6165
month = parts[8].toIntOrNull() ?: 0,
66+
preferredApi = parts.getOrNull(9)?.let { runCatching { DataSource.valueOf(it) }.getOrNull() }
6267
)
6368
}

app/src/main/java/com/davidcrespo/onewallet/data/remote/dto/InvestmentDto.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.davidcrespo.onewallet.data.remote.dto
22

33
import com.davidcrespo.onewallet.data.local.database.portfolio.entities.InvestmentEntity
4+
import com.davidcrespo.onewallet.domain.model.investment.DataSource
45
import com.davidcrespo.onewallet.domain.model.investment.Investment
56
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
67

@@ -13,7 +14,8 @@ data class InvestmentDto(
1314
val currency: CurrencyDto,
1415
val type: InvestmentType,
1516
val year: Int,
16-
val month: Int
17+
val month: Int,
18+
val preferredApi: DataSource? = null
1719
) {
1820
fun isValidName(): Boolean =
1921
name.isNotBlank()
@@ -31,7 +33,8 @@ fun InvestmentDto.toDomain(): Investment = Investment(
3133
currency = currency.toDomain(),
3234
type = type,
3335
year = year,
34-
month = month
36+
month = month,
37+
preferredApi = preferredApi
3538
)
3639

3740
fun InvestmentDto.toEntity(): InvestmentEntity = InvestmentEntity(
@@ -43,5 +46,6 @@ fun InvestmentDto.toEntity(): InvestmentEntity = InvestmentEntity(
4346
currency = currency.toEntity(),
4447
type = type,
4548
year = year,
46-
month = month
49+
month = month,
50+
preferredApi = preferredApi
4751
)

app/src/main/java/com/davidcrespo/onewallet/data/repository/FinancialRepositoryImpl.kt

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.davidcrespo.onewallet.domain.cache.CachePolicy
3232
import com.davidcrespo.onewallet.domain.di.DispatcherProvider
3333
import com.davidcrespo.onewallet.domain.logging.Telemetry
3434
import com.davidcrespo.onewallet.domain.model.investment.Currency
35+
import com.davidcrespo.onewallet.domain.model.investment.DataSource
3536
import com.davidcrespo.onewallet.domain.model.investment.EUR
3637
import com.davidcrespo.onewallet.domain.model.investment.Investment
3738
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
@@ -68,14 +69,15 @@ class FinancialRepositoryImpl(
6869
name: String,
6970
selectedCurrency: Currency?,
7071
marketType: MarketType?,
71-
investmentCurrency: Currency?
72+
investmentCurrency: Currency?,
73+
preferredApi: DataSource?
7274
): Result<Investment> {
7375
return withContext(dispatcher.io) {
7476
when (type) {
75-
InvestmentType.STOCK -> getStockPrice(symbol, name, marketType, investmentCurrency)
77+
InvestmentType.STOCK -> getStockPrice(symbol, name, marketType, investmentCurrency, preferredApi)
7678
InvestmentType.CRYPTO -> getCryptoPrice(symbol)
77-
InvestmentType.FUND -> getFundPrice(symbol)
78-
InvestmentType.ETF -> getEtfPrice(symbol, selectedCurrency)
79+
InvestmentType.FUND -> getFundPrice(symbol, preferredApi)
80+
InvestmentType.ETF -> getEtfPrice(symbol, selectedCurrency, preferredApi)
7981
else -> Result.failure(IllegalArgumentException("Invalid investment type: $type"))
8082
}
8183
}
@@ -87,64 +89,104 @@ class FinancialRepositoryImpl(
8789
?.toDomain()
8890
?.let { return@runCatching it }
8991

90-
val dto = binanceDataSource.getCryptoPrice(symbol)
91-
val valid = dto.takeIf { it.isValidPrice() }
92+
tryFetch(symbol, DataSource.BINANCE) { binanceDataSource.getCryptoPrice(symbol) }
9293
?: throw IllegalStateException("No se pudo obtener el precio de $symbol")
93-
94-
telemetry.log("(Binance) get $symbol from remote ${valid.price} ${valid.currency}")
95-
symbolCache.setCachedInvestment(valid.toEntity())
96-
valid.toDomain()
9794
}
9895
}
9996

100-
private suspend fun getStockPrice(symbol: String, name: String, marketType: MarketType?, currency: Currency?): Result<Investment> =
97+
private suspend fun getStockPrice(symbol: String, name: String, marketType: MarketType?, currency: Currency?, preferredApi: DataSource?): Result<Investment> =
10198
runCatching {
102-
symbolCache.getCachedInvestmentIfValid(symbol, cachePolicy.stockHours)
103-
?.toDomain()
104-
?.let { return@runCatching it }
99+
val cachedValid = symbolCache.getCachedInvestmentIfValid(symbol, cachePolicy.stockHours)
100+
cachedValid?.toDomain()?.let { return@runCatching it }
101+
102+
val apiToTryFirst = preferredApi ?: symbolCache.getCachedInvestment(symbol)?.preferredApi
105103

106104
val country = marketType ?: MarketType.GLOBAL
107105

108106
when (country) {
109107
MarketType.US -> {
110-
tryFetch(symbol, "Finnhub") { finnhubDataSource.getStockPrice(symbol, name) }
108+
tryFetch(symbol, DataSource.FINNHUB) { finnhubDataSource.getStockPrice(symbol, name) }
111109
?: throw IllegalStateException("No se pudo obtener el precio del fondo")
112110
}
113111
MarketType.GLOBAL -> {
114-
tryFetch(symbol, "Yahoo Finance") { yahooFinanceDataSource.getStockPrice(symbol, name) }
115-
?: tryFetch(symbol, "Finnhub") { finnhubDataSource.getStockPrice(symbol, name) }
116-
?: tryFetch(symbol, "Alpha Vantage") { alphaVantageDataSource.getStockPrice(symbol, name, currency?.toDto() ?: CurrencyDto(USD)) }
117-
?: tryFetch(symbol, "Marketstack") { marketstackDataSource.getStockPrice(symbol, name) }
112+
val apis = listOf<Pair<DataSource, suspend () -> InvestmentDto?>>(
113+
DataSource.YAHOO_FINANCE to { yahooFinanceDataSource.getStockPrice(symbol, name) },
114+
DataSource.FINNHUB to { finnhubDataSource.getStockPrice(symbol, name) },
115+
DataSource.ALPHA_VANTAGE to { alphaVantageDataSource.getStockPrice(symbol, name, currency?.toDto() ?: CurrencyDto(USD)) },
116+
DataSource.MARKETSTACK to { marketstackDataSource.getStockPrice(symbol, name) }
117+
)
118+
tryFetchFromSequence(symbol, apis, apiToTryFirst)
118119
?: throw IllegalStateException("No se pudo obtener el precio del fondo")
119120
}
120121
}
121122
}
122123

123-
private suspend fun getFundPrice(isin: String): Result<Investment> =
124+
private suspend fun getFundPrice(isin: String, preferredApi: DataSource?): Result<Investment> =
124125
runCatching {
125-
symbolCache.getCachedInvestmentIfValid(isin, cachePolicy.fundHours)
126-
?.toDomain()
127-
?.let { return@runCatching it }
126+
val cachedValid = symbolCache.getCachedInvestmentIfValid(isin, cachePolicy.fundHours)
127+
cachedValid?.toDomain()?.let { return@runCatching it }
128+
129+
val apiToTryFirst = preferredApi ?: symbolCache.getCachedInvestment(isin)?.preferredApi
128130

129-
tryFetch(isin, "Investing.com") { investingDataSource.getFundPrice(isin) }
130-
?: tryFetch(isin, "QueFondos.com") { queFondosDataSource.getFundPrice(isin, InvestmentType.FUND) }
131+
val apis = listOf<Pair<DataSource, suspend () -> InvestmentDto?>>(
132+
DataSource.INVESTING_COM to { investingDataSource.getFundPrice(isin) },
133+
DataSource.QUE_FONDOS to { queFondosDataSource.getFundPrice(isin, InvestmentType.FUND) }
134+
)
135+
136+
tryFetchFromSequence(isin, apis, apiToTryFirst)
131137
?: throw IllegalStateException("No se pudo obtener el precio del fondo")
132138
}
133139

134-
private suspend fun getEtfPrice(isin: String, selectedCurrency: Currency?): Result<Investment> {
140+
private suspend fun getEtfPrice(isin: String, selectedCurrency: Currency?, preferredApi: DataSource?): Result<Investment> {
135141
return runCatching {
136-
symbolCache.getCachedInvestmentIfValid(isin, cachePolicy.etfHours)
137-
?.toDomain()
138-
?.let { return@runCatching it }
142+
val cachedValid = symbolCache.getCachedInvestmentIfValid(isin, cachePolicy.etfHours)
143+
cachedValid?.toDomain()?.let { return@runCatching it }
144+
145+
val apiToTryFirst = preferredApi ?: symbolCache.getCachedInvestment(isin)?.preferredApi
139146

140147
val currency = selectedCurrency?.toDto() ?: CurrencyDto(EUR)
141148

142-
tryFetch(isin, "JustETF.com (detail)") { justEtfDataSource.getEtfDetail(isin, currency) }
143-
?: tryFetch(isin, "ExtraETF.com") { extraEtfDataSource.getEtfPrice(isin) }
144-
?: tryFetch(isin, "QueFondos.com") { queFondosDataSource.getFundPrice(isin, InvestmentType.ETF) }
145-
?: tryFetch(isin, "JustETF.com (price)", false) { justEtfDataSource.getEtfPrice(isin, currency) }
146-
?: throw IllegalStateException("No se pudo obtener el precio del ETF")
149+
val apis = listOf<Triple<DataSource, Boolean, suspend () -> InvestmentDto?>>(
150+
Triple(DataSource.JUST_ETF_DETAIL, true) { justEtfDataSource.getEtfDetail(isin, currency) },
151+
Triple(DataSource.EXTRA_ETF, true) { extraEtfDataSource.getEtfPrice(isin) },
152+
Triple(DataSource.QUE_FONDOS, true) { queFondosDataSource.getFundPrice(isin, InvestmentType.ETF) },
153+
Triple(DataSource.JUST_ETF_PRICE, false) { justEtfDataSource.getEtfPrice(isin, currency) }
154+
)
155+
156+
if (apiToTryFirst != null) {
157+
val preferred = apis.find { it.first == apiToTryFirst }
158+
if (preferred != null) {
159+
tryFetch(isin, preferred.first, preferred.second, preferred.third)?.let { return@runCatching it }
160+
}
161+
}
162+
163+
for (api in apis) {
164+
if (api.first == apiToTryFirst) continue
165+
tryFetch(isin, api.first, api.second, api.third)?.let { return@runCatching it }
166+
}
167+
168+
throw IllegalStateException("No se pudo obtener el precio del ETF")
169+
}
170+
}
171+
172+
private suspend fun tryFetchFromSequence(
173+
isin: String,
174+
apis: List<Pair<DataSource, suspend () -> InvestmentDto?>>,
175+
preferredApi: DataSource?
176+
): Investment? {
177+
if (preferredApi != null) {
178+
val preferred = apis.find { it.first == preferredApi }
179+
if (preferred != null) {
180+
tryFetch(isin, preferred.first, fetch = preferred.second)?.let { return it }
181+
}
147182
}
183+
184+
for ((source, fetch) in apis) {
185+
if (source == preferredApi) continue
186+
tryFetch(isin, source, fetch = fetch)?.let { return it }
187+
}
188+
189+
return null
148190
}
149191

150192
override suspend fun getStocksSymbols(exchange: String): Result<List<MarketAsset>> =
@@ -251,15 +293,16 @@ class FinancialRepositoryImpl(
251293

252294
private suspend fun tryFetch(
253295
isin: String,
254-
source: String,
296+
source: DataSource,
255297
validateName: Boolean = true,
256298
fetch: suspend () -> InvestmentDto?
257299
): Investment? {
258300
return runCatching {
259301
val inv = fetch()
260302
val valid = inv?.takeIf { if (validateName) { it.isValidName() && it.isValidPrice() } else { it.isValidPrice()} }
303+
?.copy(preferredApi = source)
261304
if (valid != null) {
262-
telemetry.log("$source get $isin succeed ${valid.price} ${valid.currency.code}")
305+
telemetry.log("${source.value} get $isin succeed ${valid.price} ${valid.currency.code}")
263306
symbolCache.setCachedInvestment(valid.toEntity())
264307
}
265308
valid?.toDomain()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.davidcrespo.onewallet.domain.model.investment
2+
3+
enum class DataSource(val value: String) {
4+
FINNHUB("Finnhub"),
5+
YAHOO_FINANCE("Yahoo Finance"),
6+
ALPHA_VANTAGE("Alpha Vantage"),
7+
MARKETSTACK("Marketstack"),
8+
INVESTING_COM("Investing.com"),
9+
QUE_FONDOS("QueFondos.com"),
10+
JUST_ETF_DETAIL("JustETF.com (detail)"),
11+
JUST_ETF_PRICE("JustETF.com (price)"),
12+
EXTRA_ETF("ExtraETF.com"),
13+
BINANCE("Binance");
14+
15+
companion object {
16+
fun fromValue(value: String?): DataSource? {
17+
return entries.find { it.value == value }
18+
}
19+
}
20+
}

app/src/main/java/com/davidcrespo/onewallet/domain/model/investment/Investment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ data class Investment(
99
val currency: Currency,
1010
val type: InvestmentType,
1111
val year: Int,
12-
val month: Int
12+
val month: Int,
13+
val preferredApi: DataSource? = null
1314
) {
1415

1516
fun setDate(month: Int, year: Int): Investment {

app/src/main/java/com/davidcrespo/onewallet/domain/repository/FinancialRepository.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.davidcrespo.onewallet.domain.repository
22

33
import com.davidcrespo.onewallet.domain.model.investment.Currency
4+
import com.davidcrespo.onewallet.domain.model.investment.DataSource
45
import com.davidcrespo.onewallet.domain.model.investment.Investment
56
import com.davidcrespo.onewallet.domain.model.investment.InvestmentType
67
import com.davidcrespo.onewallet.domain.model.investment.MarketType
@@ -14,7 +15,8 @@ interface FinancialRepository {
1415
name: String = "",
1516
selectedCurrency: Currency?,
1617
marketType: MarketType?,
17-
investmentCurrency: Currency?
18+
investmentCurrency: Currency?,
19+
preferredApi: DataSource? = null
1820
): Result<Investment>
1921
suspend fun getStocksSymbols(exchange: String): Result<List<MarketAsset>>
2022
suspend fun getStocksSymbolsByQuery(query: String): Result<List<MarketAsset>>

0 commit comments

Comments
 (0)