GoalService.kt
package com.distasilucas.cryptobalancetracker.service
import com.distasilucas.cryptobalancetracker.constants.DUPLICATED_GOAL
import com.distasilucas.cryptobalancetracker.constants.GOAL_ID_NOT_FOUND
import com.distasilucas.cryptobalancetracker.constants.GOAL_RESPONSE_GOAL_ID_CACHE
import com.distasilucas.cryptobalancetracker.constants.PAGE_GOALS_RESPONSE_PAGE_CACHE
import com.distasilucas.cryptobalancetracker.entity.Crypto
import com.distasilucas.cryptobalancetracker.entity.Goal
import com.distasilucas.cryptobalancetracker.model.request.goal.GoalRequest
import com.distasilucas.cryptobalancetracker.model.response.goal.GoalResponse
import com.distasilucas.cryptobalancetracker.model.response.goal.PageGoalResponse
import com.distasilucas.cryptobalancetracker.repository.GoalRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.math.RoundingMode
@Service
class GoalService(
private val goalRepository: GoalRepository,
private val cryptoService: CryptoService,
private val userCryptoService: UserCryptoService,
private val cacheService: CacheService
) {
private val logger = KotlinLogging.logger { }
@Cacheable(cacheNames = [GOAL_RESPONSE_GOAL_ID_CACHE], key = "#goalId")
fun retrieveGoalById(goalId: String): GoalResponse {
logger.info { "Retrieving for goal with $goalId" }
return goalRepository.findById(goalId)
.orElseThrow { GoalNotFoundException(GOAL_ID_NOT_FOUND.format(goalId)) }
.toGoalResponse(id = goalId)
}
@Cacheable(cacheNames = [PAGE_GOALS_RESPONSE_PAGE_CACHE], key = "#page")
fun retrieveGoalsForPage(page: Int): PageGoalResponse {
logger.info { "Retrieving goals for page $page" }
val pageRequest: Pageable = PageRequest.of(page, 10)
val entityGoalsPage = goalRepository.findAll(pageRequest)
val goalsResponse = entityGoalsPage.content.map { it.toGoalResponse(id = it.id) }
return PageGoalResponse(
page = page,
totalPages = entityGoalsPage.totalPages,
goals = goalsResponse
)
}
fun saveGoal(goalRequest: GoalRequest): GoalResponse {
val coingeckoCrypto = cryptoService.retrieveCoingeckoCryptoInfoByNameOrId(goalRequest.cryptoName!!)
val existingGoal: Goal? = goalRepository.findByCoingeckoCryptoId(coingeckoCrypto.id)
if (existingGoal != null) {
throw DuplicatedGoalException(DUPLICATED_GOAL.format(coingeckoCrypto.name))
}
val goal = goalRequest.toEntity(coingeckoCrypto.id)
cryptoService.saveCryptoIfNotExists(goal.coingeckoCryptoId)
goalRepository.save(goal)
cacheService.invalidate(CacheType.GOALS_CACHES)
logger.info { "Saved goal $goal" }
return goal.toGoalResponse(id = goal.id)
}
fun updateGoal(goalId: String, goalRequest: GoalRequest): GoalResponse {
val goal = goalRepository.findById(goalId)
.orElseThrow { GoalNotFoundException(GOAL_ID_NOT_FOUND.format(goalId)) }
val updatedGoal = goal.copy(goalQuantity = goalRequest.goalQuantity!!)
goalRepository.save(updatedGoal)
cacheService.invalidate(CacheType.GOALS_CACHES)
logger.info { "Updated goal. Before: $goal | After: $updatedGoal" }
return updatedGoal.toGoalResponse(id = updatedGoal.id)
}
fun deleteGoal(goalId: String) {
goalRepository.findById(goalId)
.ifPresentOrElse({
goalRepository.deleteById(goalId)
cryptoService.deleteCryptoIfNotUsed(it.coingeckoCryptoId)
cacheService.invalidate(CacheType.GOALS_CACHES)
logger.info { "Deleted goal $it" }
}, {
throw GoalNotFoundException(GOAL_ID_NOT_FOUND.format(goalId))
})
}
private fun Goal.toGoalResponse(id: String): GoalResponse {
val crypto = cryptoService.retrieveCryptoInfoById(coingeckoCryptoId)
val cryptoInfo = crypto.toCryptoInfo()
val userCryptos = userCryptoService.findAllByCoingeckoCryptoId(coingeckoCryptoId)
val actualQuantity = userCryptos.map { it.quantity }
.fold(BigDecimal.ZERO, BigDecimal::add)
val progress = getProgress(goalQuantity, actualQuantity)
val remainingQuantity = getRemainingQuantity(goalQuantity, actualQuantity)
val moneyNeeded = crypto.getMoneyNeeded(remainingQuantity)
return toGoalResponse(id, cryptoInfo, actualQuantity, progress, remainingQuantity, moneyNeeded)
}
private fun getRemainingQuantity(goalQuantity: BigDecimal, actualQuantity: BigDecimal): BigDecimal {
return if (goalQuantity <= actualQuantity) BigDecimal.ZERO else goalQuantity.minus(actualQuantity)
}
private fun getProgress(goalQuantity: BigDecimal, actualQuantity: BigDecimal): Float {
return if (goalQuantity <= actualQuantity) 100F else actualQuantity.multiply(BigDecimal("100"))
.divide(goalQuantity, RoundingMode.HALF_UP)
.setScale(2, RoundingMode.DOWN)
.toFloat()
}
private fun Crypto.getMoneyNeeded(remainingQuantity: BigDecimal): BigDecimal {
return lastKnownPrice.multiply(remainingQuantity).setScale(2, RoundingMode.HALF_UP)
}
}
class GoalNotFoundException(message: String) : RuntimeException(message)
class DuplicatedGoalException(message: String) : RuntimeException(message)