diff --git a/README.md b/README.md index f82e2494b7..df5f019a1f 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,16 @@ -# Duke project template - -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. - -## Setting up in Intellij - -Prerequisites: JDK 11 (use the exact version), update Intellij to the most recent version. - -1. **Ensure Intellij JDK 11 is defined as an SDK**, as described [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk) -- this step is not needed if you have used JDK 11 in a previous Intellij project. -1. **Import the project _as a Gradle project_**, as described [here](https://se-education.org/guides/tutorials/intellijImportGradleProject.html). -1. **Verify the set up**: After the importing is complete, locate the `src/main/java/seedu/duke/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: - ``` - > Task :compileJava - > Task :processResources NO-SOURCE - > Task :classes - - > Task :Duke.main() - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - - What is your name? - ``` - Type some word and press enter to let the execution proceed to the end. - -## Build automation using Gradle - -* This project uses Gradle for build automation and dependency management. It includes a basic build script as well (i.e. the `build.gradle` file). -* If you are new to Gradle, refer to the [Gradle Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/gradle.html). - -## Testing - -### I/O redirection tests - -* To run _I/O redirection_ tests (aka _Text UI tests_), navigate to the `text-ui-test` and run the `runtest(.bat/.sh)` script. - -### JUnit tests - -* A skeleton JUnit test (`src/test/java/seedu/duke/DukeTest.java`) is provided with this project template. -* If you are new to JUnit, refer to the [JUnit Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/junit.html). - -## Checkstyle - -* A sample CheckStyle rule configuration is provided in this project. -* If you are new to Checkstyle, refer to the [Checkstyle Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/checkstyle.html). - -## CI using GitHub Actions - -The project uses [GitHub actions](https://github.com/features/actions) for CI. When you push a commit to this repo or PR against it, GitHub actions will run automatically to build and verify the code as updated by the commit/PR. - -## Documentation - -`/docs` folder contains a skeleton version of the project documentation. - -Steps for publishing documentation to the public: -1. If you are using this project template for an individual project, go your fork on GitHub.
- If you are using this project template for a team project, go to the team fork on GitHub. -1. Click on the `settings` tab. -1. Scroll down to the `GitHub Pages` section. -1. Set the `source` as `master branch /docs folder`. -1. Optionally, use the `choose a theme` button to choose a theme for your documentation. +# BudgetBuddy +BudgetBuddy is a product for users who wish to handle and track any current/future expenses on a singular platform. +BudgetBuddy provides a faster and more efficient way to track and calculate expenses and provides the ability +to deal with finances on a singular platform with ease as long as you can type fast. + +Useful Links : +* [User Guide](docs/UserGuide.md) +* [Developer Guide](docs/DeveloperGuide.md) +* [About Us](docs/AboutUs.md) + +Contributors : +* [Chan Jun Rong](docs/team/itsmejr257.md) +* [Soh Wei Jie](docs/team/sweijie24.md) +* [Jasra Zainab](docs/team/jasraa.md) +* [Dheekshitha](docs/team/dheekshitha2.md) +* [Zhang Yangda](docs/team/yyangdaa.md) diff --git a/build.gradle b/build.gradle index ea82051fab..a69e9b0954 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.budgetbuddy.BudgetBuddy") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("budgetbuddy") archiveClassifier.set("") } @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/data/BudgetFile.txt b/data/BudgetFile.txt new file mode 100644 index 0000000000..c3a493ad1f --- /dev/null +++ b/data/BudgetFile.txt @@ -0,0 +1,3 @@ +Transport|100.00 +Groceries|1000.00 +Housing|100.00 diff --git a/data/DefaultCurrency.txt b/data/DefaultCurrency.txt new file mode 100644 index 0000000000..822166bb78 --- /dev/null +++ b/data/DefaultCurrency.txt @@ -0,0 +1 @@ +Default Currency: SGD \ No newline at end of file diff --git a/data/ExpenseFile.txt b/data/ExpenseFile.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/RecurringExpensesFile.txt b/data/RecurringExpensesFile.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/SavingsFile.txt b/data/SavingsFile.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/SplitExpensesFile.txt b/data/SplitExpensesFile.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..2897df6a28 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,10 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio | +|-----------------------------------------------------|:-------------:|:-----------------------------------------:|:---------------------------------:| +| ![](https://via.placeholder.com/100.png?text=Photo) | Zhang Yangda | [Github](https://github.com/yyangdaa) | [Portfolio](team/yyangdaa.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Dheekshitha | [Github](https://github.com/Dheekshitha2) | [Portfolio](team/dheekshitha2.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Jasra Zainab | [Github](https://github.com/jasraa) | [Portfolio](team/jasraa.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Chan Jun Rong | [Github](https://github.com/itsmejr257) | [Portfolio](team/itsmejr257.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Soh Wei Jie | [Github](https://github.com/sweijie24) | [Portfolio](team/sweijie24.md) | + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..38968ba3c3 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,1851 @@ -# Developer Guide +# Developer Guide for Budget Buddy + +## Table of Contents +[1. Introduction](#1-introduction)
+[2. Setting up](#2-setup-guide)
+  [2.1 Prerequisites](#21-prerequisites)
+[3. Design](#3design)
+  [3.1 Architecture](#31-architecture)
+  [3.2 Parser Class](#32-parser-class)
+  [3.3 Ui Class](#33-ui-class)
+  [3.4 CommandCreator](#34-commandcreator-class)
+  [3.5 CommandClass](#35-command-class)
+  [3.6 Storage Class](#36-storage-class)
+  [3.7 Commons](#37-commons)
+    [3.7.1 Transaction](#371-transaction)
+    [3.7.2 Expense](#372-expense)
+    [3.7.3 ExpenseList](#373-expenselist)
+    [3.7.4 Saving](#374-saving)
+    [3.7.5 SavingList](#375-savinglist)
+    [3.7.6 RecurringExpenseList](#376-recurringexpenselist)
+    [3.7.7 RecurringExpensesList](#377-recurringexpenselists)
+    [3.7.8 DefaultCurrency](#378-defaultcurrency)
+    [3.7.9 CurrencyConverter](#379-currencyconverter)
+[4. Implementation](#4-implementation)
+  [4.1 Menu Feature](#41-menu-feature)
+  [4.2 Add Expenses Feature](#42-add-expense-feature)
+  [4.3 Add Savings Feature](#43-add-savings-feature)
+  [4.4 Add Split Expenses Feature](#44-add-shared-bill-feature)
+  [4.5 Edit Savings Feature](#45-edit-savings-feature)
+  [4.6 Edit Expenses Feature](#46-edit-expense-feature)
+  [4.7 Reduce Savings Feature](#47-reduce-savings-feature)
+  [4.8 Delete Expenses Feature](#48-delete-expenses-feature)
+  [4.9 List Savings Feature](#49-listing-feature-list-savings)
+  [4.10 List Expenses Feature](#410-listing-feature-list-expenses)
+  [4.11 Check Splitted Expenses Feature](#411-check-split-bill-feature)
+  [4.12 Settle Splitted Expenses Feature](#412-settle-bill-feature)
+  [4.13 Find Expenses Feature](#413-find-feature)
+  [4.14 Recurring Expenses Feature](#414-recurring-expenses-feature)
+  [4.15 Currency Converter Feature](#415-currency-converter-feature)
+  [4.16 Setting Budget Feature](#416-setting-budget-feature)
+  [4.17 Get Graphical Insights for expenses](#417-get-expense-insights-feature)
+  [4.18 Get Graphical Insights for savings](#418-get-savings-insights-feature)
+[5. Documentation](#5-documentation)
+[6. Testing](#6-testing)
+[Appendix A: Product Scope](#appendix-a-product-scope)
+[Appendix B: User Stories](#appendix-b-user-stories)
+[Appendix C: Use Cases](#appendix-c-use-cases)
+[Appendix D: Non-Functional Requirements](#appendix-d-non-functional-requirements)
+[Appendix E: Glossary](#appendix-e-glossary)
+[Appendix F: Instructions for Manual Testing](#appendix-f-instructions-for-manual-testing)
+ + ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Diagrams have been created on [Draw.io](https://draw.io/). ## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +## 1. Introduction +Welcome to the Developer Guide for BudgetBuddy! This guide has been created to help you current and future +developers of BudgetBuddy understand how BudgetBuddy works and aid you in easily adding new features and +fixing bugs. In this guide, it will go over the main parts of the app, how they work together, +and why we made them that way. +## 2. Setup Guide +This section describes how to set up the coding environment, along with the tools needed to work on BudgetBuddy -## Product scope -### Target user profile +### 2.1. Prerequisites +1. JDK 11 +2. IntelliJ IDEA + +## 3.Design + +### 3.1 Architecture +The following diagram provides a rough overview of how BudgetBuddy is built + +![Diagram of overview of BudgetBuddy](diagrams/diagram_Introduction.jpg) + +`BudgetBuddy` is the main class of the application and directly interacts with the user. `BudgetBuddy` +passes along the input into the `Parser`. The `Parser` creates a `CommandCreator` object depending on the user's input +. The `CommandCreator` object then creates the `Command` object. +This `Command` object will be executed in `BudgetBuddy`. The `Command` object +utilizes methods and the classes present in `Commons`, which will be explained in more +detail in the following sections. + +#### 3.2 Parser Class +The main functionality of the Parser Class is to determine the type of `CommandCreator` object to create. Using +Boolean Functions, the Parser Class determines this by what the user input starts with. +After determining the type of `CommandCreator` object, the Parser initializes the `CommandCreator` object +with all its required parameters. + +Here are some examples : + +| Boolean Method | Checks if input starts with | Feature Requires | Creates | +|-----------------------|-----------------------------|--------------------|--------------------------------------------| +| isAddExpenseCommand() | add expense | input, ExpenseList | AddExpenseCommandCreator(input, expenses) | +| isEditSavingCommand() | edit expense | input, SavingList | EditSavingsCommandCreator(input, savings) | + +#### 3.3 Ui Class +The Ui Class is used to print certain elements to the CLI. In particular, it consists of the Welcome Message, +Goodbye Message, Divider Lines and all the corresponding commands' command format. + +#### 3.4 CommandCreator Class +The CommandCreator class has multiple subclasses, which corresponds to a specific function of the application. +Within the CommandCreator classes, it handles making sense of the user input, obtaining the relevant parameters, and finally +creating the `Command` class to be executed. + +The superclass `CommandCreator` is an abstract class which is never instantiated. Where its `createCommand()` +method is overridden by its subclasses. + +The association between the `Command` and `CommandCreator` can be seen in their names. E.g. `MenuCommandCreator`, would +create a `MenuCommand` class when its createCommand() method is called. Similarly, `FindExpensesCommandCreator` would +create a `FindCommand` class when its createCommand() method is called. + +For clarity, unlike the `BudgetBuddy` and `Parser` class, where only **one** instance of them is used for the entire +application, a **new** `CommandCreator` subclass is instantiated every time a user provides an input. Hence, +a created `CommandCreator` will always be specific to, and only handle `one` user input. This will be further illustrated in the +UML Sequence Diagram provided in section `3.4 Command Class` + +#### 3.5 Command Class +The Command class, similar to the CommandCreator class, contains multiple subclasses, all corresponding to a specific +function/feature of the application. Stated in section`3.4 CommandCreator Class` +, each subclass of the `Command` Object is created by its associated `CommandCreator` subclass. + +The superclass `Command` is an abstract class which is never instantiated. Where its `execute()` method is overridden +by its subclasses. What each Command subclass does when its `execute()` method is called would be discussed in +more detail in the Implementation section. + +For clarity, similar to the `CommandCreator` class, a **new** `Command` subclass is instantiated every time a +user provides an input. As such, a created `Command` will always be specific to, and only handle `one` user input. + +The following UML Sequence Diagram depicts the process of what happens +when a user input is passed through the application, up till the point when the command gets executed : + +**Note** : BudgetBuddy instantiates other classes such as the Storage and Ui class, however, +these steps have been left out as they have no relevance to the process of creating and executing a Command. + + +![UML Sequence Diagram of Command](diagrams/sequenceDiagram_Command.jpg) + +#### 3.6 Storage Class +The Storage Class handles the loading and saving of the features in BudgetBuddy. Different features are saved in +different files corresponding to their data type. + +The **Storing** methods are always called after every `user input`, ensuring that the saved files +are always up-to-date. + +Similarly, the **Loading** methods present in the Storage Class is always called **before** the application is fully +initialized. + +### 3.7 Commons +The classes present in this group of `Commons` refers to a collection of classes used by multiple other components +. They represent data of the user's financial transactions, including expenses and savings, along with methods +for organizing and managing this data. + +##### 3.7.1 Transaction +This is an abstract class, which is the superclass for both the Expense and Saving Classes. It contains common variables +such as Currency, Category and Amount. + + +##### 3.7.2 Expense +This class holds details regarding an expense a user has. Within this class, it has 4 class-level variables : +`String category`, `LocalDate dateAdded` , `String description` and `Double amount`. + +`String category` : This variable holds the category of the expense. It represents the type or classification +of the expense as per the pre-defined categories ("Housing", "Groceries", "Utility", "Transport", "Entertainment"). + +`LocalDate dateAdded` : This variable holds the date when the expense was added or recorded. +It is of type LocalDate, representing a date without a time zone. +Storing the date of the expense allows users to track when each expense occurred, +facilitating budget management and analysis over time. + +`String description` : This variable holds a description of the expense. +It provides additional details or information about the expense, +such as what the expense was for or any relevant notes. +Descriptions help users understand the context or purpose of each expense entry. + +`Double amount` : This variable holds the monetary amount of the expense. +It represents the cost or value of the expense, typically in the currency used by the user. +Storing the amount allows users to track how much money was spent on each expense, +aiding in budgeting and financial planning. + + +##### 3.7.3 ExpenseList +This class represents a list of expenses. Within this class, it has 2 class-level variables : +`ArrayList expenses` and `ArrayList categories`, The variables and there relevance are as follows : + +`ArrayList expenses` : This variable holds a list of `Expense` objects. +Each `Expense` object represents an expense incurred by the user. The list stores all the expenses entered by the user. +Managing expenses in a list allows for easy retrieval, modification, and deletion of individual expenses. +Additionally, it enables functionalities such as filtering, listing, and calculating total expenses. + +`ArrayList categories` : This variable holds a list of predefined expense categories. +Each category represents a classification or grouping for expenses, such as "Housing," "Groceries," "Utility," etc. +The list provides predefined options for users to select when adding or editing expenses. +It helps organize expenses into meaningful groups, +allowing users to track and analyze their spending habits across different expense categories. + +This class also contains the methods to handle any user interactions with the list of expenses. These methods would +be further explained in their corresponding `Implementation` sections. + + +##### 3.7.4 Saving +This class holds details regarding a saving a user has. Within this class, it has 3 class-level variables : +`String category`, `LocalDate dateAdded`, `Double amount`. The variables and their relevance +are as follows : + +`String Category` : This variable holds the category of the saving. +Similar to expenses, savings can also be categorized based on their purpose or intended use. +Pre-defined categories include ("Salary", "Investments", "Gifts", or "Others"). +Categorizing savings helps users allocate funds for different financial goals and track progress towards those goals. + +`LocalDate dateAdded` : This variable holds the date when the saving was added or recorded. +As with expenses, tracking the date of each saving allows users to monitor their saving habits over time. +It provides a historical record of when savings were initiated, +helping users understand their saving patterns and behaviors. + +`Double amount` : This variable holds the monetary amount of the saving. +It represents the value or sum of money saved by the user. +The amount indicates how much money has been set aside or accumulated towards achieving a particular financial goal. +Users can track their progress towards savings targets and +monitor their overall financial health based on the amount saved. + + +##### 3.7.5 SavingList +This class represents a list of savings. Within this class, it has 2 class-level variables : +`ArrayList savings` and `ArrayList categories`, The variables and there relevance are as follows : + +`ArrayList savings`: This variable holds a list of `Saving` objects, +where each object represents a saving made by the user. The list stores all the savings entered by the user. +Managing savings in a list allows for easy retrieval, modification, and reduction of individual savings. +Additionally, it enables functionalities such as listing savings, calculating remaining savings after +deducting expenses, and adding new savings. + +`ArrayList categories`: This variable holds a list of predefined saving categories. +Each category represents a classification or grouping for savings, such as "Salary," "Investments," "Gifts," etc. +The list provides predefined options for users to select when adding or editing savings. +It helps organize savings into meaningful groups, allowing users to track and +manage their savings across different categories. + +This class also contains the methods to handle any user interactions with the list of savings. These methods would +be further explained in their corresponding `Implementation` sections. + +##### 3.7.6 RecurringExpenseList +This class represents a list of recurring expenses for the Recurring Expense feature. Within this class, it has +1 class-level variable : `String name`. Which is used to store the name of the list. Given that its overall +functionality is similar to ExpenseList class, it **inherits** the ExpenseList class. + +##### 3.7.7 RecurringExpenseLists +This class represents the list of all lists of recurring expenses for the Recurring Expense feature. Within this class, +it has only 1 class-level variable : `ArrayList recurringExpenses`. Which is used to store a list of +ExpenseList objects. This class contains all methods required for the overall Recurring Expense feature to work. +The implementation of these methods would be discussed in further detail in the **Implementation** section. + +For clarity, the following Class Diagram depicts the associations between RecurringExpenseLists, RecurringExpenseList and +ExpenseList. + +![Class Diagram](diagrams/classDiagram_RecurringExpenseLists.jpg) + + +##### 3.7.8 DefaultCurrency +The `DefaultCurrency` class manages the application's default currency setting. It contains a static variable: + +- `Currency defaultCurrency`: Holds the current default currency setting, initialized to the Singapore Dollar (SGD) using the `Currency.getInstance("SGD")` method. + +This class provides two static methods that are further explained in detail in the **Implementation** section.
+This class ensures a consistent default currency is used throughout the application, essential for functions like displaying amounts and performing currency conversions. + + +##### 3.7.9 CurrencyConverter +The `CurrencyConverter` class provides functionality for converting amounts between different currencies. It includes two class-level variables: + +`Map exchangeRates`: This variable represents a map where the keys are instances of +the `Currency` class, and the values are conversion rates as `Double` values. +The map stores exchange rates for various currencies relative to a base currency (in this case, Singapore Dollar, SGD). +The exchange rates are initialized with default values for common currencies such as +USD, EUR, JPY, KRW, MYR, CNY, and HKD. + +The class includes several methods to handle currency conversion tasks, with its relevance explained in the **Implementation** section.
+ +These methods facilitate currency conversion tasks by handling the conversion logic, validating input parameters, +and logging relevant messages. They provide essential functionality for managing expenses and savings in different +currencies within the budget management application. + + + +## 4. Implementation + + +### 4.1 Menu Feature + +The menu feature is designed to allow users to view the relevant command formats by inputting the relevant menu +indexes. This feature is orchestrated by the `MenuCommand` class, which is initialized by the `MenuCommandCreator` +class. Which is in turn, created by the `Parser` class. Within the `MenuCommand` object, the +`MenuCommandCreator` would initialize one class-level variable `index` of type `String`. The relevance of +this class-level variable in `MenuCommand` is as follows + +| Variable Name | Variable Type | Relevance | +|---------------|---------------|--------------------------------------------------------| +| index | int | Refers to the corresponding item in the displayed menu | + +For Clarity, the menu items and their corresponding indexes are as follows : + +| index | Menu Item | +|---------|-------------------------| +| Empty/0 | Displays all Menu Items | +| 1 | Manage Expenses | +| 2 | Manage Savings | +| 3 | View Expenses | +| 4 | View Savings | +| 5 | Find Expenses | +| 6 | Split Expenses | +| 7 | Manage Recurring Bills | +| 8 | Change Currency | +| 9 | Manage Budget | +| 10 | Get Graphical Insights | + +Upon the call of the `execute()` method in BudgetBuddy using `command.execute()`, the `MenuCommand` object +utilizes methods from the `UI` class to display the relevant menu items. The utilized methods are as follows : + +| methodName | Return Type | Relevance | +|---------------------|-------------|-------------------------------------| +| showMenuTitles() | void | Prints all Menu Items | +| showMenuItem(INDEX) | void | Prints commands associated at INDEX | + + +**Important Note** : As the process of how the CommandCreator is created upon the receipt of a user input has already been +discussed in `3.4 CommandClass`, the following Sequence Diagrams would omit the initial methods prior to the +MenuCommandCreator being created. + +The following UML Sequence Diagram shows how the MenuCommandCreator for Menu Commands work and what +will be returned to the Parser, which will ultimately be returned to BudgetBuddy. Note that this diagram assumes that `Parser` +has already detected that the user input is a menu command and has initialized a MenuCommandCreator object: + + +![Sequence Diagram for MenuCommandCreator for Menu Command](diagrams/sequenceDiagram_MenuCommandCreator.jpg) + +The following UML Sequence Diagram shows the processes of the MenuCommand upon the call of its execute() command: + +![Sequence Diagram for Menu Command](diagrams/sequenceDiagram_MenuCommand.jpg) + +Given below is an example usage scenario and how the full Menu feature works : +1. The user types `menu 1`. This input passed from `BudgetBuddy` into `Parser#parseCommands()`. +2. Within the `Parser` , it determines that the input is a menu command from `isMenuCommand()`, and creates a new + `MenuCommandCreator` object. +3. The `Parser` then calls `MenuCommandCreator#createCommand()` +4. The checks for whether the input is valid, in particular whether it is a valid integer, + along with obtaining the value of `index` is done in `MenuCommandCreator#handleMenuCommand()` +5. `MenuCommandCreator` creates a constructor for `MenuCommand` with the parameter `1`, which in turn + also constructs a new `Ui` object +6. `MenuCommandCreator` returns this created `MenuCommand` to `Parser`, which is then returned to `BudgetBuddy` +7. `BudgetBuddy` then calls `MenuCommand#execute()` +8. `execute()` then calls `Ui#showMenuItem(1)` +9. `showMenuItem()` in `Ui` then prints all commands for `case 1` which is for `Manage Expenses` + + +### 4.2 Add Expense Feature + +The Add Expense Feature allows users to add expenses to different categories. `AddExpenseCommand` class enables this feature, +after initialized by the `Parser` class. Within the `AddExpense` object, the `Parser` would have initialized it with +4 variables, an `ExpenseList` object, along with a `category`, `amount` , `description`. +The relevance of these Class Attributes in `AddExpenseCommand` is as follows : + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|---------------------------------------------------| +| expenses | ExpenseList | ExpenseList Object containing the list of expenses| +| category | String | The category that the `expense` belongs to | +| amount | String | The amount spent | +| description | String | The description of the expense | + + +Upon the call of the `execute()` method in `BudgetBuddy` using `command.execute()`, +the `AddExpenseCommand` Object utilizes the following method from the `ExpenseList` class to add it to the existing +list of `expenses` matching against the corresponding `category`. + +| Method | Return Type | Relevance | +|--------------|-------------|-------------------------------------------------| +| addExpense() | void | Add expense to the existing list of `expenses` | + +The following UML Sequence diagram shows how the Parser works to obtain the relevant inputs for the Add Expense Feature : +![Sequence Diagram for Parser for Add Expense Feature](diagrams/sequenceDiagram_AddExpense.jpg) + +The following is a step-by-step explanation for the Parser for the Find Feature : +1. `BudgetBuddy` calls `Parser#parseCommand(input)` with `input` being the entire user input. +E.g `add expense c/Transport a/20 d/EZ-Link Top Up` +2. Within the `Parser`, it will have determined that the `input` is a Find Command from the `isAddExpenseCommand(input)` +function. +3. The `Parser` then self calls the method `handleAddExpenseCommand(input)` with the `input` still being the entire +user input. +4. Within `AddExpenseCommand(input)`, the first check would be the check for the existence of any combination of +`c/ , a/ and d/`. If none of these combinations were found, it immediately returns `null`. +5. If the checks in `4.` is passed, Three variables would be initialized. + + * | Variable Name | Variable Type | + |---------------|---------------| + | category | String | + | amount | String | + | description | String | +6. Depending on which parameters were present, the corresponding input would be extracted and placed into each variable +using the `Parser#extractDetailsForAdd(input, "parameter")` +7. Finally, `Parser#handleAddExpenseCommand()` returns a `AddExpensesCommand` to `Parser#parseCommand()`, which is +then returned to `BudgetBuddy` + + +### 4.3 Add Savings Feature + +The Add Savings Feature allows users to add savings to different categories. `AddSavingCommandCreator` class intialises the `AddSavingCommand`, after initialised by the `Parser` class. Within the `AddSavings` object, the `Parser` would have initialized it with +4 variables, a `SavingList` object, along with a `category`, `amount`. +The relevance of these Class Attributes in `AddExpenseCommand` is as follows : + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|---------------------------------------------------| +| savings | SavingList | SavingList Object containing the list of savings | +| category | String | The category that the `expense` belongs to | +| amount | String | The amount spent | + +Upon the call of the `execute()` method in `BudgetBuddy` using `command.execute()`, +the `AddSavingCommand` Object utilizes the following method from the `SavingList` class to add it to the existing +list of `savings` matching against the corresponding `category`. + +| Method | Return Type | Relevance | +|--------------|-------------|-------------------------------------------------| +| addSaving() | void | Add savings to the existing list of `savings` | + +The following UML Sequence diagram shows how the Parser works to obtain the relevant inputs for the Add Expense Feature : +![Sequence Diagram for Parser for Add Expense Feature](diagrams/sequenceDiagram_AddSaving.jpg) + +The following is a step-by-step explanation for the Parser for the Find Feature : +1. `BudgetBuddy` calls `Parser#parseCommand(input)` with `input` being the entire user input. +E.g `add savings c/Allowance a/20` +2. Within the `Parser`, it will have determined that the `input` is a Find Command from the `isAddSavingsCommand(input)` +function. +3. The `Parser` then self calls the method `handleAddExpenseCommand(input)` with the `input` still being the entire +user input. +4. Within `AddExpenseCommand(input)`, the first check would be the check for the existence of any combination of +`c/ , and a/`. If none of these combinations were found, it immediately returns `null`. +5. If the checks in `4.` is passed, two variables would be initialized. + + * | Variable Name | Variable Type | + |---------------|---------------| + | category | String | + | amount | String | +6. Depending on which parameters were present, the corresponding input would be extracted and placed into each variable +using the `Parser#extractDetailsForAdd(input, "parameter")` +7. Finally, `Parser#handleAddExpenseCommand()` intialises a `AddExpensesCommandCreator` which then returns `AddSavingCommand` to `Parser#parseCommand()`, which is then returned to `BudgetBuddy`. + + +### 4.4 Add Shared Bill feature + +The Add Shared Bill Feature allows users to enter expenses that are shared among multiple parties, facilitating easy splitting and tracking of such expenses. The feature is managed by the `SplitExpenseCommand` class, which is initialized by the `SplitExpenseCommandCreator` as a result of the Parser class interpretation. + +Class Attributes for SplitExpenseCommand: + +| Class Attribute | Variable Type | Relevance | +|-------------------|-------------------|--------------------------------------------------------------| +| splitExpenseList | SplitExpenseList | SplitExpenseList O bject where the shared bill will be added | +| amount | double | The total amount of the shared bill | +| numerOfPeople | int | The number of people that are meant for splitting the bill | +| description | String | Description of the shared bill | + +Upon the call of the execute() method via command.execute(), SplitExpenseCommand performs the following key actions: + +1. It adds the shared bill as an expense to the ExpenseList. +2. Calculates each participant's share based on the total amount divided by the number of participants. + +Key Methods used from SplitExpenseList + +| Method | Return Type | Relevance | +|------------------------|------------------------|------------------------------------------------------| +| addSplitExpense() | void | Adds the splitexpense to the list of splitexpenses | + +The SplitExpenseCommand also provides an output summarizing the shared expense, each participant's share. + +Sequence Diagram for Adding a Shared Bill +The sequence diagram illustrates the flow from when a user inputs a command to add a shared bill to its execution: +![Sequence Diagram for Parser for addSplitExpense Feature](diagrams/sequenceDiagram_SplitExpense.jpg) + +User Input: The user inputs a command in the format `add shared bill a/ n/ d/` + +Parsing: The `Parser` class identifies the input as a shared bill command and extracts the necessary parameters (`amount`, `number of people`, `description`). +Command Initialization: The `Parser` initializes a `SplitExpenseCommand` with the extracted parameters. +Execution: The `SplitExpenseCommand` is executed, which calls `addSplitExpense()` on the `SplitExpenseList` to add the shared bill. +Calculation: The command calculates each participant's share of the bill and records it. + + + +### 4.5 Edit Savings Feature +The Edit Savings feature allows users to update their previously saved financial contributions, specifically adjusting +the `category` and `amount`. This feature is facilitated by the `EditSavingCommand` class, which is prepared and issued +by the `Parser` class. An `EditSavingCommand` object encapsulates several variables that are instantiated within the +`Parser`: a `SavingList` object, `category`, and `amount`. The significance of these Class Attributes within +`EditSavingCommand` is detailed below: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|-----------------------------------------------------------------------| +| savings | SavingList | SavingList Object containing the list of savings that can be modified | +| category | String | The updated category for the saving entry at the specified index | +| amount | Double | The updated monetary value for the saving entry at the specified index| + +Upon invoking the `execute()` method in `BudgetBuddy` through `command.execute()`, the `EditSavingCommand` object +leverages the following method from the `SavingList` class to carry out the modification: + +| Method | Return Type | Relevance | +|--------------|-------------|----------------------------------------------------------------------| +| editSaving() | void | Adjusts the `amount` for the saving entry at the provided `category` | + +The following UML Sequence diagram illustrates the execution process of the Edit Savings Feature Command when a user enters a valid edit savings command: + +![EditSavingsDiagram.png](diagrams/EditSavingsDiagram.png) + +Here is a step-by-step narrative of the actions taken for a sample input: +`edit savings c/Salary a/3000` + +1. BudgetBuddy receives the command `edit savings c/Salary a/3000` and passes it to the `Parser` for interpretation. +2. The `Parser` splits the command into components and constructs an `EditSavingCommand` object with the category (`c/Salary`) and amount (`a/3000`). +3. The `Parser` returns the constructed `EditSavingCommand` object to BudgetBuddy. +4. BudgetBuddy then executes the `execute()` method on the `EditSavingCommand` object. +5. Inside its `execute()` method, `EditSavingCommand` calls the `editSaving` method of `SavingList`, supplying the relevant parameters. +6. `SavingList` updates the entry's amount to 3000 for the category Salary. +7. Finally, the console outputs a confirmation message: "Saving updated successfully." + + +### 4.6 Edit Expense Feature +The Edit Expense feature allows users to edit their previously added expenses, specifically the `category`, `amount`, +and `description`. This feature is managed by the `EditExpenseCommand` class, which is initialized by the +`Parser` class. Within the `EditExpenseCommand` object, 5 variables would have been initialized in the `Parser` class: +an `ExpenseList` object, `category`, `index`, `amount` and `description`. The relevance of these Class Attributes in +`EditExpenseCommand` is as follows: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|-----------------------------------------------------------------------| +| expenses | ExpenseList | ExpenseList Object containing the list of expenses that can be edited | +| category | String | The edited category for the expense in the specified index | +| index | Integer | The index of the expense to be edited from `ExpenseList` | +| amount | Double | The edited amount the expense in the specified index should be | +| description | String | The edited description for the expense in the specified index | + +When the `execute()` method in `BudgetBuddy` is called via `command.execute()`, the `EditExpenseCommand` Object, +utilizes the following method from the `ExpenseList` class to edit the expense. + +| Method | Return Type | Relevance | +|---------------|-------------|-------------------------------------------------------------------------------------------| +| editExpense() | void | Edits the `category`, `amount` and `description` for the expense in the specified `index` | + +The following UML Sequence diagram below shows how the Edit Expense Feature Command is executed when a user +inputs a valid edit expense command: + +![EditExpenseDiagram.drawio.png](diagrams%2FEditExpenseDiagram.drawio.png) + +The following is a step by step explanation of the processes that occur for an example input: +`edit expense c/Transport i/2 a/40 d/GRAB` + +1. The BudgetBuddy application receives the input string `edit expense c/Transport i/2 a/40 d/GRAB` and uses +the `Parser` to interpret it. +2. The `Parser` splits the input into parts and constructs a `EditExpenseCommand` Object with the category +(`c/Transport`), index (`i/2`), +amount (`a/40`), and description (`d/GRAB`). +3. `Parser` returns this called `EditExpenseCommand` Object to `BudgetBuddy`. +4. The `BudgetBuddy` application calls `execute()` on the `EditExpenseCommand` object. +5. The `EditExpenseCommand` object calls `editExpense` on the `ExpenseList` with the provided parameters. +6. The `ExpenseList` looks up the second expense in its list +(as lists are zero-indexed, it uses index - 1 to access the correct item), and updates this expense’s +category to "Transport," amount to 40.0, and description to "GRAB." +7. A message "Expense edited successfully." is printed to the console. + + +### 4.7 Reduce Savings Feature +The Reduce Savings feature enables users to decrement a specified amount from their savings at a given index. This +functionality is controlled by the `ReduceSavingCommand` class, which is produced by the `ReduceSavingCommandCreator` +based on user input. The `ReduceSavingCommand` class uses a `SavingList` object to access the relevant saving and performs +the reduction operation using the provided index and amount. Below is the relevance of these attributes: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|------------------------------------------------------------------------------| +| savings | SavingList | The `SavingList` object containing the list of savings which can be reduced | +| category | String | TThe category of savings to reduce | +| amount | double | The amount by which the savings in the specified category should be reduced | + +When `BudgetBuddy` runs the `execute()` method through `command.execute()`, the `ReduceSavingCommand` leverages the reduceSavingsByCategory method from the `SavingList` class: + +| Method | Return Type | Relevance | +|-----------------------------|-------------|------------------------------------------------------------------| +| reduceSavingsByCategory() | void | Decreases the savings by a specified amount in a given category | + +The user interaction for reducing savings follows these steps: + +1. The user commands to reduce savings by inputting `reduce savings c/[category] a/[amount]`. +2. `BudgetBuddy` processes this input with the help of a `Parser`, which identifies the suitable `CommandCreator`. +3. `Parser` constructs a `ReduceSavingCommand` object with the extracted category and amount. +4. `BudgetBuddy` then executes the `ReduceSavingCommand`. +5. The `execute()` method within ReduceSavingCommand calls the SavingList's reduceSavingsByCategory function. +6. The `reduceSavingsByCategory` method performs the deduction and updates the savings amount. + +The following UML Sequence diagram below shows how the Reduce savings Feature Command is executed when a user +inputs a valid reduce savings command: +![sequenceDiagram_ReduceSavings.png](diagrams/sequenceDiagram_ReduceSavings.png) + + +### 4.8 Delete Expenses Feature +The Delete Expense feature grants users the capability to remove expenses they have previously entered. Managed by the +DeleteExpenseCommand class, this feature is initialized through DeleteExpenseCommandCreator. During the creation process, +the command is provided with an `ExpenseList` object and an `index` indicating the specific expense to be deleted. +The following table outlines the significance of these attributes: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|-----------------------------------------------------------------------| +| expenses | ExpenseList | ExpenseList Object containing the list of expenses that can be edited | +| index | Integer | The edited category for the expense in the specified index | + + +On invocation of the `execute()` method, as part of the `command.execute() `flow within BudgetBuddy, the DeleteExpenseCommand +object engages the deleteExpense() method from the ExpenseList class. + +| Method | Return Type | Relevance | +|-----------------------------|-------------|-----------------------------------------------------------| +| deleteExpense() | void | Removes the expense at the specified index from the list | + +The user interaction for deleting expenses follows these steps: +1. The user submits a delete command in the format `delete expense i/index`, with `index` specifying the expense to be deleted. +2. `BudgetBuddy` receives the command and employs the Parser to deconstruct it. +3. The `Parser` discerns the delete command, extracting the index value and forming a DeleteExpenseCommand object. +4. `BudgetBuddy` triggers the DeleteExpenseCommand.execute() method. +5. Inside `execute()`, the `deleteExpense()` method is called on `ExpenseList`, with `index` indicating the targeted expense. +6. If the index is valid, the expense is removed, and a confirmation message is printed to the console. + + + +### 4.9 Listing Feature (List Savings) + +The Listing Savings Feature enables users to view their savings, potentially filtered by a specific category. This functionality is orchestrated by the `ListSavingsCommand` class, which is initialized by the `ListCommandCreator` class. Within the `ListSavingsCommand` object, the `ListCommandCreator` provides it with a `SavingList` object, an `ExpenseList` object, along with an optional `filterCategory`. The relevance of these class attributes in `ListSavingsCommand` is detailed in the following table: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|--------------------------------------------------------------------------------------| +| savings | SavingList | The `SavingList` object containing the list of savings to be displayed or filtered | +| expenses | ExpenseList | The `ExpenseList` object containing the list of expenses | +| filterCategory | String | The category to filter the savings by, if provided | + +When `BudgetBuddy` invokes the `execute()` method via `command.execute()`, the `ListSavingsCommand` object uses several methods from the `SavingList` class to perform its tasks: + +| Method | Return Type | Relevance | +|-----------------------------|--------------------|----------------------------------------------------------------| +| getSavings() | ArrayList | Retrieves the list of all savings from the `SavingList` | +| findTotalSavings() | void | Calculates the total amount of savings stored in `SavingList` | +| listSavings() | void | Prints the savings, filtered by `filterCategory`, to the CLI | +| calculateRemainingSavings() | double | Calculates the remaining amount after deducting total expenses | + +The Listing Savings feature follows these steps when a user inputs a command to list savings: +1. The user inputs `list savings [optional: filterCategory]`. This input is processed by the `Parser` class in `BudgetBuddy`, which creates a `ListSavingsCommand` object with `savings` set to the current `SavingList` and `filterCategory` to the user-specified category, if any. +2. The `Parser` returns this `ListSavingsCommand` object to `BudgetBuddy`, which calls `ListSavingsCommand.execute()`. +3. `execute()` calls `SavingList.listSavings(filterCategory, expenses)`, where the `filterCategory` is applied if provided. +4. Within `listSavings()`, the `findTotalSavings()` method is called first to calculate the initial total savings amount. +5. The `listSavings()` method continues by iterating through each `Saving` and printing those that match the `filterCategory` criteria. +6. After listing, the method calculates and displays the remaining savings by calling `calculateRemainingSavings(initialAmount, totalExpenses)`, accounting for any expenses deducted. +7. If the `filterCategory` is not provided, all savings are printed, and the total initial amount and remaining savings after expenses are displayed. + +#### Sequence Diagram +The UML Sequence diagram for the Listing Savings feature would illustrate the interactions between the `User`, `BudgetBuddy`, `Parser`, `ListSavingsCommand`, and `SavingList` classes, showing the method calls and returns between these objects to complete the operation. +![Sequence diagram for List Expense Feature](diagrams/SavingList_SequenceDiagram.png) + + + +### 4.10 Listing Feature (List Expenses) +The Listing Expenses Feature provides users with the ability to view their expenses, which can be filtered by category. The `ListExpenseCommand` class, generated by the `ListCommandCreator`, is responsible for this feature. The class utilizes the `ExpenseList` object to access and manipulate expense records, optionally applying a filter based on the category. The significance of the `ListExpenseCommand` class's attributes is outlined below: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|-----------------------------------------------------------------------------------| +| expenses | ExpenseList | Holds the list of expenses to be filtered and listed | +| filterCategory | String | The category to filter the expenses by (null if no filtering is needed) | + +Upon invoking the `execute()` method by `BudgetBuddy` through `command.execute()`, the `ListExpenseCommand` object calls upon several methods from the `ExpenseList` class to carry out its responsibilities: + +| Method | Return Type | Relevance | +|--------------------------|-------------|------------------------------------------------------------------------| +| listExpenses() | void | Prints the expenses, filtered by `filterCategory`, to the command line | +| calculateTotalExpenses() | double | Calculates the total expenses from the list of expenses | + +Here's an overview of the process flow when a user employs the Listing Expenses feature: +1. The user types `list expenses [optional: filterCategory]`. This command is parsed by the `Parser` class within `BudgetBuddy`, which then creates a `ListExpenseCommand` with `expenses` set to the current `ExpenseList` and `filterCategory` set to any specified by the user. +2. The `Parser` returns the `ListExpenseCommand` object to `BudgetBuddy`, which calls `ListExpenseCommand.execute()`. +3. The `execute()` method invokes `ExpenseList.listExpenses(filterCategory)`. If a `filterCategory` is provided, it will filter the expenses accordingly. +4. The `listExpenses()` method in `ExpenseList` iterates over the list of expenses and prints each one that matches the filter category criteria or all expenses if no filter is provided. +5. The method concludes by displaying the total expenses calculated using `calculateTotalExpenses()`. + +#### Sequence Diagram +The sequence diagram for the Listing Expenses feature would illustrate the above steps, showing the interactions between the `User`, `BudgetBuddy`, `Parser`, `ListExpensesCommand`, and `ExpenseList` classes. +![Sequence diagram for List Expense Feature](diagrams/ExpenseList_SequenceDiagram.png) + + +### 4.11 Check Split Bill feature + +The Check Split Bills Feature allows users to view a list of all bills that have been marked as split among multiple parties. This is particularly useful for tracking shared expenses in scenarios like shared accommodations, group trips, or joint projects. + +Class Attributes for CheckSplitExpensesCommand: + +| Class Attribute | Variable Type | Relevance | +|---------------------|-----------------------------------|-------------------------------------------------------| +| splitExpenseList | splitExpenseList | Object containing the list of split bills to display | + +When BudgetBuddy executes the `ListSplitExpenseCommand` via `command.execute()`, the `ListSplitExpenseCommand` uses the following method from the `SplitExpenseList` class to retrieve and display all split expenses: + +| Method | Return Type | Relevance | +|---------------------|-----------------------------------|-----------------------------------------------------------------------| +| listSplitExpense | ArrayList | Retrieves and displays a detailed list of all recoreded split expenses| + +Process Overview: +1. The user issues a command to check split expenses e.g. `check split bills`. +1 `BudgetBuddy` processes this input with the help of a `Parser`, which then initialises the `ListSplitExpenseCommandCreator`. +3. The `Parser` constructs a `ListSplitExpenseCommand` with the split expenses list as a parameter. +4. `BudgetBuddy` then executes the `ListSplitExpenseCommand`. +5. The `execute()` method within the `ListExpenseCommand` calls the `listSplitExpenses()` method on the `SplitExpenseList`. +6. The `listSplitExpenses()` method retrieves all split expenses and formats them for display. +7. Each split expense is printed out, showing details including the description of the split expense, the number of people in the bill and the amount payable by each person. + +Sequence Diagram: +The sequence diagram for the Check Split Expenses feature would illustrate the interactions between the User, BudgetBuddy, Parser, CheckSplitExpensesCommand, and SplitExpenseList classes, showing how the method calls and returns between these objects complete the operation to display all split expenses. +![Sequence Diagram for Parser for addSplitExpense Feature](diagrams/sequenceDiagram_checkSplitBills.jpg) + + +### 4.12 Settle Bill feature + +The Settle Bill Feature allows users to mark shared bills as settled, which is crucial for tracking repayments in scenarios such as shared accommodations or group outings. +Class Attributes for `SettleBillCommand`: + +| Class Attribute | Variable Type | Relevance | +|-----------------------------|------------------------|---------------------------------------------------------| +| splitExpenseList | SplitExpenseList | Object containing the list of shared bills to be settled| + +When `BudgetBuddy` executes the `SettleSplitExpenseCommand` via `command.execute()`, the `SettleSplitExpensesCommand` uses the following method from the `SplitExpenseList` class to delete the bill: + +| Method | Return Tyoe | Relevance | +|---------------------------|-----------------------------|-------------------------------------------------------| +| settleSplitExpense(index) | void | Marks the split expense at the given index as settled | + +Process Overview: + +1. The user issues a command to settle a bill, e.g., `settle bill 3`. +2. `BudgetBuddy` processes this input with the help of a `Parser`, which initialises the `SettleSplitExpenseCommandCreator`. +3. The `Parser` constructs a `SettleSplitExpenseCommand` with the split expense list and index as parameters. +4. `BudgetBuddy` then executes the `SettleSplitExpenseCommand`. +5. The `execute()` method within `SettleSplitExpenseommand` calls the `settleSplitExpense(index)` method on the `SplitExpenseList`. +6. The `settleSplitExpense(index)` method deletes the shared bill at the specified index. +7. A confirmation message is displayed, informing the user that the bill has been settled. + +Sequence Diagram: +The sequence diagram for the Settle Bill feature would illustrate the interactions between the `User`, `BudgetBuddy`, `Parser`, `SettleSplitExpenseCommand`, and `SplitExpenseList` classes, showing how the method calls and returns between these objects complete the operation to mark a shared bill as settled. +![Sequence Diagram for Parser for addSplitExpense Feature](diagrams/sequenceDiagram_settleSplitBill.jpg) + + + +### 4.13 Find Feature +The Find Feature allows users to search for expenses based on a specific criteria such as description, minimum amount +and maximum amount. This feature is orchestrated by the `FindExpensesCommand` class, which is created by the `FindExpensesCommandCreator` +, which is in turn created by the `Parser`. Within the `FindExpensesCommand` object, the `FindExpensesCommandCreator` +would have initialized it with 4 variables, an `ExpenseList` object, along with a `description`, `minAmount` , +`maxAmount`. The relevance of these Class Attributes in `FindExpensesCommand` is as follows : + +| Variable Name | Variable Type | Relevance | +|---------------|---------------|---------------------------------------------------------------------------| +| expenses | ExpenseList | ExpenseList Object containing the list of expenses which will be filtered | +| description | String | The description to match against expenses in `expenses` | +| minAmount | Double | The **minimum** amount matched expenses should be | +| maxAmount | Double | The **maximum** amount matched expenses should be | + + +Upon the call of the `execute()` method in `BudgetBuddy` using `command.execute()`, +the `FindExpensesCommand` Object, utilizes the following methods from the `ExpenseList` class in order to both +obtain a new `ExpenseList` object containing the filtered expenses, along with printing them. + +| Method | Return Type | Relevance | +|------------------|--------------------|-----------------------------------------------------------------| +| filterExpenses() | ArrayList | Returns an ArrayList containing all filtered expenses | +| listExpenses() | void | Prints the filtered expenses obtained from `filterExpenses()` | + +**Important Note** : As the process of how the CommandCreator is created upon the receipt of a user input has already been +discussed in `3.4 CommandClass`, the following Sequence Diagrams would omit the initial methods prior to the +FindCommandCreator being created. + +The following UML Sequence diagram below shows how FindExpensesCommandCreator works to +obtain the relevant inputs for the FindExpensesCommand, NOTING that the Parser has already determined the input to be a find +expenses command, and has also created the FindExpensesCommandCreator. + +![Sequence Diagram for FindFeatureCommandCreator](diagrams/sequenceDiagram_FindExpensesCommandCreator.jpg) + +Given that multiple methods are called in `FindExpensesCommandCreator`. The following is a step-by-step explanation for the processes that occur before the FindExpensesCommand is created : +1. `BudgetBuddy` calls `Parser#parseCommand(input)` with `input` being the entire user input. +E.g `find expenses d/bruno morethan/ lessthan/` +2. Within the `Parser`, it will have determined that the `input` is a Find Command from the `isFindCommand(input)`. +3. The `Parser` then creates a `FindExpensesCommandCreator` object, initializing it with the overall Expense List and +the provided user input +4. The `Parser` then calls `FindExpensesCommandCreator#createCommand()`. +5. `FindExpensesCommandCreator#createCommand()` then calls `FindExpensesCommandCreator#handleFindExpensesCommand()` +6. Within `handleFindExpensesCommand(input)`, the first check would be the check for the existence of any combination of +`d/ , morethan/ and lessthan/` using the method `checkForInvalidParameters()`. If none of these combinations were found, it immediately returns `null` +7. This is then followed by a second check `checkForOutOfOrderParameters()`, which checks whether `d/`, `morethan/` and `lessthan/` +is in the right order. +8. This is then followed by a third check `checkForDuplicateParameters()`, which checks for duplicates of parameters +in the user input. It duplicates are found, similarly, it immediately returns `null`. +9. If the checks in `6.` `7.` and `8.` is passed, or in this case **No Exceptions** are thrown. +Three variables would be initialized. + + * | Variable Name | Variable Type | + |---------------|---------------| + | description | String | + | minAmount | Double | + | maxAmount | Double | +10. Depending on which parameters were present, the corresponding input would be extracted from the full user input and placed into each variable +using the `FindExpensesCommandCreator#parse*()`, where `*` represents the variable name we wish to obtain. +11. Note that any parameters left empty, would be treated as **null**. +11. Should the values of `minAmount` and `maxAmount` not be empty, a check is done to ensure `minAmount` is less than +or equals to `maxAmount`. If this check does not pass, the function immediately returns `null` +12. Finally, `FindExpensesCommandCreator#handleFindExpensesCommand()` creates and returns a +`FindExpensesCommand` containing the extracted description, minAmount and maxAmount +13. `FindExpensesCommandCreator#createCommand()`, which is returned to, `Parser#parseCommand()` +, which is then returned to `BudgetBuddy` + +The following UML Sequence diagram below shows how the Find Feature command works when a user provides a **valid** +find expenses command upon the call of its execute() method: + +![Sequence diagram for Find Feature](diagrams/sequenceDiagram_FindExpensesCommand.jpg) + + +**Important Note** : Although d/ , morethan/ and lessthan/ are optional parameters, the optional component would mean +user has left that option empty if not in use, e.t.c `find expenses d/ morethan/ lessthan/200`. Hence, +unused parameters are treated as null variables instead. + +**Important Note 2** : Although the UI class is also initialized, the details of its use is omitted as its functionality in the +Find Feature is trivial. In this case, the UI class is **only** used to print dividers. + +The following is an example of the processes that occur when the user uses the find expenses feature: +1. The user types `find expenses d/bruno morethan/30 lessthan/200`. This input is passed through the `Parser` +class from `BudgetBuddy`, which constructs a `FindExpenseCommandCreator` Object. The `FindExpenseCommandCreator` then +creates a `FindExpenseCommand` object with its variables initialized to `expenses : current overall ExpenseList`, +`description : bruno`, `minAmount : 30`, `maxAmount : 200`, by calling `FindExpenseCommandCreator#createCommand()`. +2. `Parser` returns this created `FindExpenseCommand` Object to `BudgetBuddy` and `BudgetBuddy` calls +`FindExpenseCommand#execute()` +3. `execute()` is called, which initializes a variable `filteredExpenses` of type `ArrayList`. +4. `execute()`then calls `ExpenseList#filterexpenses()`, which returns the filtered expenses based on the `description`, +`minAmount` and `maxAmount`, into the `filteredExpenses` variable. +5. If `filteredExpenses` is empty, "No Matching Expenses Found" is printed and `execute` ends here. +6. If `filteredExpenses` is not empty, `execute()` then initializes a new variable `filteredExpenseList` +of type `ExpenseList` with `filteredExpenses` initialized as the `expenses` Class attribute. +7. Finally `execute()` calls `filteredExpenseList#listexpenses()` to print filtered expenses into the CLI. + + +### 4.14 Recurring Expenses Feature +The Recurring Expenses feature allows users to create list(s) of expenses, where each list can be added to +the overall expenses in a single command. This feature includes the creation of a list of expenses, the viewing of +all/each list of expenses and the removal of each list of expenses. All functions are orchestrated by the +`RecurringExpenseCommand` class, which would have been created by the `RecurringExpenseCommandCreator`, which is in turn +created by the `Parser` class. When `RecurringExpenseCommand#execute()` is called by `BudgetBuddy`, it utilizes methods +present in `ExpenseList`, `RecurringExpenseList` and `RecurringExpenseLists` to facilitate the relevant features. + +Within the RecurringExpenseCommand, the following variables would be initialized : + +| Variable | Variable Type | Relevance | +|-----------------|---------------|--------------------------------------------------------------------------------| +| overallExpenses | ExpenseList | Refer to the overall Expense List storing all of User's Expenses | +| initialListName | String | Used as the name of the new list that will be created | +| commandType | String | Type of RecurringExpenseCommand. E.g. `newlist`, `viewlists`, ... | +| listNumber | int | Refers to the List Number of a recurring expense list shown during `viewlists` | +| category | String | Category of the Expense to be added when using `newexpense` | +| amount | Double | Amount of Expense to be added when using `newexpense` | +| description | String | Description of Expense to be added when using `newexpense` | + +When viewing the code, you would notice that there are 5 different constructors in `RecurringExpensesCommand`. These +constructors correspond to the different `commandTypes` present. Each constructor would initialize only the required +parameters for the specified `commandTypes`. + +A switch statement in `RecurringExpensesCommand` is used, where it runs the corresponding function according to the +`commandType`. The following is the `commandType`, class-level methods used and methods utilized from other classes +when `RecurringExpensesCommand#execute()` is called + +| commandType | Calls Method | Uses Methods From | +|--------------|----------------------------------|------------------------------------------------------------------------------------------------------------------| +| newlist | addNewList() | `RecurringExpenseLists#addNewRecurringList()` | +| viewlists | printList() | `RecurringExpenseLists#printAllRecurringLists()` | +| removelist | removeList() | `RecurringExpenseLists#removeList()` | +| newexpense | addExpenseToList() | `RecurringExpenseLists#getExpenseListAtListNumber()`, `ExpenseList#addExpense()` | +| addrec | addRecurringExpensesToExpenses() | `RecurringExpenseLists#getExpenseListAtListNumber()`, `ExpenseList#getExpenses()`, `AddExpenseCommand#execute()` | +| viewexpenses | printExpensesAtIndex | `RecurringExpenseLists#getExpenseListAtListNumber()` , `ExpenseList#listExpenses()` | | + +From the table above, most commandTypes have a fairly straight forward process of calling a single method from the relevant classes, and follows +a similar process to many of the previous features too. Hence, the explanation of these trivial methods would be left out to avoid repetition. For details regarding these methods, you may also view the JavaDoc comments found in the code. +However, the `addrec` commandType would be the most complicated to follow, given that it utilizes 3 methods from three different classes. The following +is a UML sequence diagram to illustrate the implementation of the addRecurringExpensesToExpenses() method in `RecurringExpenseCommand`, upon the call of the `execute()` +from `BudgetBuddy` + +![Sequence Diagram for addRecurringExpensesToExpenses()](diagrams/sequenceDiagram_RecurringExpenseCommand.jpg) + +The following is an example of the processes that occur when the user uses the rec addrec command : +1. The user types `rec addrec 1`. This input is passed through the `Parser` +class from `BudgetBuddy`, which constructs a `RecurringExpenseCommandCreator` +2. `RecurringExpenseCommandCreator` identifies that the command type is `addrec`, obtains all the relevant parameters, +and uses the constructor `RecurringExpenseCommand(1, recurringExpenseLists, overallExpenses, addrec)`. Note that +`recurringExpenseLists` here is the overall list containing all lists of recurring expenses and `overallExpenses` is the user's +overall expenses. +3. The created `RecurringExpenseCommand` is returned to the `Parser`, which is then returned to `BudgetBuddy`. +4. `BudgetBuddy` calls `RecurringExpenseCommand#execute()` +5. In `execute()`, `RecurringExpenseCommand` identifies it needs to perform a `addrec` operation from its +`commandType` and calls its own `addRecurringExpensesToExpenses()` +6. The first check is passed as the listNumber is a valid number. If the listNumber is invalid, an error message is printed, +and the method would have ended here after printing an error message. +7. The `recurringExpenseList` we wish to add into the `overallExpenses` is obtained utilizing `RecurringExpensesList#getExpenseListAtListNumber(listNumber)` +where `listNumber` is `1`. +8. Next the `ArrayList expenses` is extracted by utilizing `ExpenseList#getExpenses()` from our extracted `recurringExpenseList` +9. Lastly, a for loop is utilized, extracting the `category`, `amount` and `description` of all the expenses present in `expenses` +and adding them one by one into the `overallExpenses`. This is done so by creating a new `AddExpenseCommand` with the relevant parameters and executing it. FOr more details regarding +this `AddExpenseCommand`, do refer to the `Implementation` section for `AddExpenseCommand`. +10. Finally, a success message is printed to the User. + + +### 4.15 Currency Converter Feature +The Currency Converter Feature allows users to convert the currency of expenses and savings. This feature is facilitated by the `ChangeCurrencyCommand` class, initialized by the `Parser` class with `CurrencyConverter`, `ExpenseList`, and `SavingList` objects, alongside the `newCurrency` to convert to. The importance of these class attributes is as follows: + +| Class Attribute | Variable Type | Relevance | +|-------------------|------------------------|-------------------------------------------------------------| +| currencyConverter | CurrencyConverter | The object responsible for currency conversion calculations | +| expenseList | ExpenseList | Contains the expenses whose currency will be converted | +| savingList | SavingList | Contains the savings whose currency will be converted | +| newCurrency | Currency | The new currency to which the amounts will be converted | +| exchangeRates | Map | Stores exchange rates with currencies as keys | + +When `BudgetBuddy` calls `command.execute()`, `ChangeCurrencyCommand` employs the following methods from `CurrencyConverter` to convert the currency of all financial records: -{Describe the target user profile} +| Method | Return Type | Relevance | +|------------------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------| +| convertExpenseCurrency() | void | Converts the currency of each `Expense` object to `newCurrency` | +| convertSavingCurrency() | void | Converts the currency of each `Saving` object to `newCurrency` | +| convertBudgetCurrency() | void | Converts the currency of each `Budget` object to `newCurrency` | +| convertRecurringExpensesCurrency() | void | Converts the currency of each `Expense` object in each `ExpenseList` object of the `RecurringExpenseLists` object to `newCurrency` | +| convertAmount() | double | Converts an amount from one currency to another using the exchange rates | + +The Currency Converter feature also includes a mechanism for managing a default currency across the application, facilitated by the `DefaultCurrency` class. This enhancement allows for seamless conversion of financial records to a user-specified default currency. + +* `convertAmount(double amount, Currency fromCurrency, Currency toCurrency)`: + This method converts an amount from one currency to another using exchange rates stored in the `exchangeRates` map. + It takes the original amount, the currency of the original amount (`fromCurrency`), + and the target currency (`toCurrency`) as parameters and returns the converted amount. + The method ensures that exchange rates are available for both currencies and that they are positive numbers. + +* `convertExpenseCurrency(Currency newCurrency, ExpenseList expenses)`: + This method converts the currency of expenses in a given `ExpenseList` to a specified new currency (`newCurrency`). + It iterates through the expenses in the list, converts each expense amount to the new currency + using the `convertAmount` method, and updates the expense amounts and currencies accordingly. + +* `convertSavingCurrency(Currency newCurrency, SavingList savings)`: Similar to `convertExpenseCurrency`, + this method converts the currency of savings in a given `SavingList` to a specified new currency (`newCurrency`). + It iterates through the savings in the list, converts each saving amount to the new currency using the `convertAmount` + method, and updates the saving amounts and currencies accordingly. + + +* `convertBudgetCurrency(Currency newCurrency, ExpenseList expenseList)`: + This method is responsible for converting the currency of all budgets within `ExpenseList` to a specified new currency (`newCurrency`). It + accepts the new `Currency` object representing the target currency and the `ExpenseList` containing the budgets, and updates + the budget amounts and currencies accordingly. + + +* `convertRecurringExpensesCurrency(Currency newCurrency, RecurringExpenseLists recurringExpenseLists)` + This Method converts the currency of expenses of each `ExpenseList` within `recurringExpenseLists` by continuously calling the method `convertExpenseCurrency` for each `ExpenseList`. + + +The `DefaultCurrency` class is designed to maintain and update the application-wide default currency setting. It provides static methods to get and set the default currency: + +| Method | Return Type | Relevance | +|----------------------|-------------|--------------------------------------------------------------| +| getDefaultCurrency | Currency | Retrieves the current default currency for the application | +| setDefaultCurrency | void | Updates the default currency to a new value | + + +Here's the step-by-step process when the user uses the Currency Converter feature: +1. The user inputs `change currency [newCurrencyCode]`. `Parser` processes this input and constructs a `ChangeCurrencyCommand` object with the necessary attributes. +2. The `ChangeCurrencyCommand` object is returned to `BudgetBuddy`, which calls `ChangeCurrencyCommand.execute()`. +3. `execute()` invokes `CurrencyConverter.convertExpenseCurrency(newCurrency, expenseList)` and `CurrencyConverter.convertSavingCurrency(newCurrency, savingList)`. +4. Within the `convertExpenseCurrency` and `convertSavingCurrency` call, the amounts of `Expense`, `Saving` or `Budget` objects are converted to the `newCurrency` using the `convertAmount` method. +5. The `DefaultCurrency.setDefaultCurrency(newCurrency)` method is called to update the application's default currency setting to `newCurrency`. +6. The `setAmount` and `setCurrency` methods of `ExpenseList` and `SavingList` are used to update the amounts and currency codes. +7. After successful conversion of savings, expenses and budgets, the default currency of the application is updated, reflecting the new choice across BudgetBuddy. + + +#### Sequence Diagram + +The sequence diagram would be segmented into the different features that utilises the CurrencyConverter class. + +Main Sequence Diagram before Execution: +![Sequence diagram for CurrencyConverter Feature](diagrams/CurrencyConverter_SequenceDiagram.png) + +Upon execution, the following respective conversion functions will run: + + +Sequence Diagram for convertExpenseCurrency(): +![Sequence diagram for convertExpenseCurrency method](diagrams/convertExpenseCurrency_SequenceDiagram.png) + + +Sequence Diagram for convertSavingCurrency(): +![Sequence diagram for convertSavingCurrency](diagrams/convertSavingCurrency_SequenceDiagram.png) + + +Sequence Diagram for convertRecurringExpensesCurrency(): + +![Sequence diagram for convertRecurringExpensesCurrency](diagrams/CurrencyConverter_RecurringExpenses_SequenceDiagram.png) + + +Sequence Diagram for convertSplittedExpenseCurrency(): +![Sequence diagram for convertSplittedExpenseCurrency](diagrams/sequenceDiagram_CurrencyConverter_SplitExpense.png) + + +Sequence Diagram for convertBudgetCurrency(): +![Sequence diagram for budgetCurrencyConverter](diagrams/budgetCurrencyConverter.png) + + + +### 4.16 Setting Budget Feature +The Budget Management feature allows users to set financial limits for the various categories and monitor their spending. +This feature's objective is to give users the ability to stay within their financial goals and avoid overspending. + +This feature is orchestrated by `ListBudgetCommand` and `SetBudgetCommand`, which are initialised by the `Parser` +class. Below is a description of the key class attributes and methods involved in the budget setting and listing +process: + +##### Class Attributes for `SetBudgetCommand`: + + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|---------------------------------------------------------------------| +| expenseList | ExpenseList | Object containing the list of expenses to check against set budgets | +| category | String | The category for which the budget is being set | +| budget | double | The budget amount to be set for the category | + +The UML Sequence diagram below illustrates the execution flow of the Set Budget Feature when a user inputs a valid +command to set a budget: + +![sequenceDiagram_setBudget.jpg](diagrams/sequenceDiagram_SetBudget.jpg) + +The sequence of operations for an example input, `set budget c/Transport b/500`, is as follows: +1. BudgetBuddy receives the user input and utilizes the Parser to decipher it. +2. The Parser identifies the key components of the input (category and budget) and constructs a SetBudgetCommand object with the identified category (Transport) and budget (500). +3. The Parser then hands over the SetBudgetCommand object to BudgetBuddy. +4. BudgetBuddy invokes the execute() method on the SetBudgetCommand object. +5. The SetBudgetCommand object calls the setBudget() method on the ExpenseList, passing in the category and budget amount. +6. The ExpenseList updates or creates a budget allocation for the specified category with the provided amount. +7. A confirmation message is displayed in the console indicating the budget has been successfully set or updated. + +##### Class Attributes for `ListBudgetCommand`: + + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|---------------------------------------------------------------------| +| expenseList | ExpenseList | Object containing the list of expenses to check against set budgets | + +The UML Sequence diagram below illustrates the execution flow of the Set Budget Feature when a user inputs a valid +command to list budgets: + +![sequenceDiagram_listBudget.png](diagrams/sequenceDiagram_ListBudget.png) + +Upon the call of the `execute()` method in `BudgetBuddy` using `command.execute()`, `SetBudgetCommand` will update the +budget in `ExpenseList` using `setBudget`. Similarly, `ListBudgetCommand` will fetch and display all categories with +their budgets using `getBudgets`, and highlight those that are above the set budget. + +##### Key Methods used from `ExpenseList` + + +| Method | Return Type | Relevance | +|-----------------------------|---------------|--------------------------------------------------------------------| +| setBudget(category, budget) | void | Sets or updates the budget for a given category in the ExpenseList | +| getBudgets() | List | Retrieves the list of all budgets set | + +The `ListBudgetCommand`'s updated execution function now features an improved display that not only shows the budget, +spent amount, and remaining balance but also clearly indicates when the budget has been exceeded. If the expenses +surpass the budget, instead of showing a negative remaining balance, it displays "Exceeded", providing a straightforward +and immediate visual cue that the budget limits have been surpassed. + +The "Categories above budget" section offers a concise table summarizing which categories have gone over the budget and +by what amount, making it easy for users to identify areas of concern. + + +### 4.17 Get Expense Insights Feature + +The Get Expense Insights feature allows users to analyze their spending patterns and understand where their money goes. +This feature is managed by the `GetExpenseInsightsCommand` class, which is initialized by the `Parser` class. +The `GetExpenseInsightsCommand` holds an `ExpenseList` object which contains all expenses added by the user. +The relevance of this Class Attribute in `GetExpenseInsightsCommand` is as follows: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|----------------------------------------------------------------------| +| expenseList | ExpenseList | ExpenseList object containing the list of expenses to be analyzed | + +Upon invocation of the `execute()` method in `BudgetBuddy`, the `GetExpenseInsightsCommand` leverages methods from the +`ExpenseList` class to calculate and display spending insights. + +| Method | Return Type | Relevance | +|-------------------|-------------|----------------------------------------------------------------------------------------------| +| getExpenseInsights| void | Analyzes expenses and prints insights on spending distribution, highest and lowest spending | + +The following UML Sequence diagram illustrates the execution process of the Get Expenses Insights Command when a user enters a valid command: + +![getExpenseInsightsDiagram.drawio.png](diagrams%2FgetExpenseInsightsDiagram.drawio.png) + +Here's a step-by-step explanation of the processes that occur when a user invokes the Get Expense Insights feature: + +1. The BudgetBuddy application receives the command `get expenses insights` and passes it to the `Parser`. +2. The `Parser` interprets the input and creates a new `GetExpenseInsightsCommand` object with the `ExpenseList`. +3. The `BudgetBuddy` application then calls `execute()` on the `GetExpenseInsightsCommand` object. +4. The `GetExpenseInsightsCommand` object calls the `getExpenseInsights` method on the `ExpenseList`. +5. The `ExpenseList` analyzes the expenses, calculating total spendings, average amount, and categorizing the expenses. +6. Insights such as the categories with the highest and lowest spending are then printed to the user. + + +### 4.18 Get Savings Insights Feature + +The Get Savings Insights feature enables users to analyze their savings distribution across various categories and +understand their saving habits. This feature is facilitated by the `GetSavingsInsightsCommand` class, which is +instantiated by the `Parser` class. In this class, a `SavingList` object is maintained, which contains all the savings +added by the user. The significance of the class attribute in `GetSavingsInsightsCommand` is as detailed below: + +| Class Attribute | Variable Type | Relevance | +|-----------------|---------------|---------------------------------------------------------------------| +| savingList | SavingList | SavingList object containing the list of savings to be scrutinized. | + +When the `execute()` method in `BudgetBuddy` is invoked via `command.execute()`, the `GetSavingsInsightsCommand` +leverages methods from the `SavingList` class to calculate and exhibit insights about savings. + +| Method | Return Type | Relevance | +|----------------------|-------------|----------------------------------------------------------------------------------| +| getSavingsInsights() | void | Analyzes savings and displays insights on savings distribution, highest and lowest savings, etc. | + +The following UML Sequence diagram illustrates the execution process of the Get Savings Insights Command when a user enters a valid command: + +![getSavingsInsightsDiagram.drawio.png](diagrams%2FgetSavingsInsightsDiagram.drawio.png) + +The sequential flow of execution when a user commands to get savings insights is as follows: + +1. The user inputs the command 'get savings insights' and `BudgetBuddy` captures it. +2. `BudgetBuddy` employs `Parser` to decode the input. +3. `Parser` constructs a new `GetSavingsInsightsCommand` object with the `SavingList`. +4. `Parser` sends this `GetSavingsInsightsCommand` object back to `BudgetBuddy`. +5. `BudgetBuddy` calls the `execute()` method on the `GetSavingsInsightsCommand` object. +6. `GetSavingsInsightsCommand` invokes the `getSavingsInsights()` method from the `SavingList`. +7. `SavingList` computes and prints the insights, such as the categories with the highest and lowest savings and the overall distribution. +8. The insights are shown to the user. + + +## 5. Documentation + +The following section describes how documentation for the project was written. Documentation Format follows GitHub-Flavoured Markdown. + +### 5.1 Documentation Style +- We followed the style similar to the example provided [here](https://se-education.org/addressbook-level3/DeveloperGuide.html). + +### 5.2 Diagrams +- We use [Draw.io](https://draw.io/) for our diagrams, exported as PNG with light theme. + +### 5.3 PDF Conversion +- We use **Chrome** for converting documentations to PDF format as per recommendations [here](https://se-education.org/guides/tutorials/savingPdf.html). + +## 6. Testing + +The following section describes the testing methodologies followed in this project to ensure the project is of the highest standard and as bug-free as possible. + +### 6.1 Running Tests +JUnit tests have been added to the project, which can be found under `src/test`. These JUnit tests aid in testing the respective commands and features against +both valid and invalid inputs. To run these tests, on `IntelliJ IDE`, simply +`right-click` the `test` folder followed by `More Run/Debug` -> `Run Tests with Coverage`. This would run all the pre-defined tests, and also display the +coverage for each file of the main application. + +### 6.2 Logger +A Global Logger is utilized in certain methods and features which are more prone to errors, etc., methods that may potentially deal with invalid inputs. In the releases, this Logger is disabled using the command +`Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(Level.OFF);` before `run()` is called in `BudgetBuddy#main()` . However, in the code files, the Logger is still enabled and aids in tracing the code when testing for errors. + +## Appendix A: Product scope + +### Target user profile +This product is for users who can type fast, and wishes to handle and track their current and future +expenses on a singular platform. ### Value proposition +BudgetBuddy is faster and more efficient way to track and calculate current and future expenses if a user is able to +type fast. It also provides the ability to deal with finances on a singular platform. -{Describe the value proposition: what problem does it solve?} +## Appendix B: User Stories ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|-------------------|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| v1.0 | user | be able to view my expenses | track my prior expenditures and plan future expenses accordingly | +| v1.0 | user | be able to view my savings | plan my budget accordingly | +| v1.0 | user | be able to view my expenses by their relevant categories | control my spending | +| v1.0 | user | be able to identify my largest savings category | allocate necessary saved funds | +| v1.0 | user | add expenses | track my spending | +| v1.0 | user | Categorise my expenses | manage my finances more efficiently | +| v1.0 | user | Edit or delete expenses | remove any incorrectly added items | +| v1.0 | user | allocate saved funds | know how much I will have left after expenses | +| v1.0 | user | be able to find expenses by description | know the expenses i have that is associated with the description | +| v1.0 | user | be able to find expenses more than a certain amount | know what my deemed larger expenses are | +| v1.0 | user | be able to find expenses less than a certain amount | know what my deemed lower expenses are | +| v1.0 | User | See what commands i can use | I know how to use the application | +| v2.0 | user | Plan my budget | Avoid overspending | +| v2.0 | frequent traveler | log my expenses in multiple currencies | accurately track my expenses across different countries | +| v2.0 | user | add multiple expenses at once | Add common expenditures i have monthly at one shot | +| v2.0 | user | have multiple lists of recurring expenses | separate associated recurring expenses together | +| v2.0 | user | view what expenses i have in each of my recurring expenses list | know what expenses i have put into each list | +| v2.0 | user | remove a list from my recurring expenses list | remove underutilized lists or wrongly added lists | +| v2.0 | user | save my expenses | make sure i do not have to retype all expenses again after closing the application | +| v2.0 | user | load my expenses | i can access previously added expenses when i reopen the application | +| v2.0 | user | save my expenses in my recurring expenses | make sure i do not have to retype all expenses again after closing the application | +| v2.0 | user | load my expenses in my recurring expenses | i can access previously added expenses in my recurring expenses when i reopen the application | +| v2.0 | user | divide bills that are meant for splitting | know how much others should pay me | +| v2.0 | user | settle bills that others have repaid me | see which bills have not been settled | +| v2.0 | user | view my expenses in a graphical representation | to analyse my highest and lowest expense categories | +| v2.0 | user | view my savings in a graphical representation | to analyse my highest and lowest saving categories | + + +## Appendix C: Use Cases +(For all use cases below, the System is `BudgetBuddy` and the Actor is the `user`, unless specified otherwise). + + +### Use Case: Listing Savings + +1. User requests to list savings. +2. BudgetBuddy retrieves the stored savings and expenses. +3. BudgetBuddy calculates the remaining savings left. +4. BudgetBuddy displays the existing savings along with the initial savings amount and remaining savings left. + +#### Extensions +* 1.1 User requests to list savings by a specific category. + * 1.1.1 BudgetBuddy retrieves the stored savings and expenses. + * 1.1.2 BudgetBuddy calculates remaining savings left. + * 1.1.3 BudgetBuddy displays only the existing savings with the filtered category. + * 1.1.4 BudgetBuddy displays the overall initial savings and remaining savings left.
+ Use case ends +* 1.2 The user entered an invalid category. + * 1.2.1 BudgetBuddy shows an error message.
+ Use case ends +* 2.1 BudgetBuddy retrieves an empty savings list but existing expenses list. + * 2.1.1 BudgetBuddy calculates savings required to pay off expenses. + * 2.1.2 BudgetBuddy displays savings user is short of.
+ Use case ends +* 2.2 BudgetBuddy retrieves an existing savings list but an empty expenses list. + * 2.2.1 BudgetBuddy displays all existing entries in savings list. + * 2.2.2 BudgetBuddy still calculates remaining savings left, with 0 expenses deducted. + * 2.2.3 BudgetBuddy displays overall initial savings and remaining savings left.
+ Use case ends +* 2.3 BudgetBuddy retrieves both empty savings and expenses list. + * 2.3.1 BudgetBuddy displays an empty list for both savings and expenses.
+ Use case ends + + + +### Use Case: Listing Expenses + +1. User requests to list expenses. +2. BudgetBuddy retrieves stored expenses. +3. BudgetBuddy calculates total overall expenses. +4. BudgetBuddy displays existing expenses along with the overall total expenses. + +#### Extensions + +* 1.1 User requests to list expenses by a specific category. + * 1.1.1 BudgetBuddy retrieves stored expenses. + * 1.1.2 BudgetBuddy calculates total overall expenses. + * 1.1.3 BudgetBuddy displays only the existing expenses with the filtered category, along with overall total expenses.
+ Use case ends +* 1.2 User entered an invalid category. + * 1.2.1 BudgetBuddy shows an error message.
+ Use case ends +* 2.1 BudgetBuddy retrieves an empty expense list. + * 2.1.1 BudgetBuddy displays an empty expense list.
+ Use case ends + + +### Use Case: Edit Savings + +1. User requests to edit a savings entry by specifying the category of the saving. +2. BudgetBuddy prompts the user for the category and amount. +3. BudgetBuddy validates the provided category and updates the savings entry if the category is valid. +4. BudgetBuddy displays a confirmation message indicating the savings entry has been updated. + +#### Extensions + +* 1.1 User specifies an category that does not exist. + * 1.1.1 BudgetBuddy displays an error message indicating the category is invalid. + Use case ends. + +* 1.2 User enters an invalid or non-numeric amount. + * 1.2.1 BudgetBuddy shows an error message and prompts the user to enter a valid numerical amount. + Use case ends. + +* 1.3 User attempts to update savings with a negative amount. + * 1.3.1 BudgetBuddy displays an error message indicating the savings amount must be positive. + Use case ends. + + +### Use Case: Edit Expenses + +1. User requests to edit an expense entry by specifying the index of the expense and the details to be updated. +2. BudgetBuddy prompts the user for the category, amount, and description for the expense. +3. BudgetBuddy checks if the expense index provided is valid. +4. If valid, BudgetBuddy updates the expense entry with the new details. +5. BudgetBuddy displays a confirmation message indicating the expense entry has been updated. + +#### Extensions + +* 1.1 User specifies an index that does not exist in the expense list. + * 1.1.1 BudgetBuddy displays an error message indicating the index is out of bounds. + Use case ends. + +* 1.2 User enters an invalid or non-numeric amount for the expense. + * 1.2.1 BudgetBuddy shows an error message and prompts the user to enter a valid numerical amount. + Use case ends. + +* 1.3 User enters a negative number for the expense amount. + * 1.3.1 BudgetBuddy displays an error message indicating the expense amount must be positive. + Use case ends. + + +### Use Case: Currency Converter + +1. User requests to change currency. +2. BudgetBuddy converts existing amounts in lists to new currency. +3. BudgetBuddy sets default currency to the new changed currency. +4. BudgetBuddy displays currency changed. + +#### Extensions +* 1.1 User inputs an invalid currency code. + * 1.1.1 BudgetBuddy shows an error message.
+ Use case ends +* 1.2 User inputs the same currency code. + * 1.2.1 BudgetBuddy notifies user of the same conversion.
+ Use case ends + + +### Use Case: Delete expenses + +1. User requests to delete a specific expense by specifying the index +2. BudgetBuddy retrieves the specified expense from the stored expenses list. +3. BudgetBuddy deletes the specified expense. +4. BudgetBuddy displays a confirmation message indicating the expense has been deleted. + +#### Extensions +* 1.1 User specifies an invalid or out-of-bounds index + * 1.1.1 BudgetBuddy shows an error message and prompts the user to enter a valid index.
+ Use case ends. +* 2.1 BudgetBuddy retrieves an empty expense list + * 2.1.1 BudgetBuddy displays an error message indicating there are no expenses to delete.
+ Use case ends. + + +### Use Case: Reduce Savings + +1. User requests to reduce savings by specifying a category and amount. +2. BudgetBuddy retrieves savings associated with the specified category. +3. BudgetBuddy reduces the savings by the specified amount. +4. BudgetBuddy displays a confirmation message indicating the savings have been reduced. + +#### Extensions +* 1.1 User specifies a category not present in the savings list. + * 1.1.1 BudgetBuddy shows an error message indicating the category does not exist.
+ Use case ends. +* 1.2 User specifies an amount that exceeds the available savings in the category. + * 1.2.1 BudgetBuddy shows an error message indicating insufficient savings for the reduction.
+ Use case ends. +* 2.1 BudgetBuddy retrieves an empty savings list. + * 2.1.1 BudgetBuddy displays an error message indicating there are no savings to reduce.
+ Use case ends. + + +### Use Case: Listing Budget + +1. User requests to list budgets. +2. BudgetBuddy retrieves all set budgets along with their associated categories. +3. BudgetBuddy displays each category with its corresponding budget limit. +4. BudgetBuddy also displays the total of all budgets combined. + +#### Extensions +* 2.1 BudgetBuddy retrieves an empty budget list. + * 2.1.1 BudgetBuddy displays a message indicating no budgets have been set.
+ Use case ends. + + +### Use Case: Setting Budget + +1. User requests to set a budget for a specific category by specifying the category and the budget amount. +2. BudgetBuddy checks if the category exists; if not, it adds the category. +3. BudgetBuddy sets or updates the budget for the specified category. +4. BudgetBuddy displays a confirmation message indicating the budget has been set or updated. + +#### Extensions +* 1.1 User specifies an invalid or non-numeric budget amount. + * 1.1.1 BudgetBuddy shows an error message and prompts the user to enter a valid numerical amount.
+ Use case ends. +* 1.2 User sets a budget amount to zero or a negative number. + * 1.2.1 BudgetBuddy shows an error message indicating the budget amount must be positive.
+ Use case ends. + + +### Use Case : Add a Recurring Expense List +1. User requests to add a recurring expense list with a specific name +2. BudgetBuddy creates a recurring expense list with the specified name + 3. use case ends. + +#### Extensions +* 1a. Name is Empty + * 1a1. BudgetBuddy shows an error message + * use case ends + + +### Use Case : List all recurring expense lists +1. User requests to list all recurring expense lists +2. BudgetBuddy shows all lists of recurring expense list. + 3. use case ends + +#### Extensions +* 1a. The list of all recurring expense lists is empty + * 1a1. BudgetBuddy states that no recurring expense lists has been added yet + * use case ends + + +### Use Case : Remove a recurring expense list +1. User requests to list all recurring expense lists +2. BudgetBuddy shows all lists of recurring expense list +3. User Requests to delete a specific list +4. BudgetBuddy deletes the list + 5. use case ends + +#### Extensions +* 2a. The list is empty + * use case ends + + +* 3a. The given index is invalid + * 3a.1 BudgetBuddy shows an error message + * use case resumes at step 2 + + +### Use Case : Add an expense to a Recurring Expense List +1. User requests to list all recurring expense lists +2. BudgetBuddy shows all lists of recurring expense list. +3. User requests to add an expense to a specific list +4. BudgetBuddy adds the expense to the list + 5. use case ends + +#### Extensions +* 2a. The list is empty + * use case ends + +* 3a. The given index is invalid + * 3a1. BudgetBuddy shows an error message + * use case resumes at step 2 + +* 3b. The given category is invalid + * 3b1. BudgetBuddy shows an error message + * use case resumes at step 2 + + +* 3c. The given amount is invalid + * 3c1. BudgetBuddy shows an error message + * use case resumes at step 2 + +* 3d. The given description is invalid + * 3d1. BudgetBuddy shows an error message + * use case resumes at step 2 + + +### Use Case : List all expenses in a recurring expense list +1. User requests to list all recurring expense lists +2. BudgetBuddy shows all lists of recurring expense list +3. User requests to view all expenses in a specific list +4. BudgetBuddy shows all expenses in the specific list + 5. use case ends + +#### Extensions +* 2a. The list is empty + * use case ends + +* 3a. The index is invalid + * 3a1. BudgetBuddy shows an error message + * use case resumes at step 2 + +* 3b. The list at index is empty + * 3b1. BudgetBuddy shows no expenses + * use case ends + + +### Use Case : Add all expenses in a recurring expense list to the overall expenses +1. User requests to list all recurring expense lists +2. BudgetBuddy shows all lists of recurring expense list +3. User requests to add all expenses in a specific list to the overall expenses +4. BudgetBuddy adds all expenses in the specific list to the overall expenses + 5. use case ends + +#### Extensions + +* 2a. The list is empty + * use case ends + +* 3a. The index is invalid + * 3a1. BudgetBuddy shows an error message + * use case resumes at step 2 + +* 3b. The list at index is empty + * 3b1. BuddyBuddy shows message stating nothing is added to overall expenses + * use case ends + + + +### Use Case: Get Expenses Insights + +1. User requests to get insights into their expenses. +2. BudgetBuddy retrieves all expenses from the ExpenseList. +3. BudgetBuddy calculates and displays insights, including highest and lowest expense categories, and categories with no expenses. +4. BudgetBuddy displays a visual representation of expense distribution across different categories. + +#### Extensions + +* 1.1 ExpenseList is empty. + * 1.1.1 BudgetBuddy displays a message indicating no expense data is available to analyze. + Use case ends. + + +### Use Case: Get Savings Insights + +1. User requests to get insights into their savings. +2. BudgetBuddy retrieves all savings from the SavingList. +3. BudgetBuddy calculates and displays insights, such as highest and lowest savings categories, and categories with no savings. +4. BudgetBuddy displays a visual representation of savings distribution across different categories. + +#### Extensions + +* 1.1 SavingList is empty. + * 1.1.1 BudgetBuddy displays a message indicating no savings data is available to analyze. + Use case ends. + + +## 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 1000 entries without a noticeable sluggishness in performance for typical usage. +3. A user with above average typing speed for regular English text should be able to accomplish most of the tasks faster using commands than using the mouse. + + +## Appendix E: Glossary + +* **Mainstream OS**: Windows, Linux, macOS. +* **Recurring Expenses**: A set of expenses which can be added to the overall expenses at any given point in time +* **Overall Expenses**: Refers to the overall expense list. Etc, the expense list which expenses get added to when performing an add expense command. + +## Appendix F: Instructions for manual testing + +### 1. Launch and Shutdown +* 1.1 Initial Launch + * Download the `jar` file and copy into an empty folder. + * Navigate to the `jar` file via a Terminal/PowerShell window. + * Start the `jar` file with the following command: `java -jar BudgetBuddy.jar` + * Expected: Command Line Interface should launch with the Menu being shown. + +### 2. Test Cases + + +#### 2.1 Displaying Commands +1. Test Case : `menu` + Expected : Prints all possible menu items in the command line interface +2. Test Case : `menu 1` + Expected : Prints all commands related to Manage Expenses` in the command line interface +3. Test Case : `menu string` + Expected : An error message is printed in the command line interface +4. Test Case : `menu 999` + Expected : An error message is printed in the command line interface + + +#### 2.2 Adding Expenses + +* 2.2.1 Adding an Expense + * Prerequisites: None. + * Test Case: `add expense c/Transport a/50 d/Bus fare` + * Expected: Adds an expense with category `Transport`, amount $`50`, and description `Bus fare`. Confirmation message will be printed in the command line interface. +* 2.2.2 Adding an Expense with Incomplete Information + * Prerequisites: None. + * Test Case: `add expense c/Transport a/-50 d/Bus Fare` + * Expected: Error message due to negative number input. Command line interface will instruct on correct format. +* 2.2.3 Adding an Expense with Invalid Amount + * Prerequisites: None. + * Test Case: add `expense c/Transport a/abc d/Bus Fare` + * Expected: Error message due to invalid amount format. Command line interface will instruct on correct format. +* 2.2.4 Adding a category that is not listed in the category + * Prerequisites: None. + * Test Case: `add expense c/abc a/50 d/Bus fare` + * Expected: Error message due to invalid category. Command line interface will instruct on correct format. + +#### 2.3 Adding Savings + +* 2.3.1 Adding Valid Savings + * Test Case ID: addSaving_validInput_success + * Description: Tests adding a valid saving entry to the SavingList. + * Method: `addSaving(String category, String amount)` + * Input: `Salary`, `500` + * Expected Outcome: The savings list size should be `1`. The category of the saved entry should be `Salary`. The amount of the saved entry should be `500`. + +* 2.3.2 Adding Saving with Invalid Amount Format + * Test Case ID: addSaving_invalidAmount_exceptionThrown + * Description: Tests adding a saving with a non-numeric amount. + * Method: `addSaving(String category, String amount)` + * Input: `Salary`, `abc` + * Expected Outcome: A BudgetBuddyException is thrown with the message `Invalid amount format. Amount should be a positive number with up to maximum two decimal places.` + +* 2.3.3 Adding Saving with Negative Amount + * Test Case ID: addSaving_negativeAmount_exceptionThrown + * Description: Tests adding a saving with a negative amount. + * Method: `addSaving(String category, String amount)` + * Input: `Salary`, `-1.00` + * Expected Outcome: A BudgetBuddyException is thrown with the message `Invalid amount format. Amount should be a positive number with up to maximum two decimal places.` + +* 2.3.4 Adding Saving with Non-Listed Category + * Test Case ID: addSaving_nullCategory_exceptionThrown + * Description: Tests adding a saving with a category that is not listed in the predefined categories. + * Method: `addSaving(String category, String amount)` + * Input: `abc`, `500` + * Expected Outcome: A BudgetBuddyException is thrown with the message `The category 'abc' is not listed.` + +#### 2.4 Add Split Expenses + +* 2.4.1 Adding a Valid Split Expense + * Test Case ID: addSplitExpense_addingsplitexpense_success + * Description: Tests adding a valid split expense entry to the `SplitExpenseList`. + * Method: `addSplitExpense(String amount, String numberOfPeople, String description)` + * Input: `12`, `12`, `Lunch` + * Expected Outcome: The split expenses list size should be `1`. The number of people for the split expense should be `12`. The description of the split expense should be `Lunch` + +* 2.4.2 Adding Split Expense with Invalid Amount Format + * Test Case ID: addSplitExpense_invalidAmount_exceptionThrown + * Description: Tests adding a split expense with a non-numeric amount. + * Method: `addSplitExpense(String amount, String numberOfPeople, String description)` + * Input: `abc`, `12`, `Lunch` + * Expected Outcome: A BudgetBuddyException is thrown with the message `Invalid amount format. Amount should be a number.` + +* 2.4.3 Adding Split Expense with Invalid Number of People Format + * Test Case ID: addSplitExpense_invalidNumberOfPeople_exceptionThrown + * Description: Tests adding a split expense with a non-numeric number of people. + * Method: `addSplitExpense(String amount, String numberOfPeople, String description)` + * Input: `12`, `abc`, `Lunch` + * Expected Outcome: A BudgetBuddyException is thrown with the message `Number of people should be a number.` + +* 2.4.4 Adding Split Expense with Negative Amount + * Test Case ID: addSplitExpense_negativeAmount_exceptionThrown + * Description: Tests adding a split expense with a negative amount. + * Method: `addSplitExpense(String amount, String numberOfPeople, String description)` + * Input: `-12`, `12`, `Lunch` + * Expected Outcome: A BudgetBuddyException is thrown with the message `Expenses should not be negative.` + +* 2.4.5 Adding Split Expense with Negative Number of People + * Test Case ID: addSplitExpense_negativeNumberOfPeople_exceptionThrown + * Description: Tests adding a split expense with a negative number of people. + * Method: addSplitExpense(String amount, String numberOfPeople, String description) + * Input: "12", "-12", "Lunch" + * Expected Outcome: A BudgetBuddyException is thrown with the message "Number of people should be a positive number." + +#### 2.5 Edit Savings +**Prerequisites** : Some savings has been added to the overall savings. +1. Test Case : `edit savings c/Salary a/2000` +Expected : Edits the saving with category "Salary". If there is no saving with this category, an error message stating invalid category will be printed. +2. Test Case : `edit savings c/Allowance a/2000` +Expected : An error message mentioning invalid saving category will be printed. +3. Test Case : `edit savings c/Salary a/-2000` +Expected : An error message mentioning invalid amount will be printed. + +#### 2.6 Edit Expenses +**Prerequisites** : Some savings has been added to the overall savings. +1. Test Case : `edit expense c/Transport i/2 a/2000 d/GRAB` + Expected : if there is an expense with index 2, it edits the expense at index 2. Else, an error message stating invalid index will be printed. +2. Test Case : `edit expense c/MRT i/2 a/2 d/work` + Expected : An error message mentioning invalid saving category will be printed. +3. Test Case : `edit savings c/Entertainment i/2 a/-2000` + Expected : An error message mentioning invalid amount will be printed. + + +#### 2.7 Reducing savings + +1. Test case: `reduce savings c/Salary a/100` + Expected: The savings under 'Salary' are reduced by $100, and a confirmation message is displayed. + **Prerequisites** : No savings under the category 'Investments' exist. +2. Test case: `reduce savings c/Investments a/100` + Expected: An error message is displayed indicating no savings found under the category 'Investments'. + **Prerequisites** : Savings under the category 'Salary' exist but are less than $500 +3. Test case: `reduce savings c/Salary a/500` + Expected: An error message is displayed indicating insufficient amount in 'Salary' to reduce by $500. + + +#### 2.8 Deleting an expense + +1. Test case: `delete expense i/1` + Expected: The first expense in the list, if any, is deleted and a confirmation message is displayed. +2. Test case: `delete expense i/999` + Expected: An error message is displayed stating that the index is out of bounds + + +#### 2.9 Listing Savings + +* 2.9.1 Listing Overall Savings + * Prerequisites: There must be existing savings and expenses in the list. + * Test Case: `list savings` + * Expected: All existing savings will be printed, along with the initial amount and remaining amount after deducting expenses if necessary. + +* 2.9.2 Listing Savings by a specific category + * Prerequisites: There must be existing savings of `Salary` category and expenses in the list. + * Test Case: `list savings Salary` + * Expected: Savings that have the `Salary` category will be printed, along with the overall remaining savings deducting expenses. + +#### 2.10 Listing Expenses + +* 2.10.1 Listing Overall Expenses + * Prerequisites: There must be existing expenses in the list. + * Test Case: `list expenses` + * Expected: All existing expenses will be printed, along with the overall amount. + +* 2.10.2 Listing Expenses by a specific category + * Prerequisites: there must be existing expenses of `Transport` category in the list. + * Test Case: `list expenses Transport` + * Expected: Expenses relating to the `Transport` category will be printed, along with the overall amount. + + +#### 2.11 Finding an expense +**Prerequisites** : Some expenses has been added to the overall expense. +1. Test Case : `find expenses d/cat morethan/ lessthan/` +Expected : If there are expenses matching/containing "cat", the found expenses are printed. Else, message stating no matching expenses found is printed in command line interface +2. Test Case : `find expenses d/cat morethan/20 lessthan/` +Expected : If there are expenses matching/containing "cat" and is more than 20, the found expenses are printed. Else, message stating no matching expenses found is printed in command line interface +3. Test Case : `find expenses d/cat morethan/string lessthan` +Expected : An error message is printed in the command line interface + + + +#### 2.12 Creating a new list of recurring expenses +1. Test Case : `rec newlist streaming` +Expected : A new list created called `streaming` +2. Test Case : `rec newlist ` +Expected : An error message will be printed in the command line interface +3. Test Case : `rec newlist |` +Expected : An error message will be printed in the command line interface + + +#### 2.13 Listing all lists of recurring expenses +1. Test Case : `rec viewlists`, with already added lists +Expected : All lists of recurring expenses will be printed in the command line interface +2. Test Case : `rec viewlists`, with no added lists +Expected : Message stated there being no recurring expenses is printed in the command line interface +3. Test Case : `rec viewlists extra` +Expected : `viewlists` should still work as intended, with no exceptions being thrown + + +#### 2.14 Removing a list of recurring expenses +1. Test Case : `rec removelist 1`, with a list being present at the list number `1` during `rec viewlists` +Expected : List located at list number 1 will be removed, and a success message is printed in the command line interface +2. Test Case : `rec removelist string` +Expected : Error message will be printed in the command line interface, along with the proper command format +3. Test Case : `rec removelist -1` +Expected : Error message will be printed in the command line interface +4. Test Case : `rec removelist ` +Expected : Error message will be printed in the command line interface + + +#### 2.15 Adding an expense into a list of recurring expenses +1. Test Case : `rec newexpense to/1 c/Entertainment a/200 d/description`, with a list being present at list number `1` +Expected : Expense with details Entertainment, 200, description will be added to list at list number `1` +2. Test Case : `rec newexpense to/1` +Expected : Error message will be printed in the command line interface +3. Test Case : `rec newexpense to/string c/Entertainment a/200 d/description` +Expected : Error message will be printed in the command line interface + + +#### 2.16 Viewing all expenses in a list of recurring expenses +1. Test Case : `rec viewexpenses 1`, with a list being present at list number `1` and contains expenses inside +Expected : Prints all expenses present in the recurring expense list 1 +2. Test Case : `rec viewexpenses 1` with a list not being present +Expected : Error message will be printed in the command line interface +3. Test Case : `rec viewexpenses 1` with a list being present at list number `1`, but does not contain any expenses inside +Expected : Prints an empty set of expenses to command line interface, with expenses at $0 + + +#### 2.17 Adding all expenses in a list of recurring expenses to the overall expenses +1. Test Case : `rec addrec 1`, with a list being present at list number `1` and contains expense inside +Expected : Adds all expenses present in recurring expense list 1 to the overall expenses +2. Test Case : `rec addrec 1`, with a list being present a list number `1` but does not contain any expenses inside +Expected : A message is provided in the command line interface informing the user that nothing has been added +3. Test Case : `rec addrec 1`, with a list not being present at list number `1` +Expected : Error message will be printed in the command line interface + + +#### 2.18 Loading recurring expenses +**Prerequisite** : The `RecurringExpensesFile.txt` should be empty prior to each Test Case +1. Test Case : Add a line in `RecurringExpensesFile.txt` called `!!! newlist !!!` +Expected : A recurring expense list named `newlist` will be present when doing a `rec viewlists` +2. Test Case : Add an invalid line in `RecurringExpensesFile.txt` called `!!! new!!!list !!!` +Expected : Error is printed in the CLI, RecurringExpensesFile will be reset to an empty file +3. Test Case : Add a line in `RecurringExpensesFile.txt` called `!!! newlist !!!` and another line below it `1 | 2024-04-13 | Entertainment | 203.35 | movies` +Expected : A recurring expense list named `newlist` will be present at list number 1 when doing a `rec viewlists` and an expense with the above description is present when doing a `rec viewexpenses 1` + + +#### 2.19 Saving recurring expenses +1. Test Case : `rec newlist streaming services` followed by a `bye` +Expected : The `RecurringExpensesFile.txt` should now contain a `!!! streaming services !!!`. The list will also still be present after Relaunching application. +2. Test Case : `rec newlist streaming services` followed by a `rec newexpense to/1 c/Entertainment a/200 d/description`, followed by a `bye` +Expected : The recurring list `streaming services` which contains an expense with the description above will still be present after relaunching the application + + +#### 2.20 Changing Currency + +* 2.20.1 Changing Currency + * Prerequisite: There must be existing savings and expenses in the list. + * Test Case: `change currency USD` + * Expected: All existing savings and expenses will be converted to the USD equivalent pricing. + +* 2.20.2 Changing Default Currency + * Prerequisite: There need not be existing savings or expenses in the list. + * Test Case: `change currency USD` + * Expected: Default Currency would be changed to USD. Future amounts added will be in USD. + + + +#### 2.21 Setting budget -## Non-Functional Requirements +1. Test case: `set budget c/Groceries b/200` + Expected: A budget of $200 is set for 'Groceries', and a confirmation message is displayed. +2. Test case: `set budget c/Transport b/-50` + Expected: An error message is displayed indicating the budget cannot be negative. + **Prerequisites** : A budget for 'Transport' exists. +3. Test case: `set budget c/Transport b/300` + Expected: The budget for 'Transport' is updated to $300, and a message confirming the update is displayed. -{Give non-functional requirements} + +#### 2.22 List Budget +**Prerequisites** : Budgets must be set for multiple categories. +1. Test case: `print budget` + Expected: All existing budgets are listed with their respective categories and amounts. + **Prerequisites** : No Budgets are set +2. Test case: `print budget` + Expected: A message is displayed indicating no budgets have been set. -## Glossary + + +#### 2.23 Get Graphical Insights for Expenses +* Prerequisites: There must be existing expenses in the list. +* Test Case: `get expenses insights` +* Expected: Bar graph will be printed for each category. -* *glossary item* - Definition + +#### 2.24 Get Graphical Insights for Savings +* Prerequisites: There must be existing savings in the list. +* Test Case: `get savings insights` +* Expected: Bar graph will be printed for each category. -## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..8f55032bd0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ -# Duke - -{Give product intro here} +# BudgetBuddy +BudgetBuddy is a product for users who wish to handle and track any current/future expenses on a singular platform. +BudgetBuddy provides a faster and more efficient way to track and calculate expenses and provides the ability +to deal with finances on a singular platform with ease as long as you can type fast. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..cf78e38633 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,649 @@ # User Guide -## Introduction +## Table of Contents +[1. Introduction](#1-introduction)
+[2. Quick Start](#2-quick-start)
+[3. Features](#3-features)
+  [3.0 General Information](#30-general-information)
+  [3.1 Display Commands](#31-display-commands--menu)
+  [3.2 Add Expense](#32-add-expense)
+  [3.3 Add Savings](#33-add-savings)
+  [3.4 Add Split Expenses](#34-add-shared-bill)
+  [3.5 Edit Savings](#35-edit-savings-edit-savings)
+  [3.6 Edit Expense](#36-edit-expenses-edit-expense)
+  [3.7 Reduce Savings](#37-reduce-savings-reduce-savings)
+  [3.8 Delete Expense](#38-delete-expense-delete-expense)
+  [3.9 List Savings](#39-listing-savings-list-savings)
+  [3.10 List Expense](#310-listing-expenses-list-expenses)
+  [3.11 Check Splitted Expenses](#311-check-splitted-expenses-check-splitted-expenses)
+  [3.12 Settle Splitted Expenses](#312-settle-bill-settle-bill)
+  [3.13 Find Expense](#313-finding-expenses--find-expenses)
+  [3.14 Recurring Bill Description](#314-recurring-bill-description)
+  [3.15 Add Recurring Bill](#315-add-recurring-bill--rec-newlist)
+  [3.16 List Recurring Bills](#316-list-all-recurring-bills--rec-viewlists)
+  [3.17 Remove Recurring Bill](#317-remove-recurring-bill--rec-removelist)
+  [3.18 Add Expense to a Recurring Bill](#318-add-an-expense-to-a-recurring-bill--rec-newexpense)
+  [3.19 View Expenses in a Recurring Bill](#319-view-expenses-in-a-recurring-bill--rec-viewexpenses)
+  [3.20 Add Expenses in a Recurring Bill to Overall Expenses](#320-add-expenses-in-a-recurring-bill-to-overall-expenses--rec-addrec)
+  [3.21 Change Currency](#321-changing-currencies--change-currency-currency_code)
+  [3.22 Set Budget](#322-setting-budgets-)
+  [3.23 Get Budget](#323-getting-budget-for-specific-category)
+  [3.24 Print Budget](#324-print-all-the-budgets)
+  [3.25 Get Graphical Insights for expenses](#325-get-graphical-insights-for-expenses-get-expenses-insights)
+  [3.26 Get Graphical Insights for savings](#326-get-graphical-insights-for-savings-get-savings-insights)
+  [3.27 Exiting the Application](#327-exiting-the-application--bye)
+[4. FAQ](#4-faq)
+[5. Command Summary](#5-command-summary)
+[6. For Advanced Users](#6-for-advanced-users)
-{Give a product intro} -## Quick Start +## 1. Introduction +BudgetBuddy is a product for users who wish to handle and track any current/future expenses on a singular platform. +BudgetBuddy provides a faster and more efficient way to track and calculate expenses and provides the ability +to deal with finances on a singular platform with ease as long as you can type fast. -{Give steps to get started quickly} -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +## 2. Quick Start +1. Ensure that you have Java 11 installed. +2. Download the latest version of `BudgetBuddy` from [here](https://github.com/AY2324S2-CS2113-T12-3/tp). +3. Copy the file to the folder you want to use as the home folder. +4. Open command terminal, ``cd`` into the folder you put your jar file in, and use the `java -jar [CS2113-T12-3][BudgetBuddy].jar` +command to run the application (Name of JAR File may vary) -## Features -{Give detailed description of each feature} +## 3. Features -### Adding a todo: `todo` -Adds a new item to the list of todo items. +### 3.0 General Information +1. All user input variables are denoted in all caps. Etc, if the format of a command is `menu INDEX`, `INDEX` refers +to the user input variable. Hence, you may input `menu 1`, `menu 2`, `menu string`, etc. depending on the command constraints. +2. User Input variables that are enclosed with a `[]` are denoted as **optional** inputs. Etc, if the format of a command is `menu [INDEX]`, +the user may wish to not include anything for the `INDEX` and just type `menu`. -Format: `todo n/TODO_NAME d/DEADLINE` +### 3.1 Display Commands : `menu` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Displays the corresponding features of BudgetBuddy + +Format: `menu [INDEX]` + +* The `INDEX` refers to the number associated with each menu option. If `INDEX` is not provided **OR** +is of value `0`, the overall menu list will be displayed +* `INDEX` must be either be empty OR a positive integer and a valid index in the menu list +* The `menu` command is **space sensitive**. In particular, there should only be one space between `menu` and `INDEX` +for the command to be properly recognized should you wish to view a menu item of a certain `INDEX` + +Example of usage: + +`menu` : Displays all menu list items + +`menu 1` : Displays commands related to feature associated to menu list item 1 + + +### 3.2 Add Expense +Records a new expense under a specific category with a detailed description. + +Format: `add expense c/CATEGORY a/AMOUNT d/DESCRIPTION` + +* Increments expense of the specified CATEGORY by AMOUNT given. +* The category under which the expense is to be recorded. It must match one of the + pre-defined categories exactly (not case-insensitive): + Housing + Groceries + Utility + Transport + Entertainment + Others +* The `AMOUNT` is the amount to add to the expense. It must be a positive number and can include + up to two decimal places. +* The `DESCRIPTION` is a brief description of the expense. Accepts any text string. + +Example of Usage: + +`add expense c/Entertainment a/167 d/Bruno Mars` + +### 3.3 Add Savings +Adds a specified amount to the savings under a particular category. + +Format: `add savings c/CATEGORY a/AMOUNT` + +* Increments savings of the specified CATEGORY by AMOUNT given. +* The category for the savings increment. It must be one of the pre-defined + categories (not case-insensitive): + Salary + Investments + Gifts + Others +* The `AMOUNT` is the amount to add to the savings. It must be a positive number + and can include up to two decimal places. + +Example of Usage: + +`add savings c/Salary a/500.50` + +### 3.4 Add Shared Bill +Add bills that are meant for splitting among friends or colleague + +Format: `add shared bill a/AMOUNT n/NUMBER_OF_PEOPLE d/DESCRIPTION` + +* Increments shared bills +* The `AMOUNT` must be a positive number +* The `NUMER_OF_PEOPLE` must be a positive integer. +* The `DESCRIPTION` can be any string + +Example of usage: + +`add shared bill a/100 n/10 d/Lunch` + +### 3.5 Edit Savings: `edit savings` +Edit Savings that have been added previously. + +Format: `edit savings c/CATEGORY a/AMOUNT` + +* The `CATEGORY` must be one of the following pre-defined categories: "Salary", + "Investments", "Gifts" or "Others". (Case Sensitive) +* The `AMOUNT` must be a positive float. + +Example of usage: + +`edit savings c/Salary i/2 a/180 d/Monthly Salary` + +Expected Output: + +`Saving edited successfully.` + +### 3.6 Edit Expenses: `edit expense` +Edit expenses that have been added previously. + +Format: `edit expense c/CATEGORY i/INDEX a/AMOUNT d/DESCRIPTION` + +* The `CATEGORY` must be one of the following pre-defined categories: "Housing", + "Groceries", "Utility", "Transport", "Entertainment" or "Others". (Case Sensitive) +* The `INDEX` must be a positive integer and a valid index in the menu list. +* The `AMOUNT` must be a positive integer. +* The `DESCRIPTION` can be any string. + +Example of usage: + +`edit expense c/Entertainment i/3 a/30 d/movie` + +Expected Output: + +`Expense edited successfully.` + +### 3.7 Reduce Savings: `reduce savings` + +Reduces the amount saved in a particular category + +Format: `reduce savings c/CATEGORY a/AMOUNT` + +* The `CATEGORY` must be a string and a valid category in the menu list. +* The `AMOUNT` to be reduced must be a positive integer + +Example of usage: + +`reduce savings c/Investments a/10` +Reduces the savings of category Investments listed in the savings tracker by $10 + + +### 3.8 Delete Expense: `delete expense` + +Deletes expenses that have been added wrongly or are no longer relevant. + +Format: `delete expense i/INDEX` + +* The `INDEX` must be a positive integer and a valid index in the menu list. + +Example of usage: + +`delete expense i/4` +Deletes the expense of at index 4 listed in the expenditure tracker. + +**Note:** +- Once an expense is deleted, it cannot be recovered. + + +### 3.9 Listing Savings: `list savings` + +Lists savings + +Format: `list savings CATEGORY` + +* The `CATEGORY` is optional and can be left blank. +* The `CATEGORY` must be a pre-existing category if inputted. +* The command is not case-sensitive. +* Similar to listing expenses, users can view their savings with optional category filtering. +* Savings are listed along with their respective categories and amounts. +* Filtered Categories will only display savings with that Category. + * **DOES NOT** affect total savings. + * If there are no listings with the filtered category, nothing will be shown. +* Total savings are displayed at the end of the list, after deducting relevant expenditures. + * **NOT** affected by filtered categories. (i.e will ignore filter category when calculating overall remaining amount) +* Currency of listed savings is stated at the top. + +Example Usage: + +`list savings` +`list savings Salary` +`list savings Investment` + +Expected Output (Empty Saving List) : +![ListSavings_EmptySaving.PNG](userguideimages/ListSavings_EmptySaving.PNG) + +Expected Output (Empty Expense List) : +![ListSavings_EmptyExpense.PNG](userguideimages/ListSavings_EmptyExpense.PNG) + +Expected Output (Expense Recorded) : +![ListSavings_ExpenseRecorded.PNG](userguideimages/ListSavings_ExpenseRecorded.PNG) + +Expected Output (Filtered Category) : +![ListSavings_FilterCategory.PNG](userguideimages/ListSavings_FilterCategory.PNG) + + +### 3.10 Listing Expenses: `list expenses` + +Lists expenses + +Format: `list expenses CATEGORY` + +* The `CATEGORY` is optional and can be left blank. +* The `CATEGORY` must be a pre-existing category if inputted. +* The command is not case-sensitive. +* When listing expenses, users have the option to filter expenses based on categories. +* Users can specify a category to view expenses related to that category only. + * Index is **NOT** affected so as to facilitate easier identification for deletion. +* If no category is specified, the system will list all expenses. + * If there are no listings with the filtered category, nothing will be shown. +* The listed expenses include details such as the date of the expense, category, amount, and description. +* Total expenses are displayed at the end of the list. + * **NOT** affected by filtered categories. (i.e Filtered Category still displays overall total expenses.) +* Currency of listed expenses is stated at the top. + +Example Usage: + +`list expenses` +`list expenses Transport` +`list expenses Housing` + +Expected Output (Empty Expense List) : +![ListSavings_EmptyExpense.PNG](userguideimages/ListSavings_EmptyExpense.PNG) + +Expected Output (Expense Recorded): +![ListExpenses_ExpenseRecorded.PNG](userguideimages/ListExpenses_ExpenseRecorded.PNG) + +Expected Output (Filtered Category) : +![ListExpenses_FilteredCategory.PNG](userguideimages/ListExpenses_FilteredCategory.PNG) + +### 3.11 Check splitted expenses `check splitted expenses` + +Check Split Bills + +Format: `check split bills` + +* the system will list all bills that have been split. +* The listed bills include details such as the description, the number of people and the amount payable by each person. + + +### 3.12 Settle bill `settle bill` + +Settle splitted expenses + +Format `settle bill i/Index` + +* The system will settle the splitted expense corresponding to `Index` +* `Index` must be a positive integer + +Example of usage: +`settle bill i/2`: Delete bill of index 2 listed in splittedexpenses tracker + +### 3.13 Finding expenses : `find expenses` + +Finds expenses based on their description or amount + +Format : `find expenses d/[DESCRIPTION] morethan/[MINAMOUNT] lessthan/[MAXAMOUNT]` + +* All prefixes `d/`, `morethan/` and `lessthan/` **must be** present +* `DESCRIPTION`, `MINAMOUNT`, `MAXAMOUNT` are optional to use. Leaving them all empty simply obtains the entire expense list. +* Leaving either `DESCRIPTION`, `MINAMOUNT`, `MAXAMOUNT` empty assumes that the parameter is not considered when finding expenses +* `DESCRIPTION` is the description associated with the expenses the user wishes to find +* `MINAMOUNT` is the filter for expenses with amounts higher than or equal to the specified value +* `MAXAMOUNT` is the filter for expenses with amounts lower than or equal to the specified value +* **Note** Although the user is able to combine the prefixes without any spaces, etc. + `find expenses d/descriptionmorethan/10lessthan/20`, it is strongly recommended to ensure spaces for clarity. +* The `find expenses` portion is **case sensitive**. In particular, there should only be one space between `find` and `expenses` +for the command to be recognized. + +Examples of usage : + +`find expenses d/coffee morethan/ lessthan/ ` : Finds all expenses with the word "coffee" in the description + + +`find expenses d/coffee morethan/200 lessthan/ ` : Finds all expenses with the word "coffee" and amount higher than equals to $200 + + +`find expenses d/coffee morethan/200 lessthan/400 ` : Finds all expenses with the word "coffee" and amount higher than equals to $200, but lesser than equals to $400 + +### 3.14 Recurring Bill Description +The next few features from `3.15` to `3.20` in the user guide would be related to the Recurring Bill Feature. The commands associated to +this overall feature would start with the `rec` command, followed by the relevant `commandType` and parameters. This +feature allows the user to create and manage multiple lists of expenses **separate** from the user's overall expenses +, which can be added to the users overall expenses. + +The term **recurring** here is to indicate that a user is able to **add** a set of pre-defined expenses to their +overall expenses at **any** given point in time. Hence, this could range from subscription payments, a future grocery list, +etc. + + +### 3.15 Add Recurring Bill : `rec newlist` + +Adds a new recurring Bill. + +Format : `rec newlist LISTNAME` + +* `LISTNAME` refers to the name you wish to associate the recurring Bill with +* `LISTNAME` cannot be empty, and cannot contain a `|` or `!` +* This command is **space sensitive**, in particular the space between `rec`, `newlist` and `LISTNAME` +should be **exactly** one space apart for the command to be recognised +* **Note** : You can add bills of the same `LISTNAME` as lists are differentiated by their list position + +Examples of usage : + +`rec newlist Subscriptions` : Creates a new empty recurring bill named `Subscriptions` + +### 3.16 List all Recurring Bills : `rec viewlists` + +Lists all recurring bill names, along with their associated list number + +Format : `rec viewlists` + +* This command is **space sensitive**, in particular, the space between `rec` and `viewlists` must be +**exactly** one space apart for the command to be recognised +* **Note** : Anything typed after `rec viewlists ` will be ignored. + +Examples of Output : + +When there are already added recurring bills : + +![Output of viewlists when there are recurring bills added](userguideimages/rec_viewlists_exampleOutput.jpg) + +When there are no added recurring bills + +![Output of viewlists where there are no recurring bills](userguideimages/rec_viewlists_emptyOutput.png) + +### 3.17 Remove Recurring Bill : `rec removelist` + +Removes a recurring bill + +Format : `rec removelist LISTNUMBER` + +* `LISTNUMBER` refers to the associated list number of recurring bill when doing a `rec viewlists` +* `LISTNUMBER` must be a **valid** integer > 0, and should be a **valid** list number +* This command is **space sensitive**, in particular, the space between `rec` and `removelist` must be + **exactly** one space apart for the command to be recognised + +Examples of usage : + +`rec removelist 2` : Removes the 2nd recurring bill in the list of recurring bills + +### 3.18 Add an expense to a recurring bill : `rec newexpense` + +Adds an expense to a specified recurring bill + +Format : `rec newexpense to/LISTNUMBER c/CATEGORY a/AMOUNT d/DESCRIPTION` + +* `LISTNUMBER` refers to the associated list number of the recurring bill when doing a `rec viewlists` +* `CATEGORY` refers to the category of the expense you wish to add +* `AMOUNT` refers to the amount value of the expense you wish to add +* **Note** : Any `Amount` that is more than `2 d.p.` is automatically rounded to the nearest `2 d.p.` +* `DESCRIPTION` refers to the description of the expense you wish to add +* `LISTNUMBER` must be a **valid** integer, and should be a **valid** list number +* `CATEGORY`, `AMOUNT` and `DESCRIPTION` follows the same constraints as if you were to add a normal expense. +* `to/, c/, a/, d/` must be placed in the **right order** +* **Note** Although the user is able to combine the prefixes without any spaces, etc. +`rec newexpense to/1c/Entertainmenta/200d/movies`, it is strongly recommended to ensure spaces for clarity. +* This command is **space sensitive**, in particular, the space between `rec` and `newexpense` must be + **exactly** one space apart for the command to be recognised + +Examples of usage : +`rec newexpense to/1 c/Entertainment a/200 d/movies` : Adds a new expense to the 1st recurring bill +, with category as Entertainment, amount as 200 and description as Movies + +### 3.19 View expenses in a recurring bill : `rec viewexpenses` + +Views all expenses in a specified recurring bill + +Format : `rec viewexpenses LISTNUMBER` + +* `LISTNUMBER` refers to the associated list number of the recurring bill when doing a `rec viewlists` +* `LISTNUMBER` must be a **valid** integer, and should be a **valid** list number +* This command is **space sensitive**, in particular, the space between `rec` and `viewlists` must be + **exactly** one space apart for the command to be recognised +* **Note** : In the output, the `Date` refers to the date the expense was added to the Recurring Expense List + +Examples of usage : +`rec viewexpenses 1` : Prints all expenses in the 1st recurring bill + +Here are some expected outputs for different situations : + +When the list to view contains expenses : + +![View Expenses Command when List is Non-Empty](userguideimages/rec_viewexpenses_NonEmptyList.png) + +When the list to view does not contain any expenses : + +![View Expenses Command when List is Empty](userguideimages/rec_viewexpenses_EmptyList.png) + +### 3.20 Add expenses in a recurring bill to overall expenses : `rec addrec` + +Adds all expenses in a specified recurring bill to the overall expenses + +Format : `rec addrec LISTNUMBER` + +* `LISTNUMBER` refers to the associated list number of the recurring bill when doing a `rec viewlists` +* `LISTNUMBER` must be a **valid** integer, and should be a **valid** list number +* This command is **space sensitive**, in particular, the space between `rec` and `addrec` must be + **exactly** one space apart for the command to be recognised +* You may wish to perform a `list expenses` to view the newly added expenses being added to the +overall list of expenses + +Examples of usage : +`rec addrec 1` : Adds all expenses in the 1st recurring bill into the overall expenses + +Here are some expected outputs for different situations : + +When the list to add contains 2 expenses : + +![Add Recurring Expenses Command when List is Non-Empty](userguideimages/rec_addrec_NonEmptyList.png) + +When the list to add is empty: + +![Add Recurring Expenses Command when List is Non-Empty](userguideimages/rec_addrec_EmptyList.png) + +### 3.21 Changing Currencies : `change currency [CURRENCY_CODE]` + +Converts current currency to targeted currency + +Format : `change currency [CURRENCY_CODE]` + +* Default currency is 'SGD'. +* Current Currency can be identified when listing savings/expenses. +* `CURRENCY_CODE` consists of the following currencies: 'SGD', 'USD', 'EUR', 'MYR', 'JPY', 'KRW', 'CNY', 'HKD' +* `CURRENCY_CODE` cannot be null. +* `CURRENCY_CODE` is not case-sensitive. +* Conversion of Currency is interchangeable (e.g. SGD -> USD -> JPY). +* Future additions to Expenses/Savings will be using the current currency displayed. + * (i.e. **ALL** Expenses/Savings will be converted to the new currency.) +* Currency Icon will standardise to use universal `$`. + +Examples of usage: + +`change currency USD` : Converts current currency into USD + +Expected Output (SGD -> USD) : +![CurrencyConverter_ChangeUSD.PNG](userguideimages%2FCurrencyConverter_ChangeUSD.PNG) + +Expected Output (USD -> USD) : +![CurrencyConverter_SameCurrency.PNG](userguideimages%2FCurrencyConverter_SameCurrency.PNG) + +### 3.22 Setting Budgets: + +Sets budget for specified category + +Format: `set budget c/CATEGORY b/BUDGET` + +* `CATEGORY` must be a pre-existing category +* `BUDGET` must be a positive integer + +Example of usage: +`set budget c/Utility b/100` + +Expected output (if there is no existing budget): +![SetBudget_New.png](userguideimages/SetBudget_New.png) + +Expected output (if there is already an existing budget): +![SetBudget_Existing.png](userguideimages/SetBudget_Existing.png) + +### 3.23 Getting budget for specific category: + +Retrieves the budget for the specified category, also lists expenses in descending order, +and shows the percentage of budget that each expense takes up. + +Format: `get budget c/CATEGORY` + +* `CATEGORY` must be a pre-existing category Example of usage: +`get budget c/Transport`: Retrieves the set budget for transport (if any), lists expenses +in transport category in ascending order, and shows % of budget taken up by each of them. + +### 3.24 Print all the budgets + +* Prints all the budgets in a table (Table 1) +* Table 1 contains information about amount spent, remaining amount and % of budget spent for each category +* If any of the budgets have been exceeded, will be displayed in another table (Table 2) +* Table 2 contains categories that exceeded budget and amount exceeded. + +Format: `print budget` + +Example of Expected Output (If there is no existing expenses): +![ListBudget_NoExist.png](userguideimages/ListBudget_NoExist.png) + +Example of Expected Output (If there are existing expenses): +![ListBudget_ExistingExpenses.png](userguideimages/ListBudget_ExistingExpenses.png) + +### 3.25 Get Graphical Insights for expenses: `get expenses insights` +* This feature provides an overview of the expenses distribution across different categories. +* A horizontal bar graph showing the percentage of total expenses attributed to each category. +* It highlights the category with the highest expenses, the one with the lowest (excluding categories with no expenses), +and lists any categories where no expenses have been recorded. +* Categories are Housing, Groceries, Utility, Transport, Entertainment, and Others. (Case Sensitive) + +Example of usage: `get expenses insights` + +Example of Expected Output: +![GetExpenseInsights.png](userguideimages%2FGetExpenseInsights.png) + +### 3.26 Get Graphical Insights for savings: `get savings insights` + +* This feature offers a comprehensive look at how your savings are allocated across various categories. +* A horizontal bar graph showing the percentage of total savings attributed to each category. +* It highlights the category with the highest savings, the one with the lowest (excluding categories with no savings), +and lists any categories where no savings have been added. +* Categories are Salary, Investments, Gifts, and Others (Case Sensitive) + +Example of Usage: `get savings insights` + +Example of Expected Output: +![GetSavingsInsights.png](userguideimages%2FGetSavingsInsights.png) + +### 3.27 Exiting the application : `bye` + +## 4. FAQ + +This section answers some frequently asked questions. + +### Why can I only add amounts less than 1,000,000,000,000.00 +In BudgetBuddy, we set a maximum limit on transaction amounts to ensure consistent functionality across different systems. Since variable storage capacities can vary +, this limit helps prevent errors and maintain reliable operations. With the richest man in the world having +an approximate net worth of 231,000,000,000. We believe that the cap should be sufficient to most users. + +### What happens if the total amount of all expenses is above 1,000,000,000,000.00 +Exceeding the maximum limit of 1,000,000,000,000.00 for total expenses can lead to calculation errors and issues in data representation +due to the limited range of numbers the application can handle. +While we strongly recommend staying within this limit to avoid such problems, +most users should find that BudgetBuddy meets their needs without issue. + +### How do I transfer my data to another computer. + +The save files for BudgetBuddy are stored in `[JAR file location]/data/`.
+Copy all contents found in the `data` folder to the new location. + +### Can I include my own category? + +As of current version (v2.1), the feature to include your own category has not been implemented yet. It may come in a future release. + +### Why are there only so little currency codes available? + +As of the current version (v2.1), we plan to include more conversion codes in future updates as we expand on the program. + +### Are the conversion values accurate in real-time? + +As our program does not require Internet access, the conversion ratios are taken as of the release date for each version of BudgetBuddy.
+(i.e. v2.1 released on 15 Apr 2024, so conversion ratios are taken as of that date)
+ + +## 5. Command Summary +* Display Commands: `menu [INDEX]` +* Add Savings: `add savings c/CATEGORY a/AMOUNT` +* Add Expense: `add expense c/CATEGORY a/AMOUNT d/DESCRIPTION` +* Add Shared Bill: `a/AMOUNT n/NUMBER_OF_PEOPLE d/DESCRIPTION` +* Edit Expenses: `edit expense c/CATEGORY i/INDEX a/AMOUNT d/DESCRIPTION` +* Edit Savings: `edit savings c/CATEGORY a/AMOUNT` +* Reduce Savings: `reduce savings c/CATEGORY a/AMOUNT` +* Delete Expense: `delete expense i/INDEX` +* List Expenses: `list expenses [CATEGORY]` +* List Savings: `list savings [CATEGORY]` +* Check Splitted Expenses: `check split bills` +* Settle Bill: `settle bill i/Index` +* Find Expenses: `find expenses d/[DESCRIPTION] morethan/[MINAMOUNT] lessthan/[MAXAMOUNT]` +* Add Recurring Bill: `rec newlist LISTNAME` +* List all Recurring Bills: `rec viewlists` +* Remove Recurring Bill: `rec removelist LISTNUMBER` +* Add an Expense to a Recurring Bill: `rec newexpense to/LISTNUMBER c/CATEGORY a/AMOUNT d/DESCRIPTION` +* View expense in a Recurring Bill: `rec viewexpenses LISTNUMBER` +* Add expenses in a Recurring Bill to overall Expenses: `rec addrec LISTNUMBER` +* Change Currency: `change currency [CURRENCY_CODE]` +* Set Budget: `set budget c/CATEGORY b/BUDGET` +* Get Budget: `get budget c/CATEGORY` +* Print Budgets: `print budget` +* Get Graphical Insights for expenses: `get expenses insights` +* Get Graphical Insights for savings: `get savings insights` +* Exiting the application: `bye` + +## 6. For Advanced Users: + +### 6.1 Saving the data +BudgetBuddy data is automatically saved to the hard disk after any command. There is no need to save manually. -`todo n/Write the rest of the User Guide d/next week` +**Note** : As the data to be saved depends on the **current state** of the application, any manual changes made to +the files **during** the run of the application will not be reflected in the application. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +### 6.2 Editing the data file -## FAQ +BudgetBuddy data is saved automatically as a text file `[JAR file location]/data/*.txt`. Where * represents the different names of the files +. Five files should be created upon first startup of BudgetBuddy : `SavingsFile.txt`, `ExpenseFile.txt`, `RecurringExpensesFile.txt`, `DefaultCurrency.txt` and `SplitExpensesFile.txt` +Advanced users are welcome to update the data directly by editing this file. However, caution is advised as certain edits may cause JunBot to behave unexpectedly. -**Q**: How do I transfer my data to another computer? +#### RecurringExpensesFile.txt +For advanced users who wish to edit the `RecurringExpensesFile.txt`, do take note of the following -**A**: {your answer here} +* `!!! NAME !!!` will denote the creation of a RecurringExpenseList with the name as the specified `NAME` +* The list number of each RecurringExpenseList is determined by the position of its associated `!!! NAME !!!` in `RecurringExpensesFile.txt`, where the highest in the file would have its associated list number as `1`. +* `LISTNUMBER | DATE | CATEGORY | AMOUNT | DESCRIPTION` will denote the addition of an expense into the `RecurringExpenseList` associated with the provided `LISTNUMBER` +* `DATE`, `CATEGORY`, `AMOUNT` and `DESCRIPTION` follow the same restrictions as if a user were to normally add an expense within the application itself. +* On top of the restrictions stated in the previous point, do note that `CATEGORY` is **case-sensitive** here. Hence, `entertainment` is considered an invalid category as it should be `Entertainment` +* Any `AMOUNT` that is more than 2 decimal places will be automatically treated as 2 d.p. +* The file is considered **corrupted** as long as the inputs are out of place / invalid. Etc. invalid category, invalid amount, wrong format for naming a recurring list -## Command Summary +> ⚠️ **Caution:** Certain edits can cause BudgetBuddy to behave in unexpected ways (e.g. if value entered is outside the acceptable range, or the entries are not in the right format). Therefore, edit the data file only +> if you are confident that you can update it correctly -{Give a 'cheat sheet' of commands here} -* Add todo `todo n/TODO_NAME d/DEADLINE` diff --git a/docs/diagrams/CurrencyConverter_RecurringExpenses_SequenceDiagram.png b/docs/diagrams/CurrencyConverter_RecurringExpenses_SequenceDiagram.png new file mode 100644 index 0000000000..534eaa1486 Binary files /dev/null and b/docs/diagrams/CurrencyConverter_RecurringExpenses_SequenceDiagram.png differ diff --git a/docs/diagrams/CurrencyConverter_SequenceDiagram.png b/docs/diagrams/CurrencyConverter_SequenceDiagram.png new file mode 100644 index 0000000000..5f6d86c689 Binary files /dev/null and b/docs/diagrams/CurrencyConverter_SequenceDiagram.png differ diff --git a/docs/diagrams/EditExpenseDiagram.drawio.png b/docs/diagrams/EditExpenseDiagram.drawio.png new file mode 100644 index 0000000000..2d7d618fa2 Binary files /dev/null and b/docs/diagrams/EditExpenseDiagram.drawio.png differ diff --git a/docs/diagrams/EditSavingsDiagram.png b/docs/diagrams/EditSavingsDiagram.png new file mode 100644 index 0000000000..52106ebd0e Binary files /dev/null and b/docs/diagrams/EditSavingsDiagram.png differ diff --git a/docs/diagrams/ExpenseList_SequenceDiagram.png b/docs/diagrams/ExpenseList_SequenceDiagram.png new file mode 100644 index 0000000000..5e7a45919c Binary files /dev/null and b/docs/diagrams/ExpenseList_SequenceDiagram.png differ diff --git a/docs/diagrams/SavingList_SequenceDiagram.png b/docs/diagrams/SavingList_SequenceDiagram.png new file mode 100644 index 0000000000..b6771472c4 Binary files /dev/null and b/docs/diagrams/SavingList_SequenceDiagram.png differ diff --git a/docs/diagrams/budgetCurrencyConverter.png b/docs/diagrams/budgetCurrencyConverter.png new file mode 100644 index 0000000000..ff54f596ed Binary files /dev/null and b/docs/diagrams/budgetCurrencyConverter.png differ diff --git a/docs/diagrams/classDiagram_RecurringExpenseLists.jpg b/docs/diagrams/classDiagram_RecurringExpenseLists.jpg new file mode 100644 index 0000000000..a6a7696893 Binary files /dev/null and b/docs/diagrams/classDiagram_RecurringExpenseLists.jpg differ diff --git a/docs/diagrams/convertExpenseCurrency_SequenceDiagram.png b/docs/diagrams/convertExpenseCurrency_SequenceDiagram.png new file mode 100644 index 0000000000..cc2f4430bb Binary files /dev/null and b/docs/diagrams/convertExpenseCurrency_SequenceDiagram.png differ diff --git a/docs/diagrams/convertSavingCurrency_SequenceDiagram.png b/docs/diagrams/convertSavingCurrency_SequenceDiagram.png new file mode 100644 index 0000000000..5f858544c2 Binary files /dev/null and b/docs/diagrams/convertSavingCurrency_SequenceDiagram.png differ diff --git a/docs/diagrams/diagram_Introduction.jpg b/docs/diagrams/diagram_Introduction.jpg new file mode 100644 index 0000000000..e14f8e83ff Binary files /dev/null and b/docs/diagrams/diagram_Introduction.jpg differ diff --git a/docs/diagrams/getExpenseInsightsDiagram.drawio.png b/docs/diagrams/getExpenseInsightsDiagram.drawio.png new file mode 100644 index 0000000000..7ab1072e62 Binary files /dev/null and b/docs/diagrams/getExpenseInsightsDiagram.drawio.png differ diff --git a/docs/diagrams/getSavingsInsightsDiagram.drawio.png b/docs/diagrams/getSavingsInsightsDiagram.drawio.png new file mode 100644 index 0000000000..7c3053b160 Binary files /dev/null and b/docs/diagrams/getSavingsInsightsDiagram.drawio.png differ diff --git a/docs/diagrams/sequenceDiagram_AddExpense.jpg b/docs/diagrams/sequenceDiagram_AddExpense.jpg new file mode 100644 index 0000000000..c5fcb7ac8a Binary files /dev/null and b/docs/diagrams/sequenceDiagram_AddExpense.jpg differ diff --git a/docs/diagrams/sequenceDiagram_AddSaving.jpg b/docs/diagrams/sequenceDiagram_AddSaving.jpg new file mode 100644 index 0000000000..7e92ca27d0 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_AddSaving.jpg differ diff --git a/docs/diagrams/sequenceDiagram_Command.jpg b/docs/diagrams/sequenceDiagram_Command.jpg new file mode 100644 index 0000000000..704d1f4a95 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_Command.jpg differ diff --git a/docs/diagrams/sequenceDiagram_CurrencyConverter_SplitExpense.png b/docs/diagrams/sequenceDiagram_CurrencyConverter_SplitExpense.png new file mode 100644 index 0000000000..cf64aac10c Binary files /dev/null and b/docs/diagrams/sequenceDiagram_CurrencyConverter_SplitExpense.png differ diff --git a/docs/diagrams/sequenceDiagram_FindExpensesCommand.jpg b/docs/diagrams/sequenceDiagram_FindExpensesCommand.jpg new file mode 100644 index 0000000000..e102549386 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_FindExpensesCommand.jpg differ diff --git a/docs/diagrams/sequenceDiagram_FindExpensesCommandCreator.jpg b/docs/diagrams/sequenceDiagram_FindExpensesCommandCreator.jpg new file mode 100644 index 0000000000..179e8baaf8 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_FindExpensesCommandCreator.jpg differ diff --git a/docs/diagrams/sequenceDiagram_ListBudget.png b/docs/diagrams/sequenceDiagram_ListBudget.png new file mode 100644 index 0000000000..a9854c959b Binary files /dev/null and b/docs/diagrams/sequenceDiagram_ListBudget.png differ diff --git a/docs/diagrams/sequenceDiagram_MenuCommand.jpg b/docs/diagrams/sequenceDiagram_MenuCommand.jpg new file mode 100644 index 0000000000..9b02fbc67f Binary files /dev/null and b/docs/diagrams/sequenceDiagram_MenuCommand.jpg differ diff --git a/docs/diagrams/sequenceDiagram_MenuCommandCreator.jpg b/docs/diagrams/sequenceDiagram_MenuCommandCreator.jpg new file mode 100644 index 0000000000..3a165eaa70 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_MenuCommandCreator.jpg differ diff --git a/docs/diagrams/sequenceDiagram_RecurringExpenseCommand.jpg b/docs/diagrams/sequenceDiagram_RecurringExpenseCommand.jpg new file mode 100644 index 0000000000..c379c05de8 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_RecurringExpenseCommand.jpg differ diff --git a/docs/diagrams/sequenceDiagram_ReduceSavings.png b/docs/diagrams/sequenceDiagram_ReduceSavings.png new file mode 100644 index 0000000000..41c5ec073d Binary files /dev/null and b/docs/diagrams/sequenceDiagram_ReduceSavings.png differ diff --git a/docs/diagrams/sequenceDiagram_SetBudget.jpg b/docs/diagrams/sequenceDiagram_SetBudget.jpg new file mode 100644 index 0000000000..c5d8a6a7bc Binary files /dev/null and b/docs/diagrams/sequenceDiagram_SetBudget.jpg differ diff --git a/docs/diagrams/sequenceDiagram_SplitExpense.jpg b/docs/diagrams/sequenceDiagram_SplitExpense.jpg new file mode 100644 index 0000000000..b371a2647b Binary files /dev/null and b/docs/diagrams/sequenceDiagram_SplitExpense.jpg differ diff --git a/docs/diagrams/sequenceDiagram_checkSplitBills.jpg b/docs/diagrams/sequenceDiagram_checkSplitBills.jpg new file mode 100644 index 0000000000..18c5622899 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_checkSplitBills.jpg differ diff --git a/docs/diagrams/sequenceDiagram_settleSplitBill.jpg b/docs/diagrams/sequenceDiagram_settleSplitBill.jpg new file mode 100644 index 0000000000..108ef65bd2 Binary files /dev/null and b/docs/diagrams/sequenceDiagram_settleSplitBill.jpg differ diff --git a/docs/team/dheekshitha2.md b/docs/team/dheekshitha2.md new file mode 100644 index 0000000000..eb3166794e --- /dev/null +++ b/docs/team/dheekshitha2.md @@ -0,0 +1,59 @@ +# Murali Krishnan Dheekshitha - Project Portfolio Page + +## Project: BudgetBuddy +BudgetBuddy is a product for users who wish to handle and track any current/future expenses on a singular platform. +BudgetBuddy provides a faster and more efficient way to track and calculate expenses and provides the ability to deal +with finances on a singular platform with ease. It has multiple financial tracking options to suit your needs. + +## Summary of Contributions +My primary contributions include developing the budget setting functions, and enabling users to delete their expenses +or reduce their savings, thereby improving the app's usability and user experience. + +### New Feature: Added the ability to set and manage budget (Pull Request, [#66](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/66), [#91](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/117)) +- **What it does:** Allows users to set and adjust budgets for various categories such as groceries, transport, and + housing. Users can define a maximum spending limit for each category, which helps in monitoring and controlling + their expenditures. +- **Justification:** This feature is pivotal for users aiming to adhere to their financial goals, and ensure that they + do not overspend to align with their financial targets +- **Highlights:** Implementing this feature requires a deep understanding of the app's different functions and how they + all work together. One challenge of implementing this feature was integrating existing functions like `Expenses` so that + I am able to retrieve the relevant expense information and update the utilised budget, checking if the user is still within their budget + +### New Feature: Added the ability to delete expenses (Pull Request [#25](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/25), [#38](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/38)) +- **What it does:** Allows users to delete expenses that have been added wrongly or are outdated +- **Justification:** This feature improves the app's flexibility by allowing users to correct their mistakes, ensuring + that their financial records remain accurate +- **Highlights:** Ensured that edge cases were handled, such as attempts to delete non-existent records, ensuring robustness. The delete +function also recalculates the available budget, keeping the users' financial overview up to date + +### New Feature: Added the ability to reduce savings (Pull Request [#25](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/25), [#39](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/39)) +- **What it does:** Enables users to record when they withdraw or spend from their savings, updating savings accordingly +- **Justification:** Essential for maintaining an accurate record of savings, especially when savings are used to cover +unexpected expenses or large purchases +- **Highlights:** Implemented a fluid interaction between the savings and expenses functions, allowing for automatic adjustments to savings +when expenses are updated + +## Code contributed: [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=Dheekshitha2&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +## Enhancements to existing features +- Added an alert when user tries to add an expense which would exceed the budget set (Pull Request [#195](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/195)) +- Integrated my budget function into currency converter (Pull request [#197](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/197)) +- Changed from reducing savings by index, to reducing savings by category instead (Pull request [#217](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/217)) +- Integrated saving and loading for my budget functions (Pull request [#217](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/217)) + +## Documentation + +### Developer's Guide +- Added documentation for `reduce savings` and `delete expenses` (Pull Request [#216](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/216)) +- Added sequence diagram for `ReduceSavings`, `ListBudget` and `SetBudget` (Pull Request [#220](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/220)) +- Added use cases for my functions (Pull Request [#220](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/220)) +- Added documentation for `set budget` and `print budget` (Pull Request [#69](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/69), [#216](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/216)) + +### User Guide +- Added implementation for `reduce savings` and `delete expenses` +- Added implementation for all the budget functions (Pull Request [#130](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/130)) + +### Community +- Added reviews to my teammate's code (Pull Request [#215](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/215/commits/)) +- Added review for the user guide of another team ([Link](https://github.com/nus-cs2113-AY2324S2/tp/pull/24)) +- Reported bugs for another team during the PE-D ([Link](https://github.com/AY2324S2-CS2113-W14-3/tp/issues?q=is%3Aissue+is%3Aclosed)) \ No newline at end of file diff --git a/docs/team/itsmejr257.md b/docs/team/itsmejr257.md new file mode 100644 index 0000000000..30e05300d1 --- /dev/null +++ b/docs/team/itsmejr257.md @@ -0,0 +1,69 @@ +# Chan Jun Rong - Project Portfolio Page + +## Overview +The product created is a finance-tracking application named BudgetBuddy.It is a product for users who wish +to handle and track any current/future expenses on a singular platform. +### Summary of Contributions +Given below are my contributions to the project + +#### New Feature : Add the Ability to view Menu Commands (Pull Requests [#91](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/91), [#8](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/8)) +1. What it does : Allows user to view the respective commands of a certain item displayed in the menu. + +#### New Feature : Add the Ability to Find expenses of a specific name/amount (Pull Requests [#41](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/41), [#90](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/90), [#55](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/55)) +1. What it does : Allows user to view all expenses that match a user's provided description, minimum amount and maximum +amount +2. Credits : sweijie24 for his `listExpenses()` method which aided in displaying the found expenses. + +#### New Feature : Add the Ability to create a named list of recurring expenses (Pull Requests [#68](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/68), [#92](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/92)) +1. What it does : Allows user to create an empty list of expenses of their desired name + +#### New Feature : Add the Ability to remove a named list of recurring expenses (Pull Requests [#68](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/68), [#92](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/92)) +1. What it does : Allows user to remove a list of expenses from the overall lists of Recurring Expenses + +#### New Feature : Add the ability to view all names of lists of recurring expenses (Pull Requests [#68](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/68), [#92](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/92)) + +#### New Feature : Add the ability to add an expense to a specific list of recurring expenses (Pull Requests [#68](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/68), [#92](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/92)) +1. What is does : Allows user to add an expense to a specified list in the overall list of recurring expenses + +#### New Feature : Add the ability to add all expenses in a specific list to the overall expenses (Pull Requests [#68](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/68), [#92](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/92)) +1. What it does : Allows user to add all expenses in a specified list to the overall expenses + +#### New Feature : Add the ability for recurring expenses to work with the currency converter (Pull Requests [#132](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/132)) +1. Justification : This feature improves the product significantly, as it helps to make the app more cohesive in terms of its features + +#### New Feature : Add saving and loading from a file for recurring expenses (Pull Requests [#105](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/105)) +1. What it does : Allows all expenses / lists in the recurring expenses to be saved and loaded from a file + +#### Code Contributed : [RepoSenseLink](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=itsmejr257&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +#### Project Management +1. Managed releases v1.0-v2.1 (3 releases) on Github +2. Integrated features to work as a cohesive application in v1.0 (Pull Request [#34](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/34)) +3. Handle Issue tracker for the following [issues](https://github.com/AY2324S2-CS2113-T12-3/tp/issues?q=is%3Aissue+is%3Aclosed+author%3Aitsmejr257) + +#### Enhancements to existing features: +1. Wrote JUnit tests for the Recurring Expense, Find and Menu Command feature (Pull Request [#106](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/106)) +2. Implemented Bug Fixes for `RecurringBill`, `Find` and `Menu` features. (Pull Request [#192](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/192), [#190](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/190), [#189](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/189), [#133](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/133), [#230](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/230) + +#### Community +1. PRs reviewed : [#195](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/195), [#111](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/111), [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107), [#100](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/100), [#99](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/99) +2. Provided support and help to teammates in bug-fixing and ideation of features. (Communication was done through text-messages and weekly-meeting calls, may contact group-mates to ascertain this) +3. Provided DG Peer Review Comments for another team. [CS2113-F12-2 LifeTrack](https://github.com/nus-cs2113-AY2324S2/tp/pull/56) +4. Reported bugs for another team during PE-D. [CS2113T-T09-1 MediTracker](https://github.com/AY2324S2-CS2113T-T09-1/tp/) + +#### Documentation +Developer Guide : Pull Requests [#108](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/108),[#77](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/77), [#76](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/76), [#67](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/76), [#233](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/233), [#205](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/205) +1. Added diagrams and documentation for the features `Recurring Expenses`, `Menu` and `Find` under `Implementation` Section +2. Added diagrams and documentation for the `Introduction`, `Setup Guide` and `Design` sections +3. Add user stories and use cases for my features : Pull Request [#227](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/227/files) +4. Add instructions for manual testing: Pull Request [#205](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/205/files?short_path=1a95edf#diff-1a95edf069a4136e9cb71bee758b0dc86996f6051f0d438ec2c424557de7160b) + + +User Guide : Pull Requests [#140](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/140),[#136](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/136), [#109](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/109), [#61](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/61), [#205](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/205) + 1. Added documentation for the features `Recurring Expenses`, `Menu` and `Find` + 2. Added documentation for the `Introduction` and `Quick Start` + + + + + diff --git a/docs/team/jasraa.md b/docs/team/jasraa.md new file mode 100644 index 0000000000..d2b026597e --- /dev/null +++ b/docs/team/jasraa.md @@ -0,0 +1,68 @@ +# Jasra Zainab's Project Portfolio Page + +## Project: Budget Buddy +BudgetBuddy is a streamlined finance-tracking application designed for efficient management of current and future +expenses on a single platform. BudgetBuddy simplifies expense tracking and calculations, making financial management +both quick and intuitive. With budgeting features implemented, Budget Buddy is a well-rounded financial management +application that is user-friendly. + +### Summary of Contributions +Given below are my contributions to the project + +#### New Feature: Added the ability to edit expenses [#26](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/26) [#37](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/37) [#46](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/46) [#58](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/58) [#59](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/59) +- **What it does:** Allows users to edit expenses that have already been added. Users have to specify the index of the + expense they want to edit, and they can edit the Category, Amount and Description of the expense. +- **Justification:** This feature enables to users to correct any mistakes they may have made while adding an expense. + Thus, improving the apps accuracy in managing finances since users are able to update the most correct information. + +#### New Feature: Added the ability to edit savings [#26](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/26) [#37](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/37) [#46](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/46) [#58](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/58) [#59](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/59) +- **What it does:** Allows users to edit savings that have already been added. Users have to category of the saving + they want to edit, and they can edit the amount of the saving by specifying the amount they wish to edit it to. +- **Justification:** This feature enables to users to correct any mistakes they may have made while adding a saving. + Thus, improving the apps accuracy in managing finances since users are able to update the most correct information. + +#### New Feature: Added the ability to save and load expenses and savings [#65](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/65) [#215](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/215) +- **What it does:** This feature allows users to save the state of their financial data such as their expenses and + savings added and load them back at their convenience. Information such as the Index, Category, Amount and Description + will be saved. This ensures continuity in financial tracking even after closing the application. +- **Justification:** This enhancement is critical for maintaining the integrity of financial records over multiple + sessions. Users can confidently close BudgetBuddy, knowing they can pick up exactly where they left off, making + financial management more seamless and user-friendly. + +#### New Feature: Added the ability to get graphical insights for Expenses and Savings [#111](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/111) +- **What it does:** This feature presents a visual representation of users' financial data, displaying expenses and + savings as horizontal bar graphs. It gives users a quick and clear picture of their spending and saving patterns, + enabling them to identify the largest and smallest categories at a glance. +- **Justification:** The visual summary of expenses and savings helps users better understand their financial habits + without delving into the details. The ability to see which categories take up most of their budget and where they are + saving effectively can be crucial for making informed decisions about financial planning. This addition enhances the + user experience by providing a more interactive and engaging way to engage with their financial data. +- **Highlights:** The feature includes a neat, aligned graphical output where each category is represented by a + proportional bar filled with hash symbols (#). It also includes key insights like the highest and lowest expense and + saving categories, as well as any categories that haven't been added to, ensuring users have a comprehensive view of + their financial status. +- **Usage:** Users can access this graphical summary by entering specific commands to retrieve insights on their + expenses or savings. The system will then calculate and display the information in an easy-to-read bar graph format + within the command line interface, eliminating the need for external tools or visualisation software. + +#### Code Contributed +[RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=jasraa&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +#### Enhancements to existing features +1. Wrote Junit tests for Edit Expenses, Edit Savings, Get Expenses Insight and Get Savings Insights +2. Implemented Bug fixes for "Edit Expenses", "Edit Savings", "Storage", "Get Expenses Insights" and + "Get Savings Insights" + +#### Contributions to the UG +Added documentation for the features `edit expense`, `edit savings`, `get expenses insight` +and `get savings insights` [#48](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/48) [#112](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/112) + +#### Contributions to the DG +Added diagrams and documentation for the features `edit expense`, `edit savings`, `get expenses insight` +and `get savings insights` [#78](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/78) [#223](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/223) [#226](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/226) + +#### Community +1. Communicated with teammates for ideation and enhancement of existing features. +2. Added reviews to teammate's Pull Request [#217](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/217) to help them improve their program and features. +2. Provided DG Peer Review Comments for another team. [CS2113-T15-3 SplitLiang](https://github.com/nus-cs2113-AY2324S2/tp/pull/47) +3. Reported bugs for another team during PE-D. [CS2113-T15-1 LongAh](https://github.com/AY2324S2-CS2113-T15-1/tp/releases) \ No newline at end of file diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/sweijie24.md b/docs/team/sweijie24.md new file mode 100644 index 0000000000..766eaaf777 --- /dev/null +++ b/docs/team/sweijie24.md @@ -0,0 +1,56 @@ +# Wei Jie's Project Portfolio Page + +## Project: BudgetBuddy + +**BudgetBuddy** is a product for users who wish to handle and track any current/future expenses on a singular platform. +BudgetBuddy provides a faster and more efficient way to track and calculate expenses and provides the ability +to deal with finances on a singular platform with ease as long as you can type fast. + +Given below are my contributions to the project. + +### New Features + +#### Listing Saving/Expense Feature +- **What it does**: Displays a list of all savings/expenses inputted by the user. Allows the user to view expenses/savings spent/earned so far based on his/her inputs. Also allows user to view remaining amount left in savings after deducting expenses. +- **Justification**: This is a basic essential feature surrounding the core functionality of our product. +- **Highlights**: The in-built calculation of remaining savings provides easy accessibility to information without the need for external calculation devices. + +#### Currency Converter Feature +- **What it does**: Converts currently listed and future expenses/savings into another currency of choice listed in the User Guide. This feature ties in with my Listing feature to view the currency changes. +- **Justification**: This feature is greatly beneficial for users who travel frequently and often stay in foreign countries for a period of time. It allows them to track their expenses/savings regardless of their location. Additionally, one can also check how much their savings are worth if they plan to travel to another country in the future. +- **Highlights**: The user is able to convert interchangeably between the countries listed, providing greater convenience. + +### Code Contributed +[RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=sweijie24&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other). + +### Project Management +- Managed Issue Tracker Assignment from PE-D for v2.1 on GitHub. + +### Enhancements to Existing Features +- Implemented Loading/Saving for Currency Codes to remain persistent. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). +- Implemented JUnit tests for existing features to improve test coverage. (Pull Requests : [#43](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/43), [#62](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/62)). +- Implemented Logging/Assertions for improved error handling. (Pull Requests : [#43](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/43), [#62](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/62)). +- Refactor features to use more OOP. (Pull Requests : [#89](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/89), [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). + +### Documentation + +#### User Guide +- Added documentation for the features `Listing Savings`, `Listing Expenses` and `Changing Currencies`. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). + +#### Developer Guide +- Added implementation details of the `Listing Savings`, `Listing Expenses` and `Currency Converter` feature. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). +- Added design details of the `Expense`, `Saving`, `ExpenseList`, `SavingList`, `DefaultCurrency` and `CurrencyConverter` classes. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). +- Added user stories for my respective features. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). +- Added Sequence Diagrams for `Listing Savings`, `Listing Expenses` and `Currency Converter` features. (Pull Requests : [#107](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/107)). +- Organised the structure and added miscellaneous details. (Pull Requests : [#213](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/213)). +- Added use cases and instructions for manual testing for my respective features. (Pull Requests : [#213](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/213)). + +### Community + +#### Reported Bugs and Suggestions for Other Teams +- Provided 4 DG Peer Review Comments for another team. ([Team #1](https://github.com/nus-cs2113-AY2324S2/tp/pull/30)). +- Reported 13 Bugs for another team during PE-D. ([Team #1](https://github.com/nus-cs2113-AY2324S2/tp/pull/7)). + +### Tools +- Usage of Draw.io for my Sequence Diagrams. ([Draw.io](https://draw.io/)). + diff --git a/docs/team/yyangdaa.md b/docs/team/yyangdaa.md new file mode 100644 index 0000000000..4f1a9d4252 --- /dev/null +++ b/docs/team/yyangdaa.md @@ -0,0 +1,54 @@ +# Zhang Yangda - Project Portfolio Page + +## Overview +BudgetBuddy is a finance-tracking application designed for users seeking a centralized platform to manage and +monitor their finances. Tailored for individuals who value efficiency, BudgetBuddy simplifies the process of +tracking and calculating expenses. Its user-friendly interface caters to fast typists, making financial management + both quick and intuitive. +### Summary of Contributions +Given below are my contributions to the project + +#### New Feature : Add Expense and Add Savings + +1. What it does : +- Add Expense: Users can enter details of their expenditures, such as amount, category (e.g., groceries, utilities, entertainment), and date. This facilitates a detailed tracking of where and how their money is being spent. +- Add Savings: Similarly, users can log details of their savings, including the amount saved, the category of the savings (e.g., Salary, Investments, Gifts), and the date of deposition. This helps users monitor their savings goals and growth over time. +2. Justification : This feature enhances the app's utility by enabling users to keep a meticulous record of their financial transactions. By tracking both expenses and savings, users can better manage their budgets, identify spending trends, and adjust their financial habits accordingly. + +#### New Feature : Add Shared Bill, Check Split Bill, and Settle Bill +1. What is does : This suite of features makes it easy for users to manage expenses shared with friends or colleagues. It supports the addition, listing, and settlement of shared expenses, streamlining the process of cost-sharing. +- Add Shared Bill: Users can record an expense that needs to be split with others, detailing the amount, the number of participants, and the description of the bill. +- Check Split Bill: This function displays all recorded shared expenses. Users can view details such as the description of the bill, and individual shares helping them track who has paid and who hasn't. +- Settle SplitExpense: Once all parties have paid their shares, users can mark the expense as settled. This feature could also facilitate transactions through integrated payment options, making the process smoother. +2. Justification : Managing shared bills can often be cumbersome, especially when manual calculations are involved. This feature simplifies the task by automating calculations and tracking payments. It is particularly useful for shared accommodations, group trips, or any joint activities, where financial transparency and fairness are crucial. This enhances user experience by reducing potential conflicts over shared financial responsibilities and encourages more frequent use of the app for group activities. + +#### Code Contributed +[RepoSenseLink](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=yyangdaa&breakdown=true&sort=groupTitle +%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes= +docs~functional-code~test-code~other) + +#### Enhancements to existing features: +1. Wrote JUnit tests for the ExpenseList, SavingsList, SplitExpenseList and Parser. [#35](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/35)[#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207) +2. Integrated addShared Bills with Storage. [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207) +3. Integrated Split Expense with currencyConverter. [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207) +4. Implemented Logging/Assertions for improved error handling. (Pull Requests : [#45](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/45), +[#56](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/56)). + +#### Contributions to the UG +Added documentation for the features `add expense`, `add savings`, `add shared bill`, `check split bill` and `settle bill` + +#### Developer Guide +- Added implementation details of the `AddExpense`, `AddSaving` and `SplitExpense` feature. (Pull Requests : [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207)). +- Added design details of the `AddExpense`, `AddSaving`, `SplitExpense`, `SplitExpenseList` and `SettleSplitExpenseList` classes. +(Pull Requests : [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207)). +- Added user stories for my respective features. (Pull Requests : [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207)). +- Added Sequence Diagrams for `AddExpense`, `AddSaving` and `SplitExpense` features. (Pull Requests : [#207](https://github.com/AY2324S2-CS2113-T12-3/tp/pull/207)). + +### Community + +#### Reported Bugs and Suggestions for Other Teams +- Provided 4 DG Peer Review Comments for another team. ([Team #1](https://github.com/nus-cs2113-AY2324S2/tp/pull/25)). +- Reported 5 Bugs for another team during PE-D. ([Team #1](https://github.com/nus-cs2113-AY2324S2/tp/pull/54)). + +### Tools +- Usage of Draw.io for my Sequence Diagrams. ([Draw.io](https://draw.io/)). \ No newline at end of file diff --git a/docs/userguideimages/CurrencyConverter_ChangeUSD.png b/docs/userguideimages/CurrencyConverter_ChangeUSD.png new file mode 100644 index 0000000000..764aec0155 Binary files /dev/null and b/docs/userguideimages/CurrencyConverter_ChangeUSD.png differ diff --git a/docs/userguideimages/CurrencyConverter_SameCurrency.png b/docs/userguideimages/CurrencyConverter_SameCurrency.png new file mode 100644 index 0000000000..d0ad935327 Binary files /dev/null and b/docs/userguideimages/CurrencyConverter_SameCurrency.png differ diff --git a/docs/userguideimages/GetExpenseInsights.png b/docs/userguideimages/GetExpenseInsights.png new file mode 100644 index 0000000000..79d2138b7a Binary files /dev/null and b/docs/userguideimages/GetExpenseInsights.png differ diff --git a/docs/userguideimages/GetSavingsInsights.png b/docs/userguideimages/GetSavingsInsights.png new file mode 100644 index 0000000000..09b0260f40 Binary files /dev/null and b/docs/userguideimages/GetSavingsInsights.png differ diff --git a/docs/userguideimages/ListBudget_ExistingExpenses.png b/docs/userguideimages/ListBudget_ExistingExpenses.png new file mode 100644 index 0000000000..712455a07b Binary files /dev/null and b/docs/userguideimages/ListBudget_ExistingExpenses.png differ diff --git a/docs/userguideimages/ListBudget_NoExist.png b/docs/userguideimages/ListBudget_NoExist.png new file mode 100644 index 0000000000..d73db7529d Binary files /dev/null and b/docs/userguideimages/ListBudget_NoExist.png differ diff --git a/docs/userguideimages/ListExpenses_EmptyExpense.PNG b/docs/userguideimages/ListExpenses_EmptyExpense.PNG new file mode 100644 index 0000000000..6f4d8079a6 Binary files /dev/null and b/docs/userguideimages/ListExpenses_EmptyExpense.PNG differ diff --git a/docs/userguideimages/ListExpenses_ExpenseRecorded.PNG b/docs/userguideimages/ListExpenses_ExpenseRecorded.PNG new file mode 100644 index 0000000000..d170e908c2 Binary files /dev/null and b/docs/userguideimages/ListExpenses_ExpenseRecorded.PNG differ diff --git a/docs/userguideimages/ListExpenses_FilteredCategory.PNG b/docs/userguideimages/ListExpenses_FilteredCategory.PNG new file mode 100644 index 0000000000..d75587490f Binary files /dev/null and b/docs/userguideimages/ListExpenses_FilteredCategory.PNG differ diff --git a/docs/userguideimages/ListSavings_EmptyExpense.PNG b/docs/userguideimages/ListSavings_EmptyExpense.PNG new file mode 100644 index 0000000000..9f0a2b977f Binary files /dev/null and b/docs/userguideimages/ListSavings_EmptyExpense.PNG differ diff --git a/docs/userguideimages/ListSavings_EmptySaving.PNG b/docs/userguideimages/ListSavings_EmptySaving.PNG new file mode 100644 index 0000000000..b3d660056e Binary files /dev/null and b/docs/userguideimages/ListSavings_EmptySaving.PNG differ diff --git a/docs/userguideimages/ListSavings_ExpenseRecorded.PNG b/docs/userguideimages/ListSavings_ExpenseRecorded.PNG new file mode 100644 index 0000000000..33b758b07f Binary files /dev/null and b/docs/userguideimages/ListSavings_ExpenseRecorded.PNG differ diff --git a/docs/userguideimages/ListSavings_FilterCategory.PNG b/docs/userguideimages/ListSavings_FilterCategory.PNG new file mode 100644 index 0000000000..a2e3c0f4fa Binary files /dev/null and b/docs/userguideimages/ListSavings_FilterCategory.PNG differ diff --git a/docs/userguideimages/SetBudget_Existing.png b/docs/userguideimages/SetBudget_Existing.png new file mode 100644 index 0000000000..249f228556 Binary files /dev/null and b/docs/userguideimages/SetBudget_Existing.png differ diff --git a/docs/userguideimages/SetBudget_New.png b/docs/userguideimages/SetBudget_New.png new file mode 100644 index 0000000000..996d2e95aa Binary files /dev/null and b/docs/userguideimages/SetBudget_New.png differ diff --git a/docs/userguideimages/rec_addrec_EmptyList.png b/docs/userguideimages/rec_addrec_EmptyList.png new file mode 100644 index 0000000000..5359b55025 Binary files /dev/null and b/docs/userguideimages/rec_addrec_EmptyList.png differ diff --git a/docs/userguideimages/rec_addrec_NonEmptyList.png b/docs/userguideimages/rec_addrec_NonEmptyList.png new file mode 100644 index 0000000000..657853f3ff Binary files /dev/null and b/docs/userguideimages/rec_addrec_NonEmptyList.png differ diff --git a/docs/userguideimages/rec_viewexpenses_EmptyList.png b/docs/userguideimages/rec_viewexpenses_EmptyList.png new file mode 100644 index 0000000000..87483e03dd Binary files /dev/null and b/docs/userguideimages/rec_viewexpenses_EmptyList.png differ diff --git a/docs/userguideimages/rec_viewexpenses_NonEmptyList.png b/docs/userguideimages/rec_viewexpenses_NonEmptyList.png new file mode 100644 index 0000000000..0737315934 Binary files /dev/null and b/docs/userguideimages/rec_viewexpenses_NonEmptyList.png differ diff --git a/docs/userguideimages/rec_viewlists_emptyOutput.png b/docs/userguideimages/rec_viewlists_emptyOutput.png new file mode 100644 index 0000000000..f399f7c255 Binary files /dev/null and b/docs/userguideimages/rec_viewlists_emptyOutput.png differ diff --git a/docs/userguideimages/rec_viewlists_exampleOutput.jpg b/docs/userguideimages/rec_viewlists_exampleOutput.jpg new file mode 100644 index 0000000000..f55258d671 Binary files /dev/null and b/docs/userguideimages/rec_viewlists_exampleOutput.jpg differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66c01cfeba..878fe049c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/seedu/budgetbuddy/BudgetBuddy.java b/src/main/java/seedu/budgetbuddy/BudgetBuddy.java new file mode 100644 index 0000000000..de8c82d9ac --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/BudgetBuddy.java @@ -0,0 +1,109 @@ +package seedu.budgetbuddy; + +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.exception.InvalidRecurringExpensesFileException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Scanner; + +public class BudgetBuddy { + private Ui ui; + private Parser parser; + private ExpenseList expenses; + private SavingList savings; + private SplitExpenseList splitexpenses; + private RecurringExpenseLists recurringExpenseLists; + private Storage expensesStorage; + private Storage savingsStorage; + private Storage recurringExpensesStorage; + private Storage splitexpensesStorage; + private Storage defaultCurrency; + private Storage budgetStorage; + + + + public BudgetBuddy() { + ui = new Ui(); + parser = new Parser(); + expenses = new ExpenseList(); + savings = new SavingList(); + recurringExpenseLists = new RecurringExpenseLists(); + splitexpenses = new SplitExpenseList(); + expensesStorage = new Storage("./data/ExpenseFile.txt"); + savingsStorage = new Storage("./data/SavingsFile.txt"); + recurringExpensesStorage = new Storage("./data/RecurringExpensesFile.txt"); + splitexpensesStorage = new Storage("./data/SplitExpensesFile.txt"); + defaultCurrency = new Storage("./data/DefaultCurrency.txt"); + budgetStorage = new Storage("./data/BudgetFile.txt"); + } + + public void handleCommands(String input) { + Command command = parser.parseCommand(expenses, savings, splitexpenses, recurringExpenseLists, input); + + + if (command != null) { + command.execute(); + } else { + System.out.println("(Invalid command)"); + } + + try { + expensesStorage.saveExpenses(expenses.getExpenses()); + savingsStorage.saveSavings(savings.getSavings()); + recurringExpensesStorage.saveRecurringExpenses(recurringExpenseLists); + budgetStorage.saveBudgets(expenses.getBudgets()); + splitexpensesStorage.saveSplitExpenses(splitexpenses.getSplitExpenses()); + + defaultCurrency.saveCurrency(); + } catch (IOException e) { + System.out.println("Error saving to file."); + + } catch (InvalidRecurringExpensesFileException e) { + System.out.println(e.getMessage()); + } + + } + + public void run() { + Scanner scanner = new Scanner(System.in); + + try { + defaultCurrency.loadCurrency(); + this.expenses.getExpenses().addAll(expensesStorage.loadExpenses()); + this.savings.getSavings().addAll(savingsStorage.loadSavings()); + this.splitexpenses.getSplitExpenses().addAll(splitexpensesStorage.loadSplitExpenses()); + this.recurringExpenseLists = recurringExpensesStorage.loadRecurringExpensesList(); + this.expenses.getBudgets().addAll(budgetStorage.loadBudgets()); + + } catch (FileNotFoundException e) { + System.out.println("No existing files found. Starting fresh."); + } catch (IOException e) { + System.out.println("Could not create files. Please ensure all files are present and are not directories"); + } + + ui.showWelcome(); + + boolean isExit = false; + while (!isExit) { + String input = scanner.nextLine(); + + if (parser.isExitCommand(input)) { + isExit = true; + } else { + handleCommands(input); + } + } + + ui.showGoodbye(); + scanner.close(); + } + + public static void main(String[] args) { + new BudgetBuddy().run(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/Parser.java b/src/main/java/seedu/budgetbuddy/Parser.java new file mode 100644 index 0000000000..c7aac9453f --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/Parser.java @@ -0,0 +1,244 @@ +package seedu.budgetbuddy; + +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.commandcreator.ListBudgetCommandCreator; +import seedu.budgetbuddy.commandcreator.CommandCreator; +import seedu.budgetbuddy.commandcreator.AddExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.AddSavingCommandCreator; +import seedu.budgetbuddy.commandcreator.ChangeCurrencyCommandCreator; +import seedu.budgetbuddy.commandcreator.DeleteExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.EditExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.EditSavingsCommandCreator; +import seedu.budgetbuddy.commandcreator.FindExpensesCommandCreator; +import seedu.budgetbuddy.commandcreator.GetExpenseInsightsCommandCreator; +import seedu.budgetbuddy.commandcreator.GetSavingsInsightsCommandCreator; +import seedu.budgetbuddy.commandcreator.ListCommandCreator; +import seedu.budgetbuddy.commandcreator.ListSplittedExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.MenuCommandCreator; +import seedu.budgetbuddy.commandcreator.RecurringExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.ReduceSavingCommandCreator; +import seedu.budgetbuddy.commandcreator.SetBudgetCommandCreator; +import seedu.budgetbuddy.commandcreator.SettleSplitExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.SplitExpenseCommandCreator; +import seedu.budgetbuddy.commandcreator.GetBudgetCommandCreator; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.commons.CurrencyConverter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + + + +public class Parser { + + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + protected ArrayList expenseCategories; + protected ArrayList savingsCategories; + + public Parser() { + this.expenseCategories = new ArrayList<>(Arrays.asList("Housing", + "Groceries", "Utility", "Transport", "Entertainment", "Others")); + this.savingsCategories = new ArrayList<>(Arrays.asList("Salary", + "Investments", "Gifts", "Others")); + } + + public Boolean isRecCommand(String input) { + return input.startsWith("rec "); + } + + public Boolean isFindExpensesCommand(String input) { + return input.startsWith("find expenses"); + } + + /** + * Checks if the provided input starts with the word "list" . + * + * @param input The user input string + * @return true if user input starts with "list", else returns false + */ + public Boolean isListCommand(String input) { + LOGGER.log(Level.INFO, "Checking if input is a List Command"); + return input.startsWith("list"); + } + + /** + * Checks if the provided input starts with the word "menu" . + * + * @param input The user input string + * @return true if user input starts with "menu", else returns false + */ + public Boolean isMenuCommand(String input) { + LOGGER.log(Level.INFO, "Checking if Input is a Menu Command"); + return input.startsWith("menu"); + } + + /** + * Checks if the provided input starts with the word "bye" . + * + * @param input The user input string + * @return true if user input starts with "bye", else returns false + */ + public Boolean isExitCommand(String input) { + return input.startsWith("bye"); + } + + /** + * Checks if the provided input starts with the word "add expense" . + * + * @param input The user input string + * @return true if user input starts with "add expense", else returns false + */ + public Boolean isAddExpenseCommand(String input) { + return input.startsWith("add expense"); + } + + public Boolean isAddSavingCommand(String input) { + return input.startsWith("add savings"); + } + + public Boolean isEditExpenseCommand(String input) { + return input.startsWith("edit expense"); + } + + public Boolean isEditSavingCommand(String input) { + return input.startsWith("edit savings"); + } + + public Boolean isDeleteExpenseCommand(String input) { + return input.startsWith("delete expense"); + } + + public Boolean isReduceSavingCommand(String input) { + return input.startsWith("reduce savings"); + } + + /** + * Checks if the provided input starts with the phrase "change currency". + * + * @param input The user input string + * @return true if user input starts with "change currency", else returns false + */ + public Boolean isConvertCurrencyCommand(String input) { + LOGGER.log(Level.INFO, "Checking if input is Change Currency Command"); + return input.startsWith("change currency"); + } + + public Boolean isSplitExpenseCommand(String input) { + return input.startsWith("add shared bill"); + } + + public Boolean isListSplitExpenseCommand(String input) { + return input.contentEquals("check split bills"); + } + + public Boolean isSetBudgetCommand(String input){ + return input.startsWith("set budget"); + } + + public Boolean isGetBudgetCommand(String input) { + return input.startsWith("get budget"); + } + + public boolean isListBudgetCommand(String input){ + return input.startsWith("print budget"); + } + + public Boolean isSettleSplitExpenseCommand(String input) { + return input.startsWith("settle bill"); + } + + public Boolean isGetExpensesInsightsCommand(String input) { + return input.equalsIgnoreCase("get expenses insights"); + } + + public Boolean isGetSavingsInsightsCommand(String input) { + return input.equalsIgnoreCase("get savings insights"); + } + + + + /** + * Parses a string input into a Command object and returns the associated + * command to handle the user input + * + * @param input The user input string. + * @return A Command object corresponding to the user input, or null if the + * input is invalid. + */ + public Command parseCommand(ExpenseList expenses, SavingList savings, SplitExpenseList + splitexpenses, RecurringExpenseLists expensesList, String input) { + + CommandCreator commandCreator = null; + + if(isMenuCommand(input)) { + LOGGER.log(Level.INFO, "Confirmed that input is a menu command"); + commandCreator = new MenuCommandCreator(input); + } + if (isAddExpenseCommand(input)) { + commandCreator = new AddExpenseCommandCreator(expenses, input); + } + if (isAddSavingCommand(input)) { + commandCreator = new AddSavingCommandCreator(savings, input); + } + if (isEditExpenseCommand(input)) { + commandCreator = new EditExpenseCommandCreator(input, expenses); + } + if (isEditSavingCommand(input)) { + commandCreator = new EditSavingsCommandCreator(input, savings); + } + if (isDeleteExpenseCommand(input)) { + commandCreator = new DeleteExpenseCommandCreator(expenses, input); + } + if (isReduceSavingCommand(input)) { + commandCreator = new ReduceSavingCommandCreator(savings, input); + } + if (isListCommand(input.toLowerCase())) { + commandCreator = new ListCommandCreator(expenses, savings, input); + } + if (isListSplitExpenseCommand(input)) { + commandCreator = new ListSplittedExpenseCommandCreator(input, splitexpenses); + } + if (isFindExpensesCommand(input)) { + commandCreator = new FindExpensesCommandCreator(input, expenses); + } + if (isRecCommand(input)) { + commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenses); + } + if (isConvertCurrencyCommand(input.toLowerCase())) { + commandCreator = new ChangeCurrencyCommandCreator(input, savings, expenses, splitexpenses, + expensesList, new CurrencyConverter()); + } + if (isSplitExpenseCommand(input)) { + commandCreator = new SplitExpenseCommandCreator(splitexpenses, input); + } + if (isSettleSplitExpenseCommand(input)) { + commandCreator = new SettleSplitExpenseCommandCreator(input, splitexpenses); + } + if (isSetBudgetCommand(input)) { + commandCreator = new SetBudgetCommandCreator(expenses, input); + } + if (isGetBudgetCommand(input)) { + commandCreator = new GetBudgetCommandCreator(expenses, input); + } + if (isListBudgetCommand(input)){ + commandCreator = new ListBudgetCommandCreator(expenses); + } + if (isGetExpensesInsightsCommand(input)) { + commandCreator = new GetExpenseInsightsCommandCreator(expenses); + } + if (isGetSavingsInsightsCommand(input)) { + commandCreator = new GetSavingsInsightsCommandCreator(savings); + } + + if (commandCreator == null) { + return null; + } + + return commandCreator.createCommand(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/Storage.java b/src/main/java/seedu/budgetbuddy/Storage.java new file mode 100644 index 0000000000..73da4e9ab6 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/Storage.java @@ -0,0 +1,555 @@ +package seedu.budgetbuddy; + +import seedu.budgetbuddy.commons.Saving; +import seedu.budgetbuddy.commons.SplitExpense; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.RecurringExpenseList; +import seedu.budgetbuddy.commons.DefaultCurrency; +import seedu.budgetbuddy.commons.Budget; + +import seedu.budgetbuddy.exception.BudgetBuddyException; +import seedu.budgetbuddy.exception.InvalidRecurringExpensesFileException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.Currency; +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + + +public class Storage { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static final double MAX_AMOUNT = 1_000_000_000_000.00; + private final String filePath; + + private ArrayList expenseCategories = new ArrayList<>(Arrays.asList("Housing" + , "Groceries", "Utility", "Transport", "Entertainment", "Others")); + + public Storage(String filePath) { + this.filePath = filePath; + ensureDirectoryExists(); + } + + private void ensureDirectoryExists() { + File file = new File(filePath); + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); // This will create the directory if it doesn't exist + } + try { + file.createNewFile(); // This will create the file if it doesn't exist + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Check if the provided amount is a valid acceptable amount. Checks if amount is 0 or negative, larger than the + * allowed maximum amount and if amount will be round to 0.00 at 2.dp + * + * @param amount The amount to be checked + * @throws BudgetBuddyException If amount is 0, negative, larger than allowed maximum , or is rounded to 0.00 + * at 2.dp + */ + private void checkValidAmount(Double amount) throws BudgetBuddyException{ + if (amount <= 0 || amount > MAX_AMOUNT || amount < 0.005) { + throw new BudgetBuddyException("Invalid Amount detected. Possible Corrupted File"); + } + } + + /** + * Checks if the provided category is a valid category + * + * @param category The category to be checked + * @throws BudgetBuddyException If category does not match any of the expense categories exactly + */ + private void checkValidCategory(String category) throws BudgetBuddyException { + if (!expenseCategories.contains(category)) { + throw new BudgetBuddyException("Invalid Category detected. Possible Corrupted File"); + } + } + + /** + * Checks for the presence of the `!`, `|`, or empty string in the description + * + * @param description The description to be checked + * @throws BudgetBuddyException If the description contains a `|`, `!` or is empty + */ + private void checkValidDescription(String description) throws BudgetBuddyException { + if (description.contains("|") || description.contains("!") || description.isEmpty()) { + throw new BudgetBuddyException("Invalid description detected. Possible Corrupted File"); + } + } + + /** + * Checks for the proper format for the title of the recurring expense list. + * + * @param line The line to be checked + * @throws BudgetBuddyException If the end `!!!` is not found at the end of the line + */ + private void checkValidTitle(String line) throws BudgetBuddyException { + int indexOfEndExclamation = line.indexOf("!!!", 4); + int endIndexOfEndExclamation = indexOfEndExclamation + "!!!".length(); + + if (endIndexOfEndExclamation != line.length() || line.contains("|")) { + throw new BudgetBuddyException("Invalid ListName title detected. Possible Corrupted File"); + } + } + + /** + * Checks for the presence of the `!`, `|`, or empty string in the provided `listName` + * + * @param listName The description to be checked + * @throws BudgetBuddyException If the `listName` contains a `|`, `!` or is empty + */ + private void checkValidListName(String listName) throws BudgetBuddyException { + if (listName.contains("!") || listName.contains("|") || listName.isEmpty()) { + throw new BudgetBuddyException("Invalid listName detected. Possible Corrupted File"); + } + } + + /** + * Loads a list of expenses from a file. + * If an exception occurs during the loading process (e.g., if the file is corrupted), + * the expenses list file will be reset. + * + * @return A list of {@link Expense} objects loaded from the file. + * @throws IOException If an error occurs when accessing the file. + */ + public List loadExpenses() throws IOException { + File file = new File(filePath); + List expenses = new ArrayList<>(); + Scanner scanner = new Scanner(file); + try { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] parts = line.split("\\|"); + LocalDate date = LocalDate.parse(parts[0].trim()); + String category = parts[1].trim(); + double amount = Double.parseDouble(parts[2].trim()); + String description = parts[3].trim(); + Expense expense = new Expense(date, category, amount, description); + expenses.add(expense); + } + } catch (Exception e) { + LOGGER.log(Level.INFO, "Exception successfully caught. Error has been handled"); + System.out.println(e.getMessage()); + System.out.println("Your Expenses File is corrupted, resetting the file...."); + resetExpenseListFile(); + return expenses; + } finally { + scanner.close(); + } + return expenses; + } + + /** + * Saves a list of expenses to a file. + * If an IOException occurs, the expenses list file will be reset. + * + * @param expenses A list of {@link Expense} objects to save to the file. + * @throws IOException If an error occurs during writing to the file. + */ + public void saveExpenses(List expenses) throws IOException { + ensureDirectoryExists(); // Ensure directory and file exist before writing + FileWriter writer = null; + try { + writer = new FileWriter(filePath, false); + for (Expense expense : expenses) { + writer.write(String.format("%s | %s | %.2f | %s\n", + expense.getDateAdded(), expense.getCategory(), expense.getAmount(), expense.getDescription())); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "IOException occurred while saving expenses. " + + "Resetting expense list file.", e); + resetExpenseListFile(); // Reset the expense list file if an exception occurs + throw e; // Re-throw the exception to indicate that saving was not successful + } finally { + if (writer != null) { + writer.close(); + } + } + } + + /** + * Resets the expense list file. If the file exists, it is deleted and a new empty file is created. + * + * @throws IOException If deleting the existing file or creating a new file fails. + */ + public void resetExpenseListFile() throws IOException { + File file = new File(filePath); + file.delete(); + file.createNewFile(); + FileWriter writer = new FileWriter(filePath, false); + writer.write(""); + writer.close(); + } + + /** + * Loads a list of savings from the specified file. + * If an exception occurs during the loading process (e.g., if the file is corrupted), + * the savings list file will be reset. + * + * @return A list of {@link Saving} objects representing the savings loaded from the file. + * @throws IOException If there is an issue with file access that prevents the method from reading the savings. + */ + public List loadSavings() throws IOException { + List savings = new ArrayList<>(); + File file = new File(filePath); + Scanner scanner = new Scanner(file); + try { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] parts = line.split("\\|"); + String category = parts[0].trim(); + double amount = Double.parseDouble(parts[1].trim()); + Saving saving = new Saving(category, amount); + savings.add(saving); + } + } catch (Exception e) { + LOGGER.log(Level.INFO, "Exception caught while loading savings. Resetting savings list file.", e); + System.out.println(e.getMessage()); + System.out.println("Your Savings File is corrupted, resetting the file...."); + resetSavingsListFile(); + return savings; + } finally { + scanner.close(); + } + return savings; + } + + /** + * Saves the list of savings to the specified file. + * If an IOException occurs, the savings list file will be reset. + * + * @param savings A list of {@link Saving} objects that represent the savings to save to the file. + * @throws IOException If an IOException occurs during file writing, indicating the savings could not be saved. + */ + public void saveSavings(List savings) throws IOException { + ensureDirectoryExists(); // Ensure directory and file exist before writing + FileWriter writer = null; + try { + writer = new FileWriter(filePath, false); + for (Saving saving : savings) { + writer.write(String.format("%s | %.2f\n", + saving.getCategory(), saving.getAmount())); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "IOException occurred while saving savings. Resetting savings list file.", e); + resetSavingsListFile(); + throw e; + } finally { + if (writer != null) { + writer.close(); + } + } + } + + /** + * Resets the savings list file. If the file exists, it is deleted, and a new empty file is created. + * This method is typically called when the file is found to be corrupted or + * when an issue arises during file operations. + * + * @throws IOException If there is an issue with file access that prevents the method + * from deleting or creating the file. + */ + public void resetSavingsListFile() throws IOException { + File file = new File(filePath); + file.delete(); + file.createNewFile(); + FileWriter writer = new FileWriter(filePath, false); + writer.write(""); + writer.close(); + } + + // @@author itsmejr257 + /** + * Deletes the existing recurring expenses file and create a new, empty file. + * This method is used to reset the recurring expenses file when it has been detected to be corrupted + * + * @throws IOException If there is an error deleting the old file or creating the new file + */ + public void resetRecurringExpensesListFile() throws IOException { + File file = new File(filePath); + file.delete(); + file.createNewFile(); + FileWriter writer = new FileWriter(filePath, false); + writer.write(""); + writer.close(); + } + + /** + * Parses a line of text from the recurring expenses file, adding either a recurring expense list, or an expense + * into a recurring expense list. If the line begins with a `!!!`, it adds a recurring Expense List with the name + * being the string between the two `!!!`. Else, the line contains details on an individual expense + * and adds the expense to the specified list number + * + * @param recurringExpenses The list of ExpenseLists to which the parsed data will be added to + * @param line The line of text to be parsed + * @throws BudgetBuddyException If the format of the line is corrupted + */ + public void parseRecurringExpensesFile(ArrayList recurringExpenses, String line) + throws BudgetBuddyException{ + + if (line.startsWith("!!!")) { + checkValidTitle(line); + int indexOfStartExclamation = line.indexOf("!!!", 0); + int indexOfStartOfListName = indexOfStartExclamation + 3; + + int indexOfEndExclamation = line.indexOf("!!!", 4); + int indexOfEndOfListName = indexOfEndExclamation; + + String name = line.substring(indexOfStartOfListName, indexOfEndOfListName).trim(); + checkValidListName(name); + + ExpenseList expenses = new RecurringExpenseList(name, new ArrayList<>()); + recurringExpenses.add(expenses); + } else { + String[] parts = line.split("\\|"); + + if (parts.length > 5) { + LOGGER.log(Level.WARNING, "Invalid RecurringExpensesFile detected, throwing Exception"); + throw new BudgetBuddyException("Invalid Format of Line : There should only be 4 Dividers"); + } + + int listNumber = Integer.parseInt(parts[0].trim()); + LocalDate dateAdded = LocalDate.parse(parts[1].trim()); + + String category = parts[2].trim(); + checkValidCategory(category); + + double amount = Double.parseDouble(parts[3].trim()); + checkValidAmount(amount); + + String description = parts[4].trim(); + checkValidDescription(description); + + Expense expense = new Expense(dateAdded, category, amount, description); + + int listNumberAsArrayIndex = listNumber - 1; + ExpenseList expenses = recurringExpenses.get(listNumberAsArrayIndex); + expenses.getExpenses().add(expense); + } + + } + + /** + * Loads the recurring expenses from a file into a RecurringExpenseLists object. + * + * @return RecurringExpenseLists containing all parsed recurring expenses from the file. + * @throws IOException If there is an error when reading the file + */ + public RecurringExpenseLists loadRecurringExpensesList() throws IOException{ + File file = new File(filePath); + ArrayList recurringExpenses = new ArrayList<>(); + + try { + Scanner scanner = new Scanner(file); + + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.isEmpty()) { + continue; + } + parseRecurringExpensesFile(recurringExpenses, line); + } + + scanner.close(); + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(recurringExpenses); + return recurringExpenseLists; + } catch (Exception e) { + LOGGER.log(Level.INFO, "Exception successfully caught. Error has been handled"); + System.out.println("Error Detected : " + e.getMessage()); + System.out.println("Your Recurring Expenses File is corrupted, resetting the file...."); + resetRecurringExpensesListFile(); + return new RecurringExpenseLists(); + } + + } + + /** + * Saves the details of recurring expenses into a file from a provided RecurringExpenseLists object. + * + * @param recurringExpenseLists The RecurringExpenseLists object containing all expense lists and expenses to be + * saved + * @throws InvalidRecurringExpensesFileException If the saving of the file was not successful + * @throws IOException If an error occurs when validating whether the RecurringExpensesFile.txt exists + */ + public void saveRecurringExpenses(RecurringExpenseLists recurringExpenseLists) + throws InvalidRecurringExpensesFileException, IOException { + + ensureDirectoryExists(); + + try { + FileWriter writer = new FileWriter(filePath, false); + int numberOfRecurringExpenseList = recurringExpenseLists.getSize(); + + for (int i = 0; i < numberOfRecurringExpenseList; i++) { + int listNumber = i + 1; + ExpenseList expenseList = recurringExpenseLists.getExpenseListAtListNumber(listNumber); + ArrayList expenses = expenseList.getExpenses(); + String listName = expenseList.getName(); + + writer.write(String.format("!!! %s !!!\n", listName)); + + for (Expense expense : expenses) { + writer.write(String.format("%d | %s | %s | %.2f | %s\n" + , listNumber + , expense.getDateAdded(), expense.getCategory() + , expense.getAmount(), expense.getDescription())); + } + + } + writer.close(); + + } catch (IOException e) { + resetRecurringExpensesListFile(); + throw new InvalidRecurringExpensesFileException("The RecurringExpensesFile seems to not be valid" + + ", file has been reinitialized. Run a command to save your recurringexpenses"); + } + + } + // @@author + + + /** + * Saves the default currency to the specified file path. + * + * @author sweijie24 + * @throws IOException if an I/O error occurs while writing to the file + */ + public void saveCurrency() throws IOException { + assert filePath != null : "File path should not be null"; + + ensureDirectoryExists(); + + FileWriter writer = new FileWriter(filePath, false); + + try { + Currency currentCurrency = DefaultCurrency.getDefaultCurrency(); + writer.write("Default Currency: " + currentCurrency); + writer.close(); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Problem saving currency code", e); + } + } + + //@@author yyangdaa + /** + * Loads the split expenses from the specified file path. + * + * @return A list of {@link SplitExpense} objects loaded from the file. + * @throws FileNotFoundException If the file does not exist. + */ + public List loadSplitExpenses() throws FileNotFoundException { + File file = new File(filePath); + List splitExpenses = new ArrayList<>(); + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] parts = line.split("\\|"); + LocalDate date = LocalDate.parse(parts[0].trim()); + double amount = Double.parseDouble(parts[1].trim()); + int numberOfPeople = Integer.parseInt(parts[2].trim()); + String description = parts[3].trim(); + SplitExpense splitExpense = new SplitExpense(date, amount, numberOfPeople, description); + splitExpenses.add(splitExpense); + } + scanner.close(); + return splitExpenses; + } + + + //@@author yyangdaa + /** + * Saves the list of split expenses to the specified file path. + * + * @param splitExpenses A list of {@link SplitExpense} objects to save to the file. + * @throws IOException If an error occurs during writing to the file. + */ + public void saveSplitExpenses(List splitExpenses) throws IOException { + ensureDirectoryExists(); + + FileWriter writer = new FileWriter(filePath, false); // Overwrite the file + for (SplitExpense splitExpense : splitExpenses) { + writer.write(String.format("%s | %.2f | %d | %s\n", + splitExpense.getDateAdded().toString(), + splitExpense.getAmount(), + splitExpense.getNumberOfPeople(), + splitExpense.getDescription())); + } + writer.close(); + } + + public List loadBudgets() throws FileNotFoundException { + File file = new File(filePath); + Scanner scanner = new Scanner(file); + List loadedBudgets = new ArrayList<>(); + + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] parts = line.split("\\|"); + String category = parts[0].trim(); + double budgetAmount = Double.parseDouble(parts[1].trim()); + Budget budget = new Budget(category, budgetAmount); + loadedBudgets.add(budget); + } + + scanner.close(); + return loadedBudgets; + } + + public void saveBudgets(List budgets) throws IOException { + FileWriter writer = new FileWriter(filePath); + + for (Budget budget : budgets) { + writer.write(String.format("%s|%.2f\n", budget.getCategory(), budget.getBudget())); + } + + writer.flush(); + writer.close(); + } + + /** + * Loads currency data from the specified file path and sets the default currency accordingly. + * + * @author sweijie24 + * @throws FileNotFoundException if the specified file path does not exist + */ + public void loadCurrency() throws FileNotFoundException { + + assert filePath != null : "File path should not be null"; + + File file = new File(filePath); + assert file.exists() : "Currency file does not exist"; + assert file.isFile() : "Currency file is not a regular file"; + + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + assert line != null : "Line should not be null"; + + String[] parts = line.split(": "); + assert parts.length == 2 : "Invalid line format"; + + String currencyCode = parts[1].trim(); + assert !currencyCode.isEmpty() : "Currency code should not be empty"; + + Currency currency = Currency.getInstance(currencyCode); + + DefaultCurrency.setDefaultCurrency(currency); + } + scanner.close(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/Ui.java b/src/main/java/seedu/budgetbuddy/Ui.java new file mode 100644 index 0000000000..a86fdae09e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/Ui.java @@ -0,0 +1,129 @@ +package seedu.budgetbuddy; + +import java.util.Scanner; + +public class Ui { + private static final String DIVIDER = "__________________________________________________"; + + public void showWelcome() { + System.out.println(DIVIDER); + System.out.println(" BudgetBuddy"); + System.out.println(DIVIDER); + System.out.println("Welcome to BudgetBuddy, to start, please type \"menu INDEX\" " + + "to view commands for the respective functions"); + System.out.println("To view all menu items again, type \"menu\"."); + System.out.println(DIVIDER); + System.out.println("0. Display the whole menu"); + System.out.println("1. Manage Expenses 2. Manage Savings"); + System.out.println("3. View Expenses 4. View Savings"); + System.out.println("5. Find Expenses 6. Divide Bills"); + System.out.println("7. Manage Recurring Bills 8. Change Currency"); + System.out.println("9. Manage Budget 10. Get Graphical Insights"); + System.out.println(DIVIDER); + } + + public void printDivider() { + System.out.println(DIVIDER); + } + public void showGoodbye() { + System.out.println("Goodbye! Thank you for using BudgetBuddy."); + } + + public void showMenuTitles() { + System.out.println(DIVIDER); + System.out.println("Menu Options:"); + System.out.println("0. Display the whole menu"); + System.out.println("1. Manage Expenses 2. Manage Savings"); + System.out.println("3. View Expenses 4. View Savings"); + System.out.println("5. Find Expenses 6. Divide Bills"); + System.out.println("7. Manage Recurring Bills 8. Change Currency"); + System.out.println("9. Manage Budget 10. Get Graphical Insights"); + System.out.println("Use 'menu INDEX' to select an option"); + System.out.println(DIVIDER); + } + + // Method to get user confirmation from the console + public boolean getUserConfirmation() { + Scanner scanner = new Scanner(System.in); + System.out.println("Do you want to proceed with adding this expense? (Any input that " + + "is not 'yes' is treated as a no)"); + String userInput = scanner.nextLine().trim().toLowerCase(); + return userInput.equals("yes"); + } + + /** + * Displays the menu item based on the given index. + * @param index The index of the menu item to display. + */ + public void showMenuItem(int index) { + assert index >= 0 : "Index must be a positive integer"; + + System.out.println(DIVIDER); + switch (index) { + case 1: + System.out.println("Manage Expenses"); + System.out.println("add expense c/CATEGORY a/AMOUNT d/DESCRIPTION"); + System.out.println("edit expense c/CATEGORY i/INDEX a/AMOUNT d/DESCRIPTION"); + System.out.println("delete expense i/INDEX"); + break; + case 2: + System.out.println("Manage Savings"); + System.out.println("add savings c/CATEGORY a/AMOUNT"); + System.out.println("edit savings c/CATEGORY a/AMOUNT"); + System.out.println("reduce savings c/CATEGORY a/AMOUNT"); + break; + case 3: + System.out.println("View Expenses"); + System.out.println("list expenses [CATEGORY]"); + break; + case 4: + System.out.println("View Savings"); + System.out.println("list savings [CATEGORY]"); + break; + case 5: + System.out.println("Find Expenses"); + System.out.println("find expenses d/DESCRIPTION morethan/MINAMOUNT lessthan/MAXAMOUNT " + + "(Choose the parameters according to what you wish to search for)"); + break; + case 6: + System.out.println("add shared bills"); + System.out.println("add shared bill a/AMOUNT n/NUMBER_OF_PEOPLE d/DESCRIPTION"); + System.out.println("check split bills"); + System.out.println("settle bill i/INDEX"); + break; + case 7: + System.out.println("Recurring Bills"); + System.out.println("rec newlist LISTNAME"); + System.out.println("rec removelist LISTNUMBER"); + System.out.println("rec viewlists"); + System.out.println("rec newexpense to/LISTNUMBER c/CATEGORY a/AMOUNT d/DESCRIPTION"); + System.out.println("rec viewexpenses LISTNUMBER"); + System.out.println("rec addrec LISTNUMBER"); + break; + case 8: + System.out.println("Change Currency"); + System.out.println("change currency [CURRENCY_CODE]"); + System.out.println("Default Currency: SGD"); + System.out.println("Choose from the following currency codes:"); + System.out.println("USD SGD EUR"); + System.out.println("JPY KRW MYR"); + System.out.println("CNY HKD"); + break; + case 9: + System.out.println("Manage budget"); + System.out.println("set budget c/CATEGORY b/BUDGET"); + System.out.println("get budget c/CATEGORY"); + System.out.println("print budget"); + break; + case 10: + System.out.println("Get Graphical Insights"); + System.out.println("get savings insights"); + System.out.println("get expenses insights"); + break; + default: + System.out.println("Invalid menu index."); + break; + } + System.out.println(DIVIDER); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/AddExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/AddExpenseCommand.java new file mode 100644 index 0000000000..2f7af5cd16 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/AddExpenseCommand.java @@ -0,0 +1,29 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + + +public class AddExpenseCommand extends Command{ + private ExpenseList expenses; + private final String category; + private final String amount; + private final String description; + + + public AddExpenseCommand (ExpenseList expenses,String category, String amount, String description) { + this.expenses = expenses; + this.category = category; + this.amount = amount; + this.description = description; + } + + @Override + public void execute() { + try { + expenses.addExpense(this.category,this.amount,this.description); + } catch (BudgetBuddyException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/AddSavingCommand.java b/src/main/java/seedu/budgetbuddy/command/AddSavingCommand.java new file mode 100644 index 0000000000..b2f7af3e6a --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/AddSavingCommand.java @@ -0,0 +1,27 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class AddSavingCommand extends Command { + + private SavingList savings; + private final String category; + private final String amount; + + public AddSavingCommand(SavingList savings, String category, String amount) { + this.category = category; + this.amount = amount; + this.savings = savings; + } + + @Override + public void execute(){ + try { + + savings.addSaving(this.category, this.amount); + } catch (BudgetBuddyException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/ChangeCurrencyCommand.java b/src/main/java/seedu/budgetbuddy/command/ChangeCurrencyCommand.java new file mode 100644 index 0000000000..78055e58fa --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ChangeCurrencyCommand.java @@ -0,0 +1,44 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.CurrencyConverter; +import seedu.budgetbuddy.commons.DefaultCurrency; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; + +import java.util.Currency; + +/** + * @@author sweijie24 + */ +public class ChangeCurrencyCommand extends Command { + + private Currency newCurrency; + private SavingList savings; + private ExpenseList expenses; + private SplitExpenseList splitExpenses; + private RecurringExpenseLists recurringExpenseLists; + private CurrencyConverter currencyConverter; + + public ChangeCurrencyCommand(Currency newCurrency, SavingList savings, ExpenseList expenses, SplitExpenseList + splitExpenses, RecurringExpenseLists recurringExpenseLists, + CurrencyConverter currencyConverter) { + this.newCurrency = newCurrency; + this.savings = savings; + this.expenses = expenses; + this.splitExpenses = splitExpenses; + this.recurringExpenseLists = recurringExpenseLists; + this.currencyConverter = currencyConverter; + } + + @Override + public void execute() { + currencyConverter.convertSavingCurrency(newCurrency, savings); + currencyConverter.convertExpenseCurrency(newCurrency, expenses); + currencyConverter.convertSplitExpenseCurrency(newCurrency, splitExpenses); + currencyConverter.convertRecurringExpensesCurrency(newCurrency, recurringExpenseLists); + currencyConverter.convertBudgetCurrency(newCurrency, expenses); + DefaultCurrency.setDefaultCurrency(newCurrency); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/Command.java b/src/main/java/seedu/budgetbuddy/command/Command.java new file mode 100644 index 0000000000..152f642cc7 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/Command.java @@ -0,0 +1,10 @@ +package seedu.budgetbuddy.command; + +public abstract class Command { + + public String getDescription() { + return "Empty"; + } + public void execute() { + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/DeleteExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/DeleteExpenseCommand.java new file mode 100644 index 0000000000..762066c040 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/DeleteExpenseCommand.java @@ -0,0 +1,18 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class DeleteExpenseCommand extends Command{ + private int index; + private ExpenseList expenses; + + public DeleteExpenseCommand(ExpenseList expenses, int index) { + this.index = index; + this.expenses = expenses; + } + + @Override + public void execute() { + expenses.deleteExpense(index); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/EditExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/EditExpenseCommand.java new file mode 100644 index 0000000000..5b9f877754 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/EditExpenseCommand.java @@ -0,0 +1,25 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class EditExpenseCommand extends Command{ + private ExpenseList expenses; + private String category; + private int index; + private double amount; + private String description; + + public EditExpenseCommand(ExpenseList expenses, String category, int index, + double amount, String description) { + this.expenses = expenses; + this.category = category; + this.index = index; + this.amount = amount; + this.description = description; + } + + @Override + public void execute() { + expenses.editExpense(category, index, amount, description); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/EditSavingCommand.java b/src/main/java/seedu/budgetbuddy/command/EditSavingCommand.java new file mode 100644 index 0000000000..6607b80b88 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/EditSavingCommand.java @@ -0,0 +1,20 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SavingList; +public class EditSavingCommand extends Command{ + + private SavingList savings; + private String category; + private double amount; + + public EditSavingCommand(SavingList savings, String category, double amount) { + this.savings = savings; + this.category = category; + this.amount = amount; + } + + @Override + public void execute() { + savings.editSaving(category, amount); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/FindExpensesCommand.java b/src/main/java/seedu/budgetbuddy/command/FindExpensesCommand.java new file mode 100644 index 0000000000..b8f922ce45 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/FindExpensesCommand.java @@ -0,0 +1,113 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command that finds and lists expenses based on a provided criteria. + * Criteria can include description, minimum and maximum amounts + */ +public class FindExpensesCommand extends Command { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private ExpenseList expenses; + private String description; + private Double minAmount; + private Double maxAmount; + private Ui ui; + + /** + * Constructs a FindExpenseCommand with the specified expense list, description, minimum amount and maximum amount + * + * + * @param expenses The expenseList to filter the expenses + * @param description The description to be filtered, can be null or empty + * @param minAmount The minimum amount of expense to be filtered, can be null + * @param maxAmount The maximum amount of expense to be filtered, can be null + */ + public FindExpensesCommand(ExpenseList expenses, String description, Double minAmount, Double maxAmount) { + if (minAmount != null && maxAmount != null) { + assert minAmount < maxAmount : "Minimum amount cannot be larger than Maximum Amount"; + } + + ui = new Ui(); + this.expenses = expenses; + + if(description == null || description.isEmpty()) { + this.description = ""; + } else { + this.description = description; + } + this.minAmount = minAmount; + this.maxAmount = maxAmount; + } + + /** + * Prints an initialization message that informs user of the parameters used when filtering. Diplays + * an N.A. for filters which would not be used + */ + private void printInitializationMessage() { + ui.printDivider(); + System.out.println("Looking for Expenses with the following parameters : "); + + System.out.println("Description : "); + if (description == null || description.isEmpty()) { + System.out.println("N.A"); + } else { + System.out.println(description); + } + + System.out.println("Minimum Amount : "); + if (minAmount == null) { + System.out.println("N.A"); + } else { + System.out.println(minAmount); + } + + System.out.println("Maximum Amount : "); + if (maxAmount == null) { + System.out.println("N.A"); + } else { + System.out.println(maxAmount); + } + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public void execute() { + + LOGGER.log(Level.INFO, "Start processing of Find Command"); + + assert minAmount == null || maxAmount == null || minAmount <= maxAmount + : "Minimum amount cannot be larger than Maximum Amount"; + + LOGGER.log(Level.INFO, "Creating filteredExpenses"); + + printInitializationMessage(); + ArrayList filteredExpenses = expenses.filterExpenses(description, minAmount, maxAmount); + ExpenseList filteredExpenseList = new ExpenseList(filteredExpenses); + + if (filteredExpenses.isEmpty()) { + LOGGER.log(Level.INFO, "filtered expenses is empty, returning no expenses found"); + + ui.printDivider(); + System.out.println("No matching expenses found."); + ui.printDivider(); + } else { + LOGGER.log(Level.INFO, "Filtered expenses contains items, returning matching expenses"); + + ui.printDivider(); + System.out.println("Here are the matching expenses : "); + filteredExpenseList.listExpenses(null); + ui.printDivider(); + } + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/GetBudgetCommand.java b/src/main/java/seedu/budgetbuddy/command/GetBudgetCommand.java new file mode 100644 index 0000000000..424c4ad540 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/GetBudgetCommand.java @@ -0,0 +1,18 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class GetBudgetCommand extends Command { + private ExpenseList expenseList; + private String category; + + public GetBudgetCommand(ExpenseList expenseList, String category) { + this.expenseList = expenseList; + this.category = category; + } + + @Override + public void execute() { + expenseList.getBudgetAndListExpensesForCategory(category); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/GetExpenseInsightsCommand.java b/src/main/java/seedu/budgetbuddy/command/GetExpenseInsightsCommand.java new file mode 100644 index 0000000000..076d57e95d --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/GetExpenseInsightsCommand.java @@ -0,0 +1,17 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class GetExpenseInsightsCommand extends Command { + + private ExpenseList expenseList; + + public GetExpenseInsightsCommand(ExpenseList expenseList) { + this.expenseList = expenseList; + } + + @Override + public void execute() { + expenseList.getExpenseInsights(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/GetSavingsInsightsCommand.java b/src/main/java/seedu/budgetbuddy/command/GetSavingsInsightsCommand.java new file mode 100644 index 0000000000..bad6ef63a6 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/GetSavingsInsightsCommand.java @@ -0,0 +1,17 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SavingList; + +public class GetSavingsInsightsCommand extends Command { + + private SavingList savingList; + + public GetSavingsInsightsCommand(SavingList savingList) { + this.savingList = savingList; + } + + @Override + public void execute() { + savingList.getSavingsInsights(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/ListBudgetCommand.java b/src/main/java/seedu/budgetbuddy/command/ListBudgetCommand.java new file mode 100644 index 0000000000..ec2ea88782 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ListBudgetCommand.java @@ -0,0 +1,72 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.Budget; +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.Ui; + +public class ListBudgetCommand extends Command{ + private ExpenseList expenseList; + public ListBudgetCommand(ExpenseList expenseList){ + this.expenseList = expenseList; + } + + @Override + public void execute() { + Ui ui = new Ui(); + + // Print all budgets in a table format + System.out.printf("%-20s | %-15s | %-15s | %-15s | %-15s%n", "Category", "Budget", "Spent", + "Remaining", "% Spent"); + System.out.println(String.join("", java.util.Collections.nCopies(88, "-"))); // Creates a line + + if (expenseList.getBudgets().isEmpty()) { + System.out.println("No budgets set."); + } else { + for (Budget budget : expenseList.getBudgets()) { + String category = budget.getCategory(); + double budgetAmount = budget.getBudget(); + double categorySpent = expenseList.getExpenses().stream() + .filter(expense -> expense.getCategory().equalsIgnoreCase(category)) + .mapToDouble(Expense::getAmount) + .sum(); + String remaining = categorySpent > budgetAmount ? "Exceeded" : + String.format("$%.2f", budgetAmount - categorySpent); + double percentSpent = (categorySpent / budgetAmount) * 100; + + System.out.printf("%-20s | $%-14.2f | $%-14.2f | %-14s | %-13.2f%%%n", + category, budgetAmount, categorySpent, remaining, percentSpent); + System.out.println(String.join("", + java.util.Collections.nCopies(88, "-"))); // Creates a line + } + } + + System.out.println("\nCategories above budget:"); + System.out.printf("%-20s | %-15s%n", "Category", "Exceeded by"); + System.out.println(String.join("", java.util.Collections.nCopies(44, "-"))); + + boolean found = false; + for (String category : expenseList.getCategories()) { + double totalSpent = expenseList.getExpenses().stream() + .filter(expense -> expense.getCategory().equalsIgnoreCase(category)) + .mapToDouble(Expense::getAmount) + .sum(); + + Budget budgetForCategory = expenseList.getBudgets().stream() + .filter(budget -> budget.getCategory().equalsIgnoreCase(category)) + .findFirst() + .orElse(null); + + if (budgetForCategory != null && totalSpent > budgetForCategory.getBudget()) { + double exceededBy = totalSpent - budgetForCategory.getBudget(); + System.out.printf("%-20s | $%-14.2f%n", category, exceededBy); + found = true; + } + } + + if (!found) { + System.out.println("No categories are above budget."); + } + System.out.println(String.join("", java.util.Collections.nCopies(44, "-"))); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/ListExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/ListExpenseCommand.java new file mode 100644 index 0000000000..1748df91b8 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ListExpenseCommand.java @@ -0,0 +1,37 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class ListExpenseCommand extends Command { + private ExpenseList expenses; + private String filterCategory; + + /** + * Creates a ListExpenseCommand object. + * + * @param expenses The list of expenses. + */ + public ListExpenseCommand(ExpenseList expenses) { + this.expenses = expenses; + } + + /** + * Creates a ListExpenseCommand object. + * + * @param expenses The list of expenses. + * @param filterCategory The category to filter the expenses by. + */ + + public ListExpenseCommand(ExpenseList expenses, String filterCategory) { + this.expenses = expenses; + this.filterCategory = filterCategory; + } + + /** + * Lists all the expenses. + */ + @Override + public void execute() { + expenses.listExpenses(filterCategory); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/ListSavingsCommand.java b/src/main/java/seedu/budgetbuddy/command/ListSavingsCommand.java new file mode 100644 index 0000000000..f79bdac571 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ListSavingsCommand.java @@ -0,0 +1,40 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.SavingList; + +public class ListSavingsCommand extends Command { + private SavingList savings; + private ExpenseList expenses; + private String filterCategory; + + /** + * Creates a ListSavingsCommand object. + * + * @param savings The list of savings. + * @param expenses The list of expenses. + */ + public ListSavingsCommand(SavingList savings, ExpenseList expenses) { + this.savings = savings; + this.expenses = expenses; + } + + /** + * Creates a ListSavingsCommand object. + * + * @param savings The list of savings. + * @param expenses The list of expenses. + * @param filterCategory The category to filter the savings by. + */ + public ListSavingsCommand(SavingList savings, ExpenseList expenses, String filterCategory) { + this.savings = savings; + this.expenses = expenses; + this.filterCategory = filterCategory; + } + + @Override + public void execute() { + savings.listSavings(this.filterCategory, this.expenses); + } +} + diff --git a/src/main/java/seedu/budgetbuddy/command/ListSplitExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/ListSplitExpenseCommand.java new file mode 100644 index 0000000000..2e98ab0332 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ListSplitExpenseCommand.java @@ -0,0 +1,25 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SplitExpenseList; + +public class ListSplitExpenseCommand extends Command{ + private SplitExpenseList splitexpenses; + + /** + * Creates a ListSplitExpenseCommand object. + * + * @param splitexpenses The list of split expenses. + */ + + public ListSplitExpenseCommand(SplitExpenseList splitexpenses) { + this.splitexpenses = splitexpenses; + } + + /** + * Lists all the split expenses. + */ + @Override + public void execute() { + splitexpenses.listSplitExpenses(); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/MenuCommand.java b/src/main/java/seedu/budgetbuddy/command/MenuCommand.java new file mode 100644 index 0000000000..193518615e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/MenuCommand.java @@ -0,0 +1,49 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.Ui; + +import java.util.logging.Logger; +import java.util.logging.Level; + +public class MenuCommand extends Command { + + private static Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private int index; + private Ui ui; + + public MenuCommand(int index) { + assert index >= 0 : "Index should be a positive number"; + this.index = index; + ui = new Ui(); + + } + + public int getIndex() { + assert index >= 0 : "Index must be a positive number"; + + return this.index; + } + + /** + * Executes the menu command by showing the menu titles or a specific menu item. + * + */ + @Override + public void execute() { + assert index >= 0 : "Index must be a positive number"; + + logger.log(Level.INFO, "Starting the processing of Menu Command with Index :" + index); + + if (index == 0) { + logger.log(Level.INFO, "Displaying all Menu Items"); + + ui.showMenuTitles(); + } else { + logger.log(Level.INFO, "Displaying Menu Items at Index : " + index); + + ui.showMenuItem(index); + } + + logger.log(Level.INFO, "End of Processing of Menu Command"); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/RecurringExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/RecurringExpenseCommand.java new file mode 100644 index 0000000000..79f669be47 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/RecurringExpenseCommand.java @@ -0,0 +1,270 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.Ui; + +import java.util.ArrayList; + +public class RecurringExpenseCommand extends Command{ + private RecurringExpenseLists recurringExpenseLists; + + private ExpenseList overallExpenses; + private String initialListName; + private String commandType; + private int listNumber; + private String category; + private Double amount; + private String description; + + private final Ui ui = new Ui(); + + + /** + * Constructs a RecurringExpenseCommand for operations that only require the recurringExpensesList. + * This constructor is used when the commandType is `viewlists` + * + * @param recurringExpenseLists The overall recurringExpensesList containing the list of ExpenseList + * @param commandType The commandType of the RecurringExpenseCommand + */ + public RecurringExpenseCommand(RecurringExpenseLists recurringExpenseLists, String commandType) { + this.commandType = commandType; + this.recurringExpenseLists = recurringExpenseLists; + } + + /** + * Constructs a RecurringExpenseCommand for operations that require the name of the ExpenseList + * , the recurringExpensesList + * This constructor is used when the commandType is `newlist` + * + * @param initialListName The name of the new RecurringExpenseList to create + * @param recurringExpenseLists The overall recurringExpensesList containing the list of ExpenseList + * @param commandType The commandType of the RecurringExpenseCommand + */ + public RecurringExpenseCommand(String initialListName, + RecurringExpenseLists recurringExpenseLists, String commandType) { + this.initialListName = initialListName; + this.commandType = commandType; + this.recurringExpenseLists = recurringExpenseLists; + } + + /** + * Constructs a RecurringExpenseCommand for operations that require listNumber, recurringExpensesList + * This constructor is used when the commandType is either `viewexpenses` or `removelist` + * + * @param listNumber The listNumber associated to the listName printed during a `viewlists` command + * @param recurringExpenseLists The overall recurringExpensesList containing the list of ExpenseList + * @param commandType The commandType of the RecurringExpenseCommand + */ + public RecurringExpenseCommand(int listNumber, + RecurringExpenseLists recurringExpenseLists, String commandType) { + this.listNumber = listNumber; + this.commandType = commandType; + this.recurringExpenseLists = recurringExpenseLists; + } + + + /** + * Constructs a RecurringExpenseCommand for operations that require listNumber, recurringExpensesList + * , overallExpenses. + * This constructor is used when the commandType is `addrec` + * + * @param listNumber The listNumber associated to the listName printed during a `viewlists` command + * @param recurringExpenseLists The overall recurringExpensesList containing the list of ExpenseList + * @param overallExpenses The overall ExpenseList containing all the user's expenses + * @param commandType The commandType of the RecurringExpenseCommand + */ + public RecurringExpenseCommand(int listNumber, RecurringExpenseLists recurringExpenseLists, + ExpenseList overallExpenses, String commandType) { + + this.recurringExpenseLists = recurringExpenseLists; + this.overallExpenses = overallExpenses; + this.listNumber = listNumber; + this.commandType = commandType; + } + + /** + * Constructs a RecurringExpenseCommand for operations that require listNumber, recurringExpensesList, + * category, amount, description. + * This constructor is used when the commandType is `newexpense` + * + * @param listNumber The listNumber associated to the listName printed during a `viewlists` command + * @param recurringExpenseLists The overall recurringExpensesList containing the list of ExpenseList + * @param category The category of the new expense user wishes to add + * @param amount The amount of the new expense user wishes to add + * @param description The description of the new expense user wishes to add + * @param commandType The commandType of the RecurringExpenseCommand + */ + public RecurringExpenseCommand(int listNumber, RecurringExpenseLists recurringExpenseLists, String category, + Double amount, String description, String commandType) { + + this.recurringExpenseLists = recurringExpenseLists; + this.listNumber = listNumber; + this.category = category; + this.amount = amount; + this.description = description; + this.commandType = commandType; + } + + /** + * Adds a new list with the name `listName` to the recurringExpensesList + * + * @param listName The name of the new list + */ + private void addNewList(String listName) { + recurringExpenseLists.addNewRecurringList(listName); + } + + /** + * Removes the ExpenseList located at listNumber in the overall recurringExpensesList + * + * @param listNumber The list position of the ExpenseList to remove + */ + private void removeList(int listNumber) { + + if (listNumber <= 0 || listNumber > recurringExpenseLists.getSize()) { + System.out.println("Invalid List Number. Choose a List Number from 1 onwards"); + System.out.println("Number of Lists you have currently : " + recurringExpenseLists.getSize()); + return; + } + + recurringExpenseLists.removeList(listNumber); + } + + + /** + * Adds an Expense with the provided category, amount and description to the ExpenseList located + * at the provided listNumber in the overall recurringExpensesList + * + * @param listNumber The list position of the ExpenseList to add the Expense in + * @param category The category of the Expense to add + * @param amount The amount of the Expense to add + * @param description The description of the Expense to add + */ + private void addExpenseToList(int listNumber, String category, Double amount, String description) { + + if (listNumber <= 0 || listNumber > recurringExpenseLists.getSize()) { + System.out.println("Invalid List Number. Choose a List Number from 1 onwards"); + System.out.println("Number of Lists you have currently : " + recurringExpenseLists.getSize()); + return; + } + + ExpenseList expenses = recurringExpenseLists.getExpenseListAtListNumber(listNumber); + + Expense expenseToAdd = new Expense(category, amount, description); + expenses.getExpenses().add(expenseToAdd); + + ui.printDivider(); + System.out.println("Successfully Added Expense to " + expenses.getName()); + System.out.println("|Details of Expense"); + System.out.println("--------------------"); + System.out.println("|Category : " + category); + System.out.println("|Amount : " + String.format("%.2f", amount)); + System.out.println("|Description : " + description); + ui.printDivider(); + + } + + /** + * Adds all Expenses in the ExpenseList located at the provided listNumber in the `recurringExpensesList`, + * into the provided `overallExpenses` + * + * @param listNumber The list position of the ExpenseList in recurringExpensesList + * @param recurringExpenseLists The overall recurringExpensesList + * @param overallExpenses The overall expenses + */ + private void addRecurringExpensesToExpenses(int listNumber, RecurringExpenseLists recurringExpenseLists + , ExpenseList overallExpenses) { + + if (listNumber <= 0 || listNumber > recurringExpenseLists.getSize()) { + System.out.println("Invalid List Number. Choose a List Number from 1 onwards"); + System.out.println("Number of Lists you have currently : " + recurringExpenseLists.getSize()); + return; + } + + ExpenseList expenseList = recurringExpenseLists.getExpenseListAtListNumber(listNumber); + ArrayList expenses = expenseList.getExpenses(); + + if (expenses.isEmpty()) { + ui.printDivider(); + System.out.println("There are no expenses present in \"" + expenseList.getName() + + "\". Nothing has been added to the overall expenses"); + ui.printDivider(); + return; + } + for (Expense expense : expenses) { + String category = expense.getCategory(); + Double amount = expense.getAmount(); + String description = expense.getDescription(); + + ui.printDivider(); + System.out.println("Adding : " + category + " | " + amount + " | " + description + " | "); + + overallExpenses.addExpense(category,amount,description); + ui.printDivider(); + } + + ui.printDivider(); + System.out.println("Your chosen Recurring Expenses in " + expenseList.getName() + + " has been added to your overall Expenses"); + ui.printDivider(); + + } + + /** + * Prints all expenses in the ExpenseList located at the provided `listNumber` in the overall + * `recurringExpensesList` + * + * @param listNumber The list position of the ExpenseList in recurringExpensesList + * @param recurringExpenseLists The recurringExpensesList to obtain ExpenseList from + */ + private void printExpensesAtIndex(int listNumber, RecurringExpenseLists recurringExpenseLists) { + assert recurringExpenseLists != null : "listNumber cannot be Null"; + + if (listNumber <= 0 || listNumber > recurringExpenseLists.getSize()) { + System.out.println("Invalid List Number. Choose a List Number from 1 onwards"); + System.out.println("Number of Lists you have currently : " + recurringExpenseLists.getSize()); + return; + } + + ExpenseList expenseList = recurringExpenseLists.getExpenseListAtListNumber(listNumber); + + expenseList.listExpenses(null); + } + + + /** + * Prints the names of all ExpenseList in the recurringExpensesList + */ + private void printList() { + recurringExpenseLists.printAllRecurringLists(); + } + public void execute(){ + assert commandType != null : "CommandType cannot be null"; + + switch(commandType) { + case "newlist": + addNewList(initialListName); + break; + case "viewlists": + printList(); + break; + case "removelist": + removeList(this.listNumber); + break; + case "newexpense": + addExpenseToList(this.listNumber, this.category, this.amount, this.description); + break; + case "addrec": + addRecurringExpensesToExpenses(this.listNumber, this.recurringExpenseLists, this.overallExpenses); + break; + case "viewexpenses": + printExpensesAtIndex(this.listNumber, this.recurringExpenseLists); + break; + default: + break; + } + } + +} diff --git a/src/main/java/seedu/budgetbuddy/command/ReduceSavingCommand.java b/src/main/java/seedu/budgetbuddy/command/ReduceSavingCommand.java new file mode 100644 index 0000000000..b01eb29121 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/ReduceSavingCommand.java @@ -0,0 +1,27 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SavingList; + +public class ReduceSavingCommand extends Command { + private String category; + private double amount; + private SavingList savings; + + + public ReduceSavingCommand(SavingList savings, String category, double amount) { + this.savings = savings; + this.category = category; + this.amount = amount; + } + + @Override + public void execute() { + if (savings != null) { + savings.reduceSavingsByCategory(category, amount); + } else { + System.out.println("Savings list not initialized."); + } + } +} + + diff --git a/src/main/java/seedu/budgetbuddy/command/SetBudgetCommand.java b/src/main/java/seedu/budgetbuddy/command/SetBudgetCommand.java new file mode 100644 index 0000000000..2651f2dece --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/SetBudgetCommand.java @@ -0,0 +1,20 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.ExpenseList; + +public class SetBudgetCommand extends Command { + private ExpenseList expenseList; + private String category; + private double budget; + + public SetBudgetCommand(ExpenseList expenseList, String category, double budget){ + this.expenseList = expenseList; + this.category = category; + this.budget = budget; + } + + @Override + public void execute(){ + expenseList.setBudget(this.category, this.budget); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/SettleSplitExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/SettleSplitExpenseCommand.java new file mode 100644 index 0000000000..7ca7b67184 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/SettleSplitExpenseCommand.java @@ -0,0 +1,20 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SplitExpenseList; + +public class SettleSplitExpenseCommand extends Command{ + + private SplitExpenseList splitexpenses; + private int index; + + public SettleSplitExpenseCommand(SplitExpenseList splitexpenses, int index) { + this.splitexpenses = splitexpenses; + this.index = index; + } + + @Override + public void execute() { + splitexpenses.settleSplitExpenses(index); + System.out.println("Settled expense: (" + (index+1) + ") "); + } +} diff --git a/src/main/java/seedu/budgetbuddy/command/SplitExpenseCommand.java b/src/main/java/seedu/budgetbuddy/command/SplitExpenseCommand.java new file mode 100644 index 0000000000..aa620793fd --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/command/SplitExpenseCommand.java @@ -0,0 +1,42 @@ +package seedu.budgetbuddy.command; + +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class SplitExpenseCommand extends Command { + private SplitExpenseList splitexpenses; + private final String amount; + private final String numberOfPeople; + private final String description; + + public SplitExpenseCommand(SplitExpenseList splitexpenses, String amount, + String numberOfPeople, String description) { + this.splitexpenses = splitexpenses; + this.numberOfPeople = numberOfPeople; + this.amount = amount; + this.description = description; + } + + public String getNumberOfPeople() { + return numberOfPeople; + } + + public String getAmount() { + return amount; + } + + public String getDescription() { + return description; + } + + @Override + public void execute() { + try { + splitexpenses.addSplitExpense(this.amount, this.numberOfPeople, this.description); + System.out.println("Shared Bill Added: " + "$" + amount + " spent by " + + numberOfPeople + " persons. Description: " + description); + } catch (BudgetBuddyException e) { + System.out.println("An error occurred while adding expense."); + } + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/AddExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/AddExpenseCommandCreator.java new file mode 100644 index 0000000000..ed4a05ed4e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/AddExpenseCommandCreator.java @@ -0,0 +1,99 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.AddExpenseCommand; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +/** + * Creates an AddExpenseCommand object. + */ +public class AddExpenseCommandCreator extends CommandCreator { + private ExpenseList expenses; + private String input; + + public AddExpenseCommandCreator(ExpenseList expenses, String input) { + this.expenses = expenses; + this.input = input; + } + + /** + * Parses the input and creates a new AddExpenseCommand object. + * + * @param expenses The list of expenses. + * @param input The input string. + * @return The AddExpenseCommand object. + */ + public Command handleAddExpenseCommand(ExpenseList expenses, String input) { + if (input == null || !input.contains("c/") || !input.contains("a/") || !input.contains("d/")) { + System.out.println("Invalid command format."); + return null; + } + String[] parts = input.split(" ", 2); + if (parts.length < 2) { + System.out.println("Expense details are missing."); + return null; + } + String details = parts[1]; + + String category = extractDetailsForAdd(details, "c/"); + if (category.isEmpty()) { + System.out.println("category is missing."); + return null; + } + String amount = extractDetailsForAdd(details, "a/"); + if (amount.isEmpty()) { + System.out.println("amount is missing."); + return null; + } + if (input.contains("!") || input.contains("|")) { + System.out.println("Please do not include a ! or | in your input"); + return null; + } + + try { + double amountValue = Double.parseDouble(amount); + if (amountValue <= 0) { + throw new BudgetBuddyException(amount + " is not a valid amount."); + } + } catch (NumberFormatException e) { + System.out.println("Invalid amount. Please enter a valid number."); + return null; + } catch (BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + String description = extractDetailsForAdd(details, "d/"); + if (description.isEmpty()) { + System.out.println("description is missing."); + return null; + } + return new AddExpenseCommand(expenses, category, amount, description); + } + + /** + * Extracts the details for the add command. + * + * @param details The details string. + * @param prefix The prefix to search for. + * @return The extracted details. + */ + private String extractDetailsForAdd(String details, String prefix) { + int startIndex = details.indexOf(prefix) + prefix.length(); + int endIndex = details.length(); + + String[] nextPrefixes = { "c/", "a/", "d/" }; + for (String nextPrefix : nextPrefixes) { + if (details.indexOf(nextPrefix, startIndex) != -1 && details.indexOf(nextPrefix, startIndex) < endIndex) { + endIndex = details.indexOf(nextPrefix, startIndex); + } + } + return details.substring(startIndex, endIndex).trim(); + } + + @Override + public Command createCommand(){ + return handleAddExpenseCommand(expenses, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/AddSavingCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/AddSavingCommandCreator.java new file mode 100644 index 0000000000..ed65fb17c2 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/AddSavingCommandCreator.java @@ -0,0 +1,98 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.command.AddSavingCommand; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class AddSavingCommandCreator extends CommandCreator{ + private SavingList savings; + private String input; + + /** + * Creates an AddSavingCommandCreator object. + * + * @param savings The list of savings. + * @param input The input string. + */ + public AddSavingCommandCreator(SavingList savings, String input) { + this.savings = savings; + this.input = input; + } + + /** + * Parses the input and creates a new AddSavingCommand object. + * + * @param savings The list of savings. + * @param input The input string. + * @return The AddSavingCommand object. + */ + + public Command handleAddSavingCommand(SavingList savings, String input) { + if (input == null || !input.contains(" ") || !input.contains("c/") || !input.contains("a/")) { + System.out.println("Invalid command format."); + return null; + } + + String[] parts = input.split(" ", 2); + if (parts.length < 2) { + System.out.println("Saving details are missing."); + return null; + } + + String details = parts[1]; + String category = extractDetailsForAdd(details, "c/"); + if (category.isEmpty()) { + System.out.println("Category is missing."); + return null; + } + + String amount = extractDetailsForAdd(details, "a/"); + if (amount.isEmpty()) { + System.out.println("amount is missing."); + return null; + } + + try { + double amountValue = Double.parseDouble(amount); + if (amountValue <= 0) { + throw new BudgetBuddyException(amount + " is negative. Please enter a positive amount."); + } + + } catch (NumberFormatException e) { + System.out.println("Invalid amount. Please enter a valid number."); + return null; + } catch (BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + return new AddSavingCommand(savings, category, amount); + } + + /** + * Extracts the details for adding a saving. + * + * @param details The details string. + * @param prefix The prefix string. + * @return The details for adding a saving. + */ + + private String extractDetailsForAdd(String details, String prefix) { + int startIndex = details.indexOf(prefix) + prefix.length(); + int endIndex = details.length(); + + String[] nextPrefixes = { "c/", "a/", "d/" }; + for (String nextPrefix : nextPrefixes) { + if (details.indexOf(nextPrefix, startIndex) != -1 && details.indexOf(nextPrefix, startIndex) < endIndex) { + endIndex = details.indexOf(nextPrefix, startIndex); + } + } + return details.substring(startIndex, endIndex).trim(); + } + + @Override + public Command createCommand() { + return handleAddSavingCommand(savings, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/ChangeCurrencyCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/ChangeCurrencyCommandCreator.java new file mode 100644 index 0000000000..82eade99d0 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/ChangeCurrencyCommandCreator.java @@ -0,0 +1,84 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.CurrencyConverter; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.command.ChangeCurrencyCommand; +import seedu.budgetbuddy.command.Command; + +import java.util.Currency; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ChangeCurrencyCommandCreator extends CommandCreator { + + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private ExpenseList expenses; + private SavingList savings; + private SplitExpenseList splitExpenses; + private RecurringExpenseLists recurringExpenseLists; + private String input; + private CurrencyConverter newCurrency; + + public ChangeCurrencyCommandCreator(String input, SavingList savings, ExpenseList expenses, + SplitExpenseList splitExpenses, RecurringExpenseLists recurringExpenseLists, + CurrencyConverter newCurrency) { + + this.input = input; + this.savings = savings; + this.expenses = expenses; + this.splitExpenses = splitExpenses; + this.recurringExpenseLists = recurringExpenseLists; + this.newCurrency = newCurrency; + + } + + /** + * Parses the user input to create a ChangeCurrencyCommand for changing the default currency. + * If the input is valid, a ChangeCurrencyCommand is returned with the specified new currency. + * + * @author sweijie24 + * @param input The user input to be parsed. + * @param savingList The SavingList containing savings data. + * @param expenseList The ExpenseList containing expenses data. + * @param currencyConverter The CurrencyConverter object for currency conversion. + * @return A ChangeCurrencyCommand if the input is valid; otherwise, null. + */ + public Command handleChangeCurrencyCommand(String input, SavingList savingList, ExpenseList expenseList, + SplitExpenseList splitExpenses, + RecurringExpenseLists recurringExpenseLists, + CurrencyConverter currencyConverter) { + if (input.toLowerCase().startsWith("change currency")) { + String[] parts = input.split(" "); + assert parts.length > 1 : "Input should contain currency code"; + + if (parts.length == 3) { + String currencyCode = parts[2]; + assert !currencyCode.isEmpty() : "Currency code should not be empty"; + + try { + Currency newCurrency = Currency.getInstance(currencyCode.toUpperCase()); + assert newCurrency != null : "Currency code should be valid"; + LOGGER.log(Level.INFO, "Default currency changed to " + newCurrency); + return new ChangeCurrencyCommand(newCurrency, savingList, expenseList, splitExpenses, + recurringExpenseLists, currencyConverter); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Invalid currency code: " + currencyCode); + System.out.println("Invalid currency code."); + return null; + } + } else { + LOGGER.log(Level.WARNING, "Invalid command format. Use 'change currency '."); + System.out.println("Invalid command format. Use 'change currency '."); + return null; + } + } + return null; + } + @Override + public Command createCommand() { + return handleChangeCurrencyCommand(input, savings, expenses, splitExpenses, recurringExpenseLists, newCurrency); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/CommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/CommandCreator.java new file mode 100644 index 0000000000..32eb382815 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/CommandCreator.java @@ -0,0 +1,10 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.command.Command; + +public abstract class CommandCreator { + + public Command createCommand() { + return null; + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/DeleteExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/DeleteExpenseCommandCreator.java new file mode 100644 index 0000000000..0ae1f02a89 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/DeleteExpenseCommandCreator.java @@ -0,0 +1,54 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.DeleteExpenseCommand; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DeleteExpenseCommandCreator extends CommandCreator{ + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private ExpenseList expenseList; + private String input; + + public DeleteExpenseCommandCreator(ExpenseList expenseList, String input){ + this.expenseList = expenseList; + this.input = input; + } + + public Command handleDeleteExpenseCommand(ExpenseList expenses, String input) { + LOGGER.log(Level.INFO, "Processing handleDeleteExpenseCommand"); + + assert expenses != null : "Expense list cannot be null"; + assert input != null : "Input string cannot be null"; + + String[] parts = input.split("i/", 2); + if (parts.length < 2) { + // Log and notify the user about the incorrect format without returning null. + LOGGER.log(Level.WARNING, "Invalid command format. Expected format: delete expense i/." ); + System.out.println("Invalid command format. Expected format: delete expense i/."); + return null; // Return null to indicate no command should be executed; assuming your loop can handle this. + } + + try { + int index = Integer.parseInt(parts[1].trim()) - 1; + if (index < 0 || index >= expenses.size()) { + LOGGER.log(Level.WARNING, "Index is out of bounds. Please try again."); + System.out.println("Index is out of bounds. Please try again."); + return null; + } + LOGGER.log(Level.INFO, "Successfully processed DeleteExpenseCommand"); + return new DeleteExpenseCommand(expenses, index); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "Index is not a valid number. Please try again."); + System.out.println("Index is not a valid number. Please try again."); + return null; + } + } + + @Override + public Command createCommand(){ + return handleDeleteExpenseCommand(expenseList, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/EditExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/EditExpenseCommandCreator.java new file mode 100644 index 0000000000..834c159586 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/EditExpenseCommandCreator.java @@ -0,0 +1,134 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.EditExpenseCommand; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class EditExpenseCommandCreator extends CommandCreator { + private ExpenseList expenses; + private String input; + public EditExpenseCommandCreator(String input, ExpenseList expenses) { + this.input = input; + this.expenses = expenses; + } + + /** + * Parses the input command to extract parameters for editing an expense entry and then + * creates a command to perform the edit operation. The input string is expected to contain + * parts with prefixes indicating category (c/), index (i/), amount (a/), and description (d/). + * + * @param expenses The ExpenseList object containing the list of expenses to be edited. + * @param input The input command string containing the edit parameters. + * @return An instance of EditExpenseCommand if the input is valid, or null if the input + * is invalid or incomplete. + */ + public Command handleEditExpenseCommand(ExpenseList expenses, String input) { + try { + checkForInvalidInputs(input); + checkForValidCategory(input); + checkForInvalidAmount(input); + } catch (IllegalArgumentException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + System.out.println("Command Format : edit expense c/CATEGORY i/INDEX a/AMOUNT d/DESCRIPTION"); + return null; + } + String[] parts = input.split(" "); + String category = null; + int index = -1; + double amount = -1; + String description = null; + + for (String part : parts) { + if (part.startsWith("c/")) { + category = part.substring(2); + } else if (part.startsWith("i/")) { + index = Integer.parseInt(part.substring(2)); // Removed the redundant try-catch block + } else if (part.startsWith("a/")) { + amount = Double.parseDouble(part.substring(2)); + } else if (part.startsWith("d/")) { + description = part.substring(2); + } + } + + if (category != null && index != -1 && amount != -1 && description != null) { + return new EditExpenseCommand(expenses, category, index, amount, description); + } else { + // Handle incomplete command + System.out.println("Incomplete command. Please ensure all parameters are included."); + return null; + } + } + + + public static void checkForInvalidInputs (String input) throws BudgetBuddyException { + final String categoryPrefix = "c/"; + final String indexPrefix = "i/"; + final String amountPrefix = "a/"; + final String descriptionPrefix = "d/"; + + if (input.contains("!") || input.contains("|")) { + throw new BudgetBuddyException("Please do not include a ! or | in your input"); + } + if (!input.contains("c/") || !input.contains("i/") || !input.contains("a/") || !input.contains("d/")) { + throw new IllegalArgumentException("Please Ensure that you include c/, i/, a/ and d/"); + } + + String [] parameters = {categoryPrefix, indexPrefix, amountPrefix, descriptionPrefix}; + + for (String parameter : parameters) { + if (input.indexOf(parameter) != input.lastIndexOf(parameter)) { + throw new BudgetBuddyException("Please ensure that you do not have duplicate parameters."); + } + } + } + + public static void checkForValidCategory (String input) throws BudgetBuddyException { + String[] parts = input.split(" "); + String category = null; + for (String part : parts) { + if (part.startsWith("c/")) { + category = part.substring(2); + break; + } + } + + if (category == null || !(category.equals("Transport") || category.equals("Housing") || + category.equals("Groceries") || category.equals("Utility") || + category.equals("Entertainment") || category.equals("Others"))) { + throw new BudgetBuddyException("Please enter a valid category: Housing, Groceries, Utility, Transport," + + "Entertainment or Others "); + } + + } + + public static void checkForInvalidAmount(String input) throws BudgetBuddyException { + String[] parts = input.split(" "); + double amount = -1; + + for (String part : parts) { + if (part.startsWith("a/")) { + try { + amount = Double.parseDouble(part.substring(2)); + if (amount <= 0) { // Amount must be greater than 0 + throw new BudgetBuddyException("Invalid Amount. Amount must be greater than 0."); + } + break; // Break after finding the amount to stop checking other parts + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Invalid Amount. Amount should be a numerical value."); + } + } + } + + if (amount == -1) { + // If amount is still -1, it means no amount was entered + throw new BudgetBuddyException("No amount specified. Please enter an amount using a/ prefix."); + } + } + + + @Override + public Command createCommand() { + return handleEditExpenseCommand(expenses, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/EditSavingsCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/EditSavingsCommandCreator.java new file mode 100644 index 0000000000..abbcc4d115 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/EditSavingsCommandCreator.java @@ -0,0 +1,119 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.EditSavingCommand; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class EditSavingsCommandCreator extends CommandCreator { + private SavingList savings; + private String input; + public EditSavingsCommandCreator (String input, SavingList savings) { + this.input = input; + this.savings = savings; + } + + /** + * Parses the input command to extract the parameters for editing a saving entry and then + * initiates the edit operation. The command string is expected to contain indicators + * followed by the values for category (c/) and amount (a/). + * + * @param savings The SavingList object that contains the list of savings. + * @param input The input command string containing the parameters to edit a saving entry. + * @return A Command object to execute the edit operation or null if the input is invalid. + */ + //@@author jasraa + public Command handleEditSavingCommand(SavingList savings, String input) { + try { + checkForInvalidInputs(input); + checkForValidCategory(input); + checkForInvalidAmount(input); + } catch (IllegalArgumentException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + System.out.println("Command Format : edit savings c/CATEGORY a/AMOUNT"); + return null; + } + + String category = null; + double amount = -1; + + if (input.contains("c/") && input.contains("a/")) { + category = input.substring(input.indexOf("c/") + 2, input.indexOf("a/")).trim(); + amount = Double.parseDouble(input.substring(input.indexOf("a/") + 2).trim()); + } + + if (category != null && amount != -1) { + return new EditSavingCommand(savings, category, amount); + } else { + // Handle incomplete command + return null; + } + } + + public static void checkForInvalidInputs (String input) throws BudgetBuddyException { + final String categoryPrefix = "c/"; + final String amountPrefix = "a/"; + + if (input.contains("!") || input.contains("|")) { + throw new BudgetBuddyException("Please do not include a ! or | in your input"); + } + if (!input.contains("c/") || !input.contains("a/")) { + throw new IllegalArgumentException("Please Ensure that you include c/ and a/"); + } + + String [] parameters = {categoryPrefix, amountPrefix}; + + for (String parameter : parameters) { + if (input.indexOf(parameter) != input.lastIndexOf(parameter)) { + throw new BudgetBuddyException("Please ensure that you do not have duplicate parameters."); + } + } + } + + public static void checkForValidCategory (String input) throws BudgetBuddyException { + String[] parts = input.split(" "); + String category = null; + for (String part : parts) { + if (part.startsWith("c/")) { + category = part.substring(2); + break; + } + } + + if (category == null || !(category.equalsIgnoreCase("Salary") || + category.equalsIgnoreCase("Investments") || category.equalsIgnoreCase("Gifts") + || category.equalsIgnoreCase("Others"))) { + throw new BudgetBuddyException("Please enter a valid category: Salary, Investments, Gifts and Others"); + } + } + + public static void checkForInvalidAmount(String input) throws BudgetBuddyException { + String[] parts = input.split(" "); + double amount = -1; + + for (String part : parts) { + if (part.startsWith("a/")) { + try { + amount = Double.parseDouble(part.substring(2)); + if (amount <= 0) { // Amount must be greater than 0 + throw new BudgetBuddyException("Invalid Amount. Amount must be greater than 0."); + } + break; // Break after finding the amount to stop checking other parts + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Invalid Amount. Amount should be a numerical value."); + } + } + } + + if (amount == -1) { + // If amount is still -1, it means no amount was entered + throw new BudgetBuddyException("No amount specified. Please enter an amount using a/ prefix."); + } + } + + @Override + public Command createCommand() { + return handleEditSavingCommand(savings, input); + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/FindExpensesCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/FindExpensesCommandCreator.java new file mode 100644 index 0000000000..d715b6cf36 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/FindExpensesCommandCreator.java @@ -0,0 +1,225 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.FindExpensesCommand; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FindExpensesCommandCreator extends CommandCreator { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final String DESCRIPTION_PREFIX = "d/"; + private static final String MINAMOUNT_PREFIX = "morethan/"; + private static final String MAXAMOUNT_PREFIX = "lessthan/"; + + + private ExpenseList expenses; + private String input; + + public FindExpensesCommandCreator(String input, ExpenseList expenses) { + this.input = input; + this.expenses = expenses; + } + + + /** + * Checks the order of parameters in the provided input. + * + * @param input The user input + * @throws BudgetBuddyException If the parameters are not in the order of d/, morethan/ , lessthan/. + */ + private void checkForOutOfOrderParameters(String input) throws BudgetBuddyException { + int indexOfDescriptionPrefix = input.indexOf(DESCRIPTION_PREFIX); + int indexOfMinAmountPrefix = input.indexOf(MINAMOUNT_PREFIX); + int indexOfMaxAmountPrefix = input.indexOf(MAXAMOUNT_PREFIX); + + if (indexOfDescriptionPrefix > indexOfMinAmountPrefix) { + throw new BudgetBuddyException("Please ensure that your parameters are in the right order."); + } + + if (indexOfMinAmountPrefix > indexOfMaxAmountPrefix) { + throw new BudgetBuddyException("Please ensure that your parameters are in the right order."); + } + + } + + /** + * Checks for the absence of the required parameters `d/`, `morethan/` and `lessthan/` + * + * @param input The user input + * @throws IllegalArgumentException If any of the three required parameters are missing + */ + private static void checkForInvalidParameters(String input) throws IllegalArgumentException { + if (!input.contains("d/") || !input.contains("morethan/") || !input.contains("lessthan/")) { + throw new IllegalArgumentException("Please Ensure that you include d/, morethan/ and lessthan/"); + } + } + + /** + * Parses and returns the maximum amount from the `lessthan/` prefix in the input string + * + * @param input The user input + * @return The extracted maximum amount, or null if amount is not specified + * @throws NumberFormatException If the maximum amount obtained is not a valid double + */ + private Double parseMaxAmount(String input) throws NumberFormatException{ + int indexOfMaxAmountPrefix = input.indexOf(MAXAMOUNT_PREFIX); + int startIndexOfMaxAmount = indexOfMaxAmountPrefix + MAXAMOUNT_PREFIX.length(); + + int endIndexOfMaxAmount = input.length(); + + String maxAmountAsString = input.substring(startIndexOfMaxAmount, endIndexOfMaxAmount).trim(); + + if (maxAmountAsString.trim().isEmpty()) { + return null; + } + + Double maxAmount = Double.parseDouble(maxAmountAsString); + + return maxAmount; + } + + /** + * Parses and returns the minimum amount from the `morethan/` prefix in the input string + * + * @param input The user input + * @return The extracted minimum amount, or null if amount is not specified + * @throws NumberFormatException If the minimum amount obtained is not a valid double + */ + private Double parseMinAmount(String input) throws NumberFormatException { + int indexOfMinAmountPrefix = input.indexOf(MINAMOUNT_PREFIX); + int startIndexOfMinAmount = indexOfMinAmountPrefix + MINAMOUNT_PREFIX.length(); + + int indexOfMaxAmountPrefix = input.indexOf(MAXAMOUNT_PREFIX); + int endIndexOfMinAmount = indexOfMaxAmountPrefix; + + String minAmountAsString = input.substring(startIndexOfMinAmount, endIndexOfMinAmount).trim(); + + if (minAmountAsString.trim().isEmpty()) { + return null; + } + + Double minAmount = Double.parseDouble(minAmountAsString); + + return minAmount; + } + + /** + * Parses and returns the description from the `d/` prefix in the input string + * + * @param input The user input + * @return The obtained description, or null if the description is empty + */ + private String parseDescription(String input) { + + int indexOfDescriptionPrefix = input.indexOf(DESCRIPTION_PREFIX); + int startIndexOfDescription = indexOfDescriptionPrefix + DESCRIPTION_PREFIX.length(); + + int indexOfMinAmountPrefix = input.indexOf(MINAMOUNT_PREFIX); + int endIndexOfDescription = indexOfMinAmountPrefix; + + String description = input.substring(startIndexOfDescription, endIndexOfDescription).trim(); + + if (description.isEmpty()) { + return null; + } + + return description; + } + + /** + * Checks for duplicate occurrences of a prefix in the input string + * + * @param input The user input + * @param parameter The parameter to check for duplicates + * @throws IllegalArgumentException If the parameter appears more than once + */ + private static void checkForDuplicateParameters(String input, String parameter) throws IllegalArgumentException{ + + int count = 0; + + Pattern pattern = Pattern.compile(parameter); + Matcher matcher = pattern.matcher(input); + + while (matcher.find()) { + count++; + } + + if (count > 1) { + throw new IllegalArgumentException("The parameter '" + parameter + "' can only be used once."); + } + + } + + /** + * Compares the minimum and maximum amounts and throws an exception if the minimum amount + * is larger than the maximum amount + * + * @param minAmount The minimum amount + * @param maxAmount The maximum amount + * @throws BudgetBuddyException If the minimum amount > maximum amount + */ + private static void compareMinAndMaxAmount(Double minAmount, Double maxAmount) throws BudgetBuddyException{ + + if (minAmount != null && maxAmount != null) { + if (minAmount > maxAmount) { + throw new BudgetBuddyException("Ensure minimum amount is smaller than maximum amount"); + } + } + + } + + /** + * Parses the "find expenses" command, allowing for optional and combinable parameters. + * + * @param input The full user input string. + * @param expenses The ExpenseList to search within. + * @return A Command for executing the search, or null if the input is invalid. + */ + private Command handleFindExpensesCommand(String input, ExpenseList expenses) { + assert input != null : "Input cannot be null"; + assert !input.isEmpty() : "Input cannot be empty"; + assert input.startsWith("find expenses") : "Input must be a find expenses command"; + + LOGGER.log(Level.INFO, "Begin parsing parameters in find expenses command"); + + try { + checkForInvalidParameters(input); + checkForOutOfOrderParameters(input); + checkForDuplicateParameters(input, "d/"); + checkForDuplicateParameters(input, "morethan/"); + checkForDuplicateParameters(input, "lessthan/"); + + } catch (IllegalArgumentException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + try { + String description = parseDescription(input); + Double minAmount = parseMinAmount(input); + Double maxAmount = parseMaxAmount(input); + + compareMinAndMaxAmount(minAmount, maxAmount); + + return new FindExpensesCommand(expenses, description, minAmount, maxAmount); + + } catch (NumberFormatException e) { + System.out.println("Please input a valid amount."); + return null; + } catch (BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + } + + @Override + public Command createCommand() { + return handleFindExpensesCommand(input, expenses); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/GetBudgetCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/GetBudgetCommandCreator.java new file mode 100644 index 0000000000..7a59de8f14 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/GetBudgetCommandCreator.java @@ -0,0 +1,68 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.GetBudgetCommand; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class GetBudgetCommandCreator extends CommandCreator { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private ExpenseList expenses; + private String input; + private ArrayList expenseCategories; + + public GetBudgetCommandCreator(ExpenseList expenses, String input) { + this.expenses = expenses; + this.input = input; + this.expenseCategories = new ArrayList<>(Arrays.asList("Housing", "Groceries", "Utility", "Transport", + "Entertainment", "Others")); + } + + private boolean isValidExpenseCategory(String category) { + assert category != null : "Category should not be null"; + assert !category.isEmpty() : "Category should not be empty"; + + for (String validCategory : expenseCategories) { + if (validCategory.equalsIgnoreCase(category)) { + return true; + } + } + return false; + } + + public Command createCommand() { + LOGGER.log(Level.INFO, "Entering createCommand with input: " + input); + String[] parts = input.split(" "); + String category = null; + + for (String part : parts) { + if (part.startsWith("c/")) { + category = part.substring(2); + LOGGER.log(Level.INFO, "Category extracted: " + category); + } + } + + if (category == null) { + LOGGER.log(Level.WARNING, "Invalid command format or missing value for category"); + System.out.println("Invalid command format."); + System.out.println("Expected format: get budget c/"); + return null; + } + + boolean isValidCategory = isValidExpenseCategory(category); + if (!isValidCategory) { + LOGGER.log(Level.WARNING, "Invalid category: " + category); + System.out.println("Invalid category: " + category); + System.out.println("Valid categories: Housing, Groceries, Utility, Transport, Entertainment, Others"); + return null; + } + + LOGGER.log(Level.INFO, "Exiting createCommand. Command ready for execution."); + return new GetBudgetCommand(expenses, category); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/GetExpenseInsightsCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/GetExpenseInsightsCommandCreator.java new file mode 100644 index 0000000000..6b94240e51 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/GetExpenseInsightsCommandCreator.java @@ -0,0 +1,19 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.GetExpenseInsightsCommand; +import seedu.budgetbuddy.commons.ExpenseList; + +public class GetExpenseInsightsCommandCreator extends CommandCreator { + + private ExpenseList expenseList; + + public GetExpenseInsightsCommandCreator(ExpenseList expenseList) { + this.expenseList = expenseList; + } + + @Override + public Command createCommand() { + return new GetExpenseInsightsCommand(expenseList); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/GetSavingsInsightsCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/GetSavingsInsightsCommandCreator.java new file mode 100644 index 0000000000..41fefc0f65 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/GetSavingsInsightsCommandCreator.java @@ -0,0 +1,18 @@ +package seedu.budgetbuddy.commandcreator; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.GetSavingsInsightsCommand; + +public class GetSavingsInsightsCommandCreator extends CommandCreator { + + private SavingList savingList; + + public GetSavingsInsightsCommandCreator(SavingList savingList) { + this.savingList = savingList; + } + + @Override + public Command createCommand() { + return new GetSavingsInsightsCommand(savingList); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/ListBudgetCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/ListBudgetCommandCreator.java new file mode 100644 index 0000000000..3811daa584 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/ListBudgetCommandCreator.java @@ -0,0 +1,23 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ListBudgetCommand; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ListBudgetCommandCreator extends CommandCreator { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private ExpenseList expenses; + + public ListBudgetCommandCreator(ExpenseList expenses){ + this.expenses = expenses; + } + + @Override + public Command createCommand() { + LOGGER.log(Level.INFO, "Creating ListBudgetCommand"); + return new ListBudgetCommand(expenses); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/ListCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/ListCommandCreator.java new file mode 100644 index 0000000000..1fb8f3935e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/ListCommandCreator.java @@ -0,0 +1,149 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ListExpenseCommand; +import seedu.budgetbuddy.command.ListSavingsCommand; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ListCommandCreator extends CommandCreator { + + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + protected ArrayList expenseCategories; + protected ArrayList savingsCategories; + private ExpenseList expenses; + private SavingList savings; + private String input; + + public ListCommandCreator(ExpenseList expenses, SavingList savings, + String input) { + + this.expenses = expenses; + this.savings = savings; + this.input = input; + + this.expenseCategories = new ArrayList<>(Arrays.asList("Housing", + "Groceries", "Utility", "Transport", "Entertainment", "Others")); + this.savingsCategories = new ArrayList<>(Arrays.asList("Salary", + "Investments", "Gifts", "Others")); + } + + /** + * Checks if the given category is valid for expenses. + * + * @param category The category to be validated. + * @return true if the category is valid for expenses; false otherwise. + */ + private boolean isValidExpenseCategory(String category) { + + assert category != null : "Category should not be null"; + assert !category.isEmpty() : "Category should not be empty"; + + for (String validCategory : expenseCategories) { + if (validCategory.equalsIgnoreCase(category)) { + return true; + } + } + return false; + } + + /** + * Checks if the given category is valid for savings. + * + * @param category The category to be validated. + * @return true if the category is valid for savings; false otherwise. + */ + private boolean isValidSavingsCategory(String category) { + + assert category != null : "Category should not be null"; + assert !category.isEmpty() : "Category should not be empty"; + + for (String validCategory : savingsCategories) { + if (validCategory.equalsIgnoreCase(category)) { + return true; + } + } + return false; + } + + /** + * Parses the user input to create a ListCommand for listing expenses or savings. + * If the input is valid, a ListCommand is returned with the specified list type and optional filter category. + * + * @author sweijie24 + * @param input The user input to be parsed. + * @param expenseList The ExpenseList containing expenses data. + * @param savingList The SavingList containing savings data. + * @return A ListCommand if the input is valid; otherwise, null. + */ + public Command handleListCommand(String input, ExpenseList expenseList, SavingList savingList) { + assert input != null : "Input should not be null"; + assert !input.isEmpty() : "Input should not be empty"; + + String[] parts = input.split(" "); + assert parts.length >= 1 : "At least one part should be present in the input"; + + String action = parts[0].toLowerCase(); + assert !action.isEmpty() : "Action should not be empty"; + + switch (action) { + case "list": + if (parts.length == 2) { + String listType = parts[1]; + assert !listType.isEmpty() : "List type should not be empty"; + + if (listType.equalsIgnoreCase("expenses")) { + return new ListExpenseCommand(expenseList); + } else if (listType.equalsIgnoreCase("savings")) { + return new ListSavingsCommand(savingList, expenseList); + } + } else if (parts.length == 3 && parts[1].equalsIgnoreCase("expenses")) { + String filterCategory = parts[2]; + try { + if (filterCategory != null) { + boolean isValidCategory = isValidExpenseCategory(filterCategory); + if (!isValidCategory) { + LOGGER.warning("Invalid category inputted: " + filterCategory); + System.out.println("Invalid category: " + filterCategory); + return null; + } + } + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Invalid category inputted: " + filterCategory, e); + } + return new ListExpenseCommand(expenseList, filterCategory); + } else if (parts.length == 3 && parts[1].equalsIgnoreCase("savings")) { + String filterCategory = parts[2]; + try { + if (filterCategory != null) { + boolean isValidCategory = isValidSavingsCategory(filterCategory); + if (!isValidCategory) { + LOGGER.warning("Invalid category inputted: " + filterCategory); + System.out.println("Invalid category: " + filterCategory); + return null; + } + } + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Invalid category inputted: " + filterCategory, e); + } + return new ListSavingsCommand(savingList, expenseList, filterCategory); + } else { + return null; + } + break; + default: + return null; + } + return null; + } + + @Override + public Command createCommand() { + return handleListCommand(input, expenses, savings); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/ListSplittedExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/ListSplittedExpenseCommandCreator.java new file mode 100644 index 0000000000..c126b639c0 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/ListSplittedExpenseCommandCreator.java @@ -0,0 +1,28 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ListSplitExpenseCommand; + +public class ListSplittedExpenseCommandCreator extends CommandCreator{ + + private String input; + private SplitExpenseList splitexpenseList; + + public ListSplittedExpenseCommandCreator(String input, SplitExpenseList splitexpenseList) { + this.input = input; + this.splitexpenseList = splitexpenseList; + } + + public Command handleSplitExpenseListCommand(String input, SplitExpenseList splitexpenseList) { + assert input != null : "Input should not be null"; + assert !input.isEmpty() : "Input should not be empty"; + + return new ListSplitExpenseCommand(splitexpenseList); + } + + @Override + public Command createCommand() { + return handleSplitExpenseListCommand(input, splitexpenseList); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/MenuCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/MenuCommandCreator.java new file mode 100644 index 0000000000..3c47d1faf7 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/MenuCommandCreator.java @@ -0,0 +1,63 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.MenuCommand; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MenuCommandCreator extends CommandCreator { + + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private String userInput; + + public MenuCommandCreator(String userInput) { + this.userInput = userInput; + } + + public Boolean isEmptyMenuCommand() { + return userInput.trim().equals("menu"); + } + + /** + * Processes all menu commands and returns the corresponding Command object. + * This method interprets the user's input and displays either the entire menu + * or the associated menu item + * + * + * @return A new MenuCommand object with the specified index, returns null if + * index is not an integer + */ + public Command handleNonEmptyMenuCommand() { + try { + String indexAsString = userInput.substring(5); + int index = Integer.parseInt(indexAsString); + + LOGGER.log(Level.INFO, "Menu Command has found parameter" + index); + return new MenuCommand(index); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "Index found to not be an Integer"); + return null; + } + + } + public Command createMenuCommand() { + assert userInput != null : "Input should not be empty"; + assert userInput.startsWith("menu") : "Input should be a menu command"; + + if (isEmptyMenuCommand()) { + LOGGER.log(Level.INFO, "Menu Command has no parameters"); + return new MenuCommand(0); + } + + Command menuCommand = handleNonEmptyMenuCommand(); + return menuCommand; + } + + @Override + public Command createCommand() { + return createMenuCommand(); + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/RecurringExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/RecurringExpenseCommandCreator.java new file mode 100644 index 0000000000..aae589bda0 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/RecurringExpenseCommandCreator.java @@ -0,0 +1,443 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.RecurringExpenseCommand; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RecurringExpenseCommandCreator extends CommandCreator{ + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final double MAX_AMOUNT = 1_000_000_000_000.00; + private static final String LISTNUMBER_PREFIX = "to/"; + private static final String CATEGORY_PREFIX = "c/"; + private static final String AMOUNT_PREFIX = "a/"; + private static final String DESCRIPTION_PREFIX = "d/"; + private String input; + private RecurringExpenseLists recurringExpenseLists; + private ExpenseList expenses; + + private ArrayList expenseCategories = new ArrayList<>(Arrays.asList("Housing" + , "Groceries", "Utility", "Transport", "Entertainment", "Others")); + + + /** + * Constructs a RecurringExpenseCommandCreator with the provided input, recurringExpensesList and expenses + * @param input The user input + * @param recurringExpenseLists The RecurringExpensesList containing a list of ExpenseList + * @param expenses The ExpenseList containing user's overall expenses + */ + public RecurringExpenseCommandCreator(String input, RecurringExpenseLists recurringExpenseLists + , ExpenseList expenses) { + + this. input = input; + this.recurringExpenseLists = recurringExpenseLists; + this.expenses = expenses; + } + + /** + * Checks the input for the presence of `|` and `!`. Throws the BudgetBuddyException if detected in the user input + * + * @param input The user input + * @throws IllegalArgumentException if any of the required prefixes are not found + */ + private void checkForInvalidCharacters(String input) throws BudgetBuddyException{ + if (input.contains("|") || input.contains("!")) { + LOGGER.log(Level.WARNING, "An attempt of including a | and ! in input has been detected." + + "Attempting to handle error"); + throw new BudgetBuddyException("Please do not include a | or ! in your input"); + } + } + + /** + * Creates a RecurringExpenseCommand to view all expenses in a specific ExpenseList in recurringExpensesList + * This method obtains the listNumber from the provided commandParts. + * + * @param commandParts The split parts of the input command string + * @return RecurringExpenseCommand if list number is valid, returns null if list number is invalid or empty + */ + public Command createViewExpensesCommand(String[] commandParts) { + try { + String listNumberAsString = commandParts[2]; + int listNumber = Integer.parseInt(listNumberAsString); + return new RecurringExpenseCommand(listNumber, recurringExpenseLists, "viewexpenses"); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "An invalid integer has been detected"); + System.out.println("Please input a valid Integer"); + System.out.println("Command Format : rec viewexpenses [List Number]"); + return null; + } catch (ArrayIndexOutOfBoundsException e) { + LOGGER.log(Level.WARNING, "Value at commandParts[2] does not exist"); + System.out.println("List Number Cannot be Empty"); + System.out.println("Command Format : rec viewexpenses [List Number]"); + return null; + } + } + + /** + * Creates a RecurringExpenseCommand to add the expenses in a specific ExpenseList in recurringExpensesList into + * the overall ExpenseList. + * This method obtains the listNumber from the provided commandParts. + * + * @param commandParts The split parts of the input command string + * @return RecurringExpenseCommand if the list number is valid, returns null if list number is invalid or empty + */ + public Command createAddListToOverallExpensesCommand(String[] commandParts) { + + try { + String listNumberAsString = commandParts[2]; + int listNumber = Integer.parseInt(listNumberAsString); + return new RecurringExpenseCommand(listNumber, recurringExpenseLists, expenses, "addrec"); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "An invalid integer has been detected"); + System.out.println("Please input a valid Integer"); + System.out.println("Command Format : rec addrec [List Number]"); + return null; + } catch (ArrayIndexOutOfBoundsException e) { + LOGGER.log(Level.WARNING, "Value at commandParts[2] does not exist"); + System.out.println("List Number Cannot be Empty"); + System.out.println("Command Format : rec addrec [List Number]"); + return null; + } + + } + + + /** + * Checks the input for the presence of all the required to/ , d/, a/ and c/ prefixes + * + * @param input The user input + * @throws IllegalArgumentException if any of the required prefixes are not found + */ + private static void checkForInvalidParameters(String input) { + if (!input.contains("to/") || !input.contains("d/") || !input.contains("a/") || !input.contains("c/")) { + throw new IllegalArgumentException("Please Ensure that you include to/, c/, a/ and d/"); + } + } + + /** + * Parses the description from the input string + * + * @param input The user input + * @return The extracted description from the d/ prefix + * @throws BudgetBuddyException if the description is empty + */ + private String parseDescription(String input) throws BudgetBuddyException { + int indexOfDescriptionPrefix = input.indexOf(DESCRIPTION_PREFIX); + int startIndexOfDescription = indexOfDescriptionPrefix + DESCRIPTION_PREFIX.length(); + + int endIndexOfDescription = input.length(); + + String description = input.substring(startIndexOfDescription,endIndexOfDescription).trim(); + + if(description.trim().isEmpty()) { + LOGGER.log(Level.WARNING, "Empty Description Detected, throwing BudgetBuddyException"); + throw new BudgetBuddyException("Please Ensure Description is NOT empty"); + } + + return description; + } + + /** + * Parses the amount from the input string + * + * @param input The user input + * @return The extracted amount from the a/ prefix + * @throws NumberFormatException If the extracted amount is not a valid double + * @throws BudgetBuddyException If the extracted amount is empty, larger than maximum acceptable amount, + * is a negative number, is 0, or can be rounded to 0.00 at 2.dp + */ + private Double parseAmount(String input) throws NumberFormatException, BudgetBuddyException{ + int indexOfAmountPrefix = input.indexOf(AMOUNT_PREFIX); + int startIndexOfAmount = indexOfAmountPrefix + AMOUNT_PREFIX.length(); + + int indexOfDescriptionPrefix = input.indexOf(DESCRIPTION_PREFIX); + int endIndexOfAmount = indexOfDescriptionPrefix; + + String amountAsString = input.substring(startIndexOfAmount, endIndexOfAmount); + + if(amountAsString.trim().isEmpty()) { + LOGGER.log(Level.WARNING, "Empty Amount Detected, throwing BudgetBuddyException"); + throw new BudgetBuddyException("Please Ensure Amount is NOT empty"); + } + + Double amount = Double.parseDouble(amountAsString); + + if(amount > MAX_AMOUNT || amount <= 0 || amount < 0.005) { + throw new BudgetBuddyException("Please Ensure that Amount is a positive value " + + "(Not 0 when expressed as 2.d.p.) " + "and is Less than " + "1,000,000,000,000"); + } + + return amount; + } + + + /** + * Returns a case-insensitive match to the provided `category` + * @param category The category to be found + * @return The case-insensitive match to the category to be found + * @throws BudgetBuddyException if no matches are found + */ + private String getCategory(String category) throws BudgetBuddyException{ + for (String validCategory : expenseCategories) { + if (validCategory.equalsIgnoreCase(category)) { + return validCategory; + } + } + + throw new BudgetBuddyException("Please ensure the category is a valid category\n" + + "Valid Categories : Entertainment, Housing, Groceries, Utility, Transport, Others"); + } + /** + * Parses the category from the input string + * + * @param input The user input + * @return The extracted category from the c/ prefix + * @throws BudgetBuddyException If the category is empty + */ + private String parseCategory(String input) throws BudgetBuddyException{ + int indexOfCategoryPrefix = input.indexOf(CATEGORY_PREFIX); + int startIndexOfCategory = indexOfCategoryPrefix + CATEGORY_PREFIX.length(); + + int indexOfAmountPrefix = input.indexOf(AMOUNT_PREFIX); + int endIndexOfCategory = indexOfAmountPrefix; + + String categoryToObtain = input.substring(startIndexOfCategory, endIndexOfCategory).trim(); + + if(categoryToObtain.trim().isEmpty()) { + LOGGER.log(Level.WARNING, "Empty Category Detected, throwing BudgetBuddyException"); + throw new BudgetBuddyException("Please Ensure Category is NOT empty"); + } + + String category = getCategory(categoryToObtain); + return category; + } + + /** + * Parses the list number from the input string + * + * @param input The user input + * @return The extracted list number from the `to/` prefix + * @throws NumberFormatException if the list number is not a valid number + * @throws BudgetBuddyException if the list number is empty + */ + private int parseListNumber(String input) throws NumberFormatException, BudgetBuddyException{ + int indexOfListNumberPrefix = input.indexOf(LISTNUMBER_PREFIX); + int startIndexOfListNumber = indexOfListNumberPrefix + LISTNUMBER_PREFIX.length(); + + int indexOfCategoryPrefix = input.indexOf(CATEGORY_PREFIX); + int endIndexOfListNumber = indexOfCategoryPrefix; + + String listNumberAsString = input.substring(startIndexOfListNumber, endIndexOfListNumber).trim(); + + if(listNumberAsString.trim().isEmpty()) { + LOGGER.log(Level.WARNING, "Empty ListNumber Detected, throwing BudgetBuddyException"); + throw new BudgetBuddyException("Please Ensure List Number is NOT empty"); + } + + int listNumber = Integer.parseInt(listNumberAsString); + + return listNumber; + } + + private void checkForOutOfOrderParameters(String input) throws BudgetBuddyException{ + assert (input.contains(LISTNUMBER_PREFIX) && input.contains(CATEGORY_PREFIX) + && input.contains(AMOUNT_PREFIX) && input.contains(DESCRIPTION_PREFIX)) + : "Input has all required prefixes"; + + int indexOfListNumberPrefix = input.indexOf(LISTNUMBER_PREFIX); + int indexOfCategoryPrefix = input.indexOf(CATEGORY_PREFIX); + int indexOfAmountPrefix = input.indexOf(AMOUNT_PREFIX); + int indexOfDescriptionPrefix = input.indexOf(DESCRIPTION_PREFIX); + + if (indexOfListNumberPrefix > indexOfCategoryPrefix) { + throw new BudgetBuddyException("Please Ensure your prefixes are in the right order"); + } + + if (indexOfCategoryPrefix > indexOfAmountPrefix) { + throw new BudgetBuddyException("Please Ensure your prefixes are in the right order"); + } + + if (indexOfAmountPrefix > indexOfDescriptionPrefix) { + throw new BudgetBuddyException("Please Ensure your prefixes are in the right order"); + } + } + + private void checkForDuplicateParameters(String input) throws BudgetBuddyException { + + assert (input.contains(LISTNUMBER_PREFIX) && input.contains(CATEGORY_PREFIX) + && input.contains(AMOUNT_PREFIX) && input.contains(DESCRIPTION_PREFIX)) + : "Input has all required prefixes"; + + String[] parameters = {LISTNUMBER_PREFIX, CATEGORY_PREFIX, AMOUNT_PREFIX, DESCRIPTION_PREFIX}; + + for (String parameter : parameters) { + if (input.indexOf(parameter) != input.lastIndexOf(parameter)) { + LOGGER.log(Level.WARNING, "Duplicate Parameters Detected, throwing BudgetBuddyException"); + throw new BudgetBuddyException("Please ensure that you do not have any duplicate parameters"); + } + } + } + + /** + * Creates a RecurringExpenseCommand to add an expense into a specific ExpenseList in recurringExpensesList + * + * @param input The user input + * @return RecurringExpenseCommand if user input is valid, returns null if any of the user input is invalid + */ + public Command createAddExpenseToListCommand(String input) { + try { + checkForInvalidParameters(input); + checkForDuplicateParameters(input); + checkForInvalidCharacters(input); + checkForOutOfOrderParameters(input); + } catch (IllegalArgumentException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + System.out.println("Command Format : rec newexpense to/ LISTNUMBER c/ CATEGORY" + + " a/ AMOUNT d/ DESCRIPTION"); + return null; + } + + try { + int listNumber = parseListNumber(input); + String category = parseCategory(input); + Double amount = parseAmount(input); + String description = parseDescription(input); + + return new RecurringExpenseCommand(listNumber, recurringExpenseLists, category, + amount, description, "newexpense"); + } catch (BudgetBuddyException e) { + LOGGER.log(Level.INFO, "Successfully caught BudgetBuddy Exception. Handling Error"); + System.out.println(e.getMessage()); + System.out.println("Command Format : rec newexpense to/ LISTNUMBER c/ CATEGORY" + + " a/ AMOUNT d/ DESCRIPTION"); + return null; + } catch (NumberFormatException e) { + LOGGER.log(Level.INFO, "Successfully caught NumberFormatException. Handling Error"); + System.out.println("Please ensure that listNumber and Amount are valid Numbers/Monetary amounts"); + System.out.println("Command Format : rec newexpense to/ LISTNUMBER c/ CATEGORY" + + " a/ AMOUNT d/ DESCRIPTION"); + return null; + } + + } + + /** + * Creates a RecurringExpenseCommand to remove a specified ExpenseList in the recurringExpensesList + * This method uses the provided commandParts to obtain the list Number of the ExpenseList to remove + * + * @param commandParts The split parts of the user input + * @return RecurringExpenseCommand if user input is valid, returns null if listNumber is empty or invalid + */ + public Command createRemoveListCommand(String[] commandParts) { + try { + String listNumberAsString = commandParts[2]; + int listNumber = Integer.parseInt(listNumberAsString); + return new RecurringExpenseCommand(listNumber, recurringExpenseLists, "removelist"); + } catch (ArrayIndexOutOfBoundsException e) { + LOGGER.log(Level.INFO, "Successfully caught Exception. Handling Error"); + System.out.println("List Number Cannot be Empty"); + System.out.println("Command Format : rec removelist [List Number]"); + return null; + } catch (NumberFormatException e) { + LOGGER.log(Level.INFO, "Successfully caught Exception. Handling Error"); + System.out.println("Please input a valid Integer"); + System.out.println("Command Format : rec removelist [List Number]"); + return null; + } + } + + + /** + * Creates a RecurringExpenseCommand to print all the names of the ExpenseLists present in recurringExpensesList + * + * @return A RecurringExpenseCommand + */ + public Command createViewListCommand() { + return new RecurringExpenseCommand(recurringExpenseLists, "viewlists"); + } + + /** + * Creates a RecurringExpenseCommand to add a new RecurringExpenseList into recurringExpensesList + * + * @param input The user input + * @return RecurringExpenseCommand if listName is valid, returns null if the listName extracted is empty + */ + public Command createNewListCommand(String input) { + assert (input.startsWith("rec newlist")) : "Input must start with rec newlist "; + + try { + checkForInvalidCharacters(input); + int indexOfNewListCommandType = input.indexOf("newlist"); + int indexOfListName = indexOfNewListCommandType + "newlist".length(); + int endIndexOfListName = input.length(); + + String listName = input.substring(indexOfListName, endIndexOfListName).trim(); + + if (listName.isEmpty()) { + System.out.println("Please Ensure the LISTNAME is not empty"); + return null; + } + + return new RecurringExpenseCommand(listName, this.recurringExpenseLists, "newlist"); + + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("Something went wrong"); + return null; + } catch (BudgetBuddyException e) { + LOGGER.log(Level.INFO, "BudgetBuddyException has been caught and handled"); + System.out.println(e.getMessage()); + return null; + } + + } + + /** + * Handles the creation of the various types of RecurringExpenseCommand based on the extracted commandType + * This method extracts the commandType from the user input, and calls methods based on the commandType + * + * @param input The user input + * @return RecurringExpenseCommand if commandType extracted is a valid commandType, + * returns null if commandType is not valid + */ + public Command handleRecCommand(String input){ + + try { + String[] commandParts = input.split(" "); + String commandType = commandParts[1]; + commandType = commandType.trim(); + + switch(commandType) { + case "newlist": + return createNewListCommand(this.input); + case "viewlists": + return createViewListCommand(); + case "removelist": + return createRemoveListCommand(commandParts); + case "newexpense": + return createAddExpenseToListCommand(input); + case "addrec": + return createAddListToOverallExpensesCommand(commandParts); + case "viewexpenses": + return createViewExpensesCommand(commandParts); + default: + System.out.println("This Command Type does not exist for \"rec\""); + return null; + } + } catch (IndexOutOfBoundsException e) { + System.out.println("Please do not leave the command type empty"); + return null; + } + + } + + @Override + public Command createCommand() { + return handleRecCommand(input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/ReduceSavingCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/ReduceSavingCommandCreator.java new file mode 100644 index 0000000000..b28108c4c8 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/ReduceSavingCommandCreator.java @@ -0,0 +1,62 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ReduceSavingCommand; + +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ReduceSavingCommandCreator extends CommandCreator { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private SavingList savings; + private String input; + + public ReduceSavingCommandCreator(SavingList savings, String input) { + this.savings = savings; + this.input = input; + } + + public Command handleReduceSavingCommand(SavingList savings, String input) { + LOGGER.log(Level.INFO, "Processing handleReduceSavingCommand"); + + String description = input.replace("reduce", "").trim(); + Pattern categoryPattern = Pattern.compile("c/\\s*(\\w+)\\s*"); + Pattern amountPattern = Pattern.compile("a/\\s*(-?\\d+(\\.\\d+)?)\\s*"); + + Matcher categoryMatcher = categoryPattern.matcher(description); + Matcher amountMatcher = amountPattern.matcher(description); + + if (categoryMatcher.find() && amountMatcher.find()) { + try { + String categoryToReduce = categoryMatcher.group(1); + double amountToReduce = Double.parseDouble(amountMatcher.group(1)); + + if (amountToReduce <= 0) { + LOGGER.log(Level.WARNING, "Amount must be a positive value."); + System.out.println("Amount must be a positive value."); + return null; + } + + LOGGER.log(Level.INFO, "Successfully processed ReduceSavingCommand!"); + return new ReduceSavingCommand(savings, categoryToReduce, amountToReduce); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "Amount must be a valid number. Please try again."); + System.out.println("Amount must be a valid number."); + return null; + } + } else { + LOGGER.log(Level.WARNING, "Invalid command format. Expected format: " + + "reduce savings c/ a/."); + System.out.println("Invalid command format. Expected format: reduce savings c/ a/"); + return null; + } + } + + @Override + public Command createCommand() { + return handleReduceSavingCommand(savings, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/SetBudgetCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/SetBudgetCommandCreator.java new file mode 100644 index 0000000000..9ab9c18246 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/SetBudgetCommandCreator.java @@ -0,0 +1,90 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.SetBudgetCommand; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SetBudgetCommandCreator extends CommandCreator { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + public ExpenseList expenses; + public String input; + public ArrayList expenseCategories; + + + public SetBudgetCommandCreator(ExpenseList expenses, String input){ + this.expenses = expenses; + this.input = input; + this.expenseCategories = new ArrayList<>(Arrays.asList("Housing", "Groceries", "Utility", "Transport", + "Entertainment", "Others")); + } + + private boolean isValidExpenseCategory(String category) { + + assert category != null : "Category should not be null"; + assert !category.isEmpty() : "Category should not be empty"; + + for (String validCategory : expenseCategories) { + if (validCategory.equalsIgnoreCase(category)) { + return true; + } + } + return false; + } + + public Command handleSetBudgetCommand(ExpenseList expenses, String input) { + LOGGER.log(Level.INFO, "Entering handleSetBudgetCommand with input: " + input); + String[] parts = input.split(" "); + String category = null; + double budget = -1; + + for (String part : parts) { + if (part.startsWith("c/")) { + category = part.substring(2); + LOGGER.log(Level.INFO, "Category extracted: " + category); + } else if (part.startsWith("b/")) { + try { + budget = Double.parseDouble(part.substring(2)); + LOGGER.log(Level.INFO, "Budget extracted: " + budget); + if (budget < 0) { + LOGGER.log(Level.WARNING, "Budget cannot be negative."); + System.out.println("Budget cannot be negative."); + return null; + } + } catch (NumberFormatException e) { + LOGGER.log(Level.SEVERE, "Invalid budget format. Budget should be a number.", e); + System.out.println("Invalid budget format. Budget should be a number"); + return null; + } + } + } + + if (category == null || budget == -1) { + LOGGER.log(Level.WARNING, "Invalid command format or missing values for category/budget"); + System.out.println("Invalid command format."); + System.out.println("Expected format: set budget c/ b/"); + return null; + } + + boolean isValidCategory = isValidExpenseCategory(category); + if (!isValidCategory) { + LOGGER.log(Level.WARNING, "Invalid category: " + category); + System.out.println("Invalid category: " + category); + System.out.println("Valid categories: Housing, Groceries, Utility, Transport, Entertainment, Others"); + return null; + } + + LOGGER.log(Level.INFO, "Exiting handleSetBudgetCommand. Command ready for execution."); + return new SetBudgetCommand(expenses, category, budget); + } + + @Override + public Command createCommand(){ + return handleSetBudgetCommand(expenses, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/SettleSplitExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/SettleSplitExpenseCommandCreator.java new file mode 100644 index 0000000000..f42f2d738c --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/SettleSplitExpenseCommandCreator.java @@ -0,0 +1,57 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.SettleSplitExpenseCommand; + +public class SettleSplitExpenseCommandCreator extends CommandCreator{ + private String input; + private SplitExpenseList splitexpenses; + + /* + * Creates a SettleSplitExpenseCommandCreator object. + */ + public SettleSplitExpenseCommandCreator(String input, SplitExpenseList splitexpenses) { + this.input = input; + this.splitexpenses = splitexpenses; + } + + /* + * Parses the input and creates a new SettleSplitExpenseCommand object. + * + * @param input The input string. + * @param splitexpenses The list of split expenses. + * @return The SettleSplitExpenseCommand object. + */ + public Command handleSettleExpenseCommand(String input, SplitExpenseList splitexpenses) { + + assert input != null : "Input should not be null"; + assert !input.isEmpty() : "Input should not be empty"; + + String[] parts = input.split("i/", 2); + + if (parts.length < 2) { + System.out.println("Error: Invalid command format. Expected format: i/"); + return null; + } + + try { + int index = Integer.parseInt(parts[1]) - 1; + // Check if the index is within the bounds of the expense list. + if (index < 0 || index >= splitexpenses.getSize()) { + System.out.println("Error: Index is out of bounds."); + return null; + } + return new SettleSplitExpenseCommand(splitexpenses, index); + } catch (NumberFormatException e) { + // Catch the NumberFormatException if the part after "i/" isn't a valid integer. + System.out.println("Error: Index is not a valid number."); + return null; + } + } + + @Override + public Command createCommand() { + return handleSettleExpenseCommand(input, splitexpenses); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commandcreator/SplitExpenseCommandCreator.java b/src/main/java/seedu/budgetbuddy/commandcreator/SplitExpenseCommandCreator.java new file mode 100644 index 0000000000..18ae290a43 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commandcreator/SplitExpenseCommandCreator.java @@ -0,0 +1,81 @@ +package seedu.budgetbuddy.commandcreator; + +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.SplitExpenseCommand; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class SplitExpenseCommandCreator extends CommandCreator { + + private SplitExpenseList splitexpenses; + private String input; + + public SplitExpenseCommandCreator(SplitExpenseList splitexpenses, String input) { + this.splitexpenses = splitexpenses; + this.input = input; + } + + public Command handleSplitExpenseCommand(SplitExpenseList splitexpenses, String input) { + if (input == null || !input.contains("a/") || !input.contains("n/") || !input.contains("d/")) { + System.out.println("Invalid command format."); + return null; + } + + String amount = extractDetail(input, "a/", "n/"); + String numberOfPeople = extractDetail(input, "n/", "d/"); + String description = extractDetail(input, "d/", null); // Description is last, so no nextPrefix + + if (amount.isEmpty()|| numberOfPeople.isEmpty() || description.isEmpty()) { + System.out.println("Missing details."); + return null; + } + + double amountValue; + try { + amountValue = Double.parseDouble(amount); + if (amountValue <= 0 || amountValue > 1_000_000_000_000D) { + throw new BudgetBuddyException(amount + " is not a valid amount. Amount must be positive and" + + " less than or equal" + + " to 1,000,000,000,000."); + } + if (!amount.matches("^\\d+(\\.\\d{1,2})?$")) { + throw new BudgetBuddyException("Amount must be a number with up to 2 decimal places."); + } + } catch (NumberFormatException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + int numberValue; + try { + numberValue = Integer.parseInt(numberOfPeople); + if (numberValue <= 0) { + throw new BudgetBuddyException(numberOfPeople + " is not a valid number."); + } + } catch (NumberFormatException | BudgetBuddyException e) { + System.out.println(e.getMessage()); + return null; + } + + return new SplitExpenseCommand(splitexpenses, amount, numberOfPeople, description); + } + + private String extractDetail(String input, String prefix, String nextPrefix) { + try { + int startIndex = input.indexOf(prefix) + prefix.length(); + int endIndex = nextPrefix != null ? input.indexOf(nextPrefix, startIndex) : input.length(); + if (endIndex == -1) { + endIndex = input.length(); + } + String detail = input.substring(startIndex, endIndex).trim(); + return detail.isEmpty() ? "" : detail; + } catch (Exception e) { + return ""; + } + } + + @Override + public Command createCommand() { + return handleSplitExpenseCommand(splitexpenses, input); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/Budget.java b/src/main/java/seedu/budgetbuddy/commons/Budget.java new file mode 100644 index 0000000000..7b2511a4e0 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/Budget.java @@ -0,0 +1,23 @@ +package seedu.budgetbuddy.commons; + +public class Budget { + private String category; + private double budget; + + public Budget(String category, double budget){ + this.category = category; + this.budget = budget; + } + + public String getCategory(){ + return category; + } + + public double getBudget() { + return budget; + } + + public void setBudget(double budget){ + this.budget = budget; + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/CurrencyConverter.java b/src/main/java/seedu/budgetbuddy/commons/CurrencyConverter.java new file mode 100644 index 0000000000..31dbc9c65b --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/CurrencyConverter.java @@ -0,0 +1,242 @@ +package seedu.budgetbuddy.commons; + +import seedu.budgetbuddy.Ui; + +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +//@author sweijie24 +public class CurrencyConverter { + + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private Map exchangeRates; + private Ui ui = new Ui(); + + public CurrencyConverter() { + this.exchangeRates = new HashMap<>(); + // Initialize exchange rates with default values + exchangeRates.put(Currency.getInstance("SGD"), 1.0); + exchangeRates.put(Currency.getInstance("USD"), 0.75); + exchangeRates.put(Currency.getInstance("EUR"), 0.68); + exchangeRates.put(Currency.getInstance("JPY"), 112.25); + exchangeRates.put(Currency.getInstance("KRW"), 996.85); + exchangeRates.put(Currency.getInstance("MYR"), 3.51); + exchangeRates.put(Currency.getInstance("CNY"), 5.36); + exchangeRates.put(Currency.getInstance("HKD"), 5.80); + } + + + /** + * Converts an amount from one currency to another using exchange rates. + * + * @author sweijie24 + * @param amount The amount to be converted. + * @param fromCurrency The currency of the original amount. + * @param toCurrency The currency to which the amount is to be converted. + * @return The converted amount in the target currency. + * @throws IllegalArgumentException If exchange rates are not available for one or both currencies, + * or if exchange rates are not positive numbers. + */ + public double convertAmount(double amount, Currency fromCurrency, Currency toCurrency) { + if (!exchangeRates.containsKey(fromCurrency) || !exchangeRates.containsKey(toCurrency)) { + LOGGER.warning("Exchange rates not available for one or more currencies"); + throw new IllegalArgumentException("Exchange rates not available for one or more currencies"); + } + assert exchangeRates.containsKey(fromCurrency) : "Exchange rates not available for fromCurrency: " + + fromCurrency; + assert exchangeRates.containsKey(toCurrency) : "Exchange rates not available for toCurrency: " + toCurrency; + + + double fromRate = exchangeRates.get(fromCurrency); + double toRate = exchangeRates.get(toCurrency); + + if (fromRate <= 0 || toRate <= 0) { + LOGGER.warning("Exchange rates must be positive numbers"); + throw new IllegalArgumentException("Exchange rates must be positive numbers"); + } + assert fromRate > 0 : "Exchange rate for fromCurrency must be a positive number: " + fromRate; + assert toRate > 0 : "Exchange rate for toCurrency must be a positive number: " + toRate; + + LOGGER.info("Converting " + amount + " " + fromCurrency + " to " + toCurrency); + + if (!fromCurrency.equals(toCurrency)) { + double amountInSGD = amount / fromRate; + double convertedAmount = amountInSGD * toRate; + LOGGER.info("Conversion successful. Result: " + convertedAmount + " " + toCurrency); + return convertedAmount; + } else { + LOGGER.info("Same currency. No conversion needed."); + return amount; + } + } + + /** + * Converts the currency of expenses in the given ExpenseList to the specified new currency. + * No conversion necessary if trying to convert to the same currency. + * + * @author sweijie24 + * @param newCurrency The new currency to convert expenses to. + * @param expenses The ExpenseList containing the expenses to be converted. + * @throws IllegalArgumentException If the ExpenseList is null. + */ + public void convertExpenseCurrency(Currency newCurrency, ExpenseList expenses) { + if (expenses == null) { + throw new IllegalArgumentException("ExpenseList cannot be null"); + } + assert expenses != null : "ExpenseList cannot be null"; + + if (DefaultCurrency.getDefaultCurrency() == newCurrency) { + System.out.println("Same currency for Expenses. No conversion needed"); + } else { + for (Expense expense : expenses.getExpenses()) { + if (expense == null) { + LOGGER.warning("Skipping null expense"); + System.out.println("Skipping null expense"); + continue; + } + + try { + double convertedAmount = convertAmount(expense.getAmount(), expense.getCurrency(), newCurrency); + expense.setAmount(convertedAmount); + expense.setCurrency(newCurrency); + } catch (IllegalArgumentException e) { + LOGGER.severe("Error converting amount for expense: " + e.getMessage()); + System.out.println("Error converting amount for expense: " + e.getMessage()); + } + } + System.out.println("Default currency for Expenses changed to " + newCurrency); + } + } + + public void convertSplitExpenseCurrency(Currency newCurrency, SplitExpenseList splitExpenses) { + if (splitExpenses == null) { + throw new IllegalArgumentException("SplitExpenseList cannot be null"); + } + + assert splitExpenses != null : "SplitExpenseList cannot be null"; + + if (DefaultCurrency.getDefaultCurrency() == newCurrency) { + System.out.println("Same currency for Split Expenses. No Conversion needed"); + return; + } else { // Convert the currency of each split expense in the SplitExpenseList + for (SplitExpense splitExpense : splitExpenses.getSplitExpenses()) { + if (splitExpense == null) { + LOGGER.warning("Skipping null split expense"); + System.out.println("Skipping null split expense"); + continue; + } + + try { + double convertedAmount = convertAmount(splitExpense.getAmount(), splitExpense.getCurrency(), + newCurrency); + splitExpense.setAmount(convertedAmount); + splitExpense.setCurrency(newCurrency); + } catch (IllegalArgumentException e) { + LOGGER.severe("Error converting amount for split expense: " + e.getMessage()); + System.out.println("Error converting amount for split expense: " + e.getMessage()); + } + } + System.out.println("Default currency for Split Expenses changed to " + newCurrency); + } + } + + /** + * Converts the currency of savings in the given SavingList to the specified new currency. + * No conversion necessary if trying to convert to the same currency. + * + * @author sweijie24 + * @param newCurrency The new currency to convert savings to. + * @param savings The SavingList containing the savings to be converted. + * @throws IllegalArgumentException If the SavingList is null. + */ + public void convertSavingCurrency(Currency newCurrency, SavingList savings) { + if (savings == null) { + throw new IllegalArgumentException("SavingList cannot be null"); + } + assert savings != null : "SavingList cannot be null"; + + if (DefaultCurrency.getDefaultCurrency() == newCurrency) { + System.out.println("Same currency for Savings. No conversion needed"); + } else { + for (Saving saving : savings.getSavings()) { + if (saving == null) { + LOGGER.warning("Skipping null saving"); + System.out.println("Skipping null saving"); + continue; + } + + try { + double convertedAmount = convertAmount(saving.getAmount(), saving.getCurrency(), newCurrency); + saving.setAmount(convertedAmount); + saving.setCurrency(newCurrency); + } catch (IllegalArgumentException e) { + LOGGER.severe("Error converting amount for saving: " + e.getMessage()); + System.out.println("Error converting amount for saving: " + e.getMessage()); + } + } + System.out.println("Default currency for Savings changed to " + newCurrency); + } + } + + // @@author itsmejr257 + public void convertRecurringExpensesCurrency(Currency newCurrency, RecurringExpenseLists recurringExpenseLists) { + assert recurringExpenseLists != null : "RecurringExpenseLists cannot be null"; + + if (DefaultCurrency.getDefaultCurrency() == newCurrency) { + System.out.println("Same currency for Recurring Expenses. No Conversion needed"); + return; + } + + int numberOfExpenseList = recurringExpenseLists.getSize(); + + ui.printDivider(); + System.out.println("Conversion for expenses in Recurring Expenses : "); + + for (int i = 0; i < numberOfExpenseList; i++) { + int arrayIndexAsListNumber = i + 1; + ExpenseList reccuringExpenseList = recurringExpenseLists.getExpenseListAtListNumber(arrayIndexAsListNumber); + System.out.print("Changing the default currency for " + reccuringExpenseList.getName() + ": "); + convertExpenseCurrency(newCurrency, reccuringExpenseList); + } + + System.out.println("Default currency for Recurring Expenses changed to " + newCurrency); + ui.printDivider(); + } + // @@author + + + public void convertBudgetCurrency(Currency newCurrency, ExpenseList expenseList) { + if (expenseList == null) { + throw new IllegalArgumentException("ExpenseList cannot be null"); + } + + // Check if the new currency is the same as the default currency to avoid unnecessary conversion + if (DefaultCurrency.getDefaultCurrency().equals(newCurrency)) { + System.out.println("Same currency. No conversion needed for budgets."); + return; + } + + // Iterate over each budget in the ExpenseList and convert its currency + for (Budget budget : expenseList.getBudgets()) { + // Assuming the budget amount is in the default currency + Currency defaultCurrency = DefaultCurrency.getDefaultCurrency(); + try { + // Convert the budget amount from the default currency to the new currency + double convertedBudgetAmount = convertAmount(budget.getBudget(), defaultCurrency, newCurrency); + // Update the budget amount with the converted value + budget.setBudget(convertedBudgetAmount); + } catch (IllegalArgumentException e) { + // Handle any IllegalArgumentException thrown during conversion + LOGGER.severe("Error converting budget amount for category: " + budget.getCategory() + + "; " + e.getMessage()); + System.out.println("Error converting budget amount for category: " + budget.getCategory() + + "; " + e.getMessage()); + } + } + + System.out.println("Budgets successfully converted to " + newCurrency.getCurrencyCode()); + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commons/DefaultCurrency.java b/src/main/java/seedu/budgetbuddy/commons/DefaultCurrency.java new file mode 100644 index 0000000000..a16d072976 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/DefaultCurrency.java @@ -0,0 +1,16 @@ +package seedu.budgetbuddy.commons; + +import java.util.Currency; + +//@author sweijie24 +public class DefaultCurrency { + + private static Currency defaultCurrency = Currency.getInstance("SGD"); + + public static Currency getDefaultCurrency() { + return defaultCurrency; + } + public static void setDefaultCurrency(Currency currency) { + defaultCurrency = currency; + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/Expense.java b/src/main/java/seedu/budgetbuddy/commons/Expense.java new file mode 100644 index 0000000000..86a8c39964 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/Expense.java @@ -0,0 +1,36 @@ +package seedu.budgetbuddy.commons; + +import java.time.LocalDate; + +//@author sweijie24 +public class Expense extends Transaction{ + protected String description; + private LocalDate dateAdded; + + public Expense(LocalDate dateAdded, String category, double amount, String description) { + super(category, amount); + this.dateAdded = dateAdded; + this.description = description; + } + public Expense(String category, double amount, String description) { + super(category, amount); + this.description = description; + this.dateAdded = LocalDate.now(); + } + public LocalDate getDateAdded() { + return dateAdded; + } + + public String getDescription(){ + return description; + } + + public void setDescription(String description){ + this.description = description; + } + + @Override + public String toString() { + return "Category: " + category + " Amount: " + amount + " Description: " + description; + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/ExpenseList.java b/src/main/java/seedu/budgetbuddy/commons/ExpenseList.java new file mode 100644 index 0000000000..642cf812e1 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/ExpenseList.java @@ -0,0 +1,483 @@ +package seedu.budgetbuddy.commons; + +import seedu.budgetbuddy.Ui; +import seedu.budgetbuddy.exception.BudgetBuddyException; +import java.util.Arrays; + +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; +import java.util.Comparator; + + +public class ExpenseList { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final double MAX_AMOUNT = 1_000_000_000_000.00; + protected ArrayList expenses; + protected ArrayList categories = new ArrayList<>(Arrays.asList("Housing", + "Groceries", "Utility", "Transport", "Entertainment", "Others")); + protected List budgets; + + + Ui ui = new Ui(); + + public ExpenseList(ArrayList expenses) { + this.expenses = expenses; + this.budgets = new ArrayList<>(); + } + + public ExpenseList() { + this.expenses = new ArrayList<>(); + this.budgets = new ArrayList<>(); + } + + public int size() { + return expenses.size(); + } + + public ArrayList getExpenses() { + return expenses; + } + + public List getCategories() { + return this.categories; + } + + public List getBudgets() { + return this.budgets; + } + + + // @@author itsmejr257 + /** + * Filters this.expenses based on the provided description, minimum amount and maximum amount. + * This method uses Java streams to perform a case-insensitive search for the description + * , and filters expenses to include the range specified by the minAmount and maxAmount. + * + * @param description The description to match against the description of the Expense object + * @param minAmount The minimum amount value of the Expense object + * @param maxAmount The minimum amount value of the Expense object + * + * @return An ArrayList of Expense object containing all filtered + * Expense which match the provided parameters provided + */ + public ArrayList filterExpenses(String description, Double minAmount, Double maxAmount) { + assert minAmount == null || maxAmount == null || minAmount <= maxAmount + : "Minimum Amount must be smaller than or equals to Max Amount if both are not null"; + + LOGGER.log(Level.INFO, "Start Filtering expenses based on description : " + description + " minAmount : " + + minAmount + " maxAmount : " + maxAmount); + + String descriptionInLowerCase = description.toLowerCase(); + ArrayList filteredExpenses = new ArrayList<>(this.expenses.stream() + .filter(expense -> (expense.getDescription() + .toLowerCase().contains(descriptionInLowerCase))) + .filter(expense -> (minAmount == null || expense.getAmount() >= minAmount)) + .filter(expense -> (maxAmount == null || expense.getAmount() <= maxAmount)) + .collect(Collectors.toList())); + + LOGGER.log(Level.INFO, "Ending filtering and returning filtered expenses"); + return filteredExpenses; + + } + // @@author + + /** + * Lists expenses based on the provided filter category. + * If no filter category is specified, all expenses are listed. + * + * @author sweijie24 + * @param filterCategory the category by which to filter the expenses (optional) + */ + public void listExpenses(String filterCategory) { + LOGGER.info("Listing expenses..."); + + try { + System.out.println(String.format("Current Currency: %s\n", DefaultCurrency.getDefaultCurrency())); + + System.out.println("Expenses:"); + for (int i = 0; i < expenses.size(); i++) { + Expense expense = expenses.get(i); + + // Checks for null expenses + if (expense == null) { + LOGGER.warning("Expense object at index " + i + " is null"); + continue; + } + + if (filterCategory == null || expense.getCategory().equalsIgnoreCase(filterCategory)) { + System.out.print(i+1 + " | "); + System.out.print("Date: " + expense.getDateAdded() + " | "); + System.out.print("Category: " + expense.getCategory() + " | "); + System.out.print("Amount: $" + String.format("%.2f", expense.getAmount()) + " | "); + System.out.println("Description: " + expense.getDescription() + " | "); + } + } + ui.printDivider(); + System.out.println("Overall Total Expenses: $" + String.format("%.2f", calculateTotalExpenses())); + + // Assertion: Check if total expenses calculation is correct + double totalExpenses = calculateTotalExpenses(); + assert totalExpenses >= 0 : "Total expenses should be non-negative"; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "An error occurred while listing expenses.", e); + } + } + + /** + * Calculates the total expenses from the list of expenses. + * Negative expense amounts are considered invalid. + * + * @author sweijie24 + * @return The total expenses. + * @throws IllegalArgumentException If any expense amount is negative. + */ + public double calculateTotalExpenses() { + double totalExpenses = 0; + try { + for (Expense expense: expenses) { + if (expense.getAmount() < 0) { + throw new IllegalArgumentException("Expenses should not be negative"); + } + totalExpenses += expense.getAmount(); + } + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Negative expense amount detected", e); + } + + // Assertion: Check if total expenses is non-negative + assert totalExpenses >= 0 : "Total expenses should be non-negative"; + + return totalExpenses; + } + + private boolean checkBudgetBeforeAddingExpense(String category, double amountAsDouble) { + Budget budgetForCategory = budgets.stream() + .filter(budget -> budget.getCategory().equalsIgnoreCase(category)) + .findFirst() + .orElse(null); + + if (budgetForCategory != null) { + double totalSpent = expenses.stream() + .filter(expense -> expense.getCategory().equalsIgnoreCase(category)) + .mapToDouble(Expense::getAmount) + .sum(); + return totalSpent + amountAsDouble > budgetForCategory.getBudget(); + } + return false; + } + + //@@author itsmejr257 + /** + * Adds an expense to the overall expense list. Takes in 2 strings, category and description, and one double + * value. + * This method is an overloaded method, where this method takes in an amount as a Double attribute and + * assumes that the provide category, amount and description is always valid. + * + * @param category The category of the expense to be added + * @param amount The amount of the expense to be added + * @param description The description of the expense to be added + */ + public void addExpense(String category, Double amount, String description) { + assert category != null : "Category should not be null"; + assert amount != null : "Amount should not be null"; + assert description != null : "Description should not be null"; + + boolean budgetExceeded = checkBudgetBeforeAddingExpense(category, amount); + if (budgetExceeded) { + System.out.println("Warning: Adding this expense will exceed your budget for " + category); + boolean userConfirmation = ui.getUserConfirmation(); + if (!userConfirmation) { + System.out.println("Expense not added due to budget constraints."); + return; + } + } + + Expense expense = new Expense(category, amount, description); + expenses.add(expense); + + System.out.println("Expense added: " + category + " of $" + String.format("%.2f", amount) + + " Description: " + description); + } + // @@author + + //@@author Zhang Yangda + public void addExpense(String category, String amount, String description) throws BudgetBuddyException { + assert category != null : "Category should not be null"; + assert amount != null : "Amount should not be null"; + assert description != null : "Description should not be null"; + + String matchedCategory = categories.stream() + .filter(existingCategory -> existingCategory.equalsIgnoreCase(category)) + .findFirst() + .orElseThrow(() -> new BudgetBuddyException("The category '" + category + "' is not listed.")); + + if (!amount.matches("^\\d+(\\.\\d{1,2})?$")) { + throw new BudgetBuddyException("Invalid amount format. Amount should be a positive number with up" + + " to maximum two decimal places."); + } + + double amountAsDouble; + try { + amountAsDouble = Double.parseDouble(amount); + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Invalid amount format. Amount should be a number."); + } + + if (amountAsDouble < 0) { + throw new BudgetBuddyException("Expenses should not be negative."); + } + + boolean budgetExceeded = checkBudgetBeforeAddingExpense(category, amountAsDouble); + if (budgetExceeded) { + System.out.println("Warning: Adding this expense will exceed your budget for " + category); + boolean userConfirmation = ui.getUserConfirmation(); + if (!userConfirmation) { + System.out.println("Expense not added due to budget constraints."); + return; + } + + if (amountAsDouble > MAX_AMOUNT) { + throw new BudgetBuddyException("Amount exceeds the maximum allowed limit of " + MAX_AMOUNT); + } + } + + Expense expense = new Expense(matchedCategory, amountAsDouble, description); + expenses.add(expense); + + System.out.println("Expense added: " + matchedCategory + " of $" + String.format("%.2f", amountAsDouble) + + " Description: " + description); + } + + + + /** + * Edits an expense entry in the expenses list at the specified index. Updates the category, + * amount, and description of the expense. + * + * @param category The new category to assign to the expense entry. + * @param index The index in the list where the expense entry is located. + * @param amount The new amount to assign to the expense entry. + * @param description The new description to assign to the expense entry. + */ + public void editExpense(String category, int index, double amount, String description) { + LOGGER.info(String.format("Attempting to edit expense at index %d with category '%s', " + + "amount %.2f, and description '%s'", index, category, amount, description)); + + // Assert that the provided category is not null or empty + assert category != null && !category.isEmpty() : "Category cannot be null or empty"; + // Assert that the index is within the valid bounds of the expenses list + assert index > 0 && index <= expenses.size() : "Index is out of bounds"; + // Assert that the amount is non-negative + assert amount >= 0 : "Amount cannot be negative"; + // Assert that the description is not null. + assert description != null : "Description cannot be null"; + + // Check if the category exists in the list of categories + int categoryIndex = categories.indexOf(category); + if (categoryIndex == -1) { + LOGGER.warning("Invalid category: " + category); + System.out.println("Invalid category."); + return; + } + + // Check if the index is within valid bounds + if (index <= 0 || index > expenses.size()) { + LOGGER.warning("Invalid index: " + index); + System.out.println("Invalid index. Enter \"List Expenses\" to view the index."); + return; + } + + try { + // Retrieve the expense to edit + Expense expenseToEdit = expenses.get(index - 1); + + // Update the expense details + expenseToEdit.setCategory(category); + expenseToEdit.setAmount(amount); + expenseToEdit.setDescription(description); + + LOGGER.info("Expense at index " + index + " edited successfully. New details: " + + expenseToEdit.toString()); + System.out.println("Expense edited successfully."); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error editing expense at index " + index, e); + } + } + + public void deleteExpense(int index){ + if (index >= 0 && index < expenses.size()){ + expenses.remove(index); + System.out.println("Expense deleted successfully!"); + } else { + System.out.println("Invalid expense index."); + } + } + + public String getName() { + return "placeholder"; + } + + public void setBudget(String category, double budget) { + LOGGER.info("Setting budget - Category: " + category + ", Budget: $" + budget); + for (Budget b : budgets) { + if (b.getCategory().equalsIgnoreCase(category)) { + LOGGER.info("Updating budget for category: " + category); + b.setBudget(budget); + System.out.println("Updated budget for " + category + " to $" + budget); + return; + } + } + LOGGER.info("Creating new budget for category: " + category); + budgets.add(new Budget(category, budget)); + System.out.println("New budget set for " + category + ": $" + budget); + } + + /** + * Retrieves and prints the budget for a specified category and lists all the expenses under that category. + * The expenses are sorted from the highest to the lowest amount, displaying the amount and what percentage + * of the total budget each expense constitutes. + * + * @param category The category for which to retrieve and print the budget and expenses. + */ + public void getBudgetAndListExpensesForCategory(String category) { + Budget budgetForCategory = budgets.stream() + .filter(budget -> budget.getCategory().equalsIgnoreCase(category)) + .findFirst() + .orElse(null); + + if (budgetForCategory == null) { + System.out.println("No budget set for " + category); + return; + } + + double budgetAmount = budgetForCategory.getBudget(); + System.out.println("Budget for " + category + ": $" + budgetAmount); + + List expensesForCategory = expenses.stream() + .filter(expense -> expense.getCategory().equalsIgnoreCase(category)) + .sorted(Comparator.comparingDouble(Expense::getAmount).reversed()) + .collect(Collectors.toList()); + + if (expensesForCategory.isEmpty()) { + System.out.println("No expenses recorded for " + category); + return; + } + + System.out.printf("%-20s | %-15s | %-15s%n", "Expense", "Amount", "% of Budget"); + ui.printDivider(); + + for (Expense expense : expensesForCategory) { + double amount = expense.getAmount(); + double percentOfBudget = (amount / budgetAmount) * 100; + System.out.printf("%-20s | $%-14.2f | %-14.2f%%%n", expense.getDescription(), amount, percentOfBudget); + ui.printDivider(); + } + } + + + /** + * Calculates and prints a distribution of expenses in various categories as a horizontal bar graph. + * It also identifies and prints the categories with the highest and lowest expenses, + * as well as categories where no expenses have been added. + */ + public void getExpenseInsights() { + double totalExpenses = calculateTotalExpenses(); + if (totalExpenses == 0) { + System.out.println("No expenses to display."); + return; + } + + Map sumsByCategory = new HashMap<>(); + for (Expense expense : expenses) { + sumsByCategory.merge(expense.getCategory(), expense.getAmount(), Double::sum); + } + + // Calculate the highest expense amount + double highestExpense = Collections.max(sumsByCategory.values()); + + // Identify the categories with the highest expenses + List highestCategories = sumsByCategory.entrySet().stream() + .filter(entry -> entry.getValue().equals(highestExpense)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // Calculate the lowest expense amount excluding the highest if it's the only value + double lowestExpense = sumsByCategory.entrySet().stream() + .filter(entry -> entry.getValue() > 0 && !highestCategories.contains(entry.getKey())) + .mapToDouble(Map.Entry::getValue) + .min().orElse(Double.MAX_VALUE); + + // Identify the categories with the lowest expenses + List lowestCategories = sumsByCategory.entrySet().stream() + .filter(entry -> entry.getValue() > 0 && entry.getValue().equals(lowestExpense)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // If lowestExpense is Double.MAX_VALUE, then this list should be empty + if (lowestExpense == Double.MAX_VALUE) { + lowestCategories.clear(); + } + + // Identify categories with no expenses + List noExpenseCategories = categories.stream() + .filter(category -> !sumsByCategory.containsKey(category) || sumsByCategory.get(category) == 0) + .collect(Collectors.toList()); + + ui.printDivider(); + printExpensesDistribution(sumsByCategory, totalExpenses); + ui.printDivider(); + + System.out.println("Highest Expense Category: " + formatCategoryList(highestCategories)); + System.out.println("Lowest Expense Category: " + formatCategoryList(lowestCategories)); + System.out.println("Categories with no expenses added: " + formatCategoryList(noExpenseCategories)); + ui.printDivider(); + } + + /** + * Prints the distribution of expenses in a bar graph format. + * Each category's expenses are represented by a percentage and a visual bar made of hashes ('#'). + * + * @param sumsByCategory A map containing the sum of expenses for each category. + * @param totalExpenses The total amount of expenses, used to calculate the percentage for each category. + */ + private void printExpensesDistribution(Map sumsByCategory, double totalExpenses) { + // Find the maximum percentage to scale the bars + double maxPercentage = sumsByCategory.values().stream() + .mapToDouble(amount -> (amount / totalExpenses) * 100) + .max() + .orElse(100); + + // Calculate percentages and build bars + for (String category : categories) { + Double sum = sumsByCategory.getOrDefault(category, 0.0); + double percentage = (sum / totalExpenses) * 100; + int barLength = (int) (percentage / (maxPercentage / 50)); + String bar = "[" + "#".repeat(Math.max(0, barLength)) + "]"; + System.out.println(String.format("%-15s: %6.2f%% %s", category, percentage, bar)); + } + } + + /** + * Formats a list of categories into a string. If the list contains more than one category, + * they are joined by commas, with "and" before the last category. If the list is empty, + * returns "None". + * + * @param categories The list of category names to be formatted. + * @return A string representing the formatted categories or "None" if the list is empty. + */ + private String formatCategoryList(List categories) { + if (categories.isEmpty()) { + return "None"; + } else { + return String.join(", ", categories.subList(0, categories.size() - 1)) + + (categories.size() > 1 ? " and " : "") + categories.get(categories.size() - 1); + } + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseList.java b/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseList.java new file mode 100644 index 0000000000..f749c461d3 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseList.java @@ -0,0 +1,32 @@ +package seedu.budgetbuddy.commons; + +import java.util.ArrayList; + +// @@author itsmejr257 +/** + * Represents a list of expenses for recurring expenses. This class extends + * the ExpenseList class. + */ +public class RecurringExpenseList extends ExpenseList{ + String name; + + /** + * Constructs a new RecurringExpenseList with the provided name and list of expenses + * @param name The provided name for the recurring expense list + * @param expenses An arraylist of Expense objects + */ + public RecurringExpenseList(String name, ArrayList expenses) { + this.name = name; + super.expenses = expenses; + } + + /** + * Returns the name of this recurring expense list + * + * @return The name of this recurring expense list. + */ + @Override + public String getName() { + return this.name; + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseLists.java b/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseLists.java new file mode 100644 index 0000000000..4765186d2e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/RecurringExpenseLists.java @@ -0,0 +1,113 @@ +package seedu.budgetbuddy.commons; + +import seedu.budgetbuddy.Ui; + +import java.util.ArrayList; + +// @@author itsmejr257 +/** + * Represents a list of ExpenseList. Each ExpenseList contains multiple expenses. + * This class provides methods to add, remove and manage the list of ExpenseList + * + */ +public class RecurringExpenseLists { + protected ArrayList recurringExpenses; + + Ui ui = new Ui(); + + public RecurringExpenseLists(ArrayList recurringExpenses) { + this.recurringExpenses = recurringExpenses; + } + + /** + * Constructs an RecurringExpensesList object with an empty ArrayList + */ + public RecurringExpenseLists() { + this.recurringExpenses = new ArrayList<>(); + } + + /** + * Adds a new RecurringExpenseList with the provided listName to the list of recurring expenses + * + * @param listName Name of the RecurringExpenseList to be added + */ + public void addNewRecurringList(String listName) { + + assert !listName.contains("!") && !listName.contains("|") : "List Name should not contain a ! or |"; + assert !listName.isEmpty() : "List Name should not be empty"; + ExpenseList expenses = new RecurringExpenseList(listName, new ArrayList<>()); + + recurringExpenses.add(expenses); + + ui.printDivider(); + System.out.println("New List Created with name : " + expenses.getName()); + ui.printDivider(); + } + + /** + * Removes the ExpenseList at the provided listNumber in the list of recurring expenses + * + * @param listNumber Position of list to delete according to the provided list of recurring expenses printed to user + */ + public void removeList(int listNumber) { + int listNumberAsArrayPosition = listNumber - 1; + recurringExpenses.remove(listNumberAsArrayPosition); + + ui.printDivider(); + System.out.println("List Successfully Removed"); + ui.printDivider(); + + } + + /** + * Prints the names of all ExpenseList within the main list of recurring expenses. If there are + * no ExpenseList inside recurringExpensesList, a message indicating the absence of recurring expenses + * is displayed + * + */ + public void printAllRecurringLists() { + + int counter = 1; + + if (recurringExpenses.isEmpty()) { + ui.printDivider(); + System.out.println("You currently have no Recurring Expenses"); + ui.printDivider(); + return; + } + + ui.printDivider(); + System.out.println("These are your lists of Recurring Expenses"); + + for (ExpenseList expenses : recurringExpenses) { + String listName = expenses.getName(); + System.out.println(counter + ". " + listName); + counter += 1; + } + + ui.printDivider(); + } + + /** + * Returns the number of ExpenseList present in recurringExpensesList + * + * @return The size of recurringExpensesList + */ + public int getSize() { + return recurringExpenses.size(); + } + + /** + * Returns the ExpenseList at the specified position. + * Position of the ExpenseList is 1-based, so position is subtracted by 1 to convert to + * ArrayList index + * + * @param listNumber Position of ExpenseList according to the printed list to the user + * @return The ExpenseList at the listNumber provided + */ + public ExpenseList getExpenseListAtListNumber(int listNumber) { + + int listNumberAsArrayPosition = listNumber - 1; + return recurringExpenses.get(listNumberAsArrayPosition); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/Saving.java b/src/main/java/seedu/budgetbuddy/commons/Saving.java new file mode 100644 index 0000000000..10c95500ec --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/Saving.java @@ -0,0 +1,19 @@ +package seedu.budgetbuddy.commons; + +//@author sweijie24 +public class Saving extends Transaction{ + + public Saving(String category, double amount) { + super(category, amount); + } + + @Override + public String toString() { + return "Category: " + category + " Amount: " + amount; + } + + public double getAmount() { + return amount; + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commons/SavingList.java b/src/main/java/seedu/budgetbuddy/commons/SavingList.java new file mode 100644 index 0000000000..84ef30a067 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/SavingList.java @@ -0,0 +1,376 @@ +package seedu.budgetbuddy.commons; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.Collections; + +import seedu.budgetbuddy.Ui; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +public class SavingList { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final double MAX_AMOUNT = 1_000_000_000_000.0; + + protected ArrayList savings; + protected ArrayList categories; + protected double initialAmount; + Ui ui = new Ui(); + + + + public SavingList() { + this.savings = new ArrayList<>(); + this.categories = new ArrayList<>(Arrays.asList("Salary", + "Investments", "Gifts", "Others")); + this.initialAmount = 0; + } + + + public double getInitialAmount() { + return this.initialAmount; + } + + public ArrayList getSavings() { + return savings; + } + + /** + * Calculates the total savings amount by summing up the amounts of all savings. + * Sets the initial amount to the calculated total savings. + * Logs a severe error if an AssertionError occurs during the calculation. + */ + public void findTotalSavings() { + try { + assert savings != null : "Savings list should not be null"; + + double totalSavings = 0; + for (int i = 0; i < savings.size(); i++) { + Saving saving = savings.get(i); + assert saving != null : "Saving object at index " + i + " is null"; + totalSavings += saving.getAmount(); + } + + this.initialAmount = totalSavings; + } catch (AssertionError e) { + LOGGER.log(Level.SEVERE, "Error occurred while calculating total savings", e); + } + } + + /** + * Lists the savings, optionally filtered by category, + * and calculates the remaining savings after deducting expenses. + * Prints the initial savings amount, expenses deducted, and the remaining amount. + * + * @author sweijie24 + * @param filterCategory The category to filter savings by (optional). If null, all savings are listed. + * @param expenseList The ExpenseList object containing the expenses to deduct from savings. + */ + public void listSavings(String filterCategory, ExpenseList expenseList) { + LOGGER.info("Listing savings..."); + findTotalSavings(); + + try { + System.out.println(String.format("Current Currency: %s", DefaultCurrency.getDefaultCurrency())); + System.out.println("Savings:"); + for (int i = 0; i < savings.size(); i++) { + Saving saving = savings.get(i); + if (filterCategory == null || saving.getCategory().equalsIgnoreCase(filterCategory)) { + System.out.print(i + 1 + " | "); + System.out.print("Category: " + saving.getCategory() + " | "); + System.out.println("Amount: $" + String.format("%.2f", saving.getAmount()) + " | "); + } + } + ui.printDivider(); + System.out.println("Initial Savings Amount: $" + String.format("%.2f", initialAmount)); + System.out.println("Expenses Deducted: "); + + double totalExpenses = 0; + for (Expense expense : expenseList.getExpenses()) { + totalExpenses += expense.getAmount(); + System.out.println("$" + String.format("%.2f", expense.getAmount()) + + " spent on " + expense.getDescription() + + " on " + expense.getDateAdded()); + } + ui.printDivider(); + + double remainingAmount = calculateRemainingSavings(initialAmount, totalExpenses); + if (remainingAmount < 0) { + remainingAmount *= -1; + System.out.println("You are currently short on savings by: $" + String.format("%.2f", remainingAmount)); + } else { + System.out.println("Overall Remaining Amount: $" + String.format("%.2f", remainingAmount)); + + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "An error occurred while listing savings", e); + } + } + + /** + * Calculates the remaining savings amount after deducting total expenses from the initial amount. + * + * @author sweijie24 + * @param initialAmount The initial amount of savings. + * @param totalExpenses The total amount of expenses to be deducted. + * @return The remaining savings amount after deducting total expenses. + */ + public double calculateRemainingSavings(double initialAmount, double totalExpenses) { + try { + assert initialAmount >= 0 : "Initial amount should not be negative"; + assert totalExpenses >= 0 : "Total expenses should not be negative"; + + return (initialAmount - totalExpenses); + } catch (AssertionError e) { + LOGGER.log(Level.SEVERE, "Assertion failed while calculating remaining savings", e); + } + return -1; + } + + public void addSaving(String category, String amount) throws BudgetBuddyException{ + assert category != null : "Category should not be null"; + assert amount != null : "Amount should not be null"; + LOGGER.info("Adding saving..."); + + if (categories.stream().noneMatch(existingCategory -> existingCategory.equalsIgnoreCase(category))) { + throw new BudgetBuddyException("The category '" + category + "' is not listed."); + } + + + if (!amount.matches("^\\d+(\\.\\d{1,2})?$")) { + throw new BudgetBuddyException("Invalid amount format. Amount should be a positive number with up" + + " to maximum two decimal places."); + } + + double amountDouble; + try { + amountDouble = Double.parseDouble(amount); + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Invalid amount format. Amount should be a number."); + } + + if (amountDouble < 0) { + throw new BudgetBuddyException("Savings should not be negative."); + } + + if (amountDouble > MAX_AMOUNT) { + throw new BudgetBuddyException("Amount exceeds the maximum allowed limit of " + MAX_AMOUNT); + } + + boolean found = false; + for (Saving saving : savings) { + if (saving.getCategory().equalsIgnoreCase(category)) { + saving.setAmount(saving.getAmount() + amountDouble); + found = true; + LOGGER.info("Updated existing saving for category: " + category); + break; + } + } + if (!found) { + String matchedcateogry = categories.stream() + .filter(existingCategory -> existingCategory.equalsIgnoreCase(category)) + .findFirst() + .orElse(null); + Saving saving = new Saving(matchedcateogry, amountDouble); + savings.add(saving); + System.out.println("Savings Added to: " + matchedcateogry + " of $" + amount); + } + } + + + + + public void reduceSavingsByCategory(String category, double amount) { + List matchedSavings = savings.stream() + .filter(s -> s.getCategory().equalsIgnoreCase(category)) + .collect(Collectors.toList()); + + if (matchedSavings.isEmpty()) { + System.out.println("No savings found under category: " + category); + return; + } + + boolean allReductionsSuccessful = true; + for (Saving saving : matchedSavings) { + if (saving.getAmount() >= amount) { + saving.setAmount(saving.getAmount() - amount); + } else { + System.out.println("Insufficient amount in " + category + " to reduce by $" + amount); + allReductionsSuccessful = false; + } + } + + if (allReductionsSuccessful) { + System.out.println("Savings reduced successfully for category: " + category); + } + } + + + /** + * Edits the savings entries matched by category, treating all category names in a case-insensitive manner. + * This method updates the amount of saving objects within the savings list. + * If the provided category doesn't exist, it logs a warning and prints an error message without making changes. + * + * @param category The category to which the saving entries will be updated. + * @param amount The new amount to update the saving entries. + */ + public void editSaving(String category, double amount) { + LOGGER.info(String.format("Attempting to edit savings with category '%s' and amount %.2f", category, amount)); + + // Convert category to lower case for case insensitive comparison + String lowerCaseCategory = category.toLowerCase(); + + // Assert that the provided category is not null or empty + assert category != null && !category.isEmpty() : "Category cannot be null or empty"; + + // Assert that the amount is non-negative + assert amount >= 0 : "Amount cannot be negative"; + + // Create a flag to check if any savings were edited + boolean isEdited = false; + + for (Saving saving : savings) { + if (saving.getCategory().toLowerCase().equals(lowerCaseCategory)) { + saving.setAmount(amount); + LOGGER.info("Updated saving: " + saving.toString()); + isEdited = true; + } + } + + if (!isEdited) { + LOGGER.warning("No savings found under category: " + category); + System.out.println("No savings found under category: " + category); + } else { + System.out.println("Savings successfully updated for category: " + category); + } + } + + + + + + public double calculateTotalSavings() { + double totalSavings = 0; + try { + for (Saving saving : savings) { + if (saving.getAmount() < 0) { + throw new IllegalArgumentException("Savings should not be negative"); + } + totalSavings += saving.getAmount(); + } + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Negative savings amount detected", e); + } + + assert totalSavings >= 0 : "Total savings should be non-negative"; + + return totalSavings; + } + + /** + * Analyzes and displays insights into the saved amounts across different categories. + * It prints out the highest and lowest savings categories and lists categories with no savings. + * A bar graph representing the distribution of savings is also displayed. + */ + public void getSavingsInsights() { + double totalSavings = calculateTotalSavings(); + if (totalSavings == 0) { + System.out.println("No savings to display."); + return; + } + + Map sumsByCategory = calculateSumsByCategory(); + + // Calculate the highest savings + double highestSavings = Collections.max(sumsByCategory.values()); + // Calculate the lowest savings + double lowestSavings = sumsByCategory.values().stream() + .filter(amount -> amount > 0) + .min(Double::compare) + .orElse(0.0); + + List highestCategories = getSavingsCategoriesByAmount(sumsByCategory, highestSavings); + List lowestCategories = getSavingsCategoriesByAmount(sumsByCategory, lowestSavings); + + // Print the distribution graph + ui.printDivider(); + printSavingsDistribution(sumsByCategory, totalSavings); + ui.printDivider(); + + // Print insights + System.out.println("Highest Savings Category: " + formatCategoryList(highestCategories)); + System.out.println("Lowest Savings Category: " + formatCategoryList(lowestCategories)); + System.out.println("Categories with no savings added: " + + formatCategoryList(getNoSavingsCategories(sumsByCategory))); + ui.printDivider(); + } + + private List getSavingsCategoriesByAmount(Map sumsByCategory, double amount) { + return sumsByCategory.entrySet().stream() + .filter(entry -> entry.getValue() == amount) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + private List getNoSavingsCategories(Map sumsByCategory) { + return categories.stream() + .filter(category -> !sumsByCategory.containsKey(category) || sumsByCategory.get(category) == 0) + .collect(Collectors.toList()); + } + + /** + * Prints a distribution of savings as a horizontal bar graph. + * Each category's bar length is proportional to its percentage of the total savings. + */ + private void printSavingsDistribution(Map sumsByCategory, double totalSavings) { + double maxPercentage = sumsByCategory.values().stream() + .mapToDouble(amount -> (amount / totalSavings) * 100) + .max() + .orElse(100); + + for (String category : categories) { + double percentage = (sumsByCategory.getOrDefault(category, 0.0) / totalSavings) * 100; + int barLength = (int) (percentage / (maxPercentage / 50)); + String bar = "[" + "#".repeat(Math.max(0, barLength)) + "]"; + System.out.println(String.format("%-15s: %6.2f%% %s", category, percentage, bar)); + } + } + + /** + * Calculates the sum of savings for each category. + * + * @return A map with the category as the key and the sum of savings in that category as the value. + */ + private Map calculateSumsByCategory() { + return savings.stream().collect( + Collectors.groupingBy( + Saving::getCategory, + Collectors.summingDouble(Saving::getAmount) + ) + ); + } + + /** + * Formats a list of category names into a human-readable string with categories separated by commas. + * The word "and" is inserted before the last category if there are two or more. + * Returns "None" if the list is empty. + * + * @param categories The list of categories to format. + * @return A formatted string of category names or "None" if the list is empty. + */ + private String formatCategoryList(List categories) { + if (categories.isEmpty()) { + return "None"; + } else if (categories.size() == 1) { + return categories.get(0); + } else { + String allButLast = String.join(", ", categories.subList(0, categories.size() - 1)); + return allButLast + " and " + categories.get(categories.size() - 1); + } + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commons/SplitExpense.java b/src/main/java/seedu/budgetbuddy/commons/SplitExpense.java new file mode 100644 index 0000000000..8dba021d86 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/SplitExpense.java @@ -0,0 +1,61 @@ +package seedu.budgetbuddy.commons; + +import java.time.LocalDate; +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class SplitExpense extends Transaction { + protected String description; + private int numberOfPeople; + private LocalDate dateAdded; + + public SplitExpense(LocalDate dateAdded, double totalAmount, int numberOfPeople, String description) { + super("Shared Bill", calculateAmountPerPerson(totalAmount, numberOfPeople)); + this.dateAdded = dateAdded; + this.numberOfPeople = numberOfPeople; + this.description = description; + } + + public SplitExpense(double totalAmount, int numberOfPeople, String description) { + super("Shared Bill", calculateAmountPerPerson(totalAmount, numberOfPeople)); + this.numberOfPeople = numberOfPeople; + this.description = description; + this.dateAdded = LocalDate.now(); + } + + private static double calculateAmountPerPerson(double totalAmount, int numberOfPeople) { + BigDecimal amount = BigDecimal.valueOf(totalAmount); + return amount.divide(BigDecimal.valueOf(numberOfPeople), 2, RoundingMode.HALF_UP).doubleValue(); + } + + public int getNumberOfPeople() { + return numberOfPeople; + } + + public String getDescription() { + return description; + } + + public LocalDate getDateAdded() { + return dateAdded; + } + + public Boolean isExpenseSettled() { + return false; + } + + public double getAmount() { + return amount; + } + + @Override + public String toString() { + return "Number of People: " + numberOfPeople + " Amount per person: " + amount + " Description: " + + description + " Total Amount: " + getTotalAmount(); + } + + public double getTotalAmount() { + return amount * numberOfPeople; + } + +} diff --git a/src/main/java/seedu/budgetbuddy/commons/SplitExpenseList.java b/src/main/java/seedu/budgetbuddy/commons/SplitExpenseList.java new file mode 100644 index 0000000000..7c6f523280 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/SplitExpenseList.java @@ -0,0 +1,108 @@ +package seedu.budgetbuddy.commons; + + +import java.util.ArrayList; +import java.util.List; + +import seedu.budgetbuddy.Ui; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SplitExpenseList { + private static final Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + protected ArrayList splitexpenses; + + Ui ui = new Ui(); + + public SplitExpenseList(ArrayList splitexpenses){ + this.splitexpenses = splitexpenses; + } + + public SplitExpenseList() { + this.splitexpenses = new ArrayList<>(); + } + + public int getSize() { + return splitexpenses.size(); + } + + public List getSplitExpenses() { + return splitexpenses; + } + + public SplitExpense getSplitExpenseListAtListNumber(int listNumber) { + int listNumberAsArrayPosition = listNumber - 1; + return splitexpenses.get(listNumberAsArrayPosition); + } + + /** + * Lists all the expenses in the list + */ + + public void listSplitExpenses() { + LOGGER.info("Listing splitexpenses..."); + + try { + ui.printDivider(); + System.out.println(String.format("Current Currency: %s\n", DefaultCurrency.getDefaultCurrency())); + + System.out.println("Shared Bills: "); + for (int i = 0; i < splitexpenses.size(); i++) { + SplitExpense splitexpense = splitexpenses.get(i); + + if (splitexpense == null) { + LOGGER.warning("Expense object at index " + i + " is null"); + continue; + } + System.out.print(i+1 + " | "); + System.out.print(" Description: " + splitexpense.getDescription()); + System.out.print(" Number of People: " + splitexpense.getNumberOfPeople()); + System.out.print(" Amount: $" + String.format("%.2f", splitexpense.getAmount()) + "\n"); + } + ui.printDivider(); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "An error occurred while listing expenses.", e); + } + } + + public void addSplitExpense(String amount, String numberOfPeople, String description ) throws BudgetBuddyException { + assert amount != null : "Amount should not be null"; + assert description != null : "Description should not be null"; + LOGGER.info("Adding split expense..."); + + double amountDouble; + int numberOfPeopleInt; + try{ + amountDouble = Double.parseDouble(amount); + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Invalid amount format. Amount should be a number."); + } + + if (amountDouble < 0){ + throw new BudgetBuddyException("Expenses should not be negative."); + } + + try { + numberOfPeopleInt = Integer.parseInt(numberOfPeople); + if (Integer.parseInt(numberOfPeople) < 0) { + throw new BudgetBuddyException("Number of people should be a positive number"); + } + } catch (NumberFormatException e) { + throw new BudgetBuddyException("Number of people should be a number"); + } + + SplitExpense splitexpense = new SplitExpense(amountDouble, numberOfPeopleInt, description); + splitexpenses.add(splitexpense); + } + + public void settleSplitExpenses(int index) { + LOGGER.info("Settling split expenses..."); + assert index >= 0 : "Index should be a positive integer"; + assert index < splitexpenses.size() : "Index should be within the range of the list"; + + splitexpenses.remove(index); + } +} diff --git a/src/main/java/seedu/budgetbuddy/commons/Transaction.java b/src/main/java/seedu/budgetbuddy/commons/Transaction.java new file mode 100644 index 0000000000..9024394e1d --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/commons/Transaction.java @@ -0,0 +1,39 @@ +package seedu.budgetbuddy.commons; + +import java.util.Currency; + +public abstract class Transaction { + String category; + double amount; + Currency currency; + + public Transaction(String category, double amount) { + this.category = category; + this.amount = amount; + this.currency = DefaultCurrency.getDefaultCurrency(); + + } + + public String getCategory() { + return category; + } + public double getAmount() { + return amount; + } + + public void setCategory(String category) { + this.category = category; + } + + public void setAmount(double amount){ + this.amount = amount; + } + + public Currency getCurrency() { + return currency; + } + + public void setCurrency(Currency currency) { + this.currency = currency; + } +} diff --git a/src/main/java/seedu/budgetbuddy/exception/BudgetBuddyException.java b/src/main/java/seedu/budgetbuddy/exception/BudgetBuddyException.java new file mode 100644 index 0000000000..80927516fe --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/exception/BudgetBuddyException.java @@ -0,0 +1,7 @@ +package seedu.budgetbuddy.exception; + +public class BudgetBuddyException extends Exception{ + public BudgetBuddyException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/budgetbuddy/exception/BudgetExceededException.java b/src/main/java/seedu/budgetbuddy/exception/BudgetExceededException.java new file mode 100644 index 0000000000..21fa1b5c50 --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/exception/BudgetExceededException.java @@ -0,0 +1,7 @@ +package seedu.budgetbuddy.exception; + +public class BudgetExceededException extends Exception { + public BudgetExceededException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/budgetbuddy/exception/InvalidRecurringExpensesFileException.java b/src/main/java/seedu/budgetbuddy/exception/InvalidRecurringExpensesFileException.java new file mode 100644 index 0000000000..3c53f5d35e --- /dev/null +++ b/src/main/java/seedu/budgetbuddy/exception/InvalidRecurringExpensesFileException.java @@ -0,0 +1,7 @@ +package seedu.budgetbuddy.exception; + +public class InvalidRecurringExpensesFileException extends Exception{ + public InvalidRecurringExpensesFileException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/test/java/seedu/budgetbuddy/BudgetTest.java b/src/test/java/seedu/budgetbuddy/BudgetTest.java new file mode 100644 index 0000000000..6c80b3938f --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/BudgetTest.java @@ -0,0 +1,52 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ListBudgetCommand; +import seedu.budgetbuddy.command.GetBudgetCommand; +import seedu.budgetbuddy.commandcreator.GetBudgetCommandCreator; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.exception.BudgetBuddyException; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class BudgetTest { + private ExpenseList expenseList; + + @BeforeEach + public void setUp() throws BudgetBuddyException { + expenseList = new ExpenseList(); + expenseList.setBudget("Transport", 300); + expenseList.setBudget("Groceries", 150); + expenseList.addExpense("Transport", "50", "Bus ticket"); + expenseList.addExpense("Transport", "100", "Flight ticket"); + expenseList.addExpense("Groceries", "100", "Groceries"); + } + + @Test + public void listBudgetCommand_allBudgets_success() { + Command command = new ListBudgetCommand(expenseList); + assertInstanceOf(ListBudgetCommand.class, command); + } + + @Test + public void getBudgetCommand_specificCategory_success() { + String category = "Transport"; + GetBudgetCommandCreator creator = new GetBudgetCommandCreator(expenseList, "get budget c/" + category); + Command command = creator.createCommand(); + + assertInstanceOf(GetBudgetCommand.class, command); + // Execute command and verify the output + command.execute(); + } + + @Test + public void getBudgetCommand_specificCategory_withExpensesListed() { + String category = "Transport"; + GetBudgetCommandCreator creator = new GetBudgetCommandCreator(expenseList, "get budget c/" + category); + GetBudgetCommand command = (GetBudgetCommand) creator.createCommand(); + + // Execute command + command.execute(); + } +} diff --git a/src/test/java/seedu/budgetbuddy/ChangeCurrencyCommandCreatorTest.java b/src/test/java/seedu/budgetbuddy/ChangeCurrencyCommandCreatorTest.java new file mode 100644 index 0000000000..dbafe8cbea --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/ChangeCurrencyCommandCreatorTest.java @@ -0,0 +1,79 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.ChangeCurrencyCommand; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.commandcreator.ChangeCurrencyCommandCreator; +import seedu.budgetbuddy.commons.CurrencyConverter; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons. SplitExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +//@@author sweijie24 +public class ChangeCurrencyCommandCreatorTest { + + + @Test + public void handleChangeCurrencyCommand_changeCurrencyToUSD_success() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + ExpenseList expenseList = new ExpenseList(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + CurrencyConverter currencyConverter = new CurrencyConverter(); + + savingList.addSaving("Salary", "1000"); + + String input = "change currency USD"; + + ChangeCurrencyCommandCreator changeCurrencyCommandCreator = new ChangeCurrencyCommandCreator(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + + Command command = changeCurrencyCommandCreator.handleChangeCurrencyCommand(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + + assertEquals(ChangeCurrencyCommand.class, command.getClass()); + } + + @Test + public void handleChangeCurrencyCommand_changeCurrency_invalidCurrencyCode() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + ExpenseList expenseList = new ExpenseList(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + CurrencyConverter currencyConverter = new CurrencyConverter(); + + savingList.addSaving("Salary", "1000"); + + String input = "change currency abc"; + ChangeCurrencyCommandCreator changeCurrencyCommandCreator = new ChangeCurrencyCommandCreator(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + + Command command = changeCurrencyCommandCreator.handleChangeCurrencyCommand(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + assertNull(command); + } + + @Test + public void handleChangeCurrencyCommand_changeCurrency_invalidCommandFormat() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + ExpenseList expenseList = new ExpenseList(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + CurrencyConverter currencyConverter = new CurrencyConverter(); + + savingList.addSaving("Salary", "1000"); + + String input = "change currency abc asd"; + ChangeCurrencyCommandCreator changeCurrencyCommandCreator = new ChangeCurrencyCommandCreator(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + + Command command = changeCurrencyCommandCreator.handleChangeCurrencyCommand(input, savingList, + expenseList, splitExpenseList, recurringExpenseLists, currencyConverter); + assertNull(command); + } +} diff --git a/src/test/java/seedu/budgetbuddy/CurrencyConverterTest.java b/src/test/java/seedu/budgetbuddy/CurrencyConverterTest.java new file mode 100644 index 0000000000..d9c9564c24 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/CurrencyConverterTest.java @@ -0,0 +1,75 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.commons.CurrencyConverter; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.Saving; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import java.util.Currency; +import static org.junit.jupiter.api.Assertions.assertEquals; + +//@@author sweijie24 +public class CurrencyConverterTest { + + @Test + public void convertAmount_convertSameDefaultCurrency_success() { + CurrencyConverter converter = new CurrencyConverter(); + double amount = 100.0; + Currency currency = Currency.getInstance("SGD"); + double convertedAmount = converter.convertAmount(amount, currency, currency); + assertEquals(amount, convertedAmount); + } + + @Test + public void convertAmount_convertDifferentCurrency_success() { + CurrencyConverter converter = new CurrencyConverter(); + double amount = 100.0; + Currency currency = Currency.getInstance("SGD"); + Currency newCurrency = Currency.getInstance("USD"); + double convertedAmount = converter.convertAmount(amount, currency, newCurrency); + assertEquals(75.0, convertedAmount); + } + + @Test + public void convertAmount_convertDifferentCurrencies_success() { + CurrencyConverter converter = new CurrencyConverter(); + double amount = 100.0; + Currency currency = Currency.getInstance("USD"); + Currency newCurrency = Currency.getInstance("JPY"); + double convertedAmount = converter.convertAmount(amount, currency, newCurrency); + assertEquals("14966.67", String.format("%.2f", convertedAmount)); + } + + @Test + public void convertSavingCurrency_convertCurrenciesInSavingList_success() throws BudgetBuddyException { + CurrencyConverter converter = new CurrencyConverter(); + SavingList savings = new SavingList(); + savings.addSaving("Salary", "1000"); + savings.addSaving("Investments", "200"); + Currency newCurrency = Currency.getInstance("USD"); + + converter.convertSavingCurrency(newCurrency, savings); + + for (Saving saving : savings.getSavings()) { + assertEquals(newCurrency, saving.getCurrency()); + } + } + + @Test + public void convertExpenseCurrency_convertCurrenciesInExpenseList_success() throws BudgetBuddyException { + CurrencyConverter converter = new CurrencyConverter(); + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Transport", "1000", "MRT"); + expenses.addExpense("Housing", "200", "BTO"); + Currency newCurrency = Currency.getInstance("USD"); + + converter.convertExpenseCurrency(newCurrency, expenses); + + for (Expense expense : expenses.getExpenses()) { + assertEquals(newCurrency, expense.getCurrency()); + } + } +} diff --git a/src/test/java/seedu/budgetbuddy/ExpenseListTest.java b/src/test/java/seedu/budgetbuddy/ExpenseListTest.java new file mode 100644 index 0000000000..2fab3d07f5 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/ExpenseListTest.java @@ -0,0 +1,271 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.exception.BudgetBuddyException; +import org.junit.jupiter.api.Disabled; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +import java.util.ArrayList; + + +//@@author sweijie24 +public class ExpenseListTest { + + private static final Logger LOGGER = Logger.getLogger(ExpenseListTest.class.getName()); + + //@@author yyangdaa + @Test + public void calculateTotalExpenses_addingIntegers_success() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "30", "BTO"); + + assertEquals(80, expenseList.calculateTotalExpenses()); + } + + //@@author yyangdaa + @Test + public void addExpense_addingExpense_success() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + + assertEquals(1, expenseList.getExpenses().size()); + assertEquals("Transport", expenseList.getExpenses().get(0).getCategory()); + assertEquals(50.0, expenseList.getExpenses().get(0).getAmount(), 0.01); + assertEquals("Bus Fare", expenseList.getExpenses().get(0).getDescription()); + } + + //@@author yyangdaa + @Test + public void addExpense_addingNegativeExpense_exceptionThrown() { + ExpenseList expenseList = new ExpenseList(); + try { + expenseList.addExpense("Transport", "-50", "Bus Fare"); + fail(); + } catch (Exception e) { + assertEquals("Invalid amount format. Amount should be a positive number with up to maximum two decimal " + + "places.", e.getMessage()); + } + } + + //@@author yyangdaa + @Test + public void addExpense_addingInvalidAmount_exceptionThrown() { + ExpenseList expenseList = new ExpenseList(); + try { + expenseList.addExpense("Transport", "abc", "Bus Fare"); + fail(); + } catch (Exception e) { + assertEquals("Invalid amount format. Amount should be a positive number with up to maximum two decimal " + + "places.", e.getMessage()); + } + } + + //@@author yyangdaa + @Test + public void addExpense_addingNullCategory_exceptionThrown() { + ExpenseList expenseList = new ExpenseList(); + try { + expenseList.addExpense("abc", "50", "Bus Fare"); + fail(); + } catch (Exception e) { + assertEquals("The category 'abc' is not listed.", e.getMessage()); + } + } + + //@@author yyangdaa + @Test + public void deleteExpense_validInput_success() throws BudgetBuddyException { + // Create an ExpenseList and add two expenses + ExpenseList expenseList = new ExpenseList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "30", "Lunch"); + + // Delete the first expense + expenseList.deleteExpense(1); + + // Assert: Check if the first expense is deleted + assertEquals(1, expenseList.getExpenses().size()); + assertEquals("Transport", expenseList.getExpenses().get(0).getCategory()); + assertEquals(50, expenseList.getExpenses().get(0).getAmount(), 0.01); // using delta for double comparison + assertEquals("Bus Fare", expenseList.getExpenses().get(0).getDescription()); + } + + + //@@ jasraa + @Test + public void editExpense_validInput_success() throws BudgetBuddyException { + //Create an ExpenseList and add two expenses + ExpenseList expenseList = new ExpenseList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "30", "Lunch"); + + //Edit the first expense + expenseList.editExpense("Transport", 1, 70.0, "Updated Bus Fare"); + + // Assert: Check if the edited expense details are correct + assertEquals(2, expenseList.getExpenses().size()); + assertEquals("Transport", expenseList.getExpenses().get(0).getCategory()); + assertEquals(70.0, expenseList.getExpenses().get(0).getAmount(), 0.01); // using delta for double comparison + assertEquals("Updated Bus Fare", expenseList.getExpenses().get(0).getDescription()); + } + + @Test + public void addSaving_addingSaving_success() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "1000"); + assertEquals(1, savingList.getSavings().size()); + } + + //@@ jasraa + @Test @Disabled + public void editExpense_invalidCategoryOrIndex_failure() throws BudgetBuddyException { + // Create an ExpenseList and add two expenses + ExpenseList expenseList = new ExpenseList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "30", "Lunch"); + + // Edit an expense with an invalid category + expenseList.editExpense("InvalidCategory", 1, 70.0, "Updated Bus Fare"); + + // Assert: Check if the expense list remains unchanged + assertEquals(2, expenseList.getExpenses().size()); // Should not change size + // Check if the expense details remain unchanged + assertEquals("Transport", expenseList.getExpenses().get(0).getCategory()); + assertEquals(50.0, expenseList.getExpenses().get(0).getAmount(), 0.01); + assertEquals("Bus Fare", expenseList.getExpenses().get(0).getDescription()); + } + + @Test + public void testDeleteExpense_indexOutOfBounds() { + ExpenseList expenseList = new ExpenseList(); + + int initialSize = expenseList.getExpenses().size(); + expenseList.deleteExpense(initialSize + 1); // Trying to delete with index out of bounds + assertEquals(initialSize, expenseList.getExpenses().size()); // Size should remain the same + } + + @Test + public void filterExpenses_filterByDescription_returnsTwoMatches() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "100", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("apple" + , null, null); + + assertEquals(2, filteredExpenses.size()); + + } + + @Test + public void filterExpenses_filterByMinAmount_returnsThreeMatches() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "100", "Apples"); + expenses.addExpense("Transport", "40", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("" + , 50.00, null); + + assertEquals(3, filteredExpenses.size()); + + } + + @Test + public void filterExpenses_filterByMaxAmount_returnsOneMatches() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "100", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("" + , null, 76.00); + + assertEquals(2, filteredExpenses.size()); + + } + + @Test + public void filterExpenses_filterByRange_returnsTwoMatches() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "100", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("" + , 49.00, 75.00); + + assertEquals(2, filteredExpenses.size()); + + } + + @Test + public void filterExpenses_filterByDescriptionAndMinAmount_returnsOneMatch() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "20", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("apple" + , 50.00, null); + + assertEquals(1, filteredExpenses.size()); + + } + + @Test + public void filterExpenses_filterByNull_returnsAllMatch() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "20", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + ArrayList filteredExpenses = expenses.filterExpenses("" + , null, null); + + assertEquals(4, filteredExpenses.size()); + + } + + @Test + public void testGetExpenseInsights() { + // Set up the ExpenseList with sample expenses + ExpenseList expenseList = new ExpenseList(); + try { + expenseList.addExpense("Transport", "50", "Bus fare"); + expenseList.addExpense("Groceries", "30", "Weekly groceries"); + expenseList.addExpense("Entertainment", "20", "Movie ticket"); + } catch (BudgetBuddyException e) { + LOGGER.log(Level.SEVERE, "Exception occurred while adding an expense", e); + } + + // Redirect standard output to capture the output of getExpenseInsights + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + // Invoke the method + expenseList.getExpenseInsights(); + + // Capture and assert the output + String output = outContent.toString(); + assertTrue(output.contains("Highest Expense Category:")); + assertTrue(output.contains("Lowest Expense Category:")); + assertTrue(output.contains("Categories with no expenses added:")); + + // Restore the original standard output + System.setOut(originalOut); + } +} diff --git a/src/test/java/seedu/budgetbuddy/FindExpensesCommandCreatorTest.java b/src/test/java/seedu/budgetbuddy/FindExpensesCommandCreatorTest.java new file mode 100644 index 0000000000..4fb77aafa6 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/FindExpensesCommandCreatorTest.java @@ -0,0 +1,123 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.FindExpensesCommand; +import seedu.budgetbuddy.commandcreator.CommandCreator; +import seedu.budgetbuddy.commandcreator.FindExpensesCommandCreator; +import seedu.budgetbuddy.commons.ExpenseList; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class FindExpensesCommandCreatorTest { + private FindExpensesCommandCreator initializeFindExpensesCommandCreator(String input) { + ExpenseList expenses = new ExpenseList(); + return new FindExpensesCommandCreator(input, expenses); + } + + @Test + public void createCommand_invalidParametersGiven_returnsNull(){ + String inputWithoutDescription = "find expenses morethan/ lessthan/"; + String inputWithoutMax = "find expenses d/ morethan/"; + String inputWithoutMin = "find expenses d/ lessthan/"; + String inputWithoutMinAndMax = "find expenses d/Hello"; + String inputWithoutDescriptionAndMax = "find expenses morethan/test"; + String inputWithoutDescriptionAndMin = "find expenses lessthan/test"; + String inputWithoutParameters = "find expenses "; + + assertNull(initializeFindExpensesCommandCreator(inputWithoutMax).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutDescription).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutMin).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutMinAndMax).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutDescriptionAndMax).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutDescriptionAndMin).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithoutParameters).createCommand()); + } + + @Test + public void createCommand_duplicateParameters_returnsNull() { + String inputWithDuplicateDescription = "find expenses d/23 morethan/200 lessthan/400 d/50"; + String inputWithDuplicateMinAmount = "find expenses d/23 morethan/200 lessthan/400 morethan/20"; + String inputWithDuplicateMaxAmount = "find expenses d/23 morethan/200 lessthan/400 lessthan/20"; + + assertNull(initializeFindExpensesCommandCreator(inputWithDuplicateDescription).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithDuplicateMinAmount).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithDuplicateMaxAmount).createCommand()); + } + + @Test + public void createCommand_outOfOrderParameters_returnsNull() { + String inputWithOutOfOrderAmountParameters = "find expenses d/23 lessthan/40 morethan/"; + String inputWithOutOfOrderDescriptionParameter = "find expenses morethan/ d/23 lessthan/40"; + assertNull(initializeFindExpensesCommandCreator(inputWithOutOfOrderAmountParameters).createCommand()); + assertNull(initializeFindExpensesCommandCreator(inputWithOutOfOrderDescriptionParameter).createCommand()); + } + @Test + public void createCommand_invalidMinAmount_returnsNull() { + String validInputWithEmptyDescription = "find expenses d/hello morethan/dsfefew lessthan/20"; + + Command command = initializeFindExpensesCommandCreator(validInputWithEmptyDescription).createCommand(); + assertNull(command); + } + + @Test + public void createCommand_invalidMaxAmount_returnsNull() { + String validInputWithEmptyDescription = "find expenses d/hello morethan/20 lessthan/sdsdasdasd"; + + Command command = initializeFindExpensesCommandCreator(validInputWithEmptyDescription).createCommand(); + assertNull(command); + } + @Test + public void createCommand_invalidMaxAndMinValues_returnsNull() { + + String input = "find expenses d/Bruno Mars morethan/400 lessthan/300"; + CommandCreator commandCreator = initializeFindExpensesCommandCreator(input); + Command command = commandCreator.createCommand(); + assertNull(command); + + } + + @Test + public void createCommand_maxAndMinValuesAsLetters_returnsNull() { + ExpenseList expenses = new ExpenseList(); + + String input = "find expenses d/Bruno Mars morethan/hello lessthan/hello"; + CommandCreator commandCreator = new FindExpensesCommandCreator(input, expenses); + Command command = commandCreator.createCommand(); + assertNull(command); + } + + @Test + public void createCommand_emptyDescription_returnsFindExpensesCommand() { + String validInputWithEmptyDescription = "find expenses d/ morethan/200 lessthan/400"; + + Command command = initializeFindExpensesCommandCreator(validInputWithEmptyDescription).createCommand(); + assertInstanceOf(FindExpensesCommand.class, command); + } + + @Test + public void createCommand_emptyMinAmount_returnsFindExpensesCommand() { + String validInputWithEmptyDescription = "find expenses d/hello morethan/ lessthan/200"; + + Command command = initializeFindExpensesCommandCreator(validInputWithEmptyDescription).createCommand(); + assertInstanceOf(FindExpensesCommand.class, command); + } + + @Test + public void createCommand_emptyMaxAmount_returnsFindExpensesCommand() { + String validInputWithEmptyDescription = "find expenses d/hello morethan/200 lessthan/"; + + Command command = initializeFindExpensesCommandCreator(validInputWithEmptyDescription).createCommand(); + assertInstanceOf(FindExpensesCommand.class, command); + } + + @Test + public void createCommand_validInput_returnsFindExpenseCommand() { + String input = "find expenses d/Bruno Mars morethan/400 lessthan/500"; + CommandCreator commandCreator = initializeFindExpensesCommandCreator(input); + Command command = commandCreator.createCommand(); + assertInstanceOf(FindExpensesCommand.class, command); + + } +} diff --git a/src/test/java/seedu/budgetbuddy/FindExpensesCommandTest.java b/src/test/java/seedu/budgetbuddy/FindExpensesCommandTest.java new file mode 100644 index 0000000000..44f5ed1ef3 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/FindExpensesCommandTest.java @@ -0,0 +1,50 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.FindExpensesCommand; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FindExpensesCommandTest { + + @Test + public void findExpensesCommand_createWithNullDescription_setsDescriptionToEmptyString() { + ExpenseList expenses = new ExpenseList(); + FindExpensesCommand findExpensesCommand = new FindExpensesCommand(expenses, null + , null, null); + + String description = findExpensesCommand.getDescription(); + + assertEquals("", description); + } + + @Test + public void execute_validInput_printsMatchesAndNoExceptionsThrown() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "20", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + + FindExpensesCommand findExpensesCommand = new FindExpensesCommand(expenses + , "apple", null, null); + + findExpensesCommand.execute(); + } + + @Test + public void execute_validInput_printsNoMatchesAndNoExceptionsThrown() throws BudgetBuddyException { + ExpenseList expenses = new ExpenseList(); + expenses.addExpense("Groceries", "20", "Apples"); + expenses.addExpense("Transport", "50", "Bus fare"); + expenses.addExpense("Entertainment", "75", "Movie"); + expenses.addExpense("Groceries", "100", "apple"); + + FindExpensesCommand findExpensesCommand = new FindExpensesCommand(expenses + , "chicken", null, null); + + findExpensesCommand.execute(); + } +} diff --git a/src/test/java/seedu/budgetbuddy/ListCommandCreatorTest.java b/src/test/java/seedu/budgetbuddy/ListCommandCreatorTest.java new file mode 100644 index 0000000000..d633c3c9b3 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/ListCommandCreatorTest.java @@ -0,0 +1,98 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.ListExpenseCommand; +import seedu.budgetbuddy.command.ListSavingsCommand; +import seedu.budgetbuddy.commandcreator.ListCommandCreator; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +//@@author sweijie24 +public class ListCommandCreatorTest { + + @Test + public void handleListCommand_listExpenses_success() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + SavingList savingList = new SavingList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "3000", "BTO"); + + String input = "list expenses"; + + ListCommandCreator listCommandCreator = new ListCommandCreator(expenseList, savingList, input); + + Command command = listCommandCreator.handleListCommand(input, expenseList, savingList); + + assertEquals(ListExpenseCommand.class, command.getClass()); + } + + @Test + public void handleListCommand_listExpensesWithCategory_invalidCategory() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + SavingList savingList = new SavingList(); + expenseList.addExpense("Transport", "50", "Bus Fare"); + expenseList.addExpense("Housing", "3000", "BTO"); + + String input = "list expenses qweqwe"; + + ListCommandCreator listCommandCreator = new ListCommandCreator(expenseList, savingList, input); + + Command command = listCommandCreator.handleListCommand(input, expenseList, savingList); + + assertNull(command); + } + + @Test + public void handleListCommand_listSavings_success() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "1150"); + savingList.addSaving("Investments", "300"); + + String input = "list savings"; + + ListCommandCreator listCommandCreator = new ListCommandCreator(expenseList, savingList, input); + + Command command = listCommandCreator.handleListCommand(input, expenseList, savingList); + + assertEquals(ListSavingsCommand.class, command.getClass()); + } + + @Test + public void handleListCommand_listSavingsWithCategory_success() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "1150"); + savingList.addSaving("Investments", "300"); + + String input = "list savings salary"; + + ListCommandCreator listCommandCreator = new ListCommandCreator(expenseList, savingList, input); + + Command command = listCommandCreator.handleListCommand(input, expenseList, savingList); + + assertEquals(ListSavingsCommand.class, command.getClass()); + } + + @Test + public void handleListCommand_listSavingsWithCategory_invalidCategory() throws BudgetBuddyException { + ExpenseList expenseList = new ExpenseList(); + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "1150"); + savingList.addSaving("Investments", "300"); + + String input = "list savings qweqwe"; + + ListCommandCreator listCommandCreator = new ListCommandCreator(expenseList, savingList, input); + + Command command = listCommandCreator.handleListCommand(input, expenseList, savingList); + + assertNull(command); + } + +} diff --git a/src/test/java/seedu/budgetbuddy/MenuCommandCreatorTest.java b/src/test/java/seedu/budgetbuddy/MenuCommandCreatorTest.java new file mode 100644 index 0000000000..ea521005b6 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/MenuCommandCreatorTest.java @@ -0,0 +1,33 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.MenuCommand; +import seedu.budgetbuddy.commandcreator.CommandCreator; +import seedu.budgetbuddy.commandcreator.MenuCommandCreator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class MenuCommandCreatorTest { + @Test + @Disabled + public void createMenuCommand_menuCommandWithNoIndex_success() { + CommandCreator commandCreator = new MenuCommandCreator("menu"); + Command command = commandCreator.createCommand(); + + assertInstanceOf(MenuCommand.class, command); + assertEquals(0,((MenuCommand)command).getIndex()); + } + + @Test + public void createMenuCommand_menuCommandWithValidIndex_success() { + CommandCreator commandCreator = new MenuCommandCreator("menu 1"); + Command command = commandCreator.createCommand(); + + assertInstanceOf(MenuCommand.class, command); + assertEquals(1,((MenuCommand)command).getIndex()); + } + +} diff --git a/src/test/java/seedu/budgetbuddy/MenuCommandTest.java b/src/test/java/seedu/budgetbuddy/MenuCommandTest.java new file mode 100644 index 0000000000..6377d81af3 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/MenuCommandTest.java @@ -0,0 +1,20 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.MenuCommand; + +public class MenuCommandTest { + + @Test + public void execute_validMenuCommandWithInput0_printsAndNoExceptions() { + MenuCommand menuCommand = new MenuCommand(0); + + menuCommand.execute(); + } + + @Test + public void execute_validMenuCommandWithInput1_printsAndNoExceptions() { + MenuCommand menuCommand = new MenuCommand(1); + menuCommand.execute(); + } +} diff --git a/src/test/java/seedu/budgetbuddy/ParserTest.java b/src/test/java/seedu/budgetbuddy/ParserTest.java new file mode 100644 index 0000000000..0fafdd56c4 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/ParserTest.java @@ -0,0 +1,80 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.FindExpensesCommand; +import seedu.budgetbuddy.command.MenuCommand; +import seedu.budgetbuddy.command.RecurringExpenseCommand; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.commons.SplitExpenseList; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + + + +public class ParserTest { + + + @Test + public void parseCommand_invalidCommand_returnsNull() { + Parser parser = new Parser(); + ExpenseList expenses = new ExpenseList(); + SavingList savings = new SavingList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + Command invalidCommand = parser.parseCommand(expenses, savings, splitExpenseList, + expensesList, "NotaCommand"); + + assertNull(invalidCommand); + } + + @Test + public void parseCommand_menuCommand_returnsMenuCommand() { + Parser parser = new Parser(); + ExpenseList expenses = new ExpenseList(); + SavingList savings = new SavingList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + + String input = "menu 1"; + + Command command = parser.parseCommand(expenses, savings, splitExpenseList, expensesList, input); + + assertInstanceOf(MenuCommand.class, command); + } + + @Test + public void parseCommand_findExpensesCommand_returnsFindExpensesCommand() { + Parser parser = new Parser(); + ExpenseList expenses = new ExpenseList(); + SavingList savings = new SavingList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + + String input = "find expenses d/hello morethan/200 lessthan/300"; + + Command command = parser.parseCommand(expenses, savings, splitExpenseList, expensesList, input); + + assertInstanceOf(FindExpensesCommand.class, command); + } + + @Test + public void parseCommand_recCommand_returnsRecurringExpenseCommand() { + Parser parser = new Parser(); + ExpenseList expenses = new ExpenseList(); + SavingList savings = new SavingList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + SplitExpenseList splitExpenseList = new SplitExpenseList(); + + String input = "rec newlist listname"; + + Command command = parser.parseCommand(expenses, savings, splitExpenseList, expensesList, input); + + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + +} diff --git a/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandCreatorTest.java b/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandCreatorTest.java new file mode 100644 index 0000000000..08e5e7aeac --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandCreatorTest.java @@ -0,0 +1,289 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.Command; +import seedu.budgetbuddy.command.RecurringExpenseCommand; +import seedu.budgetbuddy.commandcreator.CommandCreator; +import seedu.budgetbuddy.commandcreator.RecurringExpenseCommandCreator; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class RecurringExpenseCommandCreatorTest { + + @Test + public void handleRecCommand_invalidCommandType_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec invalid Entertainment"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + @Test + public void handleRecCommand_newListCommandWithValidInput_createsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newlist Entertainment"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_newListCommandWithInvalidInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newlist"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_removeListCommandWithValidInput_createsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec removelist 1"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_removeListCommandWithInvalidInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec removelist string"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_removeListCommandWithEmptyInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec removelist"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_viewListsCommand_returnsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec viewlists"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_addRecCommandWithEmptyInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec addrec"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_addRecCommandWithValidInput_createsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec addrec 1"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_addRecCommandWithInvalidInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec addrec sdefwre"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_viewExpensesCommandWithEmptyInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec viewexpenses"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_viewExpensesCommandWithValidInput_createsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec viewexpenses 1"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_viewExpensesCommandWithInvalidInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec viewexpenses fdgder"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + + @Test + public void handleRecCommand_newExpenseCommandWithValidInput_createsRecurringExpenseCommand() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec newexpense to/1 c/Entertainment a/100 d/Movies"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNotNull(command); + assertInstanceOf(RecurringExpenseCommand.class, command); + } + + @Test + public void handleRecCommand_newExpenseCommandWithInvalidAmount_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + expensesList.addNewRecurringList("Entertainment"); + String input = "rec newexpense to/1 c/Entertainment a/sdsdfsdf d/Movies"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithInvalidInput_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newexpense to/ "; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithEmptyCategory_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newexpense to/1 c/ a/200 d/ description"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithEmptyAmount_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newexpense to/1 c/Entertainment a/ d/ description"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithEmptyDescription_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newexpense to/1 c/Entertainment a/200 d/"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithEmptyListNumber_returnsNull() { + ExpenseList expenseList = new ExpenseList(); + RecurringExpenseLists expensesList = new RecurringExpenseLists(); + String input = "rec newexpense to/ c/Entertainment a/200 d/"; + + CommandCreator commandCreator = new RecurringExpenseCommandCreator(input, expensesList, expenseList); + Command command = commandCreator.createCommand(); + + assertNull(command); + } + + @Test + public void handleRecCommand_newExpenseToListCommandWithInvalidCategory_returnsNull() { + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + ExpenseList expenseList = new ExpenseList(); + recurringExpenseLists.addNewRecurringList("list1"); + + String userInput = "rec newexpense to/1 c/invalid a/500 d/description"; + + RecurringExpenseCommandCreator recurringExpenseCommandCreator = new RecurringExpenseCommandCreator(userInput + , recurringExpenseLists,expenseList); + + Command newExpenseCommand = recurringExpenseCommandCreator.createCommand(); + + assertNull(newExpenseCommand); + + } + +} diff --git a/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandTest.java b/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandTest.java new file mode 100644 index 0000000000..9feb91a9bd --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/RecurringExpenseCommandTest.java @@ -0,0 +1,176 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.command.RecurringExpenseCommand; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; +import seedu.budgetbuddy.exception.BudgetBuddyException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecurringExpenseCommandTest { + + @Test + public void execute_addRecCommand_addsTwoExpensesToExpenses() throws BudgetBuddyException { + ExpenseList overallExpenses = new ExpenseList(); + overallExpenses.addExpense("Entertainment", "200", "first"); + overallExpenses.addExpense("Entertainment", "200", "second"); + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("list1"); + + RecurringExpenseCommand add = new RecurringExpenseCommand(1, recurringExpenseLists + ,"Entertainment", 200.00,"description", "newexpense" ); + + RecurringExpenseCommand secondAdd = new RecurringExpenseCommand(1, recurringExpenseLists + ,"Entertainment", 500.00,"description", "newexpense" ); + + add.execute(); + secondAdd.execute(); + + RecurringExpenseCommand command = new RecurringExpenseCommand(1, recurringExpenseLists + , overallExpenses,"addrec"); + + command.execute(); + + assertEquals(4, overallExpenses.getExpenses().size()); + + + } + + @Test + public void execute_addRecCommandwithOutOfBoundsIndex_overallExpensesSizeUnchanged() throws BudgetBuddyException { + ExpenseList overallExpenses = new ExpenseList(); + overallExpenses.addExpense("Entertainment", "200", "first"); + overallExpenses.addExpense("Entertainment", "200", "second"); + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("list1"); + + RecurringExpenseCommand add = new RecurringExpenseCommand(1, recurringExpenseLists + ,"Entertainment", 200.00,"description", "newexpense" ); + + RecurringExpenseCommand secondAdd = new RecurringExpenseCommand(1, recurringExpenseLists + ,"Entertainment", 500.00,"description", "newexpense" ); + + add.execute(); + secondAdd.execute(); + + RecurringExpenseCommand command = new RecurringExpenseCommand(2, recurringExpenseLists + , overallExpenses,"addrec"); + + command.execute(); + + assertEquals(2, overallExpenses.getExpenses().size()); + + } + + @Test + public void execute_viewExpensesWithOutOfBoundsIndex_printsErrorMessageNoExceptionThrown() { + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("list1"); + + + RecurringExpenseCommand command = new RecurringExpenseCommand(2, recurringExpenseLists + , "viewexpenses"); + + command.execute(); + } + + @Test + public void execute_viewExpensesWithValidIndex_printsOutputWithNoExceptionThrown() { + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("list1"); + + + RecurringExpenseCommand command = new RecurringExpenseCommand(1, recurringExpenseLists + , "viewexpenses"); + + command.execute(); + } + @Test + public void execute_viewEmptyListOfRecurringExpensesList_printsListWithoutExceptions() { + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + RecurringExpenseCommand recurringExpenseCommand = new RecurringExpenseCommand(recurringExpenseLists + , "viewlists"); + + recurringExpenseCommand.execute(); + } + + @Test + public void execute_viewNonEmptyListOfRecurringExpensesList_printsListWithoutExceptions() { + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("listOne"); + recurringExpenseLists.addNewRecurringList("listTwo"); + recurringExpenseLists.addNewRecurringList("listThree"); + + RecurringExpenseCommand recurringExpenseCommand = new RecurringExpenseCommand(recurringExpenseLists + , "viewlists"); + + recurringExpenseCommand.execute(); + } + + @Test + public void execute_addNewList_sizeOfOverallListIncreasedByOne() { + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("listOne"); + recurringExpenseLists.addNewRecurringList("listTwo"); + recurringExpenseLists.addNewRecurringList("listThree"); + + RecurringExpenseCommand recurringExpenseCommand = new RecurringExpenseCommand("listFour" + , recurringExpenseLists, "newlist"); + + recurringExpenseCommand.execute(); + + assertEquals(4, recurringExpenseLists.getSize()); + + } + @Test + public void execute_removeList_sizeOfOverallListReducedByOne(){ + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("listOne"); + recurringExpenseLists.addNewRecurringList("listTwo"); + recurringExpenseLists.addNewRecurringList("listThree"); + + RecurringExpenseCommand recurringExpenseCommand = new RecurringExpenseCommand(2, + recurringExpenseLists, "removelist" ); + + recurringExpenseCommand.execute(); + + assertEquals(2, recurringExpenseLists.getSize()); + } + + @Test + public void execute_removeListWithOutOfBoundsListNumber_sizeOfOverallListStaysSame(){ + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("listOne"); + recurringExpenseLists.addNewRecurringList("listTwo"); + recurringExpenseLists.addNewRecurringList("listThree"); + + RecurringExpenseCommand recurringExpenseCommand = new RecurringExpenseCommand(4, + recurringExpenseLists, "removelist" ); + + recurringExpenseCommand.execute(); + + assertEquals(3, recurringExpenseLists.getSize()); + } + + + @Test + public void execute_newExpenseCommand_sizeOfRecurringExpenseListIncreaseByOne() { + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + recurringExpenseLists.addNewRecurringList("list1"); + + RecurringExpenseCommand newExpenseCommand = new RecurringExpenseCommand(1, recurringExpenseLists + ,"Entertainment", 500.00,"description", "newexpense" ); + + + newExpenseCommand.execute(); + + assertEquals(1, recurringExpenseLists.getExpenseListAtListNumber(1).getExpenses().size()); + + } +} diff --git a/src/test/java/seedu/budgetbuddy/RecurringExpenseListTest.java b/src/test/java/seedu/budgetbuddy/RecurringExpenseListTest.java new file mode 100644 index 0000000000..1503833350 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/RecurringExpenseListTest.java @@ -0,0 +1,20 @@ +package seedu.budgetbuddy; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.commons.Expense; +import seedu.budgetbuddy.commons.RecurringExpenseList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecurringExpenseListTest { + + @Test + public void getName_requestForName_returnsValidName() { + ArrayList expenses = new ArrayList<>(); + RecurringExpenseList recurringExpenseList = new RecurringExpenseList("Bruno", expenses); + + assertEquals("Bruno", recurringExpenseList.getName()); + } +} diff --git a/src/test/java/seedu/budgetbuddy/RecurringExpenseListsTest.java b/src/test/java/seedu/budgetbuddy/RecurringExpenseListsTest.java new file mode 100644 index 0000000000..97fca4aa8c --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/RecurringExpenseListsTest.java @@ -0,0 +1,67 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.commons.ExpenseList; +import seedu.budgetbuddy.commons.RecurringExpenseLists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class RecurringExpenseListsTest { + + RecurringExpenseLists recurringExpenseLists = new RecurringExpenseLists(); + + @Test + void addNewRecurringList_addValidNewList_success() { + recurringExpenseLists.addNewRecurringList("Entertainment"); + assertEquals(1, recurringExpenseLists.getSize()); + } + + @Test + void removeList_removeValidListNumber_success() { + recurringExpenseLists.addNewRecurringList("Entertainment"); + recurringExpenseLists.addNewRecurringList("Housing"); + recurringExpenseLists.addNewRecurringList("Utilities"); + recurringExpenseLists.removeList(2); + + assertEquals(2, recurringExpenseLists.getSize()); + } + + @Test + void getSize_addThreeLists_sizeReturnedCorrect() { + recurringExpenseLists.addNewRecurringList("Entertainment"); + recurringExpenseLists.addNewRecurringList("Housing"); + recurringExpenseLists.addNewRecurringList("Utilities"); + + int expectedSize = 3; + + int obtainedSize = recurringExpenseLists.getSize(); + + assertEquals(expectedSize, obtainedSize); + } + + @Test + void getExpenseListAtListNumber_validListNumber_returnsCorrectList() { + recurringExpenseLists.addNewRecurringList("Entertainment"); + recurringExpenseLists.addNewRecurringList("Utilities"); + recurringExpenseLists.addNewRecurringList("Housing"); + ExpenseList obtainedList = recurringExpenseLists.getExpenseListAtListNumber(2); + + + assertNotNull(obtainedList); + assertEquals("Utilities", obtainedList.getName()); + } + + @Test + void printAllRecurringLists_nonEmptyList_noExceptionsThrown() { + recurringExpenseLists.addNewRecurringList("Entertainment"); + recurringExpenseLists.addNewRecurringList("Utilities"); + + recurringExpenseLists.printAllRecurringLists(); + } + + @Test + void printAllRecurringLists_emptyList_noExceptionsThrown() { + recurringExpenseLists.printAllRecurringLists(); + } +} diff --git a/src/test/java/seedu/budgetbuddy/SavingListTest.java b/src/test/java/seedu/budgetbuddy/SavingListTest.java new file mode 100644 index 0000000000..f09a663718 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/SavingListTest.java @@ -0,0 +1,186 @@ +package seedu.budgetbuddy; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import seedu.budgetbuddy.commons.SavingList; +import seedu.budgetbuddy.exception.BudgetBuddyException; +import java.util.logging.Level; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.logging.Logger; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +//@@author sweijie24 +public class SavingListTest { + + private static final Logger LOGGER = Logger.getLogger(SavingListTest.class.getName()); + + + //@@author yyangdaa + @Test + public void addSaving_validInput_success() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "500"); + + assertEquals(1, savingList.getSavings().size()); + assertEquals("Salary", savingList.getSavings().get(0).getCategory()); + assertEquals(500, savingList.getSavings().get(0).getAmount()); + } + + //@@author yyangdaa + @Test + public void addSaving_invalidAmount_exceptionThrown() { + SavingList savingList = new SavingList(); + try { + savingList.addSaving("Salary", "abc"); + } catch (BudgetBuddyException e) { + assertEquals("Invalid amount format. Amount should be a positive number with up to maximum two decimal " + + "places.", e.getMessage()); + } + } + + //@@yyangdaa + @Test + public void addSaving_negativeAmount_exceptionThrown() { + SavingList savingList = new SavingList(); + try { + savingList.addSaving("Salary", "-1.00"); + } catch (BudgetBuddyException e) { + assertEquals("Invalid amount format. Amount should be a positive number with up to maximum "+ + "two decimal places.", e.getMessage()); + } + } + + //@@yyangdaa + @Test + public void addSaving_nullCategory_exceptionThrown() { + SavingList savingList = new SavingList(); + try { + savingList.addSaving("abc", "500"); + } catch (BudgetBuddyException e) { + assertEquals("The category 'abc' is not listed.", e.getMessage()); + } + } + + @Test + public void calculateRemainingSavings_sufficientFunds_success() { + SavingList savingList = new SavingList(); + double initialAmount = 1000; + double totalExpenses = 200; + double expectedRemaining = 800; + + double actualRemaining = savingList.calculateRemainingSavings(initialAmount, totalExpenses); + + assertEquals(expectedRemaining, actualRemaining); + } + + @Test + public void calculateRemainingSavings_insufficientFunds_success() { + SavingList savingList = new SavingList(); + double initialAmount = 200; + double totalExpenses = 1000; + double expectedRemaining = -800; + + double actualRemaining = savingList.calculateRemainingSavings(initialAmount, totalExpenses); + + assertEquals(expectedRemaining, actualRemaining); + } + + @Test @Disabled + public void calculateRemainingSavings_insufficientFunds_exceptionThrown() { + SavingList savingList = new SavingList(); + double initialAmount = 100; + double totalExpenses = 200; + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + savingList.calculateRemainingSavings(initialAmount, totalExpenses); + }); + assertEquals("java.lang.Exception: Insufficient Funds", exception.getMessage()); + } + + @Test + public void findTotalSavings_calculateSavingsList_success() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "500"); // Adding initial savings to work with + savingList.addSaving("Investments", "300"); + + savingList.findTotalSavings(); + + assertEquals(800, savingList.getInitialAmount()); + + } + + @Test + public void editSaving_validInput_success() throws BudgetBuddyException { + // Create a SavingList and add some savings + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", String.valueOf(100)); + savingList.addSaving("Investments", String.valueOf(200)); + + // Edit one of the savings + savingList.editSaving("Salary", 150); + + // Verify that the saving was edited successfully + assertEquals(150, savingList.getSavings().get(0).getAmount(), 0.001); + } + + @Test + public void reduceSavingsByCategory_nonExistentCategory_failure() throws BudgetBuddyException { + SavingList savingList = new SavingList(); + savingList.addSaving("Salary", "1000"); // Add a valid category for clarity + + // Set up to capture System.out output + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + // Attempt to reduce savings for a non-existent category + String nonExistentCategory = "NonExistent"; + savingList.reduceSavingsByCategory(nonExistentCategory, 50); + + // Restore System.out output to original stream + System.setOut(originalOut); + + String output = outContent.toString(); + assertTrue(output.contains("No savings found under category: " + nonExistentCategory), + "Expected message for non-existent category not found."); + + // Check that no other category was reduced + assertTrue(savingList.getSavings().stream() + .allMatch(saving -> saving.getAmount() == 1000 && saving.getCategory().equals("Salary")), + "No savings should be reduced under a non-existent category."); + } + + @Test + public void testGetSavingsInsights() { + // Set up the SavingList with sample savings + SavingList savingList = new SavingList(); + try { + savingList.addSaving("Salary", "1000"); + savingList.addSaving("Investments", "500"); + savingList.addSaving("Gifts", "200"); + } catch (BudgetBuddyException e) { + LOGGER.log(Level.SEVERE, "Exception occurred while adding savings", e); + } + + // Redirect standard output to capture the output of getSavingsInsights + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + // Invoke the method + savingList.getSavingsInsights(); + + // Capture and assert the output + String output = outContent.toString(); + assertTrue(output.contains("Highest Savings Category:")); + assertTrue(output.contains("Lowest Savings Category:")); + assertTrue(output.contains("Categories with no savings added:")); + + // Restore the original standard output + System.setOut(originalOut); + } +} diff --git a/src/test/java/seedu/budgetbuddy/SplitExpenseListTest.java b/src/test/java/seedu/budgetbuddy/SplitExpenseListTest.java new file mode 100644 index 0000000000..8d4063f403 --- /dev/null +++ b/src/test/java/seedu/budgetbuddy/SplitExpenseListTest.java @@ -0,0 +1,69 @@ +package seedu.budgetbuddy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +import seedu.budgetbuddy.commons.SplitExpenseList; +import seedu.budgetbuddy.exception.BudgetBuddyException; + + + + +public class SplitExpenseListTest { + + @Test + public void addSplitExpense_addingsplitexpense_success() throws BudgetBuddyException { + SplitExpenseList splitExpenseList = new SplitExpenseList(); + splitExpenseList.addSplitExpense("12", "12", "Lunch"); + + assertEquals(1, splitExpenseList.getSplitExpenses().size()); + assertEquals(12, splitExpenseList.getSplitExpenses().get(0).getNumberOfPeople()); + assertEquals("Lunch", splitExpenseList.getSplitExpenses().get(0).getDescription()); + } + + @Test + public void addSplitExpense_invalidAmount_exceptionThrown() throws BudgetBuddyException{ + SplitExpenseList splitExpenseList = new SplitExpenseList(); + try { + splitExpenseList.addSplitExpense("abc", "12", "Lunch"); + fail(); + } catch (BudgetBuddyException e) { + assertEquals("Invalid amount format. Amount should be a number.", e.getMessage()); + } + } + + @Test + public void addSplitExpense_invalidNumberOfPeople_exceptionThrown() throws BudgetBuddyException{ + SplitExpenseList splitExpenseList = new SplitExpenseList(); + try { + splitExpenseList.addSplitExpense("12", "abc", "Lunch"); + fail(); + } catch (BudgetBuddyException e) { + assertEquals("Number of people should be a number", e.getMessage()); + } + } + + @Test + public void addSplitExpense_negativeAmount_exceptionThrown() throws BudgetBuddyException{ + SplitExpenseList splitExpenseList = new SplitExpenseList(); + try { + splitExpenseList.addSplitExpense("-12", "12", "Lunch"); + fail(); + } catch (BudgetBuddyException e) { + assertEquals("Expenses should not be negative.", e.getMessage()); + } + } + + @Test + public void addSplitExpense_negativeNumberOfPeople_exceptionThrown() throws BudgetBuddyException{ + SplitExpenseList splitExpenseList = new SplitExpenseList(); + try { + splitExpenseList.addSplitExpense("12", "-12", "Lunch"); + fail(); + } catch (BudgetBuddyException e) { + assertEquals("Number of people should be a positive number", e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..7c5b8ac6a8 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,14 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +__________________________________________________ + BudgetBuddy +__________________________________________________ +Welcome to BudgetBuddy, to start, please type "menu INDEX" to view commands for the respective functions +To view all menu items again, type "menu". +__________________________________________________ +0. Display the whole menu +1. Manage Expenses 2. Manage Savings +3. View Expenses 4. View Savings +5. Find Expenses 6. Divide Bills +7. Manage Recurring Bills 8. Change Currency +9. Manage Budget 10. Get Graphical Insights +__________________________________________________ +Goodbye! Thank you for using BudgetBuddy. diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..b023018cab 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1 @@ -James Gosling \ No newline at end of file +bye