the_big_one/src/main/java/jesse/keeblarcraft/BankMgr/IndividualBank.java
Jkibbels e164a489b2
Some checks failed
build / build (21) (push) Has been cancelled
build / build (21) (pull_request) Has been cancelled
[factions-banking] Final commit for this branch that has grown out of control. The other stuff will be added in their own respective branches
2025-01-26 22:19:51 -05:00

594 lines
29 KiB
Java

package jesse.keeblarcraft.BankMgr;
import java.util.*;
import java.util.Map.Entry;
import static java.util.Map.entry;
import java.io.File;
import jesse.keeblarcraft.ConfigMgr.ConfigManager;
import jesse.keeblarcraft.Keeblarcraft;
import jesse.keeblarcraft.BankMgr.BankAccountType.ACCOUNT_TYPE;
import jesse.keeblarcraft.Utils.CommonStructures.Pair;
// Contains the information of an individual bank
//
// The bank will keep track of all accounts within its facilities. In addition to accounts, the bank
// maintains its own identifier which is unique and other misc things.
public class IndividualBank {
private Map<ACCOUNT_TYPE, Integer> ACCOUNT_TYPES = Map.ofEntries(
entry(ACCOUNT_TYPE.CHECKING, 0),
entry(ACCOUNT_TYPE.SAVINGS, 1)
);
private ConfigManager config = new ConfigManager();
private Integer routingNumber; // this is the banks unique identifier
private Integer numberOfAccounts = 0; // Total number of accounts the bank has. This includes only active accounts inside accountsList.
private Integer maxBankAccounts = 100_000_000; // Making this simple for myself any one type of account has 8 random numbers genereated so 10^8 possible accounts
private String bankFourLetterIdentifier;
private String registeredBankName;
private static String CONFIG_LOCATION = "config/keeblarcraft/bank/";
// Think FDIC but from the servers account (keeblarcraft insurance corporation)
// KBIC will ensure an amount of money based on its trustworthiness to a bank and the number of holders it has.
private Integer kbicInsuredAmount = 75_000;
private Boolean kbicInsured = false;
// bankMoney is the total amount of money the bank possesses itself. The bank itself is personally responsible
// for backing the amount of money it claims it has and this is the balance that is withdrawn from for credits.
// A bank can have a sum of money that is less than the total amount of money of all its account holders
private Integer bankMoney = 0;
// Key = ACCOUNT NUMBER
// Value = ACCOUNT
private class Accounts {
// Key = account identifier
// Val = account object
private HashMap<String, IndividualAccount> accountsList = new HashMap<String, IndividualAccount>();
// Key = user uuid
// Val = List of account identifiers
private HashMap<String, List<String>> accountsListFromName = new HashMap<String, List<String>>(); // This is a list that just points to a list of account numbers by person. USEFUL
}
Accounts accounts;
private List<String> lockedUsers; // A list of users who are locked out of the bank and are incapable of performing more actions within it
/////////////////////////////////////////////////////////////////////////////
/// @fn IndividualBank
///
/// @param[in] routingNumber is the routing number this bank is constructed
/// with
///
/// @param[in] nameOfBank Will be the display name of this bank to players
///
/// @brief Constructor for this bank
/////////////////////////////////////////////////////////////////////////////
public IndividualBank(String routingNumber, String nameOfBank) {
accounts = new Accounts();
lockedUsers = new ArrayList<String>();
registeredBankName = nameOfBank.toUpperCase();
bankFourLetterIdentifier = nameOfBank.substring(0, 4).toLowerCase();
this.routingNumber = Integer.parseInt(routingNumber);
System.out.println("CREATING BANK ACCOUNT WITH ROUTING NUMBER " + routingNumber + " AND NAME " + nameOfBank);
boolean existingFile = false;
try {
// Read in the global accounts list
String accountsListDir = CONFIG_LOCATION + routingNumber.toString() + "/accounts/";
System.out.println("accountsListDir + bankName is " + accountsListDir + nameOfBank);
accounts = config.GetJsonObjectFromFile(accountsListDir + nameOfBank, Accounts.class);
existingFile = accounts != null;
// TODO: REPLACE WITH SQL SERVER. DIRTY ITERATE OVER ALL FILES IN DIRECTORY TO LOAD STRUCTURE
File dir = new File(accountsListDir);
File[] allFiles = dir.listFiles();
if (accounts != null && allFiles != null) {
for (File file : allFiles ) {
// First grab file identifier as KEY
String accountIdentifier = file.getName();
String accountFromFile = accountsListDir + "/" + accountIdentifier;
System.out.println("accountIdentifier found in file is " + accountIdentifier);
System.out.println("account identifier with dir path is " + accountFromFile);
accounts.accountsList.put(accountIdentifier, config.GetJsonObjectFromFile(accountFromFile, IndividualAccount.class));
}
}
} catch (Exception e) {
System.out.println("The try-catch in IndividualBank.java failed to complete. Printing stack trace");
// e.printStackTrace();
// Falling into this catch just means the file needs to be made
}
if (!existingFile)
{
try {
// We assume the bank dir is created by server. Create this banks dir
// config.CreateDirectory("bank/" + routingNumber);
// Create this banks initial accounts dir
config.CreateDirectory(CONFIG_LOCATION + routingNumber + "/accounts");
// Flash initial account configuration file for this bank
FlashConfig("accounts");
} catch (Exception e) {
Keeblarcraft.LOGGER.error("Could not write to file in IndividualBank");
}
}
// A modified config reader is needed here for when each IndividualAccount is read in - the name is taken from that and is attached to the
// 'accountsListFromName' structure. This makes it no worse than O(n) to fill these two structures in.
// NOTE: This is an *EXPENSIVE* operation! Future us might need to update this. Also note a method is needed for everytime a player opens a new account
// or gets put on one to update the map every time
if (accounts != null) {
for (Entry<String, IndividualAccount> account : accounts.accountsList.entrySet()) {
// We must loop over the string of holders for each account as well to make the flattened accountsListFromName map
List<String> accountHolders = account.getValue().GetAccountHolders();
// Match each user to the secondary map & add to list-value if not existing
for (String accountHolder : accountHolders) {
if (accounts.accountsListFromName.containsKey(accountHolder)) {
// Case 1: User exists, update map entry
accounts.accountsListFromName.get(accountHolder).add(account.getKey()); // Add a new account id to this person in the new flat map
} else {
// Case 2: User does not already exist; add a new map entry
accounts.accountsListFromName.put(accountHolder, List.of(account.getKey())); // Store name as key, and new List with the value of ACCOUNT #
}
}
}
numberOfAccounts = accounts.accountsList.size();
} else {
accounts = new Accounts();
numberOfAccounts = 0;
}
}
/////////////////////////////////////////////////////////////////////////////
/// @fn GetBankName
///
/// @return Returns this banks name
/////////////////////////////////////////////////////////////////////////////
public String GetBankName() {
return registeredBankName;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn GetAccountsOfUser
///
/// @param[in] uuid is the players UUID to check
///
/// @brief Gets all the accounts a user has at this bank by UUID
///
/// @return List of all bank accounts. List will be EMPTY if no accounts
/////////////////////////////////////////////////////////////////////////////
public List<IndividualAccount> GetAccountsOfUser(String uuid) {
List<IndividualAccount> accountsFromUser = new ArrayList<IndividualAccount>();
List<String> listOfAccounts = accounts.accountsListFromName.get(uuid);
if (listOfAccounts != null && !listOfAccounts.isEmpty()) {
for (String listOfAccount : listOfAccounts) {
accountsFromUser.add(accounts.accountsList.get(listOfAccount));
}
}
return accountsFromUser;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn GetBankBalance
///
/// @return The banks balance
/////////////////////////////////////////////////////////////////////////////
public Integer GetBankBalance() {
return bankMoney;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn AddBankBalance
///
/// @param[in] amount Amount to add to the banks balance
///
/// @brief Adds to the banks balance
/////////////////////////////////////////////////////////////////////////////
public void AddBankBalance(Integer amount) {
bankMoney += amount;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn SubtractBankBalance
///
/// @param[in] amount Amount to subtract from banks balance
///
/// @brief Subtracts from the banks balance
/////////////////////////////////////////////////////////////////////////////
public void SubtractBankBalance(Integer amount) {
bankMoney -= amount;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn SetBankBalance
///
/// @param[in] amount Amount to give the bank
///
/// @brief Set the banks balance
/////////////////////////////////////////////////////////////////////////////
public void SetBankBalance(Integer amount) {
bankMoney = amount;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn IsBankInsured
///
/// @return True if bank is insured, false if not
/////////////////////////////////////////////////////////////////////////////
public Boolean IsBankInsured() {
return kbicInsured;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn InsuranceAmount
///
/// @brief Get the insurance amount at this bank
///
/// @return Insurance amount
/////////////////////////////////////////////////////////////////////////////
public Integer InsuranceAmount() {
Integer insuredAmnt = 0;
if (kbicInsured) {
insuredAmnt = kbicInsuredAmount;
} else {
insuredAmnt = 0;
}
return insuredAmnt;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn UpdateBankAccounts
///
/// @brief Flashes bank account information to disk; updates volatile
/// memory of banking information for this bank
/////////////////////////////////////////////////////////////////////////////
public void UpdateBankAccounts(String newHolderName, String newHolderUuid, String accountIdentifier, IndividualAccount newAccountOnly) {
// Update the fast-access map first
System.out.println("UpdateBankAccounts called with information " + newHolderName + " " + newHolderUuid + " " + accountIdentifier);
if (accounts.accountsListFromName.containsKey(newHolderUuid)) {
// Check if user is already in map
accounts.accountsListFromName.get(newHolderUuid).add(accountIdentifier);
} else {
// Add new entry to map
List<String> userAccountList = new ArrayList<String>(); // Lists are immutable; must make ArrayList
userAccountList.add(accountIdentifier);
accounts.accountsListFromName.put(newHolderUuid, userAccountList);
}
// Update regular account list
if (accounts.accountsList.containsKey(accountIdentifier)) {
// This path assumes we are adding a holder as opposed to adding an account (else, how else would this work?)
System.out.println("Account found in accounts list, adding this person as a holder instead");
accounts.accountsList.get(accountIdentifier).AddAccountHolder(newHolderName, newHolderUuid);
} else {
// Non-existent account means a new one!
System.out.println("Brand new account creation, adding!");
accounts.accountsList.put(accountIdentifier, newAccountOnly);
numberOfAccounts++;
}
System.out.println("Flashing configuration file");
FlashConfig("bank/" + routingNumber + "/accounts");
}
/////////////////////////////////////////////////////////////////////////////
/// @fn GetRoutingNumber
///
/// @return Routing number
/////////////////////////////////////////////////////////////////////////////
public Integer GetRoutingNumber() {
return this.routingNumber;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn AddMoneyToAccount
///
/// @param[in] accountId is the account to add money to at this bank
///
/// @param[in] amount is the amount of money to add to this account
///
/// @brief Adds money to an account at this bank if it exists
/////////////////////////////////////////////////////////////////////////////
public void AddMoneyToAccount(String accountId, Integer amount) {
IndividualAccount account = accounts.accountsList.get(AccountNumberGenerator.GetAccountNumberFromId(accountId));
System.out.println("Received account # " + accountId + " and money amnt " + amount);
System.out.println("Is account null? " + (account == null ? "YES":"NO"));
System.out.println("accounts.accountList.size() : " + accounts.accountsList.size());
for (Entry<String, IndividualAccount> accnt : accounts.accountsList.entrySet()) {
System.out.println("Account num in entry: " + accnt.getValue().GetAccountNumber());
}
if (account != null && account.Deposit(amount)) {
GenerateTransactionReport(TransactionMetadata.TRANSACTION_TYPE.DEPOSIT, new Pair<>("ADMIN", "ADMIN"), new Pair<>("ADMIN", "ADMIN"), amount, "ADMIN");
FlashConfig("bank/" + routingNumber + "/accounts");
}
}
/////////////////////////////////////////////////////////////////////////////
/// @fn SubtractMoneyFromAccount
///
/// @param[in] accountId is the account to subtract money to at this bank
///
/// @param[in] amount is the amount of money to subtract to this account
///
/// @brief Subtracts money from an account at this bank if it exists
/////////////////////////////////////////////////////////////////////////////
public void SubtractMoneyFromAccount(String accountId, Integer amount) {
IndividualAccount account = accounts.accountsList.get(AccountNumberGenerator.GetAccountNumberFromId(accountId));
if (account != null && account.Withdraw(amount)) {
GenerateTransactionReport(TransactionMetadata.TRANSACTION_TYPE.WITHDRAWAL, new Pair<>("ADMIN", "ADMIN"), new Pair<>("ADMIN", "ADMIN"), amount, "ADMIN");
FlashConfig("bank/" + routingNumber + "/accounts");
}
}
/////////////////////////////////////////////////////////////////////////////
/// @fn SetMoneyOnAccount
///
/// @param[in] accountId is the account number
///
/// @param[in] amount is the new balance to give this account
///
/// @brief Sets a balance on an account if it exists
/////////////////////////////////////////////////////////////////////////////
public void SetMoneyOnAccount(String accountId, Integer amount) {
IndividualAccount account = accounts.accountsList.get(accountId);
System.out.println("Is account null? " + (account == null ? "YES" : "NO"));
System.out.println("Received account # " + accountId + " and money amnt " + amount);
for (Entry<String, IndividualAccount> debug : accounts.accountsList.entrySet()) {
System.out.println("ACCOUNT ID: " + debug.getKey());
System.out.println("ACCOUNT NUM: " + debug.getValue().GetAccountNumber());
}
if (account != null) {
account.SetMoney(amount);
GenerateTransactionReport(TransactionMetadata.TRANSACTION_TYPE.OTHER, new Pair<>("ADMIN", "ADMIN"), new Pair<>("ADMIN", "ADMIN"), amount, "ADMIN");
FlashConfig("bank/" + routingNumber + "/accounts");
}
}
/////////////////////////////////////////////////////////////////////////////
/// @fn CreateAccount
///
/// @param[in] holderUuid is the new holders UUID of this account
///
/// @param[in] holderName is the display name of the holder
///
/// @param[in] accountTypeStr is the account type as a string (will be number)
///
/// @brief Create a new account at this bank
///
/// @return True if account can be created, false if it fails
/////////////////////////////////////////////////////////////////////////////
public Boolean CreateAccount(String holderUuid, String holderName, ACCOUNT_TYPE accountType) {
Boolean success = false;
if (accounts.accountsList.size() <= maxBankAccounts) {
// Verify this isn't a blacklisted user
System.out.println("Is user bank locked? " + lockedUsers.contains(holderName));
if (!lockedUsers.contains(holderName)) {
Integer maxAttempts = 15; // Reasonably a unique bank account should pop up within 1000 generations. If not, the user may try again.
String accountId = AccountNumberGenerator.GenerateNewAccountNumber(bankFourLetterIdentifier, routingNumber, ACCOUNT_TYPES.get(accountType), holderName);
System.out.println("Account generator came back with bank account id { " + accountId + " }");
System.out.println("4 letter bank: " + AccountNumberGenerator.GetFinancialSymbolFromId(accountId));
System.out.println("Routing: " + AccountNumberGenerator.GetRoutingNumberFromId(accountId));
System.out.println("Account type: " + AccountNumberGenerator.GetAccountTypeFromId(accountId));
System.out.println("RNG Account number: " + AccountNumberGenerator.GetAccountNumberFromId(accountId));
// TODO: Fix in future with a method that will guarentee a one-time necessary number generator. Statistically speaking; this will be okay for the
// entire life time of the server. BUT, you never know!
while (maxAttempts != 0 && !accounts.accountsList.containsKey(AccountNumberGenerator.GetAccountNumberFromId(accountId))) {
accountId = AccountNumberGenerator.GenerateNewAccountNumber(bankFourLetterIdentifier, routingNumber, ACCOUNT_TYPES.get(accountType), holderName);
System.out.println("Account generator came back with bank account id { " + accountId + " }");
maxAttempts--;
}
// Final check to add the account
String actualAccountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId);
System.out.println("Bank account identifier is { " + actualAccountNumber + " }. Is this already an existing account? " + accounts.accountsList.containsKey(actualAccountNumber));
if (!accounts.accountsList.containsKey(actualAccountNumber)) {
IndividualAccount newAccount = new IndividualAccount(actualAccountNumber, this.routingNumber, List.of(holderName),
List.of(holderUuid), false, 0,
"", accountType, bankFourLetterIdentifier);
System.out.println("Updating accounts list for this bank");
UpdateBankAccounts(holderName, holderUuid, actualAccountNumber, newAccount);
success = true;
}
}
}
return success;
}
// fromParty is expected to be UUID mapped to username
// toParty is expected to be UUID mapped to username
// NOTHING should be null in this generation!
public void GenerateTransactionReport(TransactionMetadata.TRANSACTION_TYPE transactionType, Pair<String, String> fromParty, Pair<String, String> toParty, Integer amount, String reason) {
// The transaction ID is to be formatted (for now) as #-FROMUUID-TOUUID-RNG
// For this version, we pray there is not a duplicate RNG dupe and no checking is done for now!
StringBuilder transactionId = new StringBuilder("#" + fromParty.GetKey() + "-" + toParty.GetKey() + Integer.toString(new Random().nextInt(100_000_000) + 1_000_000));
TransactionMetadata trans = new TransactionMetadata(transactionType, fromParty, toParty, amount, transactionId.toString(), reason);
// This is a hacky solution to make sure the transaction file doesn't exist. It is "technically" possible we fail 10 times in a row
// then can't write and thus no papertrail is left - however if the RNG fails to make a unique enough number with these UUID's between
// 1 million and 100 million then we have other issues to worry about.
int maxTries = 10;
// We now want to iterate over the files in the transaction dir and pop a new file in
while (config.DoesFileExist("bank/" + routingNumber + "/transactions/" + transactionId) && maxTries-- >= 0) {
transactionId.append(Integer.toString(new Random().nextInt(100_000_000) + 1_000_000));
}
if (!config.DoesFileExist("bank/" + routingNumber + "/transactions/" + transactionId)) {
config.WriteToJsonFile("bank/" + routingNumber + "/transactions/" + transactionId + ".json", trans);
}
}
// The value 0 should be treated as either non-exist
public Optional<Integer> GetAccountBalance(String accountId) {
Optional<Integer> amount = Optional.empty();
if (accounts.accountsList.containsKey(accountId)) {
IndividualAccount account = accounts.accountsList.get(accountId);
amount = Optional.of(account.GetAccountBalance());
}
return amount;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn LockAccountHolder
///
/// @param[in] holderName is player to lock account
///
/// @brief Locks all accounts under a name for this user
///
/// @return True if lock succeeds, false if not
/////////////////////////////////////////////////////////////////////////////
public Boolean LockAccountHolder(String holderName) {
Boolean success = false;
int accountIter = 0;
for (Entry<String, List<String>> holderAccounts : accounts.accountsListFromName.entrySet()) {
accounts.accountsList.get(holderAccounts.getValue().get(accountIter++)).LockAccount();
}
return success;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn CloseAccount
///
/// @param[in] accountId is id of account to be closed
///
/// @brief Closes an account
///
/// @note Balance of account must be 0 to close successfully
///
/// @return True if can close, false if not
/////////////////////////////////////////////////////////////////////////////
public Boolean CloseAccount(String accountId, String playerUuid) {
boolean success = false;
if (accounts.accountsList.containsKey(accountId) && accounts.accountsList.get(accountId).GetAccountBalance() == 0) {
System.out.println("Closing user account...");
// The below two lists should ALWAYS be in sync. If there is a discrepancy; this may segfault!
accounts.accountsList.remove(accountId);
accounts.accountsListFromName.get(playerUuid).remove(accountId);
FlashConfig("bank/" + routingNumber + "/accounts");
success = true;
}
return success;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn HasAccount
///
/// @param[in] accountIdentifier account number to check to see if it exists
/// at this bank
///
/// @brief See if an account identifier belongs to this bank
///
/// @return True if this bank has this account identifier, false if not
/////////////////////////////////////////////////////////////////////////////
public Boolean HasAccount(String accountIdentifier) {
return accounts.accountsList.containsKey(accountIdentifier);
}
/////////////////////////////////////////////////////////////////////////////
/// @fn IsValidWithdrawal
///
/// @param[in] withdrawalAmount The amount to be withdrawn
///
/// @param[in] accountIdentifier account to withdraw from
///
/// @brief Verifies if a withdrawal will succeed on an account prior to
/// the actual withdrawal itself
///
/// @return True if this account can afford this withdrawal. False if not
/////////////////////////////////////////////////////////////////////////////
public Boolean IsValidWithdrawal(Integer withdrawalAmount, String accountIdentifier) {
boolean isValid = false;
System.out.println("Account id: " + accountIdentifier);
System.out.println("Short account id: " + AccountNumberGenerator.GetAccountNumberFromId(accountIdentifier));
String localAccountId = AccountNumberGenerator.GetAccountNumberFromId(accountIdentifier);
System.out.println("Amount to withdraw: " + withdrawalAmount);
System.out.println("accounts list map: " + accounts.accountsList);
if (accounts.accountsList.containsKey(localAccountId)) {
IndividualAccount account = accounts.accountsList.get(localAccountId);
System.out.println("Is account null? " + (account == null));
if (account.CanWithdraw(withdrawalAmount)) {
isValid = true;
}
}
return isValid;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn IsAccountHolder
///
/// @param[in] accountIdentifier Account to check
///
/// @param[in] uuid Is players UUID to see if they are on this account
///
/// @brief Check if a player is on this account
///
/// @return True if player is on this account. False if not or account
/// does not exist
/////////////////////////////////////////////////////////////////////////////
public Boolean IsAccountHolder(String accountIdentifier, String uuid) {
Boolean isHolder = false;
System.out.println("Looking for accountIdentifier: " + accountIdentifier);
for (Entry<String, IndividualAccount> account : accounts.accountsList.entrySet()) {
System.out.println("ACCOUNT ID: " + account.getKey());
}
// Verify account exists first
if (accounts.accountsList.containsKey(accountIdentifier)) {
isHolder = accounts.accountsList.get(accountIdentifier).IsHolder(uuid);
} else {
System.out.println("Account identifier found to not exist");
}
return isHolder;
}
/////////////////////////////////////////////////////////////////////////////
/// @fn FlashConfig
///
/// @param[in] dirName is config to flash to. Banking is not trivial and will
/// require separate files to be updated at different locations!
///
/// @brief Flashes the config to the disk
///
/// @note dirName should be the relative full path to dir
/// @note Function will be removed in near future for SQL but is
/// expensive to run as it flashes everything even if un-updated
/////////////////////////////////////////////////////////////////////////////
public void FlashConfig(String dirName) {
for (Entry<String, IndividualAccount> singleAccount : accounts.accountsList.entrySet()) {
System.out.println("FlashConfig. dirname: " + dirName);
// Iterate over each one & verify if a file exists inside the dir. if it does;
// nuke it and
// replace it with the new contents in memory
String accountNum = singleAccount.getKey().toString();
// delete file
File file = new File(dirName + "/" + accountNum + ".json");
System.out.println("Checking path: " + dirName + "/" + accountNum + ".json");
if (file.exists()) {
file.delete();
}
// Re-flash file
config.WriteToJsonFile(dirName + "/" + accountNum + ".json", singleAccount.getValue());
}
}
}