Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This plugin also has some optional dependencies for expanded features
Includes pay and balance commands for businesses as well as adding an option for paying businesses
* `/firm pay <business> <amount> <user|player>`
* `/firm balance <business>`
* `/firm transactions <business> [page]`
* `/firm list`
* `/pay <amount> <business>`

Expand Down
174 changes: 162 additions & 12 deletions src/main/kotlin/me/zodd/postmanpat/addons/PlayerBusinessAddon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.olziedev.playerbusinesses.api.PlayerBusinessesAPI
import com.olziedev.playerbusinesses.api.business.BStaff
import com.olziedev.playerbusinesses.api.business.Business
import com.olziedev.playerbusinesses.api.business.BusinessPermission
import com.olziedev.playerbusinesses.api.business.transaction.BEntry
import github.scarsz.discordsrv.dependencies.jda.api.EmbedBuilder
import github.scarsz.discordsrv.dependencies.jda.api.events.interaction.SlashCommandEvent
import me.zodd.postmanpat.PostmanPat
Expand All @@ -17,6 +18,13 @@ import me.zodd.postmanpat.econ.PostmanEconManager
import me.zodd.postmanpat.econ.entity.BusinessEntity
import me.zodd.postmanpat.econ.entity.UserEntity
import java.awt.Color
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.UUID
import kotlin.math.max

internal class PlayerBusinessAddon : CommandUtils {

Expand All @@ -25,6 +33,9 @@ internal class PlayerBusinessAddon : CommandUtils {
}

private val econConf get() = PostmanPat.plugin.configManager.conf.moduleConfig.econ
private val timestampFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault())
private val transactionPageSize = 10

internal fun listOwnedBusinesses(command : CommandEvent) {
val embedBuilder = EmbedBuilder()
Expand All @@ -41,11 +52,7 @@ internal class PlayerBusinessAddon : CommandUtils {
}

internal fun firmBal(command : CommandEvent) {
val businessName = command.event["business"]?.asString
val business: Business = pba.getBusinessByName(businessName?.lowercase()) ?: run {
command.event.replyEphemeral("Business by name [$businessName] was not found!").queue()
return
}
val business = command.businessOrReply() ?: return
command.event.replyEphemeralEmbed(
embedMessage(
"Balance for ${business.name}",
Expand All @@ -56,17 +63,12 @@ internal class PlayerBusinessAddon : CommandUtils {


internal fun firmPay(command : CommandEvent) {
val businessName = command.event["business"]?.asString

val targetUser = command.event.userOrPlayerArg() ?: run {
command.event.replyEphemeral("Unable to find user! Ensure name is spelled correctly or try @tagging them").queue()
return
}

val business: Business = pba.getBusinessByName(businessName?.lowercase()) ?: run {
command.event.replyEphemeral("Business by name [$businessName] was not found!").queue()
return
}
val business = command.businessOrReply() ?: return

command.sender.hasFirmPermission(command.event, business) ?: return

Expand All @@ -75,6 +77,154 @@ internal class PlayerBusinessAddon : CommandUtils {
PostmanEconManager(businessSender, command.event).transferFunds(receiver)
}

internal fun firmTransactions(command: CommandEvent) {
val business = command.businessOrReply() ?: return
command.sender.hasFirmPermission(command.event, business) ?: return

val transactions = business.transactions.getTransactions()
.sortedByDescending { it.time }

if (transactions.isEmpty()) {
command.event.replyEphemeral("Business ${business.name} has no transactions to export.").queue()
return
}

val requestedPage = command.event["page"]?.asLong?.toInt()
val export = buildTransactionExport(business, command.sender.name, transactions, requestedPage)
?: run {
command.event.replyEphemeral("Page $requestedPage is out of range for ${business.name}.").queue()
return
}
val fileName = buildTransactionFileName(business.name, requestedPage)

command.event.reply("Attached is the transaction export for ${business.name}.")
.addFile(export.toByteArray(StandardCharsets.UTF_8), fileName)
.setEphemeral(true)
.queue()
}

private fun buildTransactionExport(
business: Business,
requestedBy: String,
transactions: List<BEntry>,
requestedPage: Int?
): String? {
val pages = transactions.chunked(transactionPageSize)
val totalPages = max(pages.size, 1)

val pageExports = if (requestedPage == null) {
pages.mapIndexed { index, pageTransactions ->
buildTransactionPage(business.name, index + 1, totalPages, pageTransactions)
}
} else {
val pageIndex = requestedPage - 1
val pageTransactions = pages.getOrNull(pageIndex) ?: return null
listOf(buildTransactionPage(business.name, requestedPage, totalPages, pageTransactions))
}

return buildString {
appendLine("Business Transactions Export")
appendLine("Business: ${business.name}")
appendLine("Requested by: $requestedBy")
appendLine("Exported at: ${timestampFormat.format(Instant.now())}")
appendLine("Total entries: ${transactions.size}")
appendLine("Page size: $transactionPageSize")
appendLine()
append(pageExports.joinToString("\n\n"))
}
}

private fun buildTransactionPage(
businessName: String,
pageNumber: Int,
totalPages: Int,
transactions: List<BEntry>
): String {
return buildString {
appendLine("BUSINESS TRANSACTIONS >> $businessName (Page $pageNumber/$totalPages)")
transactions.forEach { appendLine(formatTransactionEntry(it)) }
}.trimEnd()
}

private fun formatTransactionEntry(entry: BEntry): String {
val actor = resolvePlayerName(entry.uuid)
val amount = "${econConf.currencySymbol}${econConf.decimalFormat().format(entry.amount)}"
val action = entry.actionText(actor, amount)
return "${timestampFormat.format(Instant.ofEpochMilli(entry.time))} $action"
}

private fun resolvePlayerName(uuid: UUID): String {
return PostmanPat.plugin.ess.getUser(uuid)?.name
?: PostmanPat.plugin.server.getOfflinePlayer(uuid).name
?: uuid.toString()
}

private fun reflectTarget(entry: BEntry): String? {
val target = entry.invokeAccessor("getTargetUUID", "getTarget")
val targetUuid = when (target) {
is UUID -> target
is String -> runCatching { UUID.fromString(target) }.getOrNull()
else -> null
}

return targetUuid?.let(::resolvePlayerName)
}

private fun reflectReason(entry: BEntry): String? {
return entry.invokeAccessor("getReason") as? String
}

private fun BEntry.actionText(actor: String, amount: String): String {
val normalizedType = type.lowercase(Locale.ENGLISH)
val target = reflectTarget(this)
val reason = reflectReason(this)?.normalizeWhitespace()

val baseText = when (normalizedType) {
"deposit", "deposited" -> "$actor deposited $amount"
"withdraw", "withdrawn", "withdrew" -> "$actor withdrew $amount"
"paid", "pay" -> target?.let { "$actor paid $amount to $it" } ?: "$actor paid $amount"
"tax", "taxed" -> "$actor taxed $amount"
else -> "$actor $normalizedType $amount"
}

return reason?.takeIf { it.isNotBlank() }?.let { "$baseText [$it]" } ?: baseText
}

private fun BEntry.invokeAccessor(vararg methodNames: String): Any? {
return methodNames.firstNotNullOfOrNull { method ->
runCatching {
javaClass.methods.firstOrNull {
it.name == method && it.parameterCount == 0
}?.invoke(this)
}.getOrNull()
}
}

private fun String.normalizeWhitespace(): String {
return replace("\\s+".toRegex(), " ").trim()
}

private fun String.sanitizedFileName(): String {
return lowercase(Locale.ENGLISH)
.replace("[^a-z0-9._-]+".toRegex(), "-")
.trim('-')
.ifBlank { "business" }
}

private fun buildTransactionFileName(businessName: String, requestedPage: Int?): String {
val baseName = businessName.sanitizedFileName()
return requestedPage?.let { "$baseName-transactions-page-$it.txt" }
?: "$baseName-transactions-export.txt"
}

private fun CommandEvent.businessOrReply(): Business? {
val businessName = event["business"]?.asString
return pba.getBusinessByName(businessName?.lowercase()) ?: run {
event.replyEphemeral("Business by name [$businessName] was not found!").queue()
null
}
}

private fun User.hasFirmPermission(event: SlashCommandEvent, business: Business): BStaff? {
return business.staff?.firstOrNull {
val permCheck = it.role.permission
Expand All @@ -91,4 +241,4 @@ internal class PlayerBusinessAddon : CommandUtils {
internal fun businessByName(name: String): Business? {
return pba.getBusinessByName(name.lowercase())
}
}
}
4 changes: 3 additions & 1 deletion src/main/kotlin/me/zodd/postmanpat/econ/EconConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal data class EconConfig(
val firmPayCommand: String = "pay",
@field:Comment("Sub command for displaying firm balance")
val firmBalCommand : String = "balance",
@field:Comment("Sub command for exporting firm transactions")
val firmTransactionsCommand: String = "transactions",
@field:Comment("Sub command for displaying owned businesses")
val firmListBusinesses : String = "list",
@field:Comment("Whether the Economy module is enabled")
Expand All @@ -30,4 +32,4 @@ internal data class EconConfig(
internal fun decimalFormat(): DecimalFormat {
return DecimalFormat(decimalFormat)
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/me/zodd/postmanpat/econ/EconSlashCommands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ internal class EconSlashCommands : PostmanCommandProvider {
plugin.configManager.conf.moduleConfig.econ.firmListBusinesses,
"Command to check a business' balance"
),
ECON_FIRM_TRANSACTIONS(
plugin.configManager.conf.moduleConfig.econ.firmTransactionsCommand,
"Export a business transaction history"
),
ECON_FIRM_BALANCE(
plugin.configManager.conf.moduleConfig.econ.firmBalCommand,
"Lists businesses you have financial access to"
Expand All @@ -48,6 +52,10 @@ internal class EconSlashCommands : PostmanCommandProvider {
plugin.pba?.let { it::listOwnedBusinesses } ?: emptyCommand()
}

ECON_FIRM_TRANSACTIONS -> {
plugin.pba?.let { it::firmTransactions } ?: emptyCommand()
}

ECON_FIRM_BALANCE -> {
plugin.pba?.let {
it::firmBal
Expand Down Expand Up @@ -127,6 +135,13 @@ internal class EconSlashCommands : PostmanCommandProvider {
.addOption(
OptionType.STRING, "business", "business to check balance of", true
),
EconCommands.ECON_FIRM_TRANSACTIONS.subcommandData()
.addOption(
OptionType.STRING, "business", "business to export transactions for", true
)
.addOption(
OptionType.INTEGER, "page", "page to export from the in game transaction list", false
),
EconCommands.ECON_FIRM_LIST.subcommandData()
)
)
Expand Down