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_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 accountsList = new HashMap(); // Key = user uuid // Val = List of account identifiers private HashMap> accountsListFromName = new HashMap>(); // This is a list that just points to a list of account numbers by person. USEFUL } Accounts accounts; private List 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(); 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 account : accounts.accountsList.entrySet()) { // We must loop over the string of holders for each account as well to make the flattened accountsListFromName map List 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 GetAccountsOfUser(String uuid) { List accountsFromUser = new ArrayList(); List 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 userAccountList = new ArrayList(); // 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 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 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 fromParty, Pair 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 GetAccountBalance(String accountId) { Optional 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> 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 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 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()); } } }