CryptoService.kt
package com.distasilucas.cryptobalancetracker.service
import com.distasilucas.cryptobalancetracker.constants.COINGECKO_CRYPTO_NOT_FOUND
import com.distasilucas.cryptobalancetracker.constants.CRYPTOS_CRYPTOS_IDS_CACHE
import com.distasilucas.cryptobalancetracker.constants.CRYPTO_COINGECKO_CRYPTO_ID_CACHE
import com.distasilucas.cryptobalancetracker.entity.Crypto
import com.distasilucas.cryptobalancetracker.model.response.coingecko.CoingeckoCrypto
import com.distasilucas.cryptobalancetracker.repository.CryptoRepository
import com.distasilucas.cryptobalancetracker.repository.GoalRepository
import com.distasilucas.cryptobalancetracker.repository.PriceTargetRepository
import com.distasilucas.cryptobalancetracker.repository.UserCryptoRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import java.time.Clock
import java.time.LocalDateTime
@Service
class CryptoService(
private val coingeckoService: CoingeckoService,
private val cacheService: CacheService,
private val orphanCryptoService: OrphanCryptoService,
private val cryptoRepository: CryptoRepository,
private val clock: Clock
) {
private val logger = KotlinLogging.logger { }
@Cacheable(cacheNames = [CRYPTO_COINGECKO_CRYPTO_ID_CACHE], key = "#coingeckoCryptoId")
fun retrieveCryptoInfoById(coingeckoCryptoId: String): Crypto {
logger.info { "Retrieving crypto info for id $coingeckoCryptoId" }
return cryptoRepository.findById(coingeckoCryptoId)
.orElseGet {
val crypto = getCrypto(coingeckoCryptoId)
cacheService.invalidate(CacheType.CRYPTOS_CACHES)
logger.info { "Saved crypto $crypto because it didn't exist" }
cryptoRepository.save(crypto)
}
}
@Cacheable(cacheNames = [CRYPTOS_CRYPTOS_IDS_CACHE], key = "#ids")
fun findAllByIds(ids: Collection<String>): List<Crypto> {
logger.info { "Retrieving cryptos with ids $ids" }
return cryptoRepository.findAllByIdIn(ids)
}
fun findTopGainer24h(ids: Set<String>): Crypto {
logger.info { "Retrieving best performant crypto in the last 24H" }
return cryptoRepository.findFirstByIdInOrderByChangePercentageIn24hDesc(ids)
}
fun retrieveCoingeckoCryptoInfoByNameOrId(cryptoNameOrId: String): CoingeckoCrypto {
logger.info { "Retrieving info for coingecko crypto $cryptoNameOrId" }
return coingeckoService.retrieveAllCryptos()
.find { it.name.equals(cryptoNameOrId, true) || it.id.equals(cryptoNameOrId, true) }
?: throw CoingeckoCryptoNotFoundException(COINGECKO_CRYPTO_NOT_FOUND.format(cryptoNameOrId))
}
fun findOldestNCryptosByLastPriceUpdate(dateFilter: LocalDateTime, limit: Int) =
cryptoRepository.findOldestNCryptosByLastPriceUpdate(dateFilter, limit)
fun saveCryptoIfNotExists(coingeckoCryptoId: String) {
val cryptoOptional = cryptoRepository.findById(coingeckoCryptoId)
if (cryptoOptional.isEmpty) {
val crypto = getCrypto(coingeckoCryptoId)
cryptoRepository.save(crypto)
cacheService.invalidate(CacheType.CRYPTOS_CACHES)
logger.info { "Saved crypto $crypto" }
}
}
fun deleteCryptoIfNotUsed(coingeckoCryptoId: String) {
if (orphanCryptoService.isCryptoOrphan(coingeckoCryptoId)) {
cryptoRepository.deleteById(coingeckoCryptoId)
cacheService.invalidate(CacheType.CRYPTOS_CACHES)
logger.info { "Deleted crypto [$coingeckoCryptoId] because it was not used" }
}
}
fun deleteCryptosIfNotUsed(coingeckoCryptoIds: List<String>) {
val orphanCryptos = orphanCryptoService.getOrphanCryptos(coingeckoCryptoIds)
if (orphanCryptos.isNotEmpty()) {
cryptoRepository.deleteAllById(orphanCryptos)
cacheService.invalidate(CacheType.CRYPTOS_CACHES)
logger.info { "Deleted cryptos $coingeckoCryptoIds because they were not used" }
}
}
fun updateCryptos(cryptosToUpdate: List<Crypto>) {
cryptoRepository.saveAll(cryptosToUpdate)
logger.info { "Updated cryptos ${cryptosToUpdate.map { it.name }}" }
}
private fun getCrypto(coingeckoCryptoId: String): Crypto {
val coingeckoCryptoInfo = coingeckoService.retrieveCryptoInfo(coingeckoCryptoId)
return coingeckoCryptoInfo.toCrypto(clock)
}
}
class CoingeckoCryptoNotFoundException(message: String) : RuntimeException(message)
@Service
class OrphanCryptoService(
private val userCryptoRepository: UserCryptoRepository,
private val goalRepository: GoalRepository,
private val priceTargetRepository: PriceTargetRepository
) {
fun isCryptoOrphan(coingeckoCryptoId: String): Boolean {
return userCryptoRepository.findAllByCoingeckoCryptoId(coingeckoCryptoId).isEmpty() &&
goalRepository.findByCoingeckoCryptoId(coingeckoCryptoId) == null &&
priceTargetRepository.findAllByCoingeckoCryptoId(coingeckoCryptoId).isEmpty()
}
fun getOrphanCryptos(coingeckoCryptoId: List<String>) = coingeckoCryptoId.filter { isCryptoOrphan(it) }
}