package jesse.keeblarcraft.BankMgr; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import static java.util.Map.entry; import java.io.File; import jesse.keeblarcraft.ConfigMgr.ConfigManager; import jesse.keeblarcraft.Utils.ChatUtil; import jesse.keeblarcraft.Utils.ChatUtil.CONSOLE_COLOR; import jesse.keeblarcraft.Utils.CustomExceptions.FILE_WRITE_EXCEPTION; // 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("checking", 0), entry("savings", 1) ); private ConfigManager config = new ConfigManager(); private Integer routingNumber; // this is the banks unique identifier private Integer numberOfAccounts; // 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; // 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; private Boolean kbicInsured; // 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; // 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 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 = "bank/" + routingNumber.toString() + "/accounts/"; System.out.println("accountsListDir + bankName is " + accountsListDir + nameOfBank); accounts = config.GetJsonObjectFromFile(accountsListDir + nameOfBank, Accounts.class); existingFile = true; // 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 (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) { System.out.println(ChatUtil.ColoredString("Trying to create new file", CONSOLE_COLOR.BLUE)); 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("bank/" + routingNumber + "/accounts"); // Flash initial account configuration file for this bank FlashConfig("accounts"); } catch (Exception e) { System.out.println(ChatUtil.ColoredString("Could not write to file", CONSOLE_COLOR.RED)); } } else { System.out.println(ChatUtil.ColoredString("Moving on", CONSOLE_COLOR.BLUE)); } // 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 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 (Integer holderIndex = 0; holderIndex < accountHolders.size(); holderIndex++) { if (accounts.accountsListFromName.containsKey(accountHolders.get(holderIndex))) { // Case 1: User exists, update map entry accounts.accountsListFromName.get(accountHolders.get(holderIndex)).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(accountHolders.get(holderIndex), List.of(account.getKey())); // Store name as key, and new List with the value of ACCOUNT # } } } numberOfAccounts = accounts.accountsList.size(); } public String GetBankName() { return registeredBankName; } public List GetAccountsOfUser(String uuid) { System.out.println("UUID passed in: " + uuid); List accountsFromUser = new ArrayList(); List listOfAccounts = accounts.accountsListFromName.get(uuid); System.out.println("Is list of accounts null? " + (listOfAccounts == null ? "YES" : "NO")); System.out.println("List of account size: " + listOfAccounts.size()); if (listOfAccounts != null && listOfAccounts.size() > 0) { for (int i = 0; i < listOfAccounts.size(); i++) { accountsFromUser.add(accounts.accountsList.get(listOfAccounts.get(i))); } } return accountsFromUser; } public Integer GetBankBalance() { return bankMoney; } public void AddBankBalance(Integer amount) { bankMoney += amount; } public void SubtractBankBalance(Integer amount) { bankMoney -= amount; } public void SetBankBalance(Integer amount) { bankMoney = amount; } public Boolean IsBankInsured() { return kbicInsured; } public Integer InsuranceAmount() { Integer insuredAmnt = 0; if (kbicInsured) { insuredAmnt = kbicInsuredAmount; } else { insuredAmnt = 0; } return insuredAmnt; } // Updates the regular bank account list & the fast-access bank account list // NO values are allowed to be null. Manually update lists separately if that's the behaviour you want! 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"); } public Integer GetRoutingNumber() { return this.routingNumber; } public void AddMoneyToAccount(String accountId, Integer amount) { IndividualAccount account = accounts.accountsList.get(accountId); System.out.println("Received account # " + accountId + " and money amnt " + amount); if (account != null) { account.Deposit(amount); } FlashConfig("bank/" + routingNumber + "/accounts"); } public void SubtractMoneyFromAccount(String accountId, Integer amount) { IndividualAccount account = accounts.accountsList.get(accountId); 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.Withdraw(amount); } FlashConfig("bank/" + routingNumber + "/accounts"); } 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); } FlashConfig("bank/" + routingNumber + "/accounts"); } public Boolean CreateAccount(String holderUuid, String holderName, String accountTypeStr) { Boolean success = false; System.out.println("Attempting to create new bank account given args UUID / NAME / TYPE : " + holderUuid + " " + holderName + " " + accountTypeStr); System.out.println("accounts size is " + accounts.accountsList.size()); System.out.println("account string is { " + accountTypeStr + " }. Does this account type exist in this bank? => " + ACCOUNT_TYPES.containsKey(accountTypeStr.toLowerCase())); if (accounts.accountsList.size() <= maxBankAccounts && ACCOUNT_TYPES.containsKey(accountTypeStr.toLowerCase())) { // 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(accountTypeStr), 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(accountTypeStr), 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, "", ACCOUNT_TYPES.get(accountTypeStr), bankFourLetterIdentifier); System.out.println("Updating accounts list for this bank"); UpdateBankAccounts(holderName, holderUuid, actualAccountNumber, newAccount); success = true; } } } return success; } public void AliasAccount(String accountId, String newAlias) { String accountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId); if (accounts.accountsList.containsKey(accountNumber)) { accounts.accountsList.get(accountNumber).AliasAccount(newAlias); } } public Boolean LockAccountHolder(String holderName) { Boolean success = false; Integer accountIter = 0; for (Entry> holderAccounts : accounts.accountsListFromName.entrySet()) { accounts.accountsList.get(holderAccounts.getValue().get(accountIter++)).LockAccount(); } return success; } public Boolean CloseAccount(String accountId) { Boolean success = false; String accountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId); if (accounts.accountsList.get(accountNumber).GetAccountBalance() == 0) { accounts.accountsList.remove(accountNumber); success = true; } return success; } public Boolean HasAccount(String accountIdentifier) { Boolean containsAccount = false; if (accounts.accountsList.containsKey(accountIdentifier)) { containsAccount = true; } return containsAccount; } public Boolean IsValidWithdrawal(Integer withdrawalAmount, String accountIdentifier) { Boolean isValid = false; if (accounts.accountsList.containsKey(accountIdentifier)) { IndividualAccount account = accounts.accountsList.get(accountIdentifier); if (account.CanWithdraw(withdrawalAmount)) { isValid = true; } } return isValid; } public Boolean IsAccountHolder(String accountIdentifier, String uuid) { Boolean isHolder = false; // Verify account exists first if (accounts.accountsList.containsKey(accountIdentifier)) { isHolder = accounts.accountsList.get(accountIdentifier).IsHolder(uuid); } return isHolder; } ///////////////////////////////////////////////////////////////////////////// /// @fn FlashConfig /// /// @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()) { // 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"); if (file.exists()) { file.delete(); } // Re-flash file try { config.WriteToJsonFile(dirName + "/" + accountNum + ".json", singleAccount.getValue()); } catch (FILE_WRITE_EXCEPTION e) { System.out.println("Failed to flash configuration file properly. Printing stack trace"); e.printStackTrace(); } } } }