diff --git a/.gitignore b/.gitignore index 2873e189e1..ebcd74ed30 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +data/ diff --git a/build.gradle b/build.gradle index ea82051fab..5be1c9bbd4 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,16 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' + implementation group: 'org.knowm.xchart', name: 'xchart', version: '3.8.5' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' + implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.9' + implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.9' + + + } test { @@ -29,7 +39,7 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.financialplanner.FinancialPlanner") } shadowJar { @@ -43,4 +53,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..453fba8f18 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) | Hao Chen | [Github](https://github.com/) | [Portfolio](team/hshiah.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Ryan Chua | [Github](https://github.com/ryan1604) | [Portfolio](team/ryan1604.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Neo Min Wei | [Github](https://github.com/NeoMinWei) | [Portfolio](team/neominwei.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Frederick | [Github](https://github.com/wwweert123) | [Portfolio](team/wwweert123.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Zhengdao | [Github](https://github.com/YFshadaow) | [Portfolio](team/yfshadaow.md) | + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..5c44bee7b5 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,842 @@ # Developer Guide +## Table of contents + +* [Acknowledgements](#acknowledgements) +* [Design & implementation](#design--implementation) + * [Architecture Diagram](#architecture-diagram) + * [Storage Component](#storage-component) + * [Design considerations](#design-considerations) + * [Visualization Feature](#visualization-feature) + * [Class diagram](#class-diagram) + * [Sequence diagram](#sequence-diagram) + * [WatchList Feature](#watchlist-feature) + * [Class diagram](#watchlist-class-diagram-simplified) + * [Sequence diagram](#watchlist-sequence-diagram-simplified) + * [Add Income/Expense Feature](#add-incomeexpense-feature) + * [Class Diagram](#add-incomeexpense-class-diagram) + * [Sequence Diagram](#add-incomeexpense-sequence-diagram) + * [Recurring Cashflow Feature](#recurring-cashflow-feature) + * [Class Diagram](#recurring-cashflow-class-diagram) + * [Sequence Diagrams](#recurring-cashflow-sequence-diagrams) + * [Budget Feature](#budget-feature) + * [Set and update budget](#set-and-update-budget) + * [Delete budget](#delete-budget) + * [Reset budget](#reset-budget) + * [View budget](#view-budget) + * [Mark Goal Feature](#mark-goal-feature) + * [Sequence Diagram](#mark-goal-sequence-diagram) +* [Product Scope](#product-scope) + * [Target user profile](#target-user-profile) + * [Value proposition](#value-proposition) +* [User Stories](#user-stories) +* [Non-Functional Requirements](#non-functional-requirements) +* [Glossary](#glossary) +* [Instructions for manual testing](#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} +**Xchart (A Simple Charting Library for Java)** +- author: KNOWM +- source: [https://knowm.org/open-source/xchart/](https://knowm.org/open-source/xchart/) + +**JSON Simple (simple Java toolkit for encoding and decoding JSON)** +- author: Yidong Fang (Google Code) +- source: [https://code.google.com/archive/p/json-simple/](https://code.google.com/archive/p/json-simple/) + +**Apache Common Langs 3** +- author: Apache Commons +- source: [https://commons.apache.org/proper/commons-lang/](https://commons.apache.org/proper/commons-lang/) + +**Alpha Vantage Stock Market API** +- author: Alpha Vantage +- source: [https://www.alphavantage.co/](https://www.alphavantage.co/) + +**Gson Java library** +- author: Google +- source: [https://github.com/google/gson](https://github.com/google/gson) + +**Financial Modeling Prep Stock API** +- author: Financial Modeling Prep +- source: [https://site.financialmodelingprep.com/](https://site.financialmodelingprep.com/) + +**round() method in Cashflow.java** + - author: mhadidg + - source: [https://stackoverflow.com/questions/2808535/round-a-double-to-2-decimal-places](https://stackoverflow.com/questions/2808535/round-a-double-to-2-decimal-places) + +**capitalize() method in Cashflow.java** + - author: Nick Bolton + - source: [https://stackoverflow.com/questions/1892765/how-to-capitalize-the-first-character-of-each-word-in-a-string](https://stackoverflow.com/questions/1892765/how-to-capitalize-the-first-character-of-each-word-in-a-string) + +**DG adapted from** + +* [Addressbook-level3](https://github.com/se-edu/addressbook-level3) ## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Architecture Diagram + +![](images/ArchitectureDiagram.png) + +The **Architecture Diagram** given above explains the high-level design of the program. + +#### Main components of the architecture + +`Main` consists of the `FinancialPlanner` class, and is in charge of starting up and shutting down the app. + +The program consists of five main components: + +* `Ui`: User interactions. +* `Parser`: Parse user inputs. +* `Command`: The command executor. +* `Model`: Holds the data of the program in memory. +* `Storage`: Reads data from, and writes data to the hard disk. + +### Storage Component + +API: `Storage.java` + +![](images/Storage.png) + +* The storage component loads data from the saved text files when the application starts, and saves the data to the + text files when the application exits. +* The storage class uses the static methods in LoadData and SaveData to load and save data respectively. +* The `load` method in LoadData reads the `data.txt` file and loads any existing Income, Expense and Budget into the application. +* The `save` method in SaveData saves all Incomes, Expenses and existing Budget into the `data.txt` file. + +#### Design considerations: + +* There are 2 main ways to implement the storage, one is to save the data after every command, and the other is to save +the data one upon exiting the program with the `exit` command. +* Option 1 (Currently implemented): Saving the data once upon exit + * Advantage: Better efficiency and performance of the program. + * Disadvantage: If the program crashes or exits incorrectly, data will not be saved. +* Option 2: Saving the data after every command: + * Advantage: Changes are saved after every command. + * Disadvantage: Executing command might slow down the program when there is a large amount of data to be saved. + +Option 1 is chosen to prioritise the performance of the program. + +### Visualization Feature + +This feature is implemented with the help of [XChart](https://knowm.org/open-source/xchart/), a simple charting library for Java by Knowm. + +By typing in the vis command with the appropriate arguments (`/s` and `/t`), users will be able to visualize their +income or expense using visualization tools (Piechart, Bar Chart or Radar Chart) + +Demo: + +`vis /t expense /c pie` + +Output: + +`Displaying piechart for expense` +A message will be shown telling you that the chart is being displayed + +![](images/vis/visOutput.png) + +This feature was implemented with the help of three different classes. +They are namely: Visualizer, Categorizer, VisCommand (Inherits from abstract Command Class) + +VisCommand's Role: + +1) Read the parameters of the vis command entered by the user +- `/t` Reads the type of cashflow that the user wants to visualize (income/expense) +- `/c` Reads the type of visualization tools the user wants (piechart/barchart) + +2) Calls the Categorizer to sort cashflow (Income/Expense) according to type + +3) Calls the Visualizer to display the chart to the user + +As with other Commands in our Financial Planner application, the constructor of VisCommand +takes RawCommand as parameter. The RawCommand would provide the arguments (chart type and cashflow type) +for the VisCommand provided. + +The VisCommand also inherits from the abstract Command class which would provide the execute() abstract method +that would be called in main(). + +Categorizer's Role: + +According to the cashflow type (Income/Expense) arugment passed in, the Categorizer sorts the +specified cashflow entry according to type using a Hashmap which is returned and used by the Visualizer + +Visualizer's Role: + +According to the chart type (Pie/Bar/Radar) argument and the Hashmap obtained from the categorizer passed in, +the visualizer displays the specified visualization chart by calling the charting library Xchart. + +### Class Diagram + +![](images/vis/visualisationClass.png) + +### Sequence Diagram + +Overall + +![](images/vis/visualisationSequence.png) + +Categorizer (`sort cashflow entries` ref from overall sequence diagram above) + +![](images/vis/categorizerSequence.png) + +Visualizer (`displaying chart` ref from overall sequence diagram above) + +![](images/vis/visualizerSequence.png) + +### Watchlist Feature + +The watchlist in financial planner is similar to that of other common watchlist online. +It contains a list of stocks that the user watches with an eye toward taking advantage of prices. It allows +users to track real time data on the stocks that they are interested in. + +Simply type `watchlist` into the command line without any arguments and the watchlist will be displayed. + +Example Output: + +![](images/investments/watchlistOutput.png) + +Below are the various classes involved in the implementation of watchlist. + +#### WatchlistCommand + +1. The WatchlistCommand instance calls the `getLatestWatchListInfo()` method from the watchlist class to update the +stocks data in the watchlist. +2. It then calls the `printStocksInfo()` method of the Ui class to print out the watchlist. +3. Finally, it calls the static `saveWatchList()` method of the SaveData class to save the watchlist info to +watchlist.json. + +#### Watchlist + +The watchlist class keeps a record of the stocks that the user is interested in using a hashmap as shown. +``` +private HashMap stocks; +``` + +1. When its method `getLatestWatchListInfo()` is invoked, it calls `getExpiredStocks()` to get the list +of stocks that has expired and should be renewed with latest info. +2. With the list of expired stocks, it calls `fetchFMPStockPrices()` which connects to Financial Modeling API to retrieve +the latest stock prices and calls `extractWatchListInfoFromJSONArray()` to update the stocks in the Hashmap +with the latest stock data. + +### Stock + +Stocks class objects are the values that make up the stocks hashmap in the watchlist. They cache the stock data obtained +from the API as attributes of the class. + +``` +private String symbol; +private String exchange; +private String stockName; +private String price; +private String dayHigh; +private String dayLow; +private Date lastUpdated = null; +private long lastFetched = 0; +private int hashCode = 0; +``` + +Shown above is a complete list of attribute of the stock class. + +`lastFetched` and `hashCode` are attributes that are not related to the stock financial data. +`lastFetched` is used for caching validity checking and `hashCode` is used to tell whether saved values on disk +are corrupted. + +#### Watchlist Class Diagram (Simplified) + +![](images/investments/watchlistClassDiagram.png) + +#### Watchlist sequence Diagram (Simplified) + +![](images/investments/watchlistSequence.png) + +### Add Income/Expense Feature + +The add income/expense command has 2 compulsory arguments `/t` and `/a` and 2 optional argument `/r` and `/d`. + +Example: +``` +add income /a 100 /t salary /r 30 /d work +``` +Below are the steps that shows the implementation of add income/expense. + +#### Step 1 +The AddCashflowCommand instance calls addIncome() or addExpense(), depending on what `category` is initialised as. + +addIncome() or addExpense() instantiates an Income or Expense object respectively. + +Example: +``` +switch (category) { +case INCOME: + cashflowList.addIncome(amount, incomeType, recur, description); + break; +case EXPENSE: + cashflowList.addExpense(amount, expenseType, recur, description); + break; +default: + ui.showMessage("Unidentified entry."); + break; +} +``` +#### Step 2 +The instantiated income/expense then updates the overall balance through addIncomeValue() or addExpenseValue(). + +The income/expense object is also added to the list in Cashflowlist which contains all incomes/expenses. +#### Step 3 +The added income/expense is then displayed to the user through the Ui. + +#### Add Income/Expense Class Diagram +Given below is the class diagram showing the class structure of the add income/expense mechanism: +![](images/cashflow/CashflowClassDiagram.png) +#### Add Income/Expense Sequence Diagram +Given below is the sequence diagram showing the add income/expense mechanism: +![](images/cashflow/AddCashflowSequence.png) +### Recurring Cashflow Feature +Cashflow refers to an income or expense. + +This feature is called from the user through the `/r` argument in add income/expense command. + +If a cashflow is set to be recurring, the program would add another entry of the same cashflow to the Financial Planner after a set period of time. + +Below are the steps that shows the implementation of the recurring cashflow feature. + +#### Step 1 +Once the cashflow is set to be recurring, its corresponding `Cashflow` object would store the date at which the cashflow was added to the Financial Planner. + +The `recur` variable in the object would also be instantiated according to the user's input. + +An additional `boolean` variable, `hasRecurred` is stored in the object and is set to `false` by default. + +Example: +``` +public abstract class Cashflow { + protected int recur; + protected LocalDate date; + protected boolean hasRecurred; +} +``` +#### Step 2 +When the Financial Planner is started again in the future, the date of startup would be obtained from the system. + +After loading existing saved cashflows from data.txt, the program will check for cashflows that are set to be recurring and has not recurred. + +Example: +``` +if (recur > 0 && !hasRecurred) { + ... +} +``` + +#### Step 3 +Once a cashflow matches the above criteria, the date of its next addition to the Financial Planner, `dateOfAddition`, would be determined. + +`dateOfAddition` would be compared to the current date, and if the current date is after or equal to `dateOfAddition`, an identical cashflow would be instantiated. + +This identical cashflow would then have its `date` variable set to `dateOfAddition`, then this cashflow would be added to a temporary list, `tempCashflowList`. + +The original cashflow would then have its `hasRecurred` variable be set to `true`. + +#### Step 4 +Each cashflow in `tempCashflowList` goes through **Step 3** again, so that multiple cashflows can be added if it has recurred more than once. + +Once the process is done, all cashflows in `tempCashflowList` are then added to the Financial Planner. + +The added cashflows are then displayed to the user. + +#### Recurring Cashflow Class Diagram +Given below is the class diagram showing the class structure of the recurring cashflow mechanism: +![](images/cashflow/RecurClassDiagram.png) +#### Recurring Cashflow Sequence Diagrams +Given below is the sequence diagram showing the recurring cashflow mechanism: +![](images/cashflow/RecurSequence.png) +![](images/cashflow/AddRecurringSequence.png) + +### Budget Feature + +This feature has 5 functions, `set`, `update`, `delete`, `reset`, and `view`. + +![](images/Budget.png) + +The BudgetCommand will execute the appropriate command and print through `Budget.java` and prints any message to the user through `Ui.java`. + +#### Set and update budget: + +Example: +``` +budget set /b 500 +budget update /b 1000 +``` +The `/b` is followed by the budget amount. + +##### Set budget: + +The first line will set the budget by calling `setBudget(500)` method in `Budget.java`. The `setBudget(500)` method then sets the +`initialBudget` and `currentBudget` variable to the input amount, in this case 500. + +##### Update budget: + +The second line updates the budget by adding or subtracting the difference between the initial and updated amount to `initialBudget` +and `currentBudget`. This is done through `updateBudget(1000)` method in `Budget.java`. In the example above, since the budget is +being updated from `500` to `1000`, `500` will be added to the variables `initialBudget` and `currentBudget`. Both functions can be seen +in the diagram above. + +#### Delete budget: + +![](images/deleteBudget.png) + +The budget will be deleted by setting the `initialBudget` and `currentBudget` variables to `0` through the `deleteBudget()` method in `Budget.java`. + +Example: `budget delete` + +#### Reset budget: + +![](images/resetBudget.png) + +The budget will be reset by resetting the `currentBudget` variable to the `initialBudget` variable through the `resetBudget()` method in `Budget.java`. + +Example: `budget reset` + +#### View budget: + +![](images/viewBudget.png) + +The `currentBudget` will be shown to the user through the `Ui`. + +Example: `budget view` + +### Mark Goal Feature + +The mark goal command has 1 compulsory argument `INDEX`. + +Example: +``` +markgoal 1 +``` +Below are the steps that shows the implementation of set goal. + +#### Step 1 +The MarkGoalCommand instance calls `markGoal(INDEX)` function of wish list. +#### Step 2 +Wish list finds the corresponding goal and calls `markAsDone()` function of corresponding goal. +#### Step 3 +The marked goal is then displayed to the user through the Ui. +#### Step 4 +The wish list calls `addExpense(amount, type, label)` function of cashflow list to add corresponding expense. +#### Step 5 +The added expense is then displayed to the user through the Ui. +#### Mark goal Sequence Diagram +Given below is the sequence diagram showing the markgoal mechanism: +![](images/MarkGoalSequence.png) ## Product scope ### Target user profile -{Describe the target user profile} +Our target user profile is ... +- a working adult with a source of income +- someone who dislike navigating graphic user interface +- someone who can type fast +- someone who cannot manage their finances such as income and expenses properly +- unable to reach their financial goals +- is slightly interested in the equity market +- needs reminders for tasks + ### Value proposition -{Describe the value proposition: what problem does it solve?} +Our financial planner application can help individuals manage their finances effectively and achieve their financial +goals. The purpose of such an application is to provide users with a range of tools and features to help them better +understand their financial situation. This will enable them to make more informed decisions, and plan for their future +financial well-being. The application will allow the user to keep track of their income, expenses and overall balance. +It also lets the user view their income and expenses using visualization tool to have a better view of their cash flow +based on categories. It also allows the user to set the budget for the month. It also allows users to add their financial +goals to the wishlist. Furthermore, it allows users to track the stock market if they have interest in investing in +equities. ## 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 | Add my income | Store my income information and view/track them later | +| v1.0 | user | Delete my income | Remove the income entry that I have mistakenly added or do not keep track | +| v1.0 | user | Add my expense | Store my expense information and view/track them later | +| v1.0 | user | Delete my expense | Remove the expense entry that I have mistakenly added or do not keep track | +| v2.0 | user | set my expense type | Break down my expenses into different categories | +| v1.0 | user | set my income type | Break down my income into different categories | +| v2.0 | user | Add recurring cash flows | add a regular expense or income (salary, rent) easily | +| v2.0 | user | Delete recurring cash flows | delete a regular expense of income easily | +| v1.0 | user | list all cash flow entries | view all my income and expenses in a comprehensive list | +| v1.0 | user | list all expenses entries | view all my expenses in a comprehensive list | +| v1.0 | user | list all income entries | view all my income in a comprehensive list | +| v2.0 | user | list all recurring cash flows | view all my recurring income or expenses in a comprehensive list | +| v2.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v1.0 | user | set a budget | keep track of a budget together with my cash flow and ensure I do not exceed it | +| v1.0 | user | update the budget | make changes to the budget according to my needs | +| v1.0 | user | reset the budget | return to my initial budget easily | +| v1.0 | user | delete budget | remove the budget that I no longer want to keep track of | +| v1.0 | user | view budget | keep track of the amount of budget I have left | +| v1.0 | user | see overview of the app | see the overall view of all income, expense and overall balance as well as reminders | +| v1.0 | user | view balance | see my overall balance according to the income and expenses I am keeping track | +| v1.0 | investment enthusiast | view my watchlist | keep track of stocks that I am interested in | +| v2.0 | investment enthusiast | add new stocks to watchlist | add new stock that I am interested in investing in | +| v2.0 | investment enthusiast | delete stocks from watchlist | remove stocks that I am no longer interested in | +| v1.0 | user | add reminder | add reminders (eg to pay loans) so I will not forget | +| v1.0 | user | delete reminder | delete reminders that I no longer want to keep track | +| v1.0 | user | mark reminder | set the reminder as completed | +| v1.0 | user | view wishlist | keep track of my goals easily | +| v1.0 | user | set goals | add a new goal to my that I think of | +| v1.0 | user | delete goals | remove goals that I can no longer achieve | +| v1.0 | user | mark goal | that I have achieved | +| v1.0 | user | visualize my cash flow | easily see where the distribution for my spending and earnings | ## Non-Functional Requirements -{Give non-functional requirements} +* Should work on main OS (Windows, Linux, Mac) that has Java 11 installed. +* This app is meant for a single user. +* This app is targeted towards users with an above-average typing speed. +* Watchlist should work reliably and not crash the application when the 3rd party dependencies are down (API is down) ## Glossary -* *glossary item* - Definition +* *Cashflow* - Refers to an income or expense. +* *WishList* - A list containing goals/targets. +* *Watchlist* - A list of stocks that the financial planner is currently tracking ## 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} +Given below are instructions to test the app manually + +- Note: These instructions only provide a starting point for testers to work on + +### Launch and shutdown + +1. Initial Launch + 1. Download the jar file and copy into an empty folder + 2. Open up the terminal and run java -jar tp.jar Expected: +shows you the welcome screen for the financial planner app +2. Closing the application + 1. Type `exit` into the terminal. + 2. Expected: the financial planner will exit with a goodbye message. +Under the data newly created data directory, a watchlist.json and a data.txt file will be created + +### Add cashflow + +To test the add cashflow feature, you can use the following command: +``` +add income /a 5000 /t salary /r 30 /d work +``` +You should see the following output: + +![](images/cashflow/AddCashflow.png) + +Note: The date displayed will differ based on your system time. + +You can also use the following command to test the optional arguments: +``` +add expense /a 1000 /t necessities +``` +You should see the following output: +``` +You have added an Expense + Type: Necessities + Amount: 1000.00 +to the Financial Planner. +Balance: 4000.00 +``` + +### List + +To test the list feature, you can add these test inputs to the program first: + +Make sure there is no existing cashflows in the program in order to achieve the exact outputs below. You can clear the inputs by exiting the program and deleting the data.txt file found in data folder. + +Make sure to add each command line by line. + +``` +add income /a 5000 /t salary /r 30 /d work +add expense /a 1000 /t necessities +add income /a 500 /t investments /d stocks +add expense /a 800 /t insurance /r 365 /d insurance +``` + +After which you can test the following commands: + +Note: The dates displayed will differ based on your system time. + +Input: `list` + +Output: + +![](images/cashflow/List.png) + +Input: `list income` + +Output: + +![](images/cashflow/ListIncome.png) + +Input: `list expense` + +Output: + +![](images/cashflow/ListExpense.png) + +Input: `list recurring` + +Output: + +![](images/cashflow/ListRecurring.png) + +### Delete cashflow + +You are recommended to test this feature after testing the list feature as they share the same test inputs. + +If you have not done so, please follow the instructions to test the list feature [here](#list). + +To test the delete cashflow feature, you can use the following commands provided below: + +Use the list command before each delete command to confirm that the cashflow at the index stated in the delete command matches the expected output. + +Note: The dates displayed will differ based on your system time. + +Input: `list` followed by `delete 3` + +Output: +``` +You have removed an Income + Type: Investments + Amount: 500.00 + Description: stocks +from the Financial Planner. +Balance: 3200.00 +``` + +Input: `list income` followed by `delete income 1 /r` + +Output: +``` +You have removed future recurrences of this cashflow. +Updated cashflow: +Income + Type: Salary + Amount: 5000.00 + Description: work +``` + +Input: `list income` followed by `delete income 1` + +Output: +``` +You have removed an Income + Type: Salary + Amount: 5000.00 + Description: work +from the Financial Planner. +Balance: -1800.00 +``` + +Input: `list expense` followed by `delete expense 1` + +Output: +``` +You have removed an Expense + Type: Necessities + Amount: 1000.00 +from the Financial Planner. +Balance: -800.00 +``` + +Input: `list recurring` followed by `delete recurring 1` + +Output: + +![](images/cashflow/ListDeleteRecurring.png) + +### Recurring cashflow + +You can test the recurring cashflow feature by manually changing your system time. + +First add a cashflow that has a recurrence value. You can use the following example command: + +``` +add income /a 5000 /t salary /r 1 /d work +``` + +Next, exit the program and change the system time to be ahead by the specified days in the cashflow. + +In the case of the example command, you can bring forward the system time by 1 day. + +Finally, start the program again and you should see this output: + +![](images/cashflow/RecurringIncome.png) + +Note: The dates displayed will differ based on your system time. + +### View Balance + +Test case: `balance` + +Expected: Balance is displayed. Details of balance are shown in the status message. + +### Budget Feature + +1. Setting a monthly budget + +Test case: `budget set /b 100` + +Expected: A monthly budget of 100 is set. Details of the budget are shown in the status message. + +Test case: `budget set /b` + +Expected: No budget is set. Error details shown in status message. + +Other incorrect set budget commands to try: `budget set`, `budget set /b x`, `...` (where x is negative) + +2. Updating budget + +Test case: `budget update /b 300` + +Expected: Monthly budget is updated to 300. Details of the budget are shown in the status message. + +Test case: `budget update /b` + +Expected: Budget is not updated. Error details shown in status message. + +Other incorrect set budget commands to try: `budget update`, `budget update /b x`, `...` (where x is negative) + +3. Resetting budget + +Test case: `budget reset` + +Expected (Current budget is lower than initial budget): Budget is reset. Details of reset budget are shown in status message. + +Expected (Budget has not been spent): Budget is not reset. Error details shown in the status message. + +4. Deleting budget + +Test case: `budget delete` + +Expected (Budget exists): Budget is deleted. Details of deletion are shown in status message. + +Expected (Budget does not exist): No budget to delete. Error details shown in the status message. + +5. Viewing budget + +Test case: `budget view` + +Expected (Budget exists): Budget is displayed. Details of budget are shown in status message. + +Expected (Budget does not exist): No budget to display. Error details shown in the status message. + +### Displaying overview + +Test case: `overview` + +Expected: Displays overview of user's financials. Details of financials are shown in the status message. + +### Using Watchlist + +To test the watchlist feature, you can copy the text below into the watchlist.json file under data directory +``` +{ + "BB": { + "symbol": "BB", + "stockName": "BlackBerry Ltd" + }, + "TSLA": { + "symbol": "TSLA", + "stockName": "Tesla Inc" + } +} +``` +Start Financial Planner app and you should be able to see this output (although prices will differ) after running the +watchlist command + +![](images/investments/watchlistMT1.png) + +You can then add a stock using the command below +``` +addstock /s NET +``` +You should see a message stating that Cloudflare was added. After running the watchlist command again and exiting the +application, your watchlist output should look like this + +![](images/investments/watchlistMT2.png) + +You can also remove stocks from the command. Run these commands separately +``` +deletestock /s BB +deletestock /s TSLA +deletestock /s NET +``` +After deleting all the stocks and running the watchlist command again, the output should look like this +as you have no more stocks left in your watchlist +![!](images/investments/watchlistMT3.png) + +### Using Visualization + +We can use the visualization feature to visualize your income and expenses. + +First we will add some expenses +``` +add expense /a 1000 /t necessities /d Iphone 15 pro max +add expense /a 4 /t others /d cai png +add expense /a 100 /t travel /d JB +``` + +Now we can visualize these expenses using 3 different charts (pie/bar/radar) +``` +vis /t expense /c pie +vis /t expense /c bar +vis /t expense /c radar +``` +You can run the 3 commands separately to see different charts + +We can do the same for income. Add some entries +``` +add income /a 1800 /t salary /d mcd +add income /a 400 /t investments /d Gamestop +add income /a 100 /t allowance /d parents +``` + +Again we can visualize these income using 3 different charts in separate commands (pie/bar/radar) +``` +vis /t income /c pie +vis /t income /c bar +vis /t income /c radar +``` + +### Saving data + +Dealing with missing/corrupted data files: + +Example of a valid `data.txt` file: + +``` +I | 5000.0 | SALARY | 30 | false | 31/10/2023 +E | 50.0 | OTHERS | 0 | false +I | 500.0 | OTHERS | 0 | false +I | 5.0 | OTHERS | 0 | false +E | 5.0 | OTHERS | 0 | false +``` + +The first column specifies the type of data being saved, and the subsequent columns contain the data to be saved. +For example, `I` and `E` represent `income` and `expense` respectively, and there are other types, such as `B` for `budget`. + +For incomes and expenses, the second column represent the amount, which is a `double`. To simulate a corrupted data, you +can change the number in the column to a string for example. + +Example of corrupted `data.txt` file in the third row: + +``` +I | 5000.0 | SALARY | 30 | false | 31/10/2023 +E | 50.0 | OTHERS | 0 | false +I | sdf | OTHERS | 0 | false +I | 5.0 | OTHERS | 0 | false +E | 5.0 | OTHERS | 0 | false +``` + +When starting the program: + +Expected: Data fails to load. Error details shown in status message. Program asks user if he/she wants to create a new file +(by clearing all data) or fix it manually. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..3e632b9310 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,10 @@ -# Duke +# Financial Planner -{Give product intro here} +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. -Useful links: +**For more information:** * [User Guide](UserGuide.md) * [Developer Guide](DeveloperGuide.md) * [About Us](AboutUs.md) diff --git a/docs/Style.puml b/docs/Style.puml new file mode 100644 index 0000000000..a08bc22c02 --- /dev/null +++ b/docs/Style.puml @@ -0,0 +1,5 @@ +!define LOGIC_COLOR #3333C4 +!define LOGIC_COLOR_T1 #7777DB +!define LOGIC_COLOR_T2 #5252CE +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..40e18802f8 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,934 @@ # User Guide +## Table of contents + +* [Introduction](#introduction) +* [Quick start](#quick-start) +* [Features](#features) + * [Add cashflow](#add-cashflow) + * [Add income](#add-income-add-income) + * [Add expense](#add-expense-add-expense) + * [List](#list) + * [List all](#list-all-list) + * [List income](#list-income-list-income) + * [List expense](#list-expense-list-expense) + * [List recurring](#list-recurring-list-recurring) + * [Delete cashflow](#delete-cashflow-delete) + * [Delete income](#delete-income-delete-income) + * [Delete expense](#delete-expense-delete-expense) + * [Delete recurring cashflow](#delete-recurring-delete-recurring) + * [Find cashflow](#find-cashflow-find) + * [View Balance](#viewing-balance-balance) + * [Budget](#budget) + * [Setting budget](#setting-a-budget-budget-set) + * [Updating budget](#updating-budget-budget-update) + * [Resetting budget](#resetting-budget-budget-reset) + * [Deleting budget](#deleting-budget-budget-delete) + * [Viewing budget](#viewing-budget-budget-view) + * [Displaying Overview](#displaying-overview-overview) + * [WatchList](#viewing-watchlist-watchlist) + * [Adding Stock](#adding-stock-to-watchlist-addstock) + * [Deleting Stock](#deleting-budget-budget-delete) + * [ReminderList](#view-reminder-list-reminderlist) + * [Adding Reminder](#add-reminder-addreminder) + * [Deleting Reminder](#delete-reminder-deletereminder) + * [Marking Reminder as Done](#mark-reminder-as-done-markreminder) + * [WishList](#view-goal-list-wishlist) + * [Adding Goal](#set-goal-set-goal) + * [Deleting Goal](#delete-goal-deletegoal) + * [Marking Goal as Achieved](#mark-goal-as-achieved-markgoal) + * [Visualization](#visualizing-your-cashflow-vis) + * [Exiting the program](#exiting-the-program-exit) + * [Get command help and example usage](#getting-command-help-and-example-usage-help) + * [Saving data](#saving-the-data) + * [Loading data](#loading-the-data) +* [FAQ](#faq) +* [Command Summary](#command-summary) + ## Introduction -{Give a product intro} +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. ## Quick Start -{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. Download the latest version of `Financial Planner` from [here](https://github.com/AY2324S1-CS2113-T18-2/tp/releases). +3. Copy the file to the folder you want to use as the *home folder* for Financial Planner. +4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar tp.jar` command to run the application. +5. Refer to the **Features** section below for details of each command. + +## Features + +### Notes about the command format +- Words in `UPPER_CASE` are parameters to be supplied by the user. + + e.g. in `add income /a AMOUNT`, `AMOUNT` is a parameter which can be used as `add income /a 100`. +- Items in square brackets are optional. + + e.g. `[/r DAYS]` can be used as `/r 30` or left empty. + +### Notes about naming convention +- Cashflow refers to an income or expense. + +### Notes about program limitations +- Maximum amount for each cashflow and total balance that the program can hold is 999,999,999,999.99 +- Minimum amount for each cashflow and total balance that the program can hold is -999,999,999,999.99 +- Total Balance, Income balance, and Expense balance are different entities where the latter two do not have the same limitations. +- Maximum value for recurrences and indexes is 2,147,483,647, the maximum number an `int` can hold. + +**Important:** Data is automatically loaded on start up and saved when exited. You must exit the program using the `exit` command in order to save your data. + +### Add cashflow + +#### Add income: `add income` +Adds an income source to the Financial Planner. + +Format: `add income /a AMOUNT /t TYPE [/r DAYS] [/d DESCRIPTION]` + +- `/a` is used to specify the amount of the income, where an **integer** or **double** is expected. +- `/r` is used to denote a recurring income, with the period to the next addition is specified by an **integer** representing the number of `DAYS`. +- `/d` is used to give a description to the income, where any **String** is expected. +- `/t` is used to specify the income type, where the list of acceptable types is given below. The types are case-insensitive. + +| Income Types | +|---------------| +| `salary` | +| `investments` | +| `allowance` | +| `others` | + +Example of usage: `add income /a 5000 /t salary /r 30 /d work` + +Example output: + +``` +You have added an Income + Type: Salary + Amount: 5000.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: work +to the Financial Planner. +Balance: 5000.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +#### Add expense: `add expense` +Adds an expense to the Financial Planner + +Format: `add expense /a AMOUNT /t TYPE [/r DAYS] [/d DESCRIPTION]` + +- `/a` is used to specify the amount of the expense, where an **integer** or **double** is expected. +- `/r` is used to denote a recurring expense, with the period to the next addition is specified by an **integer** representing the number of `DAYS`. +- `/d` is used to give a description to the expense, where any **String** is expected. +- `/t` is used to specify the expense type, where the list of acceptable types is given below. The types are case-insensitive. + +| Expense | +|-----------------| +| `dining` | +| `entertainment` | +| `shopping` | +| `travel` | +| `insurance` | +| `necessities` | +| `others` | + +Example of usage: `add expense /a 300 /t necessities /r 30 /d groceries` + +Example output: +``` +You have added an Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: groceries +to the Financial Planner. +Balance: 4700.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +### List + +#### List all: `list` +Lists all cashflows. + +Format: `list` + +Example of usage: `list` + +Example output: + +``` +You have 4 matching cashflows: +1: Expense + Type: Dining + Amount: 30.00 + Description: Genki Sushi +2: Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: groceries +3: Income + Type: Allowance + Amount: 500.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 +4: Income + Type: Investments + Amount: 1000.00 +Balance: 1170.00 +``` + +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +#### List income: `list income` +Lists all incomes. + +Format: `list income` + +Example of usage: `list income` + +Example output: +``` +You have 3 matching cashflows: +1: Income + Type: Allowance + Amount: 500.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 +2: Income + Type: Investments + Amount: 1000.00 +3: Income + Type: Salary + Amount: 100.00 +Income Balance: 1600.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +#### List expense: `list expense` +Lists all expenses. + +Format: `list expense` + +Example of usage: `list expense` + +Example output: +``` +You have 3 matching cashflows: +1: Expense + Type: Dining + Amount: 30.00 + Description: Genki Sushi +2: Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: groceries +3: Expense + Type: Others + Amount: 0.23 +Expense Balance: 330.23 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +#### List recurring: `list recurring` +Lists all recurring cashflows. + +Format: `list recurring` + +- This list will not include any cashflow that has already recurred. + +Example of usage: `list recurring` + +Example output: +``` +You have 4 matching cashflows: +1: Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: groceries +2: Income + Type: Salary + Amount: 5000.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: work +3: Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 + Description: groceries +4: Income + Type: Allowance + Amount: 500.00 + Recurring every: 30 days, date added: Nov 04 2023, recurring on: Dec 04 2023 +``` +- Note: Date displayed above is just an example. Your actual date may differ. + +### Delete cashflow: `delete` +Deletes a cashflow from the Financial Planner. + +Format: `delete INDEX [/r]` + +- `INDEX` refers to the index number shown in the displayed list when [`list`](#list-all-list) command is used. +- `/r` is used to delete all **future** cashflows **only**. + +Example of usage: `delete 1 /r` + +Example output: +``` +You have removed future recurrences of this cashflow. +Updated cashflow: +Income + Type: Salary + Amount: 5000.00 + Description: work +``` +Example of usage: `delete 1` + +Example output: +``` +You have removed an Income + Type: Salary + Amount: 5000.00 + Description: work +from the Financial Planner. +Balance: -1130.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. + +#### Delete income: `delete income` +Deletes an income from the Financial Planner. + +Format: `delete income INDEX [/r]` + +- `INDEX` refers to the index number shown in the displayed list when [`list income`](#list-income-list-income) command is used. +- `/r` is used to delete all **future** incomes **only**. + +Example of usage: `delete income 2 /r` + +Example output: +``` +You have removed future recurrences of this cashflow. +Updated cashflow: +Income + Type: Allowance + Amount: 500.00 + Description: parents +``` +Example of usage: `delete income 2` + +Example output: +``` +You have removed an Income + Type: Allowance + Amount: 500.00 + Description: parents +from the Financial Planner. +Balance: 5170.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. + +#### Delete expense: `delete expense` +Deletes an expense from the Financial Planner. + +Format: `delete expense INDEX [/r]` + +- `INDEX` refers to the index number shown in the displayed list when [`list expense`](#list-expense-list-expense) command is used. +- `/r` is used to delete all **future** expenses **only**. + +Example of usage: `delete expense 2 /r` + +Example output: +``` +You have removed future recurrences of this cashflow. +Updated cashflow: +Expense + Type: Insurance + Amount: 800.00 + Description: ntuc income +``` +Example of usage: `delete expense 2` + +Example output: +``` +You have removed an Expense + Type: Insurance + Amount: 800.00 + Description: ntuc income +from the Financial Planner. +Balance: -330.00 +``` +- Note: Balance displayed above is just an example. Your actual balance may differ. + +#### Delete recurring: `delete recurring` +Deletes a recurring cashflow from the Financial Planner. + +Format: `delete recurring INDEX [/r]` + +- `INDEX` refers to the index number shown in the displayed list when [`list recurring`](#list-recurring-list-recurring) command is used. +- `/r` is used to delete all **future** recurring cashflows **only**. + +Example of usage: `delete recurring 2 /r` + +Example output: +``` +You have removed future recurrences of this cashflow. +Updated cashflow: +Expense + Type: Insurance + Amount: 800.00 + Description: ntuc income +``` +Example of usage: `delete recurring 1` + +Example output: +``` +You have removed an Expense + Type: Necessities + Amount: 300.00 + Recurring every: 30 days, starting from: Oct 30 2023 + Description: groceries +from the Financial Planner. +Balance: -830.00 +``` + +- Note: Balance displayed above is just an example. Your actual balance may differ. +- Note: Date displayed above is just an example. Your actual date may differ. + +### Find cashflow: `find` +Finds a cashflow using keywords + +Format: `find ` + +Example of usage: `find buy coffee` + +### Viewing balance: `balance` + +View user's current balance. + +Format: `balance` + +Example of usage: `balance` + +Example output: + +``` +Balance: 3790.00 +``` + +### Budget + +#### Setting a budget: `budget set` + +Sets a monthly budget. + +Format: `budget set /b BUDGET` + +* `BUDGET` has to be a positive number. + +Example of usage: `budget set /b 500` + +Example output: + +``` +A monthly budget of 500.00 has been set. +``` + +#### Updating budget: `budget update` + +Updates initial budget to a new value. Current(remaining) budget will be updated accordingly. If new initial budget is +lower(higher), the new current budget will be lower(higher) by the same amount. + +Format: `budget update /b BUDGET` + +* `Budget` has to be a positive number. +* There has to be an existing budget. + +Example of usage: `budget update /b 1000` + +Example output: + +``` +Budget has been updated: +Old initial budget: 500.00 +Old current budget: 500.00 +New initial budget: 1000.00 +New current budget: 1000.00 +``` + +#### Resetting budget: `budget reset` + +Resets current budget to initial budget if they are different. + +Format: `budget reset` + +* Budget will be reset to initial budget or current balance, whichever is lower. + +Example of usage: `budget reset` + +Example output: + +``` +Budget has been reset to 1000.00. +``` + +#### Deleting budget: `budget delete` + +Deletes existing budget. + +Format: `budget delete` + +Example of usage: `budget delete` + +Example output: + +``` +Budget has been deleted. +``` + +#### Viewing budget: `budget view` + +View existing budget. + +Format: `budget view` + +Example of usage: `budget view` + +Example output: -## Features +``` +You have a remaining budget of 1000.00. +``` -{Give detailed description of each feature} +### Displaying overview: `overview` -### Adding a todo: `todo` -Adds a new item to the list of todo items. +Displays an overview of user's financials. -Format: `todo n/TODO_NAME d/DEADLINE` +Format: `overview` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Example of usage: `overview` -Example of usage: +Example output: -`todo n/Write the rest of the User Guide d/next week` +``` +Here is an overview of your financials: +Total balance: 5450.00 +Highest income: 5000.00 Category: Salary +Highest expense: 50.00 Category: Others +Remaining budget for the month: 50.00 -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +Reminders: +No reminders added yet. + +Wishlist: +No goals added yet. +``` + +### Viewing Watchlist: `watchlist` + +- Note: Stockcode and symbol will be used interchangeably and have the same meaning +- Note: Watchlist feature requires a stable internet connection + +View your current watchlist with stocks that you are interested in with the exchanges shown as well + +Default watchlist: AAPL, GOOGL + +These stocks will be added to the watchlist automatically if: +- your watchlist file is corrupted, and you chose to override it +- your watchlist is empty on startup + +Format: `watchlist` + +Example of usage: `watchlist` + +Example of output: + +![](images/investments/watchlistOutputUG.png) + +- Note: Your watchlist information is saved under the file path `data/watchlist.json` in JSON format +- Note: the watchlist in memory is saved to the file whenever you run `watchlist` command or `exit` command + +Format of watchlist output: + +| Symbol | Market | Price | Daily High | Daily Low | Equity Name | Last Updated | +|--------------------------------------------------------|---------------------------------------|------------------------------------------------|--------------------------------|-------------------------------|------------------------|-------------------------------------------------------------------------| +| Ticker Symbol
(Abbreviation for Company 's Stocks) | Exchange at which the stock is traded | Current latest price of stock (before closing) | Intraday Highest trading price | Intraday Lowest trading price | Name of equity product | Last time at which the information of the stocks was updated by the API | + +- Note: To prevent overloading of the stock API, we will only be making watchlist updates every 10 minutes. +Any request within the 10-minute window will only show the last updated watchlist + +### Adding Stock to Watchlist: `addstock` + +Add a stock that you are interested in monitoring into your personal WatchList + +Format: `addstock /s STOCKCODE` + +- `STOCKCODE` is case-insensitive. + +Example of usage: `addstock /s META` + +Example of output: + +``` +You have successfully added: +Meta Platforms Inc - Class A +Use Watchlist to view it! +``` + +- Note: Due to the free nature of the API (Alpha Vantage and FMP), only US stock prices quote will be provided by +this application. Sorry for the inconvenience caused. +- Note: Due to the free nature of the API, there will be a cap of **five** stocks in the watchlist +- Note: StockCode should not have any spaces + +### Deleting Stock from Watchlist: `deletestock` + +Delete a stock that you are no longer interested in monitoring from your personal WatchList + +Format: `deletestock /s STOCKCODE` + +Example of usage: `deletestock /s META` + +Example of output: + +``` +You have successfully deleted: +Meta Platforms Inc - Class A +Use watchlist command to view updated Watchlist +``` + +- Note: Delete stock command is case-sensitive. Please enter the exact stock code of the stock that you have added. +- Note: StockCode should not have any spaces + +### watchlist.json + +You are able to read the watchlist.json populated by the Financial Planner to see the stock prices even when +the application is not running + +Example file content of watchlist.json: + +![](images/investments/watchlistjsonexample.png) + +**Editing of watchlist.json** + +WARNING: Do not edit the json file unless you are familiar with the format of the JSON file. +Incorrect format of JSON file may lead to: +- Corrupted file (user will be prompted to repair the file if he wants to) +- Deletion of stock entries that are erroneous (Financial Planner has a built-in method to remove +stock entries that does not match the format specified above) +- erroneous entries present in the watchlist + +**Adding stock** + +If you would like to add stock directly using the file, do provide accurate (we do not check for accuracy of information +due to free nature of api) information for only the symbol (in uppercase) and stockName as shown below. +If the format is not followed, the stock might not be loaded to watchlist upon start up. + +![](images/investments/Exampleaddingstockjson.png) + +### View Reminder List: `reminderlist` +View your current reminder list with reminders that you have added. + +Format: `reminderlist` + +Example of usage: `reminderlist` + +Example of output: + +``` +Here is your reminder list: +1. Reminder + Type: debt + Date: Dec 11 2023 + Status: Not Done + Left Days: 28 +2. Reminder + Type: loan + Date: Dec 18 2023 + Status: Not Done + Left Days: 35 +``` + +### Add reminder: `addreminder` +Adds a reminder to the Financial Planner. + +Format: `addreminder /t TYPE /d DATE` +- `/t` is used to specify the reminder type, which describes what is the reminder used for. +- `/d` is used to give a deadline date to the reminder. The date must be in the format of `DD/MM/YYYY`. + +Example of usage: `addreminder /t debt /d 11/12/2023` + +Example output: +``` +You have added Reminder + Type: debt + Date: Dec 11 2023 + Status: Not Done + Left Days: 28 +``` + +### Delete reminder: `deletereminder` +Deletes a reminder from the Financial Planner. + +Format: `deletereminder INDEX` + +- `INDEX` refers to the index number shown in the displayed list when [`reminderlist`](#view-reminder-list-reminderlist) command is used. + +Example of usage: `deletereminder 1` + +Example output: +``` +You have deleted Reminder + Type: debt + Date: Dec 12 2023 + Status: Not Done + Left Days: 29 +``` + +### Mark reminder as done: `markreminder` +Marks a reminder as done in the Financial Planner. + +Format: `markreminder INDEX` + +- `INDEX` refers to the index number shown in the displayed list when [`reminderlist`](#view-reminder-list-reminderlist) command is used. + +Example of usage: `markreminder 1` + +Example output: +``` +You have marked Reminder + Type: debt + Date: Dec 12 2023 + Status: Done + Left Days: 29 +``` + +### View Goal List: `wishlist` +View your current goal list with goals that you have added. + +Format: `wishlist` + +Example of usage: `wishlist` + +Example of output: +``` +Here is your wish list: +1. Goal + Label: car + Amount: 5000 + Status: Not Achieved +2. Goal + Label: ipad + Amount: 2000 + Status: Not Achieved +``` + +### Set goal: `set goal` +Adds a goal to the Financial Planner. + +Format: `set goal /g GOAL /l LABEL` +- `/g` is used to specify the goal amount. +- `/l` is used to give a label to the goal. + +Example of usage: `set goal /g 5000 /l car` + +Example output: +``` +You have added Goal + Label: car + Amount: 5000 + Status: Not Achieved +``` + +### Delete goal: `deletegoal` +Deletes a goal from the Financial Planner. + +Format: `deletegoal INDEX` +- `INDEX` refers to the index number shown in the displayed list when [`wishlist`](#view-goal-list-wishlist) command is used. + +Example of usage: `deletegoal 1` + +Example output: +``` +You have deleted Goal + Label: car + Amount: 5000 + Status: Not Achieved +``` + +### Mark goal as achieved: `markgoal` +Marks a goal as achieved in the Financial Planner. This operation will automatically create a corresponding expense. + +Format: `markgoal INDEX` +- `INDEX` refers to the index number shown in the displayed list when [`wishlist`](#view-goal-list-wishlist) command is used. + +Example of usage: `markgoal 1` + +Example output: +``` +You have achieved Goal + Label: ipad + Amount: 2000 + Status: Achieved +Congratulations! +You have added an Expense + Type: Others + Amount: 2000.00 + Description: ipad +to the Financial Planner. +Balance: -2000.00 +``` + +### Visualizing your cashflow: `vis` + +Using this command to visualize your income or expenses in a pie chart, bar chart or radar chart + +Format: `vis /t TYPE /c TOOL` + +| Type `/t` | +|-----------------------------| +| Income Cashflows `Income` | +| Expense Cashflows `Expense` | + +| Tool `/c` | +|--------------------| +| PieChart `pie` | +| BarChart `bar` | +| RadarChart `radar` | + +Example of usage: `vis /t expense /c pie` + +Example of output: + +``` +Displaying piechart for expense +``` + +![](images/vis/visOutput.png) + +Example of usage: `vis /t income /c bar` + +Example of output: + +``` +Displaying barchart for income +``` + +![](images/vis/barOuput.png) + +Example of usage: `vis /t income /c radar` + +Example of output: + +``` +Displaying radarchart for income +``` + +![](images/vis/radarOutput.png) + +### Exiting the program: `exit` + +Exits the program. + +Format: `exit` + +### Getting command help and example usage: `help` + +Get command help and example usage. Specify the command to find help and example usage for exactly that command. + +Format: `help [COMMAND]` + +Example of usage: `help budget` + +### Saving the data + +Data is automatically saved upon exiting the program using the `exit` command. Closing the program inappropriately +will not save the data. + +### Loading the data + +Existing data will be automatically loaded when the program starts up. ## FAQ **Q**: How do I transfer my data to another computer? -**A**: {your answer here} +**A**: Copy the /data/ folder from the home folder to the other computer. + +**Q**: Should I edit the watchlist.json file? + +**A**: You should not edit the watchlist.json file unless you are very familiar with the format used. +If you would like to edit the watchlist.json file directly to manipulate your watchlist, please follow the instructions +above in the watchlist feature section. However, do note that there is risk of file corruption. + +**Q**: How is the radar chart derived? + +**A**: To obtain the radar chart in our application, the income/expense category with the highest amount is noted. +After which, amounts of all other categories are taken as a ratio of the maximum category. The ratios are then displayed +in the radar chart + +**Q**: Why can't I add Singapore exchange stocks or other exchange stocks using the add stock command? + +**A**: Due to the restrictions of the free API provided, only US-exchange stocks are provided. Sorry for the +inconvenience caused + +**Q**: Why is it saying that API limit is reached, not working or something like that when I use watchlist +features? 🤬 + +**A**: Due to the free nature of the API, there is a restriction in the number of requests allowed in a specific time +window. Sorry for the inconvenience caused. 🥲 ## Command Summary -{Give a 'cheat sheet' of commands here} +| Action | Format | +|----------------------------------|------------------------------------------------------------| +| **Add income** | `add income /a AMOUNT /t TYPE [/r DAYS] [/d DESCRIPTION]` | +| **Add expense** | `add expense /a AMOUNT /t TYPE [/r DAYS] [/d DESCRIPTION]` | +| **Delete cashflow** | `delete INDEX [/r]` | +| **Delete income** | `delete income INDEX [/r]` | +| **Delete expense** | `delete expense INDEX [/r]` | +| **Delete recurrence** | `delete recurring INDEX [/r]` | +| **list all cashflows** | `list` | +| **list all incomes** | `list income` | +| **list all expenses** | `list expense` | +| **list all recurring cashflows** | `list recurring` | +| **Set budget** | `budget set /b BUDGET` | +| **Update budget** | `budget update /b BUDGET` | +| **Reset budget** | `budget reset` | +| **Delete budget** | `budget delete` | +| **View budget** | `budget view` | +| **Display Overview** | `overview` | +| **View balance** | `balance` | +| **View Watchlist** | `watchlist` | +| **Add to watchlist** | `addstock /s STOCKCODE` | +| **Delete from watchlist** | `deletestock /s STOCKCODE` | +| **Visualization** | `vis /t TYPE /c CHART` | +| **Exit program** | `exit` | +| **Add Reminder** | `addreminder /t TYPE /d DATE` | +| **Delete Reminder** | `deletereminder INDEX` | +| **Mark Reminder as Done** | `markreminder INDEX` | +| **Add Goal** | `set goal /g GOAL /l LABEL` | +| **Delete Goal** | `deletegoal INDEX` | +| **Mark Goal as Achieved** | `markgoal INDEX` | +| **List all reminders** | `reminderlist` | +| **List all goals** | `wishlist` | + +- Note: Cashflow is referring to an income or expense + +**Income and Expense types** -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Income | Expense | +|---------------|-----------------| +| `salary` | `dining` | +| `investments` | `entertainment` | +| `allowance` | `shopping` | +| `others` | `travel` | +| | `insurance` | +| | `necessities` | +| | `others` | diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..2934f0e3a3 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,3 @@ +remote_theme: pages-themes/hacker@v0.2.0 +plugins: +- jekyll-remote-theme # add this line to the plugins list if you already have one diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml new file mode 100644 index 0000000000..a40f4a0cd5 --- /dev/null +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -0,0 +1,29 @@ +@startuml +!include +!include style.puml + +package "" as f { +Class Ui #blue +Class Parser #DarkMagenta +Class Model #Crimson +Class Main #LightSkyBlue +Class Storage #Green +Class Command #Grey +} + +Class User #IndianRed +Class "<$documents>" as File #DarkOliveGreen + +User -down-> Ui +Ui --> Parser +Ui <--> Main +Ui <--> Storage +Main --> Storage +Main --> Parser +Main --> Command +Parser --> Command +Command --> Model + +Storage -left-> File + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Budget.puml b/docs/diagrams/Budget.puml new file mode 100644 index 0000000000..0c5f991e5d --- /dev/null +++ b/docs/diagrams/Budget.puml @@ -0,0 +1,35 @@ +@startuml + +autoactivate on + +participant ":BudgetCommand" as BudgetCommand +participant "<>\nBudget" as Budget +participant ":Ui" as Ui + +-> BudgetCommand: execute() + +alt set + BudgetCommand -> Budget: setBudget(budget) + return + BudgetCommand -> Ui: printSetBudget() + return +else update + BudgetCommand -> Ui: printBudgetBeforeUpdate() + return + BudgetCommand -> Budget: updateBudget(budget) + return + BudgetCommand -> Ui: printBudgetAfterUpdate() + return +else delete + ref over BudgetCommand: DeleteBudget +else reset + ref over BudgetCommand: ResetBudget +else view + ref over BudgetCommand: ViewBudget +else invalid command +end + +return + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/MarkGoalSequence.puml b/docs/diagrams/MarkGoalSequence.puml new file mode 100644 index 0000000000..3734785faa --- /dev/null +++ b/docs/diagrams/MarkGoalSequence.puml @@ -0,0 +1,38 @@ +@startuml + +participant ":MarkGoalCommand" as MarkGoalCommand +participant ":WishList" as WishList +participant ":Goal" as Goal +participant ":Ui" as Ui +participant ":CashflowList" as CashflowList +participant ":Expense" as Expense + +-> MarkGoalCommand: execute() +activate MarkGoalCommand +MarkGoalCommand -> WishList: markGoal(index) +activate WishList +WishList -> Goal: markAsDone() +activate Goal +return +WishList -> Ui: showMessage(AddGoalMessage) +WishList -> Goal: getAmount() +activate Goal +return amount +WishList -> Goal: getLabel() +activate Goal +return label +WishList -> CashflowList: addExpense(amount, ExpenseType, label) +activate CashflowList +create Expense +CashflowList -> Expense: Expense(amount, ExpenseType, label) +activate Expense +Expense -> Expense: addExpenseValue() +return +CashflowList -> CashflowList: addToList(toAdd) +CashflowList -> Ui: printAddedCashflow(toAdd) +return +return +return + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/Storage.puml b/docs/diagrams/Storage.puml new file mode 100644 index 0000000000..073899ee00 --- /dev/null +++ b/docs/diagrams/Storage.puml @@ -0,0 +1,40 @@ +@startuml +'https://plantuml.com/class-diagram +!include style.puml +skinparam ClassBackgroundColor STORAGE_COLOR + +package Storage as StoragePackage { +Class Storage +Class "{abstract}\nLoadData" as LoadData +Class "{abstract}\nSaveData" as SaveData +Class "{abstract}\nBudget" as Budget +Class "{abstract}\nCashflow" as Cashflow +Class "<>\nExpenseType" as ExpenseType +Class "<>\nIncomeType" as IncomeType +Class Ui +Class Lists +} + +note "Cashflow refers to income and expense" as N1 +N1 -up-> Cashflow + +note "Lists refer to CashflowList, ReminderList, WishList" as N2 +N2 -up-> Lists + +FinancialPlanner --> Storage + +Storage .right.-> LoadData: uses > +Storage .left.> SaveData: uses > +SaveData ..> Budget +SaveData ..> Cashflow +SaveData --> Lists + +LoadData --> Lists +LoadData ..> Budget +LoadData ..> ExpenseType +LoadData ..> IncomeType +LoadData ..> Cashflow +LoadData -right-> Ui: prints message > + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/cashflow/AddCashflowSequence.puml b/docs/diagrams/cashflow/AddCashflowSequence.puml new file mode 100644 index 0000000000..51edcba9b4 --- /dev/null +++ b/docs/diagrams/cashflow/AddCashflowSequence.puml @@ -0,0 +1,51 @@ +@startuml + +participant ":AddCashflowCommand" as AddCashflowCommand +participant ":CashflowList" as CashflowList +participant ":Income" as Income +participant ":Expense" as Expense +participant ":Ui" as Ui + +-> AddCashflowCommand: execute() +activate AddCashflowCommand +alt income + AddCashflowCommand -> CashflowList: addIncome(amount, incomeType, recur, description) + activate CashflowList + create Income + CashflowList -> Income: Income(value, type, recur, description) + activate Income + Income -> Income: addIncomeValue() + activate Income + return + + return + CashflowList -> CashflowList: addToList(toAdd) + activate CashflowList + return + CashflowList -> Ui: printAddedCashflow(toAdd) + activate Ui + return + return + +else expense + AddCashflowCommand -> CashflowList: addExpense(amount, incomeType, recur, description) + activate CashflowList + create Expense + CashflowList -> Expense: Expense(value, type, recur, description) + activate Expense + Expense -> Expense: addExpenseValue() + activate Expense + return + + return + CashflowList -> CashflowList: addToList(toAdd) + activate CashflowList + return + CashflowList -> Ui: printAddedCashflow(toAdd) + activate Ui + return + return +end +return +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/cashflow/AddRecurringSequence.puml b/docs/diagrams/cashflow/AddRecurringSequence.puml new file mode 100644 index 0000000000..5a8ee0c968 --- /dev/null +++ b/docs/diagrams/cashflow/AddRecurringSequence.puml @@ -0,0 +1,40 @@ +@startuml +mainframe sd add recurring cashflows to templist + +participant "<>\nLoadData" as LoadData +participant ":Income" as Income +participant ":Expense" as Expense + +loop current date is after or equals next recurring date + LoadData -> Cashflow: setHasRecurred(true) + activate Cashflow + return + +alt income + create Income + LoadData -> Income: Income((Income) cashflow) + activate Income + return + LoadData -> Income: setDate(dateOfAddition) + activate Income + return + LoadData -> LoadData: addToTempList(tempCashflowList, toAdd) + activate LoadData + return + +else expense + create Expense + LoadData -> Expense: Expense((Expense) cashflow) + activate Expense + return + LoadData -> Expense: setDate(dateOfAddition) + activate Expense + return + LoadData -> LoadData: addToTempList(tempCashflowList, toAdd) + activate LoadData + return +end + +end +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/cashflow/CashflowClassDiagram.puml b/docs/diagrams/cashflow/CashflowClassDiagram.puml new file mode 100644 index 0000000000..f3f3fbf033 --- /dev/null +++ b/docs/diagrams/cashflow/CashflowClassDiagram.puml @@ -0,0 +1,71 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classFontColor automatic + +Class "{abstract}\nCashflow" as Cashflow #MistyRose { + #balance: double = 0 + #amount: double + #recur: int + #description: String = null +} + +Class Expense #MistyRose extends Cashflow { + #type: ExpenseType + +Expense(amount: double, type: ExpenseType, recur: int, description: String) + -addExpenseValue() +} + +Class Income #MistyRose extends Cashflow { + +Income(amount: double, type: IncomeType, recur: int, description: String) + #type: IncomeType + -addIncomeValue() +} + +Class Ui #Cornsilk { + +printAddedCashflow(entry: Cashflow) +} + +Class CashflowList #Cornsilk { + +addIncome(amount: double, incometype: IncomeType, recur: int, description: String) + +addExpense(amount: double, expensetype: ExpenseType, recur: int, description: String) + -addToList(toAdd: Cashflow) +} + +Class AddCashflowCommand #HoneyDew { + #amount: double + #incomeType: IncomeType + #expenseType: ExpenseType + #recur: int + #description: String = null + +execute() +} + +enum "<>\nExpenseType" as ExpenseType #MintCream { + DINING + ENTERTAINMENT + SHOPPING + TRAVEL + INSURANCE + NECESSITIES + OTHERS +} + +enum "<>\nIncomeType" as IncomeType #MintCream{ + SALARY + INVESTMENTS + ALLOWANCE + OTHERS +} + +CashflowList -right-> "1" Ui +AddCashflowCommand --> "1" CashflowList +CashflowList --> "*" Cashflow +CashflowList ..> Income +CashflowList ..> Expense + +hide ExpenseType methods +hide IncomeType methods +hide Circle +skinparam classAttributeIconSize 0 + +@enduml \ No newline at end of file diff --git a/docs/diagrams/cashflow/RecurClassDiagram.puml b/docs/diagrams/cashflow/RecurClassDiagram.puml new file mode 100644 index 0000000000..367886c631 --- /dev/null +++ b/docs/diagrams/cashflow/RecurClassDiagram.puml @@ -0,0 +1,46 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classFontColor automatic + +Class "{abstract}\nLoadData" as LoadData #HoneyDew { + -addRecurringCashflows(currentDate: LocalDate) + -identifyRecurringCashflows() + -addRecurringCashflowToTempList() + -addToTempList(tempCashflowList: ArrayList, toAdd: Cashflow) + +} + +Class "{abstract}\nCashflow" as Cashflow #MistyRose { + #recur: int + #date: LocalDate; + #hasRecurred: boolean; + +setHasRecurred(hasRecurred: boolean) + +setDate(date: LocalDate) +} + +Class Expense #MistyRose extends Cashflow{ + +Expense(expense: Expense) +} + +Class Income #MistyRose extends Cashflow{ + +Income(income: Income) +} + +Class Ui #Cornsilk { + +printAddedCashflow(entry: Cashflow) +} + +Class CashflowList #Cornsilk { + +load(entry: Cashflow) +} + +LoadData -up-> "1" CashflowList +LoadData -up-> "1" Ui +LoadData .up.> Income +LoadData .up.> Expense +LoadData .up.> Cashflow +CashflowList -up-> "*" Cashflow + +hide Circle +skinparam classAttributeIconSize 0 +@enduml \ No newline at end of file diff --git a/docs/diagrams/cashflow/RecurSequence.puml b/docs/diagrams/cashflow/RecurSequence.puml new file mode 100644 index 0000000000..d8fed6561b --- /dev/null +++ b/docs/diagrams/cashflow/RecurSequence.puml @@ -0,0 +1,41 @@ +@startuml + +participant ":LoadData" as LoadData +participant ":Cashflow" as Cashflow +participant ":Income" as Income +participant ":Expense" as Expense +participant ":CashflowList" as CashflowList +participant ":Ui" as Ui + +LoadData -> LoadData: addRecurringCashflows(date); +activate LoadData +loop for each cashflow in cashflowList + LoadData -> LoadData: identifyRecurringCashflows() + activate LoadData +opt is Recurring and has not Recurred + LoadData -> LoadData: addRecurringCashflowToTempList() + activate LoadData +ref over LoadData, Expense: add recurring cashflows to templist +return + +return +end + +end +loop for each cashflow in tempCashflowList + LoadData -> CashflowList: load(cashflow) + activate CashflowList + return + LoadData -> Ui: printAddedCashflowWithoutBalance(cashflow) + activate Ui + return +end + +opt tempCashflowList is not empty + LoadData -> Ui: printBalance() + activate Ui + return +end +return +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/deleteBudget.puml b/docs/diagrams/deleteBudget.puml new file mode 100644 index 0000000000..4a8eb03a13 --- /dev/null +++ b/docs/diagrams/deleteBudget.puml @@ -0,0 +1,16 @@ +@startuml + +mainframe sd DeleteBudget +participant ":BudgetCommand" as BudgetCommand +participant "<>\nBudget" as Budget +participant ":Ui" as Ui + +alt hasBudget + BudgetCommand -> Budget: deleteBudget() + BudgetCommand -> Ui: printDeleteBudget() +else else + BudgetCommand -> Ui: printBudgetError("delete") +end + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/investments/watchlistClassDiagram.puml b/docs/diagrams/investments/watchlistClassDiagram.puml new file mode 100644 index 0000000000..72254b5a3b --- /dev/null +++ b/docs/diagrams/investments/watchlistClassDiagram.puml @@ -0,0 +1,40 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classFontColor automatic + +Class Ui #Cornsilk { + +printStocksInfo(watchlist: WatchList) +} + +Class WatchList #Cornsilk { + +getLatestWatchlistInfo() + +getExpiredStocks() : StringBuilder + +fetchFMPStockPrices(queryStocks : StringBuilder) + +extractWatchlistInfoFromJSONArray(jsonStocks: JSONArray) +} + +Class WatchListCommand #HoneyDew { + +execute() +} + +Class SaveData #LightPink { + +saveWatchList() +} + +Class Stock #LightBlue { + +getStockNameFromAPI(symbol: String) + +setHashCode() + +checkHashCode() +} + +WatchListCommand .right.> SaveData +WatchListCommand -right-> "1" Ui +WatchListCommand --> "1" WatchList +WatchList --> "0..5" Stock + +note right: attributes are shown in DG + +hide Circle +skinparam classAttributeIconSize 0 + +@enduml \ No newline at end of file diff --git a/docs/diagrams/investments/watchlistSequence.puml b/docs/diagrams/investments/watchlistSequence.puml new file mode 100644 index 0000000000..e8b79b0309 --- /dev/null +++ b/docs/diagrams/investments/watchlistSequence.puml @@ -0,0 +1,29 @@ +@startuml + +participant ":WatchlistCommand" as WatchListCommand +participant ":WatchList" as WatchList +participant ":Ui" as Ui +participant "<>\nSaveData" as SaveData + +-> WatchListCommand: execute() +activate WatchListCommand + WatchListCommand -> WatchList: getLatestWatchlistInfo() + activate WatchList + WatchList -> WatchList : getExpiredStocks() + activate WatchList + return queryStocks + WatchList -> WatchList : fetchFMPStockPrices() + activate WatchList + return + return + WatchListCommand -> Ui: printStocksInfo(watchlist) + activate Ui + return + WatchListCommand -> SaveData: saveWatchList() + activate SaveData + return + +return + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/resetBudget.puml b/docs/diagrams/resetBudget.puml new file mode 100644 index 0000000000..8c9e161d63 --- /dev/null +++ b/docs/diagrams/resetBudget.puml @@ -0,0 +1,22 @@ +@startuml + +mainframe sd ResetBudget +participant ":BudgetCommand" as BudgetCommand +participant "<>\nBudget" as Budget +participant ":Ui" as Ui + +alt spentBudget + opt initialBudgetExceedBalance + BudgetCommand -> Budget: setInitialBudget(balance) + BudgetCommand -> Ui: printBudgetExceedBalance() + end + BudgetCommand -> Budget: resetBudget() + BudgetCommand -> Ui: printResetBudget() +else Does not have Budget + BudgetCommand -> Ui: printBudgetError("delete") +else else + BudgetCommand -> Ui: printBudgetError("reset") +end + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml new file mode 100644 index 0000000000..ead497a169 --- /dev/null +++ b/docs/diagrams/style.puml @@ -0,0 +1,79 @@ +/' + 'Commonly used styles and colors across diagrams. + 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more + 'comprehensive list of skinparams. + '/ + + +'T1 through T4 are shades of the original color from lightest to darkest + +!define UI_COLOR #1D8900 +!define UI_COLOR_T1 #83E769 +!define UI_COLOR_T2 #3FC71B +!define UI_COLOR_T3 #166800 +!define UI_COLOR_T4 #0E4100 + +!define LOGIC_COLOR #3333C4 +!define LOGIC_COLOR_T1 #C8C8FA +!define LOGIC_COLOR_T2 #6A6ADC +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +!define MODEL_COLOR #9D0012 +!define MODEL_COLOR_T1 #F97181 +!define MODEL_COLOR_T2 #E41F36 +!define MODEL_COLOR_T3 #7B000E +!define MODEL_COLOR_T4 #51000A + +!define STORAGE_COLOR #A38300 +!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR_T2 #EDC520 +!define STORAGE_COLOR_T3 #806600 +!define STORAGE_COLOR_T2 #544400 + +!define USER_COLOR #000000 + +skinparam Package { + BackgroundColor #FFFFFF + BorderThickness 1 + FontSize 16 +} + +skinparam Class { + FontColor #FFFFFF + FontSize 15 + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #FFFFFF + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFFF + Padding 20 +} + +skinparam ArrowFontStyle bold +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +hide footbox +hide members +hide circle \ No newline at end of file diff --git a/docs/diagrams/viewBudget.puml b/docs/diagrams/viewBudget.puml new file mode 100644 index 0000000000..02c0d77042 --- /dev/null +++ b/docs/diagrams/viewBudget.puml @@ -0,0 +1,15 @@ +@startuml + +mainframe sd ViewBudget +participant ":BudgetCommand" as BudgetCommand +participant "<>\nBudget" as Budget +participant ":Ui" as Ui + +alt hasBudget + BudgetCommand -> Ui: printBudget() +else else + BudgetCommand -> Ui: printBudgetError("view") +end + +hide footbox +@enduml \ No newline at end of file diff --git a/docs/diagrams/vis/categorizerSequence.puml b/docs/diagrams/vis/categorizerSequence.puml new file mode 100644 index 0000000000..d9ba69154f --- /dev/null +++ b/docs/diagrams/vis/categorizerSequence.puml @@ -0,0 +1,26 @@ +@startuml + +mainframe sd sort cashflow entries + +autoactivate on + +participant ":VisCommand" +participant "<>\nCategorizer" + +activate ":VisCommand" + +":VisCommand"-> "<>\nCategorizer": sortType(cashflowList, type) + +alt "expense" + "<>\nCategorizer" -> "<>\nCategorizer": sortExpenses(cashflowList) + return expenseByCat +else "income" + "<>\nCategorizer" -> "<>\nCategorizer": sortIncome(cashflowList) + return incomeByCat +end + +return sortedCashflow: Map + +hide footbox + +@enduml \ No newline at end of file diff --git a/docs/diagrams/vis/visualisationClass.puml b/docs/diagrams/vis/visualisationClass.puml new file mode 100644 index 0000000000..cea4a2f36c --- /dev/null +++ b/docs/diagrams/vis/visualisationClass.puml @@ -0,0 +1,43 @@ +@startuml +'https://plantuml.com/class-diagram + +skinparam classFontColor automatic + +class VisCommand #MistyRose { +-type: String +-chart: String ++execute() +} + +class RawCommand #Cornsilk { +-args: List +-extraArgs: Map +#commandName: String +} + +class Categorizer #HoneyDew { ++sortType(cashflowList: CashflowList, type: String) ++sortExpenses(cashflowList: CashflowList) ++sortIncome(cashflowList: CashflowList) +} + +class Visualizer #Beige { ++displayChart(chart: String, cashflowByCat: HashMap, type: String) ++ displayPieChart(cashflowByCat: HashMap, type: String) ++ displayBarChart(cashflowByCat: HashMap, type: String) ++ displayRadarChart (cashflowByCat: HashMap, type: String) +} + +class "{abstract}\nCommand" #MintCream { ++execute() {abstract} +} + +"{abstract}\nCommand" <|-- VisCommand +RawCommand <.. VisCommand +Categorizer <.. VisCommand +Visualizer <.. VisCommand + +hide Circle +skinparam classAttributeIconSize 0 + +@enduml \ No newline at end of file diff --git a/docs/diagrams/vis/visualisationSequence.puml b/docs/diagrams/vis/visualisationSequence.puml new file mode 100644 index 0000000000..7137320992 --- /dev/null +++ b/docs/diagrams/vis/visualisationSequence.puml @@ -0,0 +1,26 @@ +@startuml + +participant ":VisCommand" +participant ":Ui" +participant "<>\nCategorizer" +participant "<>\nVisualizer" + +-> ":VisCommand": execute() +":VisCommand"-> ":Ui": printDisplayChartMessages(type) + +ref over "<>\nCategorizer", ":VisCommand" : sort cashflow entries + +alt #Pink cashflowbyType is empty + + ":VisCommand" -> ":Ui" : ui.printEmptyCashflow(type) + +else #LightBlue cashflowbyType is not empty + + ref over "<>\nVisualizer", ":VisCommand": displaying chart + +end + + +hide footbox + +@enduml \ No newline at end of file diff --git a/docs/diagrams/vis/visualizerSequence.puml b/docs/diagrams/vis/visualizerSequence.puml new file mode 100644 index 0000000000..2b4a01feb7 --- /dev/null +++ b/docs/diagrams/vis/visualizerSequence.puml @@ -0,0 +1,20 @@ +@startuml + +mainframe sd displaying chart + +participant ":VisCommand" +participant "<>\nVisualizer" + +":VisCommand"-> "<>\nVisualizer": displayChart(chart, sortedCashFlow, type) + +alt "pie" + "<>\nVisualizer" -> "<>\nVisualizer": displayPieChart(cashflowList, type) +else "bar" + "<>\nVisualizer" -> "<>\nVisualizer": displayBarChart(cashflowList, type) +else "radar" + "<>\nVisualizer" -> "<>\nVisualizer": displayRadarChart(cashflowList, type) +end + +hide footbox + +@enduml \ No newline at end of file diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png new file mode 100644 index 0000000000..7ab8365d79 Binary files /dev/null and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/Budget.png b/docs/images/Budget.png new file mode 100644 index 0000000000..a57a916f16 Binary files /dev/null and b/docs/images/Budget.png differ diff --git a/docs/images/MarkGoalSequence.png b/docs/images/MarkGoalSequence.png new file mode 100644 index 0000000000..613186e41d Binary files /dev/null and b/docs/images/MarkGoalSequence.png differ diff --git a/docs/images/Storage.png b/docs/images/Storage.png new file mode 100644 index 0000000000..f5c1901f13 Binary files /dev/null and b/docs/images/Storage.png differ diff --git a/docs/images/cashflow/AddCashflow.png b/docs/images/cashflow/AddCashflow.png new file mode 100644 index 0000000000..8b780eeeaa Binary files /dev/null and b/docs/images/cashflow/AddCashflow.png differ diff --git a/docs/images/cashflow/AddCashflowSequence.png b/docs/images/cashflow/AddCashflowSequence.png new file mode 100644 index 0000000000..1052417aa6 Binary files /dev/null and b/docs/images/cashflow/AddCashflowSequence.png differ diff --git a/docs/images/cashflow/AddRecurringSequence.png b/docs/images/cashflow/AddRecurringSequence.png new file mode 100644 index 0000000000..92d9a0cd92 Binary files /dev/null and b/docs/images/cashflow/AddRecurringSequence.png differ diff --git a/docs/images/cashflow/CashflowClassDiagram.png b/docs/images/cashflow/CashflowClassDiagram.png new file mode 100644 index 0000000000..3972b7b968 Binary files /dev/null and b/docs/images/cashflow/CashflowClassDiagram.png differ diff --git a/docs/images/cashflow/List.png b/docs/images/cashflow/List.png new file mode 100644 index 0000000000..588e4d0a37 Binary files /dev/null and b/docs/images/cashflow/List.png differ diff --git a/docs/images/cashflow/ListDeleteRecurring.png b/docs/images/cashflow/ListDeleteRecurring.png new file mode 100644 index 0000000000..68f6ad49a0 Binary files /dev/null and b/docs/images/cashflow/ListDeleteRecurring.png differ diff --git a/docs/images/cashflow/ListExpense.png b/docs/images/cashflow/ListExpense.png new file mode 100644 index 0000000000..8b09a3ab15 Binary files /dev/null and b/docs/images/cashflow/ListExpense.png differ diff --git a/docs/images/cashflow/ListIncome.png b/docs/images/cashflow/ListIncome.png new file mode 100644 index 0000000000..9e6d542601 Binary files /dev/null and b/docs/images/cashflow/ListIncome.png differ diff --git a/docs/images/cashflow/ListRecurring.png b/docs/images/cashflow/ListRecurring.png new file mode 100644 index 0000000000..1ecb7d3b8c Binary files /dev/null and b/docs/images/cashflow/ListRecurring.png differ diff --git a/docs/images/cashflow/RecurClassDiagram.png b/docs/images/cashflow/RecurClassDiagram.png new file mode 100644 index 0000000000..e1c249e3dd Binary files /dev/null and b/docs/images/cashflow/RecurClassDiagram.png differ diff --git a/docs/images/cashflow/RecurSequence.png b/docs/images/cashflow/RecurSequence.png new file mode 100644 index 0000000000..8c8704fda5 Binary files /dev/null and b/docs/images/cashflow/RecurSequence.png differ diff --git a/docs/images/cashflow/RecurringIncome.png b/docs/images/cashflow/RecurringIncome.png new file mode 100644 index 0000000000..60bfd997ed Binary files /dev/null and b/docs/images/cashflow/RecurringIncome.png differ diff --git a/docs/images/deleteBudget.png b/docs/images/deleteBudget.png new file mode 100644 index 0000000000..5857625618 Binary files /dev/null and b/docs/images/deleteBudget.png differ diff --git a/docs/images/investments/Exampleaddingstockjson.png b/docs/images/investments/Exampleaddingstockjson.png new file mode 100644 index 0000000000..f18a928f74 Binary files /dev/null and b/docs/images/investments/Exampleaddingstockjson.png differ diff --git a/docs/images/investments/watchlistClassDiagram.png b/docs/images/investments/watchlistClassDiagram.png new file mode 100644 index 0000000000..420bc93f5b Binary files /dev/null and b/docs/images/investments/watchlistClassDiagram.png differ diff --git a/docs/images/investments/watchlistMT1.png b/docs/images/investments/watchlistMT1.png new file mode 100644 index 0000000000..23469de278 Binary files /dev/null and b/docs/images/investments/watchlistMT1.png differ diff --git a/docs/images/investments/watchlistMT2.png b/docs/images/investments/watchlistMT2.png new file mode 100644 index 0000000000..a49b08536b Binary files /dev/null and b/docs/images/investments/watchlistMT2.png differ diff --git a/docs/images/investments/watchlistMT3.png b/docs/images/investments/watchlistMT3.png new file mode 100644 index 0000000000..ecd344381c Binary files /dev/null and b/docs/images/investments/watchlistMT3.png differ diff --git a/docs/images/investments/watchlistOutput.png b/docs/images/investments/watchlistOutput.png new file mode 100644 index 0000000000..5c92c9072e Binary files /dev/null and b/docs/images/investments/watchlistOutput.png differ diff --git a/docs/images/investments/watchlistOutputUG.png b/docs/images/investments/watchlistOutputUG.png new file mode 100644 index 0000000000..103984cca8 Binary files /dev/null and b/docs/images/investments/watchlistOutputUG.png differ diff --git a/docs/images/investments/watchlistSequence.png b/docs/images/investments/watchlistSequence.png new file mode 100644 index 0000000000..c1f961b6b5 Binary files /dev/null and b/docs/images/investments/watchlistSequence.png differ diff --git a/docs/images/investments/watchlistjsonexample.png b/docs/images/investments/watchlistjsonexample.png new file mode 100644 index 0000000000..02a48beffa Binary files /dev/null and b/docs/images/investments/watchlistjsonexample.png differ diff --git a/docs/images/resetBudget.png b/docs/images/resetBudget.png new file mode 100644 index 0000000000..dbcbe447bd Binary files /dev/null and b/docs/images/resetBudget.png differ diff --git a/docs/images/viewBudget.png b/docs/images/viewBudget.png new file mode 100644 index 0000000000..fe7179a4ec Binary files /dev/null and b/docs/images/viewBudget.png differ diff --git a/docs/images/vis/barOuput.png b/docs/images/vis/barOuput.png new file mode 100644 index 0000000000..7993e2bb6a Binary files /dev/null and b/docs/images/vis/barOuput.png differ diff --git a/docs/images/vis/categorizerSequence.png b/docs/images/vis/categorizerSequence.png new file mode 100644 index 0000000000..f0ca65a100 Binary files /dev/null and b/docs/images/vis/categorizerSequence.png differ diff --git a/docs/images/vis/radarOutput.png b/docs/images/vis/radarOutput.png new file mode 100644 index 0000000000..95577b74d7 Binary files /dev/null and b/docs/images/vis/radarOutput.png differ diff --git a/docs/images/vis/visOutput.png b/docs/images/vis/visOutput.png new file mode 100644 index 0000000000..55c0670818 Binary files /dev/null and b/docs/images/vis/visOutput.png differ diff --git a/docs/images/vis/visualisationClass.png b/docs/images/vis/visualisationClass.png new file mode 100644 index 0000000000..ad6224effc Binary files /dev/null and b/docs/images/vis/visualisationClass.png differ diff --git a/docs/images/vis/visualisationSequence.png b/docs/images/vis/visualisationSequence.png new file mode 100644 index 0000000000..c687d3cbf2 Binary files /dev/null and b/docs/images/vis/visualisationSequence.png differ diff --git a/docs/images/vis/visualizerSequence.png b/docs/images/vis/visualizerSequence.png new file mode 100644 index 0000000000..39ba05481a Binary files /dev/null and b/docs/images/vis/visualizerSequence.png differ diff --git a/docs/team/hshiah.md b/docs/team/hshiah.md new file mode 100644 index 0000000000..6785e074c5 --- /dev/null +++ b/docs/team/hshiah.md @@ -0,0 +1,49 @@ +# Shi, Haochen - Project Portfolio Page + +## Overview + +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. + +## Summary of Contributions + +### Code contributed: [Reposense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=neominwei&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&since=2023-09-22) + +### Enhancements Implemented: + +* Add Reminder feature +* List Reminder feature +* Delete Reminder feature +* Mark Reminder as Done feature +* Find Cashflow feature + * Ability to find a cashflow based on its description. +* Set Goal feature +* List Goal feature +* Delete Goal feature +* Mark Goal as Achieved feature + +### Contributions to the UG: + +* Feature guide for reminder related feature +* Feature guide for find command feature +* Feature guide for goal related feature + +### Contributions to the DG + +* Mark Goal Feature + * Implementation + * Sequence Diagram + +### Contributions to team-based tasks: + +* Approving pull requests by team members + +### Review/ Mentoring contributions: + +* Reviewing of teammates codes in Pull Requests (giving suggestions and improvements) + +### Contributions beyond the project team + +* Reviewed UG & DG of other teams +* Product system and acceptance testing for other teams \ 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/neominwei.md b/docs/team/neominwei.md new file mode 100644 index 0000000000..d7534cdc94 --- /dev/null +++ b/docs/team/neominwei.md @@ -0,0 +1,57 @@ +# Neo Min Wei - Project Portfolio Page + +## Overview + +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. + +## Summary of Contributions + +### Code contributed: [Reposense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=neominwei&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&since=2023-09-22) + +### Enhancements Implemented: + +* Add Cashflow Feature + * Keep track of total balance + * Ability to set a recurring cashflow +* Delete Cashflow Feature + * Ability to delete a cashflow based on its respective index in overall list, income list, expense list or recur list. + * Ability to delete the recurrence portion of a cashflow, making it a one-time entry only. +* Recurring Cashflow Feature + * Ability to automatically add cashflows based on the system date. +* List Recurring Cashflows Feature + +### Contributions to the UG: + +* Feature guide for add and delete cashflow feature +* Provide quick start guide +* Command summary for add, delete and listing of cashflow + +### Contributions to the DG + +* Acknowledgements of reused code found in add/delete cashflow feature +* Add/Delete Cashflow Feature + * Implementation + * Class Diagram + * Sequence Diagram +* Recurring Cashflow Feature + * Implementation + * Class Diagram + * Sequence Diagram + +### Contributions to team-based tasks: + +* Submission of first draft of UG +* Creating of demo video +* Creating and assigning relevant issues in the issue tracker to teammates +* Approving pull requests by team members + +### Review/ Mentoring contributions: + +* Reviewing of teammates codes in Pull Requests (giving suggestions and improvements) + +### Contributions beyond the project team + +* Reviewed UG & DG of other teams +* Product system and acceptance testing for other teams diff --git a/docs/team/ryan1604.md b/docs/team/ryan1604.md new file mode 100644 index 0000000000..059c9b42f5 --- /dev/null +++ b/docs/team/ryan1604.md @@ -0,0 +1,55 @@ +# Ryan Chua - Project Portfolio Page + +## Overview + +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. + +## Summary of contributions + +### Code contributed: [RepoSense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=ryan1604&breakdown=true) + +### Enhancements implemented: + +* General structure of the code (parts of FinancialPlanner, Storage, Parser and Ui) +* Budget feature +* Overview feature +* Balance feature +* General code enhancements and improvements + +### Contributions to the UG: + +* Feature guide for budget, overview, balance, and exiting program +* Saving and loading data components +* Introduction +* Command summary for my features +* Part of FAQ + +### Contributions to the DG: + +* Storage component: + * Implementation + * Class diagram + * Design considerations +* Budget feature + * Implementation + * Sequence diagrams +* Non-functional requirements +* Architecture Diagram + +### Contributions to team-based tasks: + +* Set up GitHub team org/repo +* Manage releases +* Equal share of workflow processes on GitHub (review PRs, maintain issue tracker) +* Submit deliverables on Canvas + +### Review/Mentoring contributions: + +* Reviewed teammates code (giving suggestions and improvements) + +### Contributions beyond the project team: + +* Reviewed UG & DG of other teams. +* Product system and acceptance testing for other teams. \ No newline at end of file diff --git a/docs/team/wwweert123.md b/docs/team/wwweert123.md new file mode 100644 index 0000000000..745a2e7da4 --- /dev/null +++ b/docs/team/wwweert123.md @@ -0,0 +1,62 @@ +# Frederick Pua - Project Portfolio Page + +# Overview + +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. +It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives +you a one-stop interface to access a plethora of features to manage your finances. + +## Summary of contributions + +### Code contributed: [Reposense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=wwweert123&breakdown=true) + +### Enhancements Implemented: + +* Watchlist Feature + * Querying Stock API (Alpha Vantage and FMP) using HTTP Client + * Parsing HTTP JSON response into JAVA object + * Displaying of watchlist + * Adding and deleting stock from watchlist + * Saving and loading of watchlist in file using human-readable format (JSON) +* Visualization Feature + * Sorting of Cashflow into respective categories + * Visualizing cashflow using appropriate visualization tools (Piechart, Barchart, Radarchart) +* Utility classes (Part of storage and UI) + +### Contributions to the UG: + +* Feature guide for watchlist and visualization feature +* Furnish the Q&A with relevant information and doubts that the user might have +* Command summary for my features + +### Contributions to the DG + +* Acknowledgements of relevant 3-rd Party libraries used in watchlist and visualization feature +* Visualization Component + * Implementation + * Class Diagram + * Sequence Diagrams +* Watchlist Component + * Implementation + * Class Diagram + * Sequence Diagram +* Value Proposition +* User Profile +* USer Stories +* Manual Testing + +### Contributions to team-based tasks: + +* Adding of dependencies to build.gradle +* Checking of issue tracker to ensure all group members are on task and does not miss deadline +* Approving pull request by team members + +### Review/ Mentoring contributions: + +* Reviewing of teammates codes in Pull Requests (giving suggestions and improvements) + +### Contributions beyond the project team + +* Reviewed UG & DG of other teams +* Product system and acceptance testing for other teams + diff --git a/docs/team/yfshadaow.md b/docs/team/yfshadaow.md new file mode 100644 index 0000000000..5a5b14f867 --- /dev/null +++ b/docs/team/yfshadaow.md @@ -0,0 +1,40 @@ +# Ren Zhengdao - Project Portfolio Page + +## Overview + +Financial Planner is a Command Line Interface (CLI) application for managing your finances conveniently. It is optimized for use via the CLI and leverages your expertise in CLI and your ability to type fast and gives you a one-stop interface to access a plethora of features to manage your finances. + +## Summary of contributions + +### Code contributed: [RepoSense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=YFshadaow&breakdown=true) + +### Enhancements implemented: + +* Refactor of code during multiple phases +* Switch from normal class structure to singleton to reduce long methods +* Command manager feature to auto-scan for command classes +* Introduction of RawCommand class to separate command parse and validation +* Separation of command usages/example usages into individual classes +* Use of reflections to further optimize and clean code +* General code enhancements and improvements + +### Contributions to the UG: + +* Guide for help/list/find commands +* Ensure consistency of UG with help command + +### Contributions to the DG: + +### Contributions to team-based tasks: + +* Review and approve teammates' PRs +* Give suggestions to teammates' code +* Fix bugs related to testing/logging + +### Review/Mentoring contributions: + +* Reviewed teammates code (giving suggestions and improvements) + +### Contributions beyond the project team: + +* Reviewed UG & DG of other teams. \ No newline at end of file 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/main/java/seedu/financialplanner/FinancialPlanner.java b/src/main/java/seedu/financialplanner/FinancialPlanner.java new file mode 100644 index 0000000000..bb1790a008 --- /dev/null +++ b/src/main/java/seedu/financialplanner/FinancialPlanner.java @@ -0,0 +1,78 @@ +package seedu.financialplanner; + +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.ExitCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.storage.Storage; +import seedu.financialplanner.utils.Parser; +import seedu.financialplanner.utils.Ui; + +import java.time.LocalDate; + +/** + * Represents the main class for the Financial Planner. + * It manages commands and user interactions. + */ +public class FinancialPlanner { + private static final LocalDate date = LocalDate.now(); + private static final String FILE_PATH = "data/data.txt"; + private final Storage storage = Storage.getInstance(); + private final Ui ui = Ui.getInstance(); + private final WatchList watchList = WatchList.getInstance(); + private final CashflowList cashflowList = CashflowList.getInstance(); + + private FinancialPlanner() { + } + + /** + * The main starting point of the Financial Planner. + * + * @param args Unused. + */ + public static void main(String[] args) { + FinancialPlannerLogger.initialise(); + new FinancialPlanner().run(); + } + + /** + * Loads storage from save file and starts the Financial Planner. + * Saves the storage to save file upon exit. + */ + public void run() { + try { + storage.load(FILE_PATH, date); + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + return; + } + + ui.welcomeMessage(); + String input; + Command command = null; + + while (!(command instanceof ExitCommand)) { + input = ui.userInput(); + try { + command = Parser.parseCommand(input); + command.execute(); + } catch (Exception e) { + ui.showMessage(e.getMessage()); + } + } + save(); + ui.exitMessage(); + } + + /** + * Saves existing data to the save file. + */ + public void save() { + try { + storage.save(FILE_PATH); + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/financialplanner/FinancialPlannerLogger.java b/src/main/java/seedu/financialplanner/FinancialPlannerLogger.java new file mode 100644 index 0000000000..be0114105e --- /dev/null +++ b/src/main/java/seedu/financialplanner/FinancialPlannerLogger.java @@ -0,0 +1,34 @@ +package seedu.financialplanner; + +import java.io.File; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.LogManager; + +/** + * Represents the logger for the Financial Planner. + */ +public class FinancialPlannerLogger { + private static Logger logger = Logger.getLogger("Financial Planner Logger"); + + /** + * Initialises the logger and saves logging info to a file. + */ + public static void initialise() { + try { + File dir = new File("data"); + if (!dir.exists()) { + dir.mkdir(); + } + FileHandler fh = new FileHandler("data/logger.log"); + LogManager.getLogManager().reset(); + logger.addHandler(fh); + fh.setFormatter(new SimpleFormatter()); + logger.log(Level.INFO, "Logger initialised"); + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/financialplanner/cashflow/Budget.java b/src/main/java/seedu/financialplanner/cashflow/Budget.java new file mode 100644 index 0000000000..9d055f2839 --- /dev/null +++ b/src/main/java/seedu/financialplanner/cashflow/Budget.java @@ -0,0 +1,163 @@ +package seedu.financialplanner.cashflow; + +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents the monthly budget. + */ +public abstract class Budget { + private static double initialBudget = 0; + private static double currentBudget = 0; + + /** + * Sets the monthly budget equal to amount. + * + * @param amount The monthly budget to be set + */ + public static void setBudget(double amount) { + initialBudget = amount; + currentBudget = amount; + assert initialBudget == currentBudget : "Initial and current budget should be the same"; + assert initialBudget != 0 && currentBudget != 0 : "Initial and current budget should not be 0"; + } + + /** + * Returns the initial budget set. + * + * @return The initial budget. + */ + public static double getInitialBudget() { + return initialBudget; + } + + /** + * Updates initial budget to the new budget and updates the current budget + * with the difference between the old initial budget and new initial budget. + * + * @param budget The budget to be updated to. + */ + public static void updateBudget(double budget) { + double diff; + if (budget > initialBudget) { + diff = budget - initialBudget; + initialBudget = budget; + currentBudget += diff; + } else if (budget < initialBudget) { + diff = initialBudget - budget; + initialBudget = budget; + currentBudget -= diff; + } + assert initialBudget == budget : "Initial budget should be equal to updated budget"; + } + + /** + * Returns the current budget. + * + * @return The current budget. + */ + public static double getCurrentBudget() { + return currentBudget; + } + + /** + * Returns the current budget in 2 decimal places after converting + * it into a string. + * + * @return The current budget in string format. + */ + public static String getCurrentBudgetString() { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + return decimalFormat.format(Cashflow.round(currentBudget, 2)); + } + + /** + * Returns the initial budget in 2 decimal places after converting + * it into a string. + * + * @return The initial budget in string format. + */ + public static String getInitialBudgetString() { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + return decimalFormat.format(Cashflow.round(initialBudget, 2)); + } + + /** + * Deducts the current budget by the amount given. + * + * @param amount The amount to be deducted. + */ + public static void deduct(double amount) { + currentBudget -= amount; + } + + /** + * Loads the budget from the save file. + * + * @param initial The saved initial budget. + * @param current The saved current budget. + * @param savedDate The date that was saved to storage. + */ + public static void load(double initial, double current, LocalDate savedDate) { + initialBudget = initial; + currentBudget = current; + LocalDate currentDate = LocalDate.now(); + // Resets budget if it is a new month or new year. + if (!currentDate.getMonth().equals(savedDate.getMonth()) || currentDate.getYear() != savedDate.getYear()) { + resetBudget(); + } + } + + /** + * Checks if there is a budget. + * + * @return True if there is a budget set, false otherwise. + */ + public static boolean hasBudget() { + return initialBudget != 0; + } + + /** + * Returns a string representation of the budget to be saved to the save file. + * + * @return A string representation of the budget. + */ + public static String formatString() { + return "B | " + initialBudget + " | " + currentBudget + " | " + + LocalDate.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + } + + /** + * Deletes an existing budget. + */ + public static void deleteBudget() { + initialBudget = 0; + currentBudget = 0; + } + + /** + * Resets the current budget to initial budget. + */ + public static void resetBudget() { + currentBudget = initialBudget; + } + + /** + * Sets initial budget to the amount given. + * + * @param amount The amount to be set. + */ + public static void setInitialBudget(double amount) { + initialBudget = amount; + } + + /** + * Updates current budget by adding amount to current budget. + * + * @param amount The amount to be updated. + */ + public static void updateCurrentBudget(double amount) { + currentBudget += amount; + } +} diff --git a/src/main/java/seedu/financialplanner/cashflow/Cashflow.java b/src/main/java/seedu/financialplanner/cashflow/Cashflow.java new file mode 100644 index 0000000000..d2b057a8d9 --- /dev/null +++ b/src/main/java/seedu/financialplanner/cashflow/Cashflow.java @@ -0,0 +1,204 @@ +package seedu.financialplanner.cashflow; + +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents an income or expense. + */ +public abstract class Cashflow { + + protected static double balance = 0; + protected static double incomeBalance = 0; + protected static double expenseBalance = 0; + protected double amount; + protected int recur; + protected String description; + protected LocalDate date; + protected boolean hasRecurred; + protected final double MAX_AMOUNT = 999999999999.99; + + /** + * Constructor for a cashflow. hasRecurred variable is set to false by default and date is initialised depending + * on whether recur is set by the user. + * + * @param amount The value of the cashflow. + * @param recur The number of days before the next automatic addition of the cashflow. + * @param description The description of the cashflow. + */ + public Cashflow(double amount, int recur, String description) { + this.amount = amount; + this.recur = recur; + this.description = description; + if (recur != 0) { + this.date = LocalDate.now(); + } + this.hasRecurred = false; + } + + /** + * Constructor for a cashflow. + * + * @param amount The value of the cashflow. + * @param recur The number of days before the next automatic addition of the cashflow. + * @param description The description of the cashflow. + * @param date The date that the cashflow is added. + * @param hasRecurred Whether the cashflow has recurred. + */ + public Cashflow(double amount, int recur, String description, LocalDate date, boolean hasRecurred) { + this.amount = amount; + this.recur = recur; + this.description = description; + this.date = date; + this.hasRecurred = hasRecurred; + } + + protected Cashflow() { + } + + /** + * Sets the balance to 0. + */ + public static void clearBalance() { + balance = 0; + } + + public static void setBalance(double amount) { + balance = amount; + } + + /** + * Deletes the value of a cashflow from the balance. + */ + public abstract void deleteCashflowValue(); + + /** + * Rounds a double to the specified number of decimal places. The rounding is done half-up. + * + * @param value The double to be rounded. + * @param places The number of decimal places to round to. + * @return The rounded double. + */ + //@author mhadidg-reused + //Reused from https://stackoverflow.com/questions/2808535/round-a-double-to-2-decimal-places + public static double round(double value, int places) { + if (places < 0) { + throw new IllegalArgumentException(); + } + + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(places, RoundingMode.HALF_UP); + return bd.doubleValue(); + } + //@author mhadidg + + /** + * Capitalizes the first letter of a provided string. + * + * @param line The input string to be capitalized. + * @return The string that has been capitalized. + */ + //@author Nick Bolton-reused + //Reused from + //https://stackoverflow.com/questions/1892765/how-to-capitalize-the-first-character-of-each-word-in-a-string + public String capitalize(String line) { + return Character.toUpperCase(line.charAt(0)) + line.substring(1); + } + //@author Nick Bolton + + /** + * Formats the cashflow into an easy-to-read format to be output to the user. + * + * @return The formatted cashflow. + */ + public String toString() { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + + String string = " Amount: " + decimalFormat.format(round(amount, 2)); + + if (recur != 0) { + string += System.lineSeparator() + " Recurring every: " + recur + " days"; + string += ", date added: " + date.format(DateTimeFormatter.ofPattern("MMM dd yyyy")); + string += ", recurring on: " + date.plusDays(recur).format(DateTimeFormatter.ofPattern("MMM dd yyyy")); + } + if (description != null) { + string += System.lineSeparator() + " Description: " + description; + } + return string; + } + + + public double getAmount() { + return this.amount; + } + + public static double getBalance() { + return balance; + } + + public static double getIncomeBalance() { + return incomeBalance; + } + + public static double getExpenseBalance() { + return expenseBalance; + } + + public int getRecur() { + return recur; + } + + public void setRecur(int recur) { + this.recur = recur; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public boolean getHasRecurred() { + return hasRecurred; + } + + public void setHasRecurred(boolean hasRecurred) { + this.hasRecurred = hasRecurred; + } + + public String getDescription() { + return description; + } + + /** + * Formats the cashflow into a standard format to be saved into a text file. + * + * @return The formatted cashflow. + */ + public String formatString() { + String string; + if (recur == 0) { + string = " | 0 | false"; + } else { + string = " | " + this.recur + " | " + this.hasRecurred; + string += " | " + date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + } + if (description != null) { + string += " | " + this.description; + } + + return string; + } + + public abstract ExpenseType getExpenseType(); + + public abstract IncomeType getIncomeType(); +} diff --git a/src/main/java/seedu/financialplanner/cashflow/CashflowList.java b/src/main/java/seedu/financialplanner/cashflow/CashflowList.java new file mode 100644 index 0000000000..dc98e35a39 --- /dev/null +++ b/src/main/java/seedu/financialplanner/cashflow/CashflowList.java @@ -0,0 +1,260 @@ +package seedu.financialplanner.cashflow; + +import seedu.financialplanner.enumerations.CashflowCategory; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.utils.Ui; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the list containing all cashflows. + */ +public class CashflowList { + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + private static CashflowList cashflowList = null; + public final ArrayList list = new ArrayList<>(); + protected Ui ui = Ui.getInstance(); + private CashflowList() { + } + + /** + * Gets the single instance of CashflowList class. + * + * @return the CashflowList instance. + */ + public static CashflowList getInstance() { + if (cashflowList == null) { + cashflowList = new CashflowList(); + } + return cashflowList; + } + + /** + * Add an income to the list. + * + * @param value The value of the income. + * @param type The type of the income, using the values in the enum of IncomeType. + * @param recur The number of days before the next automatic addition of the income. + * @param description The description of the income. + */ + public void addIncome(double value, IncomeType type, int recur, String description) { + try { + logger.log(Level.INFO, "Adding income"); + int existingListSize = list.size(); + + Income toAdd = new Income(value, type, recur, description); + addToList(toAdd); + ui.printAddedCashflow(toAdd); + + int newListSize = list.size(); + assert newListSize == existingListSize + 1; + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + } + + private void addToList(Cashflow toAdd) { + list.add(toAdd); + } + + /** + * Adds an expense to the list. + * + * @param value The value of the expense. + * @param type The type of the expense, using the values in the enum of ExpenseType. + * @param recur The number of days before the next automatic addition of the expense. + * @param description The description of the expense. + */ + public void addExpense(double value, ExpenseType type, int recur, String description) { + try { + logger.log(Level.INFO, "Adding expense"); + int existingListSize = list.size(); + + Expense toAdd = new Expense(value, type, recur, description); + addToList(toAdd); + ui.printAddedCashflow(toAdd); + + int newListSize = list.size(); + assert newListSize == existingListSize + 1; + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + } + + /** + * Deletes a cashflow when its category is not specified. + * + * @param index The index of the cashflow as displayed to the user. + * @return The value of the cashflow to be removed. + */ + public double deleteCashflowWithoutCategory(int index) { + int existingListSize = list.size(); + int listIndex = index - 1; + + Cashflow toRemove = list.get(listIndex); + list.remove(listIndex); + toRemove.deleteCashflowValue(); + ui.printDeletedCashflow(toRemove); + + int newListSize = list.size(); + assert newListSize == existingListSize - 1; + return toRemove.getAmount(); + } + + /** + * Deletes all future recurrences of a cashflow that has an unspecified category. + * + * @param index The index of the cashflow as displayed to the user. + */ + public void deleteRecurWithoutCategory(int index) { + int listIndex = index - 1; + + Cashflow toRemoveRecur = list.get(listIndex); + if (toRemoveRecur.getRecur() == 0 || toRemoveRecur.hasRecurred) { + ui.showMessage("Cashflow is already not recurring or has already recurred"); + } else { + toRemoveRecur.setDate(null); + toRemoveRecur.setRecur(0); + list.set(listIndex, toRemoveRecur); + ui.printDeletedRecur(toRemoveRecur); + } + } + + private int cashflowIndexFinder(CashflowCategory category, int cashflowIndex) throws FinancialPlannerException { + assert category.equals(CashflowCategory.INCOME) || category.equals(CashflowCategory.EXPENSE) + || category.equals(CashflowCategory.RECURRING); + + switch (category) { + case INCOME: + return findCashflowIndexFromIncomeIndex(cashflowIndex); + case EXPENSE: + return findCashflowIndexFromExpenseIndex(cashflowIndex); + case RECURRING: + return findCashflowIndexFromRecurIndex(cashflowIndex); + default: + throw new FinancialPlannerException("Error in finding cashflow in the list."); + } + } + + private int findCashflowIndexFromIncomeIndex(int cashflowIndex) { + int cashflowCounter = 0; + int overallCashflowIndex = 0; + + for (Cashflow entry : list) { + if (entry instanceof Income) { + cashflowCounter += 1; + } + if (cashflowCounter == cashflowIndex) { + break; + } + overallCashflowIndex += 1; + } + return overallCashflowIndex; + } + + private int findCashflowIndexFromExpenseIndex(int cashflowIndex) { + int cashflowCounter = 0; + int overallCashflowIndex = 0; + + for (Cashflow entry : list) { + if (entry instanceof Expense) { + cashflowCounter += 1; + } + if (cashflowCounter == cashflowIndex) { + break; + } + overallCashflowIndex += 1; + } + return overallCashflowIndex; + } + private int findCashflowIndexFromRecurIndex(int cashflowIndex) { + int cashflowCounter = 0; + int overallCashflowIndex = 0; + + for (Cashflow entry : list) { + if (entry.getRecur() > 0 && !entry.getHasRecurred()) { + cashflowCounter += 1; + } + if (cashflowCounter == cashflowIndex) { + break; + } + overallCashflowIndex += 1; + } + return overallCashflowIndex; + } + + /** + * Deletes all future recurrences of a cashflow that has a specified category. + * + * @param category The type of cashflow: income, expense or recurring. + * @param index The index of the cashflow as displayed to the user. + */ + public void deleteRecurWithCategory(CashflowCategory category, int index) { + try { + int listIndex = cashflowIndexFinder(category, index); + Cashflow toRemoveRecur = list.get(listIndex); + if (toRemoveRecur.getRecur() == 0 || toRemoveRecur.hasRecurred) { + ui.showMessage("Cashflow is already not recurring or has already recurred."); + } else { + toRemoveRecur.setDate(null); + toRemoveRecur.setRecur(0); + list.set(listIndex, toRemoveRecur); + ui.printDeletedRecur(toRemoveRecur); + } + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + } + + /** + * Deletes a cashflow that has a specified category. + * + * @param category The type of cashflow: income, expense or recurring. + * @param index The index of the cashflow as displayed to the user. + * @return The value of the cashflow to be deleted. + */ + public double deleteCashflowWithCategory(CashflowCategory category, int index) { + try { + int existingListSize = list.size(); + int listIndex = cashflowIndexFinder(category, index); + + Cashflow toRemove = list.get(listIndex); + list.remove(listIndex); + toRemove.deleteCashflowValue(); + ui.printDeletedCashflow(toRemove); + + int newListSize = list.size(); + assert newListSize == existingListSize - 1; + return toRemove.getAmount(); + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + return 0; + } + + /** + * Adds a saved cashflow from the storage to the list. + * + * @param entry The cashflow object to be laoded. + */ + public void load(Cashflow entry) { + addToList(entry); + } + + /** + * Formats the list to string with each entry seperated by a newline. + * + * @return The formatted list. + */ + public String getList() { + StringBuilder output = new StringBuilder(); + for (Cashflow entry : list) { + output.append(entry).append("\n"); + } + return output.toString(); + } +} diff --git a/src/main/java/seedu/financialplanner/cashflow/Expense.java b/src/main/java/seedu/financialplanner/cashflow/Expense.java new file mode 100644 index 0000000000..0bae678868 --- /dev/null +++ b/src/main/java/seedu/financialplanner/cashflow/Expense.java @@ -0,0 +1,114 @@ +package seedu.financialplanner.cashflow; + +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.time.LocalDate; + +/** + * A cashflow object that represents an expense. + */ +public class Expense extends Cashflow { + protected ExpenseType type; + + /** + * Constructor for an expense. + * + * @param amount The value of the expense. + * @param type The type of the expense, using the values in the enum of ExpenseType. + * @param recur The number of days before the next automatic addition of the expense. + * @param description The description of the expense. + * @throws FinancialPlannerException if the balance exceeds the minimum value of -999,999,999,999.99. + */ + public Expense(double amount, ExpenseType type, int recur, String description) throws FinancialPlannerException { + super(amount, recur, description); + this.type = type; + addExpenseValue(); + } + + /** + * Constructor for an expense. + * + * @param amount The value of the expense. + * @param type The type of the expense, using the values in the enum of ExpenseType. + * @param recur The number of days before the next automatic addition of the expense. + * @param description The description of the expense. + * @param date The date that the expense is added. + * @param hasRecurred Whether the expense has recurred. + * @throws FinancialPlannerException if the balance exceeds the minimum value of -999,999,999,999.99. + */ + public Expense(double amount, ExpenseType type, int recur, + String description, LocalDate date, boolean hasRecurred) throws FinancialPlannerException { + super(amount, recur, description, date, hasRecurred); + this.type = type; + addExpenseValue(); + } + + /** + * Constructor for an expense. + * + * @param expense An expense object to be copied. + * @throws FinancialPlannerException if the balance exceeds the minimum value of -999,999,999,999.99. + */ + public Expense(Expense expense) throws FinancialPlannerException { + this.amount = expense.getAmount(); + this.recur = expense.getRecur(); + this.description = expense.getDescription(); + this.date = expense.getDate(); + this.type = expense.getExpenseType(); + addExpenseValue(); + } + + @Override + public ExpenseType getExpenseType() { + return type; + } + + @Override + public IncomeType getIncomeType() { + return null; + } + + private void addExpenseValue() throws FinancialPlannerException { + double tempBalance = balance - this.amount; + + if (tempBalance < -MAX_AMOUNT) { + throw new FinancialPlannerException("Balance exceeded minimum value this program can hold." + + " Please add a different expense."); + } + + balance = tempBalance; + expenseBalance += this.amount; + } + + /** + * Deletes the value of an expense from the balance. + */ + @Override + public void deleteCashflowValue() { + balance += this.amount; + } + + /** + * Formats the expense into an easy-to-read format to be output to the user. + * + * @return The formatted expense. + */ + @Override + public String toString() { + return "Expense" + System.lineSeparator() + + " Type: " + capitalize(type.toString().toLowerCase()) + System.lineSeparator() + super.toString(); + } + + /** + * Formats the expense into a standard format to be saved into a text file. + * + * @return The formatted expense. + */ + @Override + public String formatString() { + return "E | " + this.amount + " | " + this.type + super.formatString(); + } + +} diff --git a/src/main/java/seedu/financialplanner/cashflow/Income.java b/src/main/java/seedu/financialplanner/cashflow/Income.java new file mode 100644 index 0000000000..89c51e5fec --- /dev/null +++ b/src/main/java/seedu/financialplanner/cashflow/Income.java @@ -0,0 +1,114 @@ +package seedu.financialplanner.cashflow; + +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.time.LocalDate; + +/** + * A cashflow object that represents an income. + */ +public class Income extends Cashflow{ + protected IncomeType type; + + /** + * Constructor for an income. + * + * @param amount The value of the income. + * @param type The type of the income, using the values in the enum of IncomeType. + * @param recur The number of days before the next automatic addition of the income. + * @param description The description of the income. + * @throws FinancialPlannerException if the balance exceeds the maximum value of 999,999,999,999.99. + */ + public Income(double amount, IncomeType type, int recur, String description) throws FinancialPlannerException { + super(amount, recur, description); + this.type = type; + addIncomeValue(); + } + + /** + * Constructor for an income + * + * @param amount The value of the income. + * @param type The type of the income, using the values in the enum of IncomeType. + * @param recur The number of days before the next automatic addition of the income. + * @param description The description of the income. + * @param date The date that the income is added. + * @param hasRecurred Whether the income has recurred. + * @throws FinancialPlannerException if the balance exceeds the maximum value of 999,999,999,999.99. + */ + public Income(double amount, IncomeType type, int recur, String description, LocalDate date, boolean hasRecurred) + throws FinancialPlannerException { + super(amount, recur, description, date, hasRecurred); + this.type = type; + addIncomeValue(); + } + + /** + * Constructor for an income. + * + * @param income An income object to be copied. + * @throws FinancialPlannerException if the balance exceeds the maximum value of 999,999,999,999.99. + */ + public Income(Income income) throws FinancialPlannerException { + this.amount = income.getAmount(); + this.recur = income.getRecur(); + this.description = income.getDescription(); + this.date = income.getDate(); + this.type = income.getIncomeType(); + addIncomeValue(); + } + + @Override + public IncomeType getIncomeType() { + return type; + } + + @Override + public ExpenseType getExpenseType() { + return null; + } + + private void addIncomeValue() throws FinancialPlannerException { + double tempBalance = balance + this.amount; + + if (tempBalance > MAX_AMOUNT) { + throw new FinancialPlannerException("Balance exceeded maximum value this program can hold." + + " Please add a different income."); + } + + balance = tempBalance; + incomeBalance += this.amount; + } + + /** + * Deletes the value of an income from the balance. + */ + @Override + public void deleteCashflowValue() { + balance -= this.amount; + } + + /** + * Formats the income into an easy-to-read format to be output to the user. + * + * @return The formatted income. + */ + @Override + public String toString() { + return "Income" + System.lineSeparator() + + " Type: " + capitalize(type.toString().toLowerCase()) + System.lineSeparator() + super.toString(); + } + + /** + * Formats the income into a standard format to be saved into a text file. + * + * @return The formatted income. + */ + @Override + public String formatString() { + return "I | " + this.amount + " | " + this.type + super.formatString(); + } + +} diff --git a/src/main/java/seedu/financialplanner/commands/AddCashflowCommand.java b/src/main/java/seedu/financialplanner/commands/AddCashflowCommand.java new file mode 100644 index 0000000000..7c55edcb00 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/AddCashflowCommand.java @@ -0,0 +1,183 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.enumerations.CashflowCategory; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command to add a cashflow. + */ +@SuppressWarnings("unused") +public class AddCashflowCommand extends Command { + public static final String NAME = "add"; + + public static final String USAGE = + "add income [/r DAYS] [/d DESCRIPTION]" + "\n" + + "add expense [/r DAYS] [/d DESCRIPTION]"; + + public static final String EXAMPLE = + "add income /a 5000 /t salary /r 30 /d work" + "\n" + + "add expense /a 300 /t necessities /r 30 /d groceries"; + + + protected static Ui ui = Ui.getInstance(); + private static Logger logger = Logger.getLogger("Financial Planner Logger"); + protected double amount; + protected CashflowCategory category; + protected ExpenseType expenseType; + protected IncomeType incomeType; + protected int recur = 0; + protected String description = null; + protected CashflowList cashflowList = CashflowList.getInstance(); + protected final double MAX_AMOUNT = 999999999999.99; + + /** + * Constructor for the command to add a cashflow. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public AddCashflowCommand(RawCommand rawCommand) throws IllegalArgumentException { + String categoryString = String.join(" ", rawCommand.args).trim(); + try { + logger.log(Level.INFO, "Parsing CashflowCategory"); + category = CashflowCategory.valueOf(categoryString.toUpperCase()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for CashflowCategory"); + throw new IllegalArgumentException("Entry must be either income or expense."); + } + + if (!rawCommand.extraArgs.containsKey("a")) { + logger.log(Level.WARNING, "Missing arguments for amount"); + throw new IllegalArgumentException("Entry must have an amount."); + } + try { + logger.log(Level.INFO, "Parsing amount as double"); + amount = Double.parseDouble(rawCommand.extraArgs.get("a").trim()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for amount"); + throw new IllegalArgumentException("Amount must be a number."); + } + if (amount < 0) { + logger.log(Level.WARNING, "Invalid value for amount"); + throw new IllegalArgumentException("Amount cannot be negative."); + } + if (amount > MAX_AMOUNT) { + logger.log(Level.WARNING, "Maximum value for amount exceeded."); + throw new IllegalArgumentException("Amount exceeded maximum value this program can hold. " + + "Please add a different cashflow."); + } + rawCommand.extraArgs.remove("a"); + + if (!rawCommand.extraArgs.containsKey("t")) { + logger.log(Level.WARNING, "Missing arguments for type"); + throw new IllegalArgumentException("Entry must have a type."); + } + String stringType = rawCommand.extraArgs.get("t").trim(); + if (category.equals(CashflowCategory.EXPENSE)) { + try { + logger.log(Level.INFO, "Parsing ExpenseType"); + expenseType = ExpenseType.valueOf(stringType.toUpperCase()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for ExpenseType"); + throw new IllegalArgumentException("Entry must be one of the following: " + + "dining, entertainment, shopping, travel, insurance, necessities, others"); + } + } else if (category.equals(CashflowCategory.INCOME)) { + try { + logger.log(Level.INFO, "Parsing IncomeType"); + incomeType = IncomeType.valueOf(stringType.toUpperCase()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for IncomeType"); + throw new IllegalArgumentException("Entry must be one of the following: " + + "salary, investments, allowance, others"); + } + } + rawCommand.extraArgs.remove("t"); + + if (rawCommand.extraArgs.containsKey("r")) { + try { + logger.log(Level.INFO, "Parsing recur as integer"); + recur = Integer.parseInt(rawCommand.extraArgs.get("r").trim()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for recur"); + throw new IllegalArgumentException("Recurrence must be an integer and be within the " + + "maximum value this program can hold."); + } + rawCommand.extraArgs.remove("r"); + } + if (recur < 0) { + logger.log(Level.WARNING, "Invalid value for recur"); + throw new IllegalArgumentException("Recurring value cannot be negative."); + } + + if (rawCommand.extraArgs.containsKey("d")) { + logger.log(Level.INFO, "Getting description of cashflow"); + String line = rawCommand.extraArgs.get("d"); + if (line.isBlank()) { + logger.log(Level.WARNING, "Empty description"); + throw new IllegalArgumentException("Description cannot be left empty."); + } + description = line.trim(); + } + rawCommand.extraArgs.remove("d"); + + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to add a cashflow. + */ + @Override + public void execute() { + assert category.equals(CashflowCategory.INCOME) || category.equals(CashflowCategory.EXPENSE) + || category.equals(CashflowCategory.RECURRING); + assert recur >= 0; + assert amount >= 0; + if (category.equals(CashflowCategory.EXPENSE)) { + assert expenseType.equals(ExpenseType.DINING) || expenseType.equals(ExpenseType.ENTERTAINMENT) + || expenseType.equals(ExpenseType.SHOPPING) || expenseType.equals(ExpenseType.TRAVEL) + || expenseType.equals(ExpenseType.INSURANCE) || expenseType.equals(ExpenseType.OTHERS) + || expenseType.equals(ExpenseType.NECESSITIES); + } else if (category.equals(CashflowCategory.INCOME)) { + assert incomeType.equals(IncomeType.SALARY) || incomeType.equals(IncomeType.INVESTMENTS) + || incomeType.equals(IncomeType.ALLOWANCE) || incomeType.equals(IncomeType.OTHERS); + } + + switch (category) { + case INCOME: + cashflowList.addIncome(amount, incomeType, recur, description); + break; + case EXPENSE: + cashflowList.addExpense(amount, expenseType, recur, description); + if (Budget.hasBudget()) { + deductFromBudget(cashflowList.list.get(cashflowList.list.size() - 1)); + } + break; + default: + logger.log(Level.SEVERE, "Unreachable default case reached"); + ui.showMessage("Unidentified entry."); + break; + } + } + + private static void deductFromBudget(Cashflow entry) { + double expenseAmount = entry.getAmount(); + Budget.deduct(expenseAmount); + ui.printBudgetAfterDeduction(); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/AddReminderCommand.java b/src/main/java/seedu/financialplanner/commands/AddReminderCommand.java new file mode 100644 index 0000000000..6bc4130931 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/AddReminderCommand.java @@ -0,0 +1,81 @@ +package seedu.financialplanner.commands; + + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.reminder.Reminder; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@SuppressWarnings("unused") +public class AddReminderCommand extends Command { + public static final String NAME = "addreminder"; + + public static final String USAGE = + "addreminder "; + + public static final String EXAMPLE = + "addreminder /t debt /d 11/12/2023"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private final String type; + private final LocalDate date; + + /** + * Constructor for the command to add a reminder. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public AddReminderCommand(RawCommand rawCommand) throws IllegalArgumentException { + String typeString = String.join(" ", rawCommand.args); + if (!rawCommand.extraArgs.containsKey("t")) { + throw new IllegalArgumentException("Reminder must have a type"); + } + type = rawCommand.extraArgs.get("t"); + if(type.trim().isEmpty()){ + throw new IllegalArgumentException("Reminder type cannot be empty"); + } + rawCommand.extraArgs.remove("t"); + if (!rawCommand.extraArgs.containsKey("d")) { + throw new IllegalArgumentException("Reminder must have a date"); + } + + String dateString = rawCommand.extraArgs.get("d"); + if(dateString.trim().isEmpty()){ + throw new IllegalArgumentException("Reminder date cannot be empty"); + } + + try { + date = LocalDate.parse(dateString, FORMATTER); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Reminder date must be in the format dd/MM/yyyy"); + } + + LocalDate currentTime = LocalDate.now(); + if(date.isBefore(currentTime)){ + throw new IllegalArgumentException("Reminder date cannot be in the past"); + } + + rawCommand.extraArgs.remove("d"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to add a reminder. + */ + @Override + public void execute() { + assert type != null; + assert LocalDate.now().isBefore(date); + Reminder reminder = new Reminder(type, date); + ReminderList.getInstance().list.add(reminder); + Ui.getInstance().showMessage("You have added " + reminder); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/AddStockCommand.java b/src/main/java/seedu/financialplanner/commands/AddStockCommand.java new file mode 100644 index 0000000000..5d98583af2 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/AddStockCommand.java @@ -0,0 +1,70 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Command that inherits from the Command abstract class + * Represents that command that add stock to watchlist + */ +@SuppressWarnings("unused") +public class AddStockCommand extends Command { + + public static final String NAME = "addstock"; + + public static final String USAGE = + "addstock "; + public static final String EXAMPLE = + "addstock /s META"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final String stockCode; + + /** + * Constructor for the command add stock to watchlist + * + * @param rawCommand + * @throws IllegalArgumentException + */ + public AddStockCommand(RawCommand rawCommand) throws IllegalArgumentException { + if (!rawCommand.extraArgs.containsKey("s")) { + throw new IllegalArgumentException("Stock code cannot be empty"); + } + + logger.log(Level.INFO, "Parsing stockcode from input"); + stockCode = rawCommand.extraArgs.get("s").trim(); + + rawCommand.extraArgs.remove("s"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to add stock to watchlist + */ + @Override + public void execute() { + Ui ui = Ui.getInstance(); + WatchList watchList = WatchList.getInstance(); + String stockName; + + logger.log(Level.INFO, "adding stock to watchlist"); + try { + stockName = watchList.addStock(stockCode); + assert stockName != null; + ui.printAddStock(stockName); + } catch (FinancialPlannerException e) { + logger.log(Level.WARNING, "Error adding stock to watchlist"); + Ui.getInstance().showMessage(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/financialplanner/commands/BalanceCommand.java b/src/main/java/seedu/financialplanner/commands/BalanceCommand.java new file mode 100644 index 0000000000..f3c5269bd0 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/BalanceCommand.java @@ -0,0 +1,43 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.utils.Ui; + +import java.text.DecimalFormat; +import java.util.ArrayList; + +/** + * Represents a command for displaying balance. + */ +@SuppressWarnings("unused") +public class BalanceCommand extends Command { + public static final String NAME = "balance"; + + public static final String USAGE = + "balance"; + public static final String EXAMPLE = + "balance"; + private final Ui ui = Ui.getInstance(); + + public BalanceCommand(RawCommand rawCommand) { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to display balance. + */ + @Override + public void execute() { + ui.showMessage("Balance: " + getBalanceString()); + } + + private String getBalanceString() { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + return decimalFormat.format(Cashflow.round(Cashflow.getBalance(), 2)); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/BudgetCommand.java b/src/main/java/seedu/financialplanner/commands/BudgetCommand.java new file mode 100644 index 0000000000..586e38d7a2 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/BudgetCommand.java @@ -0,0 +1,188 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to manage the budget. + */ +@SuppressWarnings("unused") +public class BudgetCommand extends Command { + public static final String NAME = "budget"; + + public static final String USAGE = + "budget set " + "\n" + + "budget update " + "\n" + + "budget delete" + "\n" + + "budget reset" + "\n" + + "budget view"; + public static final String EXAMPLE = + "budget set /b 500" + "\n" + + "budget reset"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final Ui ui = Ui.getInstance(); + private double budget; + private final String command; + + /** + * Constructs a new BudgetCommand and checks if the user input is a valid command. + * + * @param rawCommand The raw command containing the arguments and parameters given by the user. + * @throws FinancialPlannerException If there is an issue with the command provided. + */ + public BudgetCommand(RawCommand rawCommand) throws FinancialPlannerException { + if (rawCommand.args.isEmpty()) { + throw new FinancialPlannerException("Budget operation cannot be empty."); + } + command = String.join(" ", rawCommand.args).trim(); + if (command.equals("delete") || command.equals("reset") || command.equals("view")) { + return; + } + validateCommandFormat(rawCommand); + validateBudget(rawCommand); + + assert budget > 0 && budget <= Cashflow.getBalance() : "Budget should be greater than 0 and less than " + + "or equal to total balance"; + rawCommand.extraArgs.remove("b"); + + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Checks if budget is a positive number and is lower than or equal to total balance. + * + * @param rawCommand The raw command containing arguments and parameters given by the user. + * @throws FinancialPlannerException If there is an issue with budget format. + */ + private void validateBudget(RawCommand rawCommand) throws FinancialPlannerException { + try { + logger.log(Level.INFO, "Parsing budget as double"); + budget = Double.parseDouble(rawCommand.extraArgs.get("b")); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid value for budget"); + throw new IllegalArgumentException("Budget must be a number."); + } + if (budget <= 0) { + logger.log(Level.WARNING, "Invalid value for budget."); + throw new FinancialPlannerException("Budget should be greater than 0."); + } + + if (budget > Cashflow.getBalance()) { + logger.log(Level.WARNING, "Invalid value for budget"); + throw new FinancialPlannerException("Budget should be lower than or equal to total balance."); + } + } + + /** + * Checks that the command is valid and in the right format. + * + * @param rawCommand The raw command containing arguments and parameters given by the user. + * @throws FinancialPlannerException If there is an issue with the command or format. + */ + private void validateCommandFormat(RawCommand rawCommand) throws FinancialPlannerException { + if (!command.equals("set") && !command.equals("update")) { + logger.log(Level.WARNING, "Invalid arguments for budget command"); + throw new FinancialPlannerException("Budget operation must be one of the following: set, update, " + + "delete, reset, view."); + } + + if (command.equals("set") && Budget.hasBudget()) { + logger.log(Level.WARNING, "Invalid command: Trying to set existing budget"); + throw new FinancialPlannerException("There is an existing budget, try budget update instead."); + } else if (command.equals("update") && !Budget.hasBudget()) { + logger.log(Level.WARNING, "Invalid command: Trying to update non-existent budget"); + throw new FinancialPlannerException("There is no budget set yet, try budget set instead."); + } + + if (!rawCommand.extraArgs.containsKey("b")) { + logger.log(Level.WARNING, "Missing argument /b in command"); + throw new IllegalArgumentException("Missing /b argument."); + } + } + + /** + * Executes the budget command based on the specified operation. + */ + @Override + public void execute() { + assert command.equals("set") || command.equals("update") || command.equals("delete") || + command.equals("reset") || command.equals("view"); + + switch (command) { + case "set": + setBudget(); + break; + case "update": + updateBudget(); + break; + case "delete": + deleteBudget(); + break; + case "reset": + resetBudget(); + break; + case "view": + viewBudget(); + break; + default: + logger.log(Level.SEVERE, "Unreachable default case reached"); + ui.showMessage("Unknown command."); + } + } + + private void viewBudget() { + if (Budget.hasBudget()) { + ui.printBudget(); + } else { + ui.printBudgetError("view"); + } + } + + private void resetBudget() { + if (Budget.getInitialBudget() != Budget.getCurrentBudget()) { + if (Budget.getInitialBudget() > Cashflow.getBalance()) { + Budget.setInitialBudget(Cashflow.getBalance()); + ui.printBudgetExceedBalance(); + } + Budget.resetBudget(); + ui.printResetBudget(); + } else if (!Budget.hasBudget()) { + ui.printBudgetError("delete"); + } else { + ui.printBudgetError("reset"); + } + } + + private void deleteBudget() { + if (Budget.hasBudget()) { + Budget.deleteBudget(); + ui.printDeleteBudget(); + } else { + ui.printBudgetError("delete"); + } + } + + private void updateBudget() { + logger.log(Level.INFO, "Updating budget"); + ui.printBudgetBeforeUpdate(); + Budget.updateBudget(budget); + ui.printBudgetAfterUpdate(); + } + + private void setBudget() { + logger.log(Level.INFO, "Setting budget"); + Budget.setBudget(budget); + ui.printSetBudget(); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/DeleteCashflowCommand.java b/src/main/java/seedu/financialplanner/commands/DeleteCashflowCommand.java new file mode 100644 index 0000000000..69e4e2bebd --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/DeleteCashflowCommand.java @@ -0,0 +1,198 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.enumerations.CashflowCategory; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to delete a cashflow. + */ +@SuppressWarnings("unused") +public class DeleteCashflowCommand extends Command { + public static final String NAME = "delete"; + + public static final String USAGE = + "delete INDEX [/r]" + "\n" + + "delete income INDEX [/r]" + "\n" + + "delete expense INDEX [/r]" + "\n" + + "delete recurring INDEX [/r]"; + + public static final String EXAMPLE = + "delete 1" + "\n" + + "delete income 2 /r" + "\n" + + "delete expense 2 /r" + "\n" + + "delete recurring 2"; + + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + protected CashflowCategory category = null; + protected int index; + protected boolean hasRecur; + protected CashflowList cashflowList = CashflowList.getInstance(); + + /** + * Constructor of the command to delete a cashflow. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public DeleteCashflowCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringIndex; + String stringCategory = null; + int indexToDelete = 0; + ArrayList blankArgsList = new ArrayList<>(); + for (String string : rawCommand.args) { + if (string.isBlank()) { + Integer toAdd = indexToDelete; + blankArgsList.add(toAdd); + } + indexToDelete++; + } + int counter = 0; + for (Integer integer : blankArgsList) { + indexToDelete = integer - counter; + rawCommand.args.remove(indexToDelete); + counter++; + } + + if (rawCommand.args.size() == 1) { + stringIndex = rawCommand.args.get(0).trim(); + } else if (rawCommand.args.size() == 2) { + stringCategory = rawCommand.args.get(0).trim(); + handleInvalidCategory(stringCategory); + stringIndex = rawCommand.args.get(1).trim(); + } else { + throw new IllegalArgumentException("Incorrect arguments."); + } + + try { + logger.log(Level.INFO, "Parsing index as integer"); + index = Integer.parseInt(stringIndex); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid argument for index"); + throw new IllegalArgumentException("Index must be an integer and be " + + "within the maximum value this program can hold."); + } + + if (index == 0) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index must be within the list."); + } + + if (rawCommand.extraArgs.containsKey("r")) { + logger.log(Level.INFO, "Getting any arguments after /r"); + String recurArgs = rawCommand.extraArgs.get("r"); + if (!recurArgs.isBlank()) { + logger.log(Level.WARNING, "Arguments after /r found"); + throw new IllegalArgumentException("Arguments after /r should be left empty."); + } + hasRecur = true; + } else { + hasRecur = false; + } + rawCommand.extraArgs.remove("r"); + + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + private void handleInvalidCategory(String stringCategory) { + try { + logger.log(Level.INFO, "Parsing CashflowCategory"); + category = CashflowCategory.valueOf(stringCategory.toUpperCase()); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid arguments for CashflowCategory"); + throw new IllegalArgumentException("Entry must be either income, expense or recurring."); + } + } + + /** + * Executes the command to delete a cashflow. + */ + @Override + public void execute() { + if (category == null) { + if (hasRecur) { + handleDeleteRecurWithoutCategory(); + } else { + handleDeleteCashflowWithoutCategory(); + } + return; + } + + assert category.equals(CashflowCategory.INCOME) || category.equals(CashflowCategory.EXPENSE) + || category.equals(CashflowCategory.RECURRING); + assert index != 0; + + switch (category) { + case INCOME: + //Fallthrough + case EXPENSE: + //Fallthrough + case RECURRING: + if (hasRecur) { + handleDeleteRecurWithCategory(); + } else { + handleDeleteCashflowWithCategory(); + } + break; + default: + logger.log(Level.SEVERE, "Unreachable default case reached"); + Ui.getInstance().showMessage("Unidentified entry."); + break; + } + } + + private void handleDeleteRecurWithoutCategory() { + try { + logger.log(Level.INFO, "Deleting recurrence without category"); + cashflowList.deleteRecurWithoutCategory(index); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Index out of list"); + throw new IllegalArgumentException("Index must be within the list."); + } + } + + private void handleDeleteCashflowWithoutCategory() { + try { + logger.log(Level.INFO, "Deleting cashflow without category"); + double amount = cashflowList.deleteCashflowWithoutCategory(index); + if (Budget.hasBudget()) { + Budget.updateCurrentBudget(amount); + } + } catch (IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Index out of list"); + throw new IllegalArgumentException("Index must be within the list."); + } + } + private void handleDeleteRecurWithCategory() { + try { + logger.log(Level.INFO, "Deleting recurrence with category"); + cashflowList.deleteRecurWithCategory(category, index); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Index out of list"); + throw new IllegalArgumentException("Index must be within the list."); + } + } + private void handleDeleteCashflowWithCategory() { + try { + logger.log(Level.INFO, "Deleting cashflow with category"); + double amount = cashflowList.deleteCashflowWithCategory(category, index); + if (Budget.hasBudget()) { + Budget.updateCurrentBudget(amount); + } + } catch (IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Index out of list"); + throw new IllegalArgumentException("Index must be within the list."); + } + } +} diff --git a/src/main/java/seedu/financialplanner/commands/DeleteGoalCommand.java b/src/main/java/seedu/financialplanner/commands/DeleteGoalCommand.java new file mode 100644 index 0000000000..6835f13770 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/DeleteGoalCommand.java @@ -0,0 +1,70 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.goal.Goal; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.utils.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class DeleteGoalCommand extends Command { + public static final String NAME = "deletegoal"; + + public static final String USAGE = "deletegoal "; + public static final String EXAMPLE = "deletegoal 1"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final int index; + + /** + * Constructor of the command to delete a goal. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public DeleteGoalCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringIndex; + if (rawCommand.args.size() == 1) { + stringIndex = rawCommand.args.get(0); + } else { + throw new IllegalArgumentException("Please specify a valid index of goal"); + } + + try { + logger.log(Level.INFO, "Parsing index as integer"); + index = Integer.parseInt(stringIndex); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid argument for index"); + throw new IllegalArgumentException("Index must be a valid integer"); + } + + if (index <= 0) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index must be within the list"); + } + + if (index > WishList.getInstance().list.size()) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index exceed the list size"); + } + rawCommand.extraArgs.remove("i"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to delete a goal. + */ + @Override + public void execute() { + assert index > 0 && index <= WishList.getInstance().list.size(); + Goal goalToDelete = WishList.getInstance().list.get(index - 1); + WishList.getInstance().deleteGoal(index - 1); + Ui.getInstance().showMessage("You have deleted " + goalToDelete); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/DeleteReminderCommand.java b/src/main/java/seedu/financialplanner/commands/DeleteReminderCommand.java new file mode 100644 index 0000000000..26cf2ef390 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/DeleteReminderCommand.java @@ -0,0 +1,69 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.reminder.Reminder; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class DeleteReminderCommand extends Command { + public static final String NAME = "deletereminder"; + + public static final String USAGE = "deletereminder "; + public static final String EXAMPLE = "deletereminder 1"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final int index; + + /** + * Constructor of the command to delete a reminder. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public DeleteReminderCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringIndex; + if (rawCommand.args.size() == 1) { + stringIndex = rawCommand.args.get(0); + } else { + throw new IllegalArgumentException("Please specify a valid index of reminder"); + } + + try { + logger.log(Level.INFO, "Parsing index as integer"); + index = Integer.parseInt(stringIndex); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid argument for index"); + throw new IllegalArgumentException("Index must be a valid integer"); + } + + if (index <= 0) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index must be within the list"); + } + + if (index > ReminderList.getInstance().list.size()) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index exceed the list size"); + } + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to delete a reminder. + */ + @Override + public void execute() { + assert index > 0 && index <= ReminderList.getInstance().list.size(); + Reminder reminderToDelete = ReminderList.getInstance().list.get(index - 1); + ReminderList.getInstance().deleteReminder(index - 1); + Ui.getInstance().showMessage("You have deleted " + reminderToDelete); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/DeleteStockCommand.java b/src/main/java/seedu/financialplanner/commands/DeleteStockCommand.java new file mode 100644 index 0000000000..adaec2795b --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/DeleteStockCommand.java @@ -0,0 +1,74 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Command that inherits from the Command abstract class + * Represents the command to delete stock from watchlist + */ +@SuppressWarnings("unused") +public class DeleteStockCommand extends Command { + + + public static final String NAME = "deletestock"; + + public static final String USAGE = + "deletestock "; + public static final String EXAMPLE = + "deletestock /s META"; + + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final String stockCode; + + /** + * Constructor for the command to delete stock from watchlist + * + * @param rawCommand + * @throws IllegalArgumentException + */ + public DeleteStockCommand(RawCommand rawCommand) throws IllegalArgumentException { + if (!rawCommand.extraArgs.containsKey("s")) { + throw new IllegalArgumentException("Stock code cannot be empty"); + } + + logger.log(Level.INFO, "Parsing stockcode from input"); + stockCode = rawCommand.extraArgs.get("s").trim(); + + rawCommand.extraArgs.remove("s"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to delete stock from watchlist + * + * @throws Exception + */ + @Override + public void execute() throws Exception { + Ui ui = Ui.getInstance(); + WatchList watchList = WatchList.getInstance(); + String stockName; + + logger.log(Level.INFO, "deleting stock from watchlist"); + try { + stockName = watchList.deleteStock(stockCode); + assert stockName != null; + ui.printDeleteStock(stockName); + } catch (FinancialPlannerException e) { + logger.log(Level.WARNING, "Error deleting stock from watchlist"); + ui.showMessage(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/financialplanner/commands/ExitCommand.java b/src/main/java/seedu/financialplanner/commands/ExitCommand.java new file mode 100644 index 0000000000..5076539981 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/ExitCommand.java @@ -0,0 +1,30 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; + +import java.util.ArrayList; + +/** + * Represents a command to exit the program. + */ +@SuppressWarnings("unused") +public class ExitCommand extends Command { + public static final String NAME = "exit"; + + public static final String USAGE = + "exit"; + public static final String EXAMPLE = + "exit"; + + public ExitCommand(RawCommand rawCommand) { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + @Override + public void execute() { + } +} diff --git a/src/main/java/seedu/financialplanner/commands/FindCommand.java b/src/main/java/seedu/financialplanner/commands/FindCommand.java new file mode 100644 index 0000000000..c7f4dd41b7 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/FindCommand.java @@ -0,0 +1,60 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; + +@SuppressWarnings("unused") +public class FindCommand extends Command { + public static final String NAME = "find"; + + public static final String USAGE = + "find "; + public static final String EXAMPLE = + "find buy coffee"; + private final String description; + + /** + * Constructor of the command to find cashflow. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public FindCommand(RawCommand rawCommand) { + this.description = String.join(" ", rawCommand.args); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to find. + */ + @Override + public void execute() { + CashflowList cashflowList = CashflowList.getInstance(); + Ui ui = Ui.getInstance(); + WatchList watchList = WatchList.getInstance(); + ArrayList foundedFinancialList = new ArrayList<>(); + for (int i = 0; i < cashflowList.list.size(); i++) { + if (cashflowList.list.get(i).toString().toLowerCase().contains(description.toLowerCase())) { + String output = cashflowList.list.get(i).toString() + " | Index: " + (i + 1); + foundedFinancialList.add(output); + } + } + if (!foundedFinancialList.isEmpty()) { + ui.showMessage("Here are the matching financial records in your financial list:"); + } else { + ui.showMessage("There is no matching financial record in your financial list."); + } + for (String foundedFinancialRecord : foundedFinancialList) { + ui.showMessage(foundedFinancialRecord); + } + + } +} diff --git a/src/main/java/seedu/financialplanner/commands/HelpCommand.java b/src/main/java/seedu/financialplanner/commands/HelpCommand.java new file mode 100644 index 0000000000..8dd6f8faf5 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/HelpCommand.java @@ -0,0 +1,71 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.CommandManager; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; + +@SuppressWarnings("unused") +public class HelpCommand extends Command { + public static final String NAME = "help"; + + public static final String USAGE = "help [COMMAND]"; + public static final String EXAMPLE = + "help" + "\n" + + "help budget"; + + private static final String HELP_MESSAGE_GENERAL = + "<> denotes required arguments, [] denotes optional arguments"; + + private static final String HELP_ALL_PREFIX = + "Here are the available commands:"; + + // Will replace this in the future + private static final String DELIMITER = "------------------------------"; + + private final String commandName; + + public HelpCommand(RawCommand rawCommand) { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + if (rawCommand.args.isEmpty()) { + commandName = null; + } else if (rawCommand.args.size() == 1) { + commandName = rawCommand.args.get(0); + } else { + throw new IllegalArgumentException("Unknown arguments, type help for help"); + } + } + + @Override + public void execute() throws Exception { + Ui ui = Ui.getInstance(); + CommandManager commandManager = CommandManager.getInstance(); + if (commandName == null) { + ui.showMessage(HELP_MESSAGE_GENERAL); + ui.showMessage(HELP_ALL_PREFIX); + ui.showMessage(DELIMITER); + for (String name : commandManager.getCommandNames()) { + String usage = commandManager.getCommandUsage(name); + String example = commandManager.getCommandExample(name); + ui.showMessage("Usage of " + name + ":"); + ui.showMessage(usage); + ui.showMessage("Example usage of " + name + ":"); + ui.showMessage(example); + ui.showMessage(DELIMITER); + } + return; + } + String commandUsage = commandManager.getCommandUsage(commandName.toLowerCase()); + String commandExample = commandManager.getCommandExample(commandName.toLowerCase()); + ui.showMessage(HELP_MESSAGE_GENERAL); + ui.showMessage("Usage of " + commandName.toLowerCase() + ":"); + ui.showMessage(commandUsage); + ui.showMessage("Example usage of " + commandName.toLowerCase() + ":"); + ui.showMessage(commandExample); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/ListCommand.java b/src/main/java/seedu/financialplanner/commands/ListCommand.java new file mode 100644 index 0000000000..acd6c86a20 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/ListCommand.java @@ -0,0 +1,106 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.cashflow.Income; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.enumerations.CashflowCategory; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class ListCommand extends Command { + public static final String NAME = "list"; + + public static final String USAGE = + "list [income/expense/recurring]"; + + public static final String EXAMPLE = + "list" + "\n" + + "list recurring"; + protected CashflowCategory category = null; + + public ListCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringCategory = null; + int indexToDelete = 0; + ArrayList blankArgsList = new ArrayList<>(); + for (String string : rawCommand.args) { + if (string.isBlank()) { + Integer toAdd = indexToDelete; + blankArgsList.add(toAdd); + } + indexToDelete++; + } + int counter = 0; + for (Integer integer : blankArgsList) { + indexToDelete = integer - counter; + rawCommand.args.remove(indexToDelete); + counter++; + } + + if (rawCommand.args.size() == 1) { + stringCategory = rawCommand.args.get(0); + } else if (rawCommand.args.size() > 1) { + throw new IllegalArgumentException("Incorrect arguments."); + } + + if (stringCategory != null) { + try { + category = CashflowCategory.valueOf(stringCategory.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Entry must be either income, expense or recurring."); + } + } + } + + private boolean shouldPrintCashFlow(Cashflow cashflow) { + if (category == null) { + return true; + } + if (cashflow instanceof Income && category.equals(CashflowCategory.INCOME)) { + return true; + } + if (cashflow instanceof Expense && category.equals(CashflowCategory.EXPENSE)) { + return true; + } + if (cashflow.getRecur() > 0 && !cashflow.getHasRecurred()) { + return category.equals(CashflowCategory.RECURRING); + } + return false; + } + + @Override + public void execute() throws Exception { + Ui ui = Ui.getInstance(); + + List cashflowList = CashflowList.getInstance().list; + List cashflowToBePrinted = new ArrayList<>(); + for (Cashflow flow : cashflowList) { + if (!shouldPrintCashFlow(flow)) { + continue; + } + cashflowToBePrinted.add(flow); + } + + if (cashflowToBePrinted.isEmpty()) { + ui.showMessage("No matching cashflow."); + return; + } + + ui.showMessage(String.format("You have %d matching cashflows:", cashflowToBePrinted.size())); + for (int i = 0; i < cashflowToBePrinted.size(); i += 1) { + ui.showMessage((i + 1) + ": " + cashflowToBePrinted.get(i)); + } + if (category == null) { + ui.showMessage("Balance: " + ui.formatBalance(Cashflow.getBalance())); + } else if (category.equals(CashflowCategory.INCOME)) { + ui.showMessage("Income Balance: " + ui.formatBalance(Cashflow.getIncomeBalance())); + } else if (category.equals(CashflowCategory.EXPENSE)) { + ui.showMessage("Expense Balance: " + ui.formatBalance(Cashflow.getExpenseBalance())); + } + } +} diff --git a/src/main/java/seedu/financialplanner/commands/MarkGoalCommand.java b/src/main/java/seedu/financialplanner/commands/MarkGoalCommand.java new file mode 100644 index 0000000000..088c16cc7a --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/MarkGoalCommand.java @@ -0,0 +1,64 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.goal.WishList; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class MarkGoalCommand extends Command { + public static final String NAME = "markgoal"; + + public static final String USAGE = "markgoal "; + public static final String EXAMPLE = "markgoal 1"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final int index; + + /** + * Constructor of the command to mark a goal as achieved. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public MarkGoalCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringIndex; + if (rawCommand.args.size() == 1) { + stringIndex = rawCommand.args.get(0); + } else { + throw new IllegalArgumentException("Please specify a valid index of goal"); + } + + try { + logger.log(Level.INFO, "Parsing index as integer"); + index = Integer.parseInt(stringIndex); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid argument for index"); + throw new IllegalArgumentException("Index must be a valid integer"); + } + if (index <= 0) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index must be within the list"); + } + if (index > WishList.getInstance().list.size()) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index exceed the list size"); + } + rawCommand.extraArgs.remove("i"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to mark the goal. + */ + @Override + public void execute() { + assert index > 0 && index <= WishList.getInstance().list.size(); + WishList.getInstance().markGoal(index); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/MarkReminderCommand.java b/src/main/java/seedu/financialplanner/commands/MarkReminderCommand.java new file mode 100644 index 0000000000..0df423ff54 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/MarkReminderCommand.java @@ -0,0 +1,68 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class MarkReminderCommand extends Command { + public static final String NAME = "markreminder"; + + public static final String USAGE = "markreminder "; + public static final String EXAMPLE = "markreminder 1"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private final int index; + + /** + * Constructor of the command to mark a reminder as done. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public MarkReminderCommand(RawCommand rawCommand) throws IllegalArgumentException { + String stringIndex; + if (rawCommand.args.size() == 1) { + stringIndex = rawCommand.args.get(0); + } else { + throw new IllegalArgumentException("Please specify a valid index of reminder"); + } + + try { + logger.log(Level.INFO, "Parsing index as integer"); + index = Integer.parseInt(stringIndex); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Invalid argument for index"); + throw new IllegalArgumentException("Index must be a valid integer"); + } + if (index <= 0) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index must be within the list"); + } + if (index > ReminderList.getInstance().list.size()) { + logger.log(Level.WARNING, "Invalid value for index"); + throw new IllegalArgumentException("Index exceed the list size"); + } + rawCommand.extraArgs.remove("i"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to mark the reminder. + */ + @Override + public void execute() { + assert index > 0 && index <= ReminderList.getInstance().list.size(); + ReminderList.getInstance().list.get(index - 1).markAsDone(); + Ui.getInstance().showMessage("You have marked " + ReminderList.getInstance().list.get(index - 1)); + } + + +} diff --git a/src/main/java/seedu/financialplanner/commands/OverviewCommand.java b/src/main/java/seedu/financialplanner/commands/OverviewCommand.java new file mode 100644 index 0000000000..d0234b0efe --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/OverviewCommand.java @@ -0,0 +1,118 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Income; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; + +import java.text.DecimalFormat; +import java.util.ArrayList; + +/** + * Represents a command to display overview of user's financials. + */ +@SuppressWarnings("unused") +public class OverviewCommand extends Command { + public static final String NAME = "overview"; + + public static final String USAGE = + "overview"; + + public static final String EXAMPLE = + "overview"; + private static final CashflowList cashflowList = CashflowList.getInstance(); + + public OverviewCommand(RawCommand rawCommand) { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command and displays an overview of the user's financials. + */ + @Override + public void execute() { + String balance = getBalance(); + String highestIncome = getHighestIncome(); + String highestExpense = getHighestExpense(); + String budget = getBudgetDesc(); + String reminders = getReminders(); + String wishlist = getWishlist(); + + Ui.getInstance().printOverview(balance, highestIncome, highestExpense, budget, reminders, wishlist); + } + + private String getBudgetDesc() { + return Budget.getCurrentBudgetString(); + } + + private String getHighestIncome() { + double maxIncome = 0; + String incomeType = ""; + for (Cashflow entry : cashflowList.list) { + if (entry instanceof Income && entry.getAmount() > maxIncome) { + maxIncome = entry.getAmount(); + incomeType = entry.capitalize(entry.getIncomeType(). + toString().toLowerCase()); // Capitalise the first letter + } + } + + if (incomeType.isEmpty()) { + return "No income added yet."; + } + + return formatDoubleToString(maxIncome) + " Category: " + incomeType; + } + + private String getHighestExpense() { + double maxExpense = 0; + String expenseType = ""; + for (Cashflow entry : cashflowList.list) { + if (entry instanceof Expense && entry.getAmount() > maxExpense) { + maxExpense = entry.getAmount(); + expenseType = entry.capitalize(entry.getExpenseType(). + toString().toLowerCase()); // Capitalise the first letter + } + } + + if (expenseType.isEmpty()) { + return "No expense added yet."; + } + + return formatDoubleToString(maxExpense) + " Category: " + expenseType; + } + + private String formatDoubleToString(double amount) { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + + return decimalFormat.format(Cashflow.round(amount, 2)); + } + + private String getReminders() { + ReminderList reminderList = ReminderList.getInstance(); + if (reminderList.list.isEmpty()) { + return "No reminders added yet."; + } + return reminderList.toString(); + } + + private String getWishlist() { + WishList wishList = WishList.getInstance(); + if (wishList.list.isEmpty()) { + return "No goals added yet."; + } + return wishList.toString(); + } + + private String getBalance() { + return formatDoubleToString(Cashflow.getBalance()); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/ReminderListCommand.java b/src/main/java/seedu/financialplanner/commands/ReminderListCommand.java new file mode 100644 index 0000000000..bdad0e6a73 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/ReminderListCommand.java @@ -0,0 +1,47 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; +import java.util.logging.Logger; +import java.util.logging.Level; + +@SuppressWarnings("unused") +public class ReminderListCommand extends Command { + public static final String NAME = "reminderlist"; + + public static final String USAGE = "reminderlist"; + public static final String EXAMPLE = "reminderlist"; + + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + /** + * Constructor of the command to list goals. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public ReminderListCommand(RawCommand rawCommand) throws IllegalArgumentException { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to list the reminders. + */ + @Override + public void execute() { + Ui ui = Ui.getInstance(); + ReminderList reminderList = ReminderList.getInstance(); + if (reminderList.list.isEmpty()) { + ui.showMessage("You have no reminders."); + return; + } + ui.showMessage("Here is your reminder list:"); + ui.showMessage(reminderList.toString()); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/SetGoalCommand.java b/src/main/java/seedu/financialplanner/commands/SetGoalCommand.java new file mode 100644 index 0000000000..e64026efd4 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/SetGoalCommand.java @@ -0,0 +1,68 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.goal.WishList; + +@SuppressWarnings("unused") +public class SetGoalCommand extends Command { + public static final String NAME = "set"; + + public static final String USAGE = + "set goal "; + public static final String EXAMPLE = + "set goal /g 5000 /l car"; + private final String label; + private final int amount; + + /** + * Constructor for the command to set a goal. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public SetGoalCommand(RawCommand rawCommand) throws IllegalArgumentException { + String labelString = String.join(" ", rawCommand.args); + if (!rawCommand.extraArgs.containsKey("g")) { + throw new IllegalArgumentException("Goal must have an amount"); + } + + String amountString = rawCommand.extraArgs.get("g"); + if (amountString.trim().isEmpty()) { + throw new IllegalArgumentException("Amount must be specified"); + } + try { + amount = Integer.parseInt(amountString); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Amount must be a valid integer"); + } + + if (amount<= 0) { + throw new IllegalArgumentException("Amount must be positive"); + } + + rawCommand.extraArgs.remove("g"); + if (!rawCommand.extraArgs.containsKey("l")) { + throw new IllegalArgumentException("Please specify the content of the goal"); + } + label = rawCommand.extraArgs.get("l"); + + if (label.trim().isEmpty()) { + throw new IllegalArgumentException("Please specify the content of the goal"); + } + rawCommand.extraArgs.remove("l"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to set a goal. + */ + @Override + public void execute() { + assert amount > 0; + WishList.getInstance().addGoal(label, amount); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/VisCommand.java b/src/main/java/seedu/financialplanner/commands/VisCommand.java new file mode 100644 index 0000000000..6f24ae6629 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/VisCommand.java @@ -0,0 +1,81 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.utils.Ui; +import seedu.financialplanner.visualisations.Categorizer; +import seedu.financialplanner.visualisations.Visualizer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Command class that inherit from Command abstract class + * Represents the command to visualize your cash flow + */ +@SuppressWarnings("unused") +public class VisCommand extends Command { + + public static final String NAME = "vis"; + + public static final String USAGE = + "vis "; + + public static final String EXAMPLE = + "vis /t expense /c pie" + "\n" + + "vis /t income /c bar"; + + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private String type; + private String chart; + + /** + * Constructor for the command to visualize cash flow + * + * @param rawCommand + * @throws IllegalArgumentException + */ + public VisCommand(RawCommand rawCommand) throws IllegalArgumentException { + if (!rawCommand.extraArgs.containsKey("t")) { + throw new IllegalArgumentException("Entry type must be defined"); + } + if (!rawCommand.extraArgs.containsKey("c")) { + throw new IllegalArgumentException("Chart type must be defined"); + } + logger.log(Level.INFO, "Parsing entry type and chart type"); + this.type = rawCommand.extraArgs.get("t").toLowerCase().trim(); + rawCommand.extraArgs.remove("t"); + this.chart = rawCommand.extraArgs.get("c").toLowerCase().trim(); + rawCommand.extraArgs.remove("c"); + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to visualize cash flow + * + * @throws FinancialPlannerException + */ + @Override + public void execute() throws FinancialPlannerException { + Ui ui = Ui.getInstance(); + + assert !chart.isEmpty(); + assert !type.isEmpty(); + + HashMap cashflowByType = Categorizer.sortType(CashflowList.getInstance(), type); + if (cashflowByType.isEmpty()) { + ui.printEmptyCashFlow(type); + return; + } + ui.printDisplayChartMessage(type, chart); + Visualizer.displayChart(chart, cashflowByType, type); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/WatchListCommand.java b/src/main/java/seedu/financialplanner/commands/WatchListCommand.java new file mode 100644 index 0000000000..926e6b405e --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/WatchListCommand.java @@ -0,0 +1,63 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.storage.SaveData; +import seedu.financialplanner.utils.Ui; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Command that inherits from Command abstract class + * Represents the command to fetch and display watchlist data + */ +@SuppressWarnings("unused") +public class WatchListCommand extends Command { + + public static final String NAME = "watchlist"; + + public static final String USAGE = + "watchlist"; + public static final String EXAMPLE = + "watchlist"; + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + protected Ui ui = Ui.getInstance(); + protected WatchList watchList = WatchList.getInstance(); + + /** + * Constructor for the command to fetch and display watchlist data + * + * @param rawCommand + * @throws IllegalArgumentException + */ + public WatchListCommand(RawCommand rawCommand) throws IllegalArgumentException{ + if (!rawCommand.extraArgs.isEmpty()) { + logger.log(Level.WARNING, "Invalid extra arguments found"); + String unknownExtraArgument = new ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + assert unknownExtraArgument != null; + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to fetch and display watchlist data + */ + @Override + public void execute() { + ui.printWatchListHeader(); + try { + watchList.getLatestWatchlistInfo(); + + logger.log(Level.INFO, "Printing watchlist"); + ui.printStocksInfo(watchList); + SaveData.saveWatchList(); + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + } + + } +} diff --git a/src/main/java/seedu/financialplanner/commands/WishListCommand.java b/src/main/java/seedu/financialplanner/commands/WishListCommand.java new file mode 100644 index 0000000000..7b979f2d7e --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/WishListCommand.java @@ -0,0 +1,48 @@ +package seedu.financialplanner.commands; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.utils.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class WishListCommand extends Command { + public static final String NAME = "wishlist"; + + public static final String USAGE = "wishlist"; + public static final String EXAMPLE = "wishlist"; + + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + /** + * Constructor of the command to list goals. + * + * @param rawCommand The input from the user. + * @throws IllegalArgumentException if erroneous inputs are detected. + */ + public WishListCommand(RawCommand rawCommand) throws IllegalArgumentException { + if (!rawCommand.extraArgs.isEmpty()) { + String unknownExtraArgument = new java.util.ArrayList<>(rawCommand.extraArgs.keySet()).get(0); + logger.log(Level.WARNING, "Invalid extra arguments found"); + throw new IllegalArgumentException(String.format("Unknown extra argument: %s", unknownExtraArgument)); + } + } + + /** + * Executes the command to list the goals. + */ + @Override + public void execute() { + Ui ui = Ui.getInstance(); + WishList wishList = WishList.getInstance(); + if (wishList.list.isEmpty()) { + ui.showMessage("You have no wish list."); + return; + } + ui.showMessage("Here is your wish list:"); + ui.showMessage(wishList.toString()); + } +} diff --git a/src/main/java/seedu/financialplanner/commands/utils/Command.java b/src/main/java/seedu/financialplanner/commands/utils/Command.java new file mode 100644 index 0000000000..39c9779812 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/utils/Command.java @@ -0,0 +1,8 @@ +package seedu.financialplanner.commands.utils; + +/** + * Represents a generic command that can be inherited. + */ +public abstract class Command { + public abstract void execute() throws Exception; +} diff --git a/src/main/java/seedu/financialplanner/commands/utils/CommandManager.java b/src/main/java/seedu/financialplanner/commands/utils/CommandManager.java new file mode 100644 index 0000000000..9f2999a5ee --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/utils/CommandManager.java @@ -0,0 +1,106 @@ +package seedu.financialplanner.commands.utils; + +import org.reflections.Reflections; +import org.slf4j.simple.SimpleLogger; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +public class CommandManager { + + public static final String COMMAND_CLASS_USAGE_FIELD_NAME = "USAGE"; + + public static final String COMMAND_CLASS_EXAMPLE_FIELD_NAME = "EXAMPLE"; + + public static final String COMMAND_CLASS_NAME_FIELD_NAME = "NAME"; + private static final String COMMAND_PACKAGE_NAME = "seedu.financialplanner.commands"; + + private static final CommandManager instance = new CommandManager(); + private final Map> nameCommandMap = new HashMap<>(); + + private CommandManager() { + System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "ERROR"); + Reflections reflections = new Reflections(COMMAND_PACKAGE_NAME); + Set> commandClasses = reflections.getSubTypesOf(Command.class); + for (Class c : commandClasses) { + if (!Command.class.isAssignableFrom(c)) { + continue; + } + try { + String commandName = (String) c.getField(COMMAND_CLASS_NAME_FIELD_NAME).get(null); + if (nameCommandMap.containsKey(commandName)) { + throw new RuntimeException("Adding command with duplicate name: " + commandName); + } + nameCommandMap.put(commandName, c); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + public static CommandManager getInstance() { + return instance; + } + + @SuppressWarnings("unused") + public Class getCommandClass(String commandName) throws NoSuchElementException { + if (!nameCommandMap.containsKey(commandName)) { + throw new NoSuchElementException("Command not found!"); + } + return nameCommandMap.get(commandName); + } + + @SuppressWarnings("unused") + public String getCommandUsage(Class commandClass) throws NoSuchElementException { + try { + Field usageField = commandClass.getField(COMMAND_CLASS_USAGE_FIELD_NAME); + return (String) usageField.get(null); + } catch (ClassCastException | NoSuchFieldException | IllegalAccessException e) { + throw new IllegalArgumentException("Cannot get command usage. Is there a bug?"); + } + } + + + @SuppressWarnings("unused") + public String getCommandUsage(String commandName) throws NoSuchElementException { + try { + return getCommandUsage(getCommandClass(commandName)); + } catch (Exception e) { + throw new NoSuchElementException(e.getMessage()); + } + } + + @SuppressWarnings("unused") + public String getCommandExample(Class commandClass) throws NoSuchElementException { + try { + Field exampleField = commandClass.getField(COMMAND_CLASS_EXAMPLE_FIELD_NAME); + return (String) exampleField.get(null); + } catch (ClassCastException | NoSuchFieldException | IllegalAccessException e) { + throw new IllegalArgumentException("Cannot get command example. Is there a bug?"); + } + } + + @SuppressWarnings("unused") + public String getCommandExample(String commandName) { + try { + return getCommandExample(getCommandClass(commandName)); + } catch (Exception e) { + throw new NoSuchElementException(e.getMessage()); + } + } + + @SuppressWarnings("unused") + public Set getCommandNames() { + return nameCommandMap.keySet(); + } + + @SuppressWarnings("unused") + public Set> getCommandClasses() { + return new HashSet<>(nameCommandMap.values()); + } + +} diff --git a/src/main/java/seedu/financialplanner/commands/utils/RawCommand.java b/src/main/java/seedu/financialplanner/commands/utils/RawCommand.java new file mode 100644 index 0000000000..da4e26bd66 --- /dev/null +++ b/src/main/java/seedu/financialplanner/commands/utils/RawCommand.java @@ -0,0 +1,28 @@ +package seedu.financialplanner.commands.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RawCommand { + public final List args = new ArrayList<>(); + public final Map extraArgs = new HashMap<>(); + protected String commandName; + + public RawCommand(String name, List args, Map extraArgs) { + this.commandName = name; + this.args.addAll(args); + this.extraArgs.putAll(extraArgs); + } + + @SuppressWarnings("unused") + public String getCommandName() { + return commandName; + } + + @SuppressWarnings("unused") + public void setCommandName(String commandName) { + this.commandName = commandName; + } +} diff --git a/src/main/java/seedu/financialplanner/enumerations/CashflowCategory.java b/src/main/java/seedu/financialplanner/enumerations/CashflowCategory.java new file mode 100644 index 0000000000..9f5c6815ad --- /dev/null +++ b/src/main/java/seedu/financialplanner/enumerations/CashflowCategory.java @@ -0,0 +1,5 @@ +package seedu.financialplanner.enumerations; + +public enum CashflowCategory { + INCOME, EXPENSE, RECURRING +} diff --git a/src/main/java/seedu/financialplanner/enumerations/ExpenseType.java b/src/main/java/seedu/financialplanner/enumerations/ExpenseType.java new file mode 100644 index 0000000000..ab9f512fa9 --- /dev/null +++ b/src/main/java/seedu/financialplanner/enumerations/ExpenseType.java @@ -0,0 +1,11 @@ +package seedu.financialplanner.enumerations; + +import java.util.Arrays; + +public enum ExpenseType { + DINING, ENTERTAINMENT, SHOPPING, TRAVEL, INSURANCE, NECESSITIES, OTHERS; + + public static String[] getNames(Class> e) { + return Arrays.stream(e.getEnumConstants()).map(Enum::name).toArray(String[]::new); + } +} diff --git a/src/main/java/seedu/financialplanner/enumerations/IncomeType.java b/src/main/java/seedu/financialplanner/enumerations/IncomeType.java new file mode 100644 index 0000000000..e4497f2f23 --- /dev/null +++ b/src/main/java/seedu/financialplanner/enumerations/IncomeType.java @@ -0,0 +1,11 @@ +package seedu.financialplanner.enumerations; + +import java.util.Arrays; + +public enum IncomeType { + SALARY, INVESTMENTS, ALLOWANCE, OTHERS; + + public static String[] getNames(Class> e) { + return Arrays.stream(e.getEnumConstants()).map(Enum::name).toArray(String[]::new); + } +} diff --git a/src/main/java/seedu/financialplanner/exceptions/FinancialPlannerException.java b/src/main/java/seedu/financialplanner/exceptions/FinancialPlannerException.java new file mode 100644 index 0000000000..bda7b2ab4b --- /dev/null +++ b/src/main/java/seedu/financialplanner/exceptions/FinancialPlannerException.java @@ -0,0 +1,10 @@ +package seedu.financialplanner.exceptions; + +/** + * Represents exceptions specific to the Financial Planner. + */ +public class FinancialPlannerException extends Exception { + public FinancialPlannerException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/financialplanner/goal/Goal.java b/src/main/java/seedu/financialplanner/goal/Goal.java new file mode 100644 index 0000000000..69ce515587 --- /dev/null +++ b/src/main/java/seedu/financialplanner/goal/Goal.java @@ -0,0 +1,70 @@ +package seedu.financialplanner.goal; + +public class Goal { + private final String label; + private final int amount; + private boolean isDone = false; + + /** + * Constructor for a goal. + * + * @param label The description of the goal. + * @param amount The amount of the goal. + */ + public Goal(String label, int amount) { + this.label = label; + this.amount = amount; + } + + /** + * Constructor for a goal. Used for loading from a file. + * + * @param label The description of the goal. + * @param amount The amount of the goal. + * @param status The status of the goal. + */ + public Goal(String label, int amount, String status) { + this.label = label; + this.amount = amount; + if (status.equals("Achieved")) { + this.isDone = true; + } else { + this.isDone = false; + } + } + + /** + * Formats the goal into an easy-to-read format to be output to the user. + * + * @return The formatted goal. + */ + public String toString() { + String status = isDone ? "Achieved" : "Not Achieved"; + return "Goal " + System.lineSeparator()+ " Label: " + label + System.lineSeparator() + " Amount: " + + amount + System.lineSeparator() + " Status: "+status; + } + + /** + * Marks the goal as done. + */ + public void markAsDone() { + this.isDone = true; + } + + public String getLabel() { + return this.label; + } + public int getAmount() { + return this.amount; + } + + /** + * Formats the goal into an easy-to-read format to be output to the user. + * + * @return The formatted goal. + */ + public String formatString() { + String status = isDone ? "Achieved" : "Not Achieved"; + return "G" + " | " + this.label + " | " + this.amount + " | " + status; + } +} diff --git a/src/main/java/seedu/financialplanner/goal/WishList.java b/src/main/java/seedu/financialplanner/goal/WishList.java new file mode 100644 index 0000000000..8e6b70f0ab --- /dev/null +++ b/src/main/java/seedu/financialplanner/goal/WishList.java @@ -0,0 +1,72 @@ +package seedu.financialplanner.goal; + +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.utils.Ui; + +import java.util.ArrayList; +public class WishList { + private static WishList wishList = null; + public final ArrayList list = new ArrayList<>(); + private WishList() { + } + + public static WishList getInstance() { + if (wishList == null) { + wishList = new WishList(); + } + return wishList; + } + + + /** + * Loads a goal to the wish list from file. + * + * @param goal The goal to be added. + */ + public void load(Goal goal) { + list.add(goal); + } + + + /** + * Deletes a goal from the wish list. + * + * @param index + */ + public void deleteGoal(int index) { + int existingListSize = list.size(); + int listIndex = index; + assert listIndex >= 0 && listIndex < existingListSize; + list.remove(listIndex); + } + + public void addGoal(String label, int amount){ + Goal goal = new Goal(label, amount); + list.add(goal); + Ui.getInstance().showMessage("You have added " + goal); + } + + public void markGoal(int index) { + Goal goal = list.get(index - 1); + goal.markAsDone(); + Ui.getInstance().showMessage("You have achieved " + goal + System.lineSeparator() + "Congratulations!"); + CashflowList.getInstance().addExpense(goal.getAmount(), ExpenseType.OTHERS, 0, goal.getLabel()); + } + /** + * Formats the reminder list into an easy-to-read format to be output to the user. + * + * @return The formatted reminder list. + */ + public String toString() { + String result = ""; + for (int i = 0; i < list.size(); i++) { + if (i == list.size() - 1) { + result += String.format("%d. %s", i + 1, list.get(i)); + } else { + result += String.format("%d. %s\n", i + 1, list.get(i)); + } + } + return result; + } +} diff --git a/src/main/java/seedu/financialplanner/investments/Stock.java b/src/main/java/seedu/financialplanner/investments/Stock.java new file mode 100644 index 0000000000..5a77e6b052 --- /dev/null +++ b/src/main/java/seedu/financialplanner/investments/Stock.java @@ -0,0 +1,205 @@ +package seedu.financialplanner.investments; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Date; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a stock within the financial planner app + */ +public class Stock { + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private static final String API_ENDPOINT = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords="; + ///private static final String API_KEY = "LNKL0548PHY2F0QU"; + private static final String API_KEY = "1AKJMAX4CNWFKSE6"; + private String symbol; + private String exchange; + private String stockName; + private String price; + private String dayHigh; + private String dayLow; + private Date lastUpdated = null; + private long lastFetched = 0; + private int hashCode = 0; + + /** + * Constructor for stock that sets the symbol and searches the api + * for stock name for it + * + * @param symbol + * @throws FinancialPlannerException + */ + public Stock(String symbol) throws FinancialPlannerException { + this.symbol = symbol; + this.stockName = getStockNameFromAPI(symbol); + } + + /** + * Constructor that sets the symbol and stock name directly + * + * @param symbol + * @param stockName + */ + public Stock(String symbol, String stockName) { + this.symbol = symbol; + this.stockName = stockName; + } + + public String getStockName() { + return stockName; + } + + /** + * Method that gets the stock name from the Alpha vantage api. Will throw financial planner exception for any errors + * when attempting this. If succuessful, will return the stock name for the symbol provided + * + * @param symbol + * @return stock name + * @throws FinancialPlannerException + */ + public String getStockNameFromAPI(String symbol) throws FinancialPlannerException { + String requestURI = String.format("%s%s&apikey=%s", API_ENDPOINT,symbol,API_KEY); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder(URI.create(requestURI)) + .header("accept", "application/json") + .GET() + .timeout(Duration.ofSeconds(10)) + .build(); + + logger.log(Level.INFO, "Requesting API for stock info"); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new FinancialPlannerException("API might be down at the moment..."); + } + + Object obj = new JSONParser().parse(response.body()); + + JSONObject jsonObject = (JSONObject) obj; + JSONArray ja = (JSONArray) jsonObject.get("bestMatches"); + if (ja == null) { + throw new FinancialPlannerException("API limit Reached"); + } + if (ja.isEmpty()) { + throw new FinancialPlannerException("Stock not found"); + } + JSONObject stock = (JSONObject) ja.get(0); + String symbolFound = (String) stock.get("1. symbol"); + // TODO: Separate based on market + // TODO: testing + if (!symbolFound.equals(symbol)) { + throw new FinancialPlannerException("Stock not found"); + } + + String region = (String) stock.get("4. region"); + if (!region.equals("United States")) { + throw new FinancialPlannerException("Only US stocks are available due to free nature of API :("); + } + + assert stock.get("2. name") != null; + return (String) stock.get("2. name"); + } catch (IOException e) { + throw new FinancialPlannerException("Error sending request to API.. Are you connected to the internet?"); + } catch (InterruptedException e) { + throw new FinancialPlannerException("Command was interrupted... Try again"); + } catch (ParseException e) { + throw new FinancialPlannerException("Error parsing JSON response from API... Try again"); + } + } + + public void setHashCode() { + if (lastFetched == 0) { + return; + } + this.hashCode = Objects.hashCode(symbol + exchange + stockName + price + dayHigh + + dayLow + lastUpdated + lastFetched); + } + + public int checkHashCode() { + return Objects.hashCode(symbol + exchange + stockName + price + dayHigh + + dayLow + lastUpdated + lastFetched); + } + + public int getHashCode() { + return this.hashCode; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + /** + * toString method is override to output its symbol appended with a comma + * + * @return string representing stock + */ + @Override + public String toString() { + return symbol + ","; + } + + public String getPrice() { + return price; + } + + public void setPrice(String price) { + this.price = price; + } + + public String getExchange() { + return exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getDayHigh() { + return dayHigh; + } + + public void setDayHigh(String dayHigh) { + this.dayHigh = dayHigh; + } + + public String getDayLow() { + return dayLow; + } + + public void setDayLow(String dayLow) { + this.dayLow = dayLow; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public long getLastFetched() { + return lastFetched; + } + + public void setLastFetched(long lastFetched) { + this.lastFetched = lastFetched; + } +} diff --git a/src/main/java/seedu/financialplanner/investments/WatchList.java b/src/main/java/seedu/financialplanner/investments/WatchList.java new file mode 100644 index 0000000000..416c44fda6 --- /dev/null +++ b/src/main/java/seedu/financialplanner/investments/WatchList.java @@ -0,0 +1,327 @@ +package seedu.financialplanner.investments; + +import org.apache.commons.lang3.ObjectUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.storage.LoadData; +import seedu.financialplanner.utils.Ui; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class that represents the watchlist in the financial planner app + */ +public class WatchList { + private static WatchList watchlist = null; + private static final String FILE_PATH = "data/watchlist.json"; + private static Logger logger = Logger.getLogger("Financial Planner Logger"); + private static final String API_ENDPOINT = "https://financialmodelingprep.com/api/v3/quote/"; + private static final String API_KEY = "iFumtYryBCbHpS3sDqLdVKi2SdP63vSV"; + private HashMap stocks; + + /** + * Constructor for the watchlist. It will load up the watchlist data from watchlist.json file and clean up + * any erroneous inputs + */ + private WatchList() { + stocks = LoadData.loadWatchList(FILE_PATH); + cleanUpLoadedWatchList(); + } + + /** + * Method that helps to clean up the watchlist loaded up after application start up + * If watchlist stocks is null, it will help to initialize a new base watchlist. + * It will call another method checkValidStock to clean up the watchlist stocks loaded if it is not empty + */ + private void cleanUpLoadedWatchList() { + if (stocks == null) { + stocks = initalizeNewWatchlist(); + return; + } + stocks.entrySet().removeIf(stockPair -> !checkValidStock(stockPair.getKey(), stockPair.getValue())); + if (stocks.isEmpty()) { + stocks = initalizeNewWatchlist(); + } + } + + /** + * Checks the validity of the stock key, and the Stock class value in the watchlist stocks hashmap + * If it is not valid, it will return false + * + * @param key + * @param stockToCheck + * @return isValid + */ + public boolean checkValidStock(String key, Stock stockToCheck) { + boolean isValid = true; + if (!key.toUpperCase().equals(key)) { + isValid = false; + } + if (stockToCheck.getStockName() == null || stockToCheck.getSymbol() == null) { + isValid = false; + } + if(!key.equals(stockToCheck.getSymbol())) { + isValid = false; + } + if (stockToCheck.getHashCode() == 0) { + if (!ObjectUtils.allNull( + stockToCheck.getPrice(), + stockToCheck.getDayHigh(), + stockToCheck.getDayLow(), + stockToCheck.getLastUpdated(), + stockToCheck.getExchange()) || stockToCheck.getLastFetched() != 0) { + isValid = false; + } + } + if (!isValid) { + Ui.getInstance().printInvalidStockLoaded(key); + } + return isValid; + } + + /** + * Initialize a new watchlist stocks hashmap with base stocks (AAPL and GOOGL) + * + * @return Hashmap of base stocks + */ + public HashMap initalizeNewWatchlist() { + HashMap baseStocks = new HashMap<>(); + Ui.getInstance().showMessage("Initializing New watchlist.. adding AAPL and GOOGL for your reference"); + + Stock apple = new Stock("AAPL", "Apple Inc"); + assert apple.getSymbol() != null && apple.getStockName() != null; + baseStocks.put(apple.getSymbol(), apple); + + Stock google = new Stock("GOOGL", "Alphabet Inc - Class A"); + assert google.getSymbol() != null && google.getStockName() != null; + baseStocks.put(google.getSymbol(), google); + + return baseStocks; + } + + /** + * Method to get the watchlist singleton or create one if it does not exist and returns it + * + * @return watchlist singleton + */ + public static WatchList getInstance() { + if (watchlist == null) { + watchlist = new WatchList(); + } + return watchlist; + } + + /** + * Method to get the latest watchlist info such as price and daily lowest + * + * @throws FinancialPlannerException + */ + public void getLatestWatchlistInfo() throws FinancialPlannerException { + StringBuilder queryStocks = getExpiredStocks(); + fetchFMPStockPrices(queryStocks); + } + + /** + * Checks the watchlist stocks hashmap for stocks that are expired meaning their data should be refreshed using + * the api. Returns a string of stocks that are expired separated by a comma + * + * @return String containing stocks that needs to be queried + */ + public StringBuilder getExpiredStocks() { + StringBuilder queryStocks = new StringBuilder(); + long currentTime = System.currentTimeMillis(); + long tenMin = 600000; + for (Map.Entry set: stocks.entrySet()) { + Stock currentStock = set.getValue(); + if (currentStock.getLastFetched() + tenMin < currentTime) { + queryStocks.append(set.getKey()); + queryStocks.append(","); + } + } + return queryStocks; + } + + /** + * Request the Financial Modeling prep API for the latest stock info using the query stocks parameter passed in + * by the getExpiredStocks method. Will throw a FinancialPlannerException if an error is encountered in the attempt + * If the attempt is successful, it will update the stock information in the watchlist stocks. + * + * @param queryStocks + * @throws FinancialPlannerException + */ + public void fetchFMPStockPrices(StringBuilder queryStocks) throws FinancialPlannerException { + if (stocks.isEmpty()) { + throw new FinancialPlannerException("Empty Watchlist. Nothing to display..."); + } + if (queryStocks.toString().isEmpty()) { + // all stocks prices are up-to-date, just display + return; + } + + HttpClient client = HttpClient.newHttpClient(); + String requestURI = String.format("%s%s?apikey=%s", API_ENDPOINT, queryStocks, API_KEY); + HttpRequest request = HttpRequest.newBuilder(URI.create(requestURI)) + .header("accept", "application/json") + .GET() + .timeout(Duration.ofSeconds(10)) + .build(); + Object obj; + + logger.log(Level.INFO, "Requesting API endpoint FMP"); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if(response.statusCode() != 200) { + throw new FinancialPlannerException("API might be down at the moment..."); + } + obj = new JSONParser().parse(response.body()); + extractWatchlistInfoFromJSONArray((JSONArray) obj); + } catch (IOException e) { + logger.log(Level.SEVERE, "IO exception when sending request or receiving response"); + throw new FinancialPlannerException("Is your internet down?"); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Interrupted"); + throw new FinancialPlannerException("Request to API was interrupted"); + } catch (ParseException e) { + logger.log(Level.SEVERE, "Could not parse to JSON"); + throw new FinancialPlannerException("Could not parse to JSON format"); + } catch (ClassCastException e) { + logger.log(Level.SEVERE, "Did not receive object of class JSON Object"); + throw new FinancialPlannerException("Something went wrong when fetching API. Please try again"); + } + } + + /** + * Method to extract out required information from the full JSON array received from the API + * + * @param jsonStocks + * @throws FinancialPlannerException + */ + public void extractWatchlistInfoFromJSONArray(JSONArray jsonStocks) throws FinancialPlannerException { + if (jsonStocks == null) { + throw new FinancialPlannerException("Incorrect API Response Received. Please try again"); + } + if (jsonStocks.isEmpty()) { + return; + } + long fetchTime = System.currentTimeMillis(); + for (Object jo : jsonStocks) { + JSONObject stock = (JSONObject) jo; + if (stocks.containsKey(stock.get("symbol").toString().toUpperCase())) { + Stock stockLocal = stocks.get(stock.get("symbol").toString().toUpperCase()); + extractStockInfoFromJSONObject(stock, stockLocal); + } + } + setLastFetched(fetchTime); + } + + public void setLastFetched(long fetchTime) { + for (Stock stock : stocks.values()) { + stock.setLastFetched(fetchTime); + } + } + + /** + * Method called by extractWatchlistInfoFromJSONArray to set stock info for individual stocks using information + * obtained from the API (eg. setting latest price) + * + * @param stock + * @param stockLocal + */ + public void extractStockInfoFromJSONObject(JSONObject stock, Stock stockLocal) { + String price = stock.get("price").toString(); + assert price != null; + stockLocal.setPrice(price); + + String exchange = stock.get("exchange").toString(); + assert exchange != null; + stockLocal.setExchange(exchange); + + String dayHigh = stock.get("dayHigh").toString(); + assert dayHigh != null; + stockLocal.setDayHigh(dayHigh); + + String dayLow = stock.get("dayLow").toString(); + assert dayLow != null; + stockLocal.setDayLow(dayLow); + + String timestamp = stock.get("timestamp").toString(); + long lastUpdated = Long.parseLong(timestamp) * 1000; + stockLocal.setLastUpdated(new Date(lastUpdated)); + } + + /** + * Method used to add a new stock to the watchlist that has the stockCode given by the parameter provided + * + * @param stockCode + * @return stockName + * @throws FinancialPlannerException + */ + public String addStock(String stockCode) throws FinancialPlannerException { + if (stocks.size() >= 5) { + throw new FinancialPlannerException("Watchlist is full (max 5). Delete a stock to add a new one"); + } + if (stocks.containsKey(stockCode.toUpperCase())) { + throw new FinancialPlannerException("Stock is already present in Watchlist. Use watchlist to view it!"); + } + if (stocks.containsKey(stockCode)) { + throw new FinancialPlannerException("Stock is already present in Watchlist. Use watchlist to view it!"); + } + + Stock newStock; + newStock = new Stock(stockCode.toUpperCase()); + + assert newStock.getSymbol() != null && newStock.getStockName() != null; + stocks.put(newStock.getSymbol(), newStock); + return newStock.getStockName(); + } + + /** + * Method for deleting a stock from the watchlist that has the stockCode given by the parameter provided + * + * @param stockCode + * @return deleted stock name + * @throws FinancialPlannerException + */ + public String deleteStock(String stockCode) throws FinancialPlannerException { + if (stocks.isEmpty()) { + throw new FinancialPlannerException("No stock in watchlist!"); + } + Stock removedStock = stocks.remove(stockCode); + if (removedStock == null) { + removedStock = stocks.remove(stockCode.toUpperCase()); + } + if (removedStock == null) { + throw new FinancialPlannerException("Does not Exist in Watchlist"); + } + return removedStock.getStockName(); + } + + public int size() { + return stocks.size(); + } + + public Stock get(int index) { + return stocks.get(index); + } + + public HashMap getStocks() { + return stocks; + } + + public void setStocks(HashMap stocks) { + this.stocks = stocks; + } +} diff --git a/src/main/java/seedu/financialplanner/reminder/Reminder.java b/src/main/java/seedu/financialplanner/reminder/Reminder.java new file mode 100644 index 0000000000..35eac48d89 --- /dev/null +++ b/src/main/java/seedu/financialplanner/reminder/Reminder.java @@ -0,0 +1,72 @@ +package seedu.financialplanner.reminder; + + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.Duration; +public class Reminder { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private String type; + private LocalDate date; + private boolean isDone = false; + + /** + * Constructor for a reminder. + * + * @param type The description of the reminder. + * @param date The deadline of the reminder. + */ + public Reminder(String type, LocalDate date) { + this.type = type; + this.date = date; + } + + /** + * Constructor for a reminder. Used for loading from a file. + * + * @param type The description of the reminder. + * @param date The deadline of the reminder. + * @param status The status of the reminder. + */ + public Reminder(String type, String date, String status) { + this.type = type; + this.date = LocalDate.parse(date, FORMATTER); + if (status.equals("Done")) { + this.isDone = true; + } else { + this.isDone = false; + } + } + + /** + * Formats the reminder into an easy-to-read format to be output to the user. + * + * @return The formatted reminder. + */ + public String toString() { + String status = isDone ? "Done" : "Not Done"; + LocalDate currentTime = LocalDate.now(); + Duration duration = Duration.between(currentTime.atStartOfDay(), date.atStartOfDay()); + return "Reminder " + System.lineSeparator() + " Type: " + type + System.lineSeparator() + + " Date: " + date.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + + System.lineSeparator() + " Status: " + status + + System.lineSeparator() + " Left Days: " + duration.toDays(); + } + + /** + * Marks the reminder as done. + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Formats the reminder into a standard format to be saved into a text file. + * + * @return The formatted reminder. + */ + public String formatString() { + String status = isDone ? "Done" : "Not Done"; + return "R" + " | " + this.type + " | " + this.date.format(FORMATTER) + " | " + status; + } +} diff --git a/src/main/java/seedu/financialplanner/reminder/ReminderList.java b/src/main/java/seedu/financialplanner/reminder/ReminderList.java new file mode 100644 index 0000000000..5d3f9d32e2 --- /dev/null +++ b/src/main/java/seedu/financialplanner/reminder/ReminderList.java @@ -0,0 +1,55 @@ +package seedu.financialplanner.reminder; + +import java.util.ArrayList; +public class ReminderList { + private static ReminderList reminderList = null; + public final ArrayList list = new ArrayList<>(); + private ReminderList() { + } + public static ReminderList getInstance() { + if (reminderList == null) { + reminderList = new ReminderList(); + } + return reminderList; + } + + /** + * Loads a reminder into the reminder list. + * + * @param reminder The reminder to be loaded. + */ + public void load(Reminder reminder) { + list.add(reminder); + } + + /** + * Deletes a reminder from the reminder list. + * + * @param index The index of the reminder to be deleted. + */ + public void deleteReminder(int index) { + int existingListSize = list.size(); + int listIndex = index; + assert listIndex >= 0 && listIndex < existingListSize; + Reminder toRemove = list.get(listIndex); + list.remove(listIndex); + } + + + /** + * Formats the reminder list into an easy-to-read format to be output to the user. + * + * @return The formatted reminder list. + */ + public String toString() { + String result = ""; + for (int i = 0; i < list.size(); i++) { + if (i == list.size() - 1) { + result += String.format("%d. %s", i + 1, list.get(i)); + } else { + result += String.format("%d. %s\n", i + 1, list.get(i)); + } + } + return result; + } +} diff --git a/src/main/java/seedu/financialplanner/storage/LoadData.java b/src/main/java/seedu/financialplanner/storage/LoadData.java new file mode 100644 index 0000000000..29af0ce87b --- /dev/null +++ b/src/main/java/seedu/financialplanner/storage/LoadData.java @@ -0,0 +1,391 @@ +package seedu.financialplanner.storage; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.Stock; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Income; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.utils.Ui; +import seedu.financialplanner.goal.Goal; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.reminder.Reminder; +import seedu.financialplanner.reminder.ReminderList; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Scanner; + +/** + * Represents the loading of data from storage. + */ +public abstract class LoadData { + private static final CashflowList cashflowList = CashflowList.getInstance(); + private static final Ui ui = Ui.getInstance(); + private static final ReminderList reminderList = ReminderList.getInstance(); + private static final WishList wishList = WishList.getInstance(); + + /** + * Loads existing data from the storage file. + * Adds recurrences of a cashflow if applicable. + * + * @param filePath The file where the data is stored. + * @param date The current date. + * @throws FinancialPlannerException If there is an issue loading data from storage. + */ + public static void load(String filePath, LocalDate date) throws FinancialPlannerException { + try { + Scanner inputFile = new Scanner(new FileReader(filePath)); + String line; + ui.showMessage("Loading existing file..."); + + while (inputFile.hasNext()) { + line = inputFile.nextLine(); + String[] split = line.split("\\|"); + String type = split[0].trim(); + switch (type) { + case "I": + // Fallthrough + case "E": + final Cashflow entry = getEntry(type, split); + cashflowList.load(entry); + break; + case "B": + loadBudget(split); + break; + case "R": + final Reminder reminder = getReminder(split); + reminderList.load(reminder); + break; + case "G": + final Goal goal = getGoal(split); + wishList.load(goal); + break; + default: + throw new FinancialPlannerException("Error loading file."); + } + } + inputFile.close(); + deleteFutureCashflows(date); + addRecurringCashflows(date); + } catch (IOException e) { + ui.showMessage("File not found. Creating new file..."); + } catch (IndexOutOfBoundsException e) { + handleCorruptedFile("Empty/Missing arguments detected."); + } catch (IllegalArgumentException | FinancialPlannerException e) { + handleCorruptedFile(e.getMessage()); + } catch (DateTimeParseException e) { + handleCorruptedFile("Erroneous date format or Wrong position of date detected."); + } + } + + private static void deleteFutureCashflows(LocalDate currentDate) { + ArrayList tempCashflowList = new ArrayList<>(); + int indexToDelete = 0; + for (Cashflow cashflow : cashflowList.list) { + int recur = cashflow.getRecur(); + LocalDate dateOfAddition = cashflow.getDate(); + if (recur > 0 && currentDate.isBefore(dateOfAddition)) { + Integer integer = indexToDelete; + tempCashflowList.add(integer); + } + indexToDelete++; + } + if (!tempCashflowList.isEmpty()) { + ui.showMessage("Detected erroneous cashflow entries. Removing future cashflows..."); + for (int i = 0; i < tempCashflowList.size(); i++) { + indexToDelete = tempCashflowList.get(i) - i; + // deleteCashflowWithoutCategory takes in list index starting from 1, indexToDelete starts from 0 + int indexStartingFromOne = indexToDelete + 1; + cashflowList.deleteCashflowWithoutCategory(indexStartingFromOne); + } + } + } + + private static void addRecurringCashflows(LocalDate currentDate) throws FinancialPlannerException { + ui.showMessage("Adding any recurring cashflows..."); + ArrayList tempCashflowList = new ArrayList<>(); + for (Cashflow cashflow : cashflowList.list) { + int recur = cashflow.getRecur(); + LocalDate dateOfAddition = cashflow.getDate(); + boolean hasRecurred = cashflow.getHasRecurred(); + identifyRecurringCashflows(currentDate, cashflow, recur, dateOfAddition, tempCashflowList, hasRecurred); + } + for (Cashflow cashflow : tempCashflowList) { + cashflowList.load(cashflow); + ui.printAddedCashflowWithoutBalance(cashflow); + } + if (!tempCashflowList.isEmpty()) { + ui.printBalance(); + } + } + + private static void identifyRecurringCashflows(LocalDate currentDate + , Cashflow cashflow, int recur, LocalDate dateOfAddition + , ArrayList tempCashflowList, boolean hasRecurred) throws FinancialPlannerException { + if (recur > 0 && !hasRecurred) { + dateOfAddition = dateOfAddition.plusDays(recur); + addRecurringCashflowToTempList(currentDate, cashflow, recur, dateOfAddition, tempCashflowList); + } + } + + private static void addRecurringCashflowToTempList(LocalDate currentDate + , Cashflow cashflow, int recur, LocalDate dateOfAddition + , ArrayList tempCashflowList) throws FinancialPlannerException { + while (currentDate.isAfter(dateOfAddition) || currentDate.isEqual(dateOfAddition)) { + cashflow.setHasRecurred(true); + Cashflow toAdd; + if (cashflow instanceof Income) { + toAdd = new Income((Income) cashflow); + } else if (cashflow instanceof Expense) { + toAdd = new Expense((Expense) cashflow); + } else { + throw new FinancialPlannerException("Error adding recurring cashflows."); + } + toAdd.setDate(dateOfAddition); + addToTempList(tempCashflowList, toAdd); + cashflow = toAdd; + dateOfAddition = dateOfAddition.plusDays(recur); + } + } + + private static void addToTempList(ArrayList tempCashflowList, Cashflow toAdd) { + tempCashflowList.add(toAdd); + } + + private static void handleCorruptedFile(String message) throws FinancialPlannerException { + ui.showMessage("File appears to be corrupted. Do you want to create a new file? (Y/N)"); + if (createNewFile()) { + cashflowList.list.clear(); + Cashflow.clearBalance(); + } else { + throw new FinancialPlannerException("Please fix the corrupted file, " + + "which can be found in data/data.txt.\nError message: " + message); + } + } + + private static void loadBudget(String[] split) throws IllegalArgumentException { + double initial; + double current; + try { + initial = Double.parseDouble(split[1].trim()); + current = Double.parseDouble(split[2].trim()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing budget values."); + } + if (initial == 0 && current == 0) { + return; + } + if (initial < 0 || current < 0) { + throw new IllegalArgumentException("Negative values for budget."); + } + if (initial > Cashflow.getBalance() || current > Cashflow.getBalance()) { + throw new IllegalArgumentException("Budget exceeds balance."); + } + if (initial < current) { + throw new IllegalArgumentException("Current budget exceeds initial budget."); + } + LocalDate date = LocalDate.parse(split[3].trim(), DateTimeFormatter.ofPattern("dd/MM/yyyy")); + if (LocalDate.now().isBefore(date)) { + throw new IllegalArgumentException("Current date is before saved date."); + } + Budget.load(initial, current, date); + } + + private static boolean createNewFile() { + String line = ui.input(); + while (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("n")) { + ui.showMessage("Unknown input. Please enter Y or N only."); + line = ui.input(); + } + + return line.equalsIgnoreCase("y"); + } + + private static Cashflow getEntry(String type, String[] split) + throws FinancialPlannerException, IllegalArgumentException, DateTimeParseException + , IndexOutOfBoundsException { + try { + Cashflow entry; + double value = Double.parseDouble(split[1].trim()); + int recur = Integer.parseInt(split[3].trim()); + boolean hasRecurred = getHasRecurred(split, recur); + LocalDate date = getDate(split, recur); + int index = getIndex(recur); + String description = getDescription(split, index); + checkValidInput(value, recur); + + switch (type) { + case "I": + IncomeType incomeType; + incomeType = IncomeType.valueOf(split[2].trim().toUpperCase()); + entry = new Income(value, incomeType, recur, description, date, hasRecurred); + break; + case "E": + ExpenseType expenseType; + expenseType = ExpenseType.valueOf(split[2].trim().toUpperCase()); + entry = new Expense(value, expenseType, recur, description, date, hasRecurred); + break; + default: + throw new FinancialPlannerException("Error loading file."); + } + return entry; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Erroneous arguments detected."); + } + } + + private static Reminder getReminder(String[] split) throws IllegalArgumentException, IndexOutOfBoundsException, + FinancialPlannerException { + try { + Reminder entry; + String type = split[1].trim(); + String date = split[2].trim(); + String status = split[3].trim(); + entry = new Reminder(type, date, status); + return entry; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Erroneous arguments detected."); + } catch (IndexOutOfBoundsException e) { + throw new FinancialPlannerException("There should be three data members for reminder."); + } + } + + private static Goal getGoal(String[] split) throws IllegalArgumentException { + try { + Goal entry; + String type = split[1].trim(); + int amount = Integer.parseInt(split[2].trim()); + String status = split[3].trim(); + entry = new Goal(type, amount, status); + return entry; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Erroneous arguments detected."); + } + } + + private static boolean getHasRecurred(String[] split, int recur) throws IllegalArgumentException { + boolean hasRecurred; + if (recur != 0) { + String stringHasRecurred = split[4].trim(); + if (stringHasRecurred.equals("true") || stringHasRecurred.equals("false")) { + hasRecurred = Boolean.parseBoolean(stringHasRecurred); + } else { + throw new IllegalArgumentException(); + } + } else { + hasRecurred = false; + } + return hasRecurred; + } + + private static LocalDate getDate(String[] split, int recur) { + LocalDate date; + if (recur != 0) { + date = LocalDate.parse(split[5].trim(), DateTimeFormatter.ofPattern("dd/MM/uuuu") + .withResolverStyle(ResolverStyle.STRICT)); + } else { + date = null; + } + return date; + } + + private static int getIndex(int recur) { + int index; + if (recur != 0) { + index = 6; + } else { + index = 5; + } + return index; + } + + private static String getDescription(String[] split, int index) { + String description; + if (split.length > index) { + description = split[index].trim(); + } else { + description = null; + } + return description; + } + + /** + * Load the watchlist.json file into the application on startup as a hashmap. + * + * @return Hashmap of loaded stocks + */ + public static HashMap loadWatchList(String filePath) { + Ui ui = Ui.getInstance(); + Gson gson = new Gson(); + HashMap stocksData = null; + ui.showMessage("Loading existing watchlist.."); + try { + JsonReader reader = new JsonReader(new FileReader(filePath)); + stocksData = gson.fromJson(reader, new TypeToken>(){}.getType()); + if (stocksData.size() > 5) { + throw new FinancialPlannerException("You have more than 5 entries in watchlist.json"); + } + if (!checkHashCode(stocksData)) { + throw new FinancialPlannerException("watchlist.json values were edited. " + + "Please do not change the generated values!"); + } + } catch (FileNotFoundException e) { + ui.showMessage("Watchlist file not found... Creating"); + } catch (JsonSyntaxException e) { + ui.showMessage("Watchlist JSON is corrupted!"); + ui.showMessage("Would you like to create new file? (Y/N)"); + if (!createNewFile()) { + ui.showMessage("Exiting... Please fix the file."); + System.exit(1); + } + } catch (FinancialPlannerException e) { + ui.showMessage(e.getMessage()); + ui.showMessage("Would you like to create new watchlist? (Y/N)"); + if (!createNewFile()) { + ui.showMessage("Exiting... Please fix the file."); + System.exit(1); + } + stocksData = null; + } + return stocksData; + } + + private static void checkValidInput(double value, int recur) throws FinancialPlannerException { + if (value < 0) { + throw new FinancialPlannerException("Amount cannot be negative."); + } + if (value > 999999999999.99) { + throw new FinancialPlannerException("Amount exceeded maximum value this program can hold."); + } + if (recur < 0) { + throw new FinancialPlannerException("Recurring value cannot be negative."); + } + } + + private static boolean checkHashCode(HashMap stocksData) { + for (HashMap.Entry stock : stocksData.entrySet()) { + if (stock.getValue().getHashCode() == 0) { + continue; + } + if (stock.getValue().checkHashCode() != stock.getValue().getHashCode()) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/seedu/financialplanner/storage/SaveData.java b/src/main/java/seedu/financialplanner/storage/SaveData.java new file mode 100644 index 0000000000..66e1bfb7c8 --- /dev/null +++ b/src/main/java/seedu/financialplanner/storage/SaveData.java @@ -0,0 +1,72 @@ +package seedu.financialplanner.storage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.investments.Stock; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Ui; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; + +/** + * Represents the saving of data to storage. + */ +public abstract class SaveData { + private static final String FILE_PATH = "data/watchlist.json"; + private static final CashflowList cashflowList = CashflowList.getInstance(); + private static final ReminderList reminderList = ReminderList.getInstance(); + private static final WishList wishList = WishList.getInstance(); + + public static void save(String filePath) throws FinancialPlannerException { + try { + FileWriter fw = new FileWriter(filePath); + for (Cashflow entry : cashflowList.list) { + fw.write(entry.formatString() + "\n"); + } + if (Budget.hasBudget()) { + fw.write(Budget.formatString() + "\n"); + } + for (int i = 0; i < reminderList.list.size(); i++) { + fw.write(reminderList.list.get(i).formatString() + "\n"); + } + for (int i = 0; i < wishList.list.size(); i++) { + fw.write(wishList.list.get(i).formatString() + "\n"); + } + fw.close(); + } catch (IOException e) { + throw new FinancialPlannerException("Error saving file."); + } + } + + /** + * Method to save the current watchlist to watchlist.json file + * + */ + public static void saveWatchList() { + Ui ui = Ui.getInstance(); + WatchList wl = WatchList.getInstance(); + setHashCode(wl.getStocks()); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + try { + FileWriter fileWriter = new FileWriter(FILE_PATH); + gson.toJson(wl.getStocks(), fileWriter); + fileWriter.close(); + } catch (IOException e) { + ui.showMessage("Unable to save watchlist to file"); + } + } + + private static void setHashCode(HashMap stocks) { + for (HashMap.Entry stock : stocks.entrySet()) { + stock.getValue().setHashCode(); + } + } +} diff --git a/src/main/java/seedu/financialplanner/storage/Storage.java b/src/main/java/seedu/financialplanner/storage/Storage.java new file mode 100644 index 0000000000..03ed7e25a0 --- /dev/null +++ b/src/main/java/seedu/financialplanner/storage/Storage.java @@ -0,0 +1,46 @@ +package seedu.financialplanner.storage; + +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.utils.Ui; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; + +/** + * Represents the saving and loading of data. + */ +public class Storage { + private static Storage storage = null; + private final Path path = Paths.get("data"); + + private Storage() { + if (!Files.exists(path)) { + try { + Ui.getInstance().showMessage("Directory doesn't exist. Creating directory..."); + Files.createDirectory(path); + } catch (IOException e) { + Ui.getInstance().showMessage("Error creating directory: " + e.getMessage()); + } + } + } + + public static Storage getInstance() { + if (storage == null) { + storage = new Storage(); + } + return storage; + } + + public void load(String filePath, LocalDate date) throws FinancialPlannerException { + LoadData.load(filePath, date); + } + + public void save(String filePath) throws FinancialPlannerException { + SaveData.save(filePath); + SaveData.saveWatchList(); + } + +} diff --git a/src/main/java/seedu/financialplanner/utils/Parser.java b/src/main/java/seedu/financialplanner/utils/Parser.java new file mode 100644 index 0000000000..b6d78559b6 --- /dev/null +++ b/src/main/java/seedu/financialplanner/utils/Parser.java @@ -0,0 +1,109 @@ +package seedu.financialplanner.utils; + +import seedu.financialplanner.commands.utils.Command; +import seedu.financialplanner.commands.utils.CommandManager; +import seedu.financialplanner.commands.utils.RawCommand; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +public class Parser { + public static Command parseCommand(String input) throws FinancialPlannerException { + RawCommand rawCommand = parseRawCommand(input); + return parseCommand(rawCommand); + } + + public static Command parseCommand(RawCommand rawCommand) throws FinancialPlannerException { + CommandManager commandManager = CommandManager.getInstance(); + Class commandClass; + + try { + commandClass = commandManager.getCommandClass(rawCommand.getCommandName().toLowerCase()); + } catch (NoSuchElementException e) { + throw new FinancialPlannerException("Unknown command. Type help for help."); + } catch (Exception e) { + throw new FinancialPlannerException(e.getMessage()); + } + + Constructor constructorWithRawCommand; + Constructor constructorWithNothing; + + try { + constructorWithRawCommand = commandClass.getConstructor(RawCommand.class); + return constructorWithRawCommand.newInstance(rawCommand); + } catch (NoSuchMethodException noConstructorWithRawCommandException) { + try { + constructorWithNothing = commandClass.getConstructor(); + return constructorWithNothing.newInstance(); + } catch (InvocationTargetException e) { + throw new FinancialPlannerException(e.getCause().getMessage()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } catch (InvocationTargetException e) { + throw new FinancialPlannerException(e.getCause().getMessage()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static RawCommand parseRawCommand(String input) throws IllegalArgumentException { + Iterator iterator = Arrays.stream(input.split(" ")).iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException("Command cannot be empty"); + } + String commandName = iterator.next(); + List args = new ArrayList<>(); + Map extraArgs = new HashMap<>(); + + List extraArgumentContentBuffer = new ArrayList<>(); + String currentExtraArgumentName = null; + + while (iterator.hasNext()) { + String next = iterator.next(); + if (next.startsWith("/")) { + // Save previous extra argument when next extra argument is found + if (currentExtraArgumentName != null) { + savePreviousExtraArgument(extraArgs, currentExtraArgumentName, extraArgumentContentBuffer); + } + if (next.length() == 1) { + throw new IllegalArgumentException("Extra argument name cannot be empty"); + } + + currentExtraArgumentName = next.substring(1); + + } else { + if (currentExtraArgumentName == null) { + args.add(next); + } else { + extraArgumentContentBuffer.add(next); + } + } + } + // Save previous extra argument at the very end + if (currentExtraArgumentName != null) { + savePreviousExtraArgument(extraArgs, currentExtraArgumentName, extraArgumentContentBuffer); + } + + return new RawCommand(commandName, args, extraArgs); + } + + private static void savePreviousExtraArgument(Map extraArgs + , String currentExtraArgumentName, List extraArgumentContentBuffer) { + if (extraArgs.containsKey(currentExtraArgumentName)) { + throw new IllegalArgumentException( + String.format("Duplicate extra argument name: %s", currentExtraArgumentName)); + } else { + extraArgs.put(currentExtraArgumentName, String.join(" ", extraArgumentContentBuffer)); + extraArgumentContentBuffer.clear(); + } + } +} diff --git a/src/main/java/seedu/financialplanner/utils/Ui.java b/src/main/java/seedu/financialplanner/utils/Ui.java new file mode 100644 index 0000000000..5e84c07486 --- /dev/null +++ b/src/main/java/seedu/financialplanner/utils/Ui.java @@ -0,0 +1,261 @@ +package seedu.financialplanner.utils; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import seedu.financialplanner.investments.Stock; +import seedu.financialplanner.investments.WatchList; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Map; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the handling of user interactions. + */ +public class Ui { + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + private static Ui ui = null; + private static final String RED = "\u001B[31m"; + private static final String GREEN = "\u001B[32m"; + private static final String RESET = "\u001B[0m"; + private static final String YELLOW = "\u001B[33m"; + private static final String CYAN = "\u001B[36m"; + private Scanner scanner = new Scanner(System.in); + private Ui() { + } + + public static Ui getInstance() { + if (ui == null) { + ui = new Ui(); + } + return ui; + } + + public static void printCorruptedFileError(String message) { + System.out.println(message); + } + + public Scanner getScanner() { + return scanner; + } + + public void setScanner(Scanner scanner) { + this.scanner = scanner; + } + + public void showMessage(String message) { + assert !message.isEmpty(); + System.out.println(CYAN + message + RESET); + } + + public void welcomeMessage() { + showMessage("Welcome to your Financial Planner. Type something to get started."); + } + + public void exitMessage() { + showMessage("Exiting Financial Planner. Goodbye."); + } + + public String input() { + return scanner.nextLine().trim(); + } + + public String userInput() { + if (scanner.hasNextLine()) { + return scanner.nextLine().trim(); + } + System.exit(1); + return ""; + } + + public void printWatchListHeader() { + System.out.print(CYAN + "Symbol" + RESET); + System.out.print(" "); + System.out.print(CYAN + "Market" + RESET); + System.out.print(" "); + System.out.print(YELLOW + "Price" + RESET); + System.out.print(" "); + System.out.print(GREEN + "Daily High" + RESET); + System.out.print(" "); + System.out.print(RED + "Daily Low" + RESET); + System.out.print(" "); + System.out.print("EquityName"); + System.out.print(" "); + System.out.print("Last Updated"); + System.out.print(" "); + System.out.println(); + } + + public void printWatchListAcknowledgement() { + showMessage("Data provided by Financial Modeling Prep and Alpha Vantage =)"); + } + + + public void printStocksInfo(WatchList watchList) { + for (Map.Entry set : watchList.getStocks().entrySet()) { + Stock stock = set.getValue(); + + if (!ObjectUtils.allNotNull( + stock.getPrice(), + stock.getDayHigh(), + stock.getDayLow(), + stock.getLastUpdated(), + stock.getExchange() + )) { + showMessage(stock.getStockName() + " (" + stock.getSymbol() + ") is not found on FMP"); + continue; + } + + String symbol = StringUtils.rightPad(stock.getSymbol(), 10); + String market = StringUtils.rightPad(stock.getExchange(), 10); + String price = YELLOW + StringUtils.rightPad(stock.getPrice(), 10) + RESET; + String dayHigh = GREEN + StringUtils.rightPad(stock.getDayHigh(), 15) + RESET; + String dayLow = RED + StringUtils.rightPad(stock.getDayLow(), 14) + RESET; + String name = StringUtils.rightPad(stock.getStockName(), 33); + String date = new SimpleDateFormat("E, MMM dd yyyy HH:mm:ss") + .format(stock.getLastUpdated()); + String lastUpdate = StringUtils.rightPad(date, 10); + showMessage(symbol + market + price + dayHigh + dayLow + name + lastUpdate); + } + printWatchListAcknowledgement(); + } + + public void printAddStock(String stockName) { + showMessage("You have successfully added:"); + showMessage(stockName); + showMessage("Use Watchlist to view it!"); + } + + public void printDeleteStock(String stockName) { + showMessage("You have successfully deleted: "); + showMessage(stockName); + showMessage("Use watchlist command to view updated Watchlist"); + } + + public String formatBalance(double balance) { + DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + + return decimalFormat.format(Cashflow.round(balance, 2)); + } + + public void printAddedCashflow(Cashflow entry) { + showMessage("You have added an " + entry); + showMessage("to the Financial Planner."); + showMessage("Balance: " + formatBalance(Cashflow.getBalance())); + } + + public void printAddedCashflowWithoutBalance(Cashflow entry) { + showMessage("You have added an " + entry); + showMessage("to the Financial Planner."); + } + + public void printBalance() { + showMessage("Balance: " + formatBalance(Cashflow.getBalance())); + } + + public void printDeletedCashflow(Cashflow entry) { + showMessage("You have removed an " + entry); + showMessage("from the Financial Planner."); + showMessage("Balance: " + formatBalance(Cashflow.getBalance())); + } + + public void printDeletedRecur(Cashflow entry) { + showMessage("You have removed future recurrences of this cashflow."); + showMessage("Updated cashflow:"); + showMessage(entry.toString()); + } + + public void printBudgetBeforeUpdate() { + showMessage("Budget has been updated:\nOld initial budget: " + + Budget.getInitialBudgetString() + "\nOld current budget: " + + Budget.getCurrentBudgetString()); + } + + public void printBudgetAfterUpdate() { + showMessage("New initial budget: " + Budget.getInitialBudgetString() + + "\nNew current budget: " + Budget.getCurrentBudgetString()); + if (Budget.getCurrentBudget() <= 0) { + showMessage("You have exceeded your budget, please update to a larger budget or " + + "reset the current budget to initial budget."); + } + } + + public void printBudgetAfterDeduction() { + StringBuilder message = new StringBuilder(); + message.append("Your remaining budget for the month is: ").append(Budget.getCurrentBudgetString()); + if (Budget.getCurrentBudget() <= 0) { + message.append("Oops, you ran out of budget, please update to a larger budget or " + + "reset the current budget to initial budget."); + } + + showMessage(message.toString()); + } + + public void printBudget() { + showMessage("You have a remaining budget of " + Budget.getCurrentBudgetString() + "."); + } + + public void printDeleteBudget() { + showMessage("Budget has been deleted."); + } + + public void printResetBudget() { + showMessage("Budget has been reset to " + Budget.getInitialBudgetString() + "."); + } + + public void printDisplayChartMessage(String type, String chart) { + showMessage("Displaying " + chart + "chart for " + type); + } + + public void printOverview(String... args) { + String balance = args[0]; + String income = args[1]; + String expense = args[2]; + String budget = args[3]; + String reminders = args[4]; + String wishlist = args[5]; + + showMessage("Here is an overview of your financials:\n" + "Total balance: " + balance + "\n" + + "Highest income: " + income + "\n" + "Highest expense: " + expense + "\n" + + "Remaining budget for the month: " + budget + "\n\n" + "Reminders:\n" + reminders + + "\n\nWishlist:\n" + wishlist); + } + + public void printSetBudget() { + showMessage("A monthly budget of " + Budget.getInitialBudgetString() + " has been set."); + } + + public void printBudgetExceedBalance() { + showMessage("Since initial budget exceeds current balance, budget will be reset to current balance."); + } + + public void printBudgetError(String errorType) { + switch (errorType) { + case "delete": + showMessage("Budget has not been set yet."); + break; + case "reset": + showMessage("Budget has not been spent yet."); + break; + case "view": + showMessage("There is no existing budget."); + break; + default: + logger.log(Level.SEVERE, "Unreachable default case reached"); + showMessage("Unknown command"); + } + } + + public void printEmptyCashFlow(String type) { + showMessage(StringUtils.capitalize(type) + " is empty... Nothing to visualize"); + } + + public void printInvalidStockLoaded(String key) { + showMessage(RED + "Could not load " + key + " due to incorrect format. Check the UG for correct format"); + } +} diff --git a/src/main/java/seedu/financialplanner/visualisations/Categorizer.java b/src/main/java/seedu/financialplanner/visualisations/Categorizer.java new file mode 100644 index 0000000000..add79f8b34 --- /dev/null +++ b/src/main/java/seedu/financialplanner/visualisations/Categorizer.java @@ -0,0 +1,81 @@ +package seedu.financialplanner.visualisations; + +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.cashflow.Income; + +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Class that is used to sort the cash flow list into different categories according to the type they are + * (Income, Expense) + */ +public class Categorizer { + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + /** + * Method that calls the required methods to sort the cash flow based on the type is specified by the user + * (Income, Expense) + * + * @param cashflowList + * @param type + * @return hashmap of sorted income/expense according to category + * @throws FinancialPlannerException + */ + public static HashMap sortType(CashflowList cashflowList, String type) + throws FinancialPlannerException { + switch (type.toLowerCase()) { + case "expense": + logger.log(Level.INFO, "categorizing expenses"); + return sortExpenses(cashflowList); + case "income": + logger.log(Level.INFO, "categorizing income"); + return sortIncome(cashflowList); + default: + throw new FinancialPlannerException(type + " Type not found"); + } + } + + /** + * Method to sort the expenses of the cash flow list into different categories and return the sorted hashmap + * + * @param cashflowList + * @return Hashmap of sorted expenses according to category + */ + public static HashMap sortExpenses(CashflowList cashflowList) { + HashMap expensesByCat = new HashMap<>(); + for (Cashflow e: cashflowList.list) { + if (e instanceof Expense) { + String key = e.getExpenseType().toString().toLowerCase(); + double value = expensesByCat.getOrDefault(key, 0.0) + e.getAmount(); + assert value >= 0; + expensesByCat.put(key, value); + } + } + return expensesByCat; + } + + /** + * Method to sort the incomes of the cash flow list into different categories and return the sorted hashmap + * + * @param cashflowList + * @return Hashmap containing income sorted according to category + */ + public static HashMap sortIncome(CashflowList cashflowList) { + HashMap incomeByCat = new HashMap<>(); + for (Cashflow e: cashflowList.list) { + if (e instanceof Income) { + String key = e.getIncomeType().toString().toLowerCase(); + double value = incomeByCat.getOrDefault(key, 0.0) + e.getAmount(); + assert value >= 0; + incomeByCat.put(key, value); + } + } + return incomeByCat; + } +} diff --git a/src/main/java/seedu/financialplanner/visualisations/Visualizer.java b/src/main/java/seedu/financialplanner/visualisations/Visualizer.java new file mode 100644 index 0000000000..e5032d0a09 --- /dev/null +++ b/src/main/java/seedu/financialplanner/visualisations/Visualizer.java @@ -0,0 +1,187 @@ +package seedu.financialplanner.visualisations; + + +import org.apache.commons.lang3.StringUtils; +import org.knowm.xchart.PieChart; +import org.knowm.xchart.PieChartBuilder; +import org.knowm.xchart.SwingWrapper; +import org.knowm.xchart.CategoryChart; +import org.knowm.xchart.CategoryChartBuilder; +import org.knowm.xchart.style.Styler; +import org.knowm.xchart.RadarChart; +import org.knowm.xchart.RadarChartBuilder; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class the helps to output the visualization of the cash flow to the user so that the user can easily view their + * expense/income based on types + */ +public class Visualizer { + private static final Logger logger = Logger.getLogger("Financial Planner Logger"); + + /** + * Method that calls the appropriate method for printing the different visualizations tools based on the + * preference of the user (pie,bar,radar) + * + * @param chart + * @param cashFlowByCat + * @param type + * @throws FinancialPlannerException + */ + public static void displayChart(String chart, HashMap cashFlowByCat, String type) + throws FinancialPlannerException { + switch (chart.toLowerCase()) { + case "pie": + displayPieChart(cashFlowByCat, type); + break; + case "bar": + displayBarChart(cashFlowByCat, type); + break; + case "radar": + displayRadarChart(cashFlowByCat, type); + break; + default: + throw new FinancialPlannerException(chart + " Chart Type Not Found"); + } + } + + /** + * Method to display the pier chart to the screen + * + * @param cashflowByCat + * @param type + */ + public static void displayPieChart (HashMap cashflowByCat, String type) { + PieChart chart = new PieChartBuilder().width(800).height(600) + .title(StringUtils.capitalize(type) + " Chart") + .build(); + + // Customize Chart + Color[] sliceColors = new Color[] { + new Color(21, 224, 14), + new Color(62, 154, 230), + new Color(236, 186, 110), + new Color(243, 159, 242), + new Color(246, 182, 197), + new Color(210, 24, 24), + new Color(211, 164, 8), + }; + chart.getStyler().setSeriesColors(sliceColors); + + for (Map.Entry set: cashflowByCat.entrySet()) { + chart.addSeries(set.getKey(), set.getValue()); + } + logger.log(Level.INFO, "Displaying Pie Chart"); + // Show it + JFrame swHR = new SwingWrapper<>(chart).displayChart(); + javax.swing.SwingUtilities.invokeLater( + ()->swHR.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) + ); + } + + /** + * Method to display the bar chart to the screen + * + * @param cashflowByCat + * @param type + */ + public static void displayBarChart (HashMap cashflowByCat, String type) { + CategoryChart chart = new CategoryChartBuilder().width(800).height(600) + .title(StringUtils.capitalize(type) + " Chart") + .xAxisTitle(StringUtils.capitalize(type) + " Type") + .yAxisTitle("Value") + .build(); + + // Customize Chart + chart.getStyler().setLegendPosition(Styler.LegendPosition.InsideNW); + assert !cashflowByCat.isEmpty(); + // Series + List values = new ArrayList(); + List keys = new ArrayList(); + for (Map.Entry set : cashflowByCat.entrySet()) { + keys.add(set.getKey()); + values.add(set.getValue()); + } + chart.addSeries(StringUtils.capitalize(type), keys, values); + + logger.log(Level.INFO, "Displaying Bar Chart"); + JFrame swHR = new SwingWrapper<>(chart).displayChart(); + SwingUtilities.invokeLater( + ()->swHR.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) + ); + } + + + /** + * Method to display the radar chart to the screen + * + * @param cashflowByCat + * @param type + * @throws FinancialPlannerException + */ + public static void displayRadarChart (HashMap cashflowByCat, String type) + throws FinancialPlannerException { + RadarChart radarChart = new RadarChartBuilder().width(800).height(600) + .title(StringUtils.capitalize(type) + " Chart") + .build(); + + // Customize Chart + Color[] sliceColors = new Color[] { + new Color(62, 154, 230), + }; + + radarChart.getStyler().setSeriesColors(sliceColors); + + String[] keys; + switch (type.toLowerCase()) { + case "income": + keys = IncomeType.getNames(IncomeType.class); + break; + case "expense": + keys = ExpenseType.getNames(ExpenseType.class); + break; + default: + throw new FinancialPlannerException("Error displaying RadarChart"); + } + if (keys.length == 0) { + throw new FinancialPlannerException("Error displaying RadarChart"); + } + double[] values = new double[keys.length]; + double max = 1; + for (int i = 0; i < keys.length; i += 1) { + if (cashflowByCat.containsKey(keys[i].toLowerCase())) { + values[i] = cashflowByCat.get(keys[i].toLowerCase()); + max = Math.max(values[i], max); + } else { + values[i] = 0; + } + } + + for (int i = 0; i < keys.length; i += 1) { + values[i] /= max; + } + + radarChart.setRadiiLabels(keys); + radarChart.addSeries(type, values); + + logger.log(Level.INFO, "Displaying Radar Chart"); + JFrame swHR = new SwingWrapper<>(radarChart).displayChart(); + SwingUtilities.invokeLater( + ()->swHR.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) + ); + } + + +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/financialplanner/FinancialPlannerTest.java similarity index 73% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/seedu/financialplanner/FinancialPlannerTest.java index 2dda5fd651..22f624b2d5 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/financialplanner/FinancialPlannerTest.java @@ -1,10 +1,10 @@ -package seedu.duke; +package seedu.financialplanner; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -class DukeTest { +class FinancialPlannerTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/seedu/financialplanner/cashflow/BudgetTest.java b/src/test/java/seedu/financialplanner/cashflow/BudgetTest.java new file mode 100644 index 0000000000..1235919987 --- /dev/null +++ b/src/test/java/seedu/financialplanner/cashflow/BudgetTest.java @@ -0,0 +1,100 @@ +package seedu.financialplanner.cashflow; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import seedu.financialplanner.commands.AddCashflowCommand; +import seedu.financialplanner.utils.Parser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestMethodOrder(OrderAnnotation.class) +public class BudgetTest { + @Test + @Order(1) + public void testSetBudget() { + Budget.deleteBudget(); + assertFalse(Budget.hasBudget()); + Budget.setBudget(500); + assertTrue(Budget.hasBudget()); + assertEquals(500, Budget.getInitialBudget()); + assertEquals(500, Budget.getCurrentBudget()); + } + + @Test + @Order(2) + public void testNewExpense() { + AddCashflowCommand testExpense = new AddCashflowCommand(Parser.parseRawCommand("add expense /a 50 /t dining")); + testExpense.execute(); + assertEquals(450, Budget.getCurrentBudget()); + } + + @Test + @Order(3) + public void testUpdateBudget() { + Budget.updateBudget(300); + assertEquals(300, Budget.getInitialBudget()); + assertEquals(250, Budget.getCurrentBudget()); + Budget.updateBudget(1000); + assertEquals(1000, Budget.getInitialBudget()); + assertEquals(950, Budget.getCurrentBudget()); + } + + @Test + @Order(4) + public void testSetInitialBudget() { + Budget.setInitialBudget(1500); + assertEquals(1500, Budget.getInitialBudget()); + } + + @Test + @Order(5) + public void testUpdateCurrentBudget() { + Budget.updateCurrentBudget(50); + assertEquals(1000, Budget.getCurrentBudget()); + } + + @Test + @Order(6) + public void testResetBudget() { + Budget.resetBudget(); + assertEquals(1500, Budget.getInitialBudget()); + assertEquals(1500, Budget.getCurrentBudget()); + } + + @Test + @Order(7) + public void testDeleteBudget() { + Budget.deleteBudget(); + assertEquals(0, Budget.getInitialBudget()); + assertEquals(0, Budget.getCurrentBudget()); + assertFalse(Budget.hasBudget()); + } + + @Test + @Order(8) + public void testLoadBudget() { + Budget.load(100, 100, LocalDate.now()); + assertEquals(100, Budget.getInitialBudget()); + assertEquals(100, Budget.getCurrentBudget()); + Budget.deleteBudget(); + + LocalDate date = LocalDate.parse("11/04/2023", DateTimeFormatter.ofPattern("dd/MM/yyyy")); + Budget.load(100, 95, date); + assertEquals(100, Budget.getInitialBudget()); + assertEquals(100, Budget.getCurrentBudget()); + Budget.deleteBudget(); + + date = LocalDate.parse("11/10/2022", DateTimeFormatter.ofPattern("dd/MM/yyyy")); + Budget.load(100, 95, date); + assertEquals(100, Budget.getInitialBudget()); + assertEquals(100, Budget.getCurrentBudget()); + Budget.deleteBudget(); + } +} diff --git a/src/test/java/seedu/financialplanner/cashflow/CashflowListTest.java b/src/test/java/seedu/financialplanner/cashflow/CashflowListTest.java new file mode 100644 index 0000000000..1096490eec --- /dev/null +++ b/src/test/java/seedu/financialplanner/cashflow/CashflowListTest.java @@ -0,0 +1,99 @@ +package seedu.financialplanner.cashflow; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.enumerations.CashflowCategory; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; + +import java.text.DecimalFormat; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; + +class CashflowListTest { + protected CashflowList testList = CashflowList.getInstance(); + protected DecimalFormat decimalFormat = new DecimalFormat("####0.00"); + + @Test + void testAddIncomeAndExpense() { + testList.list.clear(); + + Cashflow.balance = 0; + testList.addIncome(15, IncomeType.SALARY, 30, "part time job"); + Cashflow testIncome = testList.list.get(0); + double roundedValue = Cashflow.round(testIncome.amount, 2); + double roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertTrue(testIncome instanceof Income); + assertEquals("15.00", decimalFormat.format(roundedValue)); + assertEquals(IncomeType.SALARY, testIncome.getIncomeType()); + assertEquals(30, testIncome.recur); + assertEquals("15.00", decimalFormat.format(roundedBalance)); + assertEquals("part time job", testIncome.description); + + testList.addIncome(15.999, IncomeType.INVESTMENTS, 0, "AAPL"); + testIncome = testList.list.get(1); + roundedValue = Cashflow.round(testIncome.amount, 2); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertTrue(testIncome instanceof Income); + assertEquals("16.00", decimalFormat.format(roundedValue)); + assertEquals(IncomeType.INVESTMENTS, testIncome.getIncomeType()); + assertEquals(0, testIncome.recur); + assertEquals("31.00", decimalFormat.format(roundedBalance)); + assertEquals("AAPL", testIncome.description); + + testList.addExpense(10, ExpenseType.DINING, 0, "double mcspicy"); + Cashflow testExpense = testList.list.get(2); + roundedValue = Cashflow.round(testExpense.amount, 2); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertTrue(testExpense instanceof Expense); + assertEquals("10.00", decimalFormat.format(roundedValue)); + assertEquals(ExpenseType.DINING, testExpense.getExpenseType()); + assertEquals(0, testExpense.recur); + assertEquals("21.00", decimalFormat.format(roundedBalance)); + assertEquals("double mcspicy", testExpense.description); + + testList.addExpense(19.999, ExpenseType.ENTERTAINMENT, 30, "netflix"); + testExpense = testList.list.get(3); + roundedValue = Cashflow.round(testExpense.amount, 2); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertTrue(testExpense instanceof Expense); + assertEquals("20.00", decimalFormat.format(roundedValue)); + assertEquals(ExpenseType.ENTERTAINMENT, testExpense.getExpenseType()); + assertEquals(30, testExpense.recur); + assertEquals("1.00", decimalFormat.format(roundedBalance)); + assertEquals("netflix", testExpense.description); + } + + @Test + void testDeleteIncomeAndExpense() { + testList.deleteCashflowWithCategory(CashflowCategory.INCOME, 2); + assertEquals(3, testList.list.size()); + double roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertEquals("-15.00", decimalFormat.format(roundedBalance)); + + testList.deleteCashflowWithoutCategory(1); + assertEquals(2, testList.list.size()); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertEquals("-30.00", decimalFormat.format(roundedBalance)); + + testList.deleteCashflowWithCategory(CashflowCategory.RECURRING, 1); + assertEquals(1, testList.list.size()); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertEquals("-10.00", decimalFormat.format(roundedBalance)); + + testList.deleteCashflowWithoutCategory(1); + assertEquals(0, testList.list.size()); + roundedBalance = Cashflow.round(Cashflow.balance, 2); + assertEquals("0.00", decimalFormat.format(roundedBalance)); + } + + @Test + void testDeleteRecur() { + testList.addExpense(19.999, ExpenseType.ENTERTAINMENT, 1, "netflix"); + testList.deleteRecurWithoutCategory(1); + Cashflow testExpense = testList.list.get(0); + assertEquals(0, testExpense.getRecur()); + assertNull(testExpense.getDate()); + } +} diff --git a/src/test/java/seedu/financialplanner/cashflow/CashflowTest.java b/src/test/java/seedu/financialplanner/cashflow/CashflowTest.java new file mode 100644 index 0000000000..5a7c53831b --- /dev/null +++ b/src/test/java/seedu/financialplanner/cashflow/CashflowTest.java @@ -0,0 +1,58 @@ +package seedu.financialplanner.cashflow; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class CashflowTest { + protected CashflowList cashflowList = CashflowList.getInstance(); + + + @Test + void round() { + double testValue = Cashflow.round(123.555, 2); + assertEquals(123.56, testValue); + testValue = Cashflow.round(123.554, 2); + assertEquals(123.55, testValue); + } + + @Test + void getIncomeBalance() { + cashflowList.list.clear(); + Cashflow.incomeBalance = 0.00; + cashflowList.addIncome(123.12, IncomeType.SALARY, 0 , null); + cashflowList.addIncome(321.21, IncomeType.SALARY, 0, null); + assertEquals(444.33, Cashflow.round(Cashflow.getIncomeBalance(), 2)); + } + + @Test + void getExpenseBalance() { + cashflowList.list.clear(); + Cashflow.expenseBalance = 0.00; + cashflowList.addExpense(123.12, ExpenseType.OTHERS, 0 ,null); + cashflowList.addExpense(321.21, ExpenseType.OTHERS, 0, null); + assertEquals(444.33, Cashflow.round(Cashflow.getExpenseBalance(), 2)); + } + + @Test + void testFinancialPlannerException() { + try { + Cashflow testIncome = new Income(9999999999999.99, IncomeType.SALARY, 0, null); + fail(); + } catch (FinancialPlannerException e) { + assertEquals("Balance exceeded maximum value this program can hold." + + " Please add a different income.", e.getMessage()); + } + try { + Cashflow testExpense = new Expense(9999999999999.99, ExpenseType.OTHERS, 0, null); + fail(); + } catch (FinancialPlannerException e) { + assertEquals("Balance exceeded minimum value this program can hold." + + " Please add a different expense.", e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/financialplanner/commands/AddCashflowCommandTest.java b/src/test/java/seedu/financialplanner/commands/AddCashflowCommandTest.java new file mode 100644 index 0000000000..d1b428da5c --- /dev/null +++ b/src/test/java/seedu/financialplanner/commands/AddCashflowCommandTest.java @@ -0,0 +1,142 @@ +package seedu.financialplanner.commands; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.utils.Parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +class AddCashflowCommandTest { + protected CashflowList cashflowList = CashflowList.getInstance(); + @Test + void testIllegalArgumentException() { + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add abc")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must be either income or expense.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add income")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must have an amount.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add income /a abc")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount must be a number.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add income /a -1")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount cannot be negative.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser + .parseRawCommand("add income /a 9999999999999")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount exceeded maximum value this program can hold. " + + "Please add a different cashflow.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add income /a 1")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must have a type.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add expense /a 1 /t abc")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must be one of the following: " + + "dining, entertainment, shopping, travel, insurance, necessities, others", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser.parseRawCommand("add income /a 1 /t abc")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must be one of the following: " + + "salary, investments, allowance, others", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser. + parseRawCommand("add income /a 1 /t salary /r ")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Recurrence must be an integer and be within the " + + "maximum value this program can hold.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser. + parseRawCommand("add income /a 1 /t salary /r -1")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Recurring value cannot be negative.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser. + parseRawCommand("add income /a 1 /t salary /r 1 /d ")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Description cannot be left empty.", e.getMessage()); + } + try { + AddCashflowCommand testEntry = new AddCashflowCommand(Parser. + parseRawCommand("add income /a 1 /t salary /r 1 /d 1 /x")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unknown extra argument: x", e.getMessage()); + } + } + @Test + void testExecute() { + cashflowList.list.clear(); + + AddCashflowCommand testEntry = new AddCashflowCommand(Parser + .parseRawCommand("add income /a 300 /t salary")); + testEntry.execute(); + assertEquals(1,cashflowList.list.size()); + Cashflow testIncome = cashflowList.list.get(0); + assertEquals(300, testIncome.getAmount()); + assertEquals(IncomeType.SALARY, testIncome.getIncomeType()); + assertEquals(0, testIncome.getRecur()); + assertNull(testIncome.getDescription()); + + testEntry = new AddCashflowCommand(Parser + .parseRawCommand("add income /a 300 /t salary /r 30 /d abc")); + testEntry.execute(); + assertEquals(2,cashflowList.list.size()); + testIncome = cashflowList.list.get(1); + assertEquals(300, testIncome.getAmount()); + assertEquals(IncomeType.SALARY, testIncome.getIncomeType()); + assertEquals(30, testIncome.getRecur()); + assertEquals("abc", testIncome.getDescription()); + + testEntry = new AddCashflowCommand(Parser.parseRawCommand("add expense /a 15 /t dining")); + testEntry.execute(); + assertEquals(3,cashflowList.list.size()); + Cashflow testExpense = cashflowList.list.get(2); + assertEquals(15, testExpense.getAmount()); + assertEquals(ExpenseType.DINING, testExpense.getExpenseType()); + assertEquals(0, testExpense.getRecur()); + assertNull(testExpense.getDescription()); + + testEntry = new AddCashflowCommand(Parser.parseRawCommand("add expense /a 15 /t dining /r 30 /d abc")); + testEntry.execute(); + assertEquals(4,cashflowList.list.size()); + testExpense = cashflowList.list.get(3); + assertEquals(15, testExpense.getAmount()); + assertEquals(ExpenseType.DINING, testExpense.getExpenseType()); + assertEquals(30, testExpense.getRecur()); + assertEquals("abc", testExpense.getDescription()); + } +} diff --git a/src/test/java/seedu/financialplanner/commands/AddReminderCommandTest.java b/src/test/java/seedu/financialplanner/commands/AddReminderCommandTest.java new file mode 100644 index 0000000000..747ba694dd --- /dev/null +++ b/src/test/java/seedu/financialplanner/commands/AddReminderCommandTest.java @@ -0,0 +1,54 @@ +package seedu.financialplanner.commands; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.reminder.ReminderList; +import seedu.financialplanner.utils.Parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class AddReminderCommandTest { + protected ReminderList reminderList = ReminderList.getInstance(); + + @Test + void testIllegalArgumentException() { + try { + AddReminderCommand testEntry = new AddReminderCommand(Parser.parseRawCommand("addreminder")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Reminder must have a type", e.getMessage()); + } + + try { + AddReminderCommand testEntry = new AddReminderCommand(Parser.parseRawCommand("addreminder /t debt")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Reminder must have a date", e.getMessage()); + } + + try { + AddReminderCommand testEntry = new AddReminderCommand( + Parser.parseRawCommand("addreminder /t debt /d 11/12/2020")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Reminder date cannot be in the past", e.getMessage()); + } + + try { + AddReminderCommand testEntry = new AddReminderCommand( + Parser.parseRawCommand("addreminder /t debt /d 2023/12/12")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Reminder date must be in the format dd/MM/yyyy", e.getMessage()); + } + + try { + AddReminderCommand testEntry = new AddReminderCommand( + Parser.parseRawCommand("addreminder /t /d 11/12/2023")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Reminder type cannot be empty", e.getMessage()); + } + } + +} diff --git a/src/test/java/seedu/financialplanner/commands/BudgetCommandTest.java b/src/test/java/seedu/financialplanner/commands/BudgetCommandTest.java new file mode 100644 index 0000000000..8ef615079b --- /dev/null +++ b/src/test/java/seedu/financialplanner/commands/BudgetCommandTest.java @@ -0,0 +1,127 @@ +package seedu.financialplanner.commands; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.utils.Parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@TestMethodOrder(OrderAnnotation.class) +public class BudgetCommandTest { + @Test + @Order(1) + public void testSetBudget() throws FinancialPlannerException { + Cashflow.setBalance(2000); + Budget.deleteBudget(); + BudgetCommand testBudget = new BudgetCommand(Parser.parseRawCommand("budget set /b 1000")); + testBudget.execute(); + assertEquals(1000, Budget.getInitialBudget()); + assertEquals(1000, Budget.getCurrentBudget()); + assertTrue(Budget.hasBudget()); + } + + @Test + @Order(2) + public void testUpdateBudget() throws FinancialPlannerException { + BudgetCommand testBudget = new BudgetCommand(Parser.parseRawCommand("budget update /b 1500")); + testBudget.execute(); + assertEquals(1500, Budget.getInitialBudget()); + assertEquals(1500, Budget.getCurrentBudget()); + } + + @Test + @Order(3) + public void testResetBudget() throws FinancialPlannerException { + BudgetCommand testBudget = new BudgetCommand(Parser.parseRawCommand("budget reset")); + AddCashflowCommand testExpense = new AddCashflowCommand(Parser.parseRawCommand("add expense /a 50 /t dining")); + testExpense.execute(); + testBudget.execute(); + assertEquals(1500, Budget.getInitialBudget()); + assertEquals(1500, Budget.getCurrentBudget()); + Budget.deduct(50); + Cashflow.setBalance(1000); + BudgetCommand testBudgetExceedBalance = new BudgetCommand(Parser.parseRawCommand("budget reset")); + testBudgetExceedBalance.execute(); + assertEquals(1000, Budget.getInitialBudget()); + assertEquals(1000, Budget.getCurrentBudget()); + } + + @Test + @Order(4) + public void testDeleteBudget() throws FinancialPlannerException { + BudgetCommand testBudget = new BudgetCommand(Parser.parseRawCommand("budget delete")); + testBudget.execute(); + assertFalse(Budget.hasBudget()); + } + + @Test + @Order(5) + public void testInvalidCommandFormat_throwsException() throws FinancialPlannerException { + try { + BudgetCommand testEmptyArgument = new BudgetCommand(Parser.parseRawCommand("budget")); + } catch (FinancialPlannerException e) { + assertEquals("Budget operation cannot be empty.", e.getMessage()); + } + try { + BudgetCommand testExtraArgument = new BudgetCommand(Parser.parseRawCommand("budget" + + " set /b 500 /t sdf")); + } catch (IllegalArgumentException e) { + assertEquals("Unknown extra argument: t", e.getMessage()); + } + try { + BudgetCommand testInvalidCommand = new BudgetCommand(Parser.parseRawCommand("budget random /b 5")); + } catch (FinancialPlannerException e) { + assertEquals("Budget operation must be one of the following: set, update, " + + "delete, reset, view.", e.getMessage()); + } + + Budget.setBudget(5); + try { + BudgetCommand testSetAndHasBudget = new BudgetCommand(Parser.parseRawCommand("budget set /b 55")); + } catch (FinancialPlannerException e) { + assertEquals("There is an existing budget, try budget update instead.", e.getMessage()); + } + Budget.deleteBudget(); + + try { + BudgetCommand testUpdateAndNoBudget = new BudgetCommand(Parser.parseRawCommand("budget update " + + "/b 500")); + } catch (FinancialPlannerException e) { + assertEquals("There is no budget set yet, try budget set instead.", e.getMessage()); + } + + try { + BudgetCommand testMissingArgument = new BudgetCommand(Parser.parseRawCommand("budget set")); + } catch (IllegalArgumentException e) { + assertEquals("Missing /b argument.", e.getMessage()); + } + } + + @Test + @Order(6) + public void testInvalidBudget_throwsException() throws FinancialPlannerException { + try { + BudgetCommand testStringBudget = new BudgetCommand(Parser.parseRawCommand("budget set /b f")); + } catch (IllegalArgumentException e) { + assertEquals("Budget must be a number.", e.getMessage()); + } + try { + BudgetCommand testNegativeBudget = new BudgetCommand(Parser.parseRawCommand("budget set /b -5")); + } catch (FinancialPlannerException e) { + assertEquals("Budget should be greater than 0.", e.getMessage()); + } + Cashflow.clearBalance(); + try { + BudgetCommand testBudgetExceedBalance = new BudgetCommand(Parser.parseRawCommand("budget set /b 500")); + } catch (FinancialPlannerException e) { + assertEquals("Budget should be lower than or equal to total balance.", e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/financialplanner/commands/DeleteCashflowCommandTest.java b/src/test/java/seedu/financialplanner/commands/DeleteCashflowCommandTest.java new file mode 100644 index 0000000000..9295cbad0e --- /dev/null +++ b/src/test/java/seedu/financialplanner/commands/DeleteCashflowCommandTest.java @@ -0,0 +1,127 @@ +package seedu.financialplanner.commands; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.cashflow.Cashflow; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.utils.Parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +class DeleteCashflowCommandTest { + protected CashflowList cashflowList = CashflowList.getInstance(); + + @Test + void testIllegalArgumentException() { + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Incorrect arguments.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete a")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be an integer and be within the " + + "maximum value this program can hold.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete 0")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be within the list.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete 1 /r 3")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Arguments after /r should be left empty.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete 1 /a")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unknown extra argument: a", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete abc 1")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Entry must be either income, expense or recurring.", e.getMessage()); + } + } + + @Test + void testExecute() throws FinancialPlannerException { + cashflowList.list.clear(); + + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete 2 /r")); + testEntry.execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be within the list.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete 2")); + testEntry.execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be within the list.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete income 2 /r")); + testEntry.execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be within the list.", e.getMessage()); + } + try { + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete income 2")); + testEntry.execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Index must be within the list.", e.getMessage()); + } + + cashflowList.addIncome(1, IncomeType.SALARY, 10, "work"); + cashflowList.addExpense(1, ExpenseType.ENTERTAINMENT, 10, "Apple Music"); + cashflowList.addIncome(1, IncomeType.SALARY, 10, "work"); + cashflowList.addExpense(1, ExpenseType.ENTERTAINMENT, 10, "Apple Music"); + + DeleteCashflowCommand testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete recurring 3 /r")); + testEntry.execute(); + Cashflow testIncome = cashflowList.list.get(2); + assertEquals(0, testIncome.getRecur()); + assertNull(testIncome.getDate()); + + testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete recurring 3")); + testEntry.execute(); + assertEquals(3, cashflowList.list.size()); + + testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete income 1 /r")); + testEntry.execute(); + testIncome = cashflowList.list.get(0); + assertEquals(0, testIncome.getRecur()); + assertNull(testIncome.getDate()); + + testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete income 1")); + testEntry.execute(); + assertEquals(2, cashflowList.list.size()); + + testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete expense 1 /r")); + testEntry.execute(); + Cashflow testExpense = cashflowList.list.get(1); + assertEquals(0, testExpense.getRecur()); + assertNull(testExpense.getDate()); + + testEntry = new DeleteCashflowCommand(Parser.parseRawCommand("delete expense 1")); + testEntry.execute(); + assertEquals(1, cashflowList.list.size()); + } +} diff --git a/src/test/java/seedu/financialplanner/commands/SetGoalCommandTest.java b/src/test/java/seedu/financialplanner/commands/SetGoalCommandTest.java new file mode 100644 index 0000000000..3886bc3068 --- /dev/null +++ b/src/test/java/seedu/financialplanner/commands/SetGoalCommandTest.java @@ -0,0 +1,39 @@ +package seedu.financialplanner.commands; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.goal.WishList; +import seedu.financialplanner.utils.Parser; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +class SetGoalCommandTest { + protected WishList wishList = WishList.getInstance(); + @Test + void testIllegalArgumentException() { + try { + SetGoalCommand testEntry = new SetGoalCommand(Parser.parseRawCommand("set goal")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Goal must have an amount", e.getMessage()); + } + try { + SetGoalCommand testEntry = new SetGoalCommand(Parser.parseRawCommand("set goal /g /l car")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount must be specified", e.getMessage()); + } + try { + SetGoalCommand testEntry = new SetGoalCommand(Parser.parseRawCommand("set goal /g -1 /l car")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount must be positive", e.getMessage()); + } + try { + SetGoalCommand testEntry = new SetGoalCommand( + Parser.parseRawCommand("set goal /g 2222222222222222222222222222222222222 /l car")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Amount must be a valid integer", e.getMessage()); + } + } + +} diff --git a/src/test/java/seedu/financialplanner/investments/WatchListTest.java b/src/test/java/seedu/financialplanner/investments/WatchListTest.java new file mode 100644 index 0000000000..13f3878393 --- /dev/null +++ b/src/test/java/seedu/financialplanner/investments/WatchListTest.java @@ -0,0 +1,62 @@ +package seedu.financialplanner.investments; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class WatchListTest { + + @Test + @Order(1) + void fetchFMPStockPrices() throws FinancialPlannerException { + WatchList wl = WatchList.getInstance(); + wl.getLatestWatchlistInfo(); + HashMap stocks = wl.getStocks(); + assertNotNull(stocks.get("AAPL").getPrice()); + assertNotNull(stocks.get("GOOGL").getPrice()); + } + + @Test + @Order(2) + void addStock() throws Exception { + WatchList wl = WatchList.getInstance(); + String stockCode = "gME"; + assertEquals("Gamestop Corporation - Class A", wl.addStock(stockCode)); + } + + @Test + @Order(3) + void checkValidStock() { + HashMap stocks = WatchList.getInstance().getStocks(); + boolean valid = WatchList.getInstance().checkValidStock("GME", stocks.get("GME")); + assertTrue(valid); + } + + @Test + @Order(4) + void deleteStock() throws FinancialPlannerException { + WatchList wl = WatchList.getInstance(); + String stockCode = "GMe"; + assertEquals("Gamestop Corporation - Class A", wl.deleteStock(stockCode)); + } + + @Test + @Order(5) + void initializeNewWatchlist() { + HashMap stocks = WatchList.getInstance().initalizeNewWatchlist(); + assertEquals(2, stocks.size()); + assertNotNull(stocks.get("AAPL").getStockName()); + assertNotNull(stocks.get("GOOGL").getStockName()); + assertEquals(0, stocks.get("AAPL").getHashCode()); + assertEquals(0, stocks.get("GOOGL").getHashCode()); + } +} diff --git a/src/test/java/seedu/financialplanner/storage/LoadDataTest.java b/src/test/java/seedu/financialplanner/storage/LoadDataTest.java new file mode 100644 index 0000000000..57de8fc420 --- /dev/null +++ b/src/test/java/seedu/financialplanner/storage/LoadDataTest.java @@ -0,0 +1,91 @@ +package seedu.financialplanner.storage; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.cashflow.Income; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.investments.Stock; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +class LoadDataTest { + protected CashflowList cashflowList = CashflowList.getInstance(); + + @Test + void testLoad() throws FinancialPlannerException { + cashflowList.list.clear(); + LocalDate date = stringToDate("01/01/2023"); + date = date.plusDays(30); + LoadData.load("src/test/testData/TestData.txt", date); + String actual = cashflowList.getList(); + cashflowList.list.clear(); + getTestData(); + String expected = cashflowList.getList(); + assertEquals(expected, actual); + } + + @Test + void loadWatchListBasic() { + try { + HashMap stocks = LoadData.loadWatchList("src/test/testData/basicwatchlist.json"); + assertEquals(2, stocks.size()); + assertNotNull(stocks.get("AAPL")); + assertNotNull(stocks.get("GOOGL")); + } catch (Exception e) { + System.out.println("Caught"); + } + } + + @Test + void loadWatchListExceedSize() { + Exception exception = assertThrows(NoSuchElementException.class, () -> { + HashMap stocks = LoadData.loadWatchList("src/test/testData/exceedwatchlist.json"); + }); + } + + @Test + void loadWatchListIncorrectHashCode() { + Exception exception = assertThrows(NoSuchElementException.class, () -> { + HashMap stocks = LoadData.loadWatchList("src/test/testData/incorrectwatchlist.json"); + }); + } + + private LocalDate stringToDate(String string) { + return LocalDate.parse(string, DateTimeFormatter.ofPattern("dd/MM/yyyy")); + } + private void getTestData() throws FinancialPlannerException{ + LocalDate date = stringToDate("01/01/2023"); + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 10, null, date, false)); + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 0, "parents", date, false)); + cashflowList.load(new Expense(100, ExpenseType.SHOPPING, 30, "shopee", date, false)); + cashflowList.load(new Expense(100, ExpenseType.INSURANCE, 10, "ntuc", date, true)); + + date = date.plusDays(10); + cashflowList.load(new Expense(100, ExpenseType.INSURANCE, 10, "ntuc", date, false)); + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 10, null, date, false)); + + date = date.plusDays(10); + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 10, null, date, false)); + + date = date.plusDays(10); + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 10, null, date, false)); + cashflowList.load(new Expense(100, ExpenseType.SHOPPING, 30, "shopee", date, false)); + + date = stringToDate("21/01/2023"); + cashflowList.load(new Expense(100, ExpenseType.INSURANCE, 10, "ntuc", date, false)); + + date = date.plusDays(10); + cashflowList.load(new Expense(100, ExpenseType.INSURANCE, 10, "ntuc", date, false)); + } +} diff --git a/src/test/java/seedu/financialplanner/storage/StorageTest.java b/src/test/java/seedu/financialplanner/storage/StorageTest.java new file mode 100644 index 0000000000..7499a16748 --- /dev/null +++ b/src/test/java/seedu/financialplanner/storage/StorageTest.java @@ -0,0 +1,91 @@ +package seedu.financialplanner.storage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import seedu.financialplanner.cashflow.Budget; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.enumerations.IncomeType; +import seedu.financialplanner.exceptions.FinancialPlannerException; +import seedu.financialplanner.cashflow.Expense; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.cashflow.Income; +import seedu.financialplanner.utils.Ui; + +import java.io.ByteArrayInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StorageTest { + @TempDir + public static Path testFolder; + protected CashflowList cashflowList = CashflowList.getInstance(); + protected Ui ui = Ui.getInstance(); + protected Storage storage = Storage.getInstance(); + + @Test + public void loadValidData() throws FinancialPlannerException { + cashflowList.list.clear(); + storage.load("src/test/testData/ValidData.txt", LocalDate.now()); + String actual = cashflowList.getList(); + cashflowList.list.clear(); + getTestData(); + String expected = cashflowList.getList(); + assertEquals(expected, actual); + } + + @Test + public void loadInvalidData_userInputNo() { + cashflowList.list.clear(); + ByteArrayInputStream in = new ByteArrayInputStream("n".getBytes()); + ui.setScanner(new Scanner(in)); + assertThrows(FinancialPlannerException.class, + () -> storage.load("src/test/testData/InvalidData.txt", LocalDate.now())); + } + + @Test + public void saveValidData() throws FinancialPlannerException, IOException { + cashflowList.list.clear(); + getTestData(); + Budget.setBudget(10); + storage.save(String.valueOf(testFolder.resolve("temp.txt"))); + String date = LocalDate.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + String filePath = String.valueOf(testFolder.resolve("ValidDataCopy.txt")); + getTestValidData(filePath, date); + + assertEquals(Files.readAllLines(Path.of(filePath)), + Files.readAllLines(testFolder.resolve("temp.txt"))); + Budget.deleteBudget(); + } + + @Test + public void saveNonExistentFile() throws FinancialPlannerException { + getTestData(); + assertThrows(FinancialPlannerException.class, () -> storage.save("")); + } + + private void getTestData() throws FinancialPlannerException { + cashflowList.load(new Income(123.12, IncomeType.ALLOWANCE, 0, null)); + cashflowList.load(new Expense(100, ExpenseType.SHOPPING, 0, "shopee")); + } + + private static void getTestValidData(String filePath, String date) throws IOException { + try { + Files.copy(Paths.get("src/test/testData/ValidDataWithBudget.txt"), Path.of(filePath)); + FileWriter fw = new FileWriter(filePath, true); + fw.append(" ").append(date); + fw.close(); + } catch (IOException e) { + throw new IOException(e); + } + } +} diff --git a/src/test/java/seedu/financialplanner/visualisations/CategorizerTest.java b/src/test/java/seedu/financialplanner/visualisations/CategorizerTest.java new file mode 100644 index 0000000000..54f31068a4 --- /dev/null +++ b/src/test/java/seedu/financialplanner/visualisations/CategorizerTest.java @@ -0,0 +1,37 @@ +package seedu.financialplanner.visualisations; + +import org.junit.jupiter.api.Test; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CategorizerTest { + @Test + void sortType() { + CashflowList cashflowList = CashflowList.getInstance(); + Exception exception = assertThrows(FinancialPlannerException.class, () -> { + Categorizer.sortType(cashflowList, "Hello"); + }); + String expected = "Hello Type not found"; + assertEquals(exception.getMessage(), expected); + } + + @Test + void sortExpense() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + HashMap map = Categorizer.sortType(cashflowList, "Expense"); + assertNotNull(map); + } + + @Test + void sortIncome() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + HashMap map = Categorizer.sortType(cashflowList, "Income"); + assertNotNull(map); + } +} diff --git a/src/test/java/seedu/financialplanner/visualisations/VisualizerTest.java b/src/test/java/seedu/financialplanner/visualisations/VisualizerTest.java new file mode 100644 index 0000000000..c814f55167 --- /dev/null +++ b/src/test/java/seedu/financialplanner/visualisations/VisualizerTest.java @@ -0,0 +1,54 @@ +package seedu.financialplanner.visualisations; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import seedu.financialplanner.cashflow.CashflowList; +import seedu.financialplanner.enumerations.ExpenseType; +import seedu.financialplanner.exceptions.FinancialPlannerException; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VisualizerTest { + + @Test + void visualizeInvalid() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + HashMap map = Categorizer.sortType(cashflowList, "Expense"); + Exception exception = assertThrows(FinancialPlannerException.class, () -> { + Visualizer.displayChart("Hello", map, "Expense"); + }); + String expected = "Hello Chart Type Not Found"; + assertEquals(exception.getMessage(), expected); + } + + @DisabledOnOs({OS.LINUX}) + @Test + void visualizePie() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + HashMap map = Categorizer.sortType(cashflowList, "Expense"); + assertDoesNotThrow(() -> Visualizer.displayChart("Pie", map, "Expense")); + } + + @DisabledOnOs({OS.LINUX}) + @Test + void visualizeBar() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + cashflowList.addExpense(100, ExpenseType.ENTERTAINMENT, 0, "hi"); + HashMap map = Categorizer.sortType(cashflowList, "Expense"); + assertDoesNotThrow(() -> Visualizer.displayChart("Bar", map, "Expense")); + } + + @DisabledOnOs({OS.LINUX}) + @Test + void visualizeRadar() throws FinancialPlannerException { + CashflowList cashflowList = CashflowList.getInstance(); + cashflowList.addExpense(100, ExpenseType.ENTERTAINMENT, 0, "hi"); + HashMap map = Categorizer.sortType(cashflowList, "Expense"); + assertDoesNotThrow(() -> Visualizer.displayChart("Radar", map, "Expense")); + } +} diff --git a/src/test/testData/InvalidData.txt b/src/test/testData/InvalidData.txt new file mode 100644 index 0000000000..30d74d2584 --- /dev/null +++ b/src/test/testData/InvalidData.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/src/test/testData/TestData.txt b/src/test/testData/TestData.txt new file mode 100644 index 0000000000..5df1e14b36 --- /dev/null +++ b/src/test/testData/TestData.txt @@ -0,0 +1,9 @@ +I | 123.12 | ALLOWANCE | 10 | false | 01/01/2023 +I | 123.12 | ALLOWANCE | 0 | false | parents +E | 100.0 | SHOPPING | 30 | false | 01/01/2023 | shopee +E | 100.0 | INSURANCE | 10 | true | 01/01/2023 | ntuc +E | 100.0 | INSURANCE | 10 | false | 11/01/2023 | ntuc +I | 123.12 | ALLOWANCE | 10 | false | 01/02/2023 +I | 123.12 | ALLOWANCE | 10 | false | 01/03/2023 +I | 123.12 | ALLOWANCE | 10 | false | 01/04/2023 +B | 0.0 | 0.0 \ No newline at end of file diff --git a/src/test/testData/ValidData.txt b/src/test/testData/ValidData.txt new file mode 100644 index 0000000000..dda0445911 --- /dev/null +++ b/src/test/testData/ValidData.txt @@ -0,0 +1,2 @@ +I | 123.12 | ALLOWANCE | 0 | false +E | 100.0 | SHOPPING | 0 | false | shopee \ No newline at end of file diff --git a/src/test/testData/ValidDataWithBudget.txt b/src/test/testData/ValidDataWithBudget.txt new file mode 100644 index 0000000000..a799295a5a --- /dev/null +++ b/src/test/testData/ValidDataWithBudget.txt @@ -0,0 +1,3 @@ +I | 123.12 | ALLOWANCE | 0 | false +E | 100.0 | SHOPPING | 0 | false | shopee +B | 10.0 | 10.0 | \ No newline at end of file diff --git a/src/test/testData/basicwatchlist.json b/src/test/testData/basicwatchlist.json new file mode 100644 index 0000000000..72b3bf6d5a --- /dev/null +++ b/src/test/testData/basicwatchlist.json @@ -0,0 +1,24 @@ +{ + "GOOGL": { + "symbol": "GOOGL", + "exchange": "NASDAQ", + "stockName": "Alphabet Inc - Class A", + "price": "132.59", + "dayHigh": "132.8", + "dayLow": "129.42", + "lastUpdated": "Nov 11, 2023, 5:00:00 AM", + "lastFetched": 1699698248423, + "hashCode": 438171867 + }, + "AAPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + } +} \ No newline at end of file diff --git a/src/test/testData/exceedwatchlist.json b/src/test/testData/exceedwatchlist.json new file mode 100644 index 0000000000..96f33d4b4f --- /dev/null +++ b/src/test/testData/exceedwatchlist.json @@ -0,0 +1,68 @@ +{ + "GOOGL": { + "symbol": "GOOGL", + "exchange": "NASDAQ", + "stockName": "Alphabet Inc - Class A", + "price": "132.59", + "dayHigh": "132.8", + "dayLow": "129.42", + "lastUpdated": "Nov 11, 2023, 5:00:00 AM", + "lastFetched": 1699698248423, + "hashCode": 438171867 + }, + "AAasdfPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + }, + "sdf": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + }, + "AAasfdsfdPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + }, + "AAasdfasdPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + }, + "AAasdasffasdPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + } +} \ No newline at end of file diff --git a/src/test/testData/incorrectwatchlist.json b/src/test/testData/incorrectwatchlist.json new file mode 100644 index 0000000000..58bf1adc14 --- /dev/null +++ b/src/test/testData/incorrectwatchlist.json @@ -0,0 +1,24 @@ +{ + "GOOGL": { + "symbol": "GOOGL", + "exchange": "NASDAQ", + "stockName": "Alphabet Inc - Class A", + "price": "132.59", + "dayHigh": "132.8", + "dayLow": "129.42", + "lastUpdated": "Nov 11, 2023, 5:00:00 AM", + "lastFetched": 169969824842, + "hashCode": 438171867 + }, + "AAPL": { + "symbol": "AAPL", + "exchange": "NASDAQ", + "stockName": "Apple Inc.", + "price": "186.4", + "dayHigh": "186.565", + "dayLow": "183.53", + "lastUpdated": "Nov 11, 2023, 5:00:01 AM", + "lastFetched": 1699698248423, + "hashCode": 0 + } +} \ No newline at end of file diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..500dc2db1a 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,112 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +Loading existing watchlist.. +Watchlist file not found... Creating +Initializing New watchlist.. adding AAPL and GOOGL for your reference +File not found. Creating new file... +Welcome to your Financial Planner. Type something to get started. +You have added an Income + Type: Salary + Amount: 5000.00 + Description: part time job +to the Financial Planner. +Balance: 5000.00 +You have added an Income + Type: Allowance + Amount: 123.12 +to the Financial Planner. +Balance: 5123.12 +You have added an Expense + Type: Entertainment + Amount: 100.00 + Description: netflix +to the Financial Planner. +Balance: 5023.12 +You have added an Expense + Type: Shopping + Amount: 123.21 +to the Financial Planner. +Balance: 4899.91 +You have 4 matching cashflows: +1: Income + Type: Salary + Amount: 5000.00 + Description: part time job +2: Income + Type: Allowance + Amount: 123.12 +3: Expense + Type: Entertainment + Amount: 100.00 + Description: netflix +4: Expense + Type: Shopping + Amount: 123.21 +Balance: 4899.91 +You have 2 matching cashflows: +1: Income + Type: Salary + Amount: 5000.00 + Description: part time job +2: Income + Type: Allowance + Amount: 123.12 +Income Balance: 5123.12 +You have 2 matching cashflows: +1: Expense + Type: Entertainment + Amount: 100.00 + Description: netflix +2: Expense + Type: Shopping + Amount: 123.21 +Expense Balance: 223.21 +No matching cashflow. +You have removed an Income + Type: Allowance + Amount: 123.12 +from the Financial Planner. +Balance: 4776.79 +You have removed an Income + Type: Salary + Amount: 5000.00 + Description: part time job +from the Financial Planner. +Balance: -223.21 +You have removed an Expense + Type: Shopping + Amount: 123.21 +from the Financial Planner. +Balance: -100.00 +You have removed an Expense + Type: Entertainment + Amount: 100.00 + Description: netflix +from the Financial Planner. +Balance: 0.00 +You have successfully added: +Microsoft Corporation +Use Watchlist to view it! +You have successfully added: +Gamestop Corporation - Class A +Use Watchlist to view it! +You have added an Income + Type: Salary + Amount: 5000.00 +to the Financial Planner. +Balance: 5000.00 +A monthly budget of 3000.00 has been set. +Budget has been updated: +Old initial budget: 3000.00 +Old current budget: 3000.00 +New initial budget: 1000.00 +New current budget: 1000.00 +You have added an Expense + Type: Shopping + Amount: 200.00 +to the Financial Planner. +Balance: 4800.00 +Your remaining budget for the month is: 800.00 +You have a remaining budget of 800.00. +Budget has been reset to 1000.00. +Budget has been deleted. +Unknown command. Type help for help. +Exiting Financial Planner. Goodbye. diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..aac6db94ad 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,23 @@ -James Gosling \ No newline at end of file +add income /a 5000 /t salary /d part time job +add income /a 123.12 /t allowance +add expense /a 100 /t entertainment /d netflix +add expense /a 123.21 /t shopping +list +list income +list expense +list recurring +delete income 2 +delete 1 +delete expense 2 +delete 1 +addstock /s MSFT +addstock /s GME +add income /a 5000 /t salary +budget set /b 3000 +budget update /b 1000 +add expense /a 200 /t shopping +budget view +budget reset +budget delete +sdf +exit \ No newline at end of file