TransferCryptoService.kt

package com.distasilucas.cryptobalancetracker.service

import com.distasilucas.cryptobalancetracker.constants.NOT_ENOUGH_BALANCE
import com.distasilucas.cryptobalancetracker.constants.SAME_FROM_TO_PLATFORM
import com.distasilucas.cryptobalancetracker.entity.UserCrypto
import com.distasilucas.cryptobalancetracker.exception.ApiException
import com.distasilucas.cryptobalancetracker.model.request.crypto.TransferCryptoRequest
import com.distasilucas.cryptobalancetracker.model.response.crypto.TransferCryptoResponse
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.util.UUID

@Service
class TransferCryptoService(
  private val userCryptoService: UserCryptoService,
  private val platformService: PlatformService
) {

  private val logger = KotlinLogging.logger { }

  fun transferCrypto(transferCryptoRequest: TransferCryptoRequest): TransferCryptoResponse {
    val toPlatformResponse = platformService.retrievePlatformById(transferCryptoRequest.toPlatformId!!)
    val userCryptoToTransfer = userCryptoService.findByUserCryptoId(transferCryptoRequest.userCryptoId!!)
    val fromPlatformResponse = platformService.retrievePlatformById(userCryptoToTransfer.platformId)

    if (isToAndFromSamePlatform(toPlatformResponse.id, fromPlatformResponse.id)) {
      throw ApiException(HttpStatus.BAD_REQUEST, SAME_FROM_TO_PLATFORM)
    }

    val availableQuantity = userCryptoToTransfer.quantity
    val quantityToTransfer = transferCryptoRequest.quantityToTransfer

    if (transferCryptoRequest.hasInsufficientBalance(availableQuantity)) {
      throw InsufficientBalanceException(NOT_ENOUGH_BALANCE)
    }

    val remainingCryptoQuantity = transferCryptoRequest.calculateRemainingCryptoQuantity(availableQuantity)
    val quantityToSendReceive = transferCryptoRequest.calculateQuantityToSendReceive(remainingCryptoQuantity, availableQuantity)
    val toPlatformUserCrypto = userCryptoService.findByCoingeckoCryptoIdAndPlatformId(
      userCryptoToTransfer.coingeckoCryptoId,
      transferCryptoRequest.toPlatformId
    )

    var transferCryptoResponse: TransferCryptoResponse? = null

    if (doesFromPlatformHaveRemaining(remainingCryptoQuantity) && toPlatformUserCrypto != null) {
      val newQuantity = toPlatformUserCrypto.quantity.add(quantityToSendReceive)
      val updatedFromPlatformUserCrypto = userCryptoToTransfer.copy(quantity = remainingCryptoQuantity)
      val updatedToPlatformUserCrypto = toPlatformUserCrypto.copy(quantity = newQuantity)

      userCryptoService.saveOrUpdateAll(listOf(updatedFromPlatformUserCrypto, updatedToPlatformUserCrypto))

      transferCryptoResponse = transferCryptoRequest.toTransferCryptoResponse(
        remainingCryptoQuantity,
        newQuantity,
        quantityToSendReceive
      )
    }

    if (doesFromPlatformHaveRemaining(remainingCryptoQuantity) && toPlatformUserCrypto == null) {
      val uuid = UUID.randomUUID().toString()
      val updatedToPlatformUserCrypto = UserCrypto(
        uuid,
        userCryptoToTransfer.coingeckoCryptoId,
        quantityToSendReceive,
        transferCryptoRequest.toPlatformId
      )
      val updatedUserCryptoToTransfer = userCryptoToTransfer.copy(quantity = remainingCryptoQuantity)

      if (transferCryptoRequest.sendFullQuantity == true) {
        userCryptoService.saveOrUpdateAll(listOf(updatedUserCryptoToTransfer, updatedToPlatformUserCrypto))
      } else {
        if (quantityToSendReceive > BigDecimal.ZERO) {
          userCryptoService.saveOrUpdateAll(listOf(updatedUserCryptoToTransfer, updatedToPlatformUserCrypto))
        } else {
          userCryptoService.saveOrUpdateAll(listOf(updatedUserCryptoToTransfer))
        }
      }

      transferCryptoResponse = transferCryptoRequest.toTransferCryptoResponse(
        remainingCryptoQuantity,
        quantityToSendReceive,
        quantityToSendReceive
      )
    }

    if (!doesFromPlatformHaveRemaining(remainingCryptoQuantity) && toPlatformUserCrypto != null) {
      val newQuantity = toPlatformUserCrypto.quantity.add(quantityToSendReceive)
      val updatedToPlatformUserCrypto = toPlatformUserCrypto.copy(quantity = newQuantity)

      userCryptoService.deleteUserCrypto(userCryptoToTransfer.id)
      userCryptoService.saveOrUpdateAll(listOf(updatedToPlatformUserCrypto))

      transferCryptoResponse = transferCryptoRequest.toTransferCryptoResponse(
        remainingCryptoQuantity,
        newQuantity,
        quantityToSendReceive
      )
    }

    if (!doesFromPlatformHaveRemaining(remainingCryptoQuantity) && toPlatformUserCrypto == null) {
      val updatedFromPlatformUserCrypto = UserCrypto(
        userCryptoToTransfer.id,
        userCryptoToTransfer.coingeckoCryptoId,
        quantityToSendReceive,
        toPlatformResponse.id
      )

      if (updatedFromPlatformUserCrypto.quantity > BigDecimal.ZERO) {
        userCryptoService.saveOrUpdateAll(listOf(updatedFromPlatformUserCrypto))
      } else {
        userCryptoService.deleteUserCrypto(updatedFromPlatformUserCrypto.id)
      }

      transferCryptoResponse = transferCryptoRequest.toTransferCryptoResponse(
        remainingCryptoQuantity,
        quantityToSendReceive,
        quantityToSendReceive
      )
    }

    logger.info { "Transferred $quantityToTransfer of ${userCryptoToTransfer.coingeckoCryptoId} from platform ${fromPlatformResponse.name} to ${toPlatformResponse.name}" }

    return transferCryptoResponse!!
  }

  private fun isToAndFromSamePlatform(toPlatformId: String, fromPlatformId: String) = toPlatformId == fromPlatformId

  private fun doesFromPlatformHaveRemaining(remainingCryptoQuantity: BigDecimal) = remainingCryptoQuantity > BigDecimal.ZERO
}

class InsufficientBalanceException(message: String) : RuntimeException(message)