PriceTargetService.kt

package com.distasilucas.cryptobalancetracker.service

import com.distasilucas.cryptobalancetracker.constants.PRICE_TARGET_ID_CACHE
import com.distasilucas.cryptobalancetracker.constants.PRICE_TARGET_RESPONSE_ID_CACHE
import com.distasilucas.cryptobalancetracker.constants.PRICE_TARGET_RESPONSE_PAGE_CACHE
import com.distasilucas.cryptobalancetracker.entity.PriceTarget
import com.distasilucas.cryptobalancetracker.model.request.pricetarget.PriceTargetRequest
import com.distasilucas.cryptobalancetracker.model.response.pricetarget.PagePriceTargetResponse
import com.distasilucas.cryptobalancetracker.model.response.pricetarget.PriceTargetResponse
import com.distasilucas.cryptobalancetracker.repository.PriceTargetRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.cache.annotation.Cacheable
import org.springframework.context.annotation.Scope
import org.springframework.context.annotation.ScopedProxyMode
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import java.math.BigDecimal

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class PriceTargetService(
  private val priceTargetRepository: PriceTargetRepository,
  private val cryptoService: CryptoService,
  private val cacheService: CacheService,
  private val _priceTargetService: PriceTargetService?
) {

  private val logger = KotlinLogging.logger { }

  @Cacheable(cacheNames = [PRICE_TARGET_ID_CACHE], key = "#priceTargetId")
  fun findById(priceTargetId: String): PriceTarget {
    return priceTargetRepository.findById(priceTargetId)
      .orElseThrow { PriceTargetNotFoundException("Price target with id $priceTargetId not found") }
  }

  @Cacheable(cacheNames = [PRICE_TARGET_RESPONSE_ID_CACHE], key = "#priceTargetId")
  fun retrievePriceTarget(priceTargetId: String): PriceTargetResponse {
    logger.info { "Retrieving price target for id $priceTargetId" }

    val priceTarget = _priceTargetService!!.findById(priceTargetId)
    val crypto = cryptoService.retrieveCryptoInfoById(priceTarget.coingeckoCryptoId)
    val changeNeeded = priceTarget.calculateChangeNeeded(crypto.lastKnownPrice)
    val cryptoInfo = crypto.toCryptoInfo()

    return priceTarget.toPriceTargetResponse(cryptoInfo, crypto.lastKnownPrice, changeNeeded)
  }

  @Cacheable(cacheNames = [PRICE_TARGET_RESPONSE_PAGE_CACHE], key = "#page")
  fun retrievePriceTargetsByPage(page: Int): PagePriceTargetResponse {
    logger.info { "Retrieving price targets for page $page" }

    val pageRequest = PageRequest.of(page, 10)
    val priceTargets = priceTargetRepository.findAll(pageRequest)
    val priceTargetsResponse = priceTargets.content.map {
      val crypto = cryptoService.retrieveCryptoInfoById(it.coingeckoCryptoId)
      val cryptoInfo = crypto.toCryptoInfo()
      it.toPriceTargetResponse(cryptoInfo, crypto.lastKnownPrice, it.calculateChangeNeeded(crypto.lastKnownPrice))
    }.toList()

    return PagePriceTargetResponse(page, priceTargets.totalPages, priceTargetsResponse)
  }

  fun savePriceTarget(priceTargetRequest: PriceTargetRequest): PriceTargetResponse {
    logger.info { "Saving price target $priceTargetRequest" }

    val coingeckoCrypto = cryptoService.retrieveCoingeckoCryptoInfoByNameOrId(priceTargetRequest.cryptoNameOrId!!)
    validatePriceTargetIsNotDuplicated(coingeckoCrypto.id, priceTargetRequest.priceTarget!!)
    val crypto = cryptoService.retrieveCryptoInfoById(coingeckoCrypto.id)
    val cryptoInfo = crypto.toCryptoInfo()
    val priceTarget = priceTargetRepository.save(priceTargetRequest.toEntity(crypto.id))
    cacheService.invalidate(CacheType.PRICE_TARGETS_CACHES)

    return priceTarget.toPriceTargetResponse(cryptoInfo, crypto.lastKnownPrice, priceTarget.calculateChangeNeeded(crypto.lastKnownPrice))
  }

  fun updatePriceTarget(priceTargetId: String, priceTargetRequest: PriceTargetRequest): PriceTargetResponse {
    logger.info { "Updating price target for id $priceTargetId. New value: $priceTargetRequest" }

    val priceTarget = _priceTargetService!!.findById(priceTargetId).copy(target = priceTargetRequest.priceTarget!!)
    validatePriceTargetIsNotDuplicated(priceTarget.coingeckoCryptoId, priceTargetRequest.priceTarget)
    val crypto = cryptoService.retrieveCryptoInfoById(priceTarget.coingeckoCryptoId)
    val cryptoInfo = crypto.toCryptoInfo()
    val changeNeeded = priceTarget.calculateChangeNeeded(crypto.lastKnownPrice)
    val newPriceTarget = priceTargetRepository.save(priceTarget)
    cacheService.invalidate(CacheType.PRICE_TARGETS_CACHES)

    return newPriceTarget.toPriceTargetResponse(cryptoInfo, crypto.lastKnownPrice, changeNeeded)
  }

  fun deletePriceTarget(priceTargetId: String) {
    logger.info { "Deleting price target for id $priceTargetId" }

    val priceTarget = _priceTargetService!!.findById(priceTargetId)

    priceTargetRepository.delete(priceTarget)
    cacheService.invalidate(CacheType.PRICE_TARGETS_CACHES)
    cryptoService.deleteCryptoIfNotUsed(priceTarget.coingeckoCryptoId)
  }

  private fun validatePriceTargetIsNotDuplicated(coingeckoCryptoId: String, priceTarget: BigDecimal) {
    val optionalPriceTarget: PriceTarget? = priceTargetRepository.findByCoingeckoCryptoIdAndTarget(coingeckoCryptoId, priceTarget)

    if (optionalPriceTarget != null)
      throw DuplicatedPriceTargetException("You already have a price target for $coingeckoCryptoId at that price")
  }
}

class PriceTargetNotFoundException(message: String) : RuntimeException(message)
class DuplicatedPriceTargetException(message: String) : RuntimeException(message)