Skip to content

Commit

Permalink
Adds interest option.
Browse files Browse the repository at this point in the history
Implements a Money class instead of using doubles.
  • Loading branch information
tastybento committed Dec 30, 2020
1 parent bedd88c commit 2600292
Show file tree
Hide file tree
Showing 27 changed files with 466 additions and 141 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>1.2.1</build.version>
<build.version>1.3.0</build.version>
<sonar.projectKey>BentoBoxWorld_Bank</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/world/bentobox/bank/Bank.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void onEnable() {
config.saveConfigObject(settings);
// Bank Manager
bankManager = new BankManager(this);
bankManager.loadBalances().thenRun(() -> bankManager.startInterest());
bankManager.loadBalances();
PhManager placeholderManager = new PhManager(this, bankManager);
// Register commands with GameModes
getPlugin().getAddonsManager().getGameModeAddons().stream()
Expand Down
111 changes: 65 additions & 46 deletions src/main/java/world/bentobox/bank/BankManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import world.bentobox.bank.data.AccountHistory;
import world.bentobox.bank.data.BankAccounts;
import world.bentobox.bank.data.Money;
import world.bentobox.bank.data.TxType;
import world.bentobox.bentobox.api.events.island.IslandPreclearEvent;
import world.bentobox.bentobox.api.user.User;
Expand All @@ -36,13 +37,13 @@
*/
public class BankManager implements Listener {
private static final int MAX_SIZE = 20;
private static final double MINIMUM_BALANCE = 0.1;
private static final double MINIMUM_INTEREST = 0.01;
static final long MILLISECONDS_IN_YEAR = 1000 * 60 * 60 * 24 * 365;
// Database handler for accounts
private final Database<BankAccounts> handler;
private final Bank addon;
private final Map<String, BankAccounts> cache;
private final Map<String, Double> balances;
private final Map<String, Money> balances;

/**
* Cached database bank manager for withdrawals, deposits and balance inquiries
Expand All @@ -61,49 +62,64 @@ public BankManager(Bank addon) {
}

/**
* Load the bank balances
* Load the bank balances and calculate any interest due
* @return completable future that completes when the loading is done
*/
public CompletableFuture<Void> loadBalances() {
CompletableFuture<Void> future = new CompletableFuture<>();
balances.clear();
Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> {
handler.loadObjects().forEach(ba -> balances.put(ba.getUniqueId(), ba.getBalance()));
handler.loadObjects().forEach(ba -> balances.put(ba.getUniqueId(), getBalancePlusInterest(ba)));
future.complete(null);
});
return future;
}

/**
* Start a recurring task to pay interest
* Get the new balance plus interest, if any.
* Interest will only be calculated if the last time it was added is longer than the compound period
* @param ba - bank account
* @return Money balance of account
*/
public void startInterest() {
if (addon.getSettings().getInterestRate() <= 0D) {
return;
private Money getBalancePlusInterest(BankAccounts ba) {
if (System.currentTimeMillis() - ba.getInterestLastPaid() > addon.getSettings().getCompoundPeriodInMs()) {
return calculateInterest(ba);
}
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(),
() -> calculateInterest(), addon.getSettings().getCompoundPeriod(), addon.getSettings().getCompoundPeriod());
return ba.getBalance();
}

void calculateInterest() {
balances.forEach((uuid, bal) -> {
if (bal < MINIMUM_BALANCE) return;
double interest = formatDouble(bal * addon.getSettings().getInterestRate());
if (interest > MINIMUM_INTEREST) {
addon.getIslands().getIslandById(uuid).filter(i -> i.getOwner() != null).ifPresent(island ->
this.set(User.getInstance(island.getOwner()), island.getUniqueId(), interest, (bal + interest), TxType.INTEREST));
}
});
Money calculateInterest(BankAccounts ba) {
double bal = ba.getBalance().getValue();
// Calculate compound interest over period of time
// A = P * (1 + r/n)^(n*t)
/*
* A = the total value
* P = the initial deposit
* r = the annual interest rate
* n = the number of times that interest is compounded per year
* t = the number of years the money is saved
*/
double r = (double)addon.getSettings().getInterestRate() / 100;
long n = addon.getSettings().getCompoundPeriodsPerYear();
double t = getYears(System.currentTimeMillis() - ba.getInterestLastPaid());
double A = bal * Math.pow((1 + r/n), (n*t));
double interest = A - bal;
if (interest > MINIMUM_INTEREST) {
addon.getIslands().getIslandById(ba.getUniqueId()).filter(i -> i.getOwner() != null).ifPresent(island -> {
// Set the interest payment timestamp
ba.setInterestLastPaid(System.currentTimeMillis());
// Put this account into the cache so it will be found immediately by the set method
cache.put(ba.getUniqueId(), ba);
// Add the new amount
this.set(User.getInstance(island.getOwner()), island.getUniqueId(), new Money(interest), new Money(bal + interest), TxType.INTEREST);
});

}
return new Money(A);
}

/**
* Reduce double down to 2 decimal places
* @param valueToFormat - value
* @return double reduced
*/
private Double formatDouble(Double valueToFormat) {
long rounded = Math.round(valueToFormat*100);
return rounded/100.0;
private double getYears(long l) {
return (double)l / MILLISECONDS_IN_YEAR;
}

/**
Expand All @@ -112,7 +128,7 @@ private Double formatDouble(Double valueToFormat) {
* @param world - island's world
* @return BankResponse
*/
public CompletableFuture<BankResponse> deposit(User user, double amount, World world) {
public CompletableFuture<BankResponse> deposit(User user, Money amount, World world) {
// Get player's account
Island island = addon.getIslands().getIsland(Objects.requireNonNull(Util.getWorld(world)), user);
if (island == null) {
Expand All @@ -127,10 +143,12 @@ public CompletableFuture<BankResponse> deposit(User user, double amount, World w
* @param amount - amount
* @return BankResponse
*/
public CompletableFuture<BankResponse> deposit(User user, Island island, double amount, TxType type) {
public CompletableFuture<BankResponse> deposit(User user, Island island, Money amount, TxType type) {
try {
BankAccounts account = getAccount(island.getUniqueId());
return this.set(user, island.getUniqueId(), amount, (account.getBalance() + amount), type);
// Calculate interest
this.getBalancePlusInterest(account);
return this.set(user, island.getUniqueId(), amount, Money.add(account.getBalance(), amount), type);
} catch (IOException e) {
return CompletableFuture.completedFuture(BankResponse.FAILURE_LOAD_ERROR);
}
Expand Down Expand Up @@ -163,7 +181,7 @@ private BankAccounts getAccount(String uuid) throws IOException {
* @param world - world
* @return BankResponse
*/
public CompletableFuture<BankResponse> withdraw(User user, double amount, World world) {
public CompletableFuture<BankResponse> withdraw(User user, Money amount, World world) {
// Get player's island
Island island = addon.getIslands().getIsland(Objects.requireNonNull(Util.getWorld(world)), user);
if (island == null) {
Expand All @@ -178,7 +196,7 @@ public CompletableFuture<BankResponse> withdraw(User user, double amount, World
* @param amount - amount
* @return BankResponse
*/
public CompletableFuture<BankResponse> withdraw(User user, Island island, double amount, TxType type) {
public CompletableFuture<BankResponse> withdraw(User user, Island island, Money amount, TxType type) {
BankAccounts account;
if (!handler.objectExists(island.getUniqueId())) {
// No account = no balance
Expand All @@ -191,25 +209,27 @@ public CompletableFuture<BankResponse> withdraw(User user, Island island, double
}

}
// Calculate interest
this.getBalancePlusInterest(account);
// Check balance
if (account.getBalance() < amount) {
if (Money.lessThan(account.getBalance(), amount)) {
// Low balance
return CompletableFuture.completedFuture(BankResponse.FAILURE_LOW_BALANCE);
}
// Success
return this.set(user, island.getUniqueId(), amount, (account.getBalance() - amount), type);
return this.set(user, island.getUniqueId(), amount, Money.subtract(account.getBalance(), amount), type);
}

/**
* Get balance for island
* @param island - island
* @return balance. 0 if unknown
*/
public double getBalance(@Nullable Island island) {
public Money getBalance(@Nullable Island island) {
if (island == null) {
return 0D;
return new Money();
}
return balances.getOrDefault(island.getUniqueId(), 0D);
return balances.getOrDefault(island.getUniqueId(), new Money());
}

/**
Expand All @@ -218,7 +238,7 @@ public double getBalance(@Nullable Island island) {
* @param world - world
* @return balance. 0 if unknown
*/
public double getBalance(User user, World world) {
public Money getBalance(User user, World world) {
return getBalance(addon.getIslands().getIsland(Objects.requireNonNull(Util.getWorld(world)), user));
}

Expand All @@ -230,6 +250,8 @@ public double getBalance(User user, World world) {
public List<AccountHistory> getHistory(Island island) {
try {
BankAccounts account = getAccount(island.getUniqueId());
// Calculate interest
this.getBalancePlusInterest(account);
return account.getHistory().entrySet().stream().map(en -> {
String[] split = en.getValue().split(":");
if (split.length == 3) {
Expand All @@ -248,7 +270,7 @@ public List<AccountHistory> getHistory(Island island) {
* @param world - world
* @return the balances
*/
public Map<String, Double> getBalances(World world) {
public Map<String, Money> getBalances(World world) {
return balances.entrySet().stream()
.filter(en -> addon.getIslands().getIslandById(en.getKey())
.map(i -> i.getWorld().equals(Util.getWorld(world))).orElse(false))
Expand All @@ -259,19 +281,16 @@ public Map<String, Double> getBalances(World world) {
* Sets an island's account value to an amount
* @param user - user who is doing the setting or island owner for interest
* @param islandID - island unique id
* @param amount - amount
* @param amount - amount being added or removed
* @param newBalance - the resulting new balance
* @param type - type of transaction
* @return BankResponse
*/
public CompletableFuture<BankResponse> set(@NonNull User user, @NonNull String islandID, double amount, double newBalance, TxType type) {
public CompletableFuture<BankResponse> set(@NonNull User user, @NonNull String islandID, Money amount, Money newBalance, TxType type) {
try {
BankAccounts account = getAccount(islandID);
account.setBalance(newBalance);
if (type.equals(TxType.INTEREST) ) {
account.getHistory().put(System.currentTimeMillis(), user.getTranslation("bank.statement.interest") + ":" + amount);
} else {
account.getHistory().put(System.currentTimeMillis(), user.getName() + ":" + type + ":" + amount);
}
account.getHistory().put(System.currentTimeMillis(), user.getName() + ":" + type + ":" + amount.getValue());
cache.put(islandID, account);
balances.put(islandID, account.getBalance());
CompletableFuture<BankResponse> result = new CompletableFuture<>();
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/world/bentobox/bank/PhManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.bukkit.World;
import org.eclipse.jdt.annotation.Nullable;

import world.bentobox.bank.data.Money;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.user.User;
Expand Down Expand Up @@ -65,7 +66,7 @@ protected boolean registerPlaceholders(GameModeAddon gm) {
// Island Balance
plugin.getPlaceholdersManager().registerPlaceholder(addon,
gm.getDescription().getName().toLowerCase() + "_island_balance",
user -> addon.getVault().format(bankManager.getBalance(user, gm.getOverWorld())));
user -> addon.getVault().format(bankManager.getBalance(user, gm.getOverWorld()).getValue()));

// Visited Island Balance
plugin.getPlaceholdersManager().registerPlaceholder(addon,
Expand All @@ -74,7 +75,7 @@ protected boolean registerPlaceholders(GameModeAddon gm) {
// Formatted Island Balance
plugin.getPlaceholdersManager().registerPlaceholder(addon,
gm.getDescription().getName().toLowerCase() + "_island_balance_formatted",
user -> format(bankManager.getBalance(user, gm.getOverWorld())));
user -> format(bankManager.getBalance(user, gm.getOverWorld()).getValue()));

// Formatted Visited Island Balance
plugin.getPlaceholdersManager().registerPlaceholder(addon,
Expand Down Expand Up @@ -103,7 +104,7 @@ protected boolean registerPlaceholders(GameModeAddon gm) {
*/
String getVisitedIslandBalance(GameModeAddon gm, User user, boolean formatted, boolean plain) {
if (user == null || user.getLocation() == null) return "";
double balance = gm.inWorld(user.getWorld()) ? addon.getIslands().getIslandAt(user.getLocation()).map(i -> bankManager.getBalance(i)).orElse(0D) : 0D;
double balance = gm.inWorld(user.getWorld()) ? addon.getIslands().getIslandAt(user.getLocation()).map(i -> bankManager.getBalance(i).getValue()).orElse(0D) : 0D;
if (plain) {
return String.valueOf(balance);
}
Expand Down Expand Up @@ -134,12 +135,12 @@ int checkCache(World world, int rank) {
balances.clear();
// Get a new balance map, sort it and save it to two sorted lists
bankManager.getBalances(world).entrySet()
.stream().sorted((h1, h2) -> Double.compare(h2.getValue(), h1.getValue()))
.stream().sorted((h1, h2) -> Money.compare(h2.getValue(), h1.getValue()))
.limit(addon.getSettings().getRanksNumber())
.forEach(en -> {
names.add(addon.getIslands().getIslandById(en.getKey())
.map(i -> addon.getPlayers().getName(i.getOwner())).orElse(""));
balances.add(addon.getVault().format(en.getValue()));
balances.add(addon.getVault().format(en.getValue().getValue()));
});
lastSorted = System.currentTimeMillis();
}
Expand Down
43 changes: 32 additions & 11 deletions src/main/java/world/bentobox/bank/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ public class Settings implements ConfigObject {
private int ranksNumber = 10;

@ConfigComment("The annual interest rate for accounts. If zero or less, interest will not be paid.")
private float interestRate = 10;
private int interestRate = 10;

@ConfigComment("Period that interest is compounded in hours. Default is 1 hour; minimum is 1 minute.")
@ConfigComment("Make period shorter than the server reboot period otherwise interest will not be paid.")
@ConfigComment("Period that interest is compounded in days. Default is 1 day.")
@ConfigComment("Interest calculations are done when the server starts or when the player logs in.")
private float compoundPeriod = 1;

/**
Expand Down Expand Up @@ -100,34 +100,55 @@ public void setRanksNumber(int ranksNumber) {
}

/**
* @return the interestRate for this period
* Interest rate is a yearly percentage.
* @return the yearly interestRate
*/
public float getInterestRate() {
// Interest rate is a yearly percentage. Period is hourly.
return interestRate / 365 / 24 / 100;
public int getInterestRate() {
return interestRate;
}

/**
* @param interestRate the interestRate to set
*/
public void setInterestRate(float interestRate) {
public void setInterestRate(int interestRate) {
this.interestRate = interestRate;
}

/**
* @return the compoundPeriod in ticks
*/
public long getCompoundPeriod() {
public long getCompoundPeriodInTicks() {
// Make the period a minimum of 1 minute long
return Math.max(1200L, (long) (compoundPeriod * 72000L));
return Math.max(1200L, (long) (compoundPeriod * 20 * 24 * 60 * 60));
}

/**
* @param compoundPeriod the compoundPeriod to set
* @return compound period in days
*/
public float getCompoundPeriod() {
return compoundPeriod;
}

/**
* @return the compound periods per year
*/
public long getCompoundPeriodsPerYear() {
return (long) (compoundPeriod * 365);
}

/**
* @param compoundPeriod the compoundPeriod to set in hours
*/
public void setCompoundPeriod(float compoundPeriod) {
this.compoundPeriod = compoundPeriod;
}

/**
* @return the compound period in ms
*/
public long getCompoundPeriodInMs() {
return (long) (compoundPeriod * 24 * 60 * 60 * 1000);
}


}

0 comments on commit 2600292

Please sign in to comment.