<commit_before>package net.perfectdreams.loritta.utils

import com.mrpowergamerbr.loritta.utils.extensions.await
import com.mrpowergamerbr.loritta.utils.lorittaShards
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging
import net.perfectdreams.loritta.platform.discord.LorittaDiscord
import net.perfectdreams.loritta.tables.SonhosTransaction
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.kotlin.utils.addToStdlib.sumByLong

object PaymentUtils {
    private val logger = KotlinLogging.logger {}
    var economyEnabled = true

    fun addToTransactionLogNested(
            quantity: Long,
            reason: SonhosPaymentReason,
            receivedBy: Long? = null,
            givenBy: Long? = null,
            givenAtMillis: Long = System.currentTimeMillis()
    ) {
        if (receivedBy == null && givenBy == null)
            throw IllegalArgumentException("receivedBy and givenBy is null! One of them must NOT be null!")

        SonhosTransaction.insert {
            it[SonhosTransaction.givenBy] = givenBy
            it[SonhosTransaction.receivedBy] = receivedBy
            it[givenAt] = givenAtMillis
            it[SonhosTransaction.quantity] = quantity.toBigDecimal()
            it[SonhosTransaction.reason] = reason
        }

        logger.info { "Added transaction $reason with $quantity that was given by $givenBy and received by $receivedBy at $givenAtMillis with reason $reason" }
    }

    val mutex = Mutex()

    private val List<SonhosRemovalData>?.totalQuantity
        get() = this?.sumByLong { it.quantity } ?: 0L

    /**
     * Removes sonhos from [userId] due to a chargeback of sonhos value [quantity]
     *
     * @param loritta              the loritta instance
     * @param userId               the user ID that should be checked
     * @param quantity             the quantity of the chargeback
     * @param removeSonhos         if sonhos should be removed in the check
     * @param notifyChargebackUser if enabled, the user that triggered the chargeback will receive a DM
     * @param notifyUsers          if enabled, users that had their sonhos removed will be notified
     * @return a map containing all the users and sonhos removals that were done
     */
    suspend fun removeSonhosDueToChargeback(
            loritta: LorittaDiscord,
            userId: Long,
            quantity: Long,
            removeSonhos: Boolean,
            notifyChargebackUser: Boolean,
            notifyUsers: Boolean
    ): Map<Long, MutableList<SonhosRemovalData>> {
        val triggeredSonhos = mutableMapOf<Long, MutableList<SonhosRemovalData>>()

        // We lock in a mutex because a lot of times users spam chargebacks, which could cause issues when taking out money
        mutex.withLock {
            retrieveSonhosRemovalDueToChargeback(
                    loritta,
                    userId,
                    quantity,
                    triggeredSonhos,
                    listOf(),
                    "self"
            )

            if (removeSonhos) {
                for (entry in triggeredSonhos) {
                    // Time to remove all the sonhos
                    val totalQuantity = entry.value.totalQuantity

                    // Yes, this can happen! We should figure out why this happens, but it is not a huge issue so, whatever...
                    if (0L >= totalQuantity)
                        continue

                    val profile = loritta.getOrCreateLorittaProfile(entry.key)
                    loritta.newSuspendedTransaction {
                        logger.info { "Taking $quantity sonhos from ${entry.key} due to chargeback" }
                        // Take the sonhos
                        profile.takeSonhosNested(
                                totalQuantity,
                                // Maybe the user was spending stuff while we were checking for their sonhos, so let's ignore negative money issues
                                failIfQuantityIsSmallerThanWhatUserHas = false
                        )

                        // And don't forget to log it!
                        addToTransactionLogNested(
                                totalQuantity,
                                SonhosPaymentReason.CHARGEBACK,
                                givenBy = entry.key
                        )
                    }
                }<commit_msg>Fix incorrect quantity when removing sonhos due to chargeback<commit_after>package net.perfectdreams.loritta.utils

import com.mrpowergamerbr.loritta.utils.extensions.await
import com.mrpowergamerbr.loritta.utils.lorittaShards
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging
import net.perfectdreams.loritta.platform.discord.LorittaDiscord
import net.perfectdreams.loritta.tables.SonhosTransaction
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.kotlin.utils.addToStdlib.sumByLong

object PaymentUtils {
    private val logger = KotlinLogging.logger {}
    var economyEnabled = true

    fun addToTransactionLogNested(
            quantity: Long,
            reason: SonhosPaymentReason,
            receivedBy: Long? = null,
            givenBy: Long? = null,
            givenAtMillis: Long = System.currentTimeMillis()
    ) {
        if (receivedBy == null && givenBy == null)
            throw IllegalArgumentException("receivedBy and givenBy is null! One of them must NOT be null!")

        SonhosTransaction.insert {
            it[SonhosTransaction.givenBy] = givenBy
            it[SonhosTransaction.receivedBy] = receivedBy
            it[givenAt] = givenAtMillis
            it[SonhosTransaction.quantity] = quantity.toBigDecimal()
            it[SonhosTransaction.reason] = reason
        }

        logger.info { "Added transaction $reason with $quantity that was given by $givenBy and received by $receivedBy at $givenAtMillis with reason $reason" }
    }

    val mutex = Mutex()

    private val List<SonhosRemovalData>?.totalQuantity
        get() = this?.sumByLong { it.quantity } ?: 0L

    /**
     * Removes sonhos from [userId] due to a chargeback of sonhos value [quantity]
     *
     * @param loritta              the loritta instance
     * @param userId               the user ID that should be checked
     * @param quantity             the quantity of the chargeback
     * @param removeSonhos         if sonhos should be removed in the check
     * @param notifyChargebackUser if enabled, the user that triggered the chargeback will receive a DM
     * @param notifyUsers          if enabled, users that had their sonhos removed will be notified
     * @return a map containing all the users and sonhos removals that were done
     */
    suspend fun removeSonhosDueToChargeback(
            loritta: LorittaDiscord,
            userId: Long,
            quantity: Long,
            removeSonhos: Boolean,
            notifyChargebackUser: Boolean,
            notifyUsers: Boolean
    ): Map<Long, MutableList<SonhosRemovalData>> {
        val triggeredSonhos = mutableMapOf<Long, MutableList<SonhosRemovalData>>()

        // We lock in a mutex because a lot of times users spam chargebacks, which could cause issues when taking out money
        mutex.withLock {
            retrieveSonhosRemovalDueToChargeback(
                    loritta,
                    userId,
                    quantity,
                    triggeredSonhos,
                    listOf(),
                    "self"
            )

            if (removeSonhos) {
                for (entry in triggeredSonhos) {
                    // Time to remove all the sonhos
                    val totalQuantity = entry.value.totalQuantity

                    // Yes, this can happen! We should figure out why this happens, but it is not a huge issue so, whatever...
                    if (0L >= totalQuantity)
                        continue

                    val profile = loritta.getOrCreateLorittaProfile(entry.key)
                    loritta.newSuspendedTransaction {
                        logger.info { "Taking $totalQuantity sonhos from ${entry.key} due to chargeback" }
                        // Take the sonhos
                        profile.takeSonhosNested(
                                totalQuantity,
                                // Maybe the user was spending stuff while we were checking for their sonhos, so let's ignore negative money issues
                                failIfQuantityIsSmallerThanWhatUserHas = false
                        )

                        // And don't forget to log it!
                        addToTransactionLogNested(
                                totalQuantity,
                                SonhosPaymentReason.CHARGEBACK,
                                givenBy = entry.key
                        )
                    }
                }
{'fraction_non_alphanumeric': 0.06160567995630803, 'fraction_numerical': 0.0004369197160021846, 'mean_word_length': 1.8163641956321133, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5713873', 'n_tokens_mistral': 2400, 'n_tokens_neox': 2210, 'n_words': 670}