> #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