By: CS2103T W13-4
1. Setting up
Refer to the guide here.
2. Design
2.1. Architecture
The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.
-
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.
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.
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
.
account delete 1
commandThe sections below give more details of each component.
2.2. 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
API :
Logic.java
-
Logic
uses theCommandLineParser
class to parse the user command. -
This results in a
Command
object which is executed by theLogicManager
. -
The command execution can affect the
Model
(e.g. adding a loan). -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
. -
The
CommandResult
is used to display feedback to the user and update the current active tab based on itsCommandCategory
. -
For certain commands (e.g.
help
,script add
) theCommandResult
will have aCommandContinuation
that will perform further actions or return a newCommandResult
to evaluate again.
Given below is the Sequence Diagram for interactions within the Logic
component for the execute("account delete 1")
API call.
account delete 1
Command2.4. 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
ObservableList
s that can be 'observed' e.g. the differentDisplayPanel
s 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
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:
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 offilteredAccounts
. -
AccountsManager#getFilteredAccountList()
— Gets the list offilteredAccounts
after applying itspredicate
. -
AccountsManager#resetFilteredAccountList()
— ResetfilteredAccounts
so that all accounts present inaccounts
exist infilteredAccounts
. -
AccountsManager#getActiveTransactionList()
— Gets theactiveTransactionList
of the active account. -
AccountsManager#getAccounts()
— Gets the list ofaccounts
. -
AccountsManager#size()
— Gets the size of theaccounts
list. -
AccountsManager#addAccount(Account toAdd)
— Adds theAccount toAdd
toaccounts
. -
AccountsManager#deleteAccount(Account toDelete)
— Deletes the account atIndex toDelete
fromaccounts
. -
AccountsManager#editAccount(Index toEdit, Account editedAccount)
— Edits the account atIndex Edit
to matchAccount 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 callAccountsManager#resetFilteredAccountList
to update thefilteredAccounts
, so that all accounts present inaccounts
will be present infilteredAccounts
, thenAccountsManager#editAccount
to edit the account, finallyAccountsManager#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:
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
:
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 withfilteredAccounts
asfilteredAccounts
stores all accounts. After each command,AccountsManager#resetFilteredAccountList
is called to reset the predicate to be true, so thatfilteredAccounts
matchesaccounts
.-
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 bothfilteredAccounts
andaccounts
, 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.
The following user commands, if executed successfully, can change the active account:
-
AccountAddCommand
— The addedAccount
becomes the active account. -
AccountDeleteCommand
— If the activeAccount
is deleted, a new activeAccount
will be selected. -
AccountEditCommand
— The editedAccount
becomes the active account. -
AccountFindCommand
— The firstAccount
in the list is set as the activeAccount
. -
AccountListCommand
— The firstAccount
in the list is set as the activeAccount
. -
AccountReportCommand
— The targetAccount
is set as the new activeAccount
. -
AccountSwitchCommand
— The targetAccount
is set as the new activeAccount
. -
TransactionAddCommand
— The targetAccount
is set as the new activeAccount
. -
TransactionEditCommand
— The targetAccount
is set as the new activeAccount
.
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 toisActiveBooleanProperty
andbalanceLongProperty
.-
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
.
Each Transaction
object must have:
-
direction: Direction
— The direction of the transaction (eitherIN
for money inflow orOUT
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>
— ASet
ofCategory
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 anObservableList
of the filtered transactions. -
AccountsManager#updateFilteredTransactionList(Predicate<Transaction> predicate)
— Updates thePredicate<Transaction>
of theFilteredList
to filter by the givenPredicate<Transaction>
-
AccountsManager#resetFilteredTransactionList()
— Resets and removes any filter on theFilteredList
. -
AccountsManager#updateSortedTransactionList(Comparator<Transaction> comparator)
— Updates theComparator<Transaction>
of theSortedList
to sort by the givenComparator<Transaction>
-
AccountsManager#resetSortedTransactionList()
— Resets theComparator
on theFilteredList
to the default comparator.
When Transaction
objects are added/edited/deleted, the following implemented methods are called from Account
:
-
Account#addTransaction(Transaction toAdd)
— Adds aTransaction
to thetransactionList
. -
Account#updateTransaction(Index txnIndex, Transaction editedTxn)
— Sets aTransaction
to thetransactionList
at the specifiedIndex
. -
Account#deleteTransaction(Transaction toDelete)
— Deletes aTransaction
from thetransactionList
.
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 toAccount
; any commands that create/edit/deleteTransaction
objects are called via the activeAccount
.-
Pros: Reduced coupling, which makes testing/maintenance easier.
-
Cons: More methods are required to ensure the logic of
Transaction
commands and the current activeAccount
, instead of calling getAccount() on aTransaction
.
-
-
Alternative 2:
Transaction
holds a reference toAccount
; commands that create/edit/deleteTransaction
objects directly reach theAccount
through theTransaction
.-
Pros: It would result in shorter code by avoiding having to go through
AccountsManager
to find the respectiveAccount
. -
Cons: This would result in a circular dependency between
Transaction
andAccount
, 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
):
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.
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:
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.
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 (eitherIN
orOUT
). -
status:Status
— The status of the loan (eitherPAID
orUNPAID
). -
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 offilteredLoans
topredicate
. -
LoansManager#sortLoans(Comparator<Loan> sorter)
— SortsinternalList
using the givensorter
. -
LoansManager#getFilteredLoans()
— GetsfilteredLoans
, representing the loans ininternalList
after filtering. -
LoansManager#addLoan(Loan toAdd)
— Adds theLoan toAdd
tointernalList
. -
LoansManager#editLoan(Index toEdit, Loan editedLoan)
— Replaces the loan atIndex toEdit
withLoan editedLoan
. -
LoansManager#updateStatus(Index toUpdate, Loan updatedLoan)
— Replace the loan atIndex toUpdate
withLoan updatedLoan
. -
LoansManager#deleteLoan(Index toDelete)
— Deletes the loan atIndex toDelete
frominternalList
.
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:
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:
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 thedebtor
owes money to, mapped to the amount of money owed. Each entry in theHashMap
represents a creditor, with their name as thePerson
key and the amount owed as the correspondingAmount
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:
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:
-
The
defaultSharePerPerson
is calculated. In this scenario, $150 should be split among the 4 persons. However, asBen
has a max share of $10, the other 3 persons must divide $140 among themselves, resulting in adefaultSharePerPerson
of $46.66. -
Each person in
List<Person>
is given abalance
, calculated using the amount they paid initially (fromList<Amount>
) minus either thedefaultSharePerPerson
or their max share if present. -
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.
-
For each sub-group, if the sum of their balances is zero, then the following steps are performed:
-
Take the persons with the smallest and biggest balances: the
debtor
andcreditor
respectively. -
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
andamountTransferred
is stored in aList<DebtorCreditorAmount>
. -
Repeat until the sub-group contains less than two persons.
-
-
After every sub-group has been processed,
List<DebtorCreditorAmount>
is used to create the finalList<Debtor>
stored inLoansManager
.
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
storesinternalList
; eachLoan
references aPerson
-
Pros: Easy to implement and understand.
-
Cons: Takes a longer time to get all the loans belonging to a person.
-
-
Alternative 2:
LoansManager
storespersons
list; eachPerson
storesLoanList
; eachLoan
referencesPerson
-
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.
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.
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.
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.
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
andActionExpression
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
andActionScript
contain only aScriptName
, 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 aScript
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 thePredicateScript
orActionScript
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:
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 Testable
s for testing on the
transaction, as well as the Performable
s for performing the action.
The following activity diagram shows in greater detail 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.
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
andTestableExpression
, 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
andLogic
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.
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 ScriptEnvironmentInitialiser
s. 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 ScriptEnvironmentInitialiser
s 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
.
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
ScriptEnvironmentInitialiser
s, 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 FunctionalInterface
s. 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 inScriptBindingInterfaces
.
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 thecall
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.
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 usingLogsCenter.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
-
User requests to list transactions.
-
Budget Buddy shows a list of transactions for the current account.
-
User requests to delete a specific transaction in the list.
-
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
-
User requests to add a loan out.
-
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
-
User requests to list loans.
-
Budget Buddy shows a list of loans.
-
User requests to edit the description of a loan in the list.
-
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
-
User requests to add a new account.
-
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
-
User requests to add a new rule.
-
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
-
Should work on any mainstream OS as long as it has Java
11
or above installed. -
Should be able to hold up to 1000000 transactions without a noticeable sluggishness in performance for typical usage.
-
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 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
-
Initial launch
-
Download the jar file and copy into an empty folder
-
Double-click the jar file
Expected: Shows the GUI starting in the transaction panel. The window size may not be optimum.
-
-
Saving window preferences
-
Resize the window to an optimum size. Move the window to a different location. Close the window.
-
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
-
Adding an account when no duplicate account exists in the list
-
Prerequisites: None
-
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
-
Deleting an account while all accounts are listed
-
Prerequisites: List all persons using the
account list
command. More than one account in the list. -
Test case:
account delete 1
Expected: First account is deleted from the list. Details of the deleted account shown in the status message. -
Test case:
account delete 0
Expected: No account is deleted. Error details shown in the status message. -
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
-
Editing one or all specified fields of an existing account from the account list
-
Prerequisites: List all accounts using the
account list
command. At least one account in the list. -
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)
-
Finding account(s) that contain specified keyword in their names
-
Prerequisites: None.
-
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
-
Viewing the details of an existing account in the account list
-
Prerequisites: None.
-
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
-
Viewing the reports of all accounts to have an overview of the user’s financial status.
-
Prerequisite: None.
-
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
-
Adding a loan when no duplicate loan exists in the list
-
Prerequisites: None
-
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
-
Listing loans in the main display panel with optional filtering/sorting
-
Prerequisites: None
-
Test case:
loan list
Expected: All loans are listed. -
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
-
Editing an existing loan while all loans are listed
-
Prerequisites: List all loans using the
loan list
command. At least one loan in the list. -
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
-
Marking an existing loan as paid while all loans are listed
-
Prerequisites: List all loans using the
loan list
command. At least one loan in the list that is unpaid. -
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. -
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
-
Marking an existing loan as unpaid while all loans are listed
-
Prerequisites: List all loans using the
loan list
command. At least one loan in the list that is paid. -
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. -
Test case:
loan unpaid p/John
Expected: All loans pertaining to the person "John" are marked as unpaid.
-
F.3.6. Deleting a Loan
-
Deleting an existing loan while all loans are listed
-
Prerequisites: List all loans using the
loan list
command. At least one loan in the list. -
Test case:
loan delete 1
Expected: Deletes the first loan from the list. -
Test case:
loan delete p/John
Expected: All loans pertaining to the person "John" are deleted.
-
F.3.7. Splitting a Payment into Loans
-
Splitting a group payment into loans/debts
-
Prerequisites: None
-
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. -
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 (pressCtrl + D
to switch between the two lists).
-
F.4. Rules
F.4.1. Listing rules
-
Listing rules while another panel is shown
-
Prerequisites: Display another panel using
account list
,loan list
,txn list
, orscript list
. -
Test case:
rule list
Expected: Current panel switches over to the rule panel and displays rule list.
-
-
Listing rules while on rule panel.
-
Prerequisites: Display the rule panel with with
rule list
. -
Test case:
rule list
Expected: Current panel remains on the rule panel, displays rule list.
-
F.4.2. Adding a rule
-
Adding a rule while on a different panel
-
Prerequisites: Display another panel using
account list
,loan list
,txn list
, orscript list
. -
Test case:
rule add p/desc = m a/set_desc Monthly Allowance
+ Expected: Rule is created, panel switches to rule panel. -
Test case:
rule add p/it is monday a/prep_desc [Monday]
Expected: No rule created. Error details shown in the status message. -
Other incorrect rule add commands to try:
rule add a/set_in
,rule add p/outamt > 100
Expected: Similar to previous.
-
-
Adding a rule while on the same panel
-
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
-
Editing a rule on the same panel
-
Prerequisite: At least 1 rule in the list.
-
Test case:
rule edit 1 p/date < 11/11/2019
Expected: Rule is edited and edited rule is shown on the rule list. -
Test case:
rule edit 0 p/date < 11/11/2019
Expected: No rule edited. Error details shown in the status message. -
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.
-
-
Editing a rule on a different panel
-
Prerequisites: Display another panel using
account list
,loan list
,txn list
, orscript list
. At least 1 rule in the list. -
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
-
Deleting a rule
-
Prerequisites: More than 1 rule in the list
-
Test case:
rule delete 1
Expected: Rule is deleted. If not on the rule panel, app switches to rule panel. -
Test case:
rule delete 0
Expected: No rule deleted. Error details shown in the status message. -
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
-
Rules are executed on newly added transaction
-
Prerequisites:
-
Add rule:
rule add p/desc = m a/set_desc Monthly Allowance
-
Add rule:
rule add p/inamt > 100 a/set_cat Large Savings
-
-
Test case:
txn dn/in x/10 d/m
Expected: Transaction is added. Transaction description seen in the transaction panel is "Monthly Allowance". -
Test case:
txn dn/in x/100 d/m
Expected: Similar to previous. Transaction also has a category "Large Savings" -
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
-
Evaluating an expression
-
Prerequisites: None
-
Test case:
script eval 123 + 456
Expected: "579" is shown in the command result box. -
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
-
Listing scripts
-
Prerequisites: A tab other than the script tab is active.
-
Test case:
script list
Expected: The script tab is activated.
-
F.5.3. Adding a script
-
Adding a script from the command line
-
Prerequisites: No script named
hello-world
exists. -
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 scripthello-world
with description "No description provided" is shown in the list of scripts.
-
-
Adding a script from file
-
Prerequisites: No script named
hello-world
exists. -
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 scripthello-world
with description "No description provided" is shown in the list of scripts.
-
F.5.4. Running a script
-
Running a script
-
Prerequisites:
script add hello-world s/"Hello world"
-
Test case:
script run hello-world
Expected: "Hello world" is shown in the command result box.
-
F.5.5. Deleting a script
-
Deleting a script
-
Prerequisites:
script add hello-world s/"Hello world"
-
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 namehello-world
is listed in the list of scripts.
-
F.5.6. Resetting the scripting environment
-
Resetting the scripting environment
-
Prerequisites:
script eval var a = 1
-
Test case:
script reset
, thenscript 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.
-