By: CS2103T W13-4

1. Setting up

Refer to the guide here.

2. Design

2.1. Architecture

ArchitectureDiagram
Figure 1. Architecture Diagram

The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.

Main has two classes called Main and MainApp. It is responsible for:

  • At app launch, initialising the components in the correct sequence, and connecting them up with each other.

  • At shut down, shutting down the components and invoking cleanup methods where necessary.

Commons represents a collection of classes used by multiple other components. The following class plays an important role at the architecture level:

  • LogsCenter : Used by many classes to write log messages to the App’s log file.

The rest of the App consists of four components.

  • UI: The UI of the App.

  • Logic: The command executor.

  • Model: Holds the data of the App in-memory.

  • Storage: Reads data from, and writes data to, the hard disk.

Each of the four components

  • Defines its API in an interface with the same name as the Component.

  • Exposes its functionality using a {Component Name}Manager class.

For example, the Logic component (see the class diagram given below) defines it’s API in the Logic.java interface and exposes its functionality using the LogicManager.java class.

LogicClassDiagram
Figure 2. Class Diagram of the Logic Component

How the architecture components interact with each other

The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command account delete 1.

ArchitectureSequenceDiagram
Figure 3. Component interactions for account delete 1 command

The sections below give more details of each component.

2.2. UI component

UiClassDiagram
Figure 4. Structure of the UI Component

API : Ui.java

The UI consists of a MainWindow that is made up of parts e.g.CommandBox, ResultDisplay, ListPanel, StatusBarFooter etc. All these, including the MainWindow, inherit from the abstract UiPart class.

The UI component uses JavaFx UI framework. The layout of these UI parts are defined in matching .fxml files that are in the src/main/resources/view folder. For example, the layout of the MainWindow is specified in MainWindow.fxml

The UI component,

  • Executes user commands using the Logic component.

  • Listens for changes to Model data so that the UI can be updated with the modified data.

2.3. Logic component

LogicClassDiagram
Figure 5. Structure of the Logic Component

API : Logic.java

  1. Logic uses the CommandLineParser class to parse the user command.

  2. This results in a Command object which is executed by the LogicManager.

  3. The command execution can affect the Model (e.g. adding a loan).

  4. The result of the command execution is encapsulated as a CommandResult object which is passed back to the Ui.

  5. The CommandResult is used to display feedback to the user and update the current active tab based on its CommandCategory.

  6. For certain commands (e.g. help, script add) the CommandResult will have a CommandContinuation that will perform further actions or return a new CommandResult to evaluate again.

Given below is the Sequence Diagram for interactions within the Logic component for the execute("account delete 1") API call.

DeleteSequenceDiagram
Figure 6. Interactions Inside the Logic Component for the account delete 1 Command

2.4. Model component

ModelClassDiagram
Figure 7. Structure of the Model Component
The lower levels of each XYZManager class can be viewed in their respective sections in the Implementation section. For example, a more detailed class diagram of the AccountsManager can be found in Section 3.1.1.

API : Model.java

The Model,

  • stores a UserPref object that represents the user’s preferences.

  • stores Budget Buddy’s data.

  • exposes multiple unmodifiable ObservableLists that can be 'observed' e.g. the different DisplayPanels in UI are bound to these lists so that the UI automatically updates when the data in the lists change.

  • does not depend on any of the other three components.

2.5. Storage component

StorageClassDiagram
Figure 8. Structure of the Storage Component

API : Storage.java

The Storage component

  • can save UserPref objects in JSON format and read it back.

  • can save Budget Buddy’s data in JSON format and read it back.

Budget Buddy saves its data in a few different JSON files, namely accounts.json, loans.json, rules.json and /scripts/descriptions.json. In addition, the user’s custom scripts are saved in the /scripts/ folder.

2.6. Common classes

Classes used by multiple components are in the budgetbuddy.commons package.

3. Implementation

This section describes some noteworthy details on how certain features are implemented.

3.1. Accounts Feature

3.1.1. Implementation

The Accounts Feature allows the users to manage their accounts. It is managed by AccountsManager, with Account objects stored internally in an accounts and filteredAccounts.

The class diagram below shows how the AccountsManager manages its list of Account objects:

][width=85%

Each Account object has the following attributes:

  • name:Name — The name of account created.

  • description:Description — A description of the account to describe the use of the account.

  • transactionList:TransactionList — The list of transactions associated with the account.

  • isActiveBooleanProperty:BooleanProperty — The boolean property indicating whether an account is active or inactive.

  • balance:long — The balance of the account, calculated by the net sum of expenses and income.

  • balanceLongProperty:LongProperty — The Long property of the balance.

  • categoryset:Set<Category> — The set of categories involved in the account.

To facilitate the manipulation of Account objects, AccountsManager implements the following operations:

  • AccountsManager#updateFilteredAccountList(Predicate<Account> predicate) — Updates the predicate of filteredAccounts.

  • AccountsManager#getFilteredAccountList() — Gets the list of filteredAccounts after applying its predicate.

  • AccountsManager#resetFilteredAccountList() — Reset filteredAccounts so that all accounts present in accounts exist in filteredAccounts.

  • AccountsManager#getActiveTransactionList() — Gets the activeTransactionList of the active account.

  • AccountsManager#getAccounts() — Gets the list of accounts.

  • AccountsManager#size() — Gets the size of the accounts list.

  • AccountsManager#addAccount(Account toAdd) — Adds the Account toAdd to accounts.

  • AccountsManager#deleteAccount(Account toDelete) — Deletes the account at Index toDelete from accounts.

  • AccountsManager#editAccount(Index toEdit, Account editedAccount) — Edits the account at Index Edit to match Account editedAccount.

  • AccountsManager#switchActiveAccount(Index targetAccountIndex) — Inactivate the current active account, and activate the target account.

  • AccountsManager#unsetActiveAccount() — Inactivate any currently active account.

  • AccountsManager#setActiveAccount(Index toSet) — Activate the target account.

  • AccountsManager#getAccount() — Gets the target account.

  • AccountsManager#getActiveAccountIndex() — Gets the index of the currently active account.

  • AccountsManager#getActiveAccount() — Gets the currently active account.

  • AccountsManager#transactionListSwitchSource(Account account) — Switches the account source for the TransactionList.

  • AccountsManager#transactionListUpdateSource() — Updates the transactionList linked to the currentActiveAccount.

  • AccountsManager#exportReport() — Exports the overview report of all accounts. When the user inputs a command, several of the above operations are carried out. For example, account edit will call AccountsManager#resetFilteredAccountList to update the filteredAccounts, so that all accounts present in accounts will be present in filteredAccounts, then AccountsManager#editAccount to edit the account, finally AccountsManager#getFilteredAccountList() to display the list of accounts.

After each command, the list of accounts is saved in accounts.json, which is stored in a data folder in the same directory as budgetbuddy.jar.

Given below is an example usage scenario and how the AccountsManager behaves at each step.

Step 1. The user launches the application. If this is the first time it is launched, accounts.json is created and the AccountsManager initializes with an accounts containing a default account. Otherwise, the data in accounts.json is loaded into accounts.

Step 2. The user executes account add n/Japan trip d/expense spent in Japan to add a new account. This creates a new account toAdd with the name as Japan trip and description as expense spent in Japan. AccountsManager#addAccount(Account toAdd) adds toAdd to accounts. filteredAccountList will be automatically updated to match accounts.

Step 3. The user executes the command account find trip to find account contains the keyword trip specified. AccountsManager#updateFilteredAccountList sets the predicate of filteredAccounts according to the input parameters. Finally, AccountsManager#getFilteredAccounts retrieves an immutable version of filteredAccounts (filtered) to display to the user. In this case, an account with the name as Japan trip and description as expense spent in Japan will be displayed.

The following sequence diagram shows how finding the accounts containing specified keyword works:

][width=85%

Most of the commands and operations behave in the same way. The only difference will be the the action taken by the operation (e.g. finding account or adding account).

Step 4. The user executes account delete 2 to delete the second account in the accounts. Firstly, AccountsManager#resetFilteredAccountList will update the filteredAccounts, so that all accounts present in accounts will be present in filteredAccounts, then AccountsManager#deleteAccount deletes toDelete account from accounts.

Step 5. The user executes account edit 3 n/food to edit the name of the first account. A new editedAccount is created, which is the same as the first third account except for its name which is food. AccountsManager#editAccount(Index toEdit, Account editedAccount replaces the account at index toEdit with editedAccount.

The activity diagram below shows what happens when the user executes account edit:

][width=85%

Step 6. The user executes account report 2 to view the details of the second account. Firstly, AccountsManager#resetFilteredAccountList will update the filteredAccounts, so that all accounts present in accounts will be present in filteredAccounts, then AccountsManager#getAccount and Account#getAccountInfo are used to display the details of the second account.

Step 7. The user executes account overview to view the report of all accounts in an html export file. Firstly, AccountsManager#resetFilteredAccountList will update the filteredAccounts, so that all accounts present in accounts will be present in filteredAccounts, then AccountsManager#exportReport generates the overview of all accounts html file to the exports folder.

For account edit, account delete and account report, if the user targets an index beyond the last index, an error message is displayed.

3.1.2. Design Considerations

Aspect: Interaction with ui - the list retrieved by LogicManager

In the mainWindow of ui, AccountTab is associated with a list of accounts. However, two lists of accounts are required. One stores all the current accounts present in accounts, the other one stores the filteredAccounts with the filtered accounts after account find executes.

  • Alternative 1 (current choice): AccountTab is only associated with filteredAccounts as filteredAccounts stores all accounts. After each command, AccountsManager#resetFilteredAccountList is called to reset the predicate to be true, so that filteredAccounts matches accounts.

    • Pros: Only one list of accounts is associated with LogicManager.

    • Cons: It is counter-intuitive as filteredAccounts is supposed to stored the accounts that have been selected.

  • Alternative 2: AccountListPanel is associated with both filteredAccounts and accounts, and the display of the list switches when necessary.

    • Pros: Easy to understand and align with the common sense.

    • Cons: Hard to implement.

3.2. Active Account

3.2.1. Implementation

The active Account allows operations on Transaction objects to not have to specify the concerning Account when the command is entered. This allows for a more user-friendly and intuitive experience with managing finances. By updating ActiveAccountIndex where appropriate, the activeAccount is implemented on the idea that users want to continue to manipulate transactions within the same Account that they last interacted with.

The current active Account is indicated in the UI by a highlight as shown below.

ActiveAccount1
Figure 9. Screenshot of the UI showing the highlighted active Account.

The following user commands, if executed successfully, can change the active account:

  • AccountAddCommand — The added Account becomes the active account.

  • AccountDeleteCommand — If the active Account is deleted, a new active Account will be selected.

  • AccountEditCommand — The edited Account becomes the active account.

  • AccountFindCommand — The first Account in the list is set as the active Account.

  • AccountListCommand — The first Account in the list is set as the active Account.

  • AccountReportCommand — The target Account is set as the new active Account.

  • AccountSwitchCommand — The target Account is set as the new active Account.

  • TransactionAddCommand — The target Account is set as the new active Account.

  • TransactionEditCommand — The target Account is set as the new active Account.

3.2.2. Design Considerations

Aspect: responsive UI for showing the active Account

An Extractor is used to update the UI when an Account is set as active, or when the balance is updated. This is because the FilteredList<Account> only updates automatically when the Accounts themselves are changed, but not when their internal elements are altered.

  • Alternative 1 (current choice): Use an Extractor to listen for changes to isActiveBooleanProperty and balanceLongProperty.

    • Pros: Less overhead/unnecessary movement of accounts

    • Cons: Not as easy for new developers to understand and implement.

  • Alternative 2: Refresh the account list by forcing removal and re-addition of each of the accounts.

    • Pros: Easy to implement, no need to change members to adapt to the Property class, and easier for newer developers to read and understand.

    • Cons: High overhead, as more and more accounts are added to the BudgetBuddy.

3.3. Transaction Model

3.3.1. Implementation

Transactions are the main elements of the BudgetBuddy’s expenses tracker. Within each Account, a TransactionList is stored, and inside each TransactionList, Transaction objects are stored internally in the internalList of the TransactionList.

The class diagram below shows how Transaction objects are stored within an Account.

TransactionClassDiagram
Figure 10. Class Diagram of the Transactions Model

Each Transaction object must have:

  • direction: Direction — The direction of the transaction (either IN for money inflow or OUT for money outflow).

  • amount: Amount — The amount of money transacted.

  • date: LocalDate — The date of the transaction.

Optional attributes for Transaction objects:

  • description: Description — A short description about the transaction.

  • categories: Set<Category> — A Set of Category objects that categorize the transaction.

Because Transaction and TransactionList objects are contained within Account objects, which are in turn stored within the AccountsManager, it is called when any operations is made on a Transaction. Additionally, AccountsManager has an activeTransactionList which holds the TransactionList of the current active Account. activeTransactionList is then wrapped by a SortedList and FilteredList to enable the sorting and filtering of the Transaction objects.

These methods in AccountsManager below that manipulate the activeTransactionList are called when the list of transactions needs to be filtered/sorted:

  • AccountsManager#getFilteredTransactionList() — Returns an ObservableList of the filtered transactions.

  • AccountsManager#updateFilteredTransactionList(Predicate<Transaction> predicate) — Updates the Predicate<Transaction> of the FilteredList to filter by the given Predicate<Transaction>

  • AccountsManager#resetFilteredTransactionList() — Resets and removes any filter on the FilteredList.

  • AccountsManager#updateSortedTransactionList(Comparator<Transaction> comparator) — Updates the Comparator<Transaction> of the SortedList to sort by the given Comparator<Transaction>

  • AccountsManager#resetSortedTransactionList() — Resets the Comparator on the FilteredList to the default comparator.

When Transaction objects are added/edited/deleted, the following implemented methods are called from Account:

  • Account#addTransaction(Transaction toAdd) — Adds a Transaction to the transactionList.

  • Account#updateTransaction(Index txnIndex, Transaction editedTxn) — Sets a Transaction to the transactionList at the specified Index.

  • Account#deleteTransaction(Transaction toDelete) — Deletes a Transaction from the transactionList.

Transaction objects are saved within their respective Account objects after every command, and they are saved into the file accounts.json, stored in the same directory as the JAR executable.

3.3.2. Design Considerations

Aspect: Reference to respective Account in Transaction
  • Alternative 1 (current choice): Transaction has no reference to Account; any commands that create/edit/delete Transaction objects are called via the active Account.

    • Pros: Reduced coupling, which makes testing/maintenance easier.

    • Cons: More methods are required to ensure the logic of Transaction commands and the current active Account, instead of calling getAccount() on a Transaction.

  • Alternative 2: Transaction holds a reference to Account; commands that create/edit/delete Transaction objects directly reach the Account through the Transaction.

    • Pros: It would result in shorter code by avoiding having to go through AccountsManager to find the respective Account.

    • Cons: This would result in a circular dependency between Transaction and Account, as well as high coupling. This lowers testability and increases the risk of bugs.

3.4. Adding Transactions: txn

Given below is an example usage scenario and how the txn command is processed and executed at each step.

Step 1: The user (ideally an NUS Computing Student) executes txn dn/out x/50 d/books a/school to add a purchase of $50 worth of books, to be added to the school Account.

Step 2: LogicManager calls parseCommand on the command string txn dn/out x/50 d/books a/school

Step 3: CommandLineParser creates a new TransactionAddCommandParser by identifying the txn String that represents a TransactionAddCommand

Step 4: TransactionAddCommandParser parses the rest of the command string dn/out x/50 d/books a/school and return a new TransactionAddCommand with the parsed details.

Step 5: LogicManager then calls execute() on the TransactionAddCommand, which gets the Account school from the AccountsManager

Step 6: addTransaction is called on the returned Account to add the Transaction to the TransactionList of the Account`.

Step 7: After adding the Transaction, RuleEngine is called to execute its rules over the newly added Transaction. The full diagram sequence diagram for this step can be found below, in the Implementation of the Rules Execution feature.

Step 8: The Account which was added to is set as the active Account with setActiveAccount().

Step 9: Finally, TransactionAddCommand returns a CommandResult to the LogicManager.

Step 10: Before LogicManager returns the CommandResult, it calls save(Model model) on the StorageManager, which saves all the Account objects.

The following sequence diagram illustrates the scenario as described above (the execution of txn dn/out x/50 d/books a/school):

TransactionSequenceDiagram
Figure 11. Sequence Diagram of the TransactionAddCommand

3.5. Editing transactions: txn edit

txn edit allows users to edit any attribute of the Transaction. It uses a TransactionEditDescriptor to store the details of the new Transaction, then uses getUpdatedTransaction to generate the edited Transaction as well as validate that changes have been made to the transaction.

Below is an example scenario to demonstrate how a Transaction is edited, as well as certain additional steps that are taken when certain attributes of the Transaction are edited, for example to edit the Account or to edit the categories. The activity diagram accompanies this explanation, to help visualise the steps that might take place.

TransactionActivityDiagram
Figure 12. Activity Diagram of the execution of a TransactionEditCommand

Assume that txn add dn/out x/20 d/bill a/home was just executed, and that the Transaction shows up in the first index:

TransactionEdit1
Figure 13. Screenshot of previously added transaction

Step 1: The user (ideally an NUS Computing Student) executes txn edit 1 x/28 d/phone bill a/personal c/phone c/bill to edit the previously added Transaction to a $28 phone bill, to be added to the personal Account.

Step 2: LogicManager calls parseCommand on the command string txn edit 1 x/28 d/phone bill a/personal c/phone c/bill

Step 3: CommandLineParser creates a new TransactionEditCommandParser by identifying the txn edit String that represents a TransactionAddCommand

Step 4: TransactionAddCommandParser parses the rest of the command string 1 x/28 d/phone bill a/personal c/phone c/bill and return a new TransactionEditCommand along with the TransactionEditDescriptor and the updatedAccountName, which represents the Name of the new target Account. A new Set<Categories> is also created from the c/phone and c/bill arguments provided.

Step 5: LogicManager then calls execute() on the TransactionEditCommand. Then, the original Transaction is obtained, followed by the target Account and the edited Transaction via getUpdatedTransaction. Meanwhile, validation occurs at every step, to ensure that the parsed attributes conform to the requirements to edit the `Transaction.

Step 6: deleteTransaction is called on the Account containing the Transaction to be edited.

Step 7: addTransaction is called on the target Account, followed by the execution of the RulesEngine, similar to the situation as shown further below.

Step 8: The Account which was added to is set as the active Account with setActiveAccount().

Step 9: Finally, TransactionAddCommand returns a CommandResult to the LogicManager.

Step 10: Before LogicManager returns the CommandResult, it calls save(Model model) on the StorageManager, which saves all the Account objects.

3.6. Loans Feature

3.6.1. Implementation

The Loans feature exists outside of the Account/Transaction mechanisms. It adds a separate LoansManager alongside the main AccountsManager, with Loan objects stored internally in an internalList.

The following class diagram demonstrates the association between the LoansManager and Loan objects. Miscellaneous methods (such as LoansManager#getLoans and LoansManager#getLoansCount) are omitted.

LoanClassDiagram
Figure 14. Class Diagram of the Loans Model
The Debtor class, LoansManager#debtors, LoansManager#setDebtors and LoansManager#getDebtors can be ignored for now. They are depicted here for the sake of completion, but will only be used when discussing the loan split command in a later section.

Each Loan object has the following attributes:

  • person:Person — The person that the user lent/borrowed money to/from.

  • amount:Amount — The amount of money loaned.

  • direction:Direction — The direction of the loan (either IN or OUT).

  • status:Status — The status of the loan (either PAID or UNPAID).

  • date:LocalDate — The date of the loan.

  • description:Description — A description of the loan.

To facilitate the manipulation of Loan objects, LoansManager implements the following operations:

  • LoansManager#updateFilteredList(Predicate<Loan> predicate) — Updates the current predicate of filteredLoans to predicate.

  • LoansManager#sortLoans(Comparator<Loan> sorter) — Sorts internalList using the given sorter.

  • LoansManager#getFilteredLoans() — Gets filteredLoans, representing the loans in internalList after filtering.

  • LoansManager#addLoan(Loan toAdd) — Adds the Loan toAdd to internalList.

  • LoansManager#editLoan(Index toEdit, Loan editedLoan) — Replaces the loan at Index toEdit with Loan editedLoan.

  • LoansManager#updateStatus(Index toUpdate, Loan updatedLoan) — Replace the loan at Index toUpdate with Loan updatedLoan.

  • LoansManager#deleteLoan(Index toDelete) — Deletes the loan at Index toDelete from internalList.

Each user-given command will call at least one of the above operations. For example, loan delete will call LoansManager#deleteLoan to delete the targeted loan(s), then LoansManager#getFilteredLoans to display the remaining loans.

After each command, the state of internalList is saved in the file loans.json. loans.json is stored on the local hard disk in a data folder, which is in the same directory as budgetbuddy.jar.

Given below is an example usage scenario and how the LoansManager behaves at each step.

Step 1. The user launches the application. If loans.json exists on the hard disk, its data is loaded into internalList. Otherwise, loans.json is created and the LoansManager initializes with an internalList containing a few sample loans.

Step 2. The user executes the command loan out p/John x/4.20 d/Paid for his lunch to add a new loan. This creates a new loan toAdd of amount 4.20 out to the person John, with the description Paid for his lunch. Since the user did not provide a date, the current system date is used for the date of toAdd. LoansManager#addLoan(Loan toAdd) is called and (after verifying that toAdd does not already exist in internalList) toAdd is added to internalList.

The following sequence diagram illustrates the process of adding a loan:

LoanSequenceDiagram
Figure 15. Sequence Diagram for Adding a Loan

In general, the rest of the operations work using a similar sequence of steps. Some commands might create a new Loan object (as shown above) while others might just use the Index of a loan (e.g. loan delete).

Step 3. The user executes the command loan list out p/John w/1/11/2019 s/x to see all loans out to John dated 1/11/2019, sorted by amount. First, LoansManager#sortLoans is called to sort the loans in internalList by their amounts in ascending order. LoansManager#updateFilteredList is then called to set the predicate of filteredLoans; the new predicate filters the list to loans out to the person John on 1/11/2019. Finally, LoansManager#getFilteredLoans is called to display the (sorted and filtered) list to the user.

Step 4. The user executes the command loan paid 1 to update the status of the first loan in the list to PAID. This creates a new updatedLoan identical to the first loan in internalList, except that updatedLoan has the status PAID. LoansManager#updateStatus(Index toUpdate, Loan updatedLoan) is called (where toUpdate is the index of the first loan in internalList) and the loan at index toUpdate is replaced with updatedLoan.

The command loan unpaid works identically to loan paid, except that the status of updatedLoan is set to UNPAID.

Step 5. The user executes the command loan edit 1 x/500 to edit the amount of the first loan in the list to 500. LoansManager#editLoan(Index toEdit, Loan editedLoan) is called and the loan at index toEdit is replaced with an editedLoan that has an amount of 500. While this operation appears identical to LoansManager#updateStatus, LoansManager#editLoan implements an extra check to ensure that editedLoan does not already exist in internalList.

LoansManager#updateStatus does not implement the check for a duplicate loan as the status of a Loan is not considered when comparing two loans for equality. internalList is already guaranteed to have no identical loans due to the checks in LoansManager#addLoan and LoansManager#editLoan.

Step 6. The user executes the command loan delete 1 to delete the first loan in the list. LoansManager#deleteLoan(Index toDelete) is called (where toDelete is the index of the first loan in internalList) and the loan at index toDelete is removed from internalList.

For the commands loan paid, loan unpaid and loan delete, if the user targets a loan outside of the list (e.g. index greater than list size) an error message will be displayed.
Multi-Loan Targeting

The user can target and act on multiple loans with a single command. For example, loan paid 1 3 4 can be used to mark the first, third and fourth loans in internalList as PAID. Alternatively or additionally, loan paid p/John p/Mary can also be used to mark all the loans of the persons John and Mary in internalList. The LoansManager handles this by executing the appropriate operation repeatedly. In the case of loan paid 1 3 4, LoansManager#editLoan is called once for each of the three loans.

To account for the fact that the list size might change after each operation, the size of the list before and after each operation is compared. If it has changed, the targeted indices are adjusted accordingly.

If any of the target loan indices cannot be found by the LoansManager in internalList, they are added to a missingLoanIndices list. Similarly, target persons that cannot be found are added to a missingPersons list. Both lists are displayed to the user after other target loans that are in internalList have been acted upon, notifying the user that the target indices/persons could not be found.

Multi-loan targeting is implemented for the commands loan paid, loan unpaid and loan delete.

Loan Splitting

Loan splitting exists as a command that the user can execute. Its purpose is to split a large, initially unbalanced group payment equally among the group’s members. The following example scenario should clarify the purpose of the command:

John, Mary and Peter go out for dinner. The meal costs $100, so Peter pays $90 and Mary covers the remaining $10. However, the three want to split the bill equally among themselves.

John executes the command loan split p/John p/Mary p/Peter x/0 x/10 x/90. Budget Buddy then outputs a list of the necessary payments between the three. From this list, John can now see that he owes Peter $33.33 and that Mary owes Peter $23.33.

To display the results of the calculations, LoansManager holds a list of debtors containing Debtor objects. For your convenience, the class diagram of the Loans model is reproduced here:

LoanClassDiagram
Figure 16. Reproduction of Class Diagram of the Loans Model

Each Debtor object has the following attributes:

  • debtor:Person — The person who owes money to one or more creditors.

  • creditors:HashMap<Person, Amount> — A list of persons that the debtor owes money to, mapped to the amount of money owed. Each entry in the HashMap represents a creditor, with their name as the Person key and the amount owed as the corresponding Amount value.

LoansManager also implements the getDebtors and setDebtors operations to manipulate the debtors list. This list is used to hold and display the Debtor objects created for the latest execution of loan split and is stored between sessions in loans.json.

Given below is an example scenario to demonstrate how the final list is calculated. The algorithm as a whole can be summarized in the activity diagram below, which might prove helpful for following the steps in the example scenario:

LoanSplitActivityDiagram
Figure 17. Activity Diagram for Loan Split Algorithm

Step 1. The user executes the command loan split p/Ben p/Duke p/Adam p/Zed x/0 x/20 x/80 x/50 max/10 me/Ben d/Dinner. In this scenario, out of the total bill of $150, Ben has paid $0, Duke has paid $20, Adam has paid $80 and Zed has paid $50. Furthermore, max/10 specifies that Ben should only pay/owe up to $10 overall. Finally, me/Ben marks Ben as the user; all debts involving Ben should be added to the normal loan list with the description Dinner.

Step 2. LoanSplitCommandParser parses the persons, amounts, and max shares into List<Person>, List<Amount> and List<Long> respectively. me/Ben and d/Dinner are parsed into Optional<Person> and Optional<Description>. A new LoanSplitCommand is instantiated with the lists and optional objects.

Step 3. LoanSplitCommand replaces Ben in List<Person> with a Person with the Name You. Using the static methods of a LoanSplitCalculator class, it then begins executing the following algorithm:

  1. The defaultSharePerPerson is calculated. In this scenario, $150 should be split among the 4 persons. However, as Ben has a max share of $10, the other 3 persons must divide $140 among themselves, resulting in a defaultSharePerPerson of $46.66.

  2. Each person in List<Person> is given a balance, calculated using the amount they paid initially (from List<Amount>) minus either the defaultSharePerPerson or their max share if present.

  3. A list of all possible sub-groups (combinations) of persons is generated. In this scenario, the number of sub-groups for the 4 persons would be 16.

  4. For each sub-group, if the sum of their balances is zero, then the following steps are performed:

    1. Take the persons with the smallest and biggest balances: the debtor and creditor respectively.

    2. Transfer money between the two such that one or both of their balances reaches zero. The person(s) with a balance of zero are then removed from the group, and a record of the debtor, creditor and amountTransferred is stored in a List<DebtorCreditorAmount>.

    3. Repeat until the sub-group contains less than two persons.

  5. After every sub-group has been processed, List<DebtorCreditorAmount> is used to create the final List<Debtor> stored in LoansManager.

Step 4. All debts involving the person You in List<Person> are used to create loans with the description Dinner. These loans are added to internalList in LoansManager using LoansManager#addLoan.

Step 5. The list of debtors in LoansManager is displayed to the user. In this scenario, the display will show that Duke owes Adam $26.66 and You (Ben, the user) owe Zed and Adam $3.32 and $6.68 respectively.

3.6.2. Design Considerations

Aspect: Structure of the loan - person interaction
  • Alternative 1 (current choice): LoansManager stores internalList; each Loan references a Person

    • Pros: Easy to implement and understand.

    • Cons: Takes a longer time to get all the loans belonging to a person.

  • Alternative 2: LoansManager stores persons list; each Person stores LoanList; each Loan references Person

    • Pros: Easy to retrieve the person of each loan and retrieve all the loans belonging to a person.

    • Cons: Circular dependency and high coupling, potentially leading to lower testability and a higher bug count.

3.7. Rule Management Feature

The Rules feature exists as an integration onto the Transaction system. It makes use of syntax processing, together with scripts to provide an automation solution to repetitive tasks when adding transactions.

Rules are defined with a pair of predicate and action, where an action is performed given that the predicate returns true. This predicate-action split allows us to decouple testing from performing, which helps to increase reusability of individual predicates and actions.

3.7.1. Implementation

Basic attributes and operators are exposed to provide users a way of writing simple tests on transactions without having to manually check and make changes. Storing rules works similarly to LoansManager, where individual rules are stored in a RuleManager which manages all CRUD operations.

All rules are stored in a JSON file when added, formatted to be retrieved and parsed by the application when relaunched.

The following class diagram illustrates the structure of the Rule Model component.

RuleModelClassDiagram
Figure 18. Structure of the Rule Model Component
The rest of the Script and Model components have been omitted to give focus on the Rule model component.

As mentioned above, rules are defined as a pair of predicate and action, which as seen in the above diagram, is divided into the two abstract classes RulePredicate and RuleAction. These two classes are abstract due to two implementation types, either script or expression. Their concrete classes are PredicateExpression and ActionExpression for expressions and PredicateScript and ActionScript for scripts respectively.

For predicate expressions, they are formed using binary comparison operations, which means that each expression contains a predefined Operator which takes in two arguments, a predefined Attribute to represent an attribute of a Transaction, as well as a Value to represent a given value to test against the attribute.

Here is an example of a part of what a user may type as a predicate expression:
p/outamt < 10

This is split into the Attribute: outamt , the Operator: < , as well as the Value: 10 .

Action expressions are unary operations, which means that each expression is formed with an Operator as well, but takes only a single argument, a Value to represent a given value to carry out the operation with.

Again, here is an example of a part of what a user may type as an action expression:
a/set_cat Cheap

This is split into the Operator: set_cat , as well as the Value: Cheap .

Predicates and actions implemented as scripts on the other hand are each defined with a single ScriptName, which refers to the name of the script itself.

An example of both predicate and action script names a user may type:
p/CheckIsTransport a/SetTransportFare

In this case, both CheckIsTransport and SetTransportFare are ScriptNames .

Each and every Rule is stored within the RuleManager, which serves as an interface to manipulating the list of rules. For example, RuleManager#addRule is used to add new rules to the list, whereas RuleManager#swapRules is used to swap the order of two rules in the list. The RuleManager supports basic CRUD operations, as well as other convenience methods such as the RuleManager#swapRules as mentioned.

3.7.2. Design Considerations

Aspect: Structure of expressions for rule data management
  • Alternative 1 (current choice): The two expression classes PredicateExpression and ActionExpression are split into their individual components, the operators and the arguments.

    • Pros: More control over the expression format, and increase reusability of individual components.

    • Cons: More classes required to implement, greater overhead.

  • Alternative 2: Expressions are just entirely stored as strings within the two classes.

    • Pros: Easy to implement, ease of storage.

    • Cons: Parsing has to be done even after the rule is added. This means we need to parse the expression more than once, not only to check the validity, but again to process the rule before execution.

Aspect: Structure of scripts for rule data management
  • Alternative 1 (current choice): The two script classes PredicateScript and ActionScript contain only a ScriptName, which is the name of the script they are referencing.

    • Pros: No need to reference an entire script, and therefore much easier to validate. Storing names will also be much simpler.

    • Cons: Requires checking against the ScriptLibrary to retrieve a Script when processing rules.

  • Alternative 2: The classes will each store an entire Script within them.

    • Pros: Referencing the script code when processing rules will not require access to the ScriptLibrary, and can be taken directly from the PredicateScript or ActionScript directly.

    • Cons: Creating unnecessary entire references to a script that already exists in the ScriptLibrary, and makes storage much more complicated, as the entire script will be contained within the class.

3.8. Rule Execution Feature

The structure of rules were separated from the logic of rule execution to maintain the separation of concerns between the Model and Logic components.

Rule execution is hooked into the evaluation of adding or editing a Transaction. This means that for every new transaction, all rules within the Rule Engine will be executed on that transaction. The same can be said for modifying a transaction.

The implementation of the Rule execution is elaborated on below.

3.8.1. Implementation

The RuleEngine is a static class used for interfacing with all the rule processings functionality.

Two executable classes are used in the execution of a rule, Testable and Performable. A Testable represents the executable form of a RulePredicate, which may be either an expression or a script. Correspondingly, a Performable represents the executable form of a RuleAction, which may also be either an expression or a script.

Before executing the existing rules, the index of the transaction and the account that the transaction belongs to are supplied to the RuleEngine through the RuleEngine#executeRules method. This allows for the retrieval of the transaction when a rule is executed against it.

When a rule is executed, this is firstly represented as the execution of the Testable#test method on the given transaction. If the test passes, the predicate is true, and therefore the action is performed. This is represented as the execution of the Performable#perform method on the given transaction.

The following sequence diagram shows the interaction between the RuleEngine and the different objects involved in the execution of the rules on a transaction:

RuleExecutionSequenceDiagram
Figure 19. Sequence diagram representing Rule Execution after adding a new Transaction

Shown above is a sequence diagram which takes place during the execution of the TransactionAddCommand, after the new transaction has already been added. The RuleEngine takes over, and retrieves the relevant handlers from Model.

Thereafter, the list of rules is retrieved from the RuleManager. The RuleEngine iterates through the list, using the RulePredicate and RuleAction of each rule to create the required Testables for testing on the transaction, as well as the Performables for performing the action.

The following activity diagram shows in greater detail the workflow of executing rules.

RuleExecutionActivityDiagram
Figure 20. Activity diagram of the different paths involved in the workflow of executing rules

The activity diagram above has a slightly different context as the sequence diagram, to show a separate use case. In this diagram, instead of a new transaction that is added, we have a transaction that is edited. Both types of commands do not affect the workflow of rule execution.

In this diagram, the generation of a Testable and Performable is shown in greater detail.

Testable is an interface which, like RulePredicate, have its implementations split into expressions and scripts, namely TestableExpression and TestableScript.

Similarly, Performable is an interface which, like RuleAction, have its implementations split into expressions and scripts, namely PerformableExpression and PerformableScript.

Expressions are generated by the RuleEngine when either the RulePredicate or RuleAction are of the expression type. The RuleEngine will retrieve the correct expression constructor from an internal hash map based on the Operator, and create the expression using the given attribute and/or value.

For example, we have an predicate p/desc contains food. One of the classes implementing TestableExpression, ContainsExpression, is created since the predicate has the Operator: contains. The corresponding Attribute: desc and Value: food in the predicate are provided into the ContainsExpression constructor during instantiation.

Scripts, on the other hand, are generated by the RuleEngine when either the RulePredicate or RuleAction are of the script type. The RuleEngine will generate the corresponding Testable or Performable by first retrieving the script from the ScriptLibrary based on its ScriptName. Following that, a TestableScript or PerformableScript is instantiated with a function ScriptEvaluator, which evaluates the script given the transaction and account. This function is then called when Testable#test or Performable#perform is executed.

3.8.2. Design Considerations

Aspect: Duplication of predicates and actions in model and logic
  • Alternative 1 (current choice): Both predicates and actions have their corresponding versions in both model and logic.

    • Pros: Able to split the logic flow and execution code from the data in model.

    • Cons: Seemingly duplicate classes, such as PredicateExpression and TestableExpression, which increases the number of classes.

  • Alternative 2: All execution data and logic is stored in the rule model rather than logic.

    • Pros: Reduce class duplication, less confusion.

    • Cons: Model and Logic will have unnecessary coupling which reduces testability and makes maintenance and integration harder.

3.9. Scripting

The scripting feature allows users to write scripts to automate tasks and extend the functionality of Budget Buddy.

3.9.1. Implementation

The script engine works independently of the rest of the application. At its core, it uses the Nashorn ECMAScript 5.1 engine bundled with Java 11 to evaluate scripts.

A set of convenience functions are provided to make basic tasks, such as manipulating transactions and accounts, easier. The full model is nevertheless exposed to scripts, and scripts are able to access any classes provided in the Java 11 standard library, as well as any dependencies included in the application.

There is a simple mechanism to store scripts to be run in future. This works together with rules to give the ability to have complex predicates and actions outside of those supported inherently by the program.

When the proposed alias feature is implemented, scripts and aliases can be used together to, in effect, allow users to create custom commands.

The following class diagram illustrates the design of the script engine and model. For clarity, parts of the model and logic components that do not interact directly with the script engine are omitted.

ScriptsClassDiagram
Figure 21. Structure of scripts in the model and logic components
Script engine

The logic component of the scripts feature contains the ScriptEngine class which underpins the entire scripting component. At its heart, the ScriptEngine class is simply a thin layer to abstract away the underlying script engine (in our case, Nashorn). It contains a method evaluateScript that accepts a string of script code, and arguments for the script, and then evaluates the script. There is an overload of evaluateScript that accepts a Script from the script model, which is simply a convenience method. There is no other relationship between the logic and model components.

The logic component also contains methods setVariable, addToEnvironment and resetEnvironment that allow functions and variables to be set in the script environment, to provide helper functions to the scripts, like those in the Scripting API.

To allow other components to add helper functions to the script environment, without the script engine having to be aware of those components, the ScriptEngine accepts ScriptEnvironmentInitialisers. A ScriptEnvironmentInitialiser is a FunctionalInterface that simply accepts a ScriptEngine and does some setup on it e.g. setting variables and functions using ScriptEngine#setVariable. ScriptEngine maintains a list of ScriptEnvironmentInitialisers and re-applies them each time ScriptEngine#resetEnvironment is called.

Library and storage

The model component of the scripts feature is a simple script library that allows users to store scripts with a name and a description for repeated use. See the interface ScriptLibrary and its implementation ScriptLibraryManager.

Scripts are persisted as individual files under the scripts/ directory in Budget Buddy’s data directory. Script names are not stored separately, but simply used as the file name of the scripts with a .js extension. See FlatfileScriptsStorage.

The script descriptions are stored in a separate descriptions.json file.

User interface

The last component of the scripts feature is the user interface.

The script engine is presented to the user as a set of simple commands. script eval and script run run scripts, while script reset resets the script environment. script add, script list and script delete manage the script library.

The following sequence diagram shows the interaction between the different components when the user runs a script from the script library using script run my-script.

ScriptsSequenceDiagram
Figure 22. Interaction between the components during the execution of script run my-script

The command is parsed into ScriptRunCommand by the command line parser. ScriptRunCommand then retrieves the Script from the ScriptLibraryManager and then hands the Script to ScriptEngine for evaluation.

Script bindings

Script bindings are added via ScriptEnvironmentInitialisers, which are registed with the ScriptEngine through Logic#addToScriptEnvironment.

Most of the functions exposed in the Scripting API come from the script-model bindings, in ScriptModelBinding. JavaFX-specific bindings are added in ScriptUiBinding, and bindings that require access to MainWindow internals are added in a private inner class of MainWindow.

Methods in the binding classes are added into the script environment by casting their method references to appropriate FunctionalInterfaces. Nashorn allows objects which implement a FunctionalInterface to be called as if they were functions, providing an ergonomic scripting experience for the user. Nashorn also helps to do some type conversion from JavaScript types to Java types, saving on the amount of boilerplate required in our implementation.

We declare an entire suite of functional interfaces in ScriptBindingInterfaces.

We do not use the functional interfaces provided in the Java package java.util.function because of these reasons:

  • These interfaces cannot be used if the methods throw any checked exceptions. Many script methods involve parsing strings into appropriate model types, which means that parsing exceptions need to be thrown, but they are checked exceptions.

  • These interfaces have generic type parameters. However, because Java implements generics using type erasure, it is not possible for Nashorn to determine the actual types of parameters, in order to do automatic conversion between JavaScript and Java types, if functional interfaces with generic type parameters were used. (They would all appear as Object at runtime.) This is also the reason that we do not use generics in the functional interfaces in ScriptBindingInterfaces.

3.9.2. Design considerations

Aspect: JavaScript engine

Two JavaScript engines were considered.

  • Alternative 1 (current choice): Use the Nashorn JavaScript engine.

    • Pros: It is bundled with Java 11.

    • Cons: It is deprecated and marked for removal in a future Java version, and it also interprets scripts, so it is slower than GraalVM.

  • Alternative 2: Use the GraalVM engine.

    • Pros: It is well-maintained, and it also just-in-time (JIT) compiles scripts, so it will be faster than Nashorn.

    • Cons: It is not bundled with Java 11, so it is an additional dependency.

We chose Nashorn because:

  • While it is slower, it is not too slow for the kinds of lightweight scripting tasks in Budget Buddy.

  • While it is deprecated, we are targetting specifically Java 11, so its future removal is not a concern at this moment.

  • It is bundled with Java 11, so we do not need to manage and ship an additional dependency.

Aspect: Script bindings
  • Alternative 1 (current choice): Use functional interfaces.

    • Pros: It requires less marshalling code in Budget Buddy, because Nashorn helps to perform most of the type conversion.

    • Cons: Many functional interfaces need to be declared for every unique method signature exposed to the script environment.

  • Alternative 2: Extend Nashorn’s AbstractJSObject, overriding the call method.

    • Pros: No extra declarations (e.g. functional interfaces) need to be made if a new method signature is to be exposed to the script environment.

    • Cons: Nashorn’s built-in type conversion and marshalling are not applicable; it has to be done manually in our AbstractJSObject subclass.

3.10. [Proposed] Aliases

The alias is a simple hook into the command parsing engine. If there is no built-in command corresponding to a command line, then the alias map is checked. If there is a matching alias, then the alias name in the command line is replaced, and the command execution is re-tried.

To prevent alias loops where the user creates an alias x mapping to y, and an alias y mapping to x, we track the aliases that have been applied, and stop evaluation if we see that the same alias has been applied more than once.

The following activity diagram illustrates the above algorithm.

AliasActivityDiagram
Figure 23. Alias resolution algorithm

3.11. Logging

We are using java.util.logging package for logging. The LogsCenter class is used to manage the logging levels and logging destinations.

  • The logging level can be controlled using the logLevel setting in the configuration file (See Section 3.12, “Configuration”)

  • The Logger for a class can be obtained using LogsCenter.getLogger(Class) which will log messages according to the specified logging level

  • Currently log messages are output through: Console and to a .log file.

Logging Levels

  • SEVERE : Critical problem detected which may possibly cause the termination of the application

  • WARNING : Can continue, but with caution

  • INFO : Information showing the noteworthy actions by the App

  • FINE : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size

3.12. Configuration

Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: config.json).

4. Documentation

Refer to the guide here.

5. Testing

Refer to the guide here.

6. Dev Ops

Refer to the guide here.

Appendix A: Product Scope

Target user profile:

  • has a need to manage expenses and income over a significant period of time

  • prefer desktop apps over other types

  • can type fast

  • prefers typing over mouse input

  • is reasonably comfortable using CLI apps

  • capable of basic programming to customize the app to their liking

Value proposition: manage expenses/income faster than a typical mouse/GUI driven app

Appendix B: User Stories

Priorities: High (must have) - * * *, Medium (nice to have) - * *, Low (unlikely to have) - *

Priority As a …​ I want to …​ So that I can…​

* * *

user

see an overview of all my spending from month to month

better judge my expense/income ratio

* * *

user who borrows and loans money frequently

track who owes me money/who I owe money to

settle my debts

* * *

careful spender

dedicate different accounts to different holidays

control how much I spend while on the holidays

* * *

busy user

keep track of my spending in different account books

segregate completely unrelated spending

* * *

lazy user

simplify repetitive actions by setting rules for every transaction added

automate repetitive and tedious processes

* * *

SoC student experienced in writing programs

write my own scripts to manipulate entries in the app

automate and customize the app to my liking

* *

spendthrift

set a budget for my monthly spending

avoid breaking the bank again

* *

lazy user

key in recurring expenses just once

do not have to enter them over and over again

* *

careful spender

make month-to-month comparisons of spending in a particular category

see where additional expenses for certain months come from

* *

cautious spender

carry over any budget deficits incurred each month to the following month

remember to repay it

* *

SoC student who has to pay school fees regularly

track how much I’ll have to pay and when I have to pay it by

avoid making late payments

* *

ambitious user

set goals for my cumulative income

work towards my dream home/car/goal

* *

careless user

undo a mistake

not worry even if I accidentally make an error

* *

careful spender

set different currencies for different accounts

see my expenditure during a holiday in the local currency

* *

lazy person

import expenses from csv exports from internet banking

transfer my records across software

* *

supremely lazy user

have the program detect recurring transactions and suggest them to me

not waste time adding them manually

* *

paranoid user

have the program show me all possible commands and how to use them

know exactly what I am doing

* *

user who prefers visuals

see a chart of my budget spending across past months

see how much I have been overspending/underspending

* *

busy user

move and delete multiple transactions at the same time

be more efficient

* *

lazy user

have the program autofill my command as I am typing it

enter my transactions more quickly

* *

user who cannot control spending

set a budget for different purposes

control my spending

* *

lazy user

have the app to have predictive commands based on what transactions I commonly include

spend less time typing in my expenses

* *

fast typer

type out multiple commands all at once

type the next command without having to pause

* *

person who occasionally goes overseas

assign an exchange rate to each foreign currency transaction

get reports on my total expenditure in my home currency

* *

busy user

see my overall budget surplus/deficit at a glance

know straight away when I’m below or above my budget for that month

* *

meticulous user

see how much I need to budget every month to reach a savings goal based on what the goal is and its deadline

plan my budget well

*

unmotivated person

be rewarded for entering my expenses/income daily

be motivated to do so and eventually turn it into a habit

*

expense planner

record down possible future expenses

keep track of what I planned to spend on

*

forgetful user

have the app set reminders

keep track of my spending everyday

Appendix C: Use Cases

(For all use cases below, the System is Budget Buddy and the Actor is the user, unless specified otherwise)

Use case: Delete transaction

MSS

  1. User requests to list transactions.

  2. Budget Buddy shows a list of transactions for the current account.

  3. User requests to delete a specific transaction in the list.

  4. Budget Buddy deletes the transaction.

    Use case ends.

Extensions

  • 2a. The list is empty.

    Use case ends.

  • 3a. The given index is invalid.

    • 3a1. Budget Buddy shows an error message.

      Use case resumes at step 2.

C.1. Use case: Add loan

MSS

  1. User requests to add a loan out.

  2. Budget Buddy adds the loan for the given person.

    Use case ends.

Extensions

  • 1a. The given loan out value is not positive.

    • 1a1. Budget Buddy shows an error message.

      Use case resumes at step 1.

C.2. Use case: Edit loan

MSS

  1. User requests to list loans.

  2. Budget Buddy shows a list of loans.

  3. User requests to edit the description of a loan in the list.

  4. Budget Buddy edits the description of the specified loan.

    Use case ends.

Extensions

  • 2a. The list is empty.

    Use case ends.

  • 2a. The given loan index is invalid.

    • 2a1. Budget Buddy shows an error message.

      Use case resumes at step 2.

C.3. Use case: Add account

MSS

  1. User requests to add a new account.

  2. Budget Buddy creates the new account.

    Use case ends.

Extensions

  • 1a. No account name was provided.

    • 1a1. Budget Buddy shows an error message.

      Use case resumes at step 1.

C.4. Use case: Add rule

MSS

  1. User requests to add a new rule.

  2. Budget Buddy creates the new rule.

    Use case ends.

Extensions

  • 1a. No predicate was provided.

    • 1a1. Budget Buddy shows an error message.

      Use case resumes at step 1.

  • 1b. No action was provided.

    • 1b1. Budget Buddy shows an error message.

      Use case resumes at step 1.

  • 1c. Predicate is an expression but not in the form of <attribute> <operator> <value>.

    • 1c1. Budget Buddy shows an error message.

      Use case resumes at step 1.

Appendix D: Non Functional Requirements

  1. Should work on any mainstream OS as long as it has Java 11 or above installed.

  2. Should be able to hold up to 1000000 transactions without a noticeable sluggishness in performance for typical usage.

  3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.

Appendix E: Glossary

Mainstream OS

Windows, Linux, Unix, OS-X

Loan Out

A loan where the user has lent money to another person.

Loan In

A loan where the user has borrowed money from another person.

Appendix F: Instructions for Manual Testing

Given below are instructions to test the app manually.

These instructions only provide a starting point for testers to work on.

F.1. Launch and Shutdown

  1. Initial launch

    1. Download the jar file and copy into an empty folder

    2. Double-click the jar file
      Expected: Shows the GUI starting in the transaction panel. The window size may not be optimum.

  2. Saving window preferences

    1. Resize the window to an optimum size. Move the window to a different location. Close the window.

    2. Re-launch the app by double-clicking the jar file.
      Expected: The most recent window size and location is retained.

F.2. Accounts

F.2.1. Adding an Account

  1. Adding an account when no duplicate account exists in the list

    1. Prerequisites: None

    2. Test case: account add n/food
      Expected: An account with name "food" is added to the current list of accounts.

F.2.2. Deleting an Account

  1. Deleting an account while all accounts are listed

    1. Prerequisites: List all persons using the account list command. More than one account in the list.

    2. Test case: account delete 1
      Expected: First account is deleted from the list. Details of the deleted account shown in the status message.

    3. Test case: account delete 0
      Expected: No account is deleted. Error details shown in the status message.

    4. Other incorrect delete commands to try: account delete, account delete x (where x is larger than the list size)
      Expected: Similar to previous.

F.2.3. Editing an Account

  1. Editing one or all specified fields of an existing account from the account list

    1. Prerequisites: List all accounts using the account list command. At least one account in the list.

    2. Test case: account edit 1 n/food
      Expected: The name field of the first account is changed to "food".

F.2.4. Finding Account(s)

  1. Finding account(s) that contain specified keyword in their names

    1. Prerequisites: None.

    2. Test case: account find trip
      Expected: Account(s) that contain the keyword "trip" in their names will be listed.

F.2.5. Viewing the Report of an Account

  1. Viewing the details of an existing account in the account list

    1. Prerequisites: None.

    2. Test case: account report 2
      Expected: The details of the account will be displayed in the commandBox textfield.

F.2.6. Viewing the Report of all Accounts

  1. Viewing the reports of all accounts to have an overview of the user’s financial status.

    1. Prerequisite: None.

    2. Test case: account overview
      Expected: Successful message will be displayed, with an html file generated containing the overview in the exports folder.

F.3. Loans

F.3.1. Adding a Loan

  1. Adding a loan when no duplicate loan exists in the list

    1. Prerequisites: None

    2. Test case: loan out x/4.20 p/John d/For dinner
      Expected: A loan is added to the list at the appropriate position based on the current sorting order. The date of the loan should be the current system date.

F.3.2. Listing Loans

  1. Listing loans in the main display panel with optional filtering/sorting

    1. Prerequisites: None

    2. Test case: loan list Expected: All loans are listed.

    3. Test case: loan list out p/John s/p Expected: Only loans out involving the person "John" are listed. The list is also sorted by person in alphabetical order.

F.3.3. Editing a Loan

  1. Editing an existing loan while all loans are listed

    1. Prerequisites: List all loans using the loan list command. At least one loan in the list.

    2. Test case: loan edit 1 x/500 Expected: First loan’s amount is changed to $500 and it is sorted into the appropriate position based on the current sorting order.

F.3.4. Marking a Loan as Paid

  1. Marking an existing loan as paid while all loans are listed

    1. Prerequisites: List all loans using the loan list command. At least one loan in the list that is unpaid.

    2. Test case: loan paid 1 Expected: First loan is marked as paid. A "tick" icon should appear to the left of the loan’s index.

    3. Test case: loan paid p/John Expected: All loans pertaining to the person "John" are marked as paid.

F.3.5. Marking a Loan as Unpaid

  1. Marking an existing loan as unpaid while all loans are listed

    1. Prerequisites: List all loans using the loan list command. At least one loan in the list that is paid.

    2. Test case: loan unpaid 1 Expected: First loan is marked as unpaid. The "tick" icon should disappear from the left of the loan’s index.

    3. Test case: loan unpaid p/John Expected: All loans pertaining to the person "John" are marked as unpaid.

F.3.6. Deleting a Loan

  1. Deleting an existing loan while all loans are listed

    1. Prerequisites: List all loans using the loan list command. At least one loan in the list.

    2. Test case: loan delete 1 Expected: Deletes the first loan from the list.

    3. Test case: loan delete p/John Expected: All loans pertaining to the person "John" are deleted.

F.3.7. Splitting a Payment into Loans

  1. Splitting a group payment into loans/debts

    1. Prerequisites: None

    2. Test case: loan split p/John x/0 p/Mary x/10 p/Peter x/90 Expected: Displays a list saying John owes Peter $3.33 and Mary owes Peter $23.33.

    3. Test case: loan split p/John x/0 max/10 p/Mary x/10 p/Peter x/30 p/Bruce x/90 p/Thomas x/50 me/John d/Big lunch Expected: Displays a list saying Mary owes Bruce $32.50, Peter owes Bruce $12.50, and You owe Bruce and Thomas $2.50 and $7.50 respectively. Two new loans out to Bruce and Thomas should be added to the normal loan list as well (press Ctrl + D to switch between the two lists).

F.4. Rules

F.4.1. Listing rules

  1. Listing rules while another panel is shown

    1. Prerequisites: Display another panel using account list, loan list, txn list, or script list.

    2. Test case: rule list
      Expected: Current panel switches over to the rule panel and displays rule list.

  2. Listing rules while on rule panel.

    1. Prerequisites: Display the rule panel with with rule list.

    2. Test case: rule list
      Expected: Current panel remains on the rule panel, displays rule list.

F.4.2. Adding a rule

  1. Adding a rule while on a different panel

    1. Prerequisites: Display another panel using account list, loan list, txn list, or script list.

    2. Test case: rule add p/desc = m a/set_desc Monthly Allowance+ Expected: Rule is created, panel switches to rule panel.

    3. Test case: rule add p/it is monday a/prep_desc [Monday] Expected: No rule created. Error details shown in the status message.

    4. Other incorrect rule add commands to try: rule add a/set_in, rule add p/outamt > 100
      Expected: Similar to previous.

  2. Adding a rule while on the same panel

    1. Same test cases as above: e.g. rule add p/desc = m a/set_desc Monthly Allowance Expected: Similar output as above, app remains on the rule panel.

F.4.3. Editing a rule

  1. Editing a rule on the same panel

    1. Prerequisite: At least 1 rule in the list.

    2. Test case: rule edit 1 p/date < 11/11/2019 Expected: Rule is edited and edited rule is shown on the rule list.

    3. Test case: rule edit 0 p/date < 11/11/2019 Expected: No rule edited. Error details shown in the status message.

    4. Other incorrect rule edit commands to try: rule edit 1, rule edit 1 p/outamt contains letters, rule edit 1 a/ Expected: Similar to previous.

  2. Editing a rule on a different panel

    1. Prerequisites: Display another panel using account list, loan list, txn list, or script list. At least 1 rule in the list.

    2. Same test cases as above: e.g. rule edit 1 p/date < 11/11/2019 Expected: Similar output as above, app switches over to the rule panel if rule is successfully edited.

F.4.4. Deleting a rule

  1. Deleting a rule

    1. Prerequisites: More than 1 rule in the list

    2. Test case: rule delete 1 Expected: Rule is deleted. If not on the rule panel, app switches to rule panel.

    3. Test case: rule delete 0 Expected: No rule deleted. Error details shown in the status message.

    4. Other incorrect delete commands to try: rule delete, rule delete x (where x is larger than the list size)
      Expected: Similar to previous.

F.4.5. Executing rules

  1. Rules are executed on newly added transaction

    1. Prerequisites:

      1. Add rule: rule add p/desc = m a/set_desc Monthly Allowance

      2. Add rule: rule add p/inamt > 100 a/set_cat Large Savings

    2. Test case: txn dn/in x/10 d/m Expected: Transaction is added. Transaction description seen in the transaction panel is "Monthly Allowance".

    3. Test case: txn dn/in x/100 d/m Expected: Similar to previous. Transaction also has a category "Large Savings"

    4. Test case: txn dn/out x/100 d/Buffet Expected: Transaction is added. Rules do not apply to transaction. No change to description or categories.

F.5. Scripts

F.5.1. Evaluating an expression

  1. Evaluating an expression

    1. Prerequisites: None

    2. Test case: script eval 123 + 456 Expected: "579" is shown in the command result box.

    3. Test case: script eval showAlert("Hello", "World") Expected: An alert box with "Hello" as the title and "World" as the message is shown. "Script succeeded with no result." is shown in the command result box.

F.5.2. Listing scripts

  1. Listing scripts

    1. Prerequisites: A tab other than the script tab is active.

    2. Test case: script list Expected: The script tab is activated.

F.5.3. Adding a script

  1. Adding a script from the command line

    1. Prerequisites: No script named hello-world exists.

    2. Test case: script add hello-world s/"Hello world" Expected: The script tab is shown. "New script hello-world added." is shown in the command result box. A script hello-world with description "No description provided" is shown in the list of scripts.

  2. Adding a script from file

    1. Prerequisites: No script named hello-world exists.

    2. Test case: script add hello-world Expected: The script tab is shown. A file picker dialog appears. Once any file is selected, "New script hello-world added." is shown in the command result box. A script hello-world with description "No description provided" is shown in the list of scripts.

F.5.4. Running a script

  1. Running a script

    1. Prerequisites: script add hello-world s/"Hello world"

    2. Test case: script run hello-world Expected: "Hello world" is shown in the command result box.

F.5.5. Deleting a script

  1. Deleting a script

    1. Prerequisites: script add hello-world s/"Hello world"

    2. Test case: script delete hello-world Expected: The script tab is shown. "Script hello-world deleted." is shown in the command result box. No script with the name hello-world is listed in the list of scripts.

F.5.6. Resetting the scripting environment

  1. Resetting the scripting environment

    1. Prerequisites: script eval var a = 1

    2. Test case: script reset, then script eval a Expected: "Exception while evaluating script: javax.script.ScriptException: ReferenceError: "a" is not defined in <eval> at line number 1" is shown in the command result box.