diff --git a/.gitignore b/.gitignore index 2873e189e1..3d859e4d18 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +# Log files +supertracker.log +supertracker.log.lck + +# Data files +/data/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index ea82051fab..f5bb848daf 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("supertracker.SuperTracker") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("SuperTracker") archiveClassifier.set("") } @@ -41,6 +41,7 @@ checkstyle { toolVersion = '10.2' } -run{ +run { standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..43cfb8b4a6 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,11 @@ # 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 | +|--------------------------|:----------------------------:|:-------------------------------------------:|:-----------------------------------:| +| ![](team/vimal-face.jpg) | Sim Jing Jie Ryan | [Github](https://github.com/rismm) | [Portfolio](team/rismm.md) | +| ![](team/vimal-face.jpg) | Sim Jun Hong | [Github](https://github.com/awesomesjh) | [Portfolio](team/awesomesjh.md) | +| ![](team/vimal-face.jpg) | Tay Wen Duan David | [Github](https://github.com/dtaywd) | [Portfolio](team/dtaywd.md) | +| ![](team/vimal-face.jpg) | Timothy Lau | [Github](https://github.com/TimothyLKM) | [Portfolio](team/timothylkm.md) | +| ![](team/vimal-face.jpg) | Vimalapugazhan Purushothaman | [Github](https://github.com/vimalapugazhan) | [Portfolio](team/vimalapugazhan.md) | + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..419ff1949e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,1153 @@ # Developer Guide +- [Acknowledgements](#acknowledgements) +- [Design](#design) + - [Architecture](#architecture) + - [Parser Component](#parser-component) + - [Storage Component](#storage-component) +- [Implementation](#implementation) + - [New Command](#new-command) + - [Delete Command](#delete-command) + - [Add Command](#add-command) + - [Remove Command](#remove-command) + - [Update Command](#update-command) + - [Find Command](#find-command) + - [Rename Command](#rename-command) + - [List Command](#list-command) + - [Report Command](#report-command) + - [Buy Command](#buy-command) + - [Sell Command](#sell-command) + - [Clear Command](#clear-command) + - [Expenditure Command](#expenditure-command) + - [Revenue Command](#revenue-command) + - [Profit Command](#profit-command) + - [Help Command](#help-command) +- [Appendix: Requirements](#appendix-requirements) + - [Product Scope](#product-scope) + - [User Stories](#user-stories) + - [Non-Functional Requirements](#non-functional-requirements) + - [Glossary](#glossary) +- [Appendix: Instructions for Manual Testing](#appendix-instructions-for-manual-testing) + - [Saving data](#saving-data) + - [Create a new item](#create-a-new-item) + - [Delete an item](#delete-an-item) + - [Increase quantity](#increase-quantity) + - [Decrease quantity](#decrease-quantity) + - [Update an item](#update-an-item) + - [Find an item](#find-an-item) + - [Rename an item](#rename-an-item) + - [List all items](#list-all-items) + - [Print report](#print-report) + - [Buy items](#buy-items) + - [Sell items](#sell-items) + - [Clear transactions](#clear-transactions) + - [Print expenditure](#print-expenditure) + - [Print revenue](#print-revenue) + - [Print profit](#print-profit) + - [Print a help list](#print-a-help-list) + - [Quit the program](#quit-the-program) + ## Acknowledgements +- [CS2113 Website](https://nus-cs2113-ay2324s2.github.io/website/index.html) +- [AB-3 User Guide](https://se-education.org/addressbook-level3/UserGuide.html) +- [AB-3 Developer Guide](https://se-education.org/addressbook-level3/DeveloperGuide.html) +- [PlantUML Docs](https://plantuml.com) +- and most importantly our Professor **Akshay Narayan** and TA **Vishruti Ranjan** who guided us along this project :D + +
+ +## Design + +### Architecture +![ArchitectureDiagram](uml-diagrams/ArchitectureDiagram.png)\ +The architecture diagram shown above explains the high-level design of SuperTracker + +A quick overview of the main components: +- `Main`: In charge of program launch and shut down. Takes in user input and sends it to the `Parser` component. +Represented by the main class `SuperTracker` +- `Parser`: Parses user input into a command. Represented by the `Parser` class +- `Storage`: Reads data from and writes data to the hard disk +- `Ui`: Handles and prints output messages onto the CLI. +- `Command`: List of various commands. Classes in this component all implement the `Command` interface +- `Items`: Holds item and transaction data of the program in memory + - Consists of `Item`, `Inventory`, `Transaction` and `TransactionList` classes + +
+ +### Parser Component +The program handles user inputs in the Parser class where inputs are parsed into command classes that implement the Command interface. +The Parser class only contains static methods as we have determined that it would be more practical instead of instantiating an instance of a Parser class. + +> Note on the command inputs: +> - All valid command inputs by the user will have a command word _(first word separated by whitespace)_ with its respective parameters following the word. +> - Each parameter must be entered following a flag, i.e. a name parameter will have the input `n/NAME` + +The following is a class diagram of the `Parser` and its relevant dependencies\ +![ParserClass](uml-diagrams/ParserClass.png) + +The following is a sequence diagram of the execution sequence of a single user input\ +![ParserSequence](uml-diagrams/ParserSequence.png) + +`Parser+parseCommand(input)` will return a `Command` object according to the command word in the user input. +It returns an `InvalidCommand` class if the command word detected is invalid. + +For the parsing of valid command word inputs, `Parser+parseCommand(input)` passes the parameters of the user input into the respective command's parsing method, +then calls the method `Parser-getPatternMatcher(regex, input, paramFlags)`that returns a `Matcher` object, which is Java's regex pattern matcher. +We use regular expressions to simplify the process of pattern matching for input parameters. Each command will have their respective regex strings for pattern matching + +> Note that `Parser-getPatternMatcher(regex, input, paramFlags)` will be called as long as the command word in the user input is valid, +> even if the parameters are invalid. The `Matcher` object returned will try to match the input parameters to the given regex. +> A `TrackerException` will be thrown if the input parameters are invalid. + +The input parameters will be passed into another method, `Parser-makeStringPattern(inputParam, paramFlags)` that returns a string in a pattern +according to a preset format that contains all the necessary input parameters. +Converting the parameter string to a pattern is done because **order does not matter** in the user's input parameters _(if there are multiple parameters)._ +Thus, it would be necessary to convert the user's input into a consistent format for pattern matching using regex. + +> Note: +> - All command regex are in the format:\ +> `a/(?.*) b/(?.*) c/(?.*) ` with `a/`, `b/`, `c/` being the respective parameter flags +> and `/` as the flag character. +> - Parameter flags can contain more than 1 character right before the `/` +> - Suppose we have `a/aaa b/bbb c/ccc ` as an input, according to the regex above, +> aaa will be in the named capture group "grp1", bbb in "grp2", ccc in "grp3" +> - If a parameter is optional, we would have it in a non-capture group with a `?` at the end +> - i.e. in `a/(?.*) (?(?:b/.*)?)`, the `b/` flag is an optional parameter +> - There also must be a white space before any valid input parameter flag in the user inputs +> - i.e. in `a/(?.*) (?(?:b/.*)?)`, the input `a/__b/_` will take `__b/_` to be inside "grp1" and "grp2" +> will be empty +> +> For example, suppose the program wants to match the user input parameter +> `c/coconut a/apple b/bear a/anaconda d/donkey` to the regex `a/(?.*) b/(?.*) c/(?.*) (?(?:e/.*)?)` +> 1. The input parameter string and a string array of flags `{a, b, c, e}` is passed into `Parser-makeStringPattern(...)` +> 2. `Parser-makeStringPattern` will return a pattern string `a/apple b/bear c/coconut ` +> 3. The program will then try to match the pattern string `a/apple b/bear c/coconut ` to the regex ` +> a/(?.*) b/(?.*) c/(?.*) (?(?:e/.*)?)` +> 4. The above pattern string matches the regex, with the following strings in the named capture groups: +> - "grp1": apple +> - "grp2": bear +> - "grp3": coconut +> - "grp4": is empty + +The `Matcher` will detect whether this input string pattern match the given regex pattern given to it +and extract out the necessary information if there is a match. +This will be used by each command's respective parsing method and returns the relevant parsed `Command` +object to `SuperTracker+handleCommands()` + +
+ +### Storage Component +Currently, the program handles 2 classes of save data, `Item` and `Transaction`. +Saving and loading data of these objects is performed by the `ItemStorage` and `TransactionStorage` classes that inherit +from the `FileManager` class. As the program is running, `Item` objects will be stored in the `Inventory` class, +while `Transaction` objects will be stored in the `TransactionList` class. + +#### Item objects +The following is a class diagram of `ItemStorage` and its relevant dependencies\ +![ItemStorage](./uml-diagrams/ItemStorageClass.png) + +Take note that the `Command` class in the diagram refers to the classes that implement the `Command` interface and make changes to the item list in `Inventory` + +
+ +##### **Saving** +Saving is performed automatically each time the item list is updated. Currently, there are only 6 commands that can make changes to +the item list and will call `ItemStorage+saveData()` at the end of `CommandClass+execute()`. +> Commands that call `ItemStorage+saveData()` in `execute()`: +> - `NewCommand` +> - `UpdateCommand` +> - `DeleteCommand` +> - `AddCommand` +> - `RemoveCommand` +> - `RenameCommand` +> - `BuyCommand` +> - `SellCommand` + +Item data is saved into a text file in `./data/items.txt` by generating a string for each `Item` in the list, containing the attributes of the `item`. +The string generated will have the attributes in the format and order of `NAME,QUANTITY,PRICE,EXPIRY_DATE`. Since an `Item`'s expiry date is +optional, `EXPIRY_DATE` can be the string `"no date"` if the expiry date is determined to not exist. This string will be written to the text file as mentioned. +> For example, suppose the program wants to save the `Item` with attributes `name = "Apple"`, `quantity = 5`, `price = 2.55`, `expiryDate = 19-04-2024` +> and the program uses `",,,"` as a file delimiter between groups.\ +> The program generates the string `"Apple ,,, 5 ,,, 2.55 ,,, 19-04-2024 ,,, end"` with `end` signifying the end of the data string.\ +> Do note that the `end` is only a placeholder and can be any string of characters. +> > If for some reason the user was able to enter the file delimiter, such as `",,,"`, as an item name using the `new` or `rename` commands, +> > when the program saves, it will replace every occurrence of the file delimiter in the name to a placeholder character sequence, like `"*&_"`, +> > in the converted data string to prevent the program from saving data that is deemed corrupted on the next load. + +This string is generated by the method `ItemStorage-getItemData()` + +##### **Loading** +Loading is performed at the start of the program in `SuperTracker-run()` where it calls the method `ItemStorage+loadData()`. +`loadData()` looks for the save text file at `./data/items.txt` and reads the lines from the file `items.txt` if it exists. As indicated earlier, +each line containing a single `Item`'s data will be in the format and order of `NAME,QUANTITY,PRICE,EXPIRY_DATE`. Each attribute will be parsed into its +relevant data type, creating a new `Item` with the extracted attributes, which is then added to the item list in `Inventory`. In the event where the line +of data read is not in the correct format, or the attributes are unable to be parsed into its relevant data type _(i.e. the string in the QUANTITY part reads "f1ve" instead of "5")_, +the line will be ignored and a new `Item` will not be added to the list. The corrupted lines of data will also be erased from the save file. + +The parsing of string data to an `Item` object is handled by the method `ItemStorage-parseItemData()`. + +#### Transaction objects +The following is a class diagram of `TransactionStorage` and its relevant dependencies\ +![TransactionStorage](uml-diagrams/TransactionStorageClass.png) + +Take note that the `Command` class in the diagram refers to the classes that implement the `Command` interface and add transactions to list in `TransactionList` + +The saving and loading is largely similar to that of Item objects detailed above. + +##### **Saving** +Currently, there are only 3 commands that can make changes to +the item list and will call `TransactionStorage+saveTransaction()` at the end of `CommandClass+execute()`. +> Commands that call `TransactionStorage+saveTransaction()` in `execute()`: +> - `BuyCommand` +> - `SellCommand` +> - `ClearCommand` + +Transaction data is saved into a text file in `./data/transactions.txt` by generating a string the newly added `Transaction` in the list, containing the attributes of the `Transaction`. +The string generated will have the attributes in the format and order of `NAME,QUANTITY,PRICE,TRANSACTION_DATE,TYPE`. This string will be appended to the end of the data file as mentioned. +If the data file does not exist while the program calls `TransactionStorage+saveTransaction()`, the data file will be recreated and all transaction data +currently in the program will be re-saved using the string format above in the method `TransactionStorage-saveAllTransactions()`. If the user clears transaction data, the program calls the +method `TransactionStorage+resaveAllTransactions()` instead which rewrites the whole save file using the same methods as above with all transactions in the list. +> For example, suppose the program wants to save the `Transaction` with attributes `name = "Apple"`, `quantity = 5`, `price = 2.55`, `transactionDate = 19-04-2024, type = "s"` +> and the program uses `",,,"` as a file delimiter between groups.\ +> The program generates the string `"NAME: Apple ,,, QTY: 5 ,,, PRICE: 2.55 ,,, DATE: 19-04-2024 ,,, T: s ,,, end"` with `end` signifying the end of the data string.\ +> Do note that the `end` is only a placeholder and can be any string of characters. +> > To justify why the saved string format is different from `Item` objects, the save file of `Transaction` objects can also act as a receipt of the user's expenses, thus it is made more readable. + +The string is generated by the method `TransactionStorage-getTransactionData()` + +##### **Loading** +`SuperTracker.run()` calls `TransactionStorage+loadTransactionData()` at the beginning of the program. +`loadTransactionData` looks for the save file `./data/transactions.txt` and reads the lines from `transactions.txt` if it exists. The attributes +of a `Transaction` is extracted from the line and a new `Transaction` object is created and put in the `TransactionList` class. Similar to loading `Item` data, +corrupted `Transaction` data will be dealt with accordingly. +> Note: Including the criteria for corrupted data in `Item` data, if a transaction date has been edited such that the new date is of a date that has not occurred yet, +> _(e.g. changing a date from today to 1 year from today)_, this will also be treated as corrupted 'Transaction' data. + +
+ +## Implementation + +### New Command +The following is a class diagram of the NewCommand and its relevant dependencies
+![NewCommandClass](uml-diagrams/NewCommandClass.png) + +The `NewCommand` class implements the `Command` interface and is responsible for handling the creation of new items in the `Inventory`. +A NewCommand instance is created by the `parseNewCommand` method called by Parser, which ensures that the provided parameters (name, quantity, price, expiry date) are valid. + +#### Dependencies +- `Item`: For creating the new item +- `Inventory`: For adding the new item into the inventory +- `Ui`: To notify the user about the successful execution of `NewCommand` +- `ItemStorage`: To save the new item added onto the hard disk + +The following sequence diagram shows the execution of a NewCommand
+![NewCommandSequence](uml-diagrams/NewCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `NewCommand` +2. A new `Item` object with the given parameters (name, quantity, price, expiry date) is created and returned to `NewCommand` +3. The `put` method of the `Inventory` class is called to add the newly created item into the inventory +4. The `newCommandSuccess` method of the `Ui` class is called to notify that `NewCommand` has been successfully executed +5. The `saveData` method of the `ItemStorage` class is called to save the new item added onto the hard disk + +> Note that if the user inputs a name parameter that contains the file delimiter of the save file, the file delimiter in the name will be +> replaced. e.g. If the file delimiter is determined to be `",,,"`, the item name `"app,,,le` will be renamed to `"app_le"` + +
+ +### Delete Command +The following is a class diagram of the DeleteCommand and its relevant dependencies
+![DeleteCommandClass](uml-diagrams/DeleteCommandClass.png) + +The `DeleteCommand` class implements the `Command` interface and is responsible for deleting existing items in the +inventory. A DeleteCommand instance is created when calling the `parseDeleteCommand` method called in Parser class. +This method parses the input and ensures that the command parameter (item name) exists in the inventory. The `execute()` +method in the class will call the `delete` method from `Inventory` class to remove the item. It will then execute +the `saveData` method from `ItemStorage` class to save changes to the inventory. + +#### Dependencies +- `Inventory`: To check if an item exists and delete it from the inventory +- `Ui`: To notify the user about the successful execution of `DeleteCommand` +- `ItemStorage`: To remove the deleted item from the hard disk + +The following sequence diagram shows the execution of a DeleteCommand
+![DeleteCommandSequence](uml-diagrams/DeleteCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `DeleteCommand` +2. The `contains` method of `Inventory` to check if the item exists in the inventory +3. If item exists, the `delete` method of `Inventory` is called to delete the item from inventory +4. Subsequently, the `deleteCommandSuccess` method of `Ui` is called to notify that `DeleteCommand` has been successfully executed +5. The `saveData` method of `ItemStorage` is called to remove the deleted item from the hard disk + +
+ +### Add Command +The following is a class diagram of the AddCommand and its relevant dependencies
+![AddCommandClass](uml-diagrams/AddCommandClass.png) + +The `AddCommand` class implements the `Command` interface and is responsible for increasing the quantity of an item. +An AddCommand instance is created by `parseAddCommand` method called by Parser, which ensures that the provided parameters (name, quantity) are valid. +This command is inherited by the [BuyCommand](#buy-command) class. + +#### Dependencies +- `Item`: To create a new instance of the updated item +- `Inventory`: For adding the updated item into the inventory +- `Ui`: To notify the user about the successful execution of `AddCommand` +- `ItemStorage`: To save the updated item onto the hard disk + +The following sequence diagram shows the execution of an AddCommand
+![AddCommandSequence](uml-diagrams/AddCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `AddCommand` +2. The private method `executeWithoutUi` is called to execute the command excluding Ui outputs +3. The `get` method of the `Inventory` class is called to return the old instance of the item +4. The `getQuantity`, `getName`, `getPrice`, `getExpiry` methods of the old instance of the item are called to obtain the + quantity, name, price, expiry date of the old item respectively +5. A new `Item` object with the increased quantity is created and returned to `AddCommand` +6. The `put` method of the `Inventory` class is called to add the updated item into the inventory +7. The `saveData` method of the `ItemStorage` class is called to save the updated item onto the hard disk +8. The `addCommandSuccess` method of the `Ui` class is called to notify that `AddCommand` has been successfully executed + +> Note: The execute add command without Ui block will be referenced by the sequence diagram of [BuyCommand](#buy-command) + +
+ +### Remove Command +The following is a class diagram of the RemoveCommand and its relevant dependencies
+![RemoveCommandClass](uml-diagrams/RemoveCommandClass.png) + +The `RemoveCommand` class implements the `Command` interface and is responsible for decreasing the quantity of an item. +A RemoveCommand instance is created by `parseRemoveCommand` method called by Parser, which ensures that the provided parameters (name, quantity) are valid. +This command is inherited by the [SellCommand](#sell-command) class. + +#### Dependencies +- `Item`: To create a new instance of the updated item +- `Inventory`: For adding the updated item into the inventory +- `Ui`: To notify the user about the successful execution of `RemoveCommand` +- `ItemStorage`: To save the updated item onto the hard disk + +The following sequence diagram shows the execution of a RemoveCommand
+![RemoveCommandSequence](uml-diagrams/RemoveCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `RemoveCommand` +2. The private method `executeWithoutUi` is called to execute the command excluding Ui outputs +3. The `get` method of the `Inventory` class is called to return the old instance of the item +4. The `getQuantity`, `getName`, `getPrice`, `getExpiry` methods of the old instance of the item are called to obtain the + quantity, name, price, expiry date of the old item respectively +5. A new `Item` object with the decreased quantity is created and returned to `RemoveCommand` +6. The `put` method of the `Inventory` class is called to add the updated item into the inventory +7. The `saveData` method of the `ItemStorage` class is called to save the updated item onto the hard disk +8. The `removeCommandSuccess` method of the `Ui` class is called to notify that `RemoveCommand` has been successfully executed + +> Note: The execute remove command without Ui block will be referenced by the sequence diagram of [SellCommand](#sell-command) + +
+ +### Update Command +The following is a class diagram of the UpdateCommand and its relevant dependencies
+![UpdateCommandClass](uml-diagrams/UpdateCommandClass.png) + +The `UpdateCommand` class implements the `Command` interface and is responsible for updating an existing item in the +`Inventory`. A UpdateCommand instance is created by the `parseUpdateCommand` method called by Parser, which ensures +that the provided parameters (name, quantity, price, expiryDate) are valid. At least one of the optional parameters +(quantity, price, expiryDate) must be provided. + +#### Dependencies +- `Item`: To create a new instance of the updated item +- `Inventory`: For adding the updated item into the inventory +- `Ui`: To notify the user about the successful execution of `UpdateCommand` +- `ItemStorage`: To save the updated item onto the hard disk + +The following sequence diagram shows the execution of a UpdateCommand
+![UpdateCommandSequence](uml-diagrams/UpdateCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `UpdateCommand` +2. The `get` method of `Inventory` is called to return the old instance of the item +3. There is an optional check for newQuantity being -1 (an invalid value that indicates that quantity should not be +updated). If the condition holds true it retrieves the item's previous quantity. +4. There is an optional check for newPrice being -1 (an invalid value that indicates that price should not be +updated). If the condition holds true it retrieves the item's previous price. +5. There is an optional check for newExpiryDate being "1-1-1" (an invalid value that indicates that expiry date should not be +updated). If the condition holds true it retrieves the item's previous expiry date. +6. The `getName` method of the old instance of the item is called to retrieve the item's previous name +7. A new `Item` object with the updated parameters (quantity, price expiryDate) is created and returned to `UpdateCommand` +8. The `put` method of the `Inventory` class is called to add the updated item into the inventory +9. The `UpdateCommandSuccess` method of the `Ui` class is called to notify that `UpdateCommand` has been successfully executed +10. The `saveData` method of the `ItemStorage` class is called to save updated item onto the hard disk + +
+ +### Find Command +The following is a class diagram of the FindCommand and its relevant dependencies
+![FindCommandClass](uml-diagrams/FindCommandClass.png) + +The `FindCommand` class implements the `Command` interface and is responsible for searching for items in the inventory by name. +A FindCommand instance is created by the `parseFindCommand` method called by `Parser`, which ensures that the provided parameter (name) is valid. + +#### Dependencies +- `Inventory`: For getting the list of items in the inventory +- `Ui`: To notify the user about the successful execution of `FindCommand` + +The following sequence diagram shows the execution of a FindCommand
+![FindCommandSequence](uml-diagrams/FindCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `FindCommand` +2. A boolean variable `isFound` is assigned a false value +3. The `getItems` method of the `Inventory` class is called to get the list of items in the inventory +4. The `FindCommand` class will loop through each item of the list of items +5. On every iteration, the item will be checked if it contains the word that is to be found +6. If the item contains the word, the `foundItem` method of the Ui class is called and the `isFound` variable is assigned a true value +7. After the loop ends, if the `isFound` variable is still false, the `noItemFound` method of the Ui class is called to notify that no item has been found containing the word + +
+ +### Rename Command +The following is a class diagram of the RenameCommand and its relevant dependencies
+![RenameCommandClass](uml-diagrams/RenameCommandClass.png) + +The `RenameCommand` class implements the `Command` interface and is responsible for renaming an item chosen by the user. +A RenameCommand instance is created by the `parseRenameCommand` method called by `Parser`, which ensures that the provided parameters (name and newName) are valid. + +#### Dependencies +- `Item`: To create a new instance of the renamed item +- `Inventory`: For getting the chosen item in the inventory +- `Ui`: To notify the user about the successful execution of `RenameCommand` +- `ItemStorage`: To save the renamed item onto the hard disk + +The following sequence diagram shows the execution of a RenameCommand
+![RenameCommandSequence](uml-diagrams/RenameCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `RenameCommand` +2. The item object of the item to be renamed is obtained from `Inventory` +3. The old name, quantity, price and expiry date are obtained from the item +4. A new `Item` object with the given parameters (newName, quantity, price, expiry date) is created and returned to `RenameCommand` +5. The `delete` method of the `Inventory` class is called to delete the old item +6. The `put` method of the `Inventory` class is called to put the new item into the inventory +7. The `RenameCommandSuccess` method of the `Ui` class is called to notify that `RenameCommand` has been successfully executed +8. The `saveData` method of the `ItemStorage` class is called to save the renamed item onto the hard disk + +
+ +### List Command +The following is a class diagram of the ListCommand and its relevant dependencies
+![ListCommandClass](uml-diagrams/ListCommandClass.png) + +The `ListCommand` class implements the `Command` interface and is responsible for printing out a list of items in the inventory to the output. +A ListCommand instance is created by the `parseListCommand` method called by Parser, which parses the user input to determine how the list should be printed out. + +#### Dependencies +- `Item`: For getting the comparator needed to sort the list of items +- `Inventory`: For getting the list of items in the inventory +- `Ui`: To print the list of items in the inventory to the output + +The 7 parameters in the constructor `firstParam`, `secondParam`, `thirdParam`, `firstSortParam`, `secondSortParam`, `thirdSortParam`, `isReverse` +are used to determine how the list should be printed out. +- `firstParam`,`secondParam`,`thirdParam` + - Used to determine if quantity, price and/or expiry date should be printed out and in what order + e.g. If the command specifies `list q/ p/ e/`, for each item quantity will be printed out first followed by price and expiry date + - Can only take 4 possible values `"q"`,`"p"`,`"e"`,`""` + - Value corresponds to the order of the flags in the command + e.g. If the command specifies `list p/ e/ q/`, then `firstParam == "p"`, `secondParam == "e"`, `thirdParam == "q"` + - `""` will be the default value if there are less than 3 flags in the command + e.g. If the command specifies `list e/`, then `firstParam == "e"`, `secondParam == ""`, `thirdParam == ""` +- `firstSortParam`,`secondSortParam`,`thirdSortParam` + - Used to determine how the list should be sorted and in what order in the event of ties + e.g. If the command specifies `list sq/ sp/ se/`, list will be sorted in order of ascending quantity. + Ties will be sorted in order of ascending price, then ascending date, followed by ascending alphabetical order (A-Z) + - Can only take 4 possible values `"q"`,`"p"`,`"e"`,`""` + - Value corresponds to the order of the sorting flags in the command + e.g. If the command specifies `list sp/ se/ sq/`, then `firstSortParam == "p"`, `secondSortParam == "e"`, `thirdSortParam == "q"` + - `""` will be the default value if there are less than 3 flags in the command + e.g. If the command specifies `list se/`, then `firstSortParam == "e"`, `secondSortParam == ""`, `thirdSortParam == ""` +- `isReverse` + - True if the user has input the reverse flag `r/`, false otherwise. + +There are 8 main sorting modes in total +1. `list`: Alphabetical ascending (e.g. A-Z) +2. `list r/`: Alphabetical descending (e.g. Z-A) +3. `list sq/`: Quantity ascending +4. `list sq/ r/`: Quantity descending +5. `list sp/`: Price ascending +6. `list sp/ r/`: Price descending +7. `list se/`: Expiry date ascending (e.g. 2024-2025) +8. `list se/ r/`: Expiry date descending (e.g. 2025-2024) + +The following sequence diagram shows the execution of a ListCommand
+![ListCommandSequence](uml-diagrams/ListCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `ListCommand` +2. The `getItems` method of the `Inventory` class is called to get an `ArrayList` of items in the inventory +3. The `listIntro` method of the `Ui` class is called to print out the total number of items in the inventory +4. The private method `sortBy` is called 4 times with different input parameters to sort the `ArrayList` of items according to alphabet, quantity, price and/or expiry date +5. If `isReverse` is true, the `reverse` method of the `Collections` class is called to reverse the `ArrayList` of items +6. For each item in the list, the `listItem` method of the `Ui` class is called to print each item to the output + +
+ +### Report Command +The following is a class diagram of the ReportCommand and its relevant dependencies
+![ReportCommandClass](uml-diagrams/ReportCommandClass.png) + +The `ReportCommand` class implements the `Command` interface and is responsible for printing a report to the output. +A ReportCommand instance is created by the `parseReportCommand` method called by Parser, which parses the user input to determine how the report should be printed out. + +#### Dependencies +- `Item`: For getting the comparator needed to sort the list of items +- `Inventory`: For getting the list of items in the inventory +- `Ui`: To print the list of items in the inventory to the output + +There are 2 types of reports: +1. `report r/low stock t/threshold`: Lists all items that are low on stock based on the threshold +2. `report r/expiry`: Lists all items that have expired or are 1 week to expiring + +The following sequence diagram shows the execution of a ReportCommand
+![ReportCommandSequence](uml-diagrams/ReportCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `ReportCommand` +2. The `getItems` method of the `Inventory` class is called to get an `ArrayList` of items in the inventory +3. There is an alternative path check for whether the list of items is empty. +If it is `ReportCommand` calls the method `reportNoItems` of the `Ui` class. If it is not it calls `reportHasItemsExecute` of the `Ui` class. +4. In the `reportHasItemsExecute` method, there is an alternative path check for the type of report. +If its of type "low stock" it calls the method `createLowStockReport` of `ReportCommand` class. If its of type "expiry" it calls the method `createExpiryReport` of the `ReportCommand` class. +5. In `createLowStockReport`, for each item in items it checks if the item's quantity is below the threshold requirement. +If it is, it is added to the report. At the end, the report is sorted by quantity using the ArrayList sort method and the method `reportCommandSuccess` of the `Ui` class is called for the report. +6. In `createExpiryReport`, for each item in items it checks if the item's expiry date is between the present day and a week later or has already passed +and adds the item to 2 different reports if the respective requirements are met. At the end, both reports are sorted by their expiry dates +using the ArrayList sort method and 2 instances of the method `reportCommandSuccess` of the `ReportCommand` class is called for each report. + +
+ +### Buy Command +The following is a class diagram of the BuyCommand as its relevant dependencies
+![BuyCommandClass](uml-diagrams/BuyCommandClass.png) + +The `BuyCommand` class inherits from the `AddCommand` class and is responsible for increasing the quantity of an item and creating a new buy transaction. +A BuyCommand instance is created by the `parseBuyCommand` method called by Parser, which ensures that the provided parameters (name, quantity, price) are valid. + +#### Dependencies +- `Transaction`: For creating the new buy transaction +- `TransactionList`: For adding the new buy transaction to the transaction list +- `TransactionStorage`: To save the new buy transaction onto the hard disk +- `Ui`: To notify the user about the successful execution of `BuyCommand` +- Other dependencies inherited from `AddCommand` + +The following sequence diagram shows the execution of a BuyCommand
+![BuyCommandSequence](uml-diagrams/BuyCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `BuyCommand` +2. The methods in the reference frame execute add command without Ui are executed, more details can be found in the sequence diagram of [AddCommand](#add-command) +3. A new `Transaction` object is created and returned to `BuyCommand` +4. The `add` method of the `TransactionList` class is called to add the newly created buy transaction into the transaction list +5. The `buyCommandSuccess` method of the `Ui` class is called to notify that `BuyCommand` has been successfully executed +6. The `saveTransaction` method of the `TransactionStorage` class is called to save the new buy transaction onto the hard disk + +
+ +### Sell Command +The following is a class diagram of the SellCommand as its relevant dependencies
+![SellCommandClass](uml-diagrams/SellCommandClass.png) + +The `SellCommand` class inherits from the `RemoveCommand` class and is responsible for decreasing the quantity of an item and creating a new sell transaction. +A SellCommand instance is created by the `parseSellCommand` method called by Parser, which ensures that the provided parameters (name, quantity) are valid. + +#### Dependencies +- `Transaction`: For creating the new sell transaction +- `TransactionList`: For adding the new sell transaction to the transaction list +- `TransactionStorage`: To save the new sell transaction onto the hard disk +- `Ui`: To notify the user about the successful execution of `SellCommand` +- Other dependencies inherited from `RemoveCommand` + +The following sequence diagram shows the execution of a SellCommand
+![SellCommandSequence](uml-diagrams/SellCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `SellCommand` +2. The methods in the reference frame execute remove command without Ui are executed, more details can be found in the sequence diagram of [RemoveCommand](#remove-command) +3. A new `Transaction` object is created and returned to `SellCommand` +4. If the quantity of items sold is more than 0, the `add` method of the `TransactionList` class is called to add the newly created sell transaction into the transaction list +5. The `saveTransaction` method of the `TransactionStorage` class is called to save the new sell transaction onto the hard disk +6. The `sellCommandSuccess` method of the `Ui` class is called to notify that `SellCommand` has been successfully executed + +
+ +### Clear Command +The following is a class diagram of the ClearCommand as its relevant dependencies
+![ClearCommandClass](uml-diagrams/ClearCommandClass.png) + +The `ClearCommand` class implements the `Command` interface and is responsible for clearing all transactions before a specified date. +A ClearCommand instance is created by the `parseClearCommand` method called by Parser, which ensures that the provided date is valid. + +#### Dependencies +- `TransactionList`: To iterate through the list and clear all transactions before the specified date +- `TransactionStorage`: To remove the cleared transactions from the hard disk +- `Ui`: To confirm with the user if they want to proceed with the clear operation, +and to notify the user if the operation has been cancelled or completed successfully + +The following sequence diagram shows the execution of a ClearCommand
+![ClearCommandSequence](uml-diagrams/ClearCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `ClearCommand` +2. The `clearCommandConfirmation` method of the `Ui` class is called to confirm with the user if they want to proceed with the clear operation +3. The User then inputs his/her response +4. If the input is not `y` or `Y`, the `clearCommandCancelled` method of the `Ui` class is called to notify the user that the clear operation has been cancelled +5. Else if the input is `y` or `Y`, the private method `clearOldTransactions` is called to clear all transactions before the specified date, +and the number of transactions cleared is returned +6. The `clearCommandSuccess` method of the `Ui` class is called to notify the user that the clear operation has been successfully completed +7. The `resaveCurrentTransactions` method of the `TransactionStorage` class is called to remove the cleared transactions from the hard disk + +
+ +### Expenditure Command +The following is a class diagram of the ExpenditureCommand and its relevant dependencies
+![ExpenditureCommandClass](uml-diagrams/ExpenditureCommandClass.png) + +The `ExpenditureCommand` class implements the `Command` interface and is responsible for printing out the expenditure and relevant buy transactions to the output. +A ExpenditureCommand instance is created by the `parseExpenditureCommand` method called by Parser, which parses the user input to determine the range of dates that the expenditure is supposed to calculate. + +#### Dependencies +- `Item`: For getting the comparator needed to sort the list of transactions +- `TransactionList`: For calculating the expenditure and getting the filtered list of transactions +- `Ui`: To print the filtered list of transactions to the output + +There are 4 types of expenditures that can be requested: +1. `exp type/today`: Lists all buy transactions that occurred today +2. `exp type/total`: Lists all buy transactions in total +3. `exp type/day from/{startDate}`: Lists all buy transactions that occurred during the specified day +4. `exp type/range from/{startDate} to/{endDate}`: Lists all buy transactions that occurred in the specified range of dates not inclusive of the start and end dates + +The following sequence diagram shows the execution of a ExpenditureCommand
+![ExpenditureCommandSequence](uml-diagrams/ExpenditureCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `ExpenditureCommand` +2. There is an alternative path check for whether the task is "today", "total", "day" and "range" and the +method `calculateDay`, `calculateTotal`, `calculateDay` and `calculateRange` of class `TransactionList` would be called respectively +which returns expenditure of type BigDecimal +3. The method `getFilteredTransactionList` of class `TransactionList` is then called which returns filteredList of class `ArrayList` +4. filteredList is then sorted by calling sort from the `ArrayList` class and sorted by the transaction date +5. The `reverse` method of class `Collections` is called on filteredList +6. `printRevenueExpenditure` method of class `Ui` is called and the list of filtered buy transactions is printed out + +
+ +### Revenue Command +The following is a class diagram of the RevenueCommand and its relevant dependencies
+![RevenueCommandClass](uml-diagrams/RevenueCommandClass.png) + +The `RevenueCommand` class implements the `Command` interface and for calculating and printing out the revenue and relevant sell transactions to the output. +A RevenueCommand instance is created by the `parseRevenueCommand` method called by Parser, which parses the user input to determine the range of dates that the revenue is supposed to calculate. + +#### Dependencies +- `Item`: For getting the comparator needed to sort the list of transactions +- `TransactionList`: For calculating the revenue and getting the filtered list of transactions +- `Ui`: To print the filtered list of transactions to the output + +There are 4 types of revenues that can be requested: +1. `rev type/today`: Lists all sell transactions that occurred today +2. `rev type/total`: Lists all sell transactions in total +3. `rev type/day from/{startDate}`: Lists all sell transactions that occurred during the specified day +4. `rev type/range from/{startDate} to/{endDate}`: Lists all sell transactions that occurred in the specified range of dates not inclusive of the start and end dates + +The following sequence diagram shows the execution of a RevenueCommand
+![RevenueCommandSequence](uml-diagrams/RevenueCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `RevenueCommand` +2. There is an alternative path check for whether the task is "today", "total", "day" and "range" and the + method `calculateDay`, `calculateTotal`, `calculateDay` and `calculateRange` of class `TransactionList` would be called respectively + which returns revenue of type BigDecimal +3. The method `getFilteredTransactionList` of class `TransactionList` is then called which returns filteredList of class `ArrayList` +4. filteredList is then sorted by calling sort from the `ArrayList` class and sorted by the transaction date +5. The `reverse` method of class `Collections` is called on filteredList +6. `printRevenueExpenditure` method of class `Ui` is called and the list of filtered sell transactions is printed out + +
+ +### Profit Command +The following is a class diagram of the ProfitCommand and its relevant dependencies
+![ProfitCommandClass](uml-diagrams/ProfitCommandClass.png) + +The `ProfitCommand` class implements the `Command` interface and is responsible for calculating and printing out the total profit generated over a specified time frame. +A ProfitCommand instance is created by the `parseProfitCommand` method called by Parser, which parses the user input to determine the range of dates that the profit is supposed to calculate. + +#### Dependencies +- `TransactionList`: For calculating the expenditure and revenue +- `Ui`: To print the profit to the output + +The following sequence diagram shows the execution of a ProfitCommand
+![ProfitCommandSequence](uml-diagrams/ProfitCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `ProfitCommand` +2. There is an alternative path check for whether the task is "today", "total", "day" and "range" and the + method `calculateDay`, `calculateTotal`, `calculateDay` and `calculateRange` of class `TransactionList` would be called respectively + which returns expenditure and revenue of type BigDecimal for calculation of profit +3. `printProfit` method of class `Ui` is called and the profit over the specified time frame is printed out + +
+ +### Help Command +The following is a class diagram of the HelpCommand and its relevant dependencies
+![HelpCommandClass](uml-diagrams/HelpCommandClass.png) + +The `HelpCommand` class implements the `Command` interface and is responsible for helping users with the commands. +A HelpCommand instance is created by the `parseCommand` method called by Parser. + +#### Dependencies +- `HelpCommandUi`: To print various help messages + +The following sequence diagram shows the execution of a HelpCommand
+![HelpCommandSequence](uml-diagrams/HelpCommandSequence.png) + +1. The `SuperTracker` class calls the `execute` method of `HelpCommand` +2. The `helpCommandSuccess` method of the `HelpCommandUi` class is called to notify that the help command has been successfully executed +3. `helpCommandSuccess` will also print a list of commands available for user to input +4. If the user inputs a valid command, the `printCommandParams` method of the `HelpCommandUi` is called to print the parameters needed for the chosen command. +Else, the `printInvalidHelpMessage` method of the `HelpCommandUi` is called to notify that the user has input an invalid command +5. The `helpClosingMessage` method of the `HelpCommandUi` class is then called to notify that the user has been returned to the main console + +
+ +## Appendix: Requirements +### Product Scope +#### Target user profile: +* Works as a supermarket inventory manager +* Has a need to manage a significant amount of items in an inventory +* Has a need to manage a significant amount of transactions +* Can type fast +* Prefers typing to mouse interactions with GUIs +* Is comfortable using Command Line Interface (CLI) applications + +#### Value proposition: +**SuperTracker** is designed to provide the following benefits for inventory management: +1. Fast and easy access to inventory information +2. Efficient tracking of stock level +3. Efficient tracking of expiring goods +4. Recording transactional data (buying and selling of goods) +5. Generation of expenditure and revenue reports +6. Calculating overall profit +7. Improved user experience for managers who prefer typing + +### User Stories + +| Version | As a ... | I want to ... | So that I can ... | +|---------|-----------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| v1.0 | user | use a command line interface with simple commands | navigate through the app quickly | +| v1.0 | user | enter input parameters in any order I want | have flexibility in entering input parameters | +| v1.0 | user | add and delete product items | so that I can modify the list of items in my inventory | +| v1.0 | user | list the full inventory of my products | know all products that are in my inventory | +| v1.0 | user | know the specific number of items in the inventory | keep track of the quantity of items available | +| v1.0 | user | know the price of an item in the inventory | draw correlations between price and sales | +| v1.0 | user | update an item's price and quantity | edit the parameters of my product item as I want | +| v1.0 | user | add/remove a certain number of an item's quantity without calculation | modify an item's quantity simply without needing to do math | +| v1.0 | user unfamiliar with the commands | be shown error messages informing me of my invalid inputs | know which part of my input is incorrect/invalid when I have entered an invalid command | +| v2.0 | user | sort my items by the parameter of my choice | view my items in a sorted manner | +| v2.0 | user | save the items in my inventory to the hard disk | avoid having to re-enter all items in the inventory into the program after I quit and restart the program | +| v2.0 | user | add expiry dates to my product items | keep track of how long these items can be stored | +| v2.0 | user | search for an item names that contain a specific keyword | search for specific items in my inventory | +| v2.0 | user | view a list of items that have quantity less than a number I specify | track items that are low in stock | +| v2.0 | user | view a list of items that have expired or are close to expiry | be informed of whether I can keep the items in my inventory | +| v2.1 | new user | see usage instructions | refer to them when I forget how to use the application | +| v2.1 | user | keep a record of my transactions (e.g. buying and selling of items) | be informed of all my transactions and expenses | +| v2.1 | user | know my expenditure, revenue and profit | check and keep track of how much money I am spending/earning | +| v2.1 | user | rename an existing item in the inventory | update the name of a product item without having to delete it | + +### Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has Java `11` installed. +2. Should be able to hold up to 1000 unique items or transactions without a noticeable sluggishness in performance +for typical usage. +3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) +should be able to accomplish most of the tasks faster using commands than using the mouse. + +### Glossary + +* ***Mainstream OS*** - Windows, Linux, macOS, Unix + +## Appendix: Instructions for manual testing + +### Saving data +Dealing with corrupted data files +- Prerequisite: Existing data files `items.txt` and `transactions.txt` in the `./data/` directory. Multiple valid lines of data in text files. +- **For item data:**\ +In the text file `items.txt`, with valid lines of data being `name ,,, quantity ,,, price ,,, dd-MM-yyyy ,,, end` + - For corrupted data + 1. Remove a file delimiter (the `,,,` character sequence) on any valid data line + 2. Replace an existing number in any data line (quantity or price) with a negative number + 3. In any data line with an existing expiry date, replace the `"-"` with `"/"` + - Expected behaviour: Relaunch the program. On start, the program should output an error message + indicating that there were corrupted lines of data. Re-open `items.txt` and the lines that were edited should be + deleted. + - For duplicate item data, + 1. Take an existing valid line of data and duplicate it to the bottom of the text file + 2. Replace its quantity and price with any valid positive number different from the original + - Expected behaviour: Relaunch the program. On start, the program should output a message indicating that + it detected duplicate item data, and the name of the duplicated item is specified. Use `find n/NAME` with name + being the name of the duplicated item. The displayed item should have the quantity and price of the duplicated line of data. + - Can also try: adding multiple duplicated items to the text file +- **For transaction data:**\ +In the text file `transactions.txt`, with valid lines of data being `NAME: name ,,, QTY: quantity ,,, PRICE: price ,,, DATE: date ,,, T: type ,,, end` + - For corrupted data + 1. Can use the test cases from item data text file, `items.txt` data corruption as well + 2. Replace the type in any valid line of data to anything that is not `b` or `s` + 3. Delete any one of `NAME:`, `QTY:`, `PRICE:` `DATE:`, `T:` in any valid line of data + - Expected behaviour: Relaunch the program. On start, the program should output an error message + indicating that there were corrupted lines of data. Re-open `transactions.txt` and the lines that were edited should be + deleted. + - Additionally, to any valid line of data, replace the transaction date to a date far into the future, i.e. `01-01-2999` + - Expected behaviour: Relaunch the program. Similarly, the program should output an error message + indicating that there were corrupted lines of data. There should also be an additional message that tells the user + that the date edited is a date that has not happened yet. The edited line of data should also be deleted from `transactions.txt` + +> Note: Run all the test cases below consecutively for accurate results. +> Start off by deleting all text in `./data/items.txt` and `./data/transactions.txt`. + +### Create a new item +Test case: `new n/Pie q/20 p/3 e/19-04-2024` + +Expected: Pie is successfully added to the inventory +``` +Pie has been added to the inventory! +Quantity: 20 +Price: $3.00 +Expiry Date: 19/04/2024 +``` + +Test case: `new n/Pie q/20 p/3 e/20-04-2024` + +Expected: Pie is unable to be added as it already exists in the inventory +``` +Oh no! An error has occurred +Pie already exists in inventory. Use the update command instead. +``` + +### Delete an item +Test case: `delete n/Pie` + +Expected: Pie is successfully deleted from the inventory +``` +Pie has been deleted! +``` + +Test case: `delete n/pie` + +Expected: pie is unable to be deleted as it does not exist in the inventory +``` +Oh no! An error has occurred +pie does not exist in inventory. Unable to delete something that does not exist. =( +``` + +### Increase quantity +Before this test case, run `new n/Pie q/100 p/3 e/19-04-2024` + +Test case: `add n/Pie q/200` + +Expected: Quantity of Pie is increased to 300 +``` +200 Pie added to inventory! +Quantity: 300 +``` + +Test case: `add n/Pie q/2147483647` + +Expected: Unable to increase quantity of Pie due to integer overflow +``` +Oh no! An error has occurred +Unable to add your specified number of items. Why do you need more than 2147483647 items anyway? +``` + +### Decrease quantity +Test case: `remove n/Pie q/100` + +Expected: Quantity of Pie is decreased to 200 +``` +100 Pie removed from inventory! +Quantity: 200 +``` + +Test case: `remove n/Pie q/300` + +Expected: Quantity of Pie is decreased to 0 as the quantity given in the command exceeds the current quantity of Pie +``` +200 Pie removed from inventory! +Quantity: 0 +``` + +Test case: `remove n/Pie q/1` + +Expected: Quantity of Pie remains at 0 as quantity cannot be negative +``` +No Pie removed as you don't have any! +``` + +### Update an item +Test case: `update n/Pie q/300 p/5 e/21-04-2024` + +Expected: Pie is successfully updated with the given parameters +``` +Pie has been successfully updated! +Quantity: 300 +Price: $5.00 +Expiry Date: 21/04/2024 +``` + +Test case: `update n/Pie q/0 p/0` + +Expected: Quantity and price of Pie is successfully updated to 0 +``` +Pie has been successfully updated! +Quantity: 0 +Price: $0.00 +Expiry Date: 21/04/2024 +``` + +Test case: `update n/Pie e/nil` + +Expected: Expiry date of Pie has been successfully removed +``` +Pie has been successfully updated! +Quantity: 0 +Price: $0.00 +``` + +### Find an item +Test case: `find n/pi` + +Expected: Pie is successfully found as it contains "pi" (case-insensitive) +``` +Here are your found items: +1. Name: Pie + Quantity: 0 + Price: $0.00 +``` + +Test case: `find n/Apple Pie` + +Expected: Pie is not found as it does not contain "Apple" +``` +So sorry, Your item: Apple Pie could not be found. +``` + +### Rename an item +Test case: `rename n/Pie r/Ham` + +Expected: Pie is successfully renamed to Ham +``` +Pie has been successfully renamed to Ham. +Name: Ham +Quantity: 0 +Price: $0.00 +``` + +Before this test case, run `new n/Pie q/100 p/4 e/22-04-2024` + +Test case: `rename n/Ham r/Pie` + +Expected: Unable to rename Ham to Pie as Pie already exists in the inventory +``` +Oh no! An error has occurred +Pie already exists in the inventory. Please choose another new name +``` + +### List all items +Before this test case, run `new n/Egg q/200 p/2 e/22-05-2024` + +Test case: `list q/ e/ sp/` + +Expected: The list is sorted in order of ascending quantity, the parameters of each item are printed in the order of name, quantity, expiry date, price +``` +There are 3 unique items in your inventory: +1. Name: Ham Quantity: 0 Price: $0.00 +2. Name: Egg Quantity: 200 Expiry Date: 22/05/2024 Price: $2.00 +3. Name: Pie Quantity: 100 Expiry Date: 22/04/2024 Price: $4.00 +``` + +Test case: `list r/` + +Expected: The list is sorted from Z-A, with name being the only parameter displayed +``` +There are 3 unique items in your inventory: +1. Name: Pie +2. Name: Ham +3. Name: Egg +``` + +### Print report +Test case: `report r/low stock t/100` + +Expected: Prints Ham as it is the only item with quantity less than 100. Pie is not printed out as it has a quantity of exactly 100. +``` +There is 1 item low on stocks! +1. Name: Ham + Quantity: 0 +``` + +Before this test case, run `update n/Ham e/22-03-2024`. Assuming today is 19/04/2024: + +Test case: `report r/expiry` + +Expected: Pie will be listed as close to expiry and Ham will be listed as expired. Egg will not be listed since it's more than a week from expiry +``` +There is 1 item close to expiry! +1. Name: Pie + Expiry Date: 22/04/2024 +There is 1 item that is expired! +1. Name: Ham + Expiry Date: 22/03/2024 +``` + +### Buy items +Test case: `buy n/Ham q/50 p/5` + +Expected: Quantity of Ham is successfully increased to 50 and a new buy transaction is added to the transaction list +``` +50 Ham bought at $5.00 each for $250.00 in total +Quantity: 50 +``` + +Test case: `buy n/Pie q/60` + +Expected: Unable to buy more Pie as compulsory parameter price is missing +``` +Oh no! An error has occurred +Invalid buy command format! +``` + +### Sell items +Test case: `sell n/Pie q/30` + +Expected: Quantity of Pie is successfully decreased to 20 and a new sell transaction is added to the transaction list +``` +30 Pie sold at $4.00 each for $120.00 in total +Quantity: 70 +``` + +Test case: `sell n/Pie q/-1 q/10` + +Expected: Unable to sell Pie as the command takes in the first quantity parameter which is -1 and quantity cannot be negative +``` +Oh no! An error has occurred +Quantity should be a non-negative integer +``` + +### Clear transactions +Assuming today is 19/04/2024: + +Test case: `clear` + +Expected: +``` +Are you sure you want to clear all transactions before 19/04/2024? +Enter 'y' or 'Y' if you wish to proceed +Enter anything else if you wish to cancel the clear operation +``` + +Test case: `y` + +Expected: Nothing cleared as the transaction must have strictly occurred before today and not on today itself +``` +Nothing cleared. No transactions before 19/04/2024 available to clear +``` + +Test case: `clear b/01-01-2025` + +Expected: +``` +Are you sure you want to clear all transactions before 01/01/2025? +Enter 'y' or 'Y' if you wish to proceed +Enter anything else if you wish to cancel the clear operation +``` + +Test case: `y y` + +Expected: Clear operation cancelled as confirmation input must be strictly `y` or `Y` without any whitespaces +``` +Clear operation has been cancelled +``` + +Test case: `clear b/31-12-2024` + +Expected: +``` +Are you sure you want to clear all transactions before 31/12/2024? +Enter 'y' or 'Y' if you wish to proceed +Enter anything else if you wish to cancel the clear operation +``` -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Test case: `Y` -## Design & implementation +Expected: The 2 transactions before 31/12/2024 are successfully cleared +``` +2 transactions before 31/12/2024 successfully cleared! +``` -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Print expenditure +Before this test case, run `buy n/Ham q/20 p/3`. Assuming today is 19/04/2024: +Test case: `exp type/day from/19-04-2024` -## Product scope -### Target user profile +Expected: 1 buy transaction that occurred today is printed +``` +expenditure on 19/04/2024 was $60.00 +1. Name: Ham + Quantity: 20 + Price: $3.00 + Transaction Date: 19/04/2024 +``` -{Describe the target user profile} +### Print revenue +Before this test case, run `sell n/Pie q/60`. Assuming today is 19/04/2024: -### Value proposition +Test case: `rev type/range from/18-04-2024 to/20-04-2024` -{Describe the value proposition: what problem does it solve?} +Expected: 1 sell transaction that occurred today is printed +``` +revenue between 18/04/2024 and 20/04/2024 was $240.00 +1. Name: Pie + Quantity: 60 + Price: $4.00 + Transaction Date: 19/04/2024 +``` -## User Stories +### Print profit +Test case: `profit type/today` -|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| +Expected: Profit of $180.00 +``` +Today's profit is $180.00 +Nice! You have a profit. +``` -## Non-Functional Requirements +### Print a help list +Test case: `help me` -{Give non-functional requirements} +Expected: Help command works normally as extra parameter "me" is ignored +``` +Hello! These are the list of commands that I can help you with: +1. Create a new item: type 'new' to show parameters +2. Delete an item: type 'delete' to show parameters +3. Change quantity of an item: type 'change' to show parameters +4. Update an item: type 'update' to show parameters +5. Find an item: type 'find' to show parameters +6. Rename an item: type 'rename' to show parameters +7. List all items: type 'list' to show parameters +8. Print a report: type 'report' to show parameters +9. Print expenditure: type 'exp' to show parameters +10. Print revenue: type 'rev' to show parameters +11. Print profit: type 'profit' to show parameters +12. Buy or sell items: type 'transaction' to show parameters +13. Clear transactions: type 'clear' to show parameters +** Any other invalid input will bring you back to the main console +``` -## Glossary +Test case: `delete new` -* *glossary item* - Definition +Expected: Help command prints the command format for delete as the extra parameter "new" is ignored +``` +A delete command should look like this: delete n/NAME +** Refer to UserGuide for further explanation +** DO NOTE that you have been returned to the main console +``` -## Instructions for manual testing +### Quit the program +Test case: `quit quit quit` -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +Expected: Successfully quits the program as extra parameters are ignored +``` +Goodbye! +``` diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..3f832917e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ -# Duke +# SuperTracker -{Give product intro here} +SuperTracker is a desktop app for managing a supermarket’s inventory and transactions, +optimized for use via a Command Line Interface (CLI). Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..75e1694661 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,638 @@ # User Guide -## Introduction +## SuperTracker -{Give a product intro} +SuperTracker is a desktop app for managing a supermarket's inventory and transactions, +optimized for use via a Command Line Interface (CLI). + +- [Quick Start](#quick-start) +- [Features](#features) + - [Create a new item: `new`](#create-a-new-item-new) + - [Delete an item: `delete`](#delete-an-item-delete) + - [Increase quantity: `add`](#increase-quantity-add) + - [Decrease quantity: `remove`](#decrease-quantity-remove) + - [Update an item: `update`](#update-an-item-update) + - [Find an item: `find`](#find-an-item-find) + - [Rename an item: `rename`](#rename-an-item-rename) + - [List all items: `list`](#list-all-items-list) + - [Print report: `report`](#print-report-report) + - [Buy items: `buy`](#buy-items-buy) + - [Sell items: `sell`](#sell-items-sell) + - [Clear transactions: `clear`](#clear-transactions-clear) + - [Print expenditure: `exp`](#print-expenditure-exp) + - [Print revenue: `rev`](#print-revenue-rev) + - [Print profit: `profit`](#print-profit-profit) + - [Print a help list: `help`](#print-a-help-list-help) + - [Quit the program: `quit`](#quit-the-program-quit) + - [Saving inventory data](#saving-inventory-data) + - [Loading inventory data](#loading-inventory-data) + - [Editing the data file](#editing-the-data-file) +- [FAQ](#faq) +- [Command Summary](#command-summary) + +-------------------------------------------------------------------------------------------------------------------- ## Quick Start -{Give steps to get started quickly} +1. Ensure that you have Java 11 or above installed in your Computer. +2. Download the latest `SuperTracker.jar` from [here](https://github.com/AY2324S2-CS2113-T13-4/tp/releases). +3. Open a command terminal, cd into the folder you put the jar file in, and use the `java -jar SuperTracker.jar` command to run the application. +4. Type the command in the command box and press Enter to execute it. +5. Refer to the [Features](#features) below for details of each command and its format. + +-------------------------------------------------------------------------------------------------------------------- + +## Features + +> Notes about the command format: +> - Words in uppercase are parameters to be specified by the user +> - e.g. in `add n/NAME q/QUANTITY`, `NAME` and `QUANTITY` are parameters which can be used as `add n/cheese q/100` +> - Items in square brackets are optional +> - e.g. `list [q/] [p/]` can be used as `list q/`, `list p/`, `list` etc. +> - Parameters can be in any order +> - e.g. if the command specifies `n/NAME q/QUANTITY`, `q/QUANTITY n/NAME` is also acceptable +> - Commands that do not take in parameters will ignore any additional parameters input by the user +> - e.g. if the command specifies `quit now`, it will be interpreted as `quit` +> - If the command has multiple of the same parameters, it uses the first occurrence of that parameter +> - e.g. if the command specifies `new n/Milk q/100 p/5 n/Cheese`, it will be interpreted as `new n/Milk q/100 p/5` +> - If any of the compulsory parameters are empty an error is thrown +> - The value of a parameter would be interpreted as the trimmed substring after `/` up to the space right before the next valid parameter +> - e.g. if the command specifies `add n/Milk q/10q/10`, the command would be invalid as `QUANTITY` would be interpreted as `10q/10`, +> since there is no space before the second occurrence of `q/` +> - e.g. if the command specifies `add n/Milk q/10 p/10`, the command would be invalid as `QUANTITY` would be interpreted as `10 p/10`, +> since `p/` is not a valid parameter for the add command +> - e.g. if the command specifies `add n/Milk q/ 10 q/20`, the command would be valid as `QUANTITY` would be interpreted as `10` + +
+ +### Create a new item: `new` +Create a new item in the inventory + +Format: `new n/NAME q/QUANTITY p/PRICE [e/EXPIRY_DATE]` + +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` already exists in the inventory, use the [update](#update-an-item-update) command instead +- If `NAME` contains the character sequence `",,,"`, it will be replaced by a "_" character + - e.g. `"Ba,,,ll"` will be passed as `"Ba_ll"` instead + - Please do avoid using `",,,"` as it is the program's file delimiter +- `QUANTITY` must be a non-negative integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- `PRICE` must be a non-negative integer or decimal number and less than or equal to 2147483647 + - e.g. 1, 0.20, 12.3, 12.345 +- If the `PRICE` given has more than 2 decimal places, it will be rounded off to the nearest 2 decimal places + - e.g. 12.345 ≈ 12.35 +- `EXPIRY_DATE` must be in the format of `dd-mm-yyyy` and from 01-01-0001 to 31-12-9999. +Non-existent dates such as 30-02-2024 are considered invalid + - e.g. 05-10-2054, 16-07-2245 + +Example: `new n/Milk q/100 p/5` +``` +Milk has been added to the inventory! +Quantity: 100 +Price: $5.00 +``` +Example: `new n/Cheese q/50 p/1.23 e/15-06-2113` +``` +Cheese has been added to the inventory! +Quantity: 50 +Price: $1.23 +Expiry Date: 15/06/2113 +``` + +
+ +### Delete an item: `delete` +Delete an item from the inventory + +Format: `delete n/NAME` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown + +Example: `delete n/Milk` +``` +Milk has been deleted! +``` + +
+ +### Increase quantity: `add` +Increase the quantity of an item + +Format: `add n/NAME q/QUANTITY` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown +- `QUANTITY` must be a positive integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- If the new quantity of the item exceeds 2147483647, an error would be thrown + +Example: `add n/Milk q/10` +``` +10 Milk added to inventory! +Quantity: 110 +``` + +
+ +### Decrease quantity: `remove` +Decrease the quantity of an item + +Format: `remove n/NAME q/QUANTITY` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown +- `QUANTITY` must be a positive integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- If `QUANTITY` exceeds the current quantity of the item, the new quantity would be set to 0 + +Example: `remove n/Milk q/10` +``` +10 Milk removed from inventory! +Quantity: 90 +``` + +
+ +### Find an item: `find` +Find the item/items that contain the same name + +Format: `find n/NAME` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If no items containing the same name are found, an error would be thrown + +Example: `find n/Cheese` +``` +Here are your found items: +1. Name: Cheese cake + Quantity: 100 + Price: $1.00 +2. Name: Cheese + Quantity: 100 + Price: $1.00 +``` +
+ +### Rename an item: `rename` +Rename an existing item with a new name. + +Format: `rename n/NAME r/NEW_NAME` +- `NAME` is case-insensitive +- `NEW_NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NEW_NAME` contains the character sequence `",,,"`, it will be replaced by a "_" character + - e.g. `"Ba,,,ll"` will be passed as `"Ba_ll"` instead + - Please do avoid using `",,,"` as it is the program's file delimiter +- If no items containing NAME is found, an error would be thrown +- If there already exists an item with NEW_NAME, an error would be thrown + +Example: `rename n/Cheese r/Mozzerella` +``` +Cheese has been successfully renamed to Mozzerella. +Name: Mozzerella +Quantity: 30 +Price: $4.00 +``` +
+ +### Update an item: `update` +Update the quantity, price and/or expiry date of an item + +Format: `update n/NAME [q/NEW_QUANTITY] [p/NEW_PRICE] [e/NEW_EXPIRY_DATE]` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown +- `NEW_QUANTITY` must be a non-negative integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- `NEW_PRICE` must be a non-negative integer or decimal number and less than or equal to 2147483647 + - e.g. 1, 0.20, 12.3, 12.345 +- If the `NEW_PRICE` given has more than 2 decimal places, it will be rounded off to the nearest 2 decimal places + - e.g. 12.345 ≈ 12.35 +- `NEW_EXPIRY_DATE` must be in the format of `dd-mm-yyyy` and from 01-01-0001 to 31-12-9999 +or `nil` if expiry date is to be removed. Non-existent dates such as 30-02-2024 are considered invalid + - e.g. 05-10-2054, 16-07-2245 +- At least one of the optional parameters must be present + +Example: `update n/Milk q/200 p/10 e/15-06-2113` +``` +Milk has been successfully updated! +Quantity: 200 +Price: $10.00 +Expiry Date: 15/06/2113 +``` +Example: `update n/Milk q/50 p/9.99 e/nil` +``` +Milk has been successfully updated! +Quantity: 50 +Price: $9.99 +``` + +
+ +### List all items: `list` +List all unique items in the inventory. +Output will be printed to the terminal and each row will contain the name of each item. + +Format: `list [q/] [p/] [e/] [sq/] [sp/] [se/] [r/]` +- `q/` will list the quantity of each item in each row +- `p/` will list the price of each item in each row +- `e/` will list the expiry date of each item in each row if it contains a valid expiry date +- In each row, quantity, price and expiry date will be printed in the same order as the flags + - e.g. if the command specifies `list q/ p/ e/`, the quantity will be printed first followed by price and expiry date +- `sq/` will list the items in order of ascending quantity +- `sp/` will list the items in order of ascending price +- `se/` will list the items in order of ascending date, items with no expiry date will be listed at the end +- If the command has no sorting parameters, the list will be sorted in ascending alphabetical order (A-Z) by default +- If the command has multiple sorting parameters, the list will be sorted in the same order as the sorting flags + - e.g. if the command specifies `list sq/ sp/ se/`, list will be sorted in order of ascending quantity. + Ties will be sorted in order of ascending price, then ascending date, followed by ascending alphabetical order (A-Z) +- If `q/` is not specified but `sq/` is specified, the command will be interpreted as `q/ sq/`. +Same logic applies to price and expiry date + - e.g. if the command specifies `list sq/ sp/ q/`, the command will be interpreted as `list sq/ p/ sp/ q/`, + although the list will be sorted in order of ascending quantity, price will be printed before quantity +- `r/` will reverse the order of the list + +Example: `list q/ p/ e/ sp/ r/` +``` +There are 3 unique items in your inventory: +1. Name: Milk Quantity: 100 Price: $5.00 +2. Name: Juice Quantity: 300 Price: $4.00 Expiry Date: 12/03/2025 +3. Name: Cheese Quantity: 200 Price: $3.00 +``` + +
+ +### Print report: `report` +List all items that match the requirements of the report + +There are 2 types of reports: +1. **low stock** - list all items that are below a specified threshold provided by the user +2. **expiry** - list all items that are within a week of expiring or have already expired + +The report will be printed to the terminal and will contain the name of each item +- If the report type is **low stock**: + - The quantity of each item would be listed + - The report would be sorted in order of ascending quantity +- If the report type is **expiry**: + - The expiry date of each item would be listed + - The report would be sorted in order of ascending expiry date + +Format (low stock):`report r/REPORT_TYPE t/THRESHOLD_VALUE` + +Format (expiry):`report r/REPORT_TYPE [t/THRESHOLD_VALUE]` + +- `r/` parameter that specifies the type of report. e.g. **low stock** or **expiry** +- `t/` parameter that specifies the threshold value to be compared to for **low stock report**. +All items below the threshold value would be listed out. + +> Note: If report type is **low stock** threshold value must be **included**. However, if report type is **expiry** +threshold value must be **excluded** or left **empty**. + + +Example: `report r/low stock t/35` + +``` +There are 3 items low on stocks! +1. Name: Orange + Quantity: 10 +2. Name: Banana + Quantity: 20 +3. Name: Apple + Quantity: 30 +``` +Example: `report r/expiry` or `report r/expiry t/` + +``` +There is 1 item close to expiry! +1. Name: Apple + Expiry Date: 20/04/2024 +There is 1 item that is expired! +1. Name: Orange + Expiry Date: 29/03/2024 +``` + +
+ +### Buy items: `buy` +Buy items from suppliers and add them to the inventory. +This buy transaction will create a new entry in the transaction list, +with the current date saved as the transaction date. + +Format: `buy n/NAME q/QUANTITY p/PRICE` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown +- `QUANTITY` must be a positive integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- If the new quantity of the item exceeds 2147483647, an error would be thrown +- `PRICE` must be a non-negative integer or decimal number and less than or equal to 2147483647 + - e.g. 1, 0.20, 12.3, 12.345 +- If the `PRICE` given has more than 2 decimal places, it will be rounded off to the nearest 2 decimal places + - e.g. 12.345 ≈ 12.35 -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +Example: `buy n/Milk q/10 p/3` +``` +10 Milk bought at $3.00 each for $30.00 in total +Quantity: 110 +``` -## Features +
-{Give detailed description of each feature} +### Sell items: `sell` +Sell items to customers and remove them from the inventory. +This sell transaction will create a new entry in the transaction list, +with the current date saved as the transaction date. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +Format: `sell n/NAME q/QUANTITY` +- `NAME` is case-insensitive + - e.g. `Cheese` will be interpreted as `cheese` +- If `NAME` does not exist in the inventory, an error would be thrown +- `QUANTITY` must be a positive integer and less than or equal to 2147483647 + - e.g. 1, 10, 100 +- If `QUANTITY` exceeds the current quantity of the item, the new quantity would be set to 0 +- If the quantity sold is 0, no transaction would be added to the transaction list -Format: `todo n/TODO_NAME d/DEADLINE` +Example: `sell n/Milk q/10` +``` +10 Milk sold at $5.00 each for $50.00 in total +Quantity: 90 +``` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +
-Example of usage: +### Clear transactions: `clear` +Clear all transactions *before* a specified date -`todo n/Write the rest of the User Guide d/next week` +Format `clear [b/BEFORE_DATE]` +- If the optional parameter `[b/BEFORE_DATE]` is not specified or `BEFORE_DATE` is empty, +the current date will be taken as the before date +- `BEFORE_DATE` must be in the format of `dd-mm-yyyy` and from 01-01-0001 to 31-12-9999. + Non-existent dates such as 30-02-2024 are considered invalid + - e.g. 05-10-2054, 16-07-2245 +> Note: This command does **NOT** clear transactions that occurred on `BEFORE_DATE` -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +- If the initial command is successful, an additional confirmation message will be shown to the user +- Enter `y` or `Y` to proceed with the clear operation +- Enter anything else to cancel the clear operation +> Note: `y` or `Y` must be exact and should not contain any whitespace or other characters. e.g. entering `y y` or `yY` will cancel the clear operation + +Example: `clear b/19-04-2024` +``` +Are you sure you want to clear all transactions before 19/04/2024? +Enter 'y' or 'Y' if you wish to proceed +Enter anything else if you wish to cancel the clear operation +``` + +Example: `y` +``` +2 transactions before 19/04/2024 successfully cleared! +``` + +
+ +### Print expenditure: `exp` +There are 4 types of expenditure commands: +1. **today** - list all buy transactions that occurred today +2. **total** - list all buy transactions in total +3. **day** - list all buy transactions that occurred during the specified day +4. **range** - list all buy transactions that occurred in the specified range of dates +not inclusive of the start and end dates + +The cumulative expenditure will be first printed to the terminal. +Afterward, each buy transaction will be printed to the terminal containing its name, +quantity, price and transaction date. +All buy transactions will be listed from most recent to least recent. + +Format: `exp type/EXPENDITURE_TYPE [from/START_DATE] [to/END_DATE]` + +> Note: If the type is **today** or **total**, START_DATE and END_DATE are not supposed to be filled. +> If the type is **day**, START_DATE is compulsory and END_DATE is not supposed to be filled. +> If the type is **range**, both START_DATE and END_DATE are compulsory. + +Example: `exp type/today` + +``` +Today's expenditure is $94.50 +1. Name: Apple + Quantity: 30 + Price: $2.15 + Transaction Date: 19/04/2024 +2. Name: Banana + Quantity: 10 + Price: $3.00 + Transaction Date: 19/04/2024 +``` + +
+ +### Print revenue: `rev` +There are 4 types of revenue commands: +1. **today** - list all sell transactions that occurred today +2. **total** - list all sell transactions in total +3. **day** - list all sell transactions that occurred during the specified day +4. **range** - list all sell transactions that occurred in the specified range of dates +not inclusive of the start and end dates + +The cumulative revenue will be first printed to the terminal. +Afterward, each sell transaction will be printed to the terminal containing its name, +quantity, price and transaction date. +All sell transactions will be listed from most recent to least recent. + +Format: `rev type/REVENUE_TYPE [from/START_DATE] [to/END_DATE]` + +> Note: If the type is **today** or **total**, START_DATE and END_DATE are not supposed to be filled. +> If the type is **day**, START_DATE is compulsory and END_DATE is not supposed to be filled. +> If the type is **range**, both START_DATE and END_DATE are compulsory. + +Example: `rev type/today` + +``` +Today's revenue is $39.80 +1. Name: Apple + Quantity: 20 + Price: $0.99 + Transaction Date: 19/04/2024 +2. Name: Banana + Quantity: 10 + Price: $2.00 + Transaction Date: 19/04/2024 +``` + +
+ +### Print profit: `profit` +There are 4 types of profit commands: +1. **today** - calculates the profit for the day +2. **total** - calculates total profit +3. **day** - calculates the profit on a specified day +4. **range** - calculates the profit over the specified range of dates +not inclusive of the start and end dates + +The cumulative revenue will be first printed to the terminal. +Afterward, each sell transaction will be printed to the terminal containing its name, +quantity, price and transaction date. +All sell transactions will be listed from most recent to least recent. + +Format: `profit type/PROFIT_TYPE [from/START_DATE] [to/END_DATE]` + +> Note: If the type is **today** or **total**, START_DATE and END_DATE are not supposed to be filled. +> If the type is **day**, START_DATE is compulsory and END_DATE is not supposed to be filled. +> If the type is **range**, both START_DATE and END_DATE are compulsory. + +Example: `profit type/today` + +``` +Today's profit is $9.20 +Nice! You have a profit. +``` + +
+ +### Print a help list: `help` +Print a help list which displays all functions of SuperTracker which the user can then choose to see its parameters + +There are 13 different inputs the user can key in after the help list is printed. +1. `new`: prints the parameters to create a new item +2. `delete`: prints the parameters to delete an item +3. `change`: prints the parameters to change quantity of an item +4. `update`: prints the parameters to update an item +5. `find`: prints the parameters to find an item +6. `rename`: prints the parameters to rename an item +7. `list`: prints the parameters to list out items +8. `report`: prints the parameters to print a report +9. `exp`: prints the parameters to print expenditure +10. `rev`: prints the parameters to print revenue +11. `profit`: prints the parameters to print profit +12. `transaction`: prints the parameters to buy or sell an item +13. `clear`: prints the parameters to clear transactions + +As shown above, a user can choose to input any of these 13 commands or else the user will return to the main console. +>Note: If the input consists of many words, SuperTracker will only take in the first word. +> +>e.g. "delete new" will be interpreted as "delete" and delete parameters will be printed + +Format: `help` + +Example: `help` + +``` +Hello! These are the list of commands that I can help you with: +1. Create a new item: type 'new' to show parameters +2. Delete an item: type 'delete' to show parameters +3. Change quantity of an item: type 'change' to show parameters +4. Update an item: type 'update' to show parameters +5. Find an item: type 'find' to show parameters +6. Rename an item: type 'rename' to show parameters +7. List all items: type 'list' to show parameters +8. Print a report: type 'report' to show parameters +9. Print expenditure: type 'exp' to show parameters +10. Print revenue: type 'rev' to show parameters +11. Print profit: type 'profit' to show parameters +12. Buy or sell items: type 'transaction' to show parameters +13. Clear transactions: type 'clear' to show parameters +** Any other invalid input will bring you back to the main console +``` +Next input: `new` +``` +A new item command should look like this: new n/NAME q/QUANTITY p/PRICE [e/EXPIRY_DATE] +** Refer to UserGuide for further explanation +** DO NOTE that you have been returned to the main console +``` + +
+ +### Quit the program: `quit` +Quits the program + +Format: `quit` + +-------------------------------------------------------------------------------------------------------------------- + +### Saving inventory data +Data in the program is saved to the hard disk in the file path `./data/` in the same directory that +the `SuperTracker.jar` file is in. Item data will be saved automatically after any command that changes the item data in the inventory. +Similarly, transaction data will be saved automatically after commands that add or erases transaction data. + +
+ +### Loading inventory data +Data that has been saved to the hard disk will be loaded and read by the program each time it is launched. +If there is no data file, the program will the skip loading process. + +
+ +### Editing the data file +Data of the `SuperTracker` program is stored in a text files in the path `./data/` relative to +the directory the `SuperTracker.jar` file is in. Inventory data is stored in `items.txt`, and transaction data is stored in `transactions.txt`. +You are **strongly discouraged** from editing and updating the inventory or transaction data directly through the data file. +> Note 1: **Edit the data file at your own caution.** If the changes made to the data file are in an invalid format, the program +> will ignore those changes on its next load. The corrupted changes **will be erased**, so do keep a backup of the data +> file before editing. +> +> Note 2: If the program detects duplicate item names in the save file `items.txt`, among all lines of data that share the same +> item name, the bottom most data line will **overwrite** all previous occurrences of the item data that share the same item name. +> The rest of the overwritten lines **will be erased**. +> +> Note 3: Edited transaction data with dates that have not happened yet **will be treated as corrupted data**, so do avoid adding +> transactions that have not happened yet. +> +> Note 4: The program is **NOT** responsible for any data loss if you were to delete the save files while the program is running + +-------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another computer? -**A**: {your answer here} +**A**: Install the app in the other computer and overwrite the empty data file it creates +with the file that contains the data of your previous SuperTracker inventory. -## Command Summary +**Q**: What if I want multiple items with the same name? +What if I want different batches of the same item with different expiry dates? + +**A**: Simply add a unique identifier after the item name when creating a new item. +e.g. `n/Milk-1`,`n/Milk-2`. The format of the unique identifier is completely up to the user's discretion. + +**Q**: Why can't I add new items to the inventory using the buy command? + +**A**: The buy command should mainly be used when you want to replenish the stock of an existing item. +If you wish to add a newly bought item to the inventory, +first create a new item in the inventory with its quantity set to 0 using the [new](#create-a-new-item-new) command, +then use the buy command to increase its quantity. -{Give a 'cheat sheet' of commands here} +**Q**: Why am I allowed to set the quantity and/or price of an item to 0? + +**A**: We want to provide our users with more flexibility. +This can be handy for placeholders or situations where items are free. + +**Q**: Why am I allowed to set expiry dates that have already passed? + +**A**: We want to provide our users with more flexibility. +This can be useful if the user has previously entered an incorrect expiry date, +allowing them to fix their mistake. + +-------------------------------------------------------------------------------------------------------------------- + +## Command Summary -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Action | Format | Examples | +|-----------------|--------------------------------------------------------------------|----------------------------------------------| +| **New** | `new n/NAME q/QUANTITY p/PRICE e/EXPIRY_DATE` | e.g. `new n/Milk q/100 p/5 e/05-12-2113` | +| **Delete** | `delete n/NAME` | e.g. `delete n/Milk` | +| **Add** | `add n/NAME q/QUANTITY` | e.g. `add n/Milk q/10` | +| **Remove** | `remove n/NAME q/QUANTITY` | e.g. `remove n/Milk q/10` | +| **Update** | `update n/NAME [q/NEW_QUANTITY] [p/NEW_PRICE] [e/NEW_EXPIRY_DATE]` | e.g. `update n/Milk q/200 p/10 e/05-08-2113` | +| **Find** | `find n/NAME` | e.g. `find n/apple` | +| **Rename** | `rename n/NAME r/NEW_NAME` | e.g. `rename n/Milk r/Chocolate Milk` | +| **List** | `list [q/] [p/] [e/] [sq/] [sp/] [se/] [r/]` | e.g. `list q/ p/ sp/ r/` | +| **Report** | `report r/REPORT_TYPE [t/THRESHOLD_VALUE]` | e.g. `report r/low stock t/10` | +| **Buy** | `buy n/NAME q/QUANTITY p/PRICE` | e.g. `buy n/Milk q/10 p/3` | +| **Sell** | `sell n/NAME q/QUANTITY` | e.g. `sell n/Milk q/10` | +| **Clear** | `clear [b/BEFORE_DATE]` | e.g. `clear b/19-04-2024` | +| **Expenditure** | `exp type/EXPENDITURE_TYPE [from/START_DATE] [to/END_DATE]` | e.g. `exp type/today` | +| **Revenue** | `rev type/REVENUE_TYPE [from/START_DATE] [to/END_DATE]` | e.g. `rev type/today` | +| **Profit** | `profit type/PROFIT_TYPE [from/START_DATE] [to/END_DATE]` | e.g. `profit type/today` | +| **Help** | `help` | e.g. `help` | +| **Quit** | `quit` | e.g. `quit` | diff --git a/docs/team/awesomesjh.md b/docs/team/awesomesjh.md new file mode 100644 index 0000000000..0bae552e4c --- /dev/null +++ b/docs/team/awesomesjh.md @@ -0,0 +1,102 @@ +# Sim Jun Hong's Project Portfolio Page + +## Project: SuperTracker + +SuperTracker is a desktop app for managing a supermarket's inventory and expenditures, +optimized for use via a Command Line Interface (CLI). + +Given below are my contributions to the project. + +- **New Feature:** New Command + - What it does: Allows users to add a new item to the inventory. + - Justification: This is one of the core features of an inventory management system. + - Highlights: Since this was one of the first commands implemented, + this involved me setting up the base code for the `Item` class + and the `Inventory` class, as well as the general structure of the + methods used to parse each different type of command (such as `parseNewCommand`). + + +- **New Feature:** List Command + - What it does: Allows users to list out all the items in the inventory. + - Justification: Gives users the flexibility to view the list with their desired information + (name, quantity, price, expiry date) and sorted in the desired order (by name, quantity, price, expiry date) + - Highlights: This command was challenging to implement as it took in many parameters. + The presence and order of these parameters would affect how the list would be printed out. + Please refer to the [list](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html#list-all-items-list) section in the user guide for more details. + + +- **New Feature:** Add Command + - What it does: Allows users to increase the quantity of an item in the inventory. + - Justification: Useful if the user just received a new batch of items from suppliers, + and wishes to update the quantity of the item without having to calculate the new total quantity of the item manually. + - Highlights: I had to implement a special case of error checking as it was possible that the number of items added + by the user would cause the new total quantity to overflow. + + +- **New Feature:** Remove Command + - What it does: Allows users to decrease the quantity of an item in the inventory. + - Justification: Useful if the user has just removed a bunch of items from the inventory (sold or delivered), + and wishes to update the quantity of the item without having to calculate the new total quantity of the item manually. + - Highlights: I implemented this command such that if the number of items to be removed + were to exceed the current quantity, it would simply set the new quantity to 0 instead of throwing an error. + + +- **Enhancement to existing features:** Buy and Sell Commands + - What they do: These commands inherit from the add and remove commands, + with an additional functionality to create a new buy/sell transaction which will be stored in a transaction list. + - Justification: The most common cases when items are added/removed from the inventory is when they are bought/sold. + Adding this transaction recording functionality is useful because it allows users to change the quantity of items + and keep track of their transaction history using only 2 commands (buy/sell). + - Highlights: I created a new `Transaction` class which inherited from the `Item` class + and a `TransactionList` class needed to store the transactions. + + +- **New Feature:** Clear Command + - What it does: Clears all transactions before a specified date + - Justification: This feature helps to reduce file size as the transactions will be stored on the hard disk. + Transactions from a long time ago might no longer be important and can be cleared. + - Highlights: I added a confirmation message as clearing transaction history is an irreversible operation + and this would help to ensure that the user would not clear by mistake. + + +- **Enhancement to existing features:** Add error checking for invalid inputs + - What it does: Ensure that parameters entered are valid before commands are executed. + - Justification: This was necessary to ensure that our application would not break if the user enters an invalid input. + - Highlights: This was challenging as I had to run smoke-tests on our application to ensure that + most, if not all edge cases have been handled. I wrote many private methods such as `parseQuantity`, `parsePrice`, + `validateNonEmptyParam`, `validateItemExistsInInventory` etc. + that would throw a unique error in the case of invalid inputs. + + +- **Enhancement to existing features:** Contributed to JUnit tests for new, list, add, remove, buy, sell, clear commands + - What it does: Ensure that the commands still work as expected when the codebase is modified. + - Justification: Oftentimes changing one part of the code might break another part, + hence writing automated tests would help to streamline the testing process. + - Highlights: Made use of methods not taught in the course, such as `@BeforeEach`, `@AfterEach`, + `ByteArrayInputStream` to simulate user input and `ByteArrayOutputStream` to test for console output. + + +- **Code contributed:** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=awesomesjh&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + + +- **Documentation:** + - [User Guide](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html) + - Added documentation for new, list, add, remove, buy, sell, clear commands + - Added notes about command format section + - Added FAQ section + - [Developer Guide](https://ay2324s2-cs2113-t13-4.github.io/tp/DeveloperGuide.html) + - Added implementation details of new, list, add, remove, buy, sell, clear commands + - Used PlantUML to create the UML class and sequence diagrams for new, list, add, remove, buy, sell, clear commands + + +- **Contributions to team-based tasks:** + - Refactored code in `Parser` as many of the commands were using the same error checking methods + - Refactored code in `HelpCommandUi` to make it less repetitive and more scalable + - Refactored code for `parseExpenditureCommand`, `parseRevenueCommand` and `parseProfitCommand` by + creating a generic class `Triple` + - Fixed UML diagrams in developer guide according to feedback from TA + - Helped to check and fix some of my teammates' incorrect UML diagrams + - Wrote the instructions for manual testing for most of the commands + - Helped to create new issues and close completed issues on the issue tracker + - Reviewed PRs and provided suggestions on fixing bugs, improving code quality etc. + - Teammate PRs reviewed: [#75](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/75), [#104](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/104), [#212](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/212) \ No newline at end of file diff --git a/docs/team/dtaywd.md b/docs/team/dtaywd.md new file mode 100644 index 0000000000..43e4ba8afe --- /dev/null +++ b/docs/team/dtaywd.md @@ -0,0 +1,50 @@ +# Tay Wen Duan David Project Portfolio Page + +## Project: SuperTracker + +SuperTracker is a desktop app for managing a supermarket's inventory and expenditures, +optimized for use via a Command Line Interface (CLI). + +Given below are my contributions to the project. + +- **Feature:** Update Command + - What it does: Allows users to update the price, quantity and/or expiry date. + - Justification: A supermarket manager can make mistakes when adding items + and the app should provide a convenient way to rectify them, instead of manually deleting the item from the inventory + and adding it back in with a new price, quantity and/or expiry date. + - Highlights: Before creating a new update command, the parser helps to validate the users inputs by checking if the + numbers provided are negative or over the integer limit and whether the expiry date follows the correct date format. + +- **Feature:** Report Command + - What it does: Allows supermarket managers to see what items are low on stock and are close to expiry. + - Justification: A supermarket manager may want to see what items are low on stock or close to expiry, so that they + may make an order to replace them. + - Highlights: Report command handles two different types of reports with only 1 needing the optional parameter and + sorts the report based on quantity or expiry date for low stock and expiry report respectively. + +- **Feature:** Expenditure Command + - What it does: Allows supermarket managers to check their expenditures over a period of time. + - Justification: This function is necessary to see what is the cash outflow of the supermarket. Moreover, this + function is necessary to calculate the profit of the supermarket. + - Highlights: The expenditure command unifies what the revenue commands uses to reduce the amount of repeated code. + +- **Enhancement to existing features:** Add error checking for invalid inputs to expenditure and revenue command + - What it does: Ensure that the commands still work as expected when invalid inputs are given. + - Justification: The transaction related commands have many different types and each type needs specific inputs. + - Highlights: Breaks it down to the type of task to check for the various conditions that each type requires. + + +- **Code contributed** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=dtaywd&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + + +- **Documentation** + - [User Guide](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html) + - Added documentation for update, report and expenditure command + - [Developer Guide:](https://ay2324s2-cs2113-t13-4.github.io/tp/DeveloperGuide.html) + - Added implementation details of update, report and expenditure command + - Used PlantUML to create the UML class and sequence diagrams for update, report and expenditure command + +- **Contributions to team-based tasks:** + - Teammate PR reviewed: [40](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/40), [71](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/71), [85](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/85), [240](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/240) + - Contributed to the JUnit tests for update, report, revenue and expenditure command + - Helped suggest improvements and create new issues on Github \ 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/rismm.md b/docs/team/rismm.md new file mode 100644 index 0000000000..25345f84ff --- /dev/null +++ b/docs/team/rismm.md @@ -0,0 +1,47 @@ +# Sim Jing Jie Ryan's Project Portfolio Page + +## Project: SuperTracker + +SuperTracker is a desktop app for managing a supermarket's inventory and expenditures, +optimized for use via a Command Line Interface (CLI). + +Given below are my contributions to the project. + +- **Feature:** Base functionality of the input parser + - Implemented the use of a regex format for matching user inputs. + - Justification: Streamlines the process of obtaining the user parameters and checking for valid/invalid inputs entered by the user. + The base implementation can be used to simplify the parsing of inputs for any future commands that are to be implemented in the future. + - Highlights: The implementation allows user to enter valid input parameters in any order they wish. This introduced greater complexity and challenge in + the implementation in creating a format and regex that can accomplish this feature. + + +- **Feature:** File saving and loading + - Justification: This feature enables the program to save item and transaction data that was entered in the program to a local hard disk, + and loads the saved data back into the program when the program is started up again. + - Highlights: Corrupting the save files with invalid formats would not crash the program, and corrupted lines will be ignored. + + +- **Feature:** Error handling + - Added base implementation of error handling and error output messages + - Justification: Error handling is important to ensure that the program does not crash unexpectedly. The format that I have implemented + is used widely to catch errors in user inputs. + + +- **Code contributed:** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=rismm&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + + +- **Documentation:** + - [User Guide:](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html) + - Added documentation for the features of saving, loading and editing file data + - [Developer Guide:](https://ay2324s2-cs2113-t13-4.github.io/tp/DeveloperGuide.html) + - Added implementation details of input parsing (the use of regex) and storage (data file saving and loading) + - Used PlantUML to create the UML class and sequence diagrams for input parsing and the storage implementation + - Added the architecture diagram its descriptions of the components + - Filled in the user stories in the appendix + + +- **Contributions to team-based tasks:** + - Teammate PRs reviewed: [#40](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/40), [#70](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/70), [#73](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/73) + - Contributed to JUnit tests for parser and storage, also helped fix JUnit tests for the update command + - Helped detect bugs and provided suggestions to teammates on fixes/improvements that they can make + - Created and assigned issues to teammates diff --git a/docs/team/timothylkm.md b/docs/team/timothylkm.md new file mode 100644 index 0000000000..420b69425f --- /dev/null +++ b/docs/team/timothylkm.md @@ -0,0 +1,52 @@ +# Timothy Lau Kah Ming's Project Portfolio Page + +## Project: SuperTracker + +SuperTracker is a desktop app for managing a supermarket's inventory and expenditures, +optimized for use via a Command Line Interface (CLI). + +Given below are my contributions to the project. + +- **Feature:** Rename Command + - What it does: Allows users to rename their item of choice. + - Justification: A supermarket manager can make mistakes when adding items + and the app should provide a convenient way to rectify them, instead of manually deleting the item from the inventory + and adding it back in with a new name. + - Highlights: Before creating a new rename command, the parser helps to validate the users inputs by checking if the + original item name exists in the inventory and if the inventory has an identical item with the new name. + + +- **Feature:** Find Command + - What it does: Allow users to quickly find an item of their choice. + - Justification: A supermarket should be able to quick search an item off of it's name. + - Highlights: A Find command would be able to return all items that contain the input word. + + +- **Feature:** Help Command + - What it does: Allows supermarket managers to refer to a quick help sheet if they happen to forget the parameters. + - Justification: The UG may be long and tedious to look up for parameters. With the use of the help command, it is + easier to identify parameters required. + - Highlights: Instead of throwing all possible inputs that exists in SuperTracker, the Help command first returns a + summarized list of functions SuperTracker has which the user can then find out more about a particular functions' + parameters. + + +- **Code contributed:** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=TimothyLKM&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + + +- **Documentation:** + - [User Guide](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html) + - Added documentation for Rename, Find and Help command + - Used PlantUML do make class and sequence diagrams for RenameCommand, FindCommand and HelpCommand. + + - [Developer Guide:](https://ay2324s2-cs2113-t13-4.github.io/tp/DeveloperGuide.html) + - Added documentation for Rename, Find and Help Commands + - Used PlantUML do make class and sequence diagrams for RenameCommand, FindCommand and HelpCommand. +

+ +- **Contributions to team-based tasks:** + - Teammate PRs reviewed: [#32](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/32), [#34](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/34), [#80](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/80) + - Contributed to JUnit tests for Find Command and Rename Command. + - Helped give improvements and suggestions to my teammates. Resolved bugs immediately. + - Created and assigned issues. Checked and merged pull requests in a timely manner. + \ No newline at end of file diff --git a/docs/team/vimal-face.jpg b/docs/team/vimal-face.jpg new file mode 100644 index 0000000000..765cc22cee Binary files /dev/null and b/docs/team/vimal-face.jpg differ diff --git a/docs/team/vimalapugazhan.md b/docs/team/vimalapugazhan.md new file mode 100644 index 0000000000..efde2d9843 --- /dev/null +++ b/docs/team/vimalapugazhan.md @@ -0,0 +1,56 @@ +# Vimalapugazhan Purushothaman - Project Portfolio Page + +## Project: SuperTracker +SuperTracker is a desktop app for managing a supermarket's inventory and expenditures, optimized for use via a Command Line Interface (CLI). + +Given below are my contributions to the project. + +- **Feature:** Delete Command + - What it does: Allows the user to delete an item from the Inventory List. + - Justification: Items in a supermarket may get sold out or expire and the manager would need to be able to remove the item from the inventory. + - Highlights: It checks whether there is an item in the inventory that matches the input. +

+ +- **Feature:** Revenue Command + - What it does: Allows supermarket managers to check their revenue over a period of time. + - Justification: This function is necessary to see what is the cash inflow of the supermarket. Moreover, this + function is necessary to calculate the profit of the supermarket + - Highlights: Able to give different revenue reports for 4 different time frames; Today, specified day, specified range and total. +

+ +- **Feature:** Profit Command + - What it does: Allows supermarket managers to check their profit over a period of time. + - Justification: Profit is a good indication of the performance of the supermarket. + As a supermarket manager, this function would be important in both keeping track at finances and gauging the performance of the business. + - Highlights: Provides reports for the different time frames, similar to revenue command. +

+ +- **Enhancement to existing features:** Expiry Date in New Command, List + - What it does: Allow manager to add perishable items added with an expiry date. + - Justification: Most items in a supermarket contains an expiry date and as a supermarket manager, tracking the expiry dates would be important for sorting stock and restocking. + - Highlights: The inventory can be sorted by expiry date in list and the information is also used in other commands such as report. +

+ +- **Enhancement to existing features:** Expiry Date in Update Command + - What it does: Allows manager to change the expiry dates of items. + - Justification: Expiry dates of items may need to be updated due to mistake in entry or if there was no expiry date initially added with the item. + - Highlights: Able to remove and add expiry dates in addition to just changing the dates. +

+ +- **Code contributed** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=vimalapugazhan&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=vimalapugazhan&tabRepo=AY2324S2-CS2113-T13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +

+ +- **Documentation** + - [User Guide](https://ay2324s2-cs2113-t13-4.github.io/tp/UserGuide.html) + - Added documentation for Delete, Revenue and Profit commands. + - Updated documentation for New, List and update commands to handle expiry dates. + - [Developer Guide:](https://ay2324s2-cs2113-t13-4.github.io/tp/DeveloperGuide.html) + - Added documentation for Delete, Revenue and Profit Commands + - Used PlantUML do make class and sequence diagrams for DeleteCommand, RevenueCommand and ProfitCommand. +

+ +- **Contributions to team-based tasks:** + - Teammate PR reviewed: [#254](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/254), [#188](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/188), [183](https://github.com/AY2324S2-CS2113-T13-4/tp/pull/183). + - Contributed to the JUnit tests, JavaDocs, User Guide and Developer Guide. + - Helped give improvements and suggestions to my teammates. Resolved bugs immediately. + - Created and assigned issues. Checked and merged pull requests in a timely manner. diff --git a/docs/uml-diagrams/AddCommand.puml b/docs/uml-diagrams/AddCommand.puml new file mode 100644 index 0000000000..53362114cf --- /dev/null +++ b/docs/uml-diagrams/AddCommand.puml @@ -0,0 +1,137 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class Item +class AddCommand +class Parser +class Ui +class ItemStorage + +Command <|. AddCommand +Item <--- AddCommand +Ui <.. AddCommand +Inventory <.. AddCommand +AddCommand <.. Parser +ItemStorage <.. AddCommand +Inventory <.. ItemStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class AddCommand { + #name:String + #quantity:int + #newItem:Item + +AddCommand() + #executeWithoutUi():void + +execute():void + +isQuit():boolean +} + +class Inventory { + {static}+get(name:String):Item + {static}+put(name:String, item:Item):void +} + +class Item { + +Item() + +getName():String + +getQuantity():int + +getPrice():double + +getExpiryDate():LocalDate +} + +class Ui { + {static}+addCommandSuccess():void +} + +class Parser { + {static}-parseAddCommand(input:String):AddCommand +} + +class ItemStorage { + {static}+saveData():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":AddCommand" as AddCommand #cbf7f4 +participant "ItemStorage" as ItemStorage <> #bcf7cf +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "oldItem:Item" as oldItem #ffa1a1 +participant ":Item" as Item #fbffb2 + +SuperTracker -> AddCommand : execute() +activate AddCommand #cbf7f4 + +group sd execute add command without Ui + +AddCommand -> AddCommand : executeWithoutUi() +activate AddCommand #cbf7f4 + +AddCommand -> Inventory : get(name:String) +activate Inventory #d5eac2 +Inventory -> AddCommand : oldItem:Item +deactivate Inventory + +AddCommand -> oldItem : getQuantity() +activate oldItem #ffa1a1 +oldItem --> AddCommand : :int +deactivate oldItem + +AddCommand -> oldItem : getName() +activate oldItem #ffa1a1 +oldItem --> AddCommand : :String +deactivate oldItem + +AddCommand -> oldItem : getPrice() +activate oldItem #ffa1a1 +oldItem --> AddCommand : :double +deactivate oldItem + +AddCommand -> oldItem : getExpiry() +activate oldItem #ffa1a1 +oldItem --> AddCommand : :LocalDate +deactivate oldItem + +AddCommand -> Item ** : new Item(name:String, quantity:int, price:double, expiryDate:LocalDate) +activate Item #fbffb2 +Item --> AddCommand : newItem:Item +deactivate Item + +AddCommand -> Inventory : put(name:String, item:Item) +activate Inventory #d5eac2 +Inventory --> AddCommand +deactivate Inventory + +AddCommand -> ItemStorage : saveData() +activate ItemStorage #bcf7cf +ItemStorage --> AddCommand +deactivate ItemStorage + +AddCommand --> AddCommand +deactivate AddCommand + +end + +AddCommand -> Ui : addCommandSuccess(newItem:Item, quantity:int) +activate Ui #e5c2ea +Ui --> AddCommand +deactivate Ui + +AddCommand --> SuperTracker +deactivate AddCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/AddCommandClass.png b/docs/uml-diagrams/AddCommandClass.png new file mode 100644 index 0000000000..fc4cea00c6 Binary files /dev/null and b/docs/uml-diagrams/AddCommandClass.png differ diff --git a/docs/uml-diagrams/AddCommandSequence.png b/docs/uml-diagrams/AddCommandSequence.png new file mode 100644 index 0000000000..bc5a56306c Binary files /dev/null and b/docs/uml-diagrams/AddCommandSequence.png differ diff --git a/docs/uml-diagrams/Architecture.puml b/docs/uml-diagrams/Architecture.puml new file mode 100644 index 0000000000..02e7b3ab87 --- /dev/null +++ b/docs/uml-diagrams/Architecture.puml @@ -0,0 +1,28 @@ +@startuml +title Architecture Diagram + +skinparam component { + BackgroundColor lightgrey +} + +[Main] +[Parser] +[Command] +[Storage] +[Items] +[Ui] +interface "user" + +user -[hidden] Command + +Main --> Parser : Parses user input +Main --> Command : Executes +Main --> Ui +Parser --> Command : Instantiates a command +Command --> Items : Modifies +Command --> Ui : Output message +Storage -> Items : Saves +Command --> Storage +user -[#red].> Main : Enter command inputs + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ArchitectureDiagram.png b/docs/uml-diagrams/ArchitectureDiagram.png new file mode 100644 index 0000000000..703f711911 Binary files /dev/null and b/docs/uml-diagrams/ArchitectureDiagram.png differ diff --git a/docs/uml-diagrams/BuyCommand.puml b/docs/uml-diagrams/BuyCommand.puml new file mode 100644 index 0000000000..86c4c5fe50 --- /dev/null +++ b/docs/uml-diagrams/BuyCommand.puml @@ -0,0 +1,100 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class AddCommand +class BuyCommand +class Parser +class Transaction +class TransactionList +class TransactionStorage +class Ui + +AddCommand <|- BuyCommand +Transaction <.. BuyCommand +Ui <.. BuyCommand +TransactionList <.. BuyCommand +BuyCommand <.. Parser +TransactionStorage <.. BuyCommand +TransactionList <.. TransactionStorage + +class AddCommand { + #name:String + #quantity:int + #newItem:Item + +AddCommand() + #executeWithoutUi():void + +execute():void + +isQuit():boolean +} + +class BuyCommand { + -price:double + -currentDate:LocalDate + +BuyCommand() + +execute():void +} + +class Transaction { + +Transaction() +} + +class TransactionList { + {static}+add(transaction:Transaction):void +} + +class Ui { + {static}+buyCommandSuccess():void +} + +class Parser { + {static}-parseBuyCommand(input:String):BuyCommand +} + +class TransactionStorage { + {static}+saveTransaction(transaction:Transaction):void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":BuyCommand" as BuyCommand #cbf7f4 +participant "TransactionStorage" as TransactionStorage <> #bcf7cf +participant "TransactionList" as TransactionList <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant ":Transaction" as Transaction #fbffb2 + +SuperTracker -> BuyCommand : execute() +activate BuyCommand #cbf7f4 + +ref over BuyCommand : execute add command without Ui + +BuyCommand -> Transaction ** : new Transaction(parameters omitted for brevity) +activate Transaction #fbffb2 +Transaction --> BuyCommand : transaction:Transaction +deactivate Transaction + +BuyCommand -> TransactionList : add(transaction:Transaction) +activate TransactionList #d5eac2 +TransactionList --> BuyCommand +deactivate TransactionList + +BuyCommand -> Ui : buyCommandSuccess(newItem:Item, transaction:Transaction) +activate Ui #e5c2ea +Ui --> BuyCommand +deactivate Ui + +BuyCommand -> TransactionStorage : saveTransaction(transaction:Transaction) +activate TransactionStorage #bcf7cf +TransactionStorage --> BuyCommand +deactivate TransactionStorage + +BuyCommand --> SuperTracker +deactivate BuyCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/BuyCommandClass.png b/docs/uml-diagrams/BuyCommandClass.png new file mode 100644 index 0000000000..db66f30a35 Binary files /dev/null and b/docs/uml-diagrams/BuyCommandClass.png differ diff --git a/docs/uml-diagrams/BuyCommandSequence.png b/docs/uml-diagrams/BuyCommandSequence.png new file mode 100644 index 0000000000..5cdcc65d1a Binary files /dev/null and b/docs/uml-diagrams/BuyCommandSequence.png differ diff --git a/docs/uml-diagrams/ClearCommand.puml b/docs/uml-diagrams/ClearCommand.puml new file mode 100644 index 0000000000..9832945429 --- /dev/null +++ b/docs/uml-diagrams/ClearCommand.puml @@ -0,0 +1,101 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class Command +class ClearCommand +class Parser +class TransactionList +class TransactionStorage +class Ui + +Command <|. ClearCommand +Ui <.. ClearCommand +TransactionList <.. ClearCommand +ClearCommand <.. Parser +TransactionStorage <.. ClearCommand +TransactionList <.. TransactionStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class ClearCommand { + -beforeDate:LocalDate + +ClearCommand(beforeDate:LocalDate) + -clearOldTransactions():int + +execute():void +} + +class TransactionList { + {static}+iterator():Iterator +} + +class Ui { + {static}+clearCommandConfirmation():void + {static}+clearCommandCancelled():void + {static}+clearCommandSuccess():void +} + +class Parser { + {static}-parseClearCommand(input:String):ClearCommand +} + +class TransactionStorage { + {static}+resaveCurrentTransactions():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +actor User as User +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":ClearCommand" as ClearCommand #cbf7f4 +participant "TransactionStorage" as TransactionStorage <> #bcf7cf +participant "Ui" as Ui <> #e5c2ea + +SuperTracker -> ClearCommand : execute() +activate ClearCommand #cbf7f4 + +ClearCommand -> Ui : clearCommandConfirmation(beforeDate:LocalDate) +activate Ui #e5c2ea +Ui --> ClearCommand +deactivate Ui + +User -> ClearCommand : input:String + +alt input != "y" && input != "Y" + +ClearCommand -> Ui : clearCommandCancelled() +activate Ui #e5c2ea +Ui --> ClearCommand +deactivate Ui + +else input == "y" || input == "Y" + +ClearCommand -> ClearCommand : clearOldTransactions() +activate ClearCommand #cbf7f4 +ClearCommand --> ClearCommand : transactionsCleared:int +deactivate ClearCommand + +ClearCommand -> Ui : clearCommandSuccess(transactionsCleared:int, beforeDate:LocalDate) +activate Ui #e5c2ea +Ui --> ClearCommand +deactivate Ui + +ClearCommand -> TransactionStorage : resaveCurrentTransactions() +activate TransactionStorage #bcf7cf +TransactionStorage --> ClearCommand +deactivate TransactionStorage + +end + +ClearCommand --> SuperTracker +deactivate ClearCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ClearCommandClass.png b/docs/uml-diagrams/ClearCommandClass.png new file mode 100644 index 0000000000..160228c4de Binary files /dev/null and b/docs/uml-diagrams/ClearCommandClass.png differ diff --git a/docs/uml-diagrams/ClearCommandSequence.png b/docs/uml-diagrams/ClearCommandSequence.png new file mode 100644 index 0000000000..ee6fa26c81 Binary files /dev/null and b/docs/uml-diagrams/ClearCommandSequence.png differ diff --git a/docs/uml-diagrams/DeleteCommand.puml b/docs/uml-diagrams/DeleteCommand.puml new file mode 100644 index 0000000000..c3052d411b --- /dev/null +++ b/docs/uml-diagrams/DeleteCommand.puml @@ -0,0 +1,84 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class ItemStorage +class Ui +class Parser +class DeleteCommand + +Command <|. DeleteCommand +Inventory <.. DeleteCommand +ItemStorage <.. DeleteCommand +Inventory <.. ItemStorage +DeleteCommand <.. Parser +Ui <.. DeleteCommand + +interface Command <> { + + execute():void + + isQuit():boolean +} + +class DeleteCommand { + - name:String + + DeleteCommand(name:String) + + execute():void + + isQuit():boolean +} + +class Inventory { + {static} + contains(name:String):boolean + {static} + delete(name:String):void +} + +class ItemStorage { + {static} + saveData():void +} + +class Ui { + {static} + deleteCommandSuccess(name:String):void + {static} + printError(message:String):void +} + +class Parser { + {static}-parseDeleteCommand(input:String):DeleteCommand +} +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":DeleteCommand" as DeleteCommand #cbf7f4 +participant "Inventory" as Inventory <> #d5eac2 +participant "ItemStorage" as ItemStorage <> #ffecb3 +participant "Ui" as Ui <> #e5c2ea + +SuperTracker -> DeleteCommand : execute() +activate DeleteCommand #cbf7f4 + +DeleteCommand -> Inventory : contains(name:String) +activate Inventory #d5eac2 +Inventory --> DeleteCommand : +deactivate Inventory + +DeleteCommand -> Inventory : delete(name:String) +activate Inventory +Inventory --> DeleteCommand +deactivate Inventory + +DeleteCommand -> Ui : deleteCommandSuccess(name:String) +activate Ui #e5c2ea +Ui --> DeleteCommand +deactivate Ui + +DeleteCommand -> ItemStorage : saveData() +activate ItemStorage #ffecb3 +ItemStorage --> DeleteCommand +deactivate ItemStorage +DeleteCommand --> SuperTracker +deactivate DeleteCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/DeleteCommandClass.png b/docs/uml-diagrams/DeleteCommandClass.png new file mode 100644 index 0000000000..cfaeef217d Binary files /dev/null and b/docs/uml-diagrams/DeleteCommandClass.png differ diff --git a/docs/uml-diagrams/DeleteCommandSequence.png b/docs/uml-diagrams/DeleteCommandSequence.png new file mode 100644 index 0000000000..5dd9331121 Binary files /dev/null and b/docs/uml-diagrams/DeleteCommandSequence.png differ diff --git a/docs/uml-diagrams/ExpenditureCommand.puml b/docs/uml-diagrams/ExpenditureCommand.puml new file mode 100644 index 0000000000..874c2de506 --- /dev/null +++ b/docs/uml-diagrams/ExpenditureCommand.puml @@ -0,0 +1,115 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class TransactionList +class ExpenditureCommand +class Parser +class Item + +ExpenditureCommand <.. Parser +Command <|. ExpenditureCommand +Ui <.. ExpenditureCommand +TransactionList <.. ExpenditureCommand +Item <.. ExpenditureCommand + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class ExpenditureCommand { + -startDate:LocalDate + -endDate:LocalDate + -task:String + -expenditure:BigDecimal + +ExpenditureCommand() + +execute():void + +isQuit():boolean +} + +class Ui { + {static}+printRevenueExpenditure():void +} + +class Parser { + {static}-parseExpenditureCommand(input:String):ExpenditureCommand +} + +class TransactionList { + {static}+calculateDay():BigDecimal + {static}+calculateTotal():BigDecimal + {static}+calculateRange():BigDecimal + {static}+getFilteredTransactionList():ArrayList +} + +class Item { + {static}+sortByDate():Comparator +} + +note "Some parameters and methods \nomitted for brevity" as n1 +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":ExpenditureCommand" as ExpenditureCommand #cbf7f4 +participant "TransactionList" as TransactionList <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "Collections" as Collections <> #ffcdd6 + +SuperTracker -> ExpenditureCommand : execute() +activate ExpenditureCommand #cbf7f4 +alt task is "today" + ExpenditureCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> ExpenditureCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "total" + ExpenditureCommand -> TransactionList : calculateTotal() + activate TransactionList #d5eac2 + TransactionList --> ExpenditureCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "day" + ExpenditureCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> ExpenditureCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "range" + ExpenditureCommand -> TransactionList : calculateRange() + activate TransactionList #d5eac2 + TransactionList --> ExpenditureCommand : expenditure:BigDecimal + deactivate TransactionList +end + +note right : some parameters and methods \nomitted for brevity + +ExpenditureCommand -> TransactionList : getFilteredTransactionList() +activate TransactionList #d5eac2 +TransactionList --> ExpenditureCommand : filteredList:ArrayList +deactivate TransactionList + +ExpenditureCommand -> ExpenditureCommand : sort() +note right : Transactions sorted by date + +ExpenditureCommand -> Collections : reverse(filteredList:ArrayList) +activate Collections #ffcdd6 +Collections --> ExpenditureCommand +deactivate Collections + +ExpenditureCommand -> Ui : printRevenueExpenditure() +activate Ui #e5c2ea +Ui --> ExpenditureCommand +deactivate Ui + +ExpenditureCommand --> SuperTracker +deactivate ExpenditureCommand + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ExpenditureCommandClass.png b/docs/uml-diagrams/ExpenditureCommandClass.png new file mode 100644 index 0000000000..54b2e3a9fa Binary files /dev/null and b/docs/uml-diagrams/ExpenditureCommandClass.png differ diff --git a/docs/uml-diagrams/ExpenditureCommandSequence.png b/docs/uml-diagrams/ExpenditureCommandSequence.png new file mode 100644 index 0000000000..daa31f5d80 Binary files /dev/null and b/docs/uml-diagrams/ExpenditureCommandSequence.png differ diff --git a/docs/uml-diagrams/FileManager.puml b/docs/uml-diagrams/FileManager.puml new file mode 100644 index 0000000000..5239cbb6b1 --- /dev/null +++ b/docs/uml-diagrams/FileManager.puml @@ -0,0 +1,52 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class SuperTracker +class FileManager +class Inventory +class Item +class Command +class TransactionStorage +class ItemStorage +class Transaction +class TransactionList + +FileManager <|-- ItemStorage +FileManager <|-- TransactionStorage +Inventory <.. ItemStorage +Item <. ItemStorage +Item "*" <-* Inventory +Item <|-- Transaction +Transaction "*" <-* TransactionList +FileManager <.. Command + +class FileManager { + {static}#DATA_PATH:String + {static}#checkDataDirectory():void + {static}#getNameQtyPriceStrings(item:Item):String[] +} + +class SuperTracker { + {static}-run():void +} + +class Command { + +execute():void +} + +class Inventory { + {static}+getItems():List + {static}+put(name:String, item:Item):void +} + +class Item { + -name:String + -quantity:int + -price:double + -expiryDate:LocalDate + +getName():String +} + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/FileManagerClass.png b/docs/uml-diagrams/FileManagerClass.png new file mode 100644 index 0000000000..0c1a773adf Binary files /dev/null and b/docs/uml-diagrams/FileManagerClass.png differ diff --git a/docs/uml-diagrams/FindCommand.puml b/docs/uml-diagrams/FindCommand.puml new file mode 100644 index 0000000000..cfd1efebfa --- /dev/null +++ b/docs/uml-diagrams/FindCommand.puml @@ -0,0 +1,77 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class FindCommand +class Parser +class Ui + +Command <|. FindCommand +Inventory <.. FindCommand +Ui <.. FindCommand +FindCommand <.. Parser + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class FindCommand { + -name:String + +FindCommand(name:String) + +execute():void + +isQuit():boolean +} + +class Inventory { + {static}+getItems():ArrayList +} + +class Ui { + {static}+findItem(item:Item, index:int):void + {static}+noItemFound(name:String):void +} + +class Parser { + {static}-parseFindCommand(input:String):FindCommand +} +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":FindCommand" as FindCommand #cbf7f4 +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea + +SuperTracker -> FindCommand : execute() +activate FindCommand #cbf7f4 + +FindCommand -> Inventory : getItems() +activate Inventory #d5eac2 +Inventory --> FindCommand : items +deactivate Inventory + +loop until every item in items is checked + opt item contains the word + FindCommand -> Ui : foundItem(item:String, index:int) + activate Ui #e5c2ea + Ui --> FindCommand + deactivate Ui + end +end + +opt word not found in items +FindCommand -> Ui : noItemFound(name:String) +activate Ui #e5c2ea +Ui --> FindCommand +deactivate Ui +end + +FindCommand --> SuperTracker +deactivate FindCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/FindCommandClass.png b/docs/uml-diagrams/FindCommandClass.png new file mode 100644 index 0000000000..6109589925 Binary files /dev/null and b/docs/uml-diagrams/FindCommandClass.png differ diff --git a/docs/uml-diagrams/FindCommandSequence.png b/docs/uml-diagrams/FindCommandSequence.png new file mode 100644 index 0000000000..b90a2c4886 Binary files /dev/null and b/docs/uml-diagrams/FindCommandSequence.png differ diff --git a/docs/uml-diagrams/HelpCommand.puml b/docs/uml-diagrams/HelpCommand.puml new file mode 100644 index 0000000000..c0db2d423c --- /dev/null +++ b/docs/uml-diagrams/HelpCommand.puml @@ -0,0 +1,96 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class HelpCommandUi +class HelpCommand +class Parser +class Ui + +Ui <|- HelpCommandUi +HelpCommand <.. Parser +Command <|. HelpCommand +HelpCommandUi <.. HelpCommand + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class HelpCommand { + {static}-getHelpCommandReply():String + +execute():void + +isQuit():boolean +} + +class Ui { + {static}+printLine():void +} + +class HelpCommandUi { + {static}+helpCommandSuccess():void + {static}+printNewCommandParams():void + {static}+printDeleteCommandParams():void + {static}+printChangeCommandParams():void + {static}+printUpdateCommandParams():void + {static}+helpClosingMessage():void +} + +class Parser { + {static}-parseCommand():HelpCommand +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +actor User as User +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":HelpCommand" as HelpCommand #cbf7f4 +participant "HelpCommandUi" as HelpCommandUi <> #e5c2ea + +SuperTracker -> HelpCommand : execute() +activate HelpCommand #cbf7f4 + +HelpCommand -> HelpCommandUi : helpCommandSuccess() +activate HelpCommandUi #e5c2ea +HelpCommandUi --> HelpCommand +deactivate HelpCommandUi + +User -> HelpCommand : input:String + +HelpCommand -> HelpCommand : getHelpCommandReply(input:String) +activate HelpCommand #cbf7f4 +HelpCommand --> HelpCommand : helpCommandWord:String +deactivate HelpCommand + +HelpCommand -> HelpCommandUi : printLine() +activate HelpCommandUi #e5c2ea +HelpCommandUi --> HelpCommand +deactivate HelpCommandUi + +alt Valid command word +HelpCommand -> HelpCommandUi : printCommandParams() +activate HelpCommandUi #e5c2ea +HelpCommandUi --> HelpCommand +deactivate HelpCommandUi +else Invalid command word +HelpCommand -> HelpCommandUi : printInvalidHelpMessage() +activate HelpCommandUi #e5c2ea +HelpCommandUi --> HelpCommand +deactivate HelpCommandUi +end + +HelpCommand -> HelpCommandUi : helpClosingMessage() +activate HelpCommandUi #e5c2ea +HelpCommandUi --> HelpCommand +deactivate HelpCommandUi + +HelpCommand --> SuperTracker +deactivate HelpCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/HelpCommandClass.png b/docs/uml-diagrams/HelpCommandClass.png new file mode 100644 index 0000000000..1027abffa7 Binary files /dev/null and b/docs/uml-diagrams/HelpCommandClass.png differ diff --git a/docs/uml-diagrams/HelpCommandSequence.png b/docs/uml-diagrams/HelpCommandSequence.png new file mode 100644 index 0000000000..6be5c14049 Binary files /dev/null and b/docs/uml-diagrams/HelpCommandSequence.png differ diff --git a/docs/uml-diagrams/ItemStorage.puml b/docs/uml-diagrams/ItemStorage.puml new file mode 100644 index 0000000000..e7476905ce --- /dev/null +++ b/docs/uml-diagrams/ItemStorage.puml @@ -0,0 +1,60 @@ +@startuml + +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class SuperTracker +class FileManager +class Inventory +class Item +class CommandClass +class ItemStorage + +FileManager <|--- ItemStorage +Inventory <.. ItemStorage +Item <.. ItemStorage +Item "*" <--* Inventory +ItemStorage <.. CommandClass +ItemStorage <. SuperTracker + +class FileManager { + {static}#DATA_PATH:String + {static}#checkDataDirectory():void + {static}#getNameQtyPriceStrings(item:Item):String[] +} + +class ItemStorage { + {static}+saveData():void + {static}+loadData():void + {static}-getItemData(item:Item):String + {static}-parseItemData(data:String):Item +} + +class SuperTracker { + {static}-run():void +} + +class CommandClass { + +execute():void +} + +class Inventory { + {static}+getItems():List + {static}+put(name:String, item:Item):void +} + +class Item { + -name:String + -quantity:int + -price:double + -expiryDate:LocalDate + +getName():String +} + +note right of CommandClass + CommandClass refers to the classes + that implement the Command interface +endnote + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ItemStorageClass.png b/docs/uml-diagrams/ItemStorageClass.png new file mode 100644 index 0000000000..b12aef45f0 Binary files /dev/null and b/docs/uml-diagrams/ItemStorageClass.png differ diff --git a/docs/uml-diagrams/ListCommand.puml b/docs/uml-diagrams/ListCommand.puml new file mode 100644 index 0000000000..a039e187a9 --- /dev/null +++ b/docs/uml-diagrams/ListCommand.puml @@ -0,0 +1,128 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class Item +class ListCommand +class Parser +class Ui + +Command <|. ListCommand +Item <... ListCommand +Ui <.. ListCommand +Inventory <.. ListCommand +ListCommand <.. Parser + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class ListCommand { + -firstParam:String + -secondParam:String + -thirdParam:String + -firstSortParam:String + -secondSortParam:String + -thirdSortParam:String + -isReverse:boolean + -sortBy(sortParam:String, items:List) + +ListCommand() + +execute():void + +isQuit():boolean +} + +class Item { + {static}+sortByName():Comparator + {static}+sortByQuantity():Comparator + {static}+sortByPrice():Comparator + {static}+sortByDate():Comparator +} + +class Inventory { + {static}+getItems():ArrayList +} + +class Ui { + {static}+listIntro(size:int):void + {static}+listItem():void +} + +class Parser { + {static}-parseListCommand(input:String):ListCommand +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":ListCommand" as ListCommand #cbf7f4 +participant "Inventory" as Inventory <> #d5eac2 +participant "Collections" as Collections <> #ffcdd6 +participant "Ui" as Ui <> #e5c2ea + +SuperTracker -> ListCommand : execute() +activate ListCommand #cbf7f4 + +ListCommand -> Inventory : getItems() +activate Inventory #d5eac2 + +Inventory --> ListCommand : items:ArrayList +deactivate Inventory + +ListCommand -> Ui : listIntro(size:int) +activate Ui #e5c2ea +Ui --> ListCommand +deactivate Ui + +ListCommand -> ListCommand : sortBy(ALPHABET:String, items:ArrayList) +activate ListCommand #cbf7f4 +ListCommand --> ListCommand +note right : items sorted according to alphabet +deactivate ListCommand + +ListCommand -> ListCommand : sortBy(thirdSortParam:String, items:ArrayList) +activate ListCommand #cbf7f4 +ListCommand --> ListCommand +note right : items sorted according to thirdSortParam +deactivate ListCommand + +ListCommand -> ListCommand : sortBy(secondSortParam:String, items:ArrayList) +activate ListCommand #cbf7f4 +ListCommand --> ListCommand +note right : items sorted according to secondSortParam +deactivate ListCommand + +ListCommand -> ListCommand : sortBy(firstSortParam:String, items:ArrayList) +activate ListCommand #cbf7f4 +ListCommand --> ListCommand +note right : items sorted according to firstSortParam +deactivate ListCommand + +opt isReverse + +ListCommand -> Collections : reverse(items:ArrayList) +activate Collections #ffcdd6 +Collections --> ListCommand +deactivate Collections + +end + +loop items + +ListCommand -> Ui : listItem(item:Item, index:int, firstParam:String, secondParam:String, thirdParam:String) +activate Ui #e5c2ea +Ui --> ListCommand +deactivate Ui + +end + +ListCommand --> SuperTracker +deactivate ListCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ListCommandClass.png b/docs/uml-diagrams/ListCommandClass.png new file mode 100644 index 0000000000..2da0e39602 Binary files /dev/null and b/docs/uml-diagrams/ListCommandClass.png differ diff --git a/docs/uml-diagrams/ListCommandSequence.png b/docs/uml-diagrams/ListCommandSequence.png new file mode 100644 index 0000000000..a2001c4376 Binary files /dev/null and b/docs/uml-diagrams/ListCommandSequence.png differ diff --git a/docs/uml-diagrams/NewCommand.puml b/docs/uml-diagrams/NewCommand.puml new file mode 100644 index 0000000000..10adbd80ac --- /dev/null +++ b/docs/uml-diagrams/NewCommand.puml @@ -0,0 +1,96 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class Item +class NewCommand +class Parser +class Ui +class ItemStorage + +Command <|. NewCommand +Item <... NewCommand +Ui <.. NewCommand +Inventory <.. NewCommand +NewCommand <.. Parser +ItemStorage <.. NewCommand +Inventory <.. ItemStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class NewCommand { + -name:String + -quantity:int + -price:double + -expiryDate:LocalDate + +NewCommand() + +execute():void + +isQuit():boolean +} + +class Item { + +Item() +} + +class Inventory { + {static}+put(name:String, item:Item):void +} + +class Ui { + {static}+newCommandSuccess(item:Item):void +} + +class Parser { + {static}-parseNewCommand(input:String):NewCommand +} + +class ItemStorage { + {static}+saveData():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":NewCommand" as NewCommand #cbf7f4 +participant "ItemStorage" as ItemStorage <> #bcf7cf +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant ":Item" as Item #fbffb2 + +SuperTracker -> NewCommand : execute() +activate NewCommand #cbf7f4 + +NewCommand -> Item ** : new Item(name:String, quantity:int, price:double, expiryDate:LocalDate) +activate Item #fbffb2 +Item --> NewCommand : :Item +deactivate Item + +NewCommand -> Inventory : put(name:String, item:Item) +activate Inventory #d5eac2 +Inventory --> NewCommand +deactivate Inventory + +NewCommand -> Ui : newCommandSuccess(item:Item) +activate Ui #e5c2ea +Ui --> NewCommand +deactivate Ui + +NewCommand -> ItemStorage : saveData() +activate ItemStorage #bcf7cf +ItemStorage --> NewCommand +deactivate ItemStorage + +NewCommand --> SuperTracker +deactivate NewCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/NewCommandClass.png b/docs/uml-diagrams/NewCommandClass.png new file mode 100644 index 0000000000..ca00d47c94 Binary files /dev/null and b/docs/uml-diagrams/NewCommandClass.png differ diff --git a/docs/uml-diagrams/NewCommandSequence.png b/docs/uml-diagrams/NewCommandSequence.png new file mode 100644 index 0000000000..602f5449c0 Binary files /dev/null and b/docs/uml-diagrams/NewCommandSequence.png differ diff --git a/docs/uml-diagrams/Parser.puml b/docs/uml-diagrams/Parser.puml new file mode 100644 index 0000000000..03eac73d42 --- /dev/null +++ b/docs/uml-diagrams/Parser.puml @@ -0,0 +1,57 @@ + +@startuml +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class SuperTracker +class Parser +class TrackerException +interface Command + +Parser <... SuperTracker +Command <.. SuperTracker +TrackerException <.. SuperTracker +TrackerException <.. Parser + +interface Command <> { + execute():void + isQuit():boolean +} + +class TrackerException { + #errorMessage:String + +getErrorMessage():String + +TrackerException(errorMessage:String) +} + +class Parser { + {static}+parseCommand(input:String):Command +} + +@enduml + +@startuml +hide footbox +actor User as User +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant "Parser" as Parser <> #cbf7f4 + +User -> SuperTracker : userInput +SuperTracker -> Parser : parseCommand(userInput) +activate Parser #cbf7f4 + +alt Valid command word + Parser -> Parser : getPatternMatcher(regex, userInput, paramFlags) + activate Parser #a9dff5 + Parser --> Parser : commandToExecute + deactivate Parser + Parser --> SuperTracker : commandToExecute +else Invalid command word + Parser --> SuperTracker : InvalidCommand + deactivate Parser +end + +SuperTracker --> User : Output + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ParserClass.png b/docs/uml-diagrams/ParserClass.png new file mode 100644 index 0000000000..ebd99ad99e Binary files /dev/null and b/docs/uml-diagrams/ParserClass.png differ diff --git a/docs/uml-diagrams/ParserSequence.png b/docs/uml-diagrams/ParserSequence.png new file mode 100644 index 0000000000..5112a8dd8e Binary files /dev/null and b/docs/uml-diagrams/ParserSequence.png differ diff --git a/docs/uml-diagrams/ProfitCommand.puml b/docs/uml-diagrams/ProfitCommand.puml new file mode 100644 index 0000000000..bca40452cb --- /dev/null +++ b/docs/uml-diagrams/ProfitCommand.puml @@ -0,0 +1,110 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class TransactionList +class ProfitCommand +class Parser + +ProfitCommand <.. Parser +Command <|. ProfitCommand +Ui <.. ProfitCommand +TransactionList <.. ProfitCommand + + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class ProfitCommand { + -startDate:LocalDate + -endDate:LocalDate + -task:String + +ProfitCommand() + +execute():void + +isQuit():boolean +} + +class Ui { + {static}+printProfit():void +} + +class Parser { + {static}-parseProfitCommand(input:String):ProfitCommand +} + +class TransactionList { + {static}+calculateDay():BigDecimal + {static}+calculateTotal():BigDecimal + {static}+calculateRange():BigDecimal +} + +note "Some parameters and methods \nomitted for brevity" as n1 +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":ProfitCommand" as ProfitCommand #cbf7f4 +participant "TransactionList" as TransactionList <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea + +SuperTracker -> ProfitCommand : execute() +activate ProfitCommand #cbf7f4 +alt task is "today" + ProfitCommand -> TransactionList : calculateToday() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : revenue:BigDecimal + deactivate TransactionList + ProfitCommand -> TransactionList : calculateToday() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "total" + ProfitCommand -> TransactionList : calculateTotal() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : revenue:BigDecimal + deactivate TransactionList + ProfitCommand -> TransactionList : calculateTotal() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "day" + ProfitCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : revenue:BigDecimal + deactivate TransactionList + ProfitCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : expenditure:BigDecimal + deactivate TransactionList + +else task is "range" + ProfitCommand -> TransactionList : calculateRange() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : revenue:BigDecimal + deactivate TransactionList + ProfitCommand -> TransactionList : calculateRange() + activate TransactionList #d5eac2 + TransactionList --> ProfitCommand : expenditure:BigDecimal + deactivate TransactionList +end + +note right : some parameters and methods \nomitted for brevity + +ProfitCommand -> Ui : printProfit() +activate Ui #e5c2ea +Ui --> ProfitCommand +deactivate Ui + +ProfitCommand --> SuperTracker +deactivate ProfitCommand + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ProfitCommandClass.png b/docs/uml-diagrams/ProfitCommandClass.png new file mode 100644 index 0000000000..53d40b9fe7 Binary files /dev/null and b/docs/uml-diagrams/ProfitCommandClass.png differ diff --git a/docs/uml-diagrams/ProfitCommandSequence.png b/docs/uml-diagrams/ProfitCommandSequence.png new file mode 100644 index 0000000000..3b3b2b2246 Binary files /dev/null and b/docs/uml-diagrams/ProfitCommandSequence.png differ diff --git a/docs/uml-diagrams/RemoveCommand.puml b/docs/uml-diagrams/RemoveCommand.puml new file mode 100644 index 0000000000..0871535df3 --- /dev/null +++ b/docs/uml-diagrams/RemoveCommand.puml @@ -0,0 +1,138 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +abstract class Command +class Inventory +class Item +class RemoveCommand +class Parser +class Ui +class ItemStorage + +Command <|. RemoveCommand +Item <--- RemoveCommand +Ui <.. RemoveCommand +Inventory <.. RemoveCommand +RemoveCommand <.. Parser +ItemStorage <.. RemoveCommand +Inventory <.. ItemStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class RemoveCommand { + #name:String + #quantity:int + #quantityRemoved:int + #newItem:Item + +RemoveCommand() + #executeWithoutUi():void + +execute():void + +isQuit():boolean +} + +class Inventory { + {static}+get(name:String):Item + {static}+put(name:String, item:Item):void +} + +class Item { + +Item() + +getName():String + +getQuantity():int + +getPrice():double + +getExpiryDate():LocalDate +} + +class Ui { + {static}+removeCommandSuccess():void +} + +class Parser { + {static}-parseRemoveCommand(input:String):RemoveCommand +} + +class ItemStorage { + {static}+saveData():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":RemoveCommand" as RemoveCommand #cbf7f4 +participant "ItemStorage" as ItemStorage <> #bcf7cf +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "oldItem:Item" as oldItem #ffa1a1 +participant ":Item" as Item #fbffb2 + +SuperTracker -> RemoveCommand : execute() +activate RemoveCommand #cbf7f4 + +group sd execute remove command without Ui + +RemoveCommand -> RemoveCommand : executeWithoutUi() +activate RemoveCommand #cbf7f4 + +RemoveCommand -> Inventory : get(name:String) +activate Inventory #d5eac2 +Inventory -> RemoveCommand : oldItem:Item +deactivate Inventory + +RemoveCommand -> oldItem : getQuantity() +activate oldItem #ffa1a1 +oldItem --> RemoveCommand : :int +deactivate oldItem + +RemoveCommand -> oldItem : getName() +activate oldItem #ffa1a1 +oldItem --> RemoveCommand : :String +deactivate oldItem + +RemoveCommand -> oldItem : getPrice() +activate oldItem #ffa1a1 +oldItem --> RemoveCommand : :double +deactivate oldItem + +RemoveCommand -> oldItem : getExpiry() +activate oldItem #ffa1a1 +oldItem --> RemoveCommand : :LocalDate +deactivate oldItem + +RemoveCommand -> Item ** : new Item(name:String, quantity:int, price:double, expiryDate:LocalDate) +activate Item #fbffb2 +Item --> RemoveCommand : :Item +deactivate Item + +RemoveCommand -> Inventory : put(name:String, item:Item) +activate Inventory #d5eac2 +Inventory --> RemoveCommand +deactivate Inventory + +RemoveCommand -> ItemStorage : saveData() +activate ItemStorage #bcf7cf +ItemStorage --> RemoveCommand +deactivate ItemStorage + +RemoveCommand --> RemoveCommand +deactivate RemoveCommand + +end + +RemoveCommand -> Ui : removeCommandSuccess(item:Item) +activate Ui #e5c2ea +Ui --> RemoveCommand +deactivate Ui + +RemoveCommand --> SuperTracker +deactivate RemoveCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/RemoveCommandClass.png b/docs/uml-diagrams/RemoveCommandClass.png new file mode 100644 index 0000000000..40fc3e6a46 Binary files /dev/null and b/docs/uml-diagrams/RemoveCommandClass.png differ diff --git a/docs/uml-diagrams/RemoveCommandSequence.png b/docs/uml-diagrams/RemoveCommandSequence.png new file mode 100644 index 0000000000..750c2c24ac Binary files /dev/null and b/docs/uml-diagrams/RemoveCommandSequence.png differ diff --git a/docs/uml-diagrams/RenameCommand.puml b/docs/uml-diagrams/RenameCommand.puml new file mode 100644 index 0000000000..9ba7a9543f --- /dev/null +++ b/docs/uml-diagrams/RenameCommand.puml @@ -0,0 +1,131 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class Inventory +class Item +class RenameCommand +class Parser +class ItemStorage + +RenameCommand <.. Parser +Command <|. RenameCommand +Ui <.. RenameCommand +Inventory <.. RenameCommand +Item <.. RenameCommand +ItemStorage <.. RenameCommand +Inventory <.. ItemStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class RenameCommand { + -name:String + -newName:String + +RenameCommand() + +execute():void + +isQuit():boolean +} + +class Item { + +Item() + +getName():String + +getQuantity():int + +getPrice():double + +getExpiryDate():LocalDate +} + +class Ui { + {static}+renameCommandSuccess():void +} + +class Parser { + {static}-ParseRenameCommand(input:String):RenameCommand +} + +class Inventory { + {static}+get(name:String):Item + {static}+put(name:String, item:Item):void + {static}+delete(name:String):void +} + +class ItemStorage { + {static}+saveData():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":RenameCommand" as RenameCommand #cbf7f4 +participant "ItemStorage" as ItemStorage <> #bcf7cf +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "oldItem:Item" as oldItem #ffa1a1 +participant ":Item" as Item #fbffb2 + +SuperTracker -> RenameCommand : execute() +activate RenameCommand #cbf7f4 + +RenameCommand -> Inventory : get(name:String) +activate Inventory #d5eac2 +Inventory --> RenameCommand : oldItem:Item +deactivate Inventory + +RenameCommand -> oldItem: getName() +activate oldItem #ffa1a1 +oldItem --> RenameCommand: oldName:String +deactivate oldItem + +RenameCommand -> oldItem: getQuantity() +activate oldItem #ffa1a1 +oldItem --> RenameCommand: quantity:int +deactivate oldItem + +RenameCommand -> oldItem: getPrice() +activate oldItem #ffa1a1 +oldItem --> RenameCommand: price:double +deactivate oldItem + +RenameCommand -> oldItem: getExpiryDate() +activate oldItem #ffa1a1 +oldItem --> RenameCommand: expiryDate:LocalDate +deactivate oldItem + +RenameCommand -> Item ** : new Item(name:String, quantity:int, price:double, expiryDate:LocalDate) +activate Item #fbffb2 +Item --> RenameCommand : newItem:Item +deactivate Item + +RenameCommand -> Inventory : delete(name:String) +activate Inventory #d5eac2 +Inventory --> RenameCommand +deactivate Inventory + +RenameCommand -> Inventory : put(newName:String, newItem:Item) +activate Inventory #d5eac2 +Inventory --> RenameCommand +deactivate Inventory + +RenameCommand -> Ui : renameCommandSuccess(newItem:Item, oldName:String) +activate Ui #e5c2ea +Ui --> RenameCommand +deactivate Ui + +RenameCommand -> ItemStorage : saveData() +activate ItemStorage #bcf7cf +ItemStorage --> RenameCommand +deactivate ItemStorage + +RenameCommand --> SuperTracker +deactivate RenameCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/RenameCommandClass.png b/docs/uml-diagrams/RenameCommandClass.png new file mode 100644 index 0000000000..b1202f4778 Binary files /dev/null and b/docs/uml-diagrams/RenameCommandClass.png differ diff --git a/docs/uml-diagrams/RenameCommandSequence.png b/docs/uml-diagrams/RenameCommandSequence.png new file mode 100644 index 0000000000..9896c227d8 Binary files /dev/null and b/docs/uml-diagrams/RenameCommandSequence.png differ diff --git a/docs/uml-diagrams/ReportCommand.puml b/docs/uml-diagrams/ReportCommand.puml new file mode 100644 index 0000000000..fd741644bc --- /dev/null +++ b/docs/uml-diagrams/ReportCommand.puml @@ -0,0 +1,142 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class Inventory +class Item +class ReportCommand +class Parser + +ReportCommand <.. Parser +Command <|. ReportCommand +Ui <.. ReportCommand +Inventory <.. ReportCommand +Item <.. ReportCommand + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class ReportCommand { + -reportType:String + -threshold:int + -reportHasItemsExecute():void + -createExpiryReport():void + -createLowStockReport():void + +ReportCommand() + +execute():void + +isQuit():boolean +} + +class Item { + {static}+getQuantity():int + {static}+getExpiryDate():LocalDate + {static}+sortByQuantity():Comparator + {static}+sortByDate():Comparator + {static}+isEmpty():boolean +} + +class Parser { + {static}-parseReportCommand(input:String):ReportCommand +} + +class Inventory { + {static}+getItems():List +} + +class Ui { + {static}+reportCommandSuccess(item:Item):void + {static}+reportNoItems(item:Item):void +} + +note "Some parameters and methods \nomitted for brevity" as n1 +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":ReportCommand" as ReportCommand #cbf7f4 +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant ":Item" as Item #fbffb2 + +SuperTracker -> ReportCommand : execute() +activate ReportCommand #cbf7f4 + +ReportCommand -> Inventory : getItems() +activate Inventory #d5eac2 +Inventory --> ReportCommand : items:List +deactivate Inventory + +alt items.isEmpty() + ReportCommand -> Ui : reportNoItems() + activate Ui #e5c2ea + Ui --> ReportCommand + deactivate Ui + +else + ReportCommand -> ReportCommand : reportHasItemsExecute(items) + activate ReportCommand #cbf7f4 + alt reportType.equals("low stock") + ReportCommand -> ReportCommand : createLowStockReport() + activate ReportCommand #cbf7f4 + loop for each item in items + ReportCommand -> Item : getQuantity() + activate Item #fbffb2 + Item --> ReportCommand + deactivate Item + opt items quantity is less than threshold + ReportCommand -> ReportCommand : reportLowStockItems.add(item) + end + ReportCommand -> ReportCommand : sort() + note right : items sorted by quantity + ReportCommand -> Ui : reportCommandSuccess() + activate Ui #e5c2ea + Ui --> ReportCommand + deactivate Ui + end + ReportCommand --> ReportCommand + deactivate ReportCommand + + else reportType.equals("expiry") + ReportCommand -> ReportCommand : createExpiryReport() + activate ReportCommand #cbf7f4 + loop for each item in items + ReportCommand -> Item : getExpiryDate() + activate Item #fbffb2 + Item --> ReportCommand + deactivate Item + opt items with expiry dates that are between today and a week later + ReportCommand -> ReportCommand : reportExpiryItems.add(item) + end + opt items with expiry dates that have already passed + ReportCommand -> ReportCommand : reportExpiredItems.add(item) + end + ReportCommand -> ReportCommand : sort() + note right : items sorted by expiry date for both expiry reports + ReportCommand -> Ui : reportCommandSuccess() + note left : for both expiry reports + activate Ui #e5c2ea + Ui --> ReportCommand + deactivate Ui + end + + ReportCommand --> ReportCommand + deactivate ReportCommand + end + + ReportCommand --> ReportCommand + deactivate ReportCommand +end + +note right : some parameters and methods \n are omitted for brevity + +ReportCommand --> SuperTracker +deactivate ReportCommand + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/ReportCommandClass.png b/docs/uml-diagrams/ReportCommandClass.png new file mode 100644 index 0000000000..7447d72eb7 Binary files /dev/null and b/docs/uml-diagrams/ReportCommandClass.png differ diff --git a/docs/uml-diagrams/ReportCommandSequence.png b/docs/uml-diagrams/ReportCommandSequence.png new file mode 100644 index 0000000000..e44dc9e6e7 Binary files /dev/null and b/docs/uml-diagrams/ReportCommandSequence.png differ diff --git a/docs/uml-diagrams/RevenueCommand.puml b/docs/uml-diagrams/RevenueCommand.puml new file mode 100644 index 0000000000..1714f10aaa --- /dev/null +++ b/docs/uml-diagrams/RevenueCommand.puml @@ -0,0 +1,115 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class TransactionList +class RevenueCommand +class Parser +class Item + +RevenueCommand <.. Parser +Command <|. RevenueCommand +Ui <.. RevenueCommand +TransactionList <.. RevenueCommand +Item <.. RevenueCommand + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class RevenueCommand { + -startDate:LocalDate + -endDate:LocalDate + -task:String + -expenditure:BigDecimal + +RevenueCommand() + +execute():void + +isQuit():boolean +} + +class Ui { + {static}+printRevenueExpenditure():void +} + +class Parser { + {static}-parseRevenueCommand(input:String):RevenueCommand +} + +class TransactionList { + {static}+calculateDay():BigDecimal + {static}+calculateTotal():BigDecimal + {static}+calculateRange():BigDecimal + {static}+getFilteredTransactionList():ArrayList +} + +class Item { + {static}+sortByDate():Comparator +} + +note "Some parameters and methods \nomitted for brevity" as n1 +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":RevenueCommand" as RevenueCommand #cbf7f4 +participant "TransactionList" as TransactionList <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "Collections" as Collections <> #ffcdd6 + +SuperTracker -> RevenueCommand : execute() +activate RevenueCommand #cbf7f4 +alt task is "today" + RevenueCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> RevenueCommand : revenue:BigDecimal + deactivate TransactionList + +else task is "total" + RevenueCommand -> TransactionList : calculateTotal() + activate TransactionList #d5eac2 + TransactionList --> RevenueCommand : revenue:BigDecimal + deactivate TransactionList + +else task is "day" + RevenueCommand -> TransactionList : calculateDay() + activate TransactionList #d5eac2 + TransactionList --> RevenueCommand : revenue:BigDecimal + deactivate TransactionList + +else task is "range" + RevenueCommand -> TransactionList : calculateRange() + activate TransactionList #d5eac2 + TransactionList --> RevenueCommand : revenue:BigDecimal + deactivate TransactionList +end + +note right : some parameters and methods \nomitted for brevity + +RevenueCommand -> TransactionList : getFilteredTransactionList() +activate TransactionList #d5eac2 +TransactionList --> RevenueCommand : filteredList:ArrayList +deactivate TransactionList + +RevenueCommand -> RevenueCommand : sort() +note right : Transactions sorted by date + +RevenueCommand -> Collections : reverse(filteredList:ArrayList) +activate Collections #ffcdd6 +Collections --> RevenueCommand +deactivate Collections + +RevenueCommand -> Ui : printRevenueExpenditure() +activate Ui #e5c2ea +Ui --> RevenueCommand +deactivate Ui + +RevenueCommand --> SuperTracker +deactivate RevenueCommand + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/RevenueCommandClass.png b/docs/uml-diagrams/RevenueCommandClass.png new file mode 100644 index 0000000000..633b1cf300 Binary files /dev/null and b/docs/uml-diagrams/RevenueCommandClass.png differ diff --git a/docs/uml-diagrams/RevenueCommandSequence.png b/docs/uml-diagrams/RevenueCommandSequence.png new file mode 100644 index 0000000000..9b0e5f532e Binary files /dev/null and b/docs/uml-diagrams/RevenueCommandSequence.png differ diff --git a/docs/uml-diagrams/SellCommand.puml b/docs/uml-diagrams/SellCommand.puml new file mode 100644 index 0000000000..3e3291392d --- /dev/null +++ b/docs/uml-diagrams/SellCommand.puml @@ -0,0 +1,104 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class RemoveCommand +class SellCommand +class Parser +class Transaction +class TransactionList +class TransactionStorage +class Ui + +RemoveCommand <|- SellCommand +Transaction <.. SellCommand +Ui <.. SellCommand +TransactionList <.. SellCommand +SellCommand <.. Parser +TransactionStorage <.. SellCommand +TransactionList <.. TransactionStorage + +class RemoveCommand { + #name:String + #quantity:int + #quantityRemoved:int + #newItem:Item + +RemoveCommand() + #executeWithoutUi():void + +execute():void + +isQuit():boolean +} + +class SellCommand { + -currentDate:LocalDate + +SellCommand() + +execute():void +} + +class Transaction { + +Transaction() +} + +class TransactionList { + {static}+add(transaction:Transaction):void +} + +class Ui { + {static}+sellCommandSuccess():void +} + +class Parser { + {static}-parseSellCommand(input:String):SellCommand +} + +class TransactionStorage { + {static}+saveTransaction(transaction:Transaction):void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":SellCommand" as SellCommand #cbf7f4 +participant "TransactionStorage" as TransactionStorage <> #bcf7cf +participant "TransactionList" as TransactionList <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant ":Transaction" as Transaction #fbffb2 + +SuperTracker -> SellCommand : execute() +activate SellCommand #cbf7f4 + +ref over SellCommand : execute remove command without Ui + +SellCommand -> Transaction ** : new Transaction(parameters omitted for brevity) +activate Transaction #fbffb2 +Transaction --> SellCommand : transaction:Transaction +deactivate Transaction + +opt quantityRemoved > 0 + +SellCommand -> TransactionList : add(transaction:Transaction) +activate TransactionList #d5eac2 +TransactionList --> SellCommand +deactivate TransactionList + +SellCommand -> TransactionStorage : saveTransaction(transaction:Transaction) +activate TransactionStorage #bcf7cf +TransactionStorage --> SellCommand +deactivate TransactionStorage + +end + +SellCommand -> Ui : sellCommandSuccess(newItem:Item, transaction:Transaction) +activate Ui #e5c2ea +Ui --> SellCommand +deactivate Ui + +SellCommand --> SuperTracker +deactivate SellCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/SellCommandClass.png b/docs/uml-diagrams/SellCommandClass.png new file mode 100644 index 0000000000..45357f8cf6 Binary files /dev/null and b/docs/uml-diagrams/SellCommandClass.png differ diff --git a/docs/uml-diagrams/SellCommandSequence.png b/docs/uml-diagrams/SellCommandSequence.png new file mode 100644 index 0000000000..568fa22298 Binary files /dev/null and b/docs/uml-diagrams/SellCommandSequence.png differ diff --git a/docs/uml-diagrams/TransactionStorage.puml b/docs/uml-diagrams/TransactionStorage.puml new file mode 100644 index 0000000000..fe002a822b --- /dev/null +++ b/docs/uml-diagrams/TransactionStorage.puml @@ -0,0 +1,66 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +class SuperTracker +class FileManager +class TransactionList +class Transaction +class CommandClass +class TransactionStorage +class Item + +Item <|- Transaction +FileManager <|--- TransactionStorage +TransactionList <.. TransactionStorage +Transaction <.. TransactionStorage +Transaction "*" <--* TransactionList +TransactionStorage <.. CommandClass +TransactionStorage <. SuperTracker + +class FileManager { + {static}#DATA_PATH:String + {static}#checkDataDirectory():void + {static}#getNameQtyPriceStrings(item:Item):String[] +} + +class TransactionStorage { + {static}+saveTransaction(transaction:Transaction):void + {static}+resaveCurrentTransactions():void + {static}+loadTransactionData():void + {static}-getTransactionData(item:Item):String + {static}-parseTransactionData(data:String):Transaction + {static}-saveAllTransactions():void +} + +class SuperTracker { + {static}-run():void +} + +class CommandClass { + +execute():void +} + +class TransactionList { + {static}+get(index:int):Transaction + {static}+add(transaction:Transaction):void +} + +class Transaction { + +getTransactionDate():LocalDate +} + +class Item { + -name:String + -quantity:int + -price:double + -expiryDate:LocalDate +} + +note left of CommandClass + CommandClass refers to the classes + that implement the Command interface +endnote + +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/TransactionStorageClass.png b/docs/uml-diagrams/TransactionStorageClass.png new file mode 100644 index 0000000000..761340ac18 Binary files /dev/null and b/docs/uml-diagrams/TransactionStorageClass.png differ diff --git a/docs/uml-diagrams/UpdateCommand.puml b/docs/uml-diagrams/UpdateCommand.puml new file mode 100644 index 0000000000..4ce165be41 --- /dev/null +++ b/docs/uml-diagrams/UpdateCommand.puml @@ -0,0 +1,133 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +skinparam CircledCharacterFontSize 0 +skinparam CircledCharacterRadius 0 + +interface Command +class Ui +class Inventory +class Item +class UpdateCommand +class Parser +class ItemStorage + +UpdateCommand <.. Parser +Command <|. UpdateCommand +Ui <.. UpdateCommand +Inventory <.. UpdateCommand +Item <.. UpdateCommand +ItemStorage <.. UpdateCommand +Inventory <.. ItemStorage + +interface Command <> { + +execute():void + +isQuit():boolean +} + +class UpdateCommand { + -name:String + -newQuantity:int + -newPrice:double + -newExpiryDate:LocalDate + +UpdateCommand() + +execute():void + +isQuit():boolean +} + +class Item { + +Item() + +getName():String + +getQuantity():int + +getPrice():double + +getExpiryDate():LocalDate +} + +class Ui { + {static}+updateCommandSuccess(item:Item):void +} + +class Parser { + {static}-parseUpdateCommand(input:String):UpdateCommand +} + +class Inventory { + {static}+get(name:String):Item + {static}+put(name:String, item:Item):void +} + +class ItemStorage { + {static}+saveData():void +} + +note "Some parameters and methods \nomitted for brevity" as n1 + +@enduml + +@startuml +hide footbox +participant "SuperTracker" as SuperTracker <> #f5e3a9 +participant ":UpdateCommand" as UpdateCommand #cbf7f4 +participant "ItemStorage" as ItemStorage <> #bcf7cf +participant "Inventory" as Inventory <> #d5eac2 +participant "Ui" as Ui <> #e5c2ea +participant "oldItem:Item" as oldItem #ffa1a1 +participant ":Item" as Item #fbffb2 + +SuperTracker -> UpdateCommand : execute() +activate UpdateCommand #cbf7f4 + +UpdateCommand -> Inventory : get(name:String) +activate Inventory #d5eac2 +Inventory --> UpdateCommand : oldItem:Item +deactivate Inventory + +opt newQuantity == -1 + UpdateCommand -> oldItem: getQuantity() + activate oldItem #ffa1a1 + oldItem --> UpdateCommand: oldItemQuantity:int + deactivate oldItem +end + +opt newPrice == -1 + UpdateCommand -> oldItem: getPrice() + activate oldItem #ffa1a1 + oldItem --> UpdateCommand: oldItemPrice:double + deactivate oldItem +end + +opt newExpiryDate == "1-1-1" + UpdateCommand -> oldItem: getExpiryDate() + activate oldItem #ffa1a1 + oldItem --> UpdateCommand: oldExpiryDate:LocalDate + deactivate oldItem +end + +UpdateCommand -> oldItem: getName() +activate oldItem #ffa1a1 +oldItem --> UpdateCommand: :String +deactivate oldItem + +UpdateCommand -> Item ** : new Item(name:String, quantity:int, price:double, expiryDate:LocalDate) +activate Item #fbffb2 +Item --> UpdateCommand : newItem:Item +deactivate Item + +UpdateCommand -> Inventory : put(name:String, newItem:Item) +activate Inventory #d5eac2 +Inventory --> UpdateCommand +deactivate Inventory + +UpdateCommand -> Ui : updateCommandSuccess(item:Item) +activate Ui #e5c2ea +Ui --> UpdateCommand +deactivate Ui + +UpdateCommand -> ItemStorage : saveData() +activate ItemStorage #bcf7cf +ItemStorage --> UpdateCommand +deactivate ItemStorage + +UpdateCommand --> SuperTracker +deactivate UpdateCommand +@enduml \ No newline at end of file diff --git a/docs/uml-diagrams/UpdateCommandClass.png b/docs/uml-diagrams/UpdateCommandClass.png new file mode 100644 index 0000000000..90e28de9d9 Binary files /dev/null and b/docs/uml-diagrams/UpdateCommandClass.png differ diff --git a/docs/uml-diagrams/UpdateCommandSequence.png b/docs/uml-diagrams/UpdateCommandSequence.png new file mode 100644 index 0000000000..68e1fcf4ef Binary files /dev/null and b/docs/uml-diagrams/UpdateCommandSequence.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cd..afba109285 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66c01cfeba..2f4c9a940a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew index fcb6fca147..fbf29cc490 100755 --- a/gradlew +++ b/gradlew @@ -245,4 +245,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ 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/supertracker/SuperTracker.java b/src/main/java/supertracker/SuperTracker.java new file mode 100644 index 0000000000..f86668514e --- /dev/null +++ b/src/main/java/supertracker/SuperTracker.java @@ -0,0 +1,86 @@ +package supertracker; + +import supertracker.command.Command; +import supertracker.command.InvalidCommand; +import supertracker.command.QuitCommand; +import supertracker.parser.Parser; +import supertracker.storage.ItemStorage; +import supertracker.storage.TransactionStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; +import java.util.Scanner; + +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SuperTracker { + private static final Logger logger = Logger.getLogger(SuperTracker.class.getName()); + private static final String START_MESSAGE = "Starting SuperTracker application"; + private static final String EXIT_MESSAGE = "Exiting SuperTracker application"; + private static final String LOG_FILE_LOCATION = "supertracker.log"; + private static final String COMMAND_LOG = "Command passed successfully:"; + private static final String UNSUCCESSFUL_COMMAND_LOG = "Error while passing input: "; + + /** + * Main entry-point for the java.supertracker.SuperTracker application. + */ + public static void main(String[] args) { + run(); + } + + /** + * Runs the java.supertracker.SuperTracker application. + */ + private static void run() { + setupLogger(); + logger.info(START_MESSAGE); + + try { + ItemStorage.loadData(); + TransactionStorage.loadTransactionData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_LOAD_ERROR); + } + + Ui.greetUser(); + handleCommands(); + + logger.info(EXIT_MESSAGE); + } + + private static void handleCommands() { + Scanner in = new Scanner(System.in); + Command command; + do { + String input = in.nextLine(); + Ui.printLine(); + try { + command = Parser.parseCommand(input.trim()); + command.execute(); + logger.log(Level.INFO, COMMAND_LOG,command); + } catch (TrackerException e) { + Ui.printError(e.getErrorMessage()); + logger.log(Level.INFO, UNSUCCESSFUL_COMMAND_LOG + e.getErrorMessage(), e); + command = new InvalidCommand(); + } + Ui.printLine(); + } while (!command.isQuit()); + + assert command instanceof QuitCommand; + in.close(); + } + + private static void setupLogger() { + try { + FileHandler fileHandler = new FileHandler(LOG_FILE_LOCATION); + fileHandler.setLevel(Level.INFO); // Set desired log level + logger.setUseParentHandlers(false); // Disable console output for simplicity + logger.addHandler(fileHandler); + } catch (Exception e) { + logger.log(Level.SEVERE, ErrorMessage.FILE_HANDLER_ERROR, e); + } + } +} diff --git a/src/main/java/supertracker/TrackerException.java b/src/main/java/supertracker/TrackerException.java new file mode 100644 index 0000000000..65e2cda475 --- /dev/null +++ b/src/main/java/supertracker/TrackerException.java @@ -0,0 +1,14 @@ +package supertracker; + +public class TrackerException extends Exception { + protected String errorMessage; + + public TrackerException(String errorMessage) { + assert !errorMessage.isEmpty(); + this.errorMessage = errorMessage; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/supertracker/command/AddCommand.java b/src/main/java/supertracker/command/AddCommand.java new file mode 100644 index 0000000000..c53ca0cd81 --- /dev/null +++ b/src/main/java/supertracker/command/AddCommand.java @@ -0,0 +1,81 @@ +package supertracker.command; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; + +/** + * Represents a command for increasing the quantity of an item. + */ +public class AddCommand implements Command { + protected String name; + protected int quantity; + protected Item newItem; + + /** + * Constructs a new AddCommand object with the specified name and quantity. + * + * @param name Name of the item to be added + * @param quantity Quantity of the item to be added + */ + public AddCommand(String name, int quantity) { + this.name = name; + this.quantity = quantity; + } + + /** + * Executes the AddCommand without displaying any user interface. + *

+ * This method adds the specified quantity of an item to the inventory. It ensures + * that the inventory contains the specified item and that the quantity is non-negative. + * If the item already exists in the inventory, its quantity is updated accordingly. + * After updating the inventory, the changes are saved to persistent storage. + * If an IOException occurs while saving data, an error message is printed. + * + * @throws AssertionError If the inventory does not contain the specified item + * or if the quantity is negative + */ + protected void executeWithoutUi() { + assert Inventory.contains(name); + assert quantity >= 0; + + Item oldItem = Inventory.get(name); + int newQuantity = oldItem.getQuantity() + quantity; + newItem = new Item(oldItem.getName(), newQuantity, oldItem.getPrice(), oldItem.getExpiryDate()); + Inventory.put(name, newItem); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Executes the AddCommand, including user interface interactions. + *

+ * This method executes the AddCommand by first adding the specified quantity + * of an item to the inventory using the {@code executeWithoutUi()} method. + * After that, it informs the user about the success of the command by + * displaying a success message containing the details of the added item and quantity. + */ + @Override + public void execute() { + executeWithoutUi(); + Ui.addCommandSuccess(newItem, quantity); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/BuyCommand.java b/src/main/java/supertracker/command/BuyCommand.java new file mode 100644 index 0000000000..fa18f30556 --- /dev/null +++ b/src/main/java/supertracker/command/BuyCommand.java @@ -0,0 +1,56 @@ +package supertracker.command; + +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.storage.TransactionStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Represents a command for buying items and adding them to the inventory. + */ +public class BuyCommand extends AddCommand { + private static final String BUY_FLAG = "b"; + private double price; + private LocalDate currentDate; + + /** + * Constructs a new BuyCommand object with the specified name, quantity, price, and current date. + * + * @param name Name of the item to be bought + * @param quantity Quantity of the item to be bought + * @param price Price of each unit of the item + * @param currentDate Current date of the transaction + */ + public BuyCommand(String name, int quantity, double price, LocalDate currentDate) { + super(name, quantity); + this.price = price; + this.currentDate = currentDate; + } + + /** + * Executes the BuyCommand, including user interface interactions. + *

+ * This method executes the BuyCommand by first adding the specified quantity + * of an item to the inventory using the {@code executeWithoutUi()} method inherited + * from the AddCommand class. After that, it creates a new transaction record, + * adds it to the transaction list, and informs the user about the success of the command + * by displaying a success message containing the details of the bought item and transaction. + */ + @Override + public void execute() { + super.executeWithoutUi(); + Transaction transaction = new Transaction(newItem.getName(), quantity, price, currentDate, BUY_FLAG); + TransactionList.add(transaction); + Ui.buyCommandSuccess(newItem, transaction); + + try { + TransactionStorage.saveTransaction(transaction); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } +} diff --git a/src/main/java/supertracker/command/ClearCommand.java b/src/main/java/supertracker/command/ClearCommand.java new file mode 100644 index 0000000000..d8fc011efb --- /dev/null +++ b/src/main/java/supertracker/command/ClearCommand.java @@ -0,0 +1,87 @@ +package supertracker.command; + +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.storage.TransactionStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.Iterator; +import java.util.Scanner; + +/** + * Represents a command for clearing transactions before a specified date. + */ +public class ClearCommand implements Command { + private static final String CLEAR_CONFIRM = "y"; + private LocalDate beforeDate; + + /** + * Constructs a new ClearCommand object with the specified date. + * + * @param beforeDate Date before which transactions should be cleared + */ + public ClearCommand(LocalDate beforeDate) { + this.beforeDate = beforeDate; + } + + /** + * Removes old transactions from the TransactionList before a specified date. + * + * @return Number of transactions removed from the TransactionList. + */ + private int clearOldTransactions() { + int oldTransactionListSize = TransactionList.size(); + Iterator iterator = TransactionList.iterator(); + while (iterator.hasNext()) { + Transaction transaction = iterator.next(); + if (transaction.getTransactionDate().isBefore(beforeDate)) { + iterator.remove(); + } + } + int newTransactionListSize = TransactionList.size(); + return oldTransactionListSize - newTransactionListSize; + } + + /** + * Executes the ClearCommand + *

+ * This method confirms with the user whether they want to clear transactions + * before the specified date. If confirmed, it removes transactions from the transaction list + * that occurred before the specified date. It then informs the user about the success of the command + * by displaying a success message containing the number of transactions cleared and the specified date. + */ + @Override + public void execute() { + Ui.clearCommandConfirmation(beforeDate); + Scanner in = new Scanner(System.in); + String input = in.nextLine(); + Ui.printLine(); + + if (!input.equalsIgnoreCase(CLEAR_CONFIRM)) { + Ui.clearCommandCancelled(); + return; + } + + int transactionsCleared = clearOldTransactions(); + Ui.clearCommandSuccess(transactionsCleared, beforeDate); + + try { + TransactionStorage.resaveCurrentTransactions(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/Command.java b/src/main/java/supertracker/command/Command.java new file mode 100644 index 0000000000..0d6ea48f07 --- /dev/null +++ b/src/main/java/supertracker/command/Command.java @@ -0,0 +1,6 @@ +package supertracker.command; + +public interface Command { + void execute(); + boolean isQuit(); +} diff --git a/src/main/java/supertracker/command/DeleteCommand.java b/src/main/java/supertracker/command/DeleteCommand.java new file mode 100644 index 0000000000..cbc96a2462 --- /dev/null +++ b/src/main/java/supertracker/command/DeleteCommand.java @@ -0,0 +1,52 @@ +package supertracker.command; + +import supertracker.item.Inventory; +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; + +/** + * Represents a command to delete an item from the Inventory. + */ +public class DeleteCommand implements Command { + private String name; + + /** + * Constructs a DeleteCommand with the specified name of item. + * + * @param name Name of item as well as its key in the HashMap Inventory. + */ + public DeleteCommand(String name) { + this.name = name; + } + + /** + * Checks if the item exists in the inventory and calls delete() method from inventory to remove the item. + * Display the success message. + */ + @Override + public void execute() { + assert Inventory.contains(name); + + Inventory.delete(name); + Ui.deleteCommandSuccess(name); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/ExpenditureCommand.java b/src/main/java/supertracker/command/ExpenditureCommand.java new file mode 100644 index 0000000000..0ec9f34cd0 --- /dev/null +++ b/src/main/java/supertracker/command/ExpenditureCommand.java @@ -0,0 +1,82 @@ +package supertracker.command; + +import supertracker.item.Item; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.ui.Ui; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; + +// @@ author dtaywd +/** + * Represents a command to calculate and display expenditure information based on specified criteria. + */ +public class ExpenditureCommand implements Command { + private static final String BUY_FLAG = "b"; + private LocalDate startDate; + private LocalDate endDate; + private String task; + private BigDecimal expenditure; + + /** + * Constructs an ExpenditureCommand with the specified task, start date, and end date. + * + * @param task The task type (e.g., "today", "total", "day", "range"). + * @param startDate The start date for filtering transactions. + * @param endDate The end date for filtering transactions (used with "range" task). + */ + public ExpenditureCommand(String task, LocalDate startDate, LocalDate endDate) { + this.task = task; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Executes the expenditure command based on the specified task. + * Calculates expenditure and displays relevant information using UI utilities. + */ + @Override + public void execute() { + switch (task) { + case "today": + LocalDate currDate = LocalDate.now(); + expenditure = TransactionList.calculateDay(currDate, BUY_FLAG); + break; + + case "total": + expenditure = TransactionList.calculateTotal(BUY_FLAG); + break; + + case "day": + expenditure = TransactionList.calculateDay(startDate, BUY_FLAG); + break; + + case "range": + expenditure = TransactionList.calculateRange(startDate, endDate, BUY_FLAG); + break; + + default: + assert task.isEmpty(); + break; + } + + ArrayList filteredList = TransactionList.getFilteredTransactionList( + task, startDate, endDate, BUY_FLAG); + filteredList.sort(Item.sortByDate()); + Collections.reverse(filteredList); + Ui.printRevenueExpenditure(task, expenditure, startDate, endDate, "expenditure", filteredList); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/FindCommand.java b/src/main/java/supertracker/command/FindCommand.java new file mode 100644 index 0000000000..e1bfe652ad --- /dev/null +++ b/src/main/java/supertracker/command/FindCommand.java @@ -0,0 +1,55 @@ +package supertracker.command; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.ui.Ui; + +import java.util.List; + +/** + * Represents a command to find an existing item in the inventory. + */ +//@@author TimothyLKM +public class FindCommand implements Command { + private String name; + + /** + * Constructs a FindCommand with the specified item details. + * + * @param name The name of the item to search for. + */ + public FindCommand(String name) { + this.name = name; + } + + /** + * Executes the Find command to show the various description of the item. + */ + @Override + public void execute() { + int index = 1; + boolean isFound = false; + List items = Inventory.getItems(); + + for (Item item : items) { + if (item.getName().toLowerCase().contains(name.toLowerCase())) { + Ui.printFoundItem(item, index); + index++; + isFound = true; + } + } + if (!isFound) { + Ui.printNoItemFound(name); + } + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/HelpCommand.java b/src/main/java/supertracker/command/HelpCommand.java new file mode 100644 index 0000000000..a85ffa82d8 --- /dev/null +++ b/src/main/java/supertracker/command/HelpCommand.java @@ -0,0 +1,102 @@ +package supertracker.command; + +import supertracker.ui.HelpCommandUi; + +import java.util.Scanner; + +/** + * Represents a command that prints a list of functions available in SuperTracker to help the user. + */ +// @@author TimothyLKM +public class HelpCommand implements Command { + private static final String NEW_COMMAND = "new"; + private static final String LIST_COMMAND = "list"; + private static final String UPDATE_COMMAND = "update"; + private static final String DELETE_COMMAND = "delete"; + private static final String TRANSACTION_COMMAND = "transaction"; + private static final String FIND_COMMAND = "find"; + private static final String REPORT_COMMAND = "report"; + private static final String CHANGE_QUANTITY_COMMAND = "change"; + private static final String RENAME_COMMAND = "rename"; + private static final String EXPENDITURE_COMMAND = "exp"; + private static final String REVENUE_COMMAND = "rev"; + private static final String PROFIT_COMMAND = "profit"; + private static final String CLEAR_COMMAND = "clear"; + + private static String getHelpCommandReply(String input) { + if (!input.contains(" ")) { + return input; + } + return input.substring(0, input.indexOf(" ")); + } + + /** + * Executes the Help command to print a list of functions available. + * Scans in the next input to then print the needed parameters for the chosen function. + */ + @Override + public void execute() { + HelpCommandUi.helpCommandSuccess(); + Scanner in = new Scanner(System.in); + String input = in.nextLine(); + String helpCommandWord = getHelpCommandReply(input); + HelpCommandUi.printLine(); + + switch (helpCommandWord) { + case NEW_COMMAND: + HelpCommandUi.printNewCommandParams(); + break; + case DELETE_COMMAND: + HelpCommandUi.printDeleteCommandParams(); + break; + case CHANGE_QUANTITY_COMMAND: + HelpCommandUi.printChangeCommandParams(); + break; + case UPDATE_COMMAND: + HelpCommandUi.printUpdateCommandParams(); + break; + case FIND_COMMAND: + HelpCommandUi.printFindCommandParams(); + break; + case RENAME_COMMAND: + HelpCommandUi.printRenameCommandParams(); + break; + case LIST_COMMAND: + HelpCommandUi.printListCommandParams(); + break; + case REPORT_COMMAND: + HelpCommandUi.printReportCommandParams(); + break; + case TRANSACTION_COMMAND: + HelpCommandUi.printTransactionCommandParams(); + break; + case EXPENDITURE_COMMAND: + HelpCommandUi.printExpenditureCommandParams(); + break; + case REVENUE_COMMAND: + HelpCommandUi.printRevenueCommandParams(); + break; + case PROFIT_COMMAND: + HelpCommandUi.printProfitCommandParams(); + break; + case CLEAR_COMMAND: + HelpCommandUi.printClearCommandParams(); + break; + default: + HelpCommandUi.printInvalidHelpMessage(); + break; + } + + HelpCommandUi.helpClosingMessage(); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/InvalidCommand.java b/src/main/java/supertracker/command/InvalidCommand.java new file mode 100644 index 0000000000..ef1fd659c8 --- /dev/null +++ b/src/main/java/supertracker/command/InvalidCommand.java @@ -0,0 +1,26 @@ +package supertracker.command; + +import supertracker.ui.Ui; + +/** + * Represents an invalid command that does not match any valid command format. + */ +public class InvalidCommand implements Command { + /** + * Executes the invalid command by printing a message indicating that the command is invalid. + */ + @Override + public void execute() { + Ui.printInvalidCommand(); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/ListCommand.java b/src/main/java/supertracker/command/ListCommand.java new file mode 100644 index 0000000000..4fc02070f7 --- /dev/null +++ b/src/main/java/supertracker/command/ListCommand.java @@ -0,0 +1,139 @@ +package supertracker.command; + +import supertracker.ui.Ui; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Represents a command for listing items in the inventory with sorting options. + */ +public class ListCommand implements Command { + private static final String QUANTITY_FLAG = "q"; + private static final String PRICE_FLAG = "p"; + private static final String EX_DATE_FLAG = "e"; + private static final String ALPHABET = ""; + private String firstParam; + private String secondParam; + private String thirdParam; + private String firstSortParam; + private String secondSortParam; + private String thirdSortParam; + private boolean isReverse; + + /** + * Constructs a new ListCommand object with the specified parameters. + * + * @param firstParam First item parameter (quantity/price/expiry) to be printed out + * @param secondParam Second item parameter (quantity/price/expiry) to be printed out + * @param thirdParam Third item parameter (quantity/price/expiry) to be printed out + * @param firstSortParam Highest priority sorting method (quantity/price/expiry) + * @param secondSortParam Second-highest priority sorting method (quantity/price/expiry) + * @param thirdSortParam Third-highest priority sorting method (quantity/price/expiry) + * @param isReverse indicates whether the list should be reversed + */ + public ListCommand( + String firstParam, + String secondParam, + String thirdParam, + String firstSortParam, + String secondSortParam, + String thirdSortParam, + boolean isReverse + ) { + this.firstParam = firstParam; + this.secondParam = secondParam; + this.thirdParam = thirdParam; + this.firstSortParam = firstSortParam; + this.secondSortParam = secondSortParam; + this.thirdSortParam = thirdSortParam; + this.isReverse = isReverse; + } + + /** + * Executes the ListCommand. + *

+ * This method first ensures that all provided parameters are valid. Then, it retrieves + * the list of items from the inventory and sorts them based on the sorting parameters. + * Finally, it iterates through the sorted list, print items based on the ordering of parameters, + * and displays each item along with its index and relevant information. + */ + @Override + public void execute() { + assert isValid(firstParam); + assert isValid(secondParam); + assert isValid(thirdParam); + assert isValid(firstSortParam); + assert isValid(secondSortParam); + assert isValid(thirdSortParam); + + List items = Inventory.getItems(); + Ui.listIntro(items.size()); + + sortBy(ALPHABET, items); + sortBy(thirdSortParam, items); + sortBy(secondSortParam, items); + sortBy(firstSortParam, items); + + if (isReverse) { + Collections.reverse(items); + } + + int index = 1; + for (Item item : items) { + Ui.listItem(item, index, firstParam, secondParam, thirdParam); + index++; + } + } + + /** + * Sorts the list of items based on the provided sorting parameter. + * + * @param sortParam Sorting parameter (quantity/price/expiry) + * @param items List of items to be sorted + */ + private void sortBy(String sortParam, List items) { + Comparator comparator; + + switch (sortParam) { + case QUANTITY_FLAG: + comparator = Item.sortByQuantity(); + break; + case PRICE_FLAG: + comparator = Item.sortByPrice(); + break; + case EX_DATE_FLAG: + comparator = Item.sortByDate(); + break; + default: + comparator = Item.sortByName(); + break; + } + + items.sort(comparator); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } + + /** + * Checks if the provided string is valid. + * + * @param s The string to be validated. + * @return {@code true} if the string is equal to "q" or "p" or "e", + * or if the string is empty; {@code false} otherwise. + */ + private boolean isValid(String s) { + return s.equals(QUANTITY_FLAG) || s.equals(PRICE_FLAG) || s.equals(EX_DATE_FLAG) || s.isEmpty(); + } +} diff --git a/src/main/java/supertracker/command/NewCommand.java b/src/main/java/supertracker/command/NewCommand.java new file mode 100644 index 0000000000..02ba579e19 --- /dev/null +++ b/src/main/java/supertracker/command/NewCommand.java @@ -0,0 +1,70 @@ +package supertracker.command; + +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Represents a command for creating a new item in the inventory. + */ +public class NewCommand implements Command { + private String name; + private int quantity; + private double price; + private LocalDate expiryDate; + + /** + * Constructs a new NewCommand object with the specified name, quantity, price, and expiry date. + * + * @param name Name of the new item + * @param quantity Initial quantity of the new item + * @param price Price of each unit of the new item + * @param expiryDate Expiry date of the new item + */ + public NewCommand(String name, int quantity, double price, LocalDate expiryDate) { + this.name = name; + this.quantity = quantity; + this.price = price; + this.expiryDate = expiryDate; + } + + /** + * Executes the NewCommand + *

+ * This method adds a new item to the inventory with the specified name, quantity, price, + * and expiry date. It ensures that the inventory does not already contain an item with the same name, + * and that the quantity and price are non-negative. After adding the new item to the inventory, + * it informs the user about the success of the command by displaying a success message. + */ + @Override + public void execute() { + assert !Inventory.contains(name); + assert quantity >= 0; + assert price >= 0; + + Item item = new Item(name, quantity, price, expiryDate); + Inventory.put(name, item); + Ui.newCommandSuccess(item); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/ProfitCommand.java b/src/main/java/supertracker/command/ProfitCommand.java new file mode 100644 index 0000000000..29046072ff --- /dev/null +++ b/src/main/java/supertracker/command/ProfitCommand.java @@ -0,0 +1,74 @@ +//@@author vimalapugazhan +package supertracker.command; + +import supertracker.item.TransactionList; +import supertracker.ui.Ui; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * Represents a command to calculate and display profit information based on specified criteria. + */ +public class ProfitCommand implements Command{ + private static final String BUY_FLAG = "b"; + private static final String SELL_FLAG = "s"; + private String task; + private LocalDate startDate; + private LocalDate endDate; + + /** + * Constructs an ProfitCommand with the specified task, start date, and end date. + * + * @param task The task type (e.g., "today", "total", "day", "range"). + * @param startDate The start date for filtering transactions. + * @param endDate The end date for filtering transactions (used with "range" task). + */ + public ProfitCommand (String task, LocalDate startDate, LocalDate endDate) { + this.task = task; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Executes the profit command based on the specified task. + * Calculates profit and displays relevant information using UI utilities. + */ + @Override + public void execute() { + BigDecimal revenue = BigDecimal.valueOf(0); + BigDecimal expenditure = BigDecimal.valueOf(0); + switch (task) { + case "today": + revenue = TransactionList.calculateDay(LocalDate.now(), SELL_FLAG); + expenditure = TransactionList.calculateDay(LocalDate.now(), BUY_FLAG); + break; + case "total": + revenue = TransactionList.calculateTotal(SELL_FLAG); + expenditure = TransactionList.calculateTotal(BUY_FLAG); + break; + case "day": + revenue = TransactionList.calculateDay(startDate, SELL_FLAG); + expenditure = TransactionList.calculateDay(startDate, BUY_FLAG); + break; + case "range": + revenue = TransactionList.calculateRange(startDate, endDate, SELL_FLAG); + expenditure = TransactionList.calculateRange(startDate, endDate, BUY_FLAG); + break; + default: assert task.isEmpty(); + break; + } + BigDecimal profit = revenue.subtract(expenditure); + Ui.printProfit(task, profit, startDate, endDate); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/QuitCommand.java b/src/main/java/supertracker/command/QuitCommand.java new file mode 100644 index 0000000000..7991452ac9 --- /dev/null +++ b/src/main/java/supertracker/command/QuitCommand.java @@ -0,0 +1,26 @@ +package supertracker.command; + +import supertracker.ui.Ui; + +/** + * Represents a command to quit the application. + */ +public class QuitCommand implements Command { + /** + * Executes the QuitCommand by displaying a farewell message. + */ + @Override + public void execute() { + Ui.sayGoodbye(); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns true, as executing this command triggers application quit. + */ + @Override + public boolean isQuit() { + return true; + } +} diff --git a/src/main/java/supertracker/command/RemoveCommand.java b/src/main/java/supertracker/command/RemoveCommand.java new file mode 100644 index 0000000000..35f15c4982 --- /dev/null +++ b/src/main/java/supertracker/command/RemoveCommand.java @@ -0,0 +1,84 @@ +package supertracker.command; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; + +/** + * Represents a command for decreasing the quantity of an item. + */ +public class RemoveCommand implements Command { + protected String name; + protected int quantity; + protected int quantityRemoved; + protected Item newItem; + + /** + * Constructs a new RemoveCommand object with the specified name and quantity. + * + * @param name Name of the item to be removed + * @param quantity Quantity of the item to be removed + */ + public RemoveCommand(String name, int quantity) { + this.name = name; + this.quantity = quantity; + } + + /** + * Executes the RemoveCommand without displaying any user interface. + *

+ * This method removes the specified quantity of an item from the inventory. It ensures + * that the inventory contains the specified item and that the quantity is non-negative. + * If the quantity to be removed exceeds the quantity of the item in the inventory, + * the item's quantity is set to zero. After updating the inventory, the changes are saved + * to persistent storage. If an IOException occurs while saving data, an error message is printed. + * + * @throws AssertionError If the inventory does not contain the specified item + * or if the quantity is negative + */ + protected void executeWithoutUi() { + assert Inventory.contains(name); + assert quantity >= 0; + + Item oldItem = Inventory.get(name); + int newQuantity = oldItem.getQuantity() - quantity; + newQuantity = Math.max(newQuantity, 0); + quantityRemoved = oldItem.getQuantity() - newQuantity; + newItem = new Item(oldItem.getName(), newQuantity, oldItem.getPrice(), oldItem.getExpiryDate()); + Inventory.put(name, newItem); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Executes the RemoveCommand, including user interface interactions. + *

+ * This method executes the RemoveCommand by first removing the specified quantity + * of an item from the inventory using the {@code executeWithoutUi()} method. + * After that, it informs the user about the success of the command by + * displaying a success message containing the details of the removed item and quantity. + */ + @Override + public void execute() { + executeWithoutUi(); + Ui.removeCommandSuccess(newItem, quantityRemoved); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/RenameCommand.java b/src/main/java/supertracker/command/RenameCommand.java new file mode 100644 index 0000000000..922bf5263e --- /dev/null +++ b/src/main/java/supertracker/command/RenameCommand.java @@ -0,0 +1,62 @@ +package supertracker.command; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Represents a command that rename an existing item in the inventory. + */ +// @@author TimothyLKM +public class RenameCommand implements Command { + private String name; + private String newName; + + public RenameCommand(String name, String newName) { + this.name = name; + this.newName = newName; + } + + /** + * Executes the Rename command to create a new item with the new name and transfers over + * the price, quantity and expiry date of the item. + * Deletes the old item. + */ + @Override + public void execute() { + assert Inventory.contains(name); + + Item oldItem = Inventory.get(name); + String oldName = oldItem.getName(); + int quantity = oldItem.getQuantity(); + double price = oldItem.getPrice(); + LocalDate expiryDate = oldItem.getExpiryDate(); + + Item newItem = new Item(newName, quantity, price, expiryDate); + Inventory.delete(name); + + Inventory.put(newName, newItem); + Ui.renameCommandSuccess(newItem, oldName); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/ReportCommand.java b/src/main/java/supertracker/command/ReportCommand.java new file mode 100644 index 0000000000..1f54c145fa --- /dev/null +++ b/src/main/java/supertracker/command/ReportCommand.java @@ -0,0 +1,112 @@ +package supertracker.command; + +import supertracker.ui.Ui; +import supertracker.item.Inventory; +import supertracker.item.Item; +import java.time.LocalDate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a command to generate and display reports based on inventory items. + */ +public class ReportCommand implements Command{ + private String reportType; + private int threshold; + + /** + * Constructs a ReportCommand with the specified report type and threshold. + * + * @param reportType The type of report to generate ("low stock" or "expiry"). + * @param threshold The threshold value used for filtering items in the report. + */ + public ReportCommand(String reportType, int threshold) { + this.reportType = reportType; + this.threshold = threshold; + } + + /** + * Executes the report command to generate and display the specified report type. + * Retrieves inventory items and generates the appropriate report based on the report type. + */ + @Override + public void execute() { + List items = Inventory.getItems(); + if (items.isEmpty()) { + Ui.reportNoItems(); + } else { + reportHasItemsExecute(items); + } + } + + /** + * Generates the appropriate report based on the available inventory items. + * + * @param items The list of inventory items used for generating the report. + */ + private void reportHasItemsExecute(List items) { + LocalDate currDate = LocalDate.now(); + LocalDate expiryThresholdDate = currDate.plusWeeks(1); + LocalDate dayBeforeCurrDay = currDate.minusDays(1); + if (reportType.equals("low stock")) { + createLowStockReport(items); + } else if (reportType.equals("expiry")) { + createExpiryReport(items, expiryThresholdDate, currDate, dayBeforeCurrDay); + } + } + + /** + * Creates and displays a report for items that are close to expiry or have expired. + * + * @param items The list of inventory items to check for expiry. + * @param expiryThresholdDate The threshold date to determine items close to expiry. + * @param currDate The current date used for comparison. + * @param dayBeforeCurrDay The date one day before the current date for expiry comparison. + */ + private void createExpiryReport(List items, LocalDate expiryThresholdDate, LocalDate currDate, + LocalDate dayBeforeCurrDay) { + assert threshold == -1; + List reportExpiryItems = new ArrayList<>(); + List reportExpiredItems = new ArrayList<>(); + for (Item item : items) { + if (item.getExpiryDate().isBefore(expiryThresholdDate) && item.getExpiryDate().isAfter(dayBeforeCurrDay)) { + reportExpiryItems.add(item); + } + if (item.getExpiryDate().isBefore(currDate)) { + reportExpiredItems.add(item); + } + } + reportExpiryItems.sort(Item.sortByDate()); + reportExpiredItems.sort(Item.sortByDate()); + Ui.reportCommandSuccess(reportExpiryItems, reportType); + Ui.reportCommandSuccess(reportExpiredItems, "expired"); + } + + /** + * Creates and displays a report for items with low stock quantities. + * + * @param items The list of inventory items to check for low stock. + */ + private void createLowStockReport(List items) { + List reportLowStockItems = new ArrayList<>(); + for (Item item : items) { + if (item.getQuantity() < threshold) { + reportLowStockItems.add(item); + } + } + reportLowStockItems.sort(Item.sortByQuantity()); + Ui.reportCommandSuccess(reportLowStockItems, reportType); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } + +} diff --git a/src/main/java/supertracker/command/RevenueCommand.java b/src/main/java/supertracker/command/RevenueCommand.java new file mode 100644 index 0000000000..265fdb987e --- /dev/null +++ b/src/main/java/supertracker/command/RevenueCommand.java @@ -0,0 +1,75 @@ +//@@author vimalapugazhan +package supertracker.command; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; + +import supertracker.item.Item; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.ui.Ui; + +/** + * Represents a command to calculate and display revenue information based on specified criteria. + */ +public class RevenueCommand implements Command { + private static final String SELL_FLAG = "s"; + private String task; + private LocalDate startDate; + private LocalDate endDate; + private BigDecimal revenue; + + /** + * Constructs an RevenueCommand with the specified task, start date, and end date. + * + * @param task The task type (e.g., "today", "total", "day", "range"). + * @param startDate The start date for filtering transactions. + * @param endDate The end date for filtering transactions (used with "range" task). + */ + public RevenueCommand (String task, LocalDate startDate, LocalDate endDate) { + this.task = task; + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Executes the revenue command based on the specified task. + * Calculates revenue and displays relevant information using UI utilities. + */ + @Override + public void execute() { + switch (task) { + case "today": + revenue = TransactionList.calculateDay(LocalDate.now(), SELL_FLAG); + break; + case "total": + revenue = TransactionList.calculateTotal(SELL_FLAG); + break; + case "day": + revenue = TransactionList.calculateDay(startDate, SELL_FLAG); + break; + case "range": + revenue = TransactionList.calculateRange(startDate, endDate, SELL_FLAG); + break; + default: assert task.isEmpty(); + break; + } + ArrayList filteredList = TransactionList.getFilteredTransactionList( + task, startDate, endDate, SELL_FLAG); + filteredList.sort(Item.sortByDate()); + Collections.reverse(filteredList); + Ui.printRevenueExpenditure(task, revenue, startDate, endDate, "revenue", filteredList); + } + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/command/SellCommand.java b/src/main/java/supertracker/command/SellCommand.java new file mode 100644 index 0000000000..b2c47e076f --- /dev/null +++ b/src/main/java/supertracker/command/SellCommand.java @@ -0,0 +1,60 @@ +package supertracker.command; + +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.storage.TransactionStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Represents a command for selling items and removing them from the inventory. + */ +public class SellCommand extends RemoveCommand { + private static final String SELL_FLAG = "s"; + private LocalDate currentDate; + + /** + * Constructs a new SellCommand object with the specified name, quantity, and current date. + * + * @param name Name of the item to be sold + * @param quantity Quantity of the item to be sold + * @param currentDate Current date of the transaction + */ + public SellCommand(String name, int quantity, LocalDate currentDate) { + super(name, quantity); + this.currentDate = currentDate; + } + + /** + * Executes the SellCommand, including user interface interactions. + *

+ * This method executes the SellCommand by first removing the specified quantity + * of an item from the inventory using the {@code executeWithoutUi()} method inherited + * from the RemoveCommand class. After that, it creates a new transaction record, + * adds it to the transaction list, and informs the user about the success of the command + * by displaying a success message containing the details of the sold item and transaction. + */ + @Override + public void execute() { + super.executeWithoutUi(); + Transaction transaction = new Transaction( + newItem.getName(), + quantityRemoved, + newItem.getPrice(), + currentDate, + SELL_FLAG + ); + if (quantityRemoved > 0) { + TransactionList.add(transaction); + try { + TransactionStorage.saveTransaction(transaction); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + Ui.sellCommandSuccess(newItem, transaction); + } +} diff --git a/src/main/java/supertracker/command/UpdateCommand.java b/src/main/java/supertracker/command/UpdateCommand.java new file mode 100644 index 0000000000..342b22185c --- /dev/null +++ b/src/main/java/supertracker/command/UpdateCommand.java @@ -0,0 +1,84 @@ +package supertracker.command; + +import supertracker.storage.ItemStorage; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a command to update an existing item in the inventory with new quantity, price, and expiry date. + */ +public class UpdateCommand implements Command { + private String name; + private int newQuantity; + private double newPrice; + private LocalDate newExpiryDate; + + /** + * Constructs an UpdateCommand with the specified item details. + * + * @param name The name of the item to update. + * @param newQuantity The new quantity for the item. + * @param newPrice The new price for the item. + * @param newExpiryDate The new expiry date for the item. + */ + public UpdateCommand(String name, int newQuantity, double newPrice, LocalDate newExpiryDate) { + this.name = name; + this.newQuantity = newQuantity; + this.newPrice = newPrice; + this.newExpiryDate = newExpiryDate; + } + + /** + * Executes the update command to modify an existing item in the inventory. + * Updates the specified item with new quantity, price, and expiry date. + */ + //@@author dtaywd + @Override + public void execute() { + assert Inventory.contains(name); + + Item oldItem = Inventory.get(name); + if (newQuantity == -1) { + newQuantity = oldItem.getQuantity(); + } + if (newPrice == -1) { + newPrice = oldItem.getPrice(); + } + + LocalDate invalidDate = LocalDate.parse("1-1-1", DateTimeFormatter.ofPattern("y-M-d")); + if (newExpiryDate.isEqual(invalidDate)) { + newExpiryDate = oldItem.getExpiryDate(); + } + + assert newQuantity >= 0; + assert newPrice >= 0; + assert !newExpiryDate.isEqual(invalidDate); + + Item newItem = new Item(oldItem.getName(), newQuantity, newPrice, newExpiryDate); + Inventory.put(name, newItem); + Ui.updateCommandSuccess(newItem); + + try { + ItemStorage.saveData(); + } catch (IOException e) { + Ui.printError(ErrorMessage.FILE_SAVE_ERROR); + } + } + //@@author + + /** + * Indicates whether executing this command should result in quitting the application. + * + * @return Always returns false, as executing this command does not trigger application quit. + */ + @Override + public boolean isQuit() { + return false; + } +} diff --git a/src/main/java/supertracker/item/Inventory.java b/src/main/java/supertracker/item/Inventory.java new file mode 100644 index 0000000000..92a6afdeb4 --- /dev/null +++ b/src/main/java/supertracker/item/Inventory.java @@ -0,0 +1,70 @@ +package supertracker.item; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * Represents an inventory of items. + */ +public class Inventory { + // HashMap to store items with their names as keys + private static HashMap itemMap = new HashMap<>(); + + /** + * Checks if the inventory contains an item with the specified name. + * + * @param name Name of the item to check. + * @return {@code true} if the inventory contains the item; {@code false} otherwise. + */ + public static boolean contains(String name) { + return itemMap.containsKey(name.toLowerCase()); + } + + /** + * Retrieves the item with the specified name from the inventory. + * + * @param name Name of the item to retrieve. + * @return Item with the specified name, or {@code null} if not found. + */ + public static Item get(String name) { + return itemMap.get(name.toLowerCase()); + } + + /** + * Adds an item to the inventory. + * + * @param name Name of the item to add. + * @param item Item to add to the inventory. + */ + public static void put(String name, Item item) { + itemMap.put(name.toLowerCase(), item); + } + + /** + * Deletes an item from the inventory. + * + * @param name Name of the item to delete. + */ + public static void delete(String name) { + itemMap.remove(name.toLowerCase()); + } + + /** + * Clears all items from the inventory. + */ + public static void clear() { + itemMap.clear(); + } + + /** + * Retrieves a list of all items in the inventory. + * + * @return List containing all items in the inventory. + */ + public static List getItems() { + Collection items = itemMap.values(); + return new ArrayList<>(items); + } +} diff --git a/src/main/java/supertracker/item/Item.java b/src/main/java/supertracker/item/Item.java new file mode 100644 index 0000000000..03bc5c551b --- /dev/null +++ b/src/main/java/supertracker/item/Item.java @@ -0,0 +1,122 @@ +package supertracker.item; + +import java.time.LocalDate; + +import java.time.format.DateTimeFormatter; +import java.util.Comparator; + +/** + * Represents an item in the inventory. + */ +public class Item { + private static final DateTimeFormatter DATE_FORMAT_PRINT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + protected String name; + protected int quantity; + protected double price; + protected LocalDate expiryDate; + + /** + * Constructs an Item with the specified attributes. + * + * @param name Name of the item. + * @param quantity Quantity of the item. + * @param price Price of the item. + * @param expiryDate Expiry date of the item. + */ + public Item(String name, int quantity, double price, LocalDate expiryDate) { + this.name = name; + this.quantity = quantity; + this.price = price; + this.expiryDate = expiryDate; + } + + /** + * Retrieves the name of the item. + * + * @return Name of the item. + */ + public String getName() { + return name; + } + + /** + * Retrieves the quantity of the item. + * + * @return Quantity of the item. + */ + public int getQuantity() { + return quantity; + } + + /** + * Retrieves the price of the item. + * + * @return Price of the item. + */ + public double getPrice() { + return price; + } + + /** + * Retrieves the expiry date of the item. + * + * @return Expiry date of the item. + */ + public LocalDate getExpiryDate() { + return expiryDate; + } + + /** + * Retrieves the price of the item as a formatted string. + * + * @return Formatted price of the item. + */ + public String getPriceString() { + return "$" + String.format("%.2f", price); + } + + /** + * Retrieves the expiry date of the item as a formatted string. + * + * @return Formatted expiry date of the item. + */ + public String getExpiryDateString() { + return expiryDate.format(DATE_FORMAT_PRINT); + } + + /** + * Comparator for sorting items by name. + * + * @return Comparator for sorting items by name. + */ + public static Comparator sortByName() { + return Comparator.comparing(Item::getName, String.CASE_INSENSITIVE_ORDER); + } + + /** + * Comparator for sorting items by quantity. + * + * @return Comparator for sorting items by quantity. + */ + public static Comparator sortByQuantity() { + return Comparator.comparingInt(Item::getQuantity); + } + + /** + * Comparator for sorting items by price. + * + * @return Comparator for sorting items by price. + */ + public static Comparator sortByPrice() { + return Comparator.comparingDouble(Item::getPrice); + } + + /** + * Comparator for sorting items by expiry date. + * + * @return Comparator for sorting items by expiry date. + */ + public static Comparator sortByDate() { + return Comparator.comparing(Item::getExpiryDate); + } +} diff --git a/src/main/java/supertracker/item/Transaction.java b/src/main/java/supertracker/item/Transaction.java new file mode 100644 index 0000000000..2b13f5c281 --- /dev/null +++ b/src/main/java/supertracker/item/Transaction.java @@ -0,0 +1,67 @@ +package supertracker.item; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * Represents a transaction involving the buying or selling of an item. + */ +public class Transaction extends Item { + private static final String BUY_FLAG = "b"; + private static final String SELL_FLAG = "s"; + private String type; + + /** + * Constructs a Transaction with the specified attributes. + * + * @param name Name of the item involved in the transaction. + * @param quantity Quantity of the item involved in the transaction. + * @param price Price of the item involved in the transaction. + * @param transactionDate Date of the transaction. + * @param type Type of the transaction (buy/sell). + */ + public Transaction(String name, int quantity, double price, LocalDate transactionDate, String type) { + super(name, quantity, price, transactionDate); + this.type = type; + assert type.equals(BUY_FLAG) || type.equals(SELL_FLAG); + } + + /** + * Retrieves the type of the transaction. + * + * @return Type of the transaction (buy/sell). + */ + public String getType() { + return type; + } + + /** + * Retrieves the date of the transaction. + * + * @return Date of the transaction. + */ + public LocalDate getTransactionDate() { + return getExpiryDate(); + } + + /** + * Calculates the total price of the transaction. + * + * @return Total price of the transaction. + */ + public BigDecimal getTotalPrice() { + BigDecimal bigQuantity = new BigDecimal(quantity); + BigDecimal bigPrice = new BigDecimal(price); + return bigQuantity.multiply(bigPrice); + } + + /** + * Retrieves the total price of the transaction as a formatted string. + * + * @return Formatted total price of the transaction. + */ + public String getTotalPriceString() { + BigDecimal totalPrice = getTotalPrice(); + return "$" + String.format("%.2f", totalPrice); + } +} diff --git a/src/main/java/supertracker/item/TransactionList.java b/src/main/java/supertracker/item/TransactionList.java new file mode 100644 index 0000000000..13a6deaa17 --- /dev/null +++ b/src/main/java/supertracker/item/TransactionList.java @@ -0,0 +1,197 @@ +package supertracker.item; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Represents a list of transactions. + */ +public class TransactionList { + private static final String TODAY = "today"; + private static final String TOTAL = "total"; + private static final String DAY = "day"; + private static final String RANGE = "range"; + + // ArrayList to store transactions + private static ArrayList transactionList = new ArrayList<>(); + + /** + * Retrieves the transaction at the specified index. + * + * @param index Index of the transaction to retrieve. + * @return Transaction at the specified index. + */ + public static Transaction get(int index) { + return transactionList.get(index); + } + + /** + * Adds a transaction to the list. + * + * @param transaction Transaction to be added. + */ + public static void add(Transaction transaction) { + transactionList.add(transaction); + } + + /** + * Retrieves the number of transactions in the list. + * + * @return Number of transactions in the list. + */ + public static int size() { + return transactionList.size(); + } + + /** + * Clears all transactions from the list. + */ + public static void clear() { + transactionList.clear(); + } + + /** + * Returns an iterator over the transactions in the list. + * + * @return Iterator over the transactions in the list. + */ + public static Iterator iterator() { + return transactionList.iterator(); + } + + //@@author vimalapugazhan + public static BigDecimal calculateRange(LocalDate start, LocalDate end, String flag) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (Transaction transaction : transactionList) { + LocalDate transactionDate = transaction.getTransactionDate(); + String transactionType = transaction.getType(); + if (transactionType.equals(flag) && transactionDate.isBefore(end) && transactionDate.isAfter(start)) { + BigDecimal newAmount = transaction.getTotalPrice(); + totalAmount = totalAmount.add(newAmount); + } + } + return totalAmount; + } + + public static BigDecimal calculateDay(LocalDate day, String flag) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (Transaction transaction : transactionList) { + LocalDate transactionDate = transaction.getTransactionDate(); + String transactionType = transaction.getType(); + if (transactionType.equals(flag) && transactionDate.isEqual(day)) { + BigDecimal newAmount = transaction.getTotalPrice(); + totalAmount = totalAmount.add(newAmount); + } + } + return totalAmount; + } + + public static BigDecimal calculateTotal(String flag) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (Transaction transaction : transactionList) { + String transactionType = transaction.getType(); + if (transactionType.equals(flag)) { + BigDecimal newAmount = transaction.getTotalPrice(); + totalAmount = totalAmount.add(newAmount); + } + } + return totalAmount; + } + //@@author + + // @@ author dtaywd + /** + * Retrieves a filtered list of transactions based on the specified type, start date, and end date. + * + * @param type The type of transaction filter ('today', 'total', 'day', or 'range'). + * @param start The start date for filtering transactions (used in 'day' and 'range' types). + * @param end The end date for filtering transactions (used in 'range' type). + * @param flag The flag representing the transaction type to filter (e.g., 'b' for buy transactions). + * @return An ArrayList of transactions filtered based on the specified criteria. + */ + public static ArrayList getFilteredTransactionList(String type, LocalDate start, LocalDate end, + String flag) { + ArrayList filteredList= new ArrayList<>(); + LocalDate currDate = LocalDate.now(); + switch (type) { + case TODAY: + getDayTransactionList(currDate, flag, filteredList); + break; + case TOTAL: + getTotalTransactionList(flag, filteredList); + break; + case DAY: + getDayTransactionList(start, flag, filteredList); + break; + case RANGE: + getRangeTransactionList(start, end, flag, filteredList); + break; + default: + assert type.isEmpty(); + break; + } + return filteredList; + } + //@@author + + // @@ author dtaywd + /** + * Retrieves all transactions of a specific type and adds them to the filtered list. + * + * @param flag The flag representing the transaction type to filter. + * @param filteredList The list to which filtered transactions will be added. + */ + private static void getTotalTransactionList(String flag, ArrayList filteredList) { + for (Transaction transaction : transactionList) { + String transactionType = transaction.getType(); + if (transactionType.equals(flag)) { + filteredList.add(transaction); + } + } + } + //@@author + + // @@ author dtaywd + /** + * Retrieves transactions occurring on a specific day of a given type and adds them to the filtered list. + * + * @param start The date to filter transactions for. + * @param flag The flag representing the transaction type to filter. + * @param filteredList The list to which filtered transactions will be added. + */ + private static void getDayTransactionList(LocalDate start, String flag, ArrayList filteredList) { + for (Transaction transaction : transactionList) { + LocalDate transactionDate = transaction.getTransactionDate(); + String transactionType = transaction.getType(); + if (transactionType.equals(flag) && transactionDate.isEqual(start)) { + filteredList.add(transaction); + } + } + } + //@@author + + // @@ author dtaywd + /** + * Retrieves transactions occurring within a specified date range of a given type + * and adds them to the filtered list. + * + * @param start The start date of the date range to filter transactions for (not inclusive). + * @param end The end date of the date range to filter transactions for (not inclusive). + * @param flag The flag representing the transaction type to filter. + * @param filteredList The list to which filtered transactions will be added. + */ + private static void getRangeTransactionList(LocalDate start, LocalDate end, String flag, + ArrayList filteredList) { + for (Transaction transaction : transactionList) { + LocalDate transactionDate = transaction.getTransactionDate(); + String transactionType = transaction.getType(); + if (transactionType.equals(flag) && transactionDate.isBefore(end) && transactionDate.isAfter(start)) { + filteredList.add(transaction); + } + } + } + //@@author +} + diff --git a/src/main/java/supertracker/parser/Parser.java b/src/main/java/supertracker/parser/Parser.java new file mode 100644 index 0000000000..738b723789 --- /dev/null +++ b/src/main/java/supertracker/parser/Parser.java @@ -0,0 +1,1315 @@ +package supertracker.parser; + +import supertracker.TrackerException; + +import supertracker.command.AddCommand; +import supertracker.command.BuyCommand; +import supertracker.command.ClearCommand; +import supertracker.command.Command; +import supertracker.command.DeleteCommand; +import supertracker.command.ExpenditureCommand; +import supertracker.command.FindCommand; +import supertracker.command.HelpCommand; +import supertracker.command.InvalidCommand; +import supertracker.command.ListCommand; +import supertracker.command.NewCommand; +import supertracker.command.ProfitCommand; +import supertracker.command.QuitCommand; +import supertracker.command.RemoveCommand; +import supertracker.command.RenameCommand; +import supertracker.command.ReportCommand; +import supertracker.command.RevenueCommand; +import supertracker.command.SellCommand; +import supertracker.command.UpdateCommand; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; +import supertracker.util.Triple; + +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Parser { + private static final DateTimeFormatter EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final DateTimeFormatter NULL_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyyy"); + private static final LocalDate UNDEFINED_DATE = LocalDate.parse("01-01-99999", NULL_DATE_FORMAT); + private static final int FIRST_PARAM_INDEX = 0; + private static final int SECOND_PARAM_INDEX = 1; + private static final int THIRD_PARAM_INDEX = 2; + private static final int PARAM_POS_START = 1; + private static final int PARAM_POS_END = 2; + private static final int SORT_PARAM_POS_START = 2; + private static final int SORT_PARAM_POS_END = 3; + private static final int MAX_INT_LENGTH = 10; + private static final double ROUNDING_FACTOR = 100.0; + private static final String EMPTY_STRING = ""; + private static final String SPACE = " "; + private static final String QUIT_COMMAND = "quit"; + private static final String NEW_COMMAND = "new"; + private static final String LIST_COMMAND = "list"; + private static final String HELP_COMMAND = "help"; + private static final String UPDATE_COMMAND = "update"; + private static final String DELETE_COMMAND = "delete"; + private static final String ADD_COMMAND = "add"; + private static final String REMOVE_COMMAND = "remove"; + private static final String FIND_COMMAND = "find"; + private static final String REPORT_COMMAND = "report"; + private static final String BUY_COMMAND = "buy"; + private static final String SELL_COMMAND = "sell"; + private static final String CLEAR_COMMAND = "clear"; + private static final String RENAME_COMMAND = "rename"; + private static final String EXPENDITURE_COMMAND = "exp"; + private static final String REVENUE_COMMAND = "rev"; + private static final String PROFIT_COMMAND = "profit"; + private static final String BASE_FLAG = "/"; + private static final String NAME_FLAG = "n"; + private static final String NEW_NAME_FLAG = "r"; + private static final String QUANTITY_FLAG = "q"; + private static final String PRICE_FLAG = "p"; + private static final String EX_DATE_FLAG = "e"; + private static final String BEFORE_DATE_FLAG = "b"; + private static final String SORT_QUANTITY_FLAG = "sq"; + private static final String SORT_PRICE_FLAG = "sp"; + private static final String SORT_EX_DATE_FLAG = "se"; + private static final String REVERSE_FLAG = "r"; + private static final String NAME_GROUP = "name"; + private static final String NEW_NAME_GROUP = "rename"; + private static final String QUANTITY_GROUP = "quantity"; + private static final String PRICE_GROUP = "price"; + private static final String EX_DATE_GROUP = "expiry"; + private static final String BEFORE_DATE_GROUP = "before"; + private static final String SORT_QUANTITY_GROUP = "sortQuantity"; + private static final String SORT_PRICE_GROUP = "sortPrice"; + private static final String SORT_EX_DATE_GROUP = "sortExpiry"; + private static final String REVERSE_GROUP = "reverse"; + private static final String REPORT_TYPE_FLAG = "r"; + private static final String REPORT_TYPE_GROUP = "reportType"; + private static final String THRESHOLD_FLAG = "t"; + private static final String THRESHOLD_GROUP = "threshold"; + private static final String TYPE_FLAG = "type"; + private static final String TYPE_GROUP = "type"; + private static final String TO_FLAG = "to"; + private static final String TO_GROUP = "to"; + private static final String FROM_FLAG = "from"; + private static final String FROM_GROUP = "from"; + private static final String TODAY = "today"; + private static final String TOTAL = "total"; + private static final String DAY = "day"; + private static final String RANGE = "range"; + + // Do note that the file delimiter constant needs to follow the separator constant in the FileManager class + private static final String FILE_DELIMITER = ",,,"; + + // To be used in getPatternMatcher to split the input into its respective parameter groups + private static final String NEW_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + QUANTITY_FLAG + BASE_FLAG + "(?<" + QUANTITY_GROUP + ">.*) " + + PRICE_FLAG + BASE_FLAG + "(?<" + PRICE_GROUP + ">.*) " + + "(?<" + EX_DATE_GROUP + ">(?:" + EX_DATE_FLAG + BASE_FLAG + ".*)?) "; + private static final String UPDATE_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + "(?<" + QUANTITY_GROUP + ">(?:" + QUANTITY_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + PRICE_GROUP + ">(?:" + PRICE_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + EX_DATE_GROUP + ">(?:" + EX_DATE_FLAG + BASE_FLAG + ".*)?) "; + private static final String LIST_COMMAND_REGEX = "(?<" + QUANTITY_GROUP + ">(?:" + QUANTITY_FLAG + BASE_FLAG + + ".*)?) (?<" + PRICE_GROUP + ">(?:" + PRICE_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + EX_DATE_GROUP + ">(?:" + EX_DATE_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + SORT_QUANTITY_GROUP + ">(?:" + SORT_QUANTITY_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + SORT_PRICE_GROUP + ">(?:" + SORT_PRICE_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + SORT_EX_DATE_GROUP + ">(?:" + SORT_EX_DATE_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + REVERSE_GROUP + ">(?:" + REVERSE_FLAG + BASE_FLAG + ".*)?) "; + private static final String DELETE_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) "; + private static final String ADD_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + QUANTITY_FLAG + BASE_FLAG + "(?<" + QUANTITY_GROUP + ">.*) "; + private static final String REMOVE_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + QUANTITY_FLAG + BASE_FLAG + "(?<" + QUANTITY_GROUP + ">.*) "; + private static final String FIND_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) "; + private static final String REPORT_COMMAND_REGEX = REPORT_TYPE_FLAG + BASE_FLAG + "(?<" + REPORT_TYPE_GROUP + + ">.*) " + "(?<" + THRESHOLD_GROUP + ">(?:" + THRESHOLD_FLAG + BASE_FLAG + ".*)?) "; + private static final String BUY_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + QUANTITY_FLAG + BASE_FLAG + "(?<" + QUANTITY_GROUP + ">.*) " + + PRICE_FLAG + BASE_FLAG + "(?<" + PRICE_GROUP + ">.*) "; + private static final String SELL_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + QUANTITY_FLAG + BASE_FLAG + "(?<" + QUANTITY_GROUP + ">.*) "; + private static final String CLEAR_COMMAND_REGEX = "(?<" + BEFORE_DATE_GROUP + ">(?:" + + BEFORE_DATE_FLAG + BASE_FLAG + ".*)?) "; + private static final String EXP_COMMAND_REGEX = TYPE_FLAG + BASE_FLAG + "(?<" + TYPE_GROUP + ">.*) " + + "(?<" + FROM_GROUP + ">(?:" + FROM_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + TO_GROUP + ">(?:" + TO_FLAG + BASE_FLAG + ".*)?) "; + private static final String REV_COMMAND_REGEX = TYPE_FLAG + BASE_FLAG + "(?<" + TYPE_GROUP + ">.*) " + + "(?<" + FROM_GROUP + ">(?:" + FROM_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + TO_GROUP + ">(?:" + TO_FLAG + BASE_FLAG + ".*)?) "; + private static final String PROFIT_COMMAND_REGEX = TYPE_FLAG + BASE_FLAG + "(?<" + TYPE_GROUP + ">.*) " + + "(?<" + FROM_GROUP + ">(?:" + FROM_FLAG + BASE_FLAG + ".*)?) " + + "(?<" + TO_GROUP + ">(?:" + TO_FLAG + BASE_FLAG + ".*)?) "; + private static final String RENAME_COMMAND_REGEX = NAME_FLAG + BASE_FLAG + "(?<" + NAME_GROUP + ">.*) " + + NEW_NAME_FLAG + BASE_FLAG + "(?<" + NEW_NAME_GROUP + ">.*) "; + + /** + * Returns the command word specified in the user input string + * + * @param input a String of the user's input + * @return a String of the first word in the user input + */ + private static String getCommandWord(String input) { + if (!input.contains(SPACE)) { + return input; + } + return input.substring(0, input.indexOf(SPACE)); + } + + /** + * Returns the string of parameters right after the first word separated by white space in the user's input + * + * @param input a String of the user's input + * @return a String of the parameters in the user input + */ + private static String getParameters(String input) { + if (!input.contains(SPACE)) { + return EMPTY_STRING; + } + return input.substring(input.indexOf(SPACE)).trim(); + } + + /** + * Parses a Command accordingly from the user input string + * + * @param input a String of the user's input + * @return a Command to execute + */ + public static Command parseCommand(String input) throws TrackerException { + String commandWord = getCommandWord(input); + String params = getParameters(input); + + Command command; + switch (commandWord) { + case QUIT_COMMAND: + command = new QuitCommand(); + break; + case HELP_COMMAND: + command = new HelpCommand(); + break; + case NEW_COMMAND: + command = parseNewCommand(params); + break; + case LIST_COMMAND: + command = parseListCommand(params); + break; + case UPDATE_COMMAND: + command = parseUpdateCommand(params); + break; + case DELETE_COMMAND: + command = parseDeleteCommand(params); + break; + case ADD_COMMAND: + command = parseAddCommand(params); + break; + case REMOVE_COMMAND: + command = parseRemoveCommand(params); + break; + case FIND_COMMAND: + command = parseFindCommand(params); + break; + case REPORT_COMMAND: + command = parseReportCommand(params); + break; + case BUY_COMMAND: + command = parseBuyCommand(params); + break; + case SELL_COMMAND: + command = parseSellCommand(params); + break; + case CLEAR_COMMAND: + command = parseClearCommand(params); + break; + case RENAME_COMMAND: + command = parseRenameCommand(params); + break; + case EXPENDITURE_COMMAND: + command = parseExpenditureCommand(params); + break; + case REVENUE_COMMAND: + command = parseRevenueCommand(params); + break; + case PROFIT_COMMAND: + command = parseProfitCommand(params); + break; + default: + command = new InvalidCommand(); + break; + } + return command; + } + + /** + * Returns a String in the format of a regex expression pattern for parsing of command inputs. The format depends + * on the order of the flags in the input paramFlags String array. The inputParams "q/28 n/name n/nine" with the + * paramFlags {n, q}, for example, will return a String "n/name q/28 " accordingly. + * + * @param inputParams a String of the input parameters + * @param paramFlags a String array with the specified flags to split the input parameters + * @return a String of the input parameters in the format of a regex expression specified by the input flags + */ + private static String makeStringPattern(String inputParams, String[] paramFlags) { + // Build the regex to split the inputParam String + StringBuilder flagBuilder = new StringBuilder(); + for (String flag : paramFlags) { + flagBuilder.append(flag); + flagBuilder.append("|"); + } + flagBuilder.deleteCharAt(flagBuilder.length() - 1); + String flags = flagBuilder.toString(); + + String[] params = inputParams.split("(?= (" + flags + ")" + BASE_FLAG + ")"); + StringBuilder stringPattern = new StringBuilder(); + + for (String paramFlag : paramFlags) { + for (String p : params) { + if (p.trim().startsWith(paramFlag + BASE_FLAG)) { + stringPattern.append(p.trim()); + break; + } + } + stringPattern.append(SPACE); + } + + return stringPattern.toString(); + } + + /** + * Creates a relevant pattern string from the user's input parameters and matches the string to a regular + * expression, returning a new Matcher object. + * + * @param regex the regular expression for any specific command input + * @param input a String of the user's input parameters + * @param paramFlags a String array with the specified flags to split the input parameters + * @return a Matcher object that will check for a match between the user's input parameters and the relevant regex + */ + private static Matcher getPatternMatcher(String regex, String input, String[] paramFlags) { + Pattern p = Pattern.compile(regex); + String commandPattern = makeStringPattern(input, paramFlags); + assert commandPattern.length() >= paramFlags.length; + return p.matcher(commandPattern); + } + + /** + * Rounds a double value to 2 decimal places. + * + * @param unroundedValue Value to be rounded. + * @return Rounded value. + */ + private static double roundTo2Dp(double unroundedValue) { + return Math.round(unroundedValue * ROUNDING_FACTOR) / ROUNDING_FACTOR; + } + + /** + * Validates if the quantity is positive. + * + * @param quantityString String representation of quantity + * @param quantity Quantity to validate. + * @throws TrackerException If quantity is not positive. + */ + private static void validatePositiveQuantity(String quantityString, int quantity) throws TrackerException { + if (!quantityString.isEmpty() && quantity <= 0) { + throw new TrackerException(ErrorMessage.QUANTITY_NOT_POSITIVE); + } + } + + /** + * Validates if the quantity is non-negative. + * + * @param quantityString String representation of quantity + * @param quantity Quantity to validate. + * @throws TrackerException If quantity is negative. + */ + private static void validateNonNegativeQuantity(String quantityString, int quantity) throws TrackerException { + if (!quantityString.isEmpty() && quantity < 0) { + throw new TrackerException(ErrorMessage.QUANTITY_TOO_SMALL); + } + } + + /** + * Validates if the price is non-negative. + * + * @param priceString String representation of price + * @param price Price to validate. + * @throws TrackerException If price is negative. + */ + private static void validateNonNegativePrice(String priceString, double price) throws TrackerException { + if (!priceString.isEmpty() && price < 0) { + throw new TrackerException(ErrorMessage.PRICE_TOO_SMALL); + } + } + + /** + * Validates if the string contains only digits. + * + * @param string String to validate. + * @throws TrackerException If the string does not contain only digits. + */ + private static void validateContainsOnlyDigits(String string) throws TrackerException { + String regex = "\\d+"; + Pattern pattern = Pattern.compile(regex); + if (!pattern.matcher(string).matches()) { + throw new TrackerException(ErrorMessage.QUANTITY_NOT_INTEGER); + } + } + + /** + * Validates if the string represents a number smaller than or equal to the maximum integer value. + * + * @param string String to validate. + * @throws TrackerException If the string represents a number larger than the maximum integer value. + */ + private static void validateNotTooLarge(String string) throws TrackerException { + String maxIntString = String.valueOf(Integer.MAX_VALUE); + if (string.length() > MAX_INT_LENGTH + || (string.length() == MAX_INT_LENGTH && string.compareTo(maxIntString) > 0)) { + throw new TrackerException(ErrorMessage.QUANTITY_TOO_LARGE); + } + } + + /** + * Parses the quantity from a string representation. + * + * @param quantityString String representation of the quantity. + * @return Parsed quantity. + * @throws TrackerException If the quantity string is invalid. + */ + private static int parseQuantity(String quantityString) throws TrackerException { + int quantity = -1; + try { + if (!quantityString.isEmpty()) { + validateContainsOnlyDigits(quantityString); + validateNotTooLarge(quantityString); + quantity = Integer.parseInt(quantityString); + } + return quantity; + } catch (NumberFormatException e) { + throw new TrackerException(ErrorMessage.INVALID_QUANTITY_FORMAT); + } + } + + /** + * Parses the price from a string representation. + * + * @param priceString String representation of the price. + * @return Parsed price. + * @throws TrackerException If the price string is invalid. + */ + private static double parsePrice(String priceString) throws TrackerException { + double price = -1; + try { + if (!priceString.isEmpty()) { + price = roundTo2Dp(Double.parseDouble(priceString)); + } + if (price > Integer.MAX_VALUE) { + throw new TrackerException(ErrorMessage.PRICE_TOO_LARGE); + } + return price; + } catch (NumberFormatException e) { + throw new TrackerException(ErrorMessage.INVALID_PRICE_FORMAT); + } + } + + //@@author vimalapugazhan + /** + * Checks for invalid dates inputted even if they follow the correct format (e.g. 31-02-2024) by + * comparing the datesString to the string derived from the parsed date. + * + * @param date DateString that has been parsed. + * @param dateString Date that the user has inputted as a string. + * @throws TrackerException If the input date is invalid. + */ + private static void validateDate(LocalDate date, String dateString) throws TrackerException { + if (!date.format(EX_DATE_FORMAT).equals(dateString)) { + throw new TrackerException(ErrorMessage.INVALID_DATE); + } + } + //@@author + + //@@author vimalapugazhan + /** + * Parses the date inputted to a LocalDate type. + * + * @param dateString Dates inputted as string to be parsed into LocalDate type. + * @return date Parsed valid LocalDate dates. + * @throws TrackerException If dateString is inputted in the wrong format. + */ + private static LocalDate parseDate(String dateString) throws TrackerException { + LocalDate date = UNDEFINED_DATE; + try { + if (!dateString.isEmpty()) { + date = LocalDate.parse(dateString, EX_DATE_FORMAT); + validateDate(date, dateString); + } + return date; + } catch (DateTimeParseException e) { + throw new TrackerException(ErrorMessage.INVALID_DATE_FORMAT); + } + } + //@@author + + //@@author vimalapugazhan + /** + * Parses the inputted string into a valid date if inputted string contains a new date or + * sets the new expiry date as undefined if the user inputs nil (to remove the expiry date from the item) + * + * @param dateString Dates inputted as string to be parsed into LocalDate type. + * @return expiryDate The parsed LocalDate that is used in the updateCommand. + * @throws TrackerException If DateTimeParseException when the date is the wrong format + * @throws TrackerException If NumberFormatException when date does not consist of integers. + */ + private static LocalDate parseExpiryDateUpdate(String dateString) throws TrackerException { + LocalDate expiryDate = LocalDate.parse("1-1-1", DateTimeFormatter.ofPattern("y-M-d")); + + try { + if (!dateString.isEmpty()) { + if (dateString.equals("nil")) { + expiryDate = UNDEFINED_DATE; + } else { + expiryDate = LocalDate.parse(dateString, EX_DATE_FORMAT); + validateDate(expiryDate, dateString); + } + } + } catch (NumberFormatException e) { + throw new TrackerException(ErrorMessage.INVALID_NUMBER_FORMAT); + } catch (DateTimeParseException e) { + throw new TrackerException(ErrorMessage.INVALID_DATE_FORMAT); + } + return expiryDate; + } + //@@author + + /** + * Validates if an item exists in the inventory. + * + * @param name Name of the item to check. + * @param errorMessage Error message to use if the item does not exist. + * @throws TrackerException If the item does not exist in the inventory. + */ + private static void validateItemExistsInInventory(String name, String errorMessage) throws TrackerException { + if (!Inventory.contains(name)) { + throw new TrackerException(name + errorMessage); + } + } + + /** + * Validates if an item does not exist in the inventory. + * If the item name contains the file delimiter, it replaces the item name + * and prints a message to inform the user about the name change. + * + * @param name Name of the item to validate. + * @param errorMessage Error message to use if the item already exists in the inventory. + * @return Validated item name. + * @throws TrackerException If the item already exists in the inventory + */ + private static String validateItemNotInInventory(String name, String errorMessage) throws TrackerException { + String itemName = replaceDelimitersInName(name); + + if (Inventory.contains(itemName)) { + throw new TrackerException(itemName + errorMessage); + } + return itemName; + } + + private static String replaceDelimitersInName(String name) { + String itemName = name; + if (name.contains(FILE_DELIMITER)) { + while (itemName.contains(FILE_DELIMITER)) { + itemName = itemName.replace(FILE_DELIMITER, "_").trim(); + } + + Ui.printItemNameLimitation(name, FILE_DELIMITER, itemName); + } + + return itemName; + } + + /** + * Validates if the parameters for updating an item are not all empty. + * + * @param name Name of the item. + * @param quantityString String representation of the quantity. + * @param priceString String representation of the price. + * @param expiryString String representation of the expiry date. + * @throws TrackerException If all parameters are empty. + */ + private static void validateNonEmptyParamsUpdate(String name, String quantityString, String priceString, + String expiryString) + throws TrackerException { + if (name.isEmpty() || (quantityString.isEmpty() && priceString.isEmpty() && expiryString.isEmpty())) { + throw new TrackerException(ErrorMessage.EMPTY_PARAM_INPUT); + } + } + + /** + * Validates if a parameter is not empty. + * + * @param string Parameter to validate. + * @throws TrackerException If the parameter is empty. + */ + private static void validateNonEmptyParam(String string) throws TrackerException { + if (string.isEmpty()) { + throw new TrackerException(ErrorMessage.EMPTY_PARAM_INPUT); + } + } + + /** + * Adds missing parameters to the input string. + * + * @param input Input string. + * @param hasParam Indicates if the input string has an item parameter (q/, p/, e/). + * @param hasSortParam Indicates if the input string has a sort parameter (sq/, sp/, se/). + * @param flag Flag for an item parameter (q/, p/, e/). + * @param sortFlag Flag for a sort parameter (sq/, sp/, se/). + * @return Input string with missing parameters added if necessary. + */ + private static String addMissingParams( + String input, + boolean hasParam, + boolean hasSortParam, + String flag, + String sortFlag + ) { + if (!hasParam && hasSortParam) { + int index = input.indexOf(SPACE + sortFlag + BASE_FLAG); + return input.substring(0, index) + SPACE + flag + BASE_FLAG + input.substring(index); + } + return input; + } + + /** + * Retrieves the positions of parameters in the input string. + * + * @param input Input string. + * @param hasQuantity Indicates if the input string has the quantity parameter. + * @param hasPrice Indicates if the input string has the price parameter. + * @param hasExpiry Indicates if the input string has the expiry parameter. + * @param quantityFlag Flag for the quantity parameter. + * @param priceFlag Flag for the price parameter. + * @param expiryFlag Flag for the expiry parameter. + * @return List containing the positions of parameters in the input string. + */ + private static ArrayList getParamPositions(String input, boolean hasQuantity, boolean hasPrice, + boolean hasExpiry, String quantityFlag, String priceFlag, String expiryFlag) { + ArrayList paramPositions = new ArrayList<>(); + // to check if p, q, e appears first, second or third + + int quantityPosition; + int pricePosition; + int expiryPosition; + + if (hasQuantity) { + quantityPosition = input.indexOf(SPACE + quantityFlag + BASE_FLAG); + paramPositions.add(quantityPosition); + } + if (hasPrice) { + pricePosition = input.indexOf(SPACE + priceFlag + BASE_FLAG); + paramPositions.add(pricePosition); + } + if (hasExpiry) { + expiryPosition = input.indexOf(SPACE + expiryFlag + BASE_FLAG); + paramPositions.add(expiryPosition); + } + + Collections.sort(paramPositions); + assert paramPositions.size() <= 3; + + return paramPositions; + } + + /** + * Extracts a parameter flag from the input string. + * + * @param input Input string. + * @param paramOrder List containing the positions of parameters in the input string. + * @param index Index of the parameter to extract. + * @param isSort Indicates if the parameter is a sorting parameter. + * @return Extracted parameter flag. + */ + private static String extractParam(String input, ArrayList paramOrder, int index, boolean isSort) { + try { + int paramPos = paramOrder.get(index); + if (isSort) { + return input.substring(paramPos + SORT_PARAM_POS_START, paramPos + SORT_PARAM_POS_END); + } + return input.substring(paramPos + PARAM_POS_START, paramPos + PARAM_POS_END); + } catch (IndexOutOfBoundsException | NullPointerException ignored) { + return EMPTY_STRING; + } + } + + //@@author dtaywd + /** + * Validates the input parameters for the report command to ensure they are not empty. + * + * @param reportType The type of report to generate ("low stock" or "expiry"). + * @param thresholdString The threshold value as a string (for "low stock" report). + * @throws TrackerException If the input parameters are empty or invalid. + */ + private static void validateNonEmptyParamsReport(String reportType, String thresholdString) + throws TrackerException { + if (reportType.isEmpty() || (reportType.equals("low stock") && thresholdString.isEmpty())) { + throw new TrackerException(ErrorMessage.EMPTY_PARAM_INPUT); + } + } + + /** + * Validates the format of the report command parameters. + * + * @param reportType The type of report to generate ("low stock" or "expiry"). + * @param thresholdString The threshold value as a string (for "low stock" report). + * @throws TrackerException If the report format is invalid for the specified report type. + */ + private static void validateReportFormat(String reportType, String thresholdString) throws TrackerException { + if (reportType.equals("expiry") && !thresholdString.isEmpty()) { + throw new TrackerException(ErrorMessage.INVALID_EXPIRY_REPORT_FORMAT); + } + } + + /** + * Validates the report type to ensure it is either "expiry" or "low stock". + * + * @param reportType The type of report to generate ("expiry" or "low stock"). + * @throws TrackerException If the report type is invalid. + */ + private static void validateReportType(String reportType) throws TrackerException { + if (!reportType.equals("expiry") && !reportType.equals("low stock")) { + throw new TrackerException(ErrorMessage.INVALID_REPORT_TYPE); + } + } + //@@author + + /** + * Validates if adding the specified quantity to an item will result in integer overflow. + * + * @param name Name of the item. + * @param quantityToAdd Quantity to be added. + * @throws TrackerException If adding the quantity would result in an integer overflow. + */ + private static void validateNoIntegerOverflow(String name, int quantityToAdd) throws TrackerException { + Item item = Inventory.get(name); + int currentQuantity = item.getQuantity(); + int maximumPossibleAddQuantity = Integer.MAX_VALUE - currentQuantity; + if (quantityToAdd > maximumPossibleAddQuantity) { + throw new TrackerException(ErrorMessage.INTEGER_OVERFLOW); + } + } + + //@@author vimalapugazhan + /** + * Checks for invalid inputs for each taskType by + * ensuring the params for each type is present and the params are valid. + * + * @param taskType The task type (e.g., "today", "total", "day", "range"). + * @param hasStart Whether a start date flag is present. + * @param hasEnd Whether an end date flag is present. + * @param command Revenue, Expenditure or Profit command that requires checking of format. + * @param hasStartParam The string inputted after the start date flag is not empty. + * @param hasEndParam The string inputted after the end date flag is not empty. + * @throws TrackerException If the methods called in this method throws TrackerException. + */ + private static void validateExpRevProfitFormat( + String taskType, + boolean hasStart, + boolean hasEnd, + String command, + boolean hasStartParam, + boolean hasEndParam + ) throws TrackerException { + switch (taskType) { + case TODAY: + todayErrorFormat(hasStart, hasEnd, command); + break; + case TOTAL: + totalErrorFormat(hasStart, hasEnd, command); + break; + case DAY: + dayErrorFormat(hasStart, hasEnd, command, hasStartParam); + break; + case RANGE: + rangeErrorFormat(hasStart, hasEnd, command, hasStartParam, hasEndParam); + break; + default: + handleInvalidFormat(command); + break; + } + } + //@@author + + //@@author vimalapugazhan + private static Triple parseExpRevProfit(Matcher matcher, String command) + throws TrackerException { + boolean hasFrom = !matcher.group(FROM_GROUP).isEmpty(); + boolean hasTo = !matcher.group(TO_GROUP).isEmpty(); + + String type = matcher.group(TYPE_GROUP).trim(); + String fromString = matcher.group(FROM_GROUP).replace(FROM_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + String toString = matcher.group(TO_GROUP).replace(TO_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + + boolean hasStartParam = !fromString.isEmpty(); + boolean hasEndParam = !toString.isEmpty(); + + validateExpRevProfitFormat(type, hasFrom, hasTo, command, hasStartParam, hasEndParam); + LocalDate to = parseDate(toString); + LocalDate from = parseDate(fromString); + + return new Triple<>(type, from, to); + } + //@@author + + //@@author dtaywd + /** + * Throws a TrackerException if the specified command has invalid format for "today" task. + * + * @param hasStart Whether a start date flag is present. + * @param hasEnd Whether an end date flag is present. + * @param command The type of command (e.g., EXPENDITURE_COMMAND or REVENUE_COMMAND). + * @throws TrackerException If the command format is invalid for the "today" task. + */ + private static void todayErrorFormat(boolean hasStart, boolean hasEnd, String command) throws TrackerException { + if (hasStart || hasEnd) { + switch (command) { + case EXPENDITURE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_EXP_TODAY_FORMAT); + case REVENUE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_REV_TODAY_FORMAT); + case PROFIT_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_PROFIT_TODAY_FORMAT); + default: + + } + } + } + + /** + * Throws a TrackerException if the specified command has invalid format for "total" task. + * + * @param hasStart Whether a start date flag is present. + * @param hasEnd Whether an end date flag is present. + * @param command The type of command (e.g., EXPENDITURE_COMMAND or REVENUE_COMMAND). + * @throws TrackerException If the command format is invalid for the "total" task. + */ + private static void totalErrorFormat(boolean hasStart, boolean hasEnd, String command) throws TrackerException { + if (hasStart || hasEnd) { + switch (command) { + case EXPENDITURE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_EXP_TOTAL_FORMAT); + case REVENUE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_REV_TOTAL_FORMAT); + case PROFIT_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_PROFIT_TOTAL_FORMAT); + default: + + } + } + } + + /** + * Throws a TrackerException if the specified command has invalid format for "day" task. + * + * @param hasStart Whether a start date flag is present. + * @param hasEnd Whether an end date flag is present. + * @param command The type of command (e.g., EXPENDITURE_COMMAND or REVENUE_COMMAND). + * @param hasStartParam Whether the start date parameter is provided and valid. + * @throws TrackerException If the command format is invalid for the "day" task. + */ + private static void dayErrorFormat( + boolean hasStart, + boolean hasEnd, + String command, + boolean hasStartParam) + throws TrackerException { + if (!hasStart || hasEnd) { + switch (command) { + case EXPENDITURE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_EXP_DAY_FORMAT); + case REVENUE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_REV_DAY_FORMAT); + case PROFIT_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_PROFIT_DAY_FORMAT); + default: + + } + } else if (!hasStartParam) { + throw new TrackerException(ErrorMessage.EMPTY_PARAM_INPUT); + } + } + + /** + * Throws a TrackerException if the specified command has invalid format for "range" task. + * + * @param hasStart Whether a start date flag is present. + * @param hasEnd Whether an end date flag is present. + * @param command The type of command (e.g., EXPENDITURE_COMMAND or REVENUE_COMMAND). + * @param hasStartParam Whether the start date parameter is provided and valid. + * @param hasEndParam Whether the end date parameter is provided and valid. + * @throws TrackerException If the command format is invalid for the "range" task. + */ + private static void rangeErrorFormat( + boolean hasStart, + boolean hasEnd, + String command, + boolean hasStartParam, + boolean hasEndParam) + throws TrackerException { + if (!hasStart || !hasEnd) { + switch (command) { + case EXPENDITURE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_EXP_RANGE_FORMAT); + case REVENUE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_REV_RANGE_FORMAT); + case PROFIT_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_PROFIT_RANGE_FORMAT); + default: + break; + } + } else if (!hasStartParam || !hasEndParam) { + throw new TrackerException(ErrorMessage.EMPTY_PARAM_INPUT); + } + } + + /** + * Throws a TrackerException for handling an invalid command format. + * + * @param command The type of command (e.g., EXPENDITURE_COMMAND or REVENUE_COMMAND). + * @throws TrackerException If the command format is invalid. + */ + private static void handleInvalidFormat(String command) throws TrackerException { + switch (command) { + case EXPENDITURE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_EXP_FORMAT); + case REVENUE_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_REV_FORMAT); + case PROFIT_COMMAND: + throw new TrackerException(ErrorMessage.INVALID_PROFIT_FORMAT); + default: + + } + } + //@@author + + private static Command parseNewCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG, PRICE_FLAG, EX_DATE_FLAG}; + Matcher matcher = getPatternMatcher(NEW_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_NEW_ITEM_FORMAT); + } + + String nameInput = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).trim(); + String priceString = matcher.group(PRICE_GROUP).trim(); + String dateString = matcher.group(EX_DATE_GROUP).trim().replace(EX_DATE_FLAG + BASE_FLAG, EMPTY_STRING); + + validateNonEmptyParam(nameInput); + validateNonEmptyParam(quantityString); + validateNonEmptyParam(priceString); + String name = validateItemNotInInventory(nameInput, ErrorMessage.ITEM_IN_LIST_NEW); + + int quantity = parseQuantity(quantityString); + double price = parsePrice(priceString); + LocalDate expiryDate = parseDate(dateString); + + validateNonNegativeQuantity(quantityString, quantity); + validateNonNegativePrice(priceString, price); + + return new NewCommand(name, quantity, price, expiryDate); + } + + //@@author dtaywd + /** + * Parses the input string to create an UpdateCommand based on the specified format. + * + * @param input The input string containing the update command. + * @return An UpdateCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the update command. + */ + private static Command parseUpdateCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG, PRICE_FLAG, EX_DATE_FLAG}; + Matcher matcher = getPatternMatcher(UPDATE_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_UPDATE_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).replace(QUANTITY_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + String priceString = matcher.group(PRICE_GROUP).replace(PRICE_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + String dateString = matcher.group(EX_DATE_GROUP).replace(EX_DATE_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + + validateNonEmptyParamsUpdate(name, quantityString, priceString, dateString); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_UPDATE); + + int quantity = parseQuantity(quantityString); + double price = parsePrice(priceString); + LocalDate expiryDate = parseExpiryDateUpdate(dateString); + + validateNonNegativeQuantity(quantityString, quantity); + validateNonNegativePrice(priceString, price); + + return new UpdateCommand(name, quantity, price, expiryDate); + } + //@@author + + //@@author TimothyLKM + /** + * Parses the input string to create a RenameCommand. + * + * @param input The input string containing the rename command. + * @return A rename Command object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the rename command. + */ + private static Command parseRenameCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, NEW_NAME_FLAG}; + Matcher matcher = getPatternMatcher(RENAME_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_RENAME_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + validateNonEmptyParam(name); + String newName = matcher.group(NEW_NAME_GROUP).trim(); + validateNonEmptyParam(newName); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_RENAME); + newName = replaceDelimitersInName(newName); + + if (!newName.equalsIgnoreCase(name)) { + newName = validateItemNotInInventory(newName, ErrorMessage.ITEM_IN_LIST_RENAME); + } + return new RenameCommand(name, newName); + } + //@@author + + private static Command parseListCommand(String input) throws TrackerException { + String[] flags = { + QUANTITY_FLAG, + PRICE_FLAG, + EX_DATE_FLAG, + SORT_QUANTITY_FLAG, + SORT_PRICE_FLAG, + SORT_EX_DATE_FLAG, + REVERSE_FLAG + }; + + Matcher matcher = getPatternMatcher(LIST_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_LIST_FORMAT); + } + + boolean hasQuantity = !matcher.group(QUANTITY_GROUP).isEmpty(); + boolean hasPrice = !matcher.group(PRICE_GROUP).isEmpty(); + boolean hasExpiry = !matcher.group(EX_DATE_GROUP).isEmpty(); + boolean hasSortQuantity = !matcher.group(SORT_QUANTITY_GROUP).isEmpty(); + boolean hasSortPrice = !matcher.group(SORT_PRICE_GROUP).isEmpty(); + boolean hasSortExpiry = !matcher.group(SORT_EX_DATE_GROUP).isEmpty(); + boolean isReverse = !matcher.group(REVERSE_GROUP).isEmpty(); + + input = SPACE + input; + + input = addMissingParams(input, hasQuantity, hasSortQuantity, QUANTITY_FLAG, SORT_QUANTITY_FLAG); + input = addMissingParams(input, hasPrice, hasSortPrice, PRICE_FLAG, SORT_PRICE_FLAG); + input = addMissingParams(input, hasExpiry, hasSortExpiry, EX_DATE_FLAG, SORT_EX_DATE_FLAG); + + Matcher updatedMatcher = getPatternMatcher(LIST_COMMAND_REGEX, input, flags); + + if (!updatedMatcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_LIST_FORMAT); + } + + hasQuantity = !updatedMatcher.group(QUANTITY_GROUP).isEmpty(); + hasPrice = !updatedMatcher.group(PRICE_GROUP).isEmpty(); + hasExpiry = !updatedMatcher.group(EX_DATE_GROUP).isEmpty(); + + ArrayList paramOrder = getParamPositions(input, hasQuantity, hasPrice, hasExpiry, + QUANTITY_FLAG, PRICE_FLAG, EX_DATE_FLAG); + String firstParam = extractParam(input, paramOrder, FIRST_PARAM_INDEX, false); + String secondParam = extractParam(input, paramOrder, SECOND_PARAM_INDEX, false); + String thirdParam = extractParam(input, paramOrder, THIRD_PARAM_INDEX, false); + + ArrayList sortParamOrder = getParamPositions(input, hasSortQuantity, hasSortPrice, hasSortExpiry, + SORT_QUANTITY_FLAG, SORT_PRICE_FLAG, SORT_EX_DATE_FLAG); + String firstSortParam = extractParam(input, sortParamOrder, FIRST_PARAM_INDEX, true); + String secondSortParam = extractParam(input, sortParamOrder, SECOND_PARAM_INDEX, true); + String thirdSortParam = extractParam(input, sortParamOrder, THIRD_PARAM_INDEX, true); + + return new ListCommand(firstParam, secondParam, thirdParam, + firstSortParam, secondSortParam, thirdSortParam, isReverse); + } + + //@@author vimalapugazhan + /** + * Parse input to extract the name of item being deleted to return new DeleteCommand. + * + * @param input User inputted string containing the delete command. + * @return DeleteCommand object containing the name of item being deleted. + * @throws TrackerException If item does not exist in the list. + */ + private static Command parseDeleteCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG}; + Matcher matcher = getPatternMatcher(DELETE_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_DELETE_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + + validateNonEmptyParam(name); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_DELETE); + + return new DeleteCommand(name); + } + //@@author + + private static Command parseAddCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG}; + Matcher matcher = getPatternMatcher(ADD_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_ADD_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).trim(); + + validateNonEmptyParam(name); + validateNonEmptyParam(quantityString); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_ADD); + + int quantity = parseQuantity(quantityString); + validatePositiveQuantity(quantityString, quantity); + validateNoIntegerOverflow(name, quantity); + + return new AddCommand(name,quantity); + } + + private static Command parseRemoveCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG}; + Matcher matcher = getPatternMatcher(REMOVE_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_REMOVE_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).trim(); + + validateNonEmptyParam(name); + validateNonEmptyParam(quantityString); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_REMOVE); + + int quantity = parseQuantity(quantityString); + validatePositiveQuantity(quantityString, quantity); + + return new RemoveCommand(name, quantity); + } + + //@@author TimothyLKM + /** + * Parses the input string to create FindCommand based on the specified format. + * + * @param input The input string containing the find command. + * @return A FindCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the find command. + */ + private static Command parseFindCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG}; + Matcher matcher = getPatternMatcher(FIND_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_FIND_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + + validateNonEmptyParam(name); + + return new FindCommand(name); + } + //@@author + + /** + * Parses the input string to create a ReportCommand based on the specified format. + * + * @param input The input string containing the report command. + * @return A ReportCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the report command. + */ + private static Command parseReportCommand(String input) throws TrackerException { + String[] flags = {REPORT_TYPE_FLAG, THRESHOLD_FLAG}; + Matcher matcher = getPatternMatcher(REPORT_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_REPORT_FORMAT); + } + + String reportType = matcher.group(REPORT_TYPE_GROUP).trim(); + String thresholdString = matcher.group(THRESHOLD_GROUP). + replace(THRESHOLD_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + + validateNonEmptyParamsReport(reportType, thresholdString); + validateReportFormat(reportType, thresholdString); + validateReportType(reportType); + + int threshold = -1; + if (reportType.equals("low stock")){ + threshold = parseQuantity(thresholdString); + validateNonNegativeQuantity(thresholdString, threshold); + } + return new ReportCommand(reportType, threshold); + } + + private static Command parseBuyCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG, PRICE_FLAG}; + Matcher matcher = getPatternMatcher(BUY_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_BUY_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).trim(); + String priceString = matcher.group(PRICE_GROUP).trim(); + + validateNonEmptyParam(name); + validateNonEmptyParam(quantityString); + validateNonEmptyParam(priceString); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_BUY); + + int quantity = parseQuantity(quantityString); + double price = parsePrice(priceString); + + validatePositiveQuantity(quantityString, quantity); + validateNonNegativePrice(priceString, price); + validateNoIntegerOverflow(name, quantity); + + LocalDate currentDate = LocalDate.now(); + + return new BuyCommand(name, quantity, price, currentDate); + } + + private static Command parseSellCommand(String input) throws TrackerException { + String[] flags = {NAME_FLAG, QUANTITY_FLAG}; + Matcher matcher = getPatternMatcher(SELL_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_SELL_FORMAT); + } + + String name = matcher.group(NAME_GROUP).trim(); + String quantityString = matcher.group(QUANTITY_GROUP).trim(); + + validateNonEmptyParam(name); + validateNonEmptyParam(quantityString); + validateItemExistsInInventory(name, ErrorMessage.ITEM_NOT_IN_LIST_SELL); + + int quantity = parseQuantity(quantityString); + validatePositiveQuantity(quantityString, quantity); + + LocalDate currentDate = LocalDate.now(); + + return new SellCommand(name, quantity, currentDate); + } + + private static Command parseClearCommand(String input) throws TrackerException { + String[] flags = {BEFORE_DATE_FLAG}; + Matcher matcher = getPatternMatcher(CLEAR_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_CLEAR_FORMAT); + } + + String dateString = matcher.group(BEFORE_DATE_GROUP).replace(BEFORE_DATE_FLAG + BASE_FLAG, EMPTY_STRING).trim(); + LocalDate beforeDate = parseDate(dateString); + + if (beforeDate.isEqual(UNDEFINED_DATE)) { + beforeDate = LocalDate.now(); + } + + return new ClearCommand(beforeDate); + } + + //@@author dtaywd + /** + * Parses the input string to create an ExpenditureCommand based on the specified format. + * + * @param input The input string containing the expenditure command. + * @return An ExpenditureCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the expenditure command. + */ + private static Command parseExpenditureCommand(String input) throws TrackerException { + String[] flags = {TYPE_FLAG, FROM_FLAG, TO_FLAG}; + Matcher matcher = getPatternMatcher(EXP_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_EXP_FORMAT); + } + + Triple triple = parseExpRevProfit(matcher, EXPENDITURE_COMMAND); + String type = triple.getFirst(); + LocalDate from = triple.getSecond(); + LocalDate to = triple.getThird(); + + return new ExpenditureCommand(type, from, to); + } + //@@author + + //@@author vimalapugazhan + /** + * Parses the input string to create a RevenueCommand based on the specified format. + * + * @param input The input string containing the revenue command. + * @return RevenueCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the revenue command. + */ + private static Command parseRevenueCommand(String input) throws TrackerException { + String[] flags = {TYPE_FLAG, FROM_FLAG, TO_FLAG}; + Matcher matcher = getPatternMatcher(REV_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_REV_FORMAT); + } + + Triple triple = parseExpRevProfit(matcher, REVENUE_COMMAND); + String type = triple.getFirst(); + LocalDate from = triple.getSecond(); + LocalDate to = triple.getThird(); + + return new RevenueCommand(type, from, to); + } + //@@author + + //@@author vimalapugazhan + /** + * Parses the input string to create a ProfitCommand based on the specified format. + * + * @param input The input string containing the profit command. + * @return ProfitCommand object parsed from the input string. + * @throws TrackerException If there is an error parsing or validating the profit command. + */ + private static Command parseProfitCommand(String input) throws TrackerException { + String[] flags = {TYPE_FLAG, FROM_FLAG, TO_FLAG}; + Matcher matcher = getPatternMatcher(PROFIT_COMMAND_REGEX, input, flags); + + if (!matcher.matches()) { + throw new TrackerException(ErrorMessage.INVALID_PROFIT_FORMAT); + } + + Triple triple = parseExpRevProfit(matcher, PROFIT_COMMAND); + String type = triple.getFirst(); + LocalDate from = triple.getSecond(); + LocalDate to = triple.getThird(); + + return new ProfitCommand(type, from, to); + } + //@@author +} diff --git a/src/main/java/supertracker/storage/FileManager.java b/src/main/java/supertracker/storage/FileManager.java new file mode 100644 index 0000000000..966be42df6 --- /dev/null +++ b/src/main/java/supertracker/storage/FileManager.java @@ -0,0 +1,58 @@ +package supertracker.storage; + +import supertracker.item.Item; + +import java.io.File; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class FileManager { + protected static final String DATA_PATH = "./data/"; + // Do note that the separator should also follow the file delimiter constant in the Parser class accordingly + protected static final String SEPARATOR = " ,,, "; + protected static final String PLACEHOLDER = "*&_"; + protected static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + protected static final String NO_DATE = "no date"; + protected static final DateTimeFormatter DATE_FORMAT_NULL = DateTimeFormatter.ofPattern("dd-MM-yyyyy"); + protected static final LocalDate UNDEFINED_DATE = LocalDate.parse("01-01-99999", DATE_FORMAT_NULL); + + /** + * Checks whether the data directory exists. + * If it does not exist, the directory will be created + */ + protected static void checkDataDirectory() { + File directory = new File(DATA_PATH); + if (!directory.exists()) { + directory.mkdirs(); + } + } + + /** + * Takes an item and returns a String array of the name, quantity and price of the item. + * The last element in the array is a string to signify the end of the array, for use in the respective data to + * string conversion methods. + * + * @param item an Item object to extract the name, quantity and price from + * @return a String array of size 4 with item name, quantity, price and excess in this order + */ + protected static String[] getNameQtyPriceStrings(Item item) { + String name = item.getName(); + String excess = "end"; + // The item name should not contain the separator, but we perform another check + // as an additional means of security. + if (name.contains(SEPARATOR)) { + excess = "bad end"; + name = name.replace(SEPARATOR, PLACEHOLDER); + } + if (name.contains(SEPARATOR.trim())) { + excess = "worse end"; + name = name.replace(SEPARATOR.trim(), PLACEHOLDER); + } + + String quantity = String.valueOf(item.getQuantity()); + String price = String.valueOf(item.getPrice()); + + return new String[]{name, quantity, price, excess}; + } +} diff --git a/src/main/java/supertracker/storage/ItemStorage.java b/src/main/java/supertracker/storage/ItemStorage.java new file mode 100644 index 0000000000..24ec83e107 --- /dev/null +++ b/src/main/java/supertracker/storage/ItemStorage.java @@ -0,0 +1,157 @@ +package supertracker.storage; + +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; +import java.util.Scanner; +import java.util.HashSet; + +public class ItemStorage extends FileManager { + protected static final String SAVE_FILE_NAME = "items.txt"; + protected static final String FILE_PATH = DATA_PATH + SAVE_FILE_NAME; + protected static final int MAX_NUMBER_OF_PARAMS = 5; + protected static final int NAME_INDEX = 0; + protected static final int QUANTITY_INDEX = 1; + protected static final int PRICE_INDEX = 2; + protected static final int DATE_INDEX = 3; + protected static final int EXTRA_INDEX = 3; + + /** + * Saves all items currently in the inventory by writing into a text file. + * + * @throws IOException if text file cannot be opened or accessed for whatever reason + */ + public static void saveData() throws IOException { + checkDataDirectory(); + File saveFile = new File(FILE_PATH); + if (!saveFile.createNewFile()) { + saveFile.delete(); + saveFile.createNewFile(); + } + + List items = Inventory.getItems(); + FileWriter fw = new FileWriter(saveFile); + BufferedWriter writer = new BufferedWriter(fw); + + for (Item item : items) { + String itemData = getItemData(item); + writer.write(itemData); + } + + writer.close(); + fw.close(); + } + + /** + * Loads and reads item data from a designated text file from the path specified in the class. + * Parses each line of data into an Item class and adds to the item list in the Inventory class. + * If data is corrupted, prints to the UI the number of corrupted lines. + * + * @throws IOException if specified path is unable to be opened or found + */ + public static void loadData() throws IOException { + checkDataDirectory(); + + File saveFile = new File(FILE_PATH); + if (!saveFile.exists()) { + return; + } + + Inventory.clear(); + Scanner fileScanner = new Scanner(saveFile); + String itemData; + + HashSet duplicates = new HashSet<>(); + boolean hasCorruptedData = false; + while (fileScanner.hasNext()) { + try { + itemData = fileScanner.nextLine(); + Item item = parseItemData(itemData); + if (Inventory.contains(item.getName())) { + duplicates.add(item.getName().toLowerCase()); + } + Inventory.put(item.getName(), item); + } catch (Exception e) { + hasCorruptedData = true; + } + } + fileScanner.close(); + + if (hasCorruptedData) { + Ui.printError(ErrorMessage.ITEM_FILE_CORRUPTED_ERROR); + saveData(); + } + if (!duplicates.isEmpty()) { + Ui.printDuplicatesInSavefile(duplicates); + saveData(); + } + } + + /** + * Takes an Item object and converts its attributes to a String to be saved in a data file. + * String is in the format of "(name) DELIMITER (qty) DELIMITER (price) DELIMITER (expiry date) DELIMITER end" + * + * @param item an Item object to convert its attributes to a String + * @return a String containing the Item object's attributes + */ + private static String getItemData(Item item) { + String[] itemDataStrings = getNameQtyPriceStrings(item); + assert itemDataStrings.length == 4; + + String name = itemDataStrings[NAME_INDEX]; + String excess = itemDataStrings[EXTRA_INDEX]; + String quantity = itemDataStrings[QUANTITY_INDEX]; + String price = itemDataStrings[PRICE_INDEX]; + + LocalDate exDate = item.getExpiryDate(); + String date = NO_DATE; + if (!exDate.isEqual(UNDEFINED_DATE)) { + date = exDate.format(DATE_FORMAT); + } + + return name + SEPARATOR + quantity + SEPARATOR + price + SEPARATOR + + date + SEPARATOR + excess + System.lineSeparator(); + } + + /** + * Takes string data from a line extracted from the data file and parses it to an Item object + * + * @param itemData a String containing the data of an Item object's attributes + * @return an Item object parsed from the data string + * @throws Exception if the relevant attributes are unable to be extracted from the data string or the attributes + * are invalid (i.e. the quantity is negative) + */ + private static Item parseItemData(String itemData) throws Exception { + String[] data = itemData.split(SEPARATOR, MAX_NUMBER_OF_PARAMS); + + if (data.length < MAX_NUMBER_OF_PARAMS) { + throw new Exception(); + } + assert data.length == MAX_NUMBER_OF_PARAMS; + + String name = data[NAME_INDEX].trim(); + + int quantity; + double price; + quantity = Integer.parseInt(data[QUANTITY_INDEX].trim()); + price = Double.parseDouble(data[PRICE_INDEX].trim()); + if (quantity < 0 || price < 0) { + throw new Exception(); + } + + LocalDate date = UNDEFINED_DATE; + if (!data[DATE_INDEX].equals(NO_DATE)) { + date = LocalDate.parse(data[DATE_INDEX], DATE_FORMAT); + } + + return new Item(name, quantity, price, date); + } +} diff --git a/src/main/java/supertracker/storage/TransactionStorage.java b/src/main/java/supertracker/storage/TransactionStorage.java new file mode 100644 index 0000000000..0b2f219eb3 --- /dev/null +++ b/src/main/java/supertracker/storage/TransactionStorage.java @@ -0,0 +1,200 @@ +package supertracker.storage; + +import supertracker.TrackerException; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.ui.ErrorMessage; +import supertracker.ui.Ui; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.util.Scanner; + +public class TransactionStorage extends FileManager { + protected static final String SAVE_FILE_NAME = "transactions.txt"; + protected static final String FILE_PATH = DATA_PATH + SAVE_FILE_NAME; + protected static final int MAX_NUMBER_OF_PARAMS = 6; + protected static final int NAME_INDEX = 0; + protected static final int QUANTITY_INDEX = 1; + protected static final int PRICE_INDEX = 2; + protected static final int DATE_INDEX = 3; + protected static final int TYPE_INDEX = 4; + protected static final int EXTRA_INDEX = 3; + protected static final String[] PARAM_LABELS = {"NAME: ", "QTY: ", "PRICE: ", "DATE: ", "T: "}; + + /** + * Saves a new transaction by writing into a text file. If the text file does not exist, all transactional data + * currently in the system will be saved. + * + * @throws IOException if text file cannot be opened or accessed for whatever reason + */ + public static void saveTransaction(Transaction newTransaction) throws IOException { + checkDataDirectory(); + + File saveFile = new File(FILE_PATH); + + if (!saveFile.exists()) { + saveFile.createNewFile(); + saveAllTransactions(); + return; + } + + FileWriter fw = new FileWriter(saveFile, true); + BufferedWriter writer = new BufferedWriter(fw); + String newData = getTransactionData(newTransaction); + writer.write(newData); + + writer.close(); + fw.close(); + } + + /** + * Saves all transactional data currently in the transaction list. A public method to call saveAllTransactions(). + * + * @throws IOException if text file cannot be opened or accessed for whatever reason + */ + public static void resaveCurrentTransactions() throws IOException { + checkDataDirectory(); + + File saveFile = new File(FILE_PATH); + if (!saveFile.exists()) { + saveFile.createNewFile(); + } + saveAllTransactions(); + } + + /** + * Saves all transactional data in the transaction list. Assumes that the save file already exists. + * + * @throws IOException if text file cannot be opened or accessed for whatever reason + */ + private static void saveAllTransactions() throws IOException { + File saveFile = new File(FILE_PATH); + FileWriter fw = new FileWriter(saveFile); + BufferedWriter writer = new BufferedWriter(fw); + assert saveFile.exists(); + + int transactionSize = TransactionList.size(); + for (int i = 0; i < transactionSize; i++) { + String transactionData = getTransactionData(TransactionList.get(i)); + writer.write(transactionData); + } + + writer.close(); + fw.close(); + } + + /** + * Takes a Transaction object and converts its attributes to a String to be saved in a data file. + * String is in the format of "NAME: (name) DELIMITER QTY: (qty) DELIMITER PRICE: (price) DELIMITER DATE: (date) + * DELIMITER T: (type) DELIMITER end" + * + * @param transaction a Transaction object to convert its attributes to a String + * @return a String containing the Transaction object's attributes + */ + private static String getTransactionData(Transaction transaction) { + String[] itemDataStrings = getNameQtyPriceStrings(transaction); + assert itemDataStrings.length == 4; + + String name = itemDataStrings[NAME_INDEX]; + String excess = itemDataStrings[EXTRA_INDEX]; + String quantity = itemDataStrings[QUANTITY_INDEX]; + String price = itemDataStrings[PRICE_INDEX]; + + assert !transaction.getTransactionDate().isEqual(UNDEFINED_DATE); + String date = transaction.getTransactionDate().format(DATE_FORMAT); + + return "NAME: " + name + SEPARATOR + "QTY: " + quantity + SEPARATOR + "PRICE: " + price + + SEPARATOR + "DATE: " + date + SEPARATOR + "T: " + transaction.getType() + SEPARATOR + excess + + System.lineSeparator(); + } + + /** + * Loads and reads transaction data from a designated text file from the path specified in the class. + * Parses each line of data into a Transaction class and adds to the item list in the TransactionList class. + * If data is corrupted, prints to the UI that there are corrupted lines. + * If the transaction date parsed is a date that is larger than the current date, prints to the UI a message + * to alert the user that the date is not a possible transactional date. + * + * @throws IOException if specified path is unable to be opened or found + */ + public static void loadTransactionData() throws IOException { + checkDataDirectory(); + + File saveFile = new File(FILE_PATH); + if (!saveFile.exists()) { + return; + } + + Scanner fileScanner = new Scanner(saveFile); + String transactionData; + boolean hasCorruptedData = false; + boolean hasDateAfterToday = false; + while (fileScanner.hasNext()) { + try { + transactionData = fileScanner.nextLine(); + Transaction transaction = parseTransactionData(transactionData); + TransactionList.add(transaction); + } catch (TrackerException e) { + hasCorruptedData = true; + hasDateAfterToday = true; + } catch (Exception e) { + hasCorruptedData = true; + } + } + fileScanner.close(); + + if (hasCorruptedData) { + Ui.printError(ErrorMessage.TRANSACTION_FILE_CORRUPTED_ERROR); + if (hasDateAfterToday) { + Ui.printIndent(ErrorMessage.TRANSACTION_DATE_LOAD_ERROR); + } + saveAllTransactions(); + } + } + + /** + * Takes string data from a line extracted from the data file and parses it to a Transaction object + * + * @param transactionData a String containing the data of a Transaction object's attributes + * @return a Transaction object parsed from the data string + * @throws Exception if the relevant attributes are unable to be extracted from the data string + * @throws TrackerException if the transaction date extracted from the string data is of a date that has not + * occurred yet + */ + private static Transaction parseTransactionData(String transactionData) throws Exception { + String[] data = transactionData.split(SEPARATOR, MAX_NUMBER_OF_PARAMS); + + if (data.length < MAX_NUMBER_OF_PARAMS) { + throw new Exception(); + } + for (int i = 0; i < MAX_NUMBER_OF_PARAMS - 1; i++) { + if (!data[i].startsWith(PARAM_LABELS[i])) { + throw new Exception(); + } + data[i] = data[i].substring(data[i].indexOf(" ")).trim(); + } + + String name = data[NAME_INDEX]; + + int quantity = Integer.parseInt(data[QUANTITY_INDEX]); + double price = Double.parseDouble(data[PRICE_INDEX]); + if (quantity < 0 || price < 0) { + throw new Exception(); + } + + LocalDate transactionDate = LocalDate.parse(data[DATE_INDEX], DATE_FORMAT); + if (transactionDate.isAfter(LocalDate.now())) { + throw new TrackerException(ErrorMessage.TRANSACTION_FILE_CORRUPTED_ERROR); + } + + if (!data[TYPE_INDEX].equals("b") && !data[TYPE_INDEX].equals("s")) { + throw new Exception(); + } + + return new Transaction(name, quantity, price, transactionDate, data[TYPE_INDEX]); + } +} diff --git a/src/main/java/supertracker/ui/ErrorMessage.java b/src/main/java/supertracker/ui/ErrorMessage.java new file mode 100644 index 0000000000..df18eb1965 --- /dev/null +++ b/src/main/java/supertracker/ui/ErrorMessage.java @@ -0,0 +1,91 @@ +package supertracker.ui; + +public class ErrorMessage { + public static final String INVALID_UPDATE_FORMAT = "Invalid update command format!"; + public static final String INVALID_RENAME_FORMAT = "Invalid rename command format!"; + public static final String EMPTY_PARAM_INPUT = "Parameters cannot be left empty!"; + public static final String INVALID_DELETE_FORMAT = "Invalid delete command format!"; + public static final String INVALID_LIST_FORMAT = "Invalid list command format!"; + public static final String INVALID_NEW_ITEM_FORMAT = "Invalid new command format!"; + public static final String INVALID_ADD_FORMAT = "Invalid add command format!"; + public static final String INVALID_REMOVE_FORMAT = "Invalid remove command format!"; + public static final String INVALID_BUY_FORMAT = "Invalid buy command format!"; + public static final String INVALID_SELL_FORMAT = "Invalid sell command format!"; + public static final String INVALID_CLEAR_FORMAT = "Invalid clear command format!"; + public static final String INVALID_REPORT_FORMAT = "Invalid report command format! " + + "Follow 'report r/REPORT_TYPE [t/THRESHOLD_VALUE]'"; + public static final String INVALID_EXP_FORMAT = "Invalid expenditure command format! " + + "Follow 'exp type/EXPENDITURE_TYPE [from/START_DATE] [to/END_DATE]'"; + public static final String INVALID_REV_FORMAT = "Invalid revenue command format! " + + "Follow 'rev type/REVENUE_TYPE [from/START_DATE] [to/END_DATE]'"; + public static final String INVALID_PROFIT_FORMAT = "Invalid profit command format! " + + "Follow 'profit type/PROFIT_TYPE [from/START_DATE] [to/END_DATE]'"; + public static final String INVALID_REPORT_TYPE = "Please select a valid report type. Only 'low stock' and " + + "'expiry' are available."; + public static final String INVALID_EXPIRY_REPORT_FORMAT = "If report type is 'expiry' threshold should not be " + + "specified."; + public static final String ITEM_NOT_IN_LIST_UPDATE = + " does not exist in inventory. Unable to update its values. =("; + public static final String ITEM_NOT_IN_LIST_DELETE = + " does not exist in inventory. Unable to delete something that does not exist. =("; + public static final String ITEM_NOT_IN_LIST_ADD = + " does not exist in inventory. Unable to increase its quantity. =("; + public static final String ITEM_NOT_IN_LIST_REMOVE = + " does not exist in inventory. Unable to decrease its quantity. =("; + public static final String ITEM_NOT_IN_LIST_RENAME = + " does not exist in inventory. Unable to rename your desired item."; + public static final String ITEM_NOT_IN_LIST_BUY = + " does not exist in inventory. Unable to buy. =("; + public static final String ITEM_NOT_IN_LIST_SELL = + " does not exist in inventory. Unable to sell. =("; + public static final String ITEM_IN_LIST_NEW = " already exists in inventory. Use the update command instead."; + public static final String ITEM_IN_LIST_RENAME = " already exists in the inventory. Please choose another new name"; + public static final String QUANTITY_NOT_INTEGER = "Quantity should be a non-negative integer"; + public static final String INVALID_NUMBER_FORMAT = "Invalid values for price/quantity"; + public static final String INVALID_QUANTITY_FORMAT = "Invalid value for quantity"; + public static final String INVALID_PRICE_FORMAT = "Invalid value for price"; + public static final String INVALID_DATE_FORMAT = "Invalid date. Follow \"dd-mm-yyyy\" format"; + public static final String INVALID_DATE = "This date cannot exist"; + public static final String QUANTITY_NOT_POSITIVE = "Quantity should be more than or equal to 1"; + public static final String QUANTITY_TOO_SMALL = "Quantity should be more than or equal to 0"; + public static final String PRICE_TOO_SMALL = "Price should be more than or equal to 0"; + public static final String QUANTITY_TOO_LARGE = "Quantity should be less than or equal to 2147483647"; + public static final String PRICE_TOO_LARGE = "Price should be less than or equal to 2147483647"; + public static final String FILE_HANDLER_ERROR = "Error setting up file handler"; + public static final String INVALID_FIND_FORMAT = "Invalid find command format!"; + public static final String FILE_SAVE_ERROR = "Oops! Unable to save data due to an I/O error!"; + public static final String FILE_LOAD_ERROR = "Oops! Unable to load your previous data due to an I/O error!"; + public static final String ITEM_FILE_CORRUPTED_ERROR = + "Oops! Unable to load some of your previous item data as the data in the save file has been corrupted!"; + public static final String TRANSACTION_FILE_CORRUPTED_ERROR = + "Oops! Unable to load some of your previous transaction data" + + " as the data in the save file has been corrupted!"; + public static final String TRANSACTION_DATE_LOAD_ERROR = "Looks like you might have edited a transaction date " + + "in the save file to a date that has not happened yet"; + public static final String INTEGER_OVERFLOW = "Unable to add your specified number of items. " + + "Why do you need more than 2147483647 items anyway?"; + public static final String INVALID_REV_TODAY_FORMAT = "Invalid revenue command format. " + + "\"rev task/today\""; + public static final String INVALID_REV_TOTAL_FORMAT = "Invalid revenue command format. " + + "\"rev task/total\""; + public static final String INVALID_REV_DAY_FORMAT = "Invalid revenue command format. " + + "\"rev task/day from/DATE\""; + public static final String INVALID_REV_RANGE_FORMAT = "Invalid revenue command format. " + + "\"rev task/range from/START_DATE to/END_DATE\""; + public static final String INVALID_EXP_TODAY_FORMAT = "Invalid expenditure command format. " + + "\"exp task/today\""; + public static final String INVALID_EXP_TOTAL_FORMAT = "Invalid expenditure command format. " + + "\"exp task/total\""; + public static final String INVALID_EXP_DAY_FORMAT = "Invalid expenditure command format. " + + "\"exp task/day from/DATE\""; + public static final String INVALID_EXP_RANGE_FORMAT = "Invalid expenditure command format. " + + "\"exp task/range from/START_DATE to/END_DATE\""; + public static final String INVALID_PROFIT_TODAY_FORMAT = "Invalid profit command format. " + + "\"profit task/today\""; + public static final String INVALID_PROFIT_TOTAL_FORMAT = "Invalid profit command format. " + + "\"profit task/total\""; + public static final String INVALID_PROFIT_DAY_FORMAT = "Invalid profit command format. " + + "\"profit task/day from/DATE\""; + public static final String INVALID_PROFIT_RANGE_FORMAT = "Invalid profit command format. " + + "\"profit task/range from/START_DATE to/END_DATE\""; +} diff --git a/src/main/java/supertracker/ui/HelpCommandUi.java b/src/main/java/supertracker/ui/HelpCommandUi.java new file mode 100644 index 0000000000..b4f2ebf6f6 --- /dev/null +++ b/src/main/java/supertracker/ui/HelpCommandUi.java @@ -0,0 +1,194 @@ +package supertracker.ui; + +//@@author TimothyLKM +public class HelpCommandUi extends Ui { + private static final String HELP_SUCCESS_MESSAGE_FIRST_LINE = + "Hello! These are the list of commands that I can help you with:"; + private static final String HELP_SUCCESS_MESSAGE_SECOND_LINE = + "** Any other invalid input will bring you back to the main console"; + private static final String TO_SHOW_PARAMETERS = " to show parameters"; + private static final String[] HELP_LIST_OF_COMMANDS = { + "Create a new item: type 'new'", + "Delete an item: type 'delete'", + "Change quantity of an item: type 'change'", + "Update an item: type 'update'", + "Find an item: type 'find'", + "Rename an item: type 'rename'", + "List all items: type 'list'", + "Print a report: type 'report'", + "Print expenditure: type 'exp'", + "Print revenue: type 'rev'", + "Print profit: type 'profit'", + "Buy or sell items: type 'transaction'", + "Clear transactions: type 'clear'" + }; + private static final String HELP_CLOSING_MESSAGE_FIRST_LINE = + "** Refer to UserGuide for further explanation"; + private static final String HELP_CLOSING_MESSAGE_SECOND_LINE = + "** DO NOTE that you have been returned to the main console"; + private static final String HELP_NEW_PARAMETERS = + "A new item command should look like this: new n/NAME q/QUANTITY p/PRICE [e/EXPIRY_DATE]"; + private static final String HELP_FIND_PARAMETERS = + "A find command should look like this: find n/NAME"; + private static final String HELP_DELETE_PARAMETERS = + "A delete command should look like this: delete n/NAME"; + private static final String HELP_UPDATE_PARAMETERS = + "An update command should look like this: " + + "update n/NAME [q/NEW_QUANTITY] [p/NEW_PRICE] [e/NEW_EXPIRY_DATE]"; + private static final String HELP_ADD_QUANTITY_PARAMETERS = + "A add command should look like this: add n/NAME q/QUANTITY"; + private static final String HELP_REMOVE_QUANTITY_PARAMETERS = + "A remove command should look like this: remove n/NAME q/QUANTITY"; + private static final String HELP_RENAME_PARAMETERS = + "A rename command should look like this: rename n/NAME r/NEW_NAME"; + private static final String HELP_LOW_STOCK_REPORT_PARAMETERS = + "A report command for low stock should look like this: report r/low stock t/THRESHOLD_VALUE"; + private static final String HELP_EXPIRY_REPORT_PARAMETERS = + "A report command for expiry date should look like this: report r/expiry"; + private static final String HELP_LIST_PARAMETERS = + "A list command should look like this: list [q/] [p/] [e/] [sq/] [sp/] [se/] [r/]"; + private static final String HELP_BUY_TRANSACTION_PARAMETERS = + "A buy command should look like this: buy n/NAME q/QUANTITY p/PRICE"; + private static final String HELP_SELL_TRANSACTION_PARAMETERS = + "A sell command should look like this: sell n/NAME q/QUANTITY"; + private static final String HELP_EXP_PARAMETERS = + "A expenditure command should look like this: exp type/EXPENDITURE_TYPE [from/START_DATE] [to/END_DATE]"; + private static final String HELP_REV_PARAMETERS = + "A revenue command should look like this: rev type/REVENUE_TYPE [from/START_DATE] [to/END_DATE]"; + private static final String HELP_PROFIT_PARAMETERS = + "A profit command should look like this: profit type/REVENUE_TYPE [from/START_DATE] [to/END_DATE]"; + private static final String HELP_CLEAR_PARAMETERS = + "A clear command should look like this: clear [b/BEFORE_DATE]"; + private static final String INVALID_HELP_COMMAND_MESSAGE_FIRST_LINE = + "You have input an invalid command."; + private static final String INVALID_HELP_COMMAND_MESSAGE_SECOND_LINE = + "You are now back in the main console."; + + /** + * Prints a help list of functions available in SuperTracker. + */ + private static void printHelpListOfCommands() { + int index = 1; + for (String command : HELP_LIST_OF_COMMANDS) { + printIndent(index + ". " + command + TO_SHOW_PARAMETERS); + index++; + } + } + + /** + * Prints message for successful input of help. + */ + public static void helpCommandSuccess() { + printIndent(HELP_SUCCESS_MESSAGE_FIRST_LINE); + printHelpListOfCommands(); + printIndent(HELP_SUCCESS_MESSAGE_SECOND_LINE); + printLine(); + } + + /** + * Displays the necessary parameters for a new item command. + */ + public static void printNewCommandParams() { + printIndent(HELP_NEW_PARAMETERS); + } + + /** + * Displays the necessary parameters for a delete item command. + */ + public static void printDeleteCommandParams() { + printIndent(HELP_DELETE_PARAMETERS); + } + + /** + * Displays the necessary parameters for add and remove command. + */ + public static void printChangeCommandParams() { + printIndent(HELP_ADD_QUANTITY_PARAMETERS); + printIndent(HELP_REMOVE_QUANTITY_PARAMETERS); + } + + /** + * Displays the necessary parameters for an update command. + */ + public static void printUpdateCommandParams() { + printIndent(HELP_UPDATE_PARAMETERS); + } + + /** + * Displays the necessary parameters for Find command. + */ + public static void printFindCommandParams() { + printIndent(HELP_FIND_PARAMETERS); + } + + /** + * Displays the necessary parameters for a rename command. + */ + public static void printRenameCommandParams() { + printIndent(HELP_RENAME_PARAMETERS); + } + + /** + * Displays the necessary parameters for a list command. + */ + public static void printListCommandParams() { + printIndent(HELP_LIST_PARAMETERS); + } + + /** + * Displays the necessary parameters for a report command. + */ + public static void printReportCommandParams() { + printIndent(HELP_LOW_STOCK_REPORT_PARAMETERS); + printIndent(HELP_EXPIRY_REPORT_PARAMETERS); + } + + /** + * Displays the necessary parameters for buy and sell command. + */ + public static void printTransactionCommandParams() { + printIndent(HELP_BUY_TRANSACTION_PARAMETERS); + printIndent(HELP_SELL_TRANSACTION_PARAMETERS); + } + + /** + * Displays the necessary parameters for an expenditure command. + */ + public static void printExpenditureCommandParams() { + printIndent(HELP_EXP_PARAMETERS); + } + + /** + * Displays the necessary parameters for a revenue command. + */ + public static void printRevenueCommandParams() { + printIndent(HELP_REV_PARAMETERS); + } + + /** + * Displays the necessary parameters for a clear command. + */ + public static void printProfitCommandParams() { + printIndent(HELP_PROFIT_PARAMETERS); + } + + public static void printClearCommandParams() { + printIndent(HELP_CLEAR_PARAMETERS); + } + + /** + * Displays a message to notify user that their input is invalid. + */ + public static void printInvalidHelpMessage() { + printIndent(INVALID_HELP_COMMAND_MESSAGE_FIRST_LINE); + printIndent(INVALID_HELP_COMMAND_MESSAGE_SECOND_LINE); + } + + /** + * Displays a message to notify users that they have returned to the main console. + */ + public static void helpClosingMessage() { + printIndent(HELP_CLOSING_MESSAGE_FIRST_LINE); + printIndent(HELP_CLOSING_MESSAGE_SECOND_LINE); + } +} diff --git a/src/main/java/supertracker/ui/Ui.java b/src/main/java/supertracker/ui/Ui.java new file mode 100644 index 0000000000..ea5bb75419 --- /dev/null +++ b/src/main/java/supertracker/ui/Ui.java @@ -0,0 +1,614 @@ +package supertracker.ui; + +import supertracker.item.Item; +import supertracker.item.Transaction; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class Ui { + private static final String LINE = " --------------------------------------------------------------------------"; + private static final String QUANTITY_FLAG = "q"; + private static final String PRICE_FLAG = "p"; + private static final String EX_DATE_FLAG = "e"; + private static final String EMPTY_STRING = ""; + private static final String EMPTY_LIST_MESSAGE = "Nothing to list! No items in inventory!"; + private static final String SINGLE_ITEM_LIST_MESSAGE= "There is 1 unique item in your inventory:"; + private static final String INVALID_COMMAND_MESSAGE = "Sorry! Invalid command!"; + private static final String WELCOME_MESSAGE = "Hello, welcome to SuperTracker, how may I help you?"; + private static final String FAREWELL_MESSAGE = "Goodbye!"; + private static final String BASIC_ERROR_MESSAGE = "Oh no! An error has occurred"; + private static final String FIND_OPENING_MESSAGE = "Here are your found items:"; + private static final String REPORT_LOW_STOCK_NO_ITEMS_OPENING = "There are no items that fit the criteria!"; + private static final String REPORT_EXPIRY_NO_ITEMS_OPENING = "There are no items close to expiry!"; + private static final String REPORT_EXPIRED_NO_ITEMS_OPENING = "There are no items that are expired!"; + private static final String REPORT_INVENTORY_NO_ITEMS = "There are no items in the inventory, " + + "please consider adding some in!"; + private static final String CLEAR_CONFIRMATION_FIRST_LINE = + "Are you sure you want to clear all transactions before "; + private static final String CLEAR_CONFIRMATION_SECOND_LINE = "Enter 'y' or 'Y' if you wish to proceed"; + private static final String CLEAR_CONFIRMATION_THIRD_LINE = + "Enter anything else if you wish to cancel the clear operation"; + private static final String CLEAR_CANCELLED = "Clear operation has been cancelled"; + private static final DateTimeFormatter DATE_FORMAT_NULL = DateTimeFormatter.ofPattern("dd-MM-yyyyy"); + private static final DateTimeFormatter DATE_FORMAT_PRINT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final LocalDate UNDEFINED_DATE = LocalDate.parse("01-01-99999", DATE_FORMAT_NULL); + private static final String TODAY = "today"; + private static final String TOTAL = "total"; + private static final String DAY = "day"; + private static final String RANGE = "range"; + private static final String PROFIT_MESSAGE = "Nice! You have a profit."; + private static final String BREAK_EVEN_MESSAGE = "You have broken even."; + private static final String LOSS_MESSAGE = "You lost money."; + + + private static String getListSize(int size){ + return ("There are " + size + " unique items in your inventory:"); + } + + private static String getPriceMessage(Item item) { + return "Price: " + item.getPriceString(); + } + + private static String getNameMessage(Item item) { + return "Name: " + item.getName(); + } + + private static String getQuantityMessage(Item item) { + return "Quantity: " + item.getQuantity(); + } + + private static String getNewItemOpening(Item item) { + return item.getName() + " has been added to the inventory!"; + } + + /** + * Formats the expiry date to be printed in the correct format and alignment. + * + * @param item Item from inventory. + * @return Formatted expiry date message. + */ + private static String getExpiryDateMessage(Item item) { + if (!item.getExpiryDate().isEqual(UNDEFINED_DATE)) { + return "Expiry Date: " + item.getExpiryDateString(); + } + return EMPTY_STRING; + } + + private static String updateItemOpening(Item item) { + return item.getName() + " has been successfully updated!"; + } + + private static String renameItemOpening(Item item, String name) { + return name + " has been successfully renamed to " + item.getName() + "."; + } + + private static String deleteItemOpening(String name) { + return name + " has been deleted!"; + } + + private static String addItemOpening(Item item, int quantityAdded) { + return quantityAdded + " " + item.getName() + " added to inventory!"; + } + + private static String removeItemOpening(Item item, int quantityRemoved) { + return quantityRemoved + " " + item.getName() + " removed from inventory!"; + } + + private static String nothingRemovedOpening(Item item) { + return "No " + item.getName() + " removed as you don't have any!"; + } + + private static String buyItemOpening(Transaction transaction) { + return transaction.getQuantity() + " " + transaction.getName() + " bought at " + + transaction.getPriceString() + " each for " + + transaction.getTotalPriceString() + " in total"; + } + + private static String sellItemOpening(Transaction transaction) { + return transaction.getQuantity() + " " + transaction.getName() + " sold at " + + transaction.getPriceString() + " each for " + + transaction.getTotalPriceString() + " in total"; + } + + private static String nothingSoldOpening(Item item) { + return "No " + item.getName() + " sold as you don't have any!"; + } + + /** + * Generates a message indicating the total number of items low on stock. + * + * @param quantity The quantity of items that are low on stock. + * @return A string message describing the low stock situation. + */ + private static String reportLowStockOpening(int quantity) { + assert quantity >= 0; + String isOrAre = quantity == 1 ? "is " : "are "; + String plural = quantity == 1 ? EMPTY_STRING : "s"; + return "There " + isOrAre + quantity + " item" + plural + " low on stocks!"; + } + + /** + * Generates a message indicating the total number of items close to expiry. + * + * @param quantity The quantity of items that are close to expiry. + * @return A string message describing the items close to expiry. + */ + private static String reportExpiryHasItemsOpening(int quantity) { + String isOrAre = quantity == 1 ? "is " : "are "; + String itemOrItems = quantity == 1 ? " item " : "items "; + return "There " + isOrAre + quantity + itemOrItems +"close to expiry!"; + } + + /** + * Generates a message indicating the total number of items that have expired. + * + * @param quantity The quantity of items that have expired. + * @return A string message describing the items that have expired. + */ + private static String reportExpiredHasItemsOpening(int quantity) { + String isOrAre = quantity == 1 ? "is " : "are "; + String itemOrItems = quantity == 1 ? " item " : " items "; + return "There " + isOrAre + quantity + itemOrItems +"that " + isOrAre + "expired!"; + } + + /** + * Generates a message detailing the name of an item with its count for the report. + * + * @param reportItem The item being reported. + * @param count The count or index of the item in the report list. + * @return A string message describing the item's name. + */ + private static String reportNameMessage(Item reportItem, int count) { + return count + ". Name: " + reportItem.getName(); + } + + /** + * Generates a message detailing the quantity of an item for the report. + * + * @param reportItem The item being reported. + * @return A string message describing the item's quantity. + */ + private static String reportQuantityMessage(Item reportItem) { + return " Quantity: " + reportItem.getQuantity(); + } + + /** + * Generates a message detailing the expiry date of an item for the report. + * + * @param reportItem The item being reported. + * @return A string message describing the item's expiry date. + */ + private static String reportExpiryDateMessage(Item reportItem) { + return " Expiry Date: " + reportItem.getExpiryDateString(); + } + + public static void printIndent(String string) { + if (string.isEmpty()) { + return; + } + System.out.println(" " + string); + } + + public static void printLine() { + System.out.println(LINE); + } + + public static void greetUser() { + printLine(); + printIndent(WELCOME_MESSAGE); + printLine(); + } + + public static void sayGoodbye() { + printIndent(FAREWELL_MESSAGE); + } + + public static void printInvalidCommand() { + printIndent(INVALID_COMMAND_MESSAGE); + } + + public static void newCommandSuccess(Item item) { + printIndent(getNewItemOpening(item)); + printIndent(getQuantityMessage(item)); + printIndent(getPriceMessage(item)); + printIndent(getExpiryDateMessage(item)); + } + + public static void updateCommandSuccess(Item item) { + printIndent(updateItemOpening(item)); + printIndent(getQuantityMessage(item)); + printIndent(getPriceMessage(item)); + printIndent(getExpiryDateMessage(item)); + } + + public static void renameCommandSuccess(Item item, String name) { + printIndent(renameItemOpening(item, name)); + printIndent(getNameMessage(item)); + printIndent(getQuantityMessage(item)); + printIndent(getPriceMessage(item)); + printIndent(getExpiryDateMessage(item)); + } + + /** + * Prints message for successful deletion of item from inventory. + * + * @param name Name of item in inventory to be deleted. + */ + public static void deleteCommandSuccess(String name) { + printIndent(deleteItemOpening(name)); + } + + public static void addCommandSuccess(Item item, int quantityAdded) { + assert quantityAdded >= 0; + printIndent(addItemOpening(item, quantityAdded)); + printIndent(getQuantityMessage(item)); + } + + /** + * Prints message for a successful removal of a specified quantity of the chosen item in the inventory. + * + * @param item Name of item in inventory whose quantity has been removed. + * @param quantityRemoved The amount of quantity that has been removed from the item. + */ + public static void removeCommandSuccess(Item item, int quantityRemoved) { + assert quantityRemoved >= 0; + if (quantityRemoved == 0) { + printIndent(nothingRemovedOpening(item)); + return; + } + printIndent(removeItemOpening(item, quantityRemoved)); + printIndent(getQuantityMessage(item)); + } + + public static void buyCommandSuccess(Item item, Transaction transaction) { + printIndent(buyItemOpening(transaction)); + printIndent(getQuantityMessage(item)); + } + + public static void sellCommandSuccess(Item item, Transaction transaction) { + if (transaction.getQuantity() == 0) { + printIndent(nothingSoldOpening(item)); + return; + } + printIndent(sellItemOpening(transaction)); + printIndent(getQuantityMessage(item)); + } + + /** + * Generates a message for the report if the inventory has no items. + */ + public static void reportNoItems() { + printIndent(REPORT_INVENTORY_NO_ITEMS); + } + + /** + * Displays a success message based on the report type and the list of reported items. + * + * @param reportItems The list of items that meet the criteria for the reported. + * @param reportType The type of report (e.g., "low stock", "expiry", "expired"). + */ + public static void reportCommandSuccess(List reportItems, String reportType) { + int numReportItems = reportItems.size(); + switch (reportType) { + case "low stock": + if (reportItems.isEmpty()) { + printIndent(REPORT_LOW_STOCK_NO_ITEMS_OPENING); + } else { + lowStockSuccess(reportItems, numReportItems); + } + break; + case "expiry": + if (reportItems.isEmpty()) { + printIndent(REPORT_EXPIRY_NO_ITEMS_OPENING); + } else { + expirySuccess(reportItems, numReportItems); + } + break; + case "expired": + if (reportItems.isEmpty()) { + printIndent(REPORT_EXPIRED_NO_ITEMS_OPENING); + } else { + expiredSuccess(reportItems, numReportItems); + } + break; + default: + assert reportType.isEmpty(); + break; + } + } + + /** + * Displays a success message for an expiry report, listing items and their expiry dates that are close to the + * expiry date (1 week before the expiry date). + * + * @param reportItems The list of items that are close to expiry. + * @param numReportItems The number of items in the report. + */ + private static void expirySuccess(List reportItems, int numReportItems) { + int count = 1; + printIndent(reportExpiryHasItemsOpening(numReportItems)); + for (Item item : reportItems) { + printIndent(reportNameMessage(item, count)); + printIndent(reportExpiryDateMessage(item)); + count += 1; + } + } + + /** + * Displays a success message for items that have already expired, listing items and their expiry dates. + * + * @param reportItems The list of items that have expired. + * @param numReportItems The number of items in the report. + */ + private static void expiredSuccess(List reportItems, int numReportItems) { + int count = 1; + printIndent(reportExpiredHasItemsOpening(numReportItems)); + for (Item item : reportItems) { + printIndent(reportNameMessage(item, count)); + printIndent(reportExpiryDateMessage(item)); + count += 1; + } + } + + /** + * Displays a success message for a low stock report, listing items and their quantities. + * + * @param reportItems The list of items reported as low in stock. + * @param numReportItems The number of items in the report. + */ + private static void lowStockSuccess(List reportItems, int numReportItems) { + int count = 1; + printIndent(reportLowStockOpening(numReportItems)); + for (Item item : reportItems) { + printIndent(reportNameMessage(item, count)); + printIndent(reportQuantityMessage(item)); + count += 1; + } + } + + /** + * Displays the revenue/expenditure value and corresponding transactions + * over a length of time according to the task, start and end dates. + * + * @param task The task type (e.g., "today", "total", "day", "range"). + * @param amount Revenue or expenditure value. + * @param startDate The start date for filtering transactions. + * @param endDate The end date for filtering transactions (used with "range" task). + * @param financeType Revenue or expenditure command. + * @param filteredList List of transaction items fitting the criteria that has been sorted alphabetically. + */ + public static void printRevenueExpenditure(String task, BigDecimal amount, LocalDate startDate, LocalDate endDate, + String financeType, ArrayList filteredList) { + String amountString = String.format("%.2f", amount); + switch (task) { + case TODAY: + printIndent("Today's " + financeType + " is $" + amountString); + printFilteredList(filteredList); + break; + case TOTAL: + printIndent("Total " + financeType + " is $" + amountString); + printFilteredList(filteredList); + break; + case DAY: + printIndent(financeType + " on " + startDate.format(DATE_FORMAT_PRINT) + " was $" + amountString); + printFilteredList(filteredList); + break; + case RANGE: + printIndent( financeType + " between " + startDate.format(DATE_FORMAT_PRINT) + " and " + + endDate.format(DATE_FORMAT_PRINT) + " was $" + amountString); + printFilteredList(filteredList); + break; + default: assert task.isEmpty(); + break; + } + } + + /** + * Displays the profit value over a length of time according to the task, start and end dates. + * + * @param task The task type (e.g., "today", "total", "day", "range"). + * @param amount Revenue or expenditure value. + * @param startDate The start date for filtering transactions. + * @param endDate The end date for filtering transactions (used with "range" task) + */ + public static void printProfit(String task, BigDecimal amount, LocalDate startDate, LocalDate endDate) { + int profitSign = Integer.compare(amount.compareTo(BigDecimal.valueOf(0)), 0); + String amountString = String.format("%.2f", amount); + switch (task) { + case TODAY: + printIndent("Today's profit is $" + amountString); + break; + case TOTAL: + printIndent("Total profit is $" + amountString); + break; + case DAY: + printIndent("Your profit on " + startDate.format(DATE_FORMAT_PRINT) + " was $" + amountString); + break; + case RANGE: + printIndent( "Your profit between " + startDate.format(DATE_FORMAT_PRINT) + " and " + + endDate.format(DATE_FORMAT_PRINT) + " was $" + amountString); + break; + default: assert task.isEmpty(); + break; + } + if (profitSign == 1) { + printIndent(PROFIT_MESSAGE); + } else if (profitSign == 0) { + printIndent(BREAK_EVEN_MESSAGE); + } else { + printIndent(LOSS_MESSAGE); + } + } + + //@@ author dtaywd + /** + * Prints a formatted list of transactions with details including name, quantity, price, and transaction date. + * + * @param filteredList The list of transactions to be printed. + */ + private static void printFilteredList(ArrayList filteredList) { + int count = 1; + for (Transaction transaction: filteredList) { + String formattedTransactionDate = transaction.getTransactionDate().format(DATE_FORMAT_PRINT); + printIndent(count + ". Name: " + transaction.getName()); + printIndent(" Quantity: " + transaction.getQuantity()); + printIndent(" Price: " + transaction.getPriceString()); + printIndent(" Transaction Date: " + formattedTransactionDate); + count += 1; + } + } + //@@author + + public static void listIntro(int size) { + assert size >= 0; + if (size == 0) { + printIndent(EMPTY_LIST_MESSAGE); + return; + } + if (size == 1) { + printIndent(SINGLE_ITEM_LIST_MESSAGE); + return; + } + printIndent(getListSize(size)); + } + + public static void findIntro() { + printIndent(FIND_OPENING_MESSAGE); + } + + public static void listItem(Item item, int index, String firstParam, String secondParam, String thirdParam) { + String nameString = index + ". Name: " + item.getName(); + String quantityString = " Quantity: " + item.getQuantity(); + String priceString = " Price: " + item.getPriceString(); + String expiryString; + if (!item.getExpiryDate().isEqual(UNDEFINED_DATE)) { + expiryString = " Expiry Date: " + item.getExpiryDateString(); + } else { + expiryString = EMPTY_STRING; + } + String itemString = getItemString(firstParam, secondParam, thirdParam, + nameString, quantityString, priceString, expiryString); + printIndent(itemString); + } + + private static String buildItemString( + String param, + String itemString, + String quantityString, + String priceString, + String expiryString + ) { + switch (param) { + case QUANTITY_FLAG: + itemString += quantityString; + break; + case PRICE_FLAG: + itemString += priceString; + break; + case EX_DATE_FLAG: + itemString += expiryString; + break; + default: + assert param.isEmpty(); + break; + } + return itemString; + } + + private static String getItemString( + String firstParam, + String secondParam, + String thirdParam, + String nameString, + String quantityString, + String priceString, + String expiryString + ) { + String itemString = nameString; + itemString = buildItemString(firstParam, itemString, quantityString, priceString, expiryString); + itemString = buildItemString(secondParam, itemString, quantityString, priceString, expiryString); + itemString = buildItemString(thirdParam, itemString, quantityString, priceString, expiryString); + return itemString; + } + + public static void printError(String errorMessage) { + printIndent(BASIC_ERROR_MESSAGE); + printIndent(errorMessage); + } + + public static void printFoundItem(Item item, int index) { + if (index == 1) { + Ui.findIntro(); + } + String stringToPrint = index + ". Name: " + item.getName(); + printIndent(stringToPrint); + String quantityString = " Quantity: " + item.getQuantity(); + printIndent(quantityString); + String priceString = " Price: " + item.getPriceString(); + printIndent(priceString); + if (!item.getExpiryDate().isEqual(UNDEFINED_DATE)) { + printIndent(" Expiry Date: " + item.getExpiryDateString()); + } + } + + public static void printNoItemFound(String name) { + String stringToPrint = "So sorry, Your item: " + name + " could not be found."; + printIndent(stringToPrint); + } + + public static void printItemNameLimitation(String name, String delimiter, String newName) { + String nameOutputString = padStringWithQuotes(name, true); + String delimiterOutputString = padStringWithQuotes(delimiter, false); + String newNameOutputString = padStringWithQuotes(newName, false); + printIndent("It appears that the input item name, " + nameOutputString); + printIndent("contains the program's file delimiter, " + delimiterOutputString); + printIndent("Unfortunately due to system limitations, " + nameOutputString); + printIndent("will be renamed and saved as " + newNameOutputString); + printIndent("Please avoid using the file delimiter in your item names" + System.lineSeparator()); + } + + private static String padStringWithQuotes(String name, boolean hasComma) { + String end = hasComma ? "\"," : "\""; + return "\"" + name + end; + } + + public static void clearCommandConfirmation(LocalDate beforeDate) { + printIndent(CLEAR_CONFIRMATION_FIRST_LINE + beforeDate.format(DATE_FORMAT_PRINT) + "?"); + printIndent(CLEAR_CONFIRMATION_SECOND_LINE); + printIndent(CLEAR_CONFIRMATION_THIRD_LINE); + printLine(); + } + + public static void clearCommandCancelled() { + printIndent(CLEAR_CANCELLED); + } + + public static void clearCommandSuccess(int transactionsCleared, LocalDate beforeDate) { + String dateString = beforeDate.format(DATE_FORMAT_PRINT); + if (transactionsCleared == 0) { + printIndent("Nothing cleared. No transactions before " + dateString + " available to clear"); + return; + } + String plural = transactionsCleared == 1 ? EMPTY_STRING : "s"; + printIndent(transactionsCleared + " transaction" + plural + + " before " + dateString + " successfully cleared!"); + } + + public static void printDuplicatesInSavefile(HashSet duplicates) { + StringBuilder duplicateItemNames = new StringBuilder(); + for (String dup : duplicates) { + duplicateItemNames.append(dup); + duplicateItemNames.append(", "); + } + printIndent("SuperTracker has detected the following duplicate item names in the save file:"); + printIndent(duplicateItemNames.toString()); + printIndent("Only the latest occurrence of data lines with these item names in"); + printIndent("the save file will be loaded into the inventory list."); + } +} diff --git a/src/main/java/supertracker/util/Triple.java b/src/main/java/supertracker/util/Triple.java new file mode 100644 index 0000000000..45080a32b3 --- /dev/null +++ b/src/main/java/supertracker/util/Triple.java @@ -0,0 +1,50 @@ +package supertracker.util; + +/** + * A generic class representing a triple of three elements. + */ +public class Triple { + private final A first; + private final B second; + private final C third; + + /** + * Constructs a new Triple object with the specified elements. + * + * @param first First element. + * @param second Second element. + * @param third Third element. + */ + public Triple(A first, B second, C third) { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * Gets the first element of the triple. + * + * @return First element. + */ + public A getFirst() { + return first; + } + + /** + * Gets the second element of the triple. + * + * @return Second element. + */ + public B getSecond() { + return second; + } + + /** + * Gets the third element of the triple. + * + * @return Third element. + */ + public C getThird() { + return third; + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/supertracker/SuperTrackerTest.java similarity index 78% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/supertracker/SuperTrackerTest.java index 2dda5fd651..28187fff54 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/supertracker/SuperTrackerTest.java @@ -1,10 +1,10 @@ -package seedu.duke; +package supertracker; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -class DukeTest { +class SuperTrackerTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/supertracker/command/AddCommandTest.java b/src/test/java/supertracker/command/AddCommandTest.java new file mode 100644 index 0000000000..10db072f87 --- /dev/null +++ b/src/test/java/supertracker/command/AddCommandTest.java @@ -0,0 +1,79 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.parser.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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AddCommandTest { + @BeforeEach + public void setUp() { + Inventory.clear(); + + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + } + + @Test + public void addCommand_validData_correctlyConstructed(){ + String name = "Milk"; + int quantity = 100; + + int quantityToAdd = 50; + int newQuantity = quantity + quantityToAdd; + + Command addCommand = new AddCommand(name, quantityToAdd); + addCommand.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(newQuantity, item.getQuantity()); + } + + @Test + public void addCommand_missingParamInput() { + String userInput = "add n/Milk"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void addCommand_emptyParamInput() { + String userInput = "add n/Milk q/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void addCommand_itemNotInList() { + String userInput = "add n/Cake q/100"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void addCommand_quantityNotPositive() { + String userInput = "add n/Milk q/0"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void addCommand_integerOverflow() { + String userInput = "add n/Milk q/2147483647"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/BuyCommandTest.java b/src/test/java/supertracker/command/BuyCommandTest.java new file mode 100644 index 0000000000..a06d5c065b --- /dev/null +++ b/src/test/java/supertracker/command/BuyCommandTest.java @@ -0,0 +1,85 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.parser.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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BuyCommandTest { + private static final String BUY_FLAG = "b"; + + @BeforeEach + public void setUp() { + Inventory.clear(); + TransactionList.clear(); + + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + } + + @Test + public void buyCommand_validData_correctlyConstructed(){ + String name = "Milk"; + + int quantityToBuy = 50; + double priceToBuy = 3.00; + LocalDate buyDate = LocalDate.parse("12-04-2024", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command buyCommand = new BuyCommand(name, quantityToBuy, priceToBuy, buyDate); + buyCommand.execute(); + + assertEquals(1, TransactionList.size()); + Transaction transaction = TransactionList.get(0); + assertNotNull(transaction); + assertEquals(name, transaction.getName()); + assertEquals(quantityToBuy, transaction.getQuantity()); + assertEquals(priceToBuy, transaction.getPrice()); + assertEquals(buyDate, transaction.getTransactionDate()); + assertEquals(BUY_FLAG, transaction.getType()); + } + + @Test + public void buyCommand_missingParamInput() { + String userInput = "buy n/Milk q/50"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void buyCommand_emptyParamInput() { + String userInput = "buy n/Milk q/50 p/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void buyCommand_itemNotInList() { + String userInput = "buy n/Cake q/100 p/3.00"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void buyCommand_quantityNotPositive() { + String userInput = "buy n/Milk q/0 p/3.00"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void buyCommand_integerOverflow() { + String userInput = "buy n/Milk q/2147483647 p/3.00"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/ClearCommandTest.java b/src/test/java/supertracker/command/ClearCommandTest.java new file mode 100644 index 0000000000..b52a3d3e27 --- /dev/null +++ b/src/test/java/supertracker/command/ClearCommandTest.java @@ -0,0 +1,108 @@ +package supertracker.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.parser.Parser; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ClearCommandTest { + private static final String BUY_FLAG = "b"; + private static final String SELL_FLAG = "s"; + private final InputStream sysInBackup = System.in; + private ByteArrayInputStream in; + + @BeforeEach + public void setUp() { + TransactionList.clear(); + Transaction[] transactions = { + new Transaction("Apple", 1, 1.00, LocalDate.parse("2024-01-01"), BUY_FLAG), + new Transaction("Banana", 2, 2.00, LocalDate.parse("2023-01-01"), SELL_FLAG), + new Transaction("Cake", 3, 3.00, LocalDate.parse("2022-01-01"), BUY_FLAG), + new Transaction("Egg", 4, 4.00, LocalDate.parse("2021-01-01"), SELL_FLAG), + new Transaction("Milk", 5, 5.00, LocalDate.parse("2020-01-01"), BUY_FLAG), + }; + for (Transaction transaction : transactions) { + TransactionList.add(transaction); + } + System.setIn(sysInBackup); + } + + @Test + public void clearCommand_validData_correctlyConstructed(){ + String input = "Y"; + in = new ByteArrayInputStream(input.getBytes()); + System.setIn(in); + + LocalDate beforeDate = LocalDate.parse("2024-01-01"); + + Command clearCommand = new ClearCommand(beforeDate); + clearCommand.execute(); + + assertEquals(1, TransactionList.size()); + assertEquals("Apple", TransactionList.get(0).getName()); + } + + @Test + public void clearCommand_validData_clearAll(){ + String input = "y"; + in = new ByteArrayInputStream(input.getBytes()); + System.setIn(in); + + LocalDate beforeDate = LocalDate.parse("2024-02-02"); + + Command clearCommand = new ClearCommand(beforeDate); + clearCommand.execute(); + + assertEquals(0, TransactionList.size()); + } + + @Test + public void clearCommand_validData_clearNone(){ + String input = "y"; + in = new ByteArrayInputStream(input.getBytes()); + System.setIn(in); + + LocalDate beforeDate = LocalDate.parse("1999-01-01"); + + Command clearCommand = new ClearCommand(beforeDate); + clearCommand.execute(); + + assertEquals(5, TransactionList.size()); + assertEquals("Milk", TransactionList.get(4).getName()); + } + + @Test + public void clearCommand_inValidConfirmation(){ + String input = " y "; + in = new ByteArrayInputStream(input.getBytes()); + System.setIn(in); + + LocalDate beforeDate = LocalDate.parse("2024-01-01"); + + Command clearCommand = new ClearCommand(beforeDate); + clearCommand.execute(); + + assertEquals(5, TransactionList.size()); + assertEquals("Milk", TransactionList.get(4).getName()); + } + + @Test + public void clearCommand_invalidDate(){ + String userInput = "clear b/29-02-2023"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @AfterEach void tearDown() { + System.setIn(sysInBackup); + } +} diff --git a/src/test/java/supertracker/command/DeleteCommandTest.java b/src/test/java/supertracker/command/DeleteCommandTest.java new file mode 100644 index 0000000000..0ce8ada4d0 --- /dev/null +++ b/src/test/java/supertracker/command/DeleteCommandTest.java @@ -0,0 +1,58 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.parser.Parser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DeleteCommandTest { + private static final String NAME = "Milk"; + @BeforeEach + public void setUp() { + Inventory.clear(); + + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(NAME, quantity, price, date); + newCommand.execute(); + } + + @Test + public void deleteCommand_validData_correctlyConstructed() { + Command deleteCommand = new DeleteCommand(NAME); + deleteCommand.execute(); + + assertFalse(Inventory.contains(NAME)); + Item item = Inventory.get(NAME); + assertNull(item); + } + + @Test + public void deleteCommand_missingParamInput() { + String userInput = "delete"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void deleteCommand_emptyParamInput() { + String userInput = "delete n/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void deleteCommand_itemNotInList() { + String userInput = "delete n/cake"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/ExpenditureCommandTest.java b/src/test/java/supertracker/command/ExpenditureCommandTest.java new file mode 100644 index 0000000000..f84b31fcf1 --- /dev/null +++ b/src/test/java/supertracker/command/ExpenditureCommandTest.java @@ -0,0 +1,217 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.TransactionList; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ExpenditureCommandTest { + public static final DateTimeFormatter VALID_USER_INPUT_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final String INVALID_EX_DATE_FORMAT = "dd-MM-yyyyy"; + private static final String INVALID_EX_DATE = "01-01-99999"; + private static final DateTimeFormatter VALID_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final LocalDate CURR_DATE = LocalDate.now(); + private static final String CURR_DATE_FORMATTED = CURR_DATE.format(VALID_EX_DATE_FORMAT); + private static final LocalDate CURR_PLUS_ONE = CURR_DATE.plusDays(1); + private static final String CURR_PLUS_ONE_FORMATTED = CURR_PLUS_ONE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_PLUS_ONE_INPUT = CURR_PLUS_ONE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO = CURR_DATE.minusDays(2); + private static final String CURR_MINUS_TWO_FORMATTED = CURR_MINUS_TWO.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_TWO_INPUT = CURR_MINUS_TWO.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_THREE = CURR_DATE.minusDays(3); + private static final String CURR_MINUS_THREE_FORMATTED = CURR_MINUS_THREE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_THREE_INPUT = CURR_MINUS_THREE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO_WEEK = CURR_DATE.minusWeeks(2); + private static final String CURR_MINUS_TWO_WEEK_FORMATTED = CURR_MINUS_TWO_WEEK.format(VALID_EX_DATE_FORMAT); + private static final LocalDate INVALID_DATE = LocalDate.parse + (INVALID_EX_DATE, DateTimeFormatter.ofPattern(INVALID_EX_DATE_FORMAT)); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + + /** + * Sets up the inventory and executes initial commands before running any test. + * Clears the inventory, then executes a series of commands to populate it with items. + */ + @BeforeAll + public static void setUp() { + Inventory.clear(); + TransactionList.clear(); + + Command[] commands = { + new NewCommand("orange", 0, 2.00, INVALID_DATE), + new NewCommand("grape", 0, 1.00, INVALID_DATE), + new NewCommand("banana", 0, 3.00, INVALID_DATE), + new BuyCommand("orange", 20, 1.00, CURR_DATE), + new BuyCommand("grape", 10, 0.50, CURR_MINUS_TWO), + new BuyCommand("banana", 5, 1.50, CURR_MINUS_TWO_WEEK), + }; + for (Command c : commands) { + c.execute(); + } + } + + /** + * Redirects system output to a PrintStream for capturing output during tests. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + + /** + * Tests the construction of expenditure report for today's transactions. + * Verifies that the correct output is printed based on executed commands. + */ + @Test + public void expenditureCommand_today_correctlyConstructed() throws TrackerException { + String userInput = "exp type/today"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (20 * 1); + String amountString = String.format("%.2f", amount); + + String expected = " Today's expenditure is $" + amountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of total expenditure report. + * Verifies that the correct total expenditure report is printed based on executed commands. + */ + @Test + public void expenditureCommand_total_correctlyConstructed() throws TrackerException { + String userInput = "exp type/total"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double totalAmount = 32.5; + String totalAmountString = String.format("%.2f", totalAmount); + + String expected = " Total expenditure is $" + totalAmountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR + + " 2. Name: grape" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $0.50" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR + + " 3. Name: banana" + LINE_SEPARATOR + + " Quantity: 5" + LINE_SEPARATOR + + " Price: $1.50" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_WEEK_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of expenditure report for a specific day. + * Verifies that the correct expenditure report for a given day is printed. + */ + @Test + public void expenditureCommand_day_correctlyConstructed() throws TrackerException { + String userInput = "exp type/day from/" + CURR_MINUS_TWO_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (10 * 0.5); + String amountString = String.format("%.2f", amount); + + String expected = " expenditure on " + CURR_MINUS_TWO_FORMATTED + " was $" + amountString + LINE_SEPARATOR + + " 1. Name: grape" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $0.50" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + + /** + * Tests the construction of expenditure report for a date range. + * Verifies that the correct expenditure report for a given date range is printed. + */ + @Test + public void expenditureCommand_range_correctlyConstructed() throws TrackerException { + String userInput = "exp type/range from/" + CURR_MINUS_THREE_INPUT + " to/" + CURR_PLUS_ONE_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (20 * 1 + 10 * 0.5); + String amountString = String.format("%.2f", amount); + + String expected = " expenditure between " + CURR_MINUS_THREE_FORMATTED + " and " + CURR_PLUS_ONE_FORMATTED + + " was $" + amountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR + + " 2. Name: grape" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $0.50" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the behavior when the user input for expenditure command is incomplete. + * Verifies that a TrackerException is thrown when required parameters are missing. + */ + @Test + public void expenditureCommand_missingParamInput() { + String userInput = "exp type/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for expenditure command has too many parameters. + * Verifies that a TrackerException is thrown when unexpected parameters are provided. + */ + @Test + public void expenditureCommand_tooManyParamInput() { + String userInput = "exp type/total from/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for expenditure command is missing the flags for day task. + * Verifies that a TrackerException is thrown when essential flags are absent. + */ + @Test + public void expenditureCommand_missingDayFlag() { + String userInput = "exp type/day"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for expenditure command is missing the flag for range task. + * Verifies that a TrackerException is thrown when the range flag is missing. + */ + @Test + public void expenditureCommand_missingRangeFlag() { + String userInput = "exp type/range from/" + CURR_MINUS_THREE_INPUT; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/FindCommandTest.java b/src/test/java/supertracker/command/FindCommandTest.java new file mode 100644 index 0000000000..58679015bb --- /dev/null +++ b/src/test/java/supertracker/command/FindCommandTest.java @@ -0,0 +1,86 @@ +package supertracker.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +//@@author [TimothyLKM] +public class FindCommandTest { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String FIND_INTRO = " Here are your found items:" + LINE_SEPARATOR; + private static final String INDEX = " 1."; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeAll + public static void setUp() { + Inventory.clear(); + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + } + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + @Test + public void findCommand_validData_correctlyConstructed() throws TrackerException { + String word = "Milk"; + + String userInput = "find n/" + word; + Command c = Parser.parseCommand(userInput); + c.execute(); + + String expected = FIND_INTRO + + INDEX + " Name: Milk" + LINE_SEPARATOR + + " Quantity: 100" + LINE_SEPARATOR + + " Price: $5.00" + LINE_SEPARATOR + + " Expiry Date: 01/01/2113" + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void findCommand_missingParamInput() { + String userInput = "find"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void findCommand_emptyParamInput() { + String userInput = "find n/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void findCommand_itemNotInList() throws TrackerException { + String userInput = "find n/cake"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + String expected = " So sorry, Your item: cake could not be found." + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } +} diff --git a/src/test/java/supertracker/command/ListCommandTest.java b/src/test/java/supertracker/command/ListCommandTest.java new file mode 100644 index 0000000000..d38ea5d855 --- /dev/null +++ b/src/test/java/supertracker/command/ListCommandTest.java @@ -0,0 +1,249 @@ +package supertracker.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ListCommandTest { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String LIST_INTRO = " There are 3 unique items in your inventory:" + LINE_SEPARATOR; + private static final String INDEX_1 = " 1."; + private static final String INDEX_2 = " 2."; + private static final String INDEX_3 = " 3."; + private static final String A_NAME = " Name: Apple"; + private static final String B_NAME = " Name: Berry"; + private static final String C_NAME = " Name: Cake"; + private static final String A_QUANTITY = " Quantity: 3"; + private static final String B_QUANTITY = " Quantity: 2"; + private static final String C_QUANTITY = " Quantity: 1"; + private static final String A_PRICE = " Price: $2.00"; + private static final String B_PRICE = " Price: $1.00"; + private static final String C_PRICE = " Price: $3.00"; + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final String A_EX_DATE = " Expiry Date: 01/01/2113"; + private static final String B_EX_DATE = " Expiry Date: 13/03/2123"; + private static final String C_EX_DATE = " Expiry Date: 22/08/2013"; + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeAll + public static void setUp() { + Inventory.clear(); + LocalDate dateA = LocalDate.parse("01-01-2113", DATE_FORMAT); + LocalDate dateB = LocalDate.parse("13-03-2123", DATE_FORMAT); + LocalDate dateC = LocalDate.parse("22-08-2013", DATE_FORMAT); + + Command[] commands = { + new NewCommand("Apple", 3, 2.00, dateA), + new NewCommand("Berry", 2, 1.00, dateB), + new NewCommand("Cake", 1, 3.00, dateC) + }; + for (Command c : commands) { + c.execute(); + } + } + + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @Test + public void listCommand_alphabeticalAscending_correctlyConstructed() throws TrackerException { + String userInput = "list"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + LINE_SEPARATOR + + INDEX_2 + B_NAME + LINE_SEPARATOR + + INDEX_3 + C_NAME + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_alphabeticalDescending_correctlyConstructed() throws TrackerException { + String userInput = "list r/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + C_NAME + LINE_SEPARATOR + + INDEX_2 + B_NAME + LINE_SEPARATOR + + INDEX_3 + A_NAME + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_quantityAscending_correctlyConstructed() throws TrackerException { + String userInput = "list q/ sq/ sp/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + C_NAME + C_QUANTITY + C_PRICE + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_QUANTITY + B_PRICE + LINE_SEPARATOR + + INDEX_3 + A_NAME + A_QUANTITY + A_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_quantityDescending_correctlyConstructed() throws TrackerException { + String userInput = "list q/ sq/ sp/ r/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + A_QUANTITY + A_PRICE + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_QUANTITY + B_PRICE + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_QUANTITY + C_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_priceAscending_correctlyConstructed() throws TrackerException { + String userInput = "list p/ sp/ sq/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + B_NAME + B_PRICE + B_QUANTITY + LINE_SEPARATOR + + INDEX_2 + A_NAME + A_PRICE + A_QUANTITY + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_PRICE + C_QUANTITY + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_priceDescending_correctlyConstructed() throws TrackerException { + String userInput = "list p/ sp/ sq/ r/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + C_NAME + C_PRICE + C_QUANTITY + LINE_SEPARATOR + + INDEX_2 + A_NAME + A_PRICE + A_QUANTITY + LINE_SEPARATOR + + INDEX_3 + B_NAME + B_PRICE + B_QUANTITY + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_quantityBeforePrice_correctlyConstructed() throws TrackerException { + String userInput = "list q/ p/ q/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + A_QUANTITY + A_PRICE + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_QUANTITY + B_PRICE + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_QUANTITY + C_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_priceBeforeQuantity_correctlyConstructed() throws TrackerException { + String userInput = "list p/ q/ p/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + A_PRICE + A_QUANTITY + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_PRICE + B_QUANTITY + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_PRICE + C_QUANTITY + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_expiryDate_correctlyConstructed() throws TrackerException { + String userInput = "list e/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + A_EX_DATE + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_EX_DATE + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_EX_DATE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_expiryDateBeforePriceQuantity_correctlyConstructed() throws TrackerException { + String userInput = "list e/ p/ q/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + A_NAME + A_EX_DATE + A_PRICE + A_QUANTITY + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_EX_DATE + B_PRICE + B_QUANTITY + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_EX_DATE + C_PRICE + C_QUANTITY + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_expiryAscending_correctlyConstructed() throws TrackerException { + String userInput = "list e/ se/ sq/ sp/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + C_NAME + C_EX_DATE + C_QUANTITY + C_PRICE + LINE_SEPARATOR + + INDEX_2 + A_NAME + A_EX_DATE + A_QUANTITY + A_PRICE + LINE_SEPARATOR + + INDEX_3 + B_NAME + B_EX_DATE + B_QUANTITY + B_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_expiryDescending_correctlyConstructed() throws TrackerException { + String userInput = "list e/ se/ sq/ sp/ r/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + B_NAME + B_EX_DATE + B_QUANTITY + B_PRICE + LINE_SEPARATOR + + INDEX_2 + A_NAME + A_EX_DATE + A_QUANTITY + A_PRICE + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_EX_DATE + C_QUANTITY + C_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_quantityAfterSortQuantity_correctlyConstructed() throws TrackerException { + String userInput = "list sq/ sp/ q/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + C_NAME + C_PRICE + C_QUANTITY + LINE_SEPARATOR + + INDEX_2 + B_NAME + B_PRICE + B_QUANTITY + LINE_SEPARATOR + + INDEX_3 + A_NAME + A_PRICE + A_QUANTITY + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @Test + public void listCommand_multipleInvalidParameters() throws TrackerException { + String userInput = "list n/sp/ sp/sp/ /sq/"; + Command c = Parser.parseCommand(userInput); + c.execute(); + String expected = LIST_INTRO + + INDEX_1 + B_NAME + B_PRICE + LINE_SEPARATOR + + INDEX_2 + A_NAME + A_PRICE + LINE_SEPARATOR + + INDEX_3 + C_NAME + C_PRICE + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } +} diff --git a/src/test/java/supertracker/command/NewCommandTest.java b/src/test/java/supertracker/command/NewCommandTest.java new file mode 100644 index 0000000000..e9604d1485 --- /dev/null +++ b/src/test/java/supertracker/command/NewCommandTest.java @@ -0,0 +1,105 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.parser.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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NewCommandTest { + @BeforeEach + public void setUp() { + Inventory.clear(); + } + + @Test + public void newCommand_validData_correctlyConstructed() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("22-08-2013", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + Command command = new NewCommand(name, quantity, price, date); + command.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(quantity, item.getQuantity()); + assertEquals(price, item.getPrice()); + } + + @Test + public void newCommand_missingParamInput() { + String userInput = "new n/Milk"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void newCommand_emptyParamInput() { + String userInput = "new n/Milk q/100 p/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void newCommand_itemAlreadyInList() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("22-08-2013", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + + newCommand.execute(); + + String userInput = "new n/milk q/100 p/5.00 e/22-08-2013"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void newCommand_quantityOrPriceLessThanZero() { + String invalidQuantityInput = "new n/milk q/-100 p/5.00"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidQuantityInput)); + + String invalidPriceInput = "new n/milk q/100 p/-5.00"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidPriceInput)); + } + + @Test + public void newCommand_invalidExpiryDate() { + String invalidExpiryDateInput = "new n/milk q/100 p/5.33 e/hello"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidExpiryDateInput)); + + String invalidExpiryDateInputNumber = "new n/milk q/100 p/5.33 e/5.33"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidExpiryDateInputNumber)); + + String invalidExpiryDateYearFormat = "new n/milk q/100 p/5.33 e/22-11-22331"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidExpiryDateYearFormat)); + + String invalidExpiryDateOrder = "new n/milk q/100 p/5.33 e/21/11/2113"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(invalidExpiryDateOrder)); + } + + @Test + public void newCommand_priceRoundedTo2Dp() throws TrackerException { + String name = "Milk"; + + String userInput = "new n/" + name + " q/100 p/5.555"; + Command command = Parser.parseCommand(userInput); + command.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(5.56, item.getPrice()); + } +} diff --git a/src/test/java/supertracker/command/ProfitCommandTest.java b/src/test/java/supertracker/command/ProfitCommandTest.java new file mode 100644 index 0000000000..f318cfc234 --- /dev/null +++ b/src/test/java/supertracker/command/ProfitCommandTest.java @@ -0,0 +1,191 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.TransactionList; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ProfitCommandTest { + public static final DateTimeFormatter VALID_USER_INPUT_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final String INVALID_EX_DATE_FORMAT = "dd-MM-yyyyy"; + private static final String INVALID_EX_DATE = "01-01-99999"; + private static final DateTimeFormatter VALID_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final LocalDate CURR_DATE = LocalDate.now(); + private static final LocalDate CURR_PLUS_ONE = CURR_DATE.plusDays(1); + private static final String CURR_PLUS_ONE_FORMATTED = CURR_PLUS_ONE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_PLUS_ONE_INPUT = CURR_PLUS_ONE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO = CURR_DATE.minusDays(2); + private static final String CURR_MINUS_TWO_FORMATTED = CURR_MINUS_TWO.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_TWO_INPUT = CURR_MINUS_TWO.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_THREE = CURR_DATE.minusDays(3); + private static final String CURR_MINUS_THREE_FORMATTED = CURR_MINUS_THREE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_THREE_INPUT = CURR_MINUS_THREE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO_WEEK = CURR_DATE.minusWeeks(2); + private static final LocalDate INVALID_DATE = LocalDate.parse + (INVALID_EX_DATE, DateTimeFormatter.ofPattern(INVALID_EX_DATE_FORMAT)); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + + /** + * Sets up the inventory and executes initial commands before running any test. + * Clears the inventory, then executes a series of commands to populate it with items. + */ + @BeforeAll + public static void setUp() { + Inventory.clear(); + TransactionList.clear(); + + Command[] commands = { + new NewCommand("orange", 20, 2.00, INVALID_DATE), + new NewCommand("apple", 10, 1.00, INVALID_DATE), + new NewCommand("banana", 5, 3.00, INVALID_DATE), + new BuyCommand("orange", 60, 1.20, CURR_DATE), + new BuyCommand("apple", 41, 1, CURR_MINUS_TWO), + new BuyCommand("banana", 30, 1.50, CURR_MINUS_TWO_WEEK), + new SellCommand("orange", 50, CURR_DATE), + new SellCommand("apple", 40, CURR_MINUS_TWO), + new SellCommand("banana", 10, CURR_MINUS_TWO_WEEK), + }; + for (Command c : commands) { + c.execute(); + } + } + + /** + * Redirects system output to a PrintStream for capturing output during tests. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + /** + * Tests the construction of profit report for today's transactions. + * Verifies that the correct output is printed based on executed commands. + */ + @Test + public void profitCommand_today_correctlyConstructed() throws TrackerException { + String userInput = "profit type/today"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (50 * 2 - 60 * 1.2); + String amountString = String.format("%.2f", amount); + + String expected = " Today's profit is $" + amountString + LINE_SEPARATOR + + " Nice! You have a profit." + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of total profit report. + * Verifies that the correct total profit report is printed based on executed commands. + */ + @Test + public void profitCommand_total_correctlyConstructed() throws TrackerException { + String userInput = "profit type/total"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (50 * 2 + 40 + 10 * 3 - 60 * 1.2 - 41 - 30 * 1.5); + String amountString = String.format("%.2f", amount); + + String expected = " Total profit is $" + amountString + LINE_SEPARATOR + + " Nice! You have a profit." + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of profit report for a specific day. + * Verifies that the correct profit report for a given day is printed. + */ + @Test + public void profitCommand_day_correctlyConstructed() throws TrackerException { + String userInput = "profit type/day from/" + CURR_MINUS_TWO_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (40 - 41); + String amountString = String.format("%.2f", amount); + + String expected = " Your profit on " + CURR_MINUS_TWO_FORMATTED + " was $" + amountString + LINE_SEPARATOR + + " You lost money." + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of profit report for a date range. + * Verifies that the correct profit report for a given date range is printed. + */ + @Test + public void profitCommand_range_correctlyConstructed() throws TrackerException { + String userInput = "profit type/range from/" + CURR_MINUS_THREE_INPUT + " to/" + CURR_PLUS_ONE_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (50 * 2 + 40 - 60 * 1.2 - 41); + String amountString = String.format("%.2f", amount); + + String expected = " Your profit between " + CURR_MINUS_THREE_FORMATTED + " and " + CURR_PLUS_ONE_FORMATTED + + " was $" + amountString + LINE_SEPARATOR + " Nice! You have a profit." + LINE_SEPARATOR;; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the behavior when the user input for profit command is incomplete. + * Verifies that a TrackerException is thrown when required parameters are missing. + */ + @Test + public void profitCommand_missingParamInput() { + String userInput = "profit type/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for profit command has too many parameters. + * Verifies that a TrackerException is thrown when unexpected parameters are provided. + */ + @Test + public void profitCommand_tooManyParamInput() { + String userInput = "profit type/total from/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for profit command is missing the flags for day task. + * Verifies that a TrackerException is thrown when essential flags are absent. + */ + @Test + public void profitCommand_missingDayFlag() { + String userInput = "profit type/day"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for profit command is missing the flag for range task. + * Verifies that a TrackerException is thrown when the range flag is missing. + */ + @Test + public void profitCommand_missingRangeFlag() { + String userInput = "profit type/range from/" + CURR_MINUS_THREE_INPUT; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/RemoveCommandTest.java b/src/test/java/supertracker/command/RemoveCommandTest.java new file mode 100644 index 0000000000..e9b107d3c5 --- /dev/null +++ b/src/test/java/supertracker/command/RemoveCommandTest.java @@ -0,0 +1,90 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; +import supertracker.parser.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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RemoveCommandTest { + @BeforeEach + public void setUp() { + Inventory.clear(); + + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("22-08-2013", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + } + + @Test + public void removeCommand_validData_correctlyConstructed(){ + String name = "Milk"; + int quantity = 100; + + int quantityToRemove = 50; + int newQuantity = quantity - quantityToRemove; + + Command removeCommand = new RemoveCommand(name, quantityToRemove); + removeCommand.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(newQuantity, item.getQuantity()); + } + + @Test + public void removeCommand_missingParamInput() { + String userInput = "remove n/Milk"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void removeCommand_emptyParamInput() { + String userInput = "remove n/Milk q/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void removeCommand_itemNotInList() { + String userInput = "remove n/Cake /50"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void removeCommand_quantityNotPositive() { + String userInput = "remove n/Milk q/0"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void removeCommand_exceedCurrentQuantity() { + String name = "Milk"; + + int quantityToRemove = 100; + int newQuantity = 0; + + Command removeCommand = new RemoveCommand(name, quantityToRemove); + removeCommand.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(newQuantity, item.getQuantity()); + } +} diff --git a/src/test/java/supertracker/command/RenameCommandTest.java b/src/test/java/supertracker/command/RenameCommandTest.java new file mode 100644 index 0000000000..9c9f7118c7 --- /dev/null +++ b/src/test/java/supertracker/command/RenameCommandTest.java @@ -0,0 +1,100 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.parser.Parser; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * JUnit test class for testing the behavior of the RenameCommand class. + */ +public class RenameCommandTest { + /** + * Clears the inventory before each test method execution. + */ + @BeforeEach + public void setUp() { + Inventory.clear(); + } + + /** + * Tests the execution of Rename Command with valid input data. + * Verifies that an item is renamed correctly with no change in quantity, price and expiry date. + */ + @Test + public void renameCommand_validData_correctlyConstructed(){ + String name = "Milk"; + String newName = "Fresh Milk"; + + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + Command renameCommand = new RenameCommand(name, newName); + renameCommand.execute(); + + assertTrue(Inventory.contains(newName)); + assertFalse(Inventory.contains(name)); + Item item = Inventory.get(newName); + assertNotNull(item); + assertEquals(newName, item.getName()); + assertEquals(quantity, item.getQuantity()); + assertEquals(price, item.getPrice()); + assertEquals(date, item.getExpiryDate()); + } + + /** + * Tests the behavior of RenameCommand when input parameters are empty. + * Verifies that a TrackerException is thrown due to missing rename parameters. + */ + @Test + public void renameCommand_emptyParamInput() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + String userInput = "rename n/ r/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior of RenameCommand when the new name input already exists in the inventory. + * Verifies that a TrackerException is thrown due to new name being used. + */ + @Test + public void renameCommand_usedNewNameInput() { + String nameA = "Milk"; + String nameB = "Coconut"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommandA = new NewCommand(nameA, quantity, price, date); + newCommandA.execute(); + Command newCommandB = new NewCommand(nameB, quantity, price, date); + newCommandB.execute(); + + String userInput = "rename n/Milk r/Coconut"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/ReportCommandTest.java b/src/test/java/supertracker/command/ReportCommandTest.java new file mode 100644 index 0000000000..43da31f7a6 --- /dev/null +++ b/src/test/java/supertracker/command/ReportCommandTest.java @@ -0,0 +1,152 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * JUnit test class for testing the behavior of the ReportCommand class. + */ +public class ReportCommandTest { + private static final String INVALID_EX_DATE_FORMAT = "dd-MM-yyyyy"; + private static final DateTimeFormatter VALID_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final String INVALID_EX_DATE = "01-01-99999"; + private static final String LINE_SEPARATOR = System.lineSeparator(); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + + /** + * Sets up the inventory and executes initial commands before running any test. + * Initializes inventory items with various expiration dates and quantities. + */ + @BeforeAll + public static void setUp() { + Inventory.clear(); + + LocalDate currDate = LocalDate.now(); + LocalDate notExpiredDate = currDate.plusWeeks(2); + LocalDate expiredDate = currDate.minusWeeks(2); + LocalDate invalidDate = LocalDate.parse(INVALID_EX_DATE, DateTimeFormatter.ofPattern(INVALID_EX_DATE_FORMAT)); + + Command[] commands = { + new NewCommand("orange", 10, 2.00, currDate), + new NewCommand("apple", 20, 1.00, notExpiredDate), + new NewCommand("banana", 30, 3.00, expiredDate), + new NewCommand("honey", 40, 10, invalidDate) + }; + for (Command c : commands) { + c.execute(); + } + } + + /** + * Redirects system output to a PrintStream for capturing output during tests. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + /** + * Tests the construction of a low stock report based on a specified threshold. + * Verifies that the correct output is printed when executing the corresponding command. + */ + @Test + public void reportCommand_lowStock_correctlyConstructed() throws TrackerException { + String userInput = "report r/low stock t/20"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + String expected = " There is 1 item low on stocks!" + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of an expiry report. + * Verifies that the correct expiry report is printed when executing the corresponding command. + */ + @Test + public void reportCommand_expiry_correctlyConstructed() throws TrackerException { + String userInput = "report r/expiry"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + LocalDate currDate = LocalDate.now(); + String dateToday = currDate.format(VALID_EX_DATE_FORMAT); + String dateTwoWeeksAgo = currDate.minusWeeks(2).format(VALID_EX_DATE_FORMAT); + + String expected = " There is 1 item close to expiry!" + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Expiry Date: " + dateToday + LINE_SEPARATOR + + " There is 1 item that is expired!" + LINE_SEPARATOR + + " 1. Name: banana" + LINE_SEPARATOR + + " Expiry Date: " + dateTwoWeeksAgo + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the behavior when the user input for report command is incomplete. + * Verifies that a TrackerException is thrown when required parameters are missing. + */ + @Test + public void reportCommand_missingParamInput() { + String userInput = "report"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for report command has an empty report parameter. + * Verifies that a TrackerException is thrown when the report parameter is empty. + */ + @Test + public void reportCommand_emptyReportParamInput() { + String userInput = "report r/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for report command lacks threshold parameter when report type + * is low stock. + * Verifies that a TrackerException is thrown when the threshold parameter is missing. + */ + @Test + public void reportCommand_emptyLowStockThresholdParamInput() { + String userInput = "report r/low stock"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for report command has an invalid threshold when report type + * is low stock. + * Verifies that a TrackerException is thrown when the threshold parameter is missing. + */ + @Test + public void reportCommand_invalidLowStockThresholdParamInput() { + String userInput = "report r/low stock, t/-2"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for report command has a threshold parameter when report type is expiry. + * Verifies that a TrackerException is thrown when the threshold parameter is invalid. + */ + @Test + public void reportCommand_notEmptyExpiryThresholdParamInput() { + String userInput = "report r/expiry t/1"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/RevenueCommandTest.java b/src/test/java/supertracker/command/RevenueCommandTest.java new file mode 100644 index 0000000000..e85e6aeb9d --- /dev/null +++ b/src/test/java/supertracker/command/RevenueCommandTest.java @@ -0,0 +1,215 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.TransactionList; +import supertracker.parser.Parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class RevenueCommandTest { + public static final DateTimeFormatter VALID_USER_INPUT_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final String INVALID_EX_DATE_FORMAT = "dd-MM-yyyyy"; + private static final String INVALID_EX_DATE = "01-01-99999"; + private static final DateTimeFormatter VALID_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final LocalDate CURR_DATE = LocalDate.now(); + private static final String CURR_DATE_FORMATTED = CURR_DATE.format(VALID_EX_DATE_FORMAT); + private static final LocalDate CURR_PLUS_ONE = CURR_DATE.plusDays(1); + private static final String CURR_PLUS_ONE_FORMATTED = CURR_PLUS_ONE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_PLUS_ONE_INPUT = CURR_PLUS_ONE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO = CURR_DATE.minusDays(2); + private static final String CURR_MINUS_TWO_FORMATTED = CURR_MINUS_TWO.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_TWO_INPUT = CURR_MINUS_TWO.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_THREE = CURR_DATE.minusDays(3); + private static final String CURR_MINUS_THREE_FORMATTED = CURR_MINUS_THREE.format(VALID_EX_DATE_FORMAT); + private static final String CURR_MINUS_THREE_INPUT = CURR_MINUS_THREE.format(VALID_USER_INPUT_EX_DATE_FORMAT); + private static final LocalDate CURR_MINUS_TWO_WEEK = CURR_DATE.minusWeeks(2); + private static final String CURR_MINUS_TWO_WEEK_FORMATTED = CURR_MINUS_TWO_WEEK.format(VALID_EX_DATE_FORMAT); + private static final LocalDate INVALID_DATE = LocalDate.parse + (INVALID_EX_DATE, DateTimeFormatter.ofPattern(INVALID_EX_DATE_FORMAT)); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + + /** + * Sets up the inventory and executes initial commands before running any test. + * Clears the inventory, then executes a series of commands to populate it with items. + */ + @BeforeAll + public static void setUp() { + Inventory.clear(); + TransactionList.clear(); + + Command[] commands = { + new NewCommand("orange", 20, 2.00, INVALID_DATE), + new NewCommand("apple", 10, 1.00, INVALID_DATE), + new NewCommand("banana", 5, 3.00, INVALID_DATE), + new SellCommand("orange", 20, CURR_DATE), + new SellCommand("apple", 10, CURR_MINUS_TWO), + new SellCommand("banana", 5, CURR_MINUS_TWO_WEEK), + }; + for (Command c : commands) { + c.execute(); + } + } + + /** + * Redirects system output to a PrintStream for capturing output during tests. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + /** + * Tests the construction of revenue report for today's transactions. + * Verifies that the correct output is printed based on executed commands. + */ + @Test + public void revenueCommand_today_correctlyConstructed() throws TrackerException { + String userInput = "rev type/today"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (20 * 2); + String amountString = String.format("%.2f", amount); + + String expected = " Today's revenue is $" + amountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $2.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR; + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of total revenue report. + * Verifies that the correct total revenue report is printed based on executed commands. + */ + @Test + public void revenueCommand_total_correctlyConstructed() throws TrackerException { + String userInput = "rev type/total"; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (20 * 2 + 10 * 1 + 5 * 3); + String amountString = String.format("%.2f", amount); + + String expected = " Total revenue is $" + amountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $2.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR + + " 2. Name: apple" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR + + " 3. Name: banana" + LINE_SEPARATOR + + " Quantity: 5" + LINE_SEPARATOR + + " Price: $3.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_WEEK_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of revenue report for a specific day. + * Verifies that the correct revenue report for a given day is printed. + */ + @Test + public void revenueCommand_day_correctlyConstructed() throws TrackerException { + String userInput = "rev type/day from/" + CURR_MINUS_TWO_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (10 * 1); + String amountString = String.format("%.2f", amount); + + String expected = " revenue on " + CURR_MINUS_TWO_FORMATTED + " was $" + amountString + LINE_SEPARATOR + + " 1. Name: apple" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the construction of revenue report for a date range. + * Verifies that the correct revenue report for a given date range is printed. + */ + @Test + public void revenueCommand_range_correctlyConstructed() throws TrackerException { + String userInput = "rev type/range from/" + CURR_MINUS_THREE_INPUT + " to/" + CURR_PLUS_ONE_INPUT; + Command c = Parser.parseCommand(userInput); + c.execute(); + + Double amount = (double) (20 * 2 + 10 * 1); + String amountString = String.format("%.2f", amount); + + String expected = " revenue between " + CURR_MINUS_THREE_FORMATTED + " and " + CURR_PLUS_ONE_FORMATTED + + " was $" + amountString + LINE_SEPARATOR + + " 1. Name: orange" + LINE_SEPARATOR + + " Quantity: 20" + LINE_SEPARATOR + + " Price: $2.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_DATE_FORMATTED + LINE_SEPARATOR + + " 2. Name: apple" + LINE_SEPARATOR + + " Quantity: 10" + LINE_SEPARATOR + + " Price: $1.00" + LINE_SEPARATOR + + " Transaction Date: " + CURR_MINUS_TWO_FORMATTED + LINE_SEPARATOR; + + String actual = outContent.toString(); + assertEquals(expected, actual); + } + + /** + * Tests the behavior when the user input for revenue command is incomplete. + * Verifies that a TrackerException is thrown when required parameters are missing. + */ + @Test + public void revenueCommand_missingParamInput() { + String userInput = "rev type/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for revenue command has too many parameters. + * Verifies that a TrackerException is thrown when unexpected parameters are provided. + */ + @Test + public void revenueCommand_tooManyParamInput() { + String userInput = "rev type/total from/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for revenue command is missing the flags for day task. + * Verifies that a TrackerException is thrown when essential flags are absent. + */ + @Test + public void revenueCommand_missingDayFlag() { + String userInput = "rev type/day"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior when the user input for revenue command is missing the flag for range task. + * Verifies that a TrackerException is thrown when the range flag is missing. + */ + @Test + public void revenueCommand_missingRangeFlag() { + String userInput = "rev type/range from/" + CURR_MINUS_THREE_INPUT; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/command/SellCommandTest.java b/src/test/java/supertracker/command/SellCommandTest.java new file mode 100644 index 0000000000..48167e4c16 --- /dev/null +++ b/src/test/java/supertracker/command/SellCommandTest.java @@ -0,0 +1,97 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; +import supertracker.parser.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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SellCommandTest { + private static final String SELL_FLAG = "s"; + + @BeforeEach + public void setUp() { + Inventory.clear(); + TransactionList.clear(); + + String name = "Milk"; + int quantity = 100; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + } + + @Test + public void sellCommand_validData_correctlyConstructed(){ + String name = "Milk"; + double price = 5.00; + + int quantityToSell = 50; + LocalDate sellDate = LocalDate.parse("12-04-2024", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command sellCommand = new SellCommand(name, quantityToSell, sellDate); + sellCommand.execute(); + + assertEquals(1, TransactionList.size()); + Transaction transaction = TransactionList.get(0); + assertNotNull(transaction); + assertEquals(name, transaction.getName()); + assertEquals(quantityToSell, transaction.getQuantity()); + assertEquals(price, transaction.getPrice()); + assertEquals(sellDate, transaction.getTransactionDate()); + assertEquals(SELL_FLAG, transaction.getType()); + } + + @Test + public void sellCommand_missingParamInput() { + String userInput = "sell n/Milk"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void sellCommand_emptyParamInput() { + String userInput = "sell n/Milk q/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void sellCommand_itemNotInList() { + String userInput = "sell n/Cake /50"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void sellCommand_quantityNotPositive() { + String userInput = "sell n/Milk q/0"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + @Test + public void sellCommand_nothingSold() { + String name = "Cake"; + int quantity = 0; + double price = 5.00; + LocalDate date = LocalDate.parse("01-01-2113", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + int quantityToSell = 50; + LocalDate sellDate = LocalDate.parse("12-04-2024", DateTimeFormatter.ofPattern("dd-MM-yyyy")); + + Command sellCommand = new SellCommand(name, quantityToSell, sellDate); + sellCommand.execute(); + assertEquals(0, TransactionList.size()); + } +} diff --git a/src/test/java/supertracker/command/UpdateCommandTest.java b/src/test/java/supertracker/command/UpdateCommandTest.java new file mode 100644 index 0000000000..d92883a87d --- /dev/null +++ b/src/test/java/supertracker/command/UpdateCommandTest.java @@ -0,0 +1,135 @@ +package supertracker.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import supertracker.parser.Parser; +import supertracker.TrackerException; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * JUnit test class for testing the behavior of the UpdateCommand class. + */ +public class UpdateCommandTest { + /** + * Clears the inventory before each test method execution. + */ + @BeforeEach + public void setUp() { + Inventory.clear(); + } + + /** + * Tests the execution of UpdateCommand with valid input data. + * Verifies that an item is updated correctly with new quantity, price, and expiry date. + */ + @Test + public void updateCommand_validData_correctlyConstructed(){ + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + int newQuantity = 200; + double newPrice = 3.00; + LocalDate newExpiryDate = LocalDate.parse("05-12-2113", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + Command updateCommand = new UpdateCommand(name, newQuantity, newPrice, newExpiryDate); + updateCommand.execute(); + + assertTrue(Inventory.contains(name)); + Item item = Inventory.get(name); + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(newQuantity, item.getQuantity()); + assertEquals(newPrice, item.getPrice()); + assertEquals(newExpiryDate, item.getExpiryDate()); + } + + /** + * Tests the behavior of UpdateCommand with invalid input data. + * Verifies that a TrackerException is thrown when updating with invalid parameters. + */ + @Test + public void updateCommand_invalidInput() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + String userInput = "update n/Milk p/-1"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior of UpdateCommand when input parameters are empty. + * Verifies that a TrackerException is thrown due to missing update parameters. + */ + @Test + public void updateCommand_emptyParamInput() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + String userInput = "update n/Milk p/"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior of UpdateCommand when updating an item that does not exist in the inventory. + * Verifies that a TrackerException is thrown when attempting to update a non-existing item. + */ + @Test + public void updateCommand_itemNotInList() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + String userInput = "update n/apple q/20 p/3"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } + + /** + * Tests the behavior of UpdateCommand when providing an invalid expiry date format. + * Verifies that a TrackerException is thrown due to an invalid date format. + */ + @Test + public void updateCommand_invalidDateInput() { + String name = "Milk"; + int quantity = 100; + double price = 5.00; + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse("22-08-2013", dateFormat); + + Command newCommand = new NewCommand(name, quantity, price, date); + newCommand.execute(); + + String userInput = "update n/Milk p/3 e/17/23/13"; + assertThrows(TrackerException.class, () -> Parser.parseCommand(userInput)); + } +} diff --git a/src/test/java/supertracker/parser/ParserTest.java b/src/test/java/supertracker/parser/ParserTest.java new file mode 100644 index 0000000000..558f7e2d40 --- /dev/null +++ b/src/test/java/supertracker/parser/ParserTest.java @@ -0,0 +1,107 @@ +package supertracker.parser; + +import org.junit.jupiter.api.Test; +import supertracker.TrackerException; +import supertracker.command.Command; +import supertracker.command.InvalidCommand; +import supertracker.command.NewCommand; +import supertracker.command.UpdateCommand; +import supertracker.command.QuitCommand; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class ParserTest { + protected static final DateTimeFormatter DATE_FORMAT_NULL = DateTimeFormatter.ofPattern("dd-MM-yyyyy"); + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + protected static final LocalDate DATE_NOT_EXIST = LocalDate.parse("01-01-99999", DATE_FORMAT_NULL); + + + @Test + public void parseCommand_validNewCommandInput_newCommand() throws TrackerException { + String[] inputs = {"new n/apple q/50 p/99.5", "new p/99.5 q/23 n/elephant", "new q/88 n/banana p/213"}; + + for (String input : inputs) { + Command resultCommand = Parser.parseCommand(input); + assertInstanceOf(NewCommand.class, resultCommand); + } + } + + @Test + public void parseCommand_validNewCommandInputWithExpiry_newCommand() throws TrackerException { + String[] inputs = {"new n/apple q/50 p/99.5 e/22-06-2024", "new e/12-11-2033 p/99.5 q/23 n/ball", + "new q/88 e/02-12-2024 n/cookie p/1.50"}; + + for (String input : inputs) { + Command resultCommand = Parser.parseCommand(input); + assertInstanceOf(NewCommand.class, resultCommand); + } + } + + @Test + public void parseCommand_validUpdateCommandInput_updateCommand() throws TrackerException { + Command newItem = Parser.parseCommand("new n/banana milkshake q/11 p/12.2 e/13-12-2054"); + newItem.execute(); + + Command update = Parser.parseCommand("update n/banana milkshake q/15 p/9.11"); + assertInstanceOf(UpdateCommand.class, update); + update.execute(); + Item bShake = Inventory.get("banana milkshake"); + assertEquals(15, bShake.getQuantity()); + assertEquals(9.11, bShake.getPrice()); + assertEquals(LocalDate.parse("13-12-2054", DATE_FORMAT), bShake.getExpiryDate()); + + update = Parser.parseCommand("update n/banana milkshake q/6969"); + update.execute(); + bShake = Inventory.get("banana milkshake"); + assertEquals(6969, bShake.getQuantity()); + + update = Parser.parseCommand("update n/banana milkshake p/96.96"); + update.execute(); + bShake = Inventory.get("banana milkshake"); + assertEquals(96.96, bShake.getPrice()); + + update = Parser.parseCommand("update n/banana milkshake q/96 e/"); + update.execute(); + bShake = Inventory.get("banana milkshake"); + assertEquals(96.96, bShake.getPrice()); + } + + @Test + public void parseCommand_validDeleteCommandInput_deleteCommand() throws TrackerException { + + Command newItem = Parser.parseCommand("new n/strawberry q/12 p/2.2"); + newItem.execute(); + + Command deleteItem = Parser.parseCommand("delete n/strawberry"); + deleteItem.execute(); + + assertFalse(Inventory.contains("strawberry")); + } + + @Test + public void parseCommand_invalidCommandInput_invalidCommand() throws TrackerException { + + String[] inputs = {"abcdefg", "1239", "newnew n/j q/2 p/123", "elephant"}; + + for (String input : inputs) { + Command resultCommand = Parser.parseCommand(input); + assertInstanceOf(InvalidCommand.class, resultCommand); + } + } + + @Test + public void parseCommand_validQuitCommandInput_quitCommand() throws TrackerException { + + String input = "quit"; + Command resultCommand = Parser.parseCommand(input); + + assertInstanceOf(QuitCommand.class, resultCommand); + } +} diff --git a/src/test/java/supertracker/storage/ItemStorageTest.java b/src/test/java/supertracker/storage/ItemStorageTest.java new file mode 100644 index 0000000000..807cc5e14f --- /dev/null +++ b/src/test/java/supertracker/storage/ItemStorageTest.java @@ -0,0 +1,84 @@ +package supertracker.storage; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import supertracker.command.NewCommand; +import supertracker.item.Inventory; +import supertracker.item.Item; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ItemStorageTest { + private static final String INVALID_EX_DATE = "01-01-99999"; + private static final DateTimeFormatter INVALID_EX_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyyy"); + private static final LocalDate UNDEFINED_DATE = LocalDate.parse(INVALID_EX_DATE, INVALID_EX_DATE_FORMAT); + private static final LocalDate CURR_DATE = LocalDate.now(); + + @BeforeAll + public static void setUp() throws IOException { + Inventory.clear(); + Item[] newItems = { + new Item("orange", 10, 2.00, CURR_DATE), + new Item("b", -10, 2.00, CURR_DATE), + new Item("a", 10, -2.00, CURR_DATE), + new Item("6969", 50, 15.9, UNDEFINED_DATE), + new Item("a1@ lol qwe^^%qw)e", 9431, 21.57, UNDEFINED_DATE), + new Item("1_+$%$_)00", 9999999, 20.90, CURR_DATE), + new Item("cheese,,,p/,,,,", 9, 7.00, UNDEFINED_DATE), + new Item("cheese*&_yoyo*&_ ,,,q/-120,,", 9, 0, CURR_DATE), + new Item("p ,,, l", 40, 0, UNDEFINED_DATE) + }; + + for (Item newItem : newItems) { + Inventory.put(newItem.getName(), newItem); + } + ItemStorage.saveData(); + Inventory.clear(); + } + + @Test + void loadData_validData_correctlyRead() throws IOException { + ItemStorage.loadData(); + + Item[] items = { + new Item("orange", 10, 2.00, CURR_DATE), + new Item("6969", 50, 15.9, UNDEFINED_DATE), + new Item("a1@ lol qwe^^%qw)e", 9431, 21.57, UNDEFINED_DATE), + new Item("1_+$%$_)00", 9999999, 20.90, CURR_DATE), + new Item("cheese*&_p/*&_,", 9, 7.00, UNDEFINED_DATE), + new Item("cheese*&_yoyo*&_ *&_q/-120,,", 9, 0, CURR_DATE), + new Item("p*&_l", 40, 0, UNDEFINED_DATE) + }; + + for (Item item : items) { + Item loadedItem = Inventory.get(item.getName()); + assertNotNull(loadedItem); + assertEquals(item.getQuantity(), loadedItem.getQuantity()); + assertEquals(item.getPrice(), loadedItem.getPrice()); + assertEquals(item.getExpiryDate(), loadedItem.getExpiryDate()); + } + } + + @AfterAll + public static void reset() throws IOException { + Inventory.clear(); + + NewCommand[] newCommands = { + new NewCommand("Apple", 10, 2.00, UNDEFINED_DATE), + new NewCommand("Banana", 20, 3.00, CURR_DATE), + new NewCommand("Cake", 30, 4.00, UNDEFINED_DATE) + }; + + for (NewCommand newCommand : newCommands) { + newCommand.execute(); + } + + ItemStorage.saveData(); + } +} diff --git a/src/test/java/supertracker/storage/TransactionStorageTest.java b/src/test/java/supertracker/storage/TransactionStorageTest.java new file mode 100644 index 0000000000..6eebd687b4 --- /dev/null +++ b/src/test/java/supertracker/storage/TransactionStorageTest.java @@ -0,0 +1,67 @@ +package supertracker.storage; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import supertracker.item.Transaction; +import supertracker.item.TransactionList; + +import java.io.IOException; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TransactionStorageTest { + private static final LocalDate CURR_DATE = LocalDate.now(); + + @BeforeAll + public static void setUp() throws IOException { + TransactionList.clear(); + + Transaction[] transactions = { + new Transaction("apple", 10, 2.00, CURR_DATE, "b"), + new Transaction("orange", 10, 2.00, CURR_DATE.minusDays(1), "b"), + new Transaction("cool ,,, grapes", 50, 0.91, CURR_DATE, "s"), + new Transaction("cool beans", 50, 12.21, CURR_DATE.plusMonths(1), "s"), + new Transaction("cool,,,beans,,,,", 50, 12.21, CURR_DATE, "s"), + new Transaction("cool beans", 50, 88.912, CURR_DATE, "s"), + new Transaction("cool beans", -50, 88.912, CURR_DATE, "s"), + new Transaction("cool beans", 50, -88.912, CURR_DATE, "s"), + new Transaction("@#!!@#( )*889.pp", 50, 88.912, CURR_DATE, "b"), + new Transaction("@#!!@#( )*889.pp", 50, 88.912, CURR_DATE.minusYears(1), "b") + }; + + for (Transaction t : transactions) { + TransactionList.add(t); + } + + TransactionStorage.resaveCurrentTransactions(); + TransactionList.clear(); + } + + @Test + void loadTransactionData_validData_correctlyRead() throws IOException { + TransactionStorage.loadTransactionData(); + + Transaction[] transactions = { + new Transaction("apple", 10, 2.00, CURR_DATE, "b"), + new Transaction("orange", 10, 2.00, CURR_DATE.minusDays(1), "b"), + new Transaction("cool*&_grapes", 50, 0.91, CURR_DATE, "s"), + new Transaction("cool*&_beans*&_,", 50, 12.21, CURR_DATE, "s"), + new Transaction("cool beans", 50, 88.912, CURR_DATE, "s"), + new Transaction("@#!!@#( )*889.pp", 50, 88.912, CURR_DATE, "b"), + new Transaction("@#!!@#( )*889.pp", 50, 88.912, CURR_DATE.minusYears(1), "b") + }; + + for (int i = 0; i < TransactionList.size(); i++) { + Transaction loadedTransaction = TransactionList.get(i); + assertEquals(transactions[i].getName(), loadedTransaction.getName()); + assertEquals(transactions[i].getQuantity(), loadedTransaction.getQuantity()); + assertEquals(transactions[i].getPrice(), loadedTransaction.getPrice()); + assertEquals(transactions[i].getTransactionDate(), loadedTransaction.getTransactionDate()); + assertEquals(transactions[i].getType(), loadedTransaction.getType()); + } + + TransactionList.clear(); + TransactionStorage.resaveCurrentTransactions(); + } +} diff --git a/supertracker.log.1 b/supertracker.log.1 new file mode 100644 index 0000000000..be5ccc624d --- /dev/null +++ b/supertracker.log.1 @@ -0,0 +1,15 @@ + + + + + 2024-04-07T02:13:45.431101400Z + 1712456025431 + 101400 + 0 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + run + 1 + Starting SuperTracker application + diff --git a/supertracker.log.1.lck b/supertracker.log.1.lck new file mode 100644 index 0000000000..e69de29bb2 diff --git a/supertracker.log.2 b/supertracker.log.2 new file mode 100644 index 0000000000..a56d28992b --- /dev/null +++ b/supertracker.log.2 @@ -0,0 +1,67 @@ + + + + + 2024-04-07T02:26:56.985074300Z + 1712456816985 + 74300 + 0 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + run + 1 + Starting SuperTracker application + + + 2024-04-07T02:27:01.871664300Z + 1712456821871 + 664300 + 1 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + handleCommands + 1 + Command passed successfully: + supertracker.command.ListCommand@64729b1e + + + 2024-04-07T02:27:15.964401600Z + 1712456835964 + 401600 + 2 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + handleCommands + 1 + Command passed successfully: + supertracker.command.ReportCommand@2833cc44 + + + 2024-04-07T02:27:21.003998100Z + 1712456841003 + 998100 + 3 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + handleCommands + 1 + Command passed successfully: + supertracker.command.QuitCommand@27a8c74e + + + 2024-04-07T02:27:21.005073600Z + 1712456841005 + 73600 + 4 + supertracker.SuperTracker + INFO + supertracker.SuperTracker + run + 1 + Exiting SuperTracker application + + diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..235f4d539e 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,52 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling + -------------------------------------------------------------------------- + Hello, welcome to SuperTracker, how may I help you? + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + cake has been added to the inventory! + Quantity: 50 + Price: $15.60 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + blueberry has been added to the inventory! + Quantity: 200 + Price: $10.00 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + Oh no! An error has occurred + Invalid new command format! + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + There are 2 unique items in your inventory: + 1. Name: blueberry Price: $10.00 Quantity: 200 + 2. Name: cake Price: $15.60 Quantity: 50 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + cake has been successfully updated! + Quantity: 99 + Price: $15.60 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + blueberry has been successfully updated! + Quantity: 54 + Price: $18.82 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + There are 2 unique items in your inventory: + 1. Name: blueberry Quantity: 54 + 2. Name: cake Quantity: 99 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + There are 2 unique items in your inventory: + 1. Name: blueberry Price: $18.82 + 2. Name: cake Price: $15.60 + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + cake has been deleted! + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + There is 1 unique item in your inventory: + 1. Name: blueberry + -------------------------------------------------------------------------- + -------------------------------------------------------------------------- + Goodbye! + -------------------------------------------------------------------------- diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..7cf755677d 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,11 @@ -James Gosling \ No newline at end of file +new n/cake q/50 p/15.60 +new n/blueberry p/10 q/200 +new n/hello +list p/ q/ +update n/cake q/99 +update n/blueberry p/18.82 q/54 +list q/ +list p/ +delete n/cake +list +quit \ No newline at end of file