Skip to content

Commit c3eed52

Browse files
committed
ExtendedEconomy - backpay command
1 parent f1c8541 commit c3eed52

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed

extendedeconomy/commands/admin.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import calendar
2+
import io
23
import logging
34
import typing as t
45
from datetime import datetime, timezone
@@ -7,6 +8,7 @@
78
from discord import app_commands
89
from redbot.core import Config, bank, commands
910
from redbot.core.i18n import Translator, cog_i18n
11+
from redbot.core.utils.chat_formatting import humanize_number, humanize_timedelta
1012

1113
from ..abc import MixinMeta
1214
from ..common.models import CommandCost
@@ -548,3 +550,242 @@ async def bank_set_role(self, ctx: commands.Context, role: discord.Role, creds:
548550
f"{users_affected} {grammar}", f"{total} {currency}"
549551
)
550552
await ctx.send(msg)
553+
554+
@commands.command(name="backpay")
555+
@bank.is_owner_if_bank_global()
556+
@commands.admin_or_permissions(manage_guild=True)
557+
@commands.guild_only()
558+
async def backpay_cmd(self, ctx: commands.Context, duration: commands.TimedeltaConverter, confirm: bool = False):
559+
"""Calculate and award missed paydays for all members within a time period.
560+
561+
This will calculate how many paydays each member could have claimed within the
562+
specified time period and award them accordingly.
563+
564+
By default, this command will only show a preview. Use `confirm=True` to apply changes.
565+
566+
Examples:
567+
- `[p]backpay 48h` - Preview paydays missed in the last 48 hours
568+
- `[p]backpay 7d True` - Calculate and give paydays missed in the last 7 days
569+
570+
**Arguments**
571+
- `<duration>` How far back to check for missed paydays. Use time abbreviations like 1d, 12h, etc.
572+
- `[confirm]` Set to True to actually apply the changes. Default: False (preview only)
573+
"""
574+
if await bank.is_global() and ctx.author.id not in self.bot.owner_ids:
575+
return await ctx.send(_("You must be a bot owner to use this command while global bank is active."))
576+
577+
if duration.total_seconds() <= 0:
578+
return await ctx.send(_("Duration must be positive!"))
579+
580+
async with ctx.typing():
581+
eco_cog = self.bot.get_cog("Economy")
582+
if not eco_cog:
583+
return await ctx.send(_("Economy cog is not loaded."))
584+
585+
eco_conf: Config = eco_cog.config
586+
is_global = await bank.is_global()
587+
currency = await bank.get_currency_name(ctx.guild)
588+
max_bal = await bank.get_max_balance(ctx.guild)
589+
590+
# Get current time and the time to look back to
591+
current_time = calendar.timegm(datetime.now(tz=timezone.utc).utctimetuple())
592+
lookback_time = current_time - int(duration.total_seconds())
593+
594+
# Create a list to store the report data
595+
report_data = []
596+
users_updated = 0
597+
total_credits = 0
598+
599+
if is_global:
600+
bankgroup = bank._config._get_base_group(bank._config.USER)
601+
ecogroup = eco_conf._get_base_group(eco_conf.USER)
602+
accounts: t.Dict[str, dict] = await bankgroup.all()
603+
ecousers: t.Dict[str, dict] = await ecogroup.all()
604+
payday_time = await eco_conf.PAYDAY_TIME()
605+
payday_credits = await eco_conf.PAYDAY_CREDITS()
606+
607+
for member in ctx.guild.members:
608+
uid = str(member.id)
609+
610+
# Skip users with no bank accounts or economy data
611+
if uid not in accounts or uid not in ecousers:
612+
continue
613+
614+
# Get their last payday time
615+
last_payday = ecousers[uid].get("next_payday", 0)
616+
617+
# If their last payday is after our lookback time, skip
618+
if last_payday > lookback_time:
619+
continue
620+
621+
# Calculate how many paydays they could have claimed
622+
potential_paydays = (current_time - last_payday) // payday_time
623+
if potential_paydays <= 0:
624+
continue
625+
626+
# Calculate amount to give (base amount * number of paydays)
627+
amount_to_give = payday_credits * potential_paydays
628+
629+
# Don't exceed max balance
630+
current_balance = accounts[uid]["balance"]
631+
new_balance = min(current_balance + amount_to_give, max_bal)
632+
added_amount = new_balance - current_balance
633+
634+
# Add to report even if no credits are added due to max balance
635+
report_data.append(
636+
{
637+
"name": member.display_name,
638+
"id": member.id,
639+
"paydays": potential_paydays,
640+
"amount": added_amount,
641+
"current_balance": current_balance,
642+
"new_balance": new_balance,
643+
"max_hit": added_amount < amount_to_give,
644+
}
645+
)
646+
647+
# Track stats
648+
total_credits += added_amount
649+
users_updated += 1
650+
651+
# Only update the account if confirm is True
652+
if confirm:
653+
accounts[uid]["balance"] = new_balance
654+
ecousers[uid]["next_payday"] = current_time
655+
656+
# Save changes if any and confirmation is True
657+
if users_updated > 0 and confirm:
658+
await bankgroup.set(accounts)
659+
await ecogroup.set(ecousers)
660+
661+
else:
662+
# Per-guild logic
663+
conf = self.db.get_conf(ctx.guild)
664+
bankgroup = bank._config._get_base_group(bank._config.MEMBER, str(ctx.guild.id))
665+
ecogroup = eco_conf._get_base_group(eco_conf.MEMBER, str(ctx.guild.id))
666+
accounts: t.Dict[str, dict] = await bankgroup.all()
667+
ecousers: t.Dict[str, dict] = await ecogroup.all()
668+
payday_time = await eco_conf.guild(ctx.guild).PAYDAY_TIME()
669+
payday_credits = await eco_conf.guild(ctx.guild).PAYDAY_CREDITS()
670+
payday_roles: t.Dict[int, dict] = await eco_conf.all_roles()
671+
672+
for member in ctx.guild.members:
673+
uid = str(member.id)
674+
675+
# Skip users with no bank accounts or economy data
676+
if uid not in accounts or uid not in ecousers:
677+
continue
678+
679+
# Get their last payday time
680+
last_payday = ecousers[uid].get("next_payday", 0)
681+
682+
# If their last payday is after our lookback time, skip
683+
if last_payday > lookback_time:
684+
continue
685+
686+
# Calculate how many paydays they could have claimed
687+
potential_paydays = (current_time - last_payday) // payday_time
688+
if potential_paydays <= 0:
689+
continue
690+
691+
# Calculate per-payday amount with role bonuses
692+
base_amount = payday_credits
693+
for role in member.roles:
694+
if role.id in payday_roles:
695+
role_credits = payday_roles[role.id]["PAYDAY_CREDITS"]
696+
if conf.stack_paydays:
697+
base_amount += role_credits
698+
elif role_credits > base_amount:
699+
base_amount = role_credits
700+
701+
# Apply role bonus multipliers if configured
702+
if conf.role_bonuses and any(role.id in conf.role_bonuses for role in member.roles):
703+
highest_bonus = max(conf.role_bonuses.get(role.id, 0) for role in member.roles)
704+
base_amount += round(base_amount * highest_bonus)
705+
706+
# Calculate total amount to give
707+
amount_to_give = base_amount * potential_paydays
708+
709+
# Don't exceed max balance
710+
current_balance = accounts[uid]["balance"]
711+
new_balance = min(current_balance + amount_to_give, max_bal)
712+
added_amount = new_balance - current_balance
713+
714+
# Add to report even if no credits are added due to max balance
715+
report_data.append(
716+
{
717+
"name": member.display_name,
718+
"id": member.id,
719+
"paydays": potential_paydays,
720+
"amount": added_amount,
721+
"current_balance": current_balance,
722+
"new_balance": new_balance,
723+
"max_hit": added_amount < amount_to_give,
724+
"payday_value": base_amount,
725+
}
726+
)
727+
728+
# Track stats
729+
total_credits += added_amount
730+
users_updated += 1
731+
732+
# Only update the account if confirm is True
733+
if confirm:
734+
accounts[uid]["balance"] = new_balance
735+
ecousers[uid]["next_payday"] = current_time
736+
737+
# Save changes if any and confirmation is True
738+
if users_updated > 0 and confirm:
739+
await bankgroup.set(accounts)
740+
await ecogroup.set(ecousers)
741+
742+
# Generate report
743+
if users_updated > 0:
744+
# Sort by amount in descending order
745+
report_data.sort(key=lambda x: x["amount"], reverse=True)
746+
747+
report_lines = [
748+
f"# Backpay Report for {humanize_timedelta(seconds=int(duration.total_seconds()))}\n",
749+
f"Total Users: {users_updated}",
750+
f"Total Credits: {humanize_number(total_credits)} {currency}\n",
751+
"Details:",
752+
]
753+
754+
for entry in report_data:
755+
max_note = " (Max balance hit)" if entry.get("max_hit") else ""
756+
per_payday = (
757+
f" ({humanize_number(entry.get('payday_value', payday_credits))}/payday)"
758+
if "payday_value" in entry
759+
else ""
760+
)
761+
report_lines.append(
762+
f"{entry['name']} (ID: {entry['id']}): "
763+
f"{humanize_number(entry['amount'])} {currency} for {entry['paydays']} paydays{per_payday}{max_note}"
764+
)
765+
766+
report_text = "\n".join(report_lines)
767+
report_file = discord.File(io.StringIO(report_text), filename=f"backpay_report_{ctx.guild.id}.txt")
768+
769+
if confirm:
770+
msg = _(
771+
"Backpay complete for {duration}!\n{users} users received a total of {credits} {currency}."
772+
).format(
773+
duration=humanize_timedelta(seconds=int(duration.total_seconds())),
774+
users=humanize_number(users_updated),
775+
credits=humanize_number(total_credits),
776+
currency=currency,
777+
)
778+
await ctx.send(msg, file=report_file)
779+
else:
780+
msg = _(
781+
"Backpay preview for {duration}:\n{users} users would receive a total of {credits} {currency}.\n"
782+
"Run with `confirm=True` to apply these changes."
783+
).format(
784+
duration=humanize_timedelta(seconds=int(duration.total_seconds())),
785+
users=humanize_number(users_updated),
786+
credits=humanize_number(total_credits),
787+
currency=currency,
788+
)
789+
await ctx.send(msg, file=report_file)
790+
else:
791+
await ctx.send(_("No users were eligible for backpay in that time period."))

extendedeconomy/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ExtendedEconomy(
3333
"""
3434

3535
__author__ = "[vertyco](https://github.com/vertyco/vrt-cogs)"
36-
__version__ = "0.6.0"
36+
__version__ = "0.7.0"
3737

3838
def __init__(self, bot: Red):
3939
super().__init__()

0 commit comments

Comments
 (0)