expiryDate = Optional.empty();
+ String quantityStr;
+
+ if (isValidDate(splitArguments[splitArguments.length - 2])) {
+ expiryDate = Optional.of(parseExpiryDate(splitArguments[splitArguments.length - 2]));
+ quantityStr = splitArguments[splitArguments.length - 1];
+ itemName = buildItemName(splitArguments, 0, splitArguments.length - 2);
+ } else {
+ quantityStr = splitArguments[splitArguments.length - 1];
+ itemName = buildItemName(splitArguments, 0, splitArguments.length - 1);
+ }
+
+ if (!isAPositiveInteger(quantityStr)) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY);
+ }
+
+ int quantity = Integer.parseInt(quantityStr);
+ if (quantity <= 0) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY);
+ }
+
+ return new RestockItemCommand(itemName, expiryDate, quantity);
+ }
+
+ /**
+ * Parses the user input and creates a {@code SetCostCommand} object.
+ *
+ * @param arguments A string representing the user's input for setting the cost.
+ * @return A {@code SetCostCommand} containing the parsed item name and cost.
+ * @throws PillException If the input format is invalid.
+ */
+ private SetCostCommand parseSetCostCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+ if (splitArguments.length < 2) {
+ throw new PillException(ExceptionMessages.INVALID_COST_COMMAND);
+ }
+
+ String itemName = buildItemName(splitArguments, 0, splitArguments.length - 1);
+ String costStr = splitArguments[splitArguments.length - 1];
+
+ if (!isANumber(costStr)) {
+ throw new PillException(ExceptionMessages.INVALID_COST_COMMAND);
+ }
+
+ double cost = Double.parseDouble(costStr);
+ if (cost < 0) {
+ throw new PillException(ExceptionMessages.INVALID_COST_COMMAND);
+ }
+
+ return new SetCostCommand(itemName, cost);
+ }
+
+ /**
+ * Parses the user input and creates a {@code SetPriceCommand} object.
+ *
+ * @param arguments A string representing the user's input for setting the price.
+ * @return A {@code SetPriceCommand} containing the parsed item name and price.
+ * @throws PillException If the input format is invalid.
+ */
+ private SetPriceCommand parseSetPriceCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+ if (splitArguments.length < 2) {
+ throw new PillException(ExceptionMessages.INVALID_PRICE_COMMAND);
+ }
+
+ String itemName = buildItemName(splitArguments, 0, splitArguments.length - 1);
+ String priceStr = splitArguments[splitArguments.length - 1];
+
+ if (!isANumber(priceStr)) {
+ throw new PillException(ExceptionMessages.INVALID_PRICE_COMMAND);
+ }
+
+ double price = Double.parseDouble(priceStr);
+ if (price < 0) {
+ throw new PillException(ExceptionMessages.INVALID_PRICE_COMMAND);
+ }
+
+ return new SetPriceCommand(itemName, price);
+ }
+
+ /**
+ * Parses the user input and creates an {@code AddItemCommand} object.
+ * The input is expected to contain the item name, quantity, and optional expiry date.
+ * If a valid date is found, it must be the last element in the input.
+ * Only one date and one quantity are allowed.
+ *
+ * The method loops through the input array to determine the item name, quantity, and expiry date,
+ * applying default values when necessary (e.g., quantity defaults to 1 if not specified).
+ *
+ * @param arguments A string representing the user's input for adding an item
+ * @return An {@code AddItemCommand} containing the parsed item name, quantity, and optional expiry date.
+ * @throws PillException If the input format is invalid
+ */
+ private AddItemCommand parseAddItemCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+
+ if (splitArguments.length == 0) {
+ throw new PillException(ExceptionMessages.INVALID_ADD_COMMAND);
+ }
+
+ Integer quantityIndex = null;
+ Integer dateIndex = null;
+
+ for (int i = 0; i < splitArguments.length; i++) {
+ String currentArgument = splitArguments[i];
+
+ if (isValidDate(currentArgument)) {
+ if (dateIndex != null) {
+ throw new PillException(ExceptionMessages.INVALID_ADD_COMMAND);
+ }
+ dateIndex = i;
+
+ if (i != splitArguments.length - 1) {
+ throw new PillException(ExceptionMessages.INVALID_ADD_COMMAND);
+ }
+ }
+
+ if (isANumber(currentArgument)) {
+ quantityIndex = i;
+ }
+ }
+
+ String itemName;
+ String quantityStr = null;
+ String expiryDateStr = null;
+
+ if (quantityIndex != null && quantityIndex == splitArguments.length - 1) {
+ quantityStr = splitArguments[quantityIndex];
+ expiryDateStr = null;
+ itemName = buildItemName(splitArguments, 0, quantityIndex);
+ } else if (quantityIndex != null && quantityIndex == splitArguments.length - 2
+ && isValidDate(splitArguments[quantityIndex + 1])) {
+ quantityStr = splitArguments[quantityIndex];
+ expiryDateStr = splitArguments[quantityIndex + 1];
+ itemName = buildItemName(splitArguments, 0, quantityIndex);
+ } else if (dateIndex != null && dateIndex == splitArguments.length - 1) {
+ expiryDateStr = splitArguments[dateIndex];
+ quantityStr = "1";
+ itemName = buildItemName(splitArguments, 0, dateIndex);
+ } else {
+ quantityStr = "1";
+ expiryDateStr = null;
+ itemName = buildItemName(splitArguments, 0, splitArguments.length);
+ }
+
+ /*
+ if (itemName.contains(",")) {
+ throw new PillException(ExceptionMessages.INVALID_ITEM_NAME);
+ }
+ */
+
+ if (expiryDateStr != null) {
+ if (!isValidDate(expiryDateStr)) {
+ throw new PillException(ExceptionMessages.INVALID_DATE_FORMAT);
+ }
+ }
+
+ if (!isAPositiveInteger(quantityStr)) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY);
+ }
+
+ assert !itemName.isEmpty() : "Item name should not be empty";
+
+ assert isANumber(quantityStr) : "Quantity should be a valid number";
+
+ return new AddItemCommand(itemName, parseQuantity(quantityStr), parseExpiryDate(expiryDateStr));
+ }
+
+ /**
+ * Parses the user input and creates a {@code DeleteItemCommand} object.
+ * The input is expected to contain the item name and optionally an expiry date.
+ * If a valid date is found, it must be the last element in the input.
+ *
+ * The method constructs the item name by looping through the input until a valid date is found.
+ * Any valid date found is treated as the item's expiry date.
+ *
+ * @param arguments A string representing the user's input for deleting an item
+ * @return A {@code DeleteItemCommand} containing the parsed item name and optional expiry date.
+ * @throws PillException If the input format is invalid (e.g., more than one date or no item name provided).
+ */
+ private DeleteItemCommand parseDeleteItemCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+
+ if (splitArguments.length == 0) {
+ throw new PillException(ExceptionMessages.INVALID_DELETE_COMMAND);
+ }
+
+ StringBuilder itemNameBuilder = new StringBuilder();
+ int currentIndex = 0;
+ String expiryDateStr = null;
+
+ while (currentIndex < splitArguments.length) {
+ if (isValidDate(splitArguments[currentIndex])) {
+ expiryDateStr = splitArguments[currentIndex];
+ break;
+ }
+ if (currentIndex > 0) {
+ itemNameBuilder.append(" ");
+ }
+ itemNameBuilder.append(splitArguments[currentIndex]);
+ currentIndex++;
+ }
+
+ String itemName = itemNameBuilder.toString().trim();
+
+ assert !itemName.isEmpty() : "Item name should not be empty";
+
+ LocalDate expiryDate = expiryDateStr != null ? parseExpiryDate(expiryDateStr) : null;
+
+ return new DeleteItemCommand(itemName, expiryDate);
+ }
+
+
+ /**
+ * Parses the user input to create an {@code EditItemCommand} object.
+ * The input is expected to contain the item name, the new quantity, and optionally the expiry date.
+ * The expiry date, if present, must be the second-to-last element, with the quantity being the last element.
+ *
+ * The method loops through the input to determine the item name, quantity, and optional expiry date.
+ * The quantity is mandatory for editing, while the expiry date is optional.
+ *
+ * @param arguments A string representing the user's input for editing an item.
+ * @return An {@code EditItemCommand} containing the parsed item name, quantity, and optional expiry date.
+ * @throws PillException If the input format is invalid (e.g., no quantity provided, or multiple dates found).
+ */
+ private EditItemCommand parseEditItemCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+
+ if (splitArguments.length == 0) {
+ throw new PillException(ExceptionMessages.INVALID_EDIT_COMMAND);
+ }
+
+ Integer quantityIndex = null;
+ Integer dateIndex = null;
+
+ for (int i = 0; i < splitArguments.length; i++) {
+ String currentArgument = splitArguments[i];
+
+ if (isValidDate(currentArgument)) {
+ if (dateIndex != null) {
+ throw new PillException(ExceptionMessages.INVALID_EDIT_COMMAND);
+ }
+ dateIndex = i;
+
+ if (i != splitArguments.length - 1 && i != splitArguments.length - 2) {
+ throw new PillException(ExceptionMessages.INVALID_EDIT_COMMAND);
+ }
+ }
+
+ if (isANumber(currentArgument)) {
+ quantityIndex = i;
+ }
+ }
+
+ String itemName;
+ String quantityStr = null;
+ String expiryDateStr = null;
+
+ if (quantityIndex != null && quantityIndex == splitArguments.length - 1) {
+ quantityStr = splitArguments[quantityIndex];
+ expiryDateStr = null;
+ itemName = buildItemName(splitArguments, 0, quantityIndex);
+ } else if (quantityIndex != null && quantityIndex == splitArguments.length - 2
+ && isValidDate(splitArguments[quantityIndex + 1])) {
+ quantityStr = splitArguments[quantityIndex];
+ expiryDateStr = splitArguments[quantityIndex + 1];
+ itemName = buildItemName(splitArguments, 0, quantityIndex);
+ } else {
+ throw new PillException(ExceptionMessages.INVALID_EDIT_COMMAND);
+ }
+
+ assert !itemName.isEmpty() : "Item name should not be empty";
+
+ assert isANumber(quantityStr) : "Quantity should be a valid number";
+
+ return new EditItemCommand(itemName, parseQuantity(quantityStr), parseExpiryDate(expiryDateStr));
+ }
+
+ /**
+ * Parses the user input and creates an {@code UseItemCommand} object.
+ * The input is expected to contain the item name and an optional quantity.
+ * Only one quantity is allowed.
+ *
+ * The method loops through the input array to determine the item name and quantity,
+ * applying default values when necessary (e.g., quantity defaults to 1 if not specified).
+ *
+ * @param arguments A string representing the user's input for using an item
+ * @return An {@code UseItemCommand} containing the parsed item name and quantity.
+ * @throws PillException If the input format is invalid
+ */
+ private UseItemCommand parseUseItemCommand(String arguments) throws PillException {
+ String[] splitArguments = arguments.split("\\s+");
+
+ if (splitArguments.length < 2) {
+ throw new PillException(ExceptionMessages.INVALID_USE_COMMAND);
+ }
+
+ StringBuilder itemNameBuilder = new StringBuilder();
+ int currentIndex = 0;
+ int quantity = 1; // default use amount is 1
+
+ while (currentIndex < splitArguments.length) {
+ if (isANumber(splitArguments[currentIndex])) {
+ quantity = parseQuantity(splitArguments[currentIndex]);
+ break;
+ } else if (currentIndex > 0) {
+ itemNameBuilder.append(" ");
+ }
+ itemNameBuilder.append(splitArguments[currentIndex]);
+ currentIndex++;
+ }
+
+ String itemName = itemNameBuilder.toString().trim();
+ assert !itemName.isEmpty() : "Item name should not be empty";
+
+ return new UseItemCommand(itemName, quantity);
+ }
+
+ /**
+ * Checks if the provided string represents a valid date in the format {@code YYYY-MM-DD}.
+ * This method attempts to parse the string into a {@code LocalDate} object.
+ *
+ * @param dateStr A string representing the date to be validated.
+ * @return {@code true} if the string is a valid date, {@code false} otherwise.
+ */
+ private boolean isValidDate(String dateStr) {
+ try {
+ String[] dateparts = dateStr.split("-");
+ if (Integer.parseInt(dateparts[1]) > 12 || Integer.parseInt(dateparts[1]) < 1) {
+ return false;
+ } else if (Integer.parseInt(dateparts[2]) > 31 || Integer.parseInt(dateparts[2]) < 1) {
+ return false;
+ }
+ } catch (Exception e) {
+ return false;
+ }
+
+ try {
+ LocalDate.parse(dateStr);
+ return true;
+ } catch (DateTimeParseException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Constructs the item name by concatenating the elements of the input array from startIndex to endIndex.
+ * The method ensures that the item name is built by appending each element with a space between words.
+ *
+ * @param splitArguments An array of strings representing the user's input.
+ * @param startIndex The starting index (inclusive) for building the item name.
+ * @param endIndex The ending index (exclusive) for building the item name.
+ * @return A string representing the item name, constructed from the input array.
+ */
+ private String buildItemName(String[] splitArguments, int startIndex, int endIndex) {
+ StringBuilder itemNameBuilder = new StringBuilder();
+ for (int i = startIndex; i < endIndex; i++) {
+ if (i > startIndex) {
+ itemNameBuilder.append(" ");
+ }
+ itemNameBuilder.append(splitArguments[i]);
+ }
+ return itemNameBuilder.toString().trim();
+ }
+
+ /**
+ * Parses a string representing an expiry date into a {@code LocalDate} object.
+ *
+ * @param expiryDateStr A string representing the expiry date in ISO-8601 format (yyyy-MM-dd).
+ * @return A {@code LocalDate} object representing the expiry date, or {@code null} if no expiry date is provided.
+ * @throws PillException If the expiry date string is not in the correct format.
+ */
+ private LocalDate parseExpiryDate(String expiryDateStr) throws PillException {
+ try {
+ if (expiryDateStr == null) {
+ return null;
+ }
+ return LocalDate.parse(expiryDateStr);
+ } catch (DateTimeParseException e) {
+ throw new PillException(ExceptionMessages.PARSE_DATE_ERROR);
+ }
+ }
+
+ /**
+ * Checks if a given string is a valid number (integer or decimal).
+ *
+ * @param s The string to check.
+ * @return {@code true} if the string can be parsed into a double; {@code false} otherwise.
+ */
+ private boolean isANumber(String s) {
+ try {
+ Double.parseDouble(s);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if a given string is a valid positive integer.
+ *
+ * @param s The string to check.
+ * @return {@code true} if the string can be parsed into a positive integer; {@code false} otherwise.
+ */
+ private boolean isAPositiveInteger(String s) {
+ try {
+ int number = Integer.parseInt(s);
+ return number > 0;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Parses the quantity string into an integer.
+ *
+ * @param quantityStr The string representation of the quantity to parse.
+ * @return The parsed quantity as an integer.
+ * @throws PillException If the quantity is not a valid positive integer.
+ */
+ private int parseQuantity(String quantityStr) throws PillException {
+ try {
+ int quantity = Integer.parseInt(quantityStr);
+ assert quantity > 0 : "Quantity must be positive";
+ if (quantity <= 0) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY);
+ }
+ return quantity;
+ } catch (NumberFormatException e) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY_FORMAT);
+ }
+ }
+
+ /**
+ * Returns an exit flag for the Pill bot to exit.
+ *
+ * @return The state of exit flag.
+ */
+ public boolean getExitFlag() {
+ return this.exitFlag;
+ }
+}
diff --git a/src/main/java/seedu/pill/util/PillLogger.java b/src/main/java/seedu/pill/util/PillLogger.java
new file mode 100644
index 0000000000..acc8ed3f92
--- /dev/null
+++ b/src/main/java/seedu/pill/util/PillLogger.java
@@ -0,0 +1,63 @@
+package seedu.pill.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Logger;
+import java.util.logging.Handler;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.FileHandler;
+import java.util.logging.SimpleFormatter;
+
+/**
+ * A utility class that manages a logger for the application, logging to both
+ * console and file. Logs are stored in {@code PillLog.log} under the {@code ./log/} directory.
+ * Console logging is disabled ({@code Level.OFF}), while file logging captures all levels.
+ */
+public class PillLogger {
+ private static Logger logger;
+ private static final String PATH = "./log/";
+ private static final String FILE_NAME = "PillLog.log";
+
+ /**
+ * Sets up the logger with a console handler and a file handler. Logs all messages to the file.
+ */
+ private static void setUpLogger() {
+ logger = Logger.getLogger("PillLogger");
+
+ // Disable parent handlers to prevent unintended terminal output
+ logger.setUseParentHandlers(false);
+
+ Handler consoleHandler = new ConsoleHandler();
+ consoleHandler.setFormatter(new SimpleFormatter());
+ consoleHandler.setLevel(Level.OFF);
+ logger.addHandler(consoleHandler);
+
+ try {
+ File dir = new File(PATH);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ Handler fileHandler = new FileHandler(PATH + FILE_NAME, true);
+ fileHandler.setFormatter(new SimpleFormatter());
+ fileHandler.setLevel(Level.ALL);
+ logger.addHandler(fileHandler);
+ } catch (IOException e) {
+ // Log to console and carry on with normal app execution
+ logger.log(Level.SEVERE, "Logger handler initialization failed", e);
+ }
+ }
+
+ /**
+ * Returns the logger instance, initializing it if necessary.
+ *
+ * @return the logger instance
+ */
+ public static Logger getLogger() {
+ if (logger == null) {
+ setUpLogger();
+ }
+ return logger;
+ }
+}
diff --git a/src/main/java/seedu/pill/util/Printer.java b/src/main/java/seedu/pill/util/Printer.java
new file mode 100644
index 0000000000..6b50777e1d
--- /dev/null
+++ b/src/main/java/seedu/pill/util/Printer.java
@@ -0,0 +1,58 @@
+package seedu.pill.util;
+
+import java.time.LocalDate;
+
+public final class Printer {
+ private static final String NAME = "PILL";
+ private static final String ASCII = """
+ . . . .. . . . . . . . . .:+++: .
+ :&&&&&&&&&&X ;&&&&&&&&&&&& . &&& .. .:&&: . .. . .+XXXXXXXXX: \s
+ . :&&;.. ..&&&& . $&& &&& . .. . :&&: +X+;xXXXXXXX: \s
+ :&&; .. :&&X . . $&& . . &&& .. . .. :&&: . . . ;X+;xXXXXXXXX; \s
+ . :&&; . .&&& . . $&& . &&& :&&:. . . . ..Xx;+XXXXXXXXx. \s
+ :&&; . ;&&+ . . $&& . . &&& . . . :&&: . .Xx;;XXXXXXXXX. \s
+ ..:&&X+++++$&&&x. $&& . . &&&. . :&&: . . .. .++::+xXXXXXXX:. ..\s
+ :&&&&&&&&&&. $&&.. . &&& . . :&&: . . .:+::;++++++xX; \s
+ :&&; . . $&& . . &&&. . . . :&&: . :++++++++++xx+ \s
+ . :&&; . . .... $&& . . .&&& .. :&&: .. . ++++++++++xx+ \s
+ :&&; $&&. &&& .. .. :&&: . . .+++++++++xxx. \s
+ :&&; .. :&&&&&&&&&&&& ... &&&&&&&&&&&&&.. .:&&&&&&&&&&&&$ ++++++++xxx: \s
+ .XX. . . .XXXXXXXXXXXx .XXXXXXXXXXXXX. .XXXXXXXXXXXX+ ... .++++xxx; .. . \s
+ . . . . . . .. . . . . . . . . .. .. . .\s
+ """;
+
+ /**
+ * Prints a horizontal line.
+ */
+ public static void printSpace(){
+ System.out.println("\n");
+ }
+
+ /**
+ * Initializes the bot, prints the ASCII logo.
+ * Prints expired items if any.
+ * Prints the list of items to be restocked if there are any.
+ * Finally, prints the welcome message.
+ *
+ * @param items Reference ItemMap to print restock list.
+ * @param threshold The minimum number of items before it is deemed to require replenishment.
+ */
+ public static void printInitMessage(ItemMap items, int threshold){
+ System.out.println(ASCII);
+ printSpace();
+ if (!items.isEmpty()) {
+ items.listExpiringItems(LocalDate.now());
+ items.listItemsToRestock(threshold);
+ printSpace();
+ }
+ System.out.println("Hello! I'm " + NAME + "! " + "How can I help you today?");
+ }
+
+ /**
+ * Exit bot, prints goodbye sequence.
+ */
+ public static void printExitMessage(){
+ System.out.println("Bye. Hope to see you again soon!");
+ }
+}
+
diff --git a/src/main/java/seedu/pill/util/Storage.java b/src/main/java/seedu/pill/util/Storage.java
new file mode 100644
index 0000000000..86f9adc45e
--- /dev/null
+++ b/src/main/java/seedu/pill/util/Storage.java
@@ -0,0 +1,198 @@
+package seedu.pill.util;
+
+import seedu.pill.exceptions.ExceptionMessages;
+import seedu.pill.exceptions.PillException;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+import java.util.Scanner;
+import java.util.TreeSet;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Storage class handles the storage of ItemMap objects
+ * in a file-based system, allowing for saving items and lists
+ * of items to a specified text file.
+ */
+public class Storage {
+ private static final String PATH = "./data/";
+ private static final String FILE_NAME = "pill.txt";
+ private static final String SEPARATOR = ",";
+
+ private static String escapeCommas(String input) {
+ return input.replace(",", "\\,");
+ }
+
+ private static String unescapeCommas(String input) {
+ return input.replace("\\,", ",");
+ }
+
+ /**
+ * Initializes the storage file and creates the necessary
+ * directories if they do not exist.
+ *
+ * @return The File object representing the storage file.
+ * @throws IOException if an I/O error occurs during file creation.
+ */
+ private static File initializeFile() throws IOException {
+ File dir = new File(PATH);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ assert dir.isDirectory();
+
+ File items = new File(dir, FILE_NAME);
+ if (!items.exists()) {
+ items.createNewFile();
+ }
+ assert items.isFile();
+
+ return items;
+ }
+
+ /**
+ * Saves the provided ItemMap to the storage file, overwriting
+ * existing content.
+ *
+ * @param itemMap The {@link ItemMap} containing items to be saved.
+ * @throws PillException if an error occurs during the saving process.
+ */
+ public void saveItemMap(ItemMap itemMap) throws PillException {
+ try {
+ File file = initializeFile();
+ FileWriter fw = new FileWriter(file);
+
+ for (String itemName : itemMap.items.keySet()) {
+ TreeSet- itemSet = itemMap.items.get(itemName);
+ for (Item item : itemSet) {
+ fw.write(escapeCommas(item.getName()) + SEPARATOR + item.getQuantity());
+
+ fw.write(SEPARATOR + escapeCommas(item.getExpiryDate().map(LocalDate::toString).orElse("")));
+ fw.write(SEPARATOR + escapeCommas(item.getCost() > 0 ?
+ String.format("%.2f", item.getCost()) : ""));
+ fw.write(SEPARATOR + escapeCommas((item.getPrice() > 0 ?
+ String.format("%.2f", item.getPrice()) : "")));
+
+ fw.write(System.lineSeparator());
+ }
+ }
+
+ fw.close();
+ } catch (IOException e) {
+ throw new PillException(ExceptionMessages.SAVE_ERROR);
+ }
+ }
+
+ /**
+ * Appends a single item to the storage file.
+ *
+ * @param item The {@link Item} to be saved.
+ * @throws PillException if an error occurs during the saving process.
+ */
+ public void saveItem(Item item) throws PillException {
+ try {
+ File file = initializeFile();
+ FileWriter fw = new FileWriter(file, true);
+ fw.write(escapeCommas(item.getName()) + SEPARATOR + item.getQuantity());
+
+ fw.write(SEPARATOR + escapeCommas(item.getExpiryDate().map(LocalDate::toString).orElse("")));
+ fw.write(SEPARATOR + escapeCommas(item.getCost() > 0 ?
+ String.format("%.2f", item.getCost()) : ""));
+ fw.write(SEPARATOR + escapeCommas((item.getPrice() > 0 ?
+ String.format("%.2f", item.getPrice()) : "")));
+
+ fw.write(System.lineSeparator());
+ fw.close();
+ } catch (IOException e) {
+ throw new PillException(ExceptionMessages.SAVE_ERROR);
+ }
+ }
+
+ /**
+ * Loads saved CSV data into an ItemMap
+ *
+ * @return The ItemMap containing saved items
+ */
+ public ItemMap loadData() {
+ ItemMap loadedItems = new ItemMap();
+ try {
+ File file = initializeFile();
+ Scanner scanner = new Scanner(file);
+ while (scanner.hasNextLine()) {
+ try {
+ String line = scanner.nextLine();
+ Item item = loadLine(line);
+ loadedItems.addItemSilent(item);
+ } catch (PillException e) {
+ PillException.printException(e);
+ }
+ }
+ scanner.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return loadedItems;
+ }
+
+ /**
+ * Returns data in current line as an Item
+ * @param line Next line string read by the scanner
+ * @return The item present in the line
+ * @throws PillException if format of saved data is incorrect
+ */
+ public Item loadLine(String line) throws PillException {
+ Item item;
+ List data = new ArrayList<>();
+ StringBuilder currentField = new StringBuilder();
+
+ int unescapedCommaCount = 0;
+ boolean inEscape = false;
+
+ // Loop through each character in the line
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+
+ if (c == '\\' && !inEscape) {
+ inEscape = true;
+ } else if (c == ',' && !inEscape) {
+ unescapedCommaCount++;
+ if (unescapedCommaCount > 4) {
+ throw new PillException(ExceptionMessages.INVALID_LINE_FORMAT);
+ }
+
+ data.add(currentField.toString());
+ currentField.setLength(0); // Clear current field
+ } else {
+ currentField.append(c);
+ inEscape = false;
+ }
+ }
+
+ // Add the last field
+ data.add(currentField.toString());
+
+ // Parse fields as before, handling potential exceptions
+ try {
+ String name = data.get(0); // Already unescaped
+ int quantity = Integer.parseInt(data.get(1));
+ LocalDate expiryDate = data.size() > 2 && !data.get(2).isEmpty() ? LocalDate.parse(data.get(2)) : null;
+ double cost = data.size() > 3 && !data.get(3).isEmpty() ? Double.parseDouble(data.get(3)) : 0;
+ double price = data.size() > 4 && !data.get(4).isEmpty() ? Double.parseDouble(data.get(4)) : 0;
+
+ item = new Item(name, quantity, expiryDate, cost, price);
+ } catch (NumberFormatException e) {
+ throw new PillException(ExceptionMessages.INVALID_QUANTITY_FORMAT);
+ } catch (DateTimeParseException e) {
+ throw new PillException(ExceptionMessages.PARSE_DATE_ERROR);
+ } catch (IndexOutOfBoundsException e) {
+ throw new PillException(ExceptionMessages.INVALID_LINE_FORMAT);
+ }
+
+ return item;
+ }
+
+}
diff --git a/src/main/java/seedu/pill/util/StringMatcher.java b/src/main/java/seedu/pill/util/StringMatcher.java
new file mode 100644
index 0000000000..ca8c638a51
--- /dev/null
+++ b/src/main/java/seedu/pill/util/StringMatcher.java
@@ -0,0 +1,88 @@
+package seedu.pill.util;
+
+import java.util.List;
+
+/**
+ * A utility class that provides string matching and comparison functionality.
+ * This class implements methods to find similar strings and calculate string distances,
+ * which is particularly useful for command suggestions and error handling.
+ */
+public class StringMatcher {
+
+ /**
+ * Calculates the Levenshtein distance between two strings.
+ * The Levenshtein distance is the minimum number of single-character edits
+ * (insertions, deletions, or substitutions) required to change one string into another.
+ *
+ * @param s1 - The first string to compare
+ * @param s2 - The second string to compare
+ * @return - The minimum number of edits needed to transform s1 into s2
+ */
+ public static int levenshteinDistance(String s1, String s2) {
+ // Create a matrix with one extra row and column for empty string comparisons
+ // dp[i][j] will store the distance between the first i characters of s1 and the first j characters of s2
+ int[][] dp = new int[s1.length() + 1][s2.length() + 1];
+
+ for (int i = 0; i <= s1.length(); i++) {
+ for (int j = 0; j <= s2.length(); j++) {
+ if (i == 0) {
+ // If first string is empty, the only option is to insert all characters of second string
+ dp[i][j] = j;
+ } else if (j == 0) {
+ // If second string is empty, the only option is to delete all characters of first string
+ dp[i][j] = i;
+ } else {
+ // Calculate minimum cost for current position using three possible operations:
+ dp[i][j] = min(
+ // Substitution (or no change if characters are same)
+ dp[i - 1][j - 1] + (s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1),
+ // Deletion from s1
+ dp[i - 1][j] + 1,
+ // Insertion into s1
+ dp[i][j - 1] + 1);
+ }
+ }
+ }
+
+ // Return the final distance between the complete strings
+ return dp[s1.length()][s2.length()];
+ }
+
+ /**
+ * Helper method to find the minimum of three integers.
+ * Used by the levenshteinDistance method to determine the smallest edit distance.
+ *
+ * @param a - The first integer to compare
+ * @param b - The second integer to compare
+ * @param c - The third integer to compare
+ * @return - The smallest value among the three input integers
+ */
+ private static int min(int a, int b, int c) {
+ return Math.min(Math.min(a, b), c);
+ }
+
+ /**
+ * Finds the closest matching string from a list of valid strings.
+ * A string is considered a close match if its Levenshtein distance from the input
+ * is 2 or less. If multiple strings have the same distance, returns the first match found.
+ * The comparison is case-insensitive.
+ *
+ * @param input - The input string to find matches for
+ * @param validStrings - A list of valid strings to compare against
+ * @return - The closest matching string, or null if no match is found within distance of 2
+ */
+ public static String findClosestMatch(String input, List validStrings) {
+ String closestMatch = null;
+ int minDistance = Integer.MAX_VALUE;
+
+ for (String valid : validStrings) {
+ int distance = levenshteinDistance(input.toLowerCase(), valid.toLowerCase());
+ if (distance < minDistance && distance <= 2) { // Allow up to 2 edits
+ minDistance = distance;
+ closestMatch = valid;
+ }
+ }
+
+ return closestMatch;
+ }
+}
diff --git a/src/main/java/seedu/pill/util/Transaction.java b/src/main/java/seedu/pill/util/Transaction.java
new file mode 100644
index 0000000000..ad1d194bcf
--- /dev/null
+++ b/src/main/java/seedu/pill/util/Transaction.java
@@ -0,0 +1,130 @@
+package seedu.pill.util;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Represents a transaction in the inventory management system.
+ * Each transaction records a change in inventory, either through receiving new items (INCOMING)
+ * or dispensing existing items (OUTGOING). Transactions can be associated with an Order or
+ * can be direct/manual transactions.
+ */
+public class Transaction {
+ private final UUID id;
+ private final String itemName;
+ private final int quantity;
+ private final TransactionType type;
+ private final LocalDateTime timestamp;
+ private final String notes;
+ private final Order associatedOrder;
+
+ /**
+ * Defines the types of transactions possible in the system.
+ * INCOMING represents receiving new stock.
+ * OUTGOING represents dispensing items.
+ */
+ public enum TransactionType {
+ INCOMING,
+ OUTGOING
+ }
+
+ /**
+ * Creates a new Transaction with the specified details.
+ *
+ * @param itemName - The name of the item involved in the transaction
+ * @param quantity - The number of items involved in the transaction
+ * @param type - The type of transaction (INCOMING or OUTGOING)
+ * @param notes - Additional notes or comments about the transaction
+ * @param associatedOrder - The order associated with this transaction, if any (can be null)
+ */
+ public Transaction(String itemName, int quantity, TransactionType type, String notes, Order associatedOrder) {
+ this.id = UUID.randomUUID();
+ this.itemName = itemName;
+ this.quantity = quantity;
+ this.type = type;
+ this.timestamp = LocalDateTime.now();
+ this.notes = notes;
+ this.associatedOrder = associatedOrder;
+ }
+
+ /**
+ * Gets the unique identifier for this transaction.
+ *
+ * @return - The UUID of this transaction
+ */
+ public UUID getId() {
+ return id;
+ }
+
+ /**
+ * Gets the name of the item involved in this transaction.
+ *
+ * @return - The item name
+ */
+ public String getItemName() {
+ return itemName;
+ }
+
+ /**
+ * Gets the quantity of items involved in this transaction.
+ *
+ * @return - The quantity
+ */
+ public int getQuantity() {
+ return quantity;
+ }
+
+ /**
+ * Gets the type of this transaction (INCOMING or OUTGOING).
+ *
+ * @return - The transaction type
+ */
+ public TransactionType getType() {
+ return type;
+ }
+
+ /**
+ * Gets the timestamp when this transaction was created.
+ *
+ * @return - The creation timestamp
+ */
+ public LocalDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Gets any additional notes associated with this transaction.
+ *
+ * @return - The transaction notes
+ */
+ public String getNotes() {
+ return notes;
+ }
+
+ /**
+ * Gets the order associated with this transaction, if any.
+ *
+ * @return - The associated order, or null if this was a direct transaction
+ */
+ public Order getAssociatedOrder() {
+ return associatedOrder;
+ }
+
+ /**
+ * Returns a string representation of this transaction, including timestamp,
+ * type, quantity, item name, notes, and associated order ID (if any).
+ *
+ * @return - A formatted string representing this transaction
+ */
+ @Override
+ public String toString() {
+ return String.format("[%s] %s: %d %s - %s %s",
+ timestamp.toString(),
+ type,
+ quantity,
+ itemName,
+ notes,
+ associatedOrder != null ? "(Order: " + associatedOrder.getId() + ")" : ""
+ );
+ }
+}
diff --git a/src/main/java/seedu/pill/util/TransactionManager.java b/src/main/java/seedu/pill/util/TransactionManager.java
new file mode 100644
index 0000000000..3c832e0f2d
--- /dev/null
+++ b/src/main/java/seedu/pill/util/TransactionManager.java
@@ -0,0 +1,225 @@
+package seedu.pill.util;
+
+import seedu.pill.exceptions.ExceptionMessages;
+import seedu.pill.exceptions.PillException;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.stream.IntStream;
+
+/**
+ * Manages all transactions and orders in the inventory management system. This class serves as the central point for
+ * handling inventory movements, both incoming (purchases) and outgoing (dispensing) transactions, as well as managing
+ * orders and their fulfillment.
+ *
+ * The TransactionManager maintains a complete audit trail of all inventory changes and ensures data consistency between
+ * transactions and the actual inventory state.
+ */
+public class TransactionManager {
+ private final List transactions;
+ private final List orders;
+ private final ItemMap itemMap;
+ private final Storage storage;
+
+ /**
+ * Constructs a new TransactionManager with a reference to the system's inventory.
+ *
+ * @param itemMap - The inventory system's ItemMap instance to track and modify stock levels
+ */
+ public TransactionManager(ItemMap itemMap, Storage storage) {
+ this.transactions = new ArrayList<>();
+ this.orders = new ArrayList<>();
+ this.itemMap = itemMap;
+ this.storage = storage;
+ }
+
+ /**
+ * Creates and processes a new transaction in the system. This method handles both incoming and outgoing
+ * transactions, updating the inventory accordingly. For incoming transactions, it adds new stock to the inventory.
+ * For outgoing transactions, it verifies sufficient stock and removes items from inventory, prioritizing items with
+ * earlier expiry dates.
+ *
+ * @param itemName - The name of the item involved in the transaction
+ * @param quantity - The quantity of items being transacted
+ * @param type - The type of transaction (INCOMING or OUTGOING)
+ * @param notes - Additional notes or comments about the transaction
+ * @param associatedOrder - The order associated with this transaction, if any
+ * @return - The created Transaction object
+ * @throws PillException - If there's insufficient stock for an outgoing transaction or if any other validation
+ * fails
+ */
+ public Transaction createTransaction(String itemName, int quantity, LocalDate expiryDate,
+ Transaction.TransactionType type,
+ String notes, Order associatedOrder) throws PillException {
+
+ Transaction transaction = new Transaction(itemName, quantity, type, notes, associatedOrder);
+
+ if (type == Transaction.TransactionType.INCOMING) {
+ Item item = new Item(itemName, quantity, expiryDate);
+ itemMap.addItem(item);
+ } else {
+ itemMap.useItem(itemName, quantity);
+ }
+ storage.saveItemMap(itemMap);
+
+ transactions.add(transaction);
+ return transaction;
+ }
+
+ /**
+ * Creates a new order in the system. Orders can be either purchase orders (to receive stock) or dispense orders
+ * (to provide items to customers).
+ *
+ * @param type - The type of order (PURCHASE or DISPENSE).
+ * @param itemsToOrder - The items to be included in this order.
+ * @param notes - Any additional notes or comments about the order.
+ * @return - The created Order object.
+ */
+ public Order createOrder(Order.OrderType type, ItemMap itemsToOrder, String notes) {
+ Order order = new Order(type, itemsToOrder, notes);
+ orders.add(order);
+ System.out.println("Order placed! Listing order details");
+ order.listItems();
+ return order;
+ }
+
+ /**
+ * Fulfills a pending order by creating appropriate transactions for each item in the order. This method processes
+ * all items in the order and updates the inventory accordingly. For purchase orders, it creates incoming
+ * transactions. For dispense orders, it creates outgoing transactions.
+ *
+ * @param order - The order to be fulfilled
+ * @throws PillException - If the order is not in PENDING status or if there's insufficient stock for any item in a
+ * dispense order
+ */
+ public void fulfillOrder(Order order) throws PillException {
+ if (order.getStatus() != Order.OrderStatus.PENDING) {
+ throw new PillException(ExceptionMessages.ORDER_NOT_PENDING);
+ }
+
+ Transaction.TransactionType transactionType = order.getType() == Order.OrderType.PURCHASE
+ ? Transaction.TransactionType.INCOMING
+ : Transaction.TransactionType.OUTGOING;
+
+ for (Map.Entry> entry : order.getItems().items.entrySet()) {
+ TreeSet- itemSet = entry.getValue();
+ try {
+ itemSet.forEach(item -> {
+ try {
+ createTransaction(
+ item.getName(),
+ item.getQuantity(),
+ item.getExpiryDate().orElse(null),
+ transactionType,
+ "Order fulfillment",
+ order
+ );
+ } catch (PillException e) {
+ throw new RuntimeException("Error creating transaction", e);
+ }
+ });
+ } catch (RuntimeException e) {
+ throw new PillException(ExceptionMessages.TRANSACTION_ERROR);
+ }
+ }
+ order.fulfill();
+ }
+
+ /**
+ * Returns a copy of the complete transaction history.
+ *
+ * @return - A new ArrayList containing all transactions
+ */
+ public List getTransactions() {
+ return new ArrayList<>(transactions);
+ }
+
+ /**
+ * Returns a copy of all orders in the system.
+ *
+ * @return - A new ArrayList containing all orders
+ */
+ public List getOrders() {
+ return new ArrayList<>(orders);
+ }
+
+ /**
+ * Retrieves all transactions related to a specific item.
+ *
+ * @param itemName - The name of the item to find transactions for
+ * @return - A list of all transactions involving the specified item
+ */
+ public List getItemTransactions(String itemName) {
+ return transactions.stream()
+ .filter(t -> t.getItemName().equals(itemName))
+ .toList();
+ }
+
+ /**
+ * Lists all transactions by printing each transaction with a numbered format.
+ *
+ *
This method retrieves a list of {@link Transaction} objects using {@link #getTransactions()}.
+ * It then iterates through the list, printing each transaction with an index in the format "1. transaction
+ * details", "2. transaction details", etc.
+ */
+ public void listTransactions() {
+ List transactions = getTransactions();
+ if (transactions.isEmpty()) {
+ System.out.println("No transactions found");
+ } else {
+ IntStream.rangeClosed(1, transactions.size())
+ .forEach(i -> System.out.println(i + ". " + transactions.get(i - 1).toString()));
+ }
+ }
+
+ /**
+ * Lists all current orders by printing the items in each order.
+ *
+ * This method retrieves a list of {@link Order} objects using {@link #getOrders()}.
+ * It then iterates through each order, invoking {@code listItems()} on each order to print the details of its items
+ * to the console.
+ */
+ public void listOrders() {
+ List orders = getOrders();
+ if (orders.isEmpty()) {
+ System.out.println("No orders recorded...");
+ } else {
+ IntStream.rangeClosed(1, orders.size())
+ .forEach(i -> {
+ System.out.print(i + ". ");
+ orders.get(i - 1).listItems();
+ System.out.println();
+ });
+ }
+ }
+
+ /**
+ * Retrieves all transactions that occurred within a specified time period.
+ *
+ * @param start - The start date of the period (inclusive)
+ * @param end - The end date of the period (inclusive)
+ * @return - A list of transactions that occurred within the specified period
+ */
+ public List getTransactionHistory(LocalDate start, LocalDate end) {
+ return transactions.stream()
+ .filter(t -> !t.getTimestamp().toLocalDate().isBefore(start) && !t.getTimestamp().toLocalDate()
+ .isAfter(end))
+ .toList();
+ }
+
+ /**
+ * Lists the transaction history within the specified date range by printing each transaction to the console
+ * with a numbered format.
+ *
+ * @param start The start of the date range for retrieving transactions.
+ * @param end The end of the date range for retrieving transactions.
+ */
+ public void listTransactionHistory(LocalDate start, LocalDate end) {
+ List transactions = getTransactionHistory(start, end);
+ IntStream.rangeClosed(1, transactions.size())
+ .forEach(i -> System.out.println(i + ". " + transactions.get(i - 1).toString()));
+ }
+}
diff --git a/src/main/java/seedu/pill/util/Ui.java b/src/main/java/seedu/pill/util/Ui.java
new file mode 100644
index 0000000000..4632128061
--- /dev/null
+++ b/src/main/java/seedu/pill/util/Ui.java
@@ -0,0 +1,25 @@
+package seedu.pill.util;
+
+import java.util.Scanner;
+
+public final class Ui {
+ private final Scanner sc = new Scanner(System.in);
+ private final ItemMap items;
+
+ public Ui(ItemMap items) {
+ this.items = items;
+ }
+
+ /**
+ * Scans for user input.
+ * @return The user input in string representation.
+ */
+ public String getInput() {
+ Printer.printSpace();
+ return this.sc.nextLine();
+ }
+
+ public String getRawInput() {
+ return this.sc.nextLine();
+ }
+}
diff --git a/src/main/java/seedu/pill/util/Visualizer.java b/src/main/java/seedu/pill/util/Visualizer.java
new file mode 100644
index 0000000000..d0345edd45
--- /dev/null
+++ b/src/main/java/seedu/pill/util/Visualizer.java
@@ -0,0 +1,239 @@
+package seedu.pill.util;
+
+import java.awt.Frame;
+import java.util.ArrayList;
+import java.util.List;
+import org.knowm.xchart.CategoryChart;
+import org.knowm.xchart.CategoryChartBuilder;
+import org.knowm.xchart.SwingWrapper;
+import javax.swing.JFrame;
+import java.util.logging.Logger;
+
+/**
+ * The Visualizer class is responsible for rendering graphical charts to visualize item data,
+ * such as prices, costs, and stock levels. It utilizes the XChart library to generate bar charts.
+ */
+public class Visualizer {
+
+ private static final Logger LOGGER = PillLogger.getLogger();
+ private ArrayList- items;
+ private List itemNamesWithDates;
+ private List itemPrices;
+ private List itemCosts;
+ private List itemStocks;
+
+ /**
+ * Constructs a Visualizer with the specified list of items.
+ *
+ * @param items The list of items to be visualized.
+ */
+ public Visualizer(ArrayList
- items) {
+ this.items = items;
+ }
+
+ /**
+ * Processes item data for prices, preparing the item names with expiry dates (if applicable)
+ * and collecting price values for items that have a price greater than 0.
+ */
+ private void processPriceData() {
+ itemNamesWithDates = new ArrayList<>();
+ itemPrices = new ArrayList<>();
+
+ for (Item item : items) {
+ if (item.getPrice() > 0) {
+ String itemNameWithDate = item.getName();
+ if (item.getExpiryDate() != null && item.getExpiryDate().isPresent()) {
+ itemNameWithDate += " (Expires: " + item.getExpiryDate().get().toString() + ")";
+ }
+ itemNamesWithDates.add(itemNameWithDate);
+ itemPrices.add(item.getPrice());
+ }
+ }
+ }
+
+ /**
+ * Processes item data for costs, preparing the item names with expiry dates (if applicable)
+ * and collecting cost values for items that have a cost greater than 0.
+ */
+ private void processCostData() {
+ itemNamesWithDates = new ArrayList<>();
+ itemCosts = new ArrayList<>();
+
+ for (Item item : items) {
+ if (item.getCost() > 0) {
+ String itemNameWithDate = item.getName();
+ if (item.getExpiryDate() != null && item.getExpiryDate().isPresent()) {
+ itemNameWithDate += " (Expires: " + item.getExpiryDate().get().toString() + ")";
+ }
+ itemNamesWithDates.add(itemNameWithDate);
+ itemCosts.add(item.getCost());
+ }
+ }
+ }
+
+ /**
+ * Processes item data for stocks, preparing the item names with expiry dates (if applicable)
+ * and collecting stock quantities.
+ */
+ private void processStockData() {
+ itemNamesWithDates = new ArrayList<>();
+ itemStocks = new ArrayList<>();
+
+ for (Item item : items) {
+ String itemNameWithDate = item.getName();
+ if (item.getExpiryDate() != null && item.getExpiryDate().isPresent()) {
+ itemNameWithDate += " (Expires: " + item.getExpiryDate().get().toString() + ")";
+ }
+ itemNamesWithDates.add(itemNameWithDate);
+ itemStocks.add(item.getQuantity());
+ }
+ }
+
+ /**
+ * Processes item data for cost and price comparison, preparing item names with expiry dates
+ * (if applicable) and collecting both cost and price values for items that have both values greater than 0.
+ */
+ private void processCostPriceData() {
+ itemNamesWithDates = new ArrayList<>();
+ itemPrices = new ArrayList<>();
+ itemCosts = new ArrayList<>();
+
+ for (Item item : items) {
+ if (item.getCost() > 0 && item.getPrice() > 0) {
+ String itemNameWithDate = item.getName();
+ if (item.getExpiryDate() != null && item.getExpiryDate().isPresent()) {
+ itemNameWithDate += " (Expires: " + item.getExpiryDate().get().toString() + ")";
+ }
+ itemNamesWithDates.add(itemNameWithDate);
+ itemPrices.add(item.getPrice());
+ itemCosts.add(item.getCost());
+ }
+ }
+ }
+
+ /**
+ * Draws a bar chart comparing item costs and prices.
+ * The chart displays two bars for each item: one for cost and one for price.
+ */
+ public void drawCostPriceChart() {
+ LOGGER.info("Drawing Cost-Price Chart");
+ processCostPriceData();
+
+ CategoryChart costPriceChart = new CategoryChartBuilder()
+ .width(900)
+ .height(650)
+ .title("Cost and Price Comparison Chart")
+ .xAxisTitle("Item Name (with Expiry Date)")
+ .yAxisTitle("Value")
+ .build();
+
+ costPriceChart.getStyler().setLegendVisible(true);
+ costPriceChart.getStyler().setPlotGridLinesVisible(true);
+ costPriceChart.getStyler().setXAxisLabelRotation(45);
+
+ costPriceChart.addSeries("Price", itemNamesWithDates, itemPrices);
+ costPriceChart.addSeries("Cost", itemNamesWithDates, itemCosts);
+
+ SwingWrapper swingWrapper = new SwingWrapper<>(costPriceChart);
+ JFrame chartFrame = swingWrapper.displayChart();
+ chartFrame.setTitle("Cost and Price Comparison Chart");
+ chartFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ }
+
+ /**
+ * Draws a bar chart for item prices.
+ * The chart displays each item with its name, expiry date (if applicable), and price.
+ */
+ public void drawPriceChart() {
+ LOGGER.info("Drawing Price Chart");
+ processPriceData();
+
+ CategoryChart priceChart = new CategoryChartBuilder()
+ .width(900)
+ .height(650)
+ .title("Item Prices Chart")
+ .xAxisTitle("Item Name (with Expiry Date)")
+ .yAxisTitle("Price")
+ .build();
+
+ priceChart.getStyler().setLegendVisible(false);
+ priceChart.getStyler().setPlotGridLinesVisible(true);
+ priceChart.getStyler().setXAxisLabelRotation(45);
+
+ priceChart.addSeries("Price", itemNamesWithDates, itemPrices);
+
+ SwingWrapper swingWrapper = new SwingWrapper<>(priceChart);
+ JFrame chartFrame = swingWrapper.displayChart();
+ chartFrame.setTitle("Item Prices Chart");
+ chartFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ }
+
+ /**
+ * Draws a bar chart for item costs.
+ * The chart displays each item with its name, expiry date (if applicable), and cost.
+ */
+ public void drawCostChart() {
+ LOGGER.info("Drawing Cost Chart");
+ processCostData();
+
+ CategoryChart costChart = new CategoryChartBuilder()
+ .width(900)
+ .height(650)
+ .title("Item Costs Chart")
+ .xAxisTitle("Item Name (with Expiry Date)")
+ .yAxisTitle("Cost")
+ .build();
+
+ costChart.getStyler().setLegendVisible(false);
+ costChart.getStyler().setPlotGridLinesVisible(true);
+ costChart.getStyler().setXAxisLabelRotation(45);
+
+ costChart.addSeries("Cost", itemNamesWithDates, itemCosts);
+
+ SwingWrapper swingWrapper = new SwingWrapper<>(costChart);
+ JFrame chartFrame = swingWrapper.displayChart();
+ chartFrame.setTitle("Item Costs Chart");
+ chartFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ }
+
+ /**
+ * Draws a bar chart for item stock levels.
+ * The chart displays each item with its name, expiry date (if applicable), and stock quantity.
+ */
+ public void drawStockChart() {
+ LOGGER.info("Drawing Stock Chart");
+ processStockData();
+
+ CategoryChart stockChart = new CategoryChartBuilder()
+ .width(900)
+ .height(650)
+ .title("Item Stocks Chart")
+ .xAxisTitle("Item Name (with Expiry Date)")
+ .yAxisTitle("Stock")
+ .build();
+
+ stockChart.getStyler().setLegendVisible(false);
+ stockChart.getStyler().setPlotGridLinesVisible(true);
+ stockChart.getStyler().setXAxisLabelRotation(45);
+
+ stockChart.addSeries("Stock", itemNamesWithDates, itemStocks);
+
+ SwingWrapper swingWrapper = new SwingWrapper<>(stockChart);
+ JFrame chartFrame = swingWrapper.displayChart();
+ chartFrame.setTitle("Item Stocks Chart");
+ chartFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ }
+
+ public void setItems(ArrayList
- items) {
+ this.items = items;
+ }
+
+ // Add this method to close any open chart frames.
+ public void closeCharts() {
+ for (Frame frame : JFrame.getFrames()) {
+ if (frame.getTitle().contains("Chart")) { // Close frames with "Chart" in their title
+ frame.dispose();
+ }
+ }
+ }
+}
diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java
deleted file mode 100644
index 2dda5fd651..0000000000
--- a/src/test/java/seedu/duke/DukeTest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package seedu.duke;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-
-class DukeTest {
- @Test
- public void sampleTest() {
- assertTrue(true);
- }
-}
diff --git a/src/test/java/seedu/pill/PillTest.java b/src/test/java/seedu/pill/PillTest.java
new file mode 100644
index 0000000000..8690df3426
--- /dev/null
+++ b/src/test/java/seedu/pill/PillTest.java
@@ -0,0 +1,34 @@
+package seedu.pill;
+
+import seedu.pill.exceptions.PillException;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+class PillTest {
+ @Test
+ public void uiExitTest() throws PillException {
+ // Prepare input
+ String input = "exit\n";
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
+ System.setIn(inputStream);
+
+ // Capture output
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ PrintStream printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+
+ // Run the Pill program
+ Pill.main(new String[]{});
+
+ // Get the output
+ String output = outputStream.toString();
+
+ // Assert that the exit message is printed
+ assertTrue(output.contains("Bye. Hope to see you again soon!"));
+ }
+}
diff --git a/src/test/java/seedu/pill/command/AddItemCommandTest.java b/src/test/java/seedu/pill/command/AddItemCommandTest.java
new file mode 100644
index 0000000000..b1b534c893
--- /dev/null
+++ b/src/test/java/seedu/pill/command/AddItemCommandTest.java
@@ -0,0 +1,72 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.Item;
+
+import java.time.LocalDate;
+import java.util.TreeSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Unit tests for AddItemCommand class.
+ */
+public class AddItemCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ }
+
+ @Test
+ public void execute_validItemWithoutExpiry_itemAddedToMap() throws PillException {
+ // Arrange
+ AddItemCommand command = new AddItemCommand("panadol", 20);
+
+ // Act
+ command.execute(itemMap, storage);
+
+ // Assert
+ TreeSet
- items = itemMap.get("panadol");
+ assertEquals(1, items.size(), "Should have exactly one item");
+ Item addedItem = items.first();
+ assertEquals("panadol", addedItem.getName(), "Name should match");
+ assertEquals(20, addedItem.getQuantity(), "Quantity should match");
+ assertFalse(addedItem.getExpiryDate().isPresent(), "Should not have expiry date");
+ }
+
+ @Test
+ public void execute_validItemWithExpiry_itemAddedToMap() throws PillException {
+ // Arrange
+ LocalDate expiryDate = LocalDate.parse("2024-12-01");
+ AddItemCommand command = new AddItemCommand("panadol", 10, expiryDate);
+
+ // Act
+ command.execute(itemMap, storage);
+
+ // Assert
+ TreeSet
- items = itemMap.get("panadol");
+ assertEquals(1, items.size(), "Should have exactly one item");
+ Item addedItem = items.first();
+ assertEquals("panadol", addedItem.getName(), "Name should match");
+ assertEquals(10, addedItem.getQuantity(), "Quantity should match");
+ assertEquals(expiryDate, addedItem.getExpiryDate().get(), "Expiry date should match");
+ }
+
+ @Test
+ public void isExit_returnsFalse() {
+ // Arrange
+ AddItemCommand command = new AddItemCommand("panadol", 1);
+
+ // Act & Assert
+ assertFalse(command.isExit(), "Should always return false");
+ }
+}
diff --git a/src/test/java/seedu/pill/command/CommandTest.java b/src/test/java/seedu/pill/command/CommandTest.java
new file mode 100644
index 0000000000..bfc755ff2d
--- /dev/null
+++ b/src/test/java/seedu/pill/command/CommandTest.java
@@ -0,0 +1,22 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class CommandTest {
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ Command command = new Command() {
+ @Override
+ public void execute(ItemMap itemMap, Storage storage) throws PillException {
+ // Test implementation
+ }
+ };
+ assertFalse(command.isExit());
+ }
+}
diff --git a/src/test/java/seedu/pill/command/DeleteItemCommandTest.java b/src/test/java/seedu/pill/command/DeleteItemCommandTest.java
new file mode 100644
index 0000000000..eea00edef5
--- /dev/null
+++ b/src/test/java/seedu/pill/command/DeleteItemCommandTest.java
@@ -0,0 +1,74 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+import java.util.TreeSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class DeleteItemCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void deleteCommand_nonexistentItem_printsError() throws PillException {
+ DeleteItemCommand command = new DeleteItemCommand("nonexistent");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Item not found: nonexistent" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void deleteCommand_multipleItemsWithDifferentExpiry_deletesCorrectItem() throws PillException {
+ // Setup
+ LocalDate date1 = LocalDate.parse("2024-12-01");
+ LocalDate date2 = LocalDate.parse("2025-12-01");
+ itemMap.addItem(new Item("panadol", 20, date1));
+ itemMap.addItem(new Item("panadol", 30, date2));
+ outputStream.reset();
+
+ // Execute
+ DeleteItemCommand command = new DeleteItemCommand("panadol", date1);
+ command.execute(itemMap, storage);
+
+ // Verify
+ TreeSet
- remainingItems = itemMap.get("panadol");
+ assertEquals(1, remainingItems.size(), "Should have one item remaining");
+ assertEquals(date2, remainingItems.first().getExpiryDate().get(),
+ "Item with incorrect expiry date was deleted");
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ DeleteItemCommand command = new DeleteItemCommand("test");
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/EditItemCommandTest.java b/src/test/java/seedu/pill/command/EditItemCommandTest.java
new file mode 100644
index 0000000000..7ab0618274
--- /dev/null
+++ b/src/test/java/seedu/pill/command/EditItemCommandTest.java
@@ -0,0 +1,120 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+import java.util.TreeSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class EditItemCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ void editCommand_existingItemWithoutExpiry_success() throws PillException {
+ // Setup
+ Item item = new Item("panadol", 20);
+ itemMap.addItem(item);
+ outputStream.reset();
+
+ // Execute
+ EditItemCommand command = new EditItemCommand("panadol", 30);
+ command.execute(itemMap, storage);
+
+ // Verify
+ assertEquals(30, itemMap.get("panadol").first().getQuantity(),
+ "Quantity should be updated");
+ String expectedOutput = "Edited item: panadol: 30 in stock" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void editCommand_existingItemWithExpiry_success() throws PillException {
+ // Setup
+ LocalDate expiryDate = LocalDate.parse("2024-12-01");
+ Item item = new Item("panadol", 20, expiryDate);
+ itemMap.addItem(item);
+ outputStream.reset();
+
+ // Execute
+ EditItemCommand command = new EditItemCommand("panadol", 30, expiryDate);
+ command.execute(itemMap, storage);
+
+ // Verify
+ assertEquals(30, itemMap.get("panadol").first().getQuantity(),
+ "Quantity should be updated");
+ String expectedOutput = "Edited item: panadol: 30 in stock, expiring: 2024-12-01"
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void editCommand_nonexistentItem_printsError() throws PillException {
+ EditItemCommand command = new EditItemCommand("nonexistent", 30);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Item not found: nonexistent" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void editCommand_multipleItemsDifferentExpiry_updatesCorrectItem() throws PillException {
+ // Setup
+ LocalDate date1 = LocalDate.parse("2024-12-01");
+ LocalDate date2 = LocalDate.parse("2025-12-01");
+ itemMap.addItem(new Item("panadol", 20, date1));
+ itemMap.addItem(new Item("panadol", 30, date2));
+ outputStream.reset();
+
+ // Execute
+ EditItemCommand command = new EditItemCommand("panadol", 40, date1);
+ command.execute(itemMap, storage);
+
+ // Verify
+ TreeSet
- items = itemMap.get("panadol");
+ boolean foundUpdatedItem = false;
+ for (Item item : items) {
+ if (item.getExpiryDate().get().equals(date1)) {
+ assertEquals(40, item.getQuantity(), "Item quantity should be updated");
+ foundUpdatedItem = true;
+ } else {
+ assertEquals(30, item.getQuantity(), "Other item should remain unchanged");
+ }
+ }
+ assertTrue(foundUpdatedItem, "Updated item should be found");
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ EditItemCommand command = new EditItemCommand("test", 1);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/ExpiredCommandTest.java b/src/test/java/seedu/pill/command/ExpiredCommandTest.java
new file mode 100644
index 0000000000..c0e6006f5c
--- /dev/null
+++ b/src/test/java/seedu/pill/command/ExpiredCommandTest.java
@@ -0,0 +1,159 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class ExpiredCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ void expiredCommand_emptyInventory_printsEmptyMessage() throws PillException {
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items that have expired." + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiredCommand_noExpiredItems_printsNoExpiredMessage() throws PillException {
+ // Add non-expired item
+ LocalDate futureDate = LocalDate.now().plusDays(30);
+ Item item = new Item("panadol", 20, futureDate);
+ itemMap.addItem(item);
+ outputStream.reset();
+
+ // Execute command
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items that have expired." + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiredCommand_hasExpiredItems_listsExpiredItems() throws PillException {
+ // Add expired item
+ LocalDate expiredDate = LocalDate.now().minusDays(1);
+ Item expiredItem = new Item("oldMed", 10, expiredDate);
+ itemMap.addItem(expiredItem);
+
+ // Add non-expired item
+ LocalDate futureDate = LocalDate.now().plusDays(30);
+ Item validItem = new Item("newMed", 20, futureDate);
+ itemMap.addItem(validItem);
+
+ outputStream.reset();
+
+ // Execute command
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that have expired" + System.lineSeparator()
+ + "1. oldMed: 10 in stock, expiring: " + expiredDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiredCommand_itemsWithNoExpiryDate_ignoresItemsWithNoExpiry() throws PillException {
+ // Add item with no expiry
+ Item noExpiryItem = new Item("med", 10);
+ itemMap.addItem(noExpiryItem);
+
+ // Add expired item
+ LocalDate expiredDate = LocalDate.now().minusDays(1);
+ Item expiredItem = new Item("oldMed", 20, expiredDate);
+ itemMap.addItem(expiredItem);
+
+ outputStream.reset();
+
+ // Execute command
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that have expired" + System.lineSeparator()
+ + "1. oldMed: 20 in stock, expiring: " + expiredDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiredCommand_mixedExpiryDates_listsOnlyExpiredItems() throws PillException {
+ // Add mix of expired, non-expired, and no expiry items
+ LocalDate expiredDate = LocalDate.now().minusDays(1);
+ LocalDate futureDate = LocalDate.now().plusDays(1);
+
+ itemMap.addItem(new Item("expiredMed", 10, expiredDate));
+ itemMap.addItem(new Item("futureMed", 20, futureDate));
+ itemMap.addItem(new Item("noExpiryMed", 30));
+
+ outputStream.reset();
+
+ // Execute command
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that have expired" + System.lineSeparator()
+ + "1. expiredMed: 10 in stock, expiring: " + expiredDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiredCommand_sameNameDifferentExpiry_handlesCorrectly() throws PillException {
+ // Add same item name with different expiry dates
+ LocalDate expiredDate = LocalDate.now().minusDays(1);
+ LocalDate futureDate = LocalDate.now().plusDays(1);
+
+ itemMap.addItem(new Item("med", 10, expiredDate));
+ itemMap.addItem(new Item("med", 20, futureDate));
+
+ outputStream.reset();
+
+ // Execute command
+ ExpiredCommand command = new ExpiredCommand();
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that have expired" + System.lineSeparator()
+ + "1. med: 10 in stock, expiring: " + expiredDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ ExpiredCommand command = new ExpiredCommand();
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/ExpiringCommandTest.java b/src/test/java/seedu/pill/command/ExpiringCommandTest.java
new file mode 100644
index 0000000000..a9a034c89c
--- /dev/null
+++ b/src/test/java/seedu/pill/command/ExpiringCommandTest.java
@@ -0,0 +1,148 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class ExpiringCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ void expiringCommand_emptyInventory_printsEmptyMessage() throws PillException {
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items expiring before " + cutOffDate + "."
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiringCommand_noExpiringItems_printsNoItemsMessage() throws PillException {
+ // Setup
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ LocalDate laterDate = LocalDate.now().plusDays(60);
+ itemMap.addItem(new Item("futureMed", 20, laterDate));
+ outputStream.reset();
+
+ // Execute
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items expiring before " + cutOffDate + "."
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiringCommand_hasExpiringItems_listsItems() throws PillException {
+ // Setup
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ LocalDate expiringDate = LocalDate.now().plusDays(20);
+ itemMap.addItem(new Item("expiringMed", 10, expiringDate));
+ outputStream.reset();
+
+ // Execute
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items expiring before " + cutOffDate + System.lineSeparator()
+ + "1. expiringMed: 10 in stock, expiring: " + expiringDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiringCommand_multipleExpiringItems_listsAllItems() throws PillException {
+ // Setup
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ LocalDate date1 = LocalDate.now().plusDays(10);
+ LocalDate date2 = LocalDate.now().plusDays(20);
+ itemMap.addItem(new Item("med1", 10, date1));
+ itemMap.addItem(new Item("med2", 20, date2));
+ outputStream.reset();
+
+ // Execute
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items expiring before " + cutOffDate + System.lineSeparator()
+ + "1. med1: 10 in stock, expiring: " + date1 + System.lineSeparator()
+ + "2. med2: 20 in stock, expiring: " + date2 + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiringCommand_mixedExpiryDates_listsOnlyExpiringItems() throws PillException {
+ // Setup
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ LocalDate expiringDate = LocalDate.now().plusDays(20);
+ LocalDate nonExpiringDate = LocalDate.now().plusDays(40);
+ itemMap.addItem(new Item("expiringMed", 10, expiringDate));
+ itemMap.addItem(new Item("nonExpiringMed", 20, nonExpiringDate));
+ itemMap.addItem(new Item("noExpiryMed", 30));
+ outputStream.reset();
+
+ // Execute
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items expiring before " + cutOffDate + System.lineSeparator()
+ + "1. expiringMed: 10 in stock, expiring: " + expiringDate + System.lineSeparator()
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void expiringCommand_itemsExpiringOnCutoffDate_notIncluded() throws PillException {
+ // Setup
+ LocalDate cutOffDate = LocalDate.now().plusDays(30);
+ itemMap.addItem(new Item("medOnCutoff", 15, cutOffDate));
+ outputStream.reset();
+
+ // Execute
+ ExpiringCommand command = new ExpiringCommand(cutOffDate);
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items expiring before " + cutOffDate + "."
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ ExpiringCommand command = new ExpiringCommand(LocalDate.now());
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/FindCommandTest.java b/src/test/java/seedu/pill/command/FindCommandTest.java
new file mode 100644
index 0000000000..637a6c2695
--- /dev/null
+++ b/src/test/java/seedu/pill/command/FindCommandTest.java
@@ -0,0 +1,117 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.Item;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for FindCommand
+ */
+public class FindCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_emptyItemMap_throwsException() {
+ FindCommand findCommand = new FindCommand("panadol");
+
+ PillException exception = assertThrows(PillException.class, () -> {
+ findCommand.execute(itemMap, storage);
+ });
+
+ assertEquals("Item not found...", exception.getMessage());
+ }
+
+ @Test
+ public void execute_exactMatch_findsItem() throws PillException {
+ itemMap.addItemSilent(new Item("panadol", 5));
+ FindCommand findCommand = new FindCommand("panadol");
+
+ findCommand.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("panadol"));
+ }
+
+ @Test
+ public void execute_partialMatch_findsItem() throws PillException {
+ itemMap.addItemSilent(new Item("panadol extra", 5));
+ FindCommand findCommand = new FindCommand("panadol");
+
+ findCommand.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("panadol extra"));
+ }
+
+ @Test
+ public void execute_caseInsensitiveMatch_findsItem() throws PillException {
+ itemMap.addItemSilent(new Item("Panadol", 5));
+ FindCommand findCommand = new FindCommand("panadol");
+
+ findCommand.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Panadol"));
+ }
+
+ @Test
+ public void execute_multipleMatches_findsAllItems() throws PillException {
+ itemMap.addItemSilent(new Item("panadol extra", 5));
+ itemMap.addItemSilent(new Item("panadol active", 3));
+ FindCommand findCommand = new FindCommand("panadol");
+
+ findCommand.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("panadol extra"));
+ assertTrue(output.contains("panadol active"));
+ }
+
+ @Test
+ public void execute_noMatch_throwsException() {
+ itemMap.addItemSilent(new Item("aspirin", 5));
+ FindCommand findCommand = new FindCommand("panadol");
+
+ PillException exception = assertThrows(PillException.class, () -> {
+ findCommand.execute(itemMap, storage);
+ });
+
+ assertEquals("Item not found...", exception.getMessage());
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ FindCommand command = new FindCommand("test");
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/FulfillCommandTest.java b/src/test/java/seedu/pill/command/FulfillCommandTest.java
new file mode 100644
index 0000000000..c061b16aa5
--- /dev/null
+++ b/src/test/java/seedu/pill/command/FulfillCommandTest.java
@@ -0,0 +1,64 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Order;
+import seedu.pill.util.TransactionManager;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for FulfillCommand
+ */
+public class FulfillCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_insufficientStock_throwsException() {
+ // Create dispense order without sufficient stock
+ ItemMap orderItems = new ItemMap();
+ orderItems.addItemSilent(new Item("Paracetamol", 10));
+ Order order = transactionManager.createOrder(Order.OrderType.DISPENSE, orderItems, "Test failure");
+
+ FulfillCommand command = new FulfillCommand(order, transactionManager);
+
+ assertThrows(PillException.class, () -> command.execute(itemMap, storage));
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ ItemMap orderItems = new ItemMap();
+ Order order = transactionManager.createOrder(Order.OrderType.PURCHASE, orderItems, "Test");
+ FulfillCommand command = new FulfillCommand(order, transactionManager);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/HelpCommandTest.java b/src/test/java/seedu/pill/command/HelpCommandTest.java
new file mode 100644
index 0000000000..56776d7f83
--- /dev/null
+++ b/src/test/java/seedu/pill/command/HelpCommandTest.java
@@ -0,0 +1,1002 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.AfterEach;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class HelpCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ System.setOut(new PrintStream(outContent));
+ }
+
+ @Test
+ void execute_emptyCommand_printsGeneralHelp() throws PillException {
+ HelpCommand command = new HelpCommand("", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Available commands:"));
+ assertTrue(output.contains("\nItem Management:"));
+ assertTrue(output.contains("\nOther Commands:"));
+ }
+
+ @Test
+ void execute_generalHelpCategories_showsAllCategories() throws PillException {
+ HelpCommand command = new HelpCommand("", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("\nItem Management:"));
+ assertTrue(output.contains("\nPrice and Cost Management:"));
+ assertTrue(output.contains("\nOrder Management:"));
+ assertTrue(output.contains("\nTransaction Management:"));
+ assertTrue(output.contains("\nOther Commands:"));
+ }
+
+ @Test
+ void execute_nullCommand_printsGeneralHelp() throws PillException {
+ HelpCommand command = new HelpCommand(null, false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Available commands:"));
+ assertTrue(output.contains("\nItem Management:"));
+ }
+
+ @Test
+ void execute_helpForHelp_printsHelpHelp() throws PillException {
+ HelpCommand command = new HelpCommand("help", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("help: Shows help information"));
+ }
+
+ @Test
+ void execute_verboseHelpForHelp_printsDetailedHelpHelp() throws PillException {
+ HelpCommand command = new HelpCommand("help", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: help [command] [-v]"));
+ assertTrue(output.contains("[command] - Optional. Specify a command to get detailed help."));
+ assertTrue(output.contains("[-v] - Optional. Show verbose output with examples."));
+ assertTrue(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_addCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("add", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("add: Adds a new item to the inventory"));
+ assertFalse(output.contains("Usage:"));
+ }
+
+ @Test
+ void execute_verboseAddHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("add", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: add "));
+ assertTrue(output.contains(" - Name of the item"));
+ assertTrue(output.contains("[quantity] - Optional: Initial quantity of the item"));
+ assertTrue(output.contains("[expiry] - Optional: Expiry date of the item"));
+ assertTrue(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_deleteCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("delete", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("delete: Removes an item from the inventory"));
+ }
+
+ @Test
+ void execute_editCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("edit", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("edit: Edits the item in the inventory"));
+ }
+
+ @Test
+ void execute_findCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("find", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("find: Finds all items with the same keyword"));
+ }
+
+ @Test
+ void execute_useCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("use", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("use: Priority removal of items from the list"));
+ }
+
+ @Test
+ void execute_restockCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock: Restocks a specified item"));
+ }
+
+ @Test
+ void execute_restockAllCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock-all", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock-all: Restocks all items below a specified threshold."));
+ assertFalse(output.contains("Usage:"));
+ }
+
+ @Test
+ void execute_costCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("cost", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("cost: Sets the cost for a specified item"));
+ }
+
+ @Test
+ void execute_priceCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("price", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("price: Sets the selling price for a specified item"));
+ }
+
+ @Test
+ void execute_orderCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("order", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("order: Creates a new purchase or dispense order"));
+ }
+
+ @Test
+ void execute_fulfillOrderCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("fulfill-order", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order"));
+ }
+
+ @Test
+ void execute_viewOrdersCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("view-orders", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("view-orders: Lists all orders"));
+ }
+
+ @Test
+ void execute_transactionsCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("transactions", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("transactions: Views all transactions"));
+ }
+
+ @Test
+ void execute_transactionHistoryCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("transaction-history", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("transaction-history: Views transaction history"));
+ }
+
+ @Test
+ void execute_verboseViewOrdersHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("view-orders", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: view-orders"));
+ assertTrue(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_verboseTransactionsHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("transactions", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: transactions"));
+ assertTrue(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_verboseTransactionHistoryHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("transaction-history", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: transaction-history "));
+ assertTrue(output.contains(" - Show transactions from this date"));
+ assertTrue(output.contains(" - Show transactions until this date"));
+ assertTrue(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_invalidCommand_suggestsSimilarCommand() throws PillException {
+ HelpCommand command = new HelpCommand("hel", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Did you mean: help?"));
+ }
+
+ @Test
+ void execute_noSimilarCommand_handleNullMatch() throws PillException {
+ HelpCommand command = new HelpCommand("xyzabc", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Unknown command: xyzabc"));
+ assertTrue(output.contains("No similar command found"));
+ assertTrue(output.contains("Available commands:"));
+ }
+
+ @Test
+ void execute_nullItemMap_throwsAssertionError() {
+ HelpCommand command = new HelpCommand("help", false);
+ assertThrows(AssertionError.class, () -> command.execute(null, storage));
+ }
+
+ @Test
+ void execute_nullStorage_throwsAssertionError() {
+ HelpCommand command = new HelpCommand("help", false);
+ assertThrows(AssertionError.class, () -> command.execute(itemMap, null));
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ HelpCommand command = new HelpCommand("help", false);
+ assertFalse(command.isExit());
+ }
+
+ @Test
+ void execute_mixedCaseCommand_handlesCorrectly() throws PillException {
+ HelpCommand command = new HelpCommand("HeLp", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("help: Shows help information"));
+ }
+
+ @Test
+ void execute_commandWithExtraSpaces_handlesCorrectly() throws PillException {
+ HelpCommand command = new HelpCommand("help -v", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: help [command] [-v]"));
+ }
+
+ @Test
+ void execute_helpWithSpecialCharacters_handlesCorrectly() throws PillException {
+ HelpCommand command = new HelpCommand("help#$%", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Unknown command:"));
+ assertTrue(output.contains("Available commands:"));
+ }
+
+ @Test
+ void execute_visualizePriceCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-price", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-price: Displays a bar chart of item prices"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_verboseVisualizePriceCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-price", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: visualize-price"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("This will display a chart of item prices"));
+ }
+
+ @Test
+ void execute_visualizeCostCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-cost", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-cost: Displays a bar chart of item costs"));
+ }
+
+ @Test
+ void execute_visualizeStockCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-stock", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-stock: Displays a bar chart of item stocks"));
+ }
+
+ @Test
+ void execute_visualizeCostPriceCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-cost-price", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-cost-price: Displays a bar chart comparing item costs and prices"));
+ }
+
+ @Test
+ void execute_verboseStockCheckCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("stock-check", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: stock-check "));
+ assertTrue(output.contains(" - Items with strictly less than this quantity will be printed"));
+ assertTrue(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_verboseExpiredCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("expired", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: expired"));
+ assertTrue(output.contains("Shows all items with expiry dates before today's date"));
+ assertTrue(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_commandWithMultipleWords_handlesCorrectly() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-cost-price verbose", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-cost-price: Displays a bar chart"));
+ }
+
+ @Test
+ void execute_similarCommandSuggestion_handlesCloseMatch() throws PillException {
+ HelpCommand command = new HelpCommand("vsualize-price", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Did you mean: visualize-price?"));
+ }
+
+ @Test
+ void execute_whitespaceOnlyCommand_printsGeneralHelp() throws PillException {
+ HelpCommand command = new HelpCommand(" ", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Available commands:"));
+ }
+
+ @Test
+ void execute_generalHelp_showsAllCommandCategories() throws PillException {
+ HelpCommand command = new HelpCommand(null, false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("\nItem Management:"));
+ assertTrue(output.contains("\nVisualization:"));
+ assertTrue(output.contains("\nPrice and Cost Management:"));
+ assertTrue(output.contains("\nOrder Management:"));
+ assertTrue(output.contains("\nTransaction Management:"));
+ assertTrue(output.contains("\nOther Commands:"));
+ }
+
+ @Test
+ void execute_verboseFlagWithInvalidCommand_showsSuggestions() throws PillException {
+ HelpCommand command = new HelpCommand("hlp", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Did you mean: help?"));
+ }
+
+ @Test
+ void execute_verboseFlagWithoutCommand_showsDetailedGeneralHelp() throws PillException {
+ HelpCommand command = new HelpCommand(null, true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Available commands:"));
+ assertTrue(output.contains("Type 'help ' for more information"));
+ assertTrue(output.contains("Type 'help -v' for verbose output"));
+ }
+
+ @Test
+ void execute_expiringCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("expiring", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("expiring: Lists all items that will expire before a specified date"));
+ assertFalse(output.contains("Usage:"));
+ }
+
+ @Test
+ void execute_verboseExpiringCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("expiring", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: expiring "));
+ assertTrue(output.contains(" - The cutoff date in yyyy-MM-dd format"));
+ assertTrue(output.contains("Shows all items with expiry dates before the specified date"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("expiring 2024-12-31"));
+ assertTrue(output.contains("This will show all items expiring before December 31, 2024"));
+ }
+
+ @Test
+ void execute_listCommand_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("list", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("list: Displays all items in the inventory"));
+ assertFalse(output.contains("Usage:"));
+ }
+
+ @Test
+ void execute_verboseListCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("list", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("Usage: list"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains(" list"));
+ }
+
+ @Test
+ void execute_verboseVisualizeCostCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-cost", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-cost: Displays a bar chart of item costs"));
+ assertTrue(output.contains("Usage: visualize-cost"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains(" visualize-cost"));
+ assertTrue(output.contains(" This will display a chart of item costs"));
+ }
+
+ @Test
+ void execute_verboseVisualizeStockCommand_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("visualize-stock", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("visualize-stock: Displays a bar chart of item stocks"));
+ assertTrue(output.contains("Usage: visualize-stock"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains(" visualize-stock"));
+ assertTrue(output.contains(" This will display a chart of item stocks"));
+ }
+
+ @Test
+ void execute_verboseFindHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("find", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("find: Finds all items with the same keyword"));
+ assertTrue(output.contains("Usage: find "));
+ assertTrue(output.contains(" - Keyword to search for in item names"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("find Aspirin"));
+ assertTrue(output.contains("Correct input format: find "));
+ }
+
+ @Test
+ void execute_verboseRestockHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock: Restocks a specified item with an optional expiry date and quantity"));
+ assertTrue(output.contains("Usage: restock [expiry-date] "));
+ assertTrue(output.contains(" - The name of the item to restock"));
+ assertTrue(output.contains("[expiry-date] - Optional. The expiry date of the item in yyyy-MM-dd format"));
+ assertTrue(output.contains("[quantity] - Optional. The quantity to restock up to. Defaults to 50"));
+ assertTrue(output.contains("Examples:"));
+ assertTrue(output.contains("restock apple 100"));
+ assertTrue(output.contains("restock orange 2025-12-12 50"));
+ }
+
+ @Test
+ void execute_verboseRestockAllHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock-all", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock-all: Restocks all items below a specified threshold"));
+ assertTrue(output.contains("Usage: restockall [threshold]"));
+ assertTrue(output.contains("[threshold] - Optional. The minimum quantity for restocking. Defaults to 50"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("restockall 100"));
+ }
+
+ @Test
+ void execute_verboseCostHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("cost", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("cost: Sets the cost for a specified item"));
+ assertTrue(output.contains("Usage: cost "));
+ assertTrue(output.contains(" - The name of the item"));
+ assertTrue(output.contains(" - The cost value to set for the item"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("cost apple 20.0"));
+ }
+
+ @Test
+ void execute_verbosePriceHelp_printsDetailedHelp() throws PillException {
+ HelpCommand command = new HelpCommand("price", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("price: Sets the selling price for a specified item"));
+ assertTrue(output.contains("Usage: price "));
+ assertTrue(output.contains(" - The name of the item"));
+ assertTrue(output.contains(" - The price value to set for the item"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("price apple 30.0"));
+ }
+
+ @Test
+ void execute_basicUseHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("use", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("use: Priority removal of items from the list"));
+ assertTrue(output.contains("Correct input format: use "));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicFindHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("find", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("find: Finds all items with the same keyword"));
+ assertTrue(output.contains("Correct input format: find "));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicRestockHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock: Restocks a specified item with an optional expiry date and quantity"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicRestockAllHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("restock-all", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("restock-all: Restocks all items below a specified threshold"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicCostHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("cost", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("cost: Sets the cost for a specified item"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicPriceHelp_printsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("price", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("price: Sets the selling price for a specified item"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_verboseDeleteHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("delete", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("delete: Removes an item from the inventory"));
+ assertTrue(output.contains("Usage: delete "));
+ assertTrue(output.contains(" - Name of the item to delete (as shown in the list)"));
+ assertTrue(output.contains(" - Expiry date of the item in yyyy/MM/dd format"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("delete Aspirin 2024-05-24"));
+ assertTrue(output.contains("Correct input format: delete "));
+ }
+
+ @Test
+ void execute_deleteHelpCaseInsensitive_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("DeLeTe", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("delete: Removes an item from the inventory"));
+ assertTrue(output.contains("Usage: delete "));
+ }
+
+ @Test
+ void execute_deleteHelpWithExtraSpaces_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("delete ", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("delete: Removes an item from the inventory"));
+ }
+
+ @Test
+ void execute_verboseEditHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("edit", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("edit: Edits the item in the inventory"));
+ assertTrue(output.contains("Usage: edit "));
+ assertTrue(output.contains(" - Name of the item to edit (as shown in the list)"));
+ assertTrue(output.contains(" - New quantity of the item"));
+ assertTrue(output.contains(" - Expiry date of the item in yyyy-MM-dd format"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("edit Aspirin 100 2024-05-24"));
+ assertTrue(output.contains("Correct input format: edit "));
+ }
+
+ @Test
+ void execute_editHelpCaseInsensitive_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("EdIt", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("edit: Edits the item in the inventory"));
+ assertTrue(output.contains("Usage: edit "));
+ }
+
+ @Test
+ void execute_editHelpWithExtraSpaces_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("edit ", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("edit: Edits the item in the inventory"));
+ }
+
+ @Test
+ void execute_deleteHelpWithInvalidFlag_showsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("delete -x", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("delete: Removes an item from the inventory"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_editHelpWithInvalidFlag_showsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("edit -x", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("edit: Edits the item in the inventory"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicOrderHelp_showsBasicInformation() throws PillException {
+ HelpCommand command = new HelpCommand("order", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("order: Creates a new purchase or dispense order."));
+
+ assertFalse(output.contains("Usage:"));
+ assertFalse(output.contains(" - Type of order: 'purchase' or 'dispense'"));
+ assertFalse(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_verboseOrderHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("order", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("order: Creates a new purchase or dispense order."));
+
+ assertTrue(output.contains("Usage:"));
+ assertTrue(output.contains("order "));
+ assertTrue(output.contains(" "));
+ assertTrue(output.contains("[item2 quantity2]"));
+ assertTrue(output.contains("[-n \"notes\"]"));
+
+ assertTrue(output.contains(" - Type of order: 'purchase' or 'dispense'"));
+ assertTrue(output.contains(" - Name of item to order"));
+ assertTrue(output.contains("- Quantity of the item"));
+ assertTrue(output.contains("-n - Optional notes about the order"));
+
+ assertTrue(output.contains("Examples:"));
+ assertTrue(output.contains("order purchase 2"));
+ assertTrue(output.contains("Aspirin 100"));
+ assertTrue(output.contains("Bandages 50"));
+ assertTrue(output.contains("-n \"Monthly stock replenishment\""));
+
+ assertTrue(output.contains("order dispense 1"));
+ assertTrue(output.contains("Paracetamol 20"));
+ assertTrue(output.contains("-n \"Emergency room request\""));
+ }
+
+ @Test
+ void execute_orderHelpCaseInsensitive_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("OrDeR", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("order: Creates a new purchase or dispense order."));
+ assertTrue(output.contains("Usage:"));
+ assertTrue(output.contains(" - Type of order: 'purchase' or 'dispense'"));
+ }
+
+ @Test
+ void execute_orderHelpWithExtraSpaces_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("order ", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("order: Creates a new purchase or dispense order."));
+ }
+
+ @Test
+ void execute_basicFulfillOrderHelp_showsBasicInformation() throws PillException {
+ HelpCommand command = new HelpCommand("fulfill-order", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order."));
+
+ assertFalse(output.contains("Usage: fulfill-order "));
+ assertFalse(output.contains("Example:"));
+ assertFalse(output.contains("Note: This will create the necessary transactions"));
+ }
+
+ @Test
+ void execute_verboseFulfillOrderHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("fulfill-order", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order."));
+
+ assertTrue(output.contains("Usage: fulfill-order "));
+ assertTrue(output.contains(" - The unique identifier of the order to fulfill"));
+
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("fulfill-order 123e4567-e89b-12d3-a456-556642440000"));
+
+ assertTrue(output.contains("Note: This will create the necessary transactions and update inventory levels"));
+ }
+
+ @Test
+ void execute_fulfillOrderHelpCaseInsensitive_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("FuLfIlL-OrDeR", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order."));
+ assertTrue(output.contains("Usage: fulfill-order "));
+ }
+
+ @Test
+ void execute_fulfillOrderHelpWithExtraSpaces_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("fulfill-order ", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order."));
+ }
+
+ @Test
+ void execute_orderHelpWithInvalidFlag_showsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("order -x", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("order: Creates a new purchase or dispense order."));
+ assertFalse(output.contains("Examples:"));
+ }
+
+ @Test
+ void execute_fulfillOrderHelpWithInvalidFlag_showsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("fulfill-order -x", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("fulfill-order: Processes and completes a pending order."));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicExitHelp_showsBasicInformation() throws PillException {
+ HelpCommand command = new HelpCommand("exit", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("exit: Exits the program."));
+
+ assertFalse(output.contains("Usage: exit"));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_verboseExitHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("exit", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("exit: Exits the program."));
+ assertTrue(output.contains("Usage: exit"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains(" exit"));
+ }
+
+ @Test
+ void execute_exitHelpCaseInsensitive_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("ExIt", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("exit: Exits the program."));
+ assertTrue(output.contains("Usage: exit"));
+ }
+
+ @Test
+ void execute_exitHelpWithExtraSpaces_showsHelp() throws PillException {
+ HelpCommand command = new HelpCommand("exit ", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("exit: Exits the program."));
+ }
+
+ @Test
+ void execute_exitHelpWithInvalidFlag_showsBasicHelp() throws PillException {
+ HelpCommand command = new HelpCommand("exit -x", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+ assertTrue(output.contains("exit: Exits the program."));
+ assertFalse(output.contains("Example:"));
+ }
+
+ @Test
+ void execute_basicStockCheckHelp_showsBasicInformation() throws PillException {
+ HelpCommand command = new HelpCommand("stock-check", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("stock-check: Displays all items in the inventory that need to be restocked."));
+ assertTrue(output.contains("Correct input format: stock-check "));
+
+ assertFalse(output.contains("Usage: stock-check "));
+ assertFalse(output.contains(" - Items with strictly less than this quantity will be printed."));
+ assertFalse(output.contains("Example:"));
+ assertFalse(output.contains("stock-check 100"));
+ }
+
+ @Test
+ void execute_verboseStockCheckHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("stock-check", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("stock-check: Displays all items in the inventory that need to be restocked."));
+ assertTrue(output.contains("Correct input format: stock-check "));
+
+ assertTrue(output.contains("Usage: stock-check "));
+ assertTrue(output.contains(" - Items with strictly less than this quantity will be printed."));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("stock-check 100"));
+ }
+
+ @Test
+ void execute_basicExpiredHelp_showsBasicInformation() throws PillException {
+ HelpCommand command = new HelpCommand("expired", false);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("expired: Lists all items that have expired as of today."));
+
+ assertFalse(output.contains("Usage: expired"));
+ assertFalse(output.contains("Shows all items with expiry dates before today's date"));
+ assertFalse(output.contains("Example:"));
+ assertFalse(output.contains("This will show all items that have passed their expiry date"));
+ }
+
+ @Test
+ void execute_verboseExpiredHelp_showsDetailedInformation() throws PillException {
+ HelpCommand command = new HelpCommand("expired", true);
+ command.execute(itemMap, storage);
+
+ String output = outContent.toString();
+
+ assertTrue(output.contains("expired: Lists all items that have expired as of today."));
+
+ assertTrue(output.contains("Usage: expired"));
+ assertTrue(output.contains("Shows all items with expiry dates before today's date"));
+ assertTrue(output.contains("Example:"));
+ assertTrue(output.contains("expired"));
+ assertTrue(output.contains("This will show all items that have passed their expiry date"));
+ }
+
+ @AfterEach
+ void restoreSystemStreams() {
+ System.setOut(originalOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/ListCommandTest.java b/src/test/java/seedu/pill/command/ListCommandTest.java
new file mode 100644
index 0000000000..6a5257575e
--- /dev/null
+++ b/src/test/java/seedu/pill/command/ListCommandTest.java
@@ -0,0 +1,53 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class ListCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ // Existing test cases
+ @Test
+ public void listCommandEmptyPasses() throws PillException {
+ ListCommand listCommand = new ListCommand();
+
+ listCommand.execute(itemMap, storage);
+
+ String expectedOutput = "The inventory is empty." + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ ListCommand command = new ListCommand();
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/OrderCommandTest.java b/src/test/java/seedu/pill/command/OrderCommandTest.java
new file mode 100644
index 0000000000..540dbedc81
--- /dev/null
+++ b/src/test/java/seedu/pill/command/OrderCommandTest.java
@@ -0,0 +1,78 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.TransactionManager;
+import seedu.pill.util.Order;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class OrderCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_purchaseOrder_createsOrder() throws PillException {
+ ItemMap itemsToOrder = new ItemMap();
+ itemsToOrder.addItemSilent(new Item("Paracetamol", 10));
+ String notes = "test";
+
+ OrderCommand command = new OrderCommand(itemsToOrder, transactionManager,
+ Order.OrderType.PURCHASE, notes);
+ command.execute(itemMap, storage);
+
+ assertEquals(1, transactionManager.getOrders().size());
+ Order createdOrder = transactionManager.getOrders().get(0);
+ assertEquals(Order.OrderType.PURCHASE, createdOrder.getType());
+ }
+
+ @Test
+ public void execute_dispenseOrder_createsOrder() throws PillException {
+ ItemMap itemsToOrder = new ItemMap();
+ itemsToOrder.addItemSilent(new Item("Paracetamol", 5));
+ String notes = "test";
+
+ OrderCommand command = new OrderCommand(itemsToOrder, transactionManager,
+ Order.OrderType.DISPENSE, notes);
+ command.execute(itemMap, storage);
+
+ assertEquals(1, transactionManager.getOrders().size());
+ Order createdOrder = transactionManager.getOrders().get(0);
+ assertEquals(Order.OrderType.DISPENSE, createdOrder.getType());
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ OrderCommand command = new OrderCommand(new ItemMap(), transactionManager,
+ Order.OrderType.PURCHASE, "");
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/RestockAllCommandTest.java b/src/test/java/seedu/pill/command/RestockAllCommandTest.java
new file mode 100644
index 0000000000..f09d47688d
--- /dev/null
+++ b/src/test/java/seedu/pill/command/RestockAllCommandTest.java
@@ -0,0 +1,69 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RestockAllCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_noItemsBelowThreshold_printsNoChanges() throws PillException {
+ itemMap.addItemSilent(new Item("Paracetamol", 20, LocalDate.now(), 5.0, 7.0));
+ itemMap.addItemSilent(new Item("Aspirin", 15, LocalDate.now(), 4.0, 6.0));
+
+ RestockAllCommand command = new RestockAllCommand(10);
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Total Restock Cost for all items below threshold 10: $0.00"));
+ }
+
+ @Test
+ public void execute_calculatesCorrectCosts() throws PillException {
+ itemMap.addItemSilent(new Item("Paracetamol", 5, LocalDate.now(), 5.0, 7.0));
+ // Need to restock 5 units at $5.0 each = $25.0
+
+ RestockAllCommand command = new RestockAllCommand(10);
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Restock Cost: $25.00"));
+ assertTrue(output.contains("Total Restock Cost for all items below threshold 10: $25.00"));
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ RestockAllCommand command = new RestockAllCommand(10);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/RestockItemCommandTest.java b/src/test/java/seedu/pill/command/RestockItemCommandTest.java
new file mode 100644
index 0000000000..c45624d8e1
--- /dev/null
+++ b/src/test/java/seedu/pill/command/RestockItemCommandTest.java
@@ -0,0 +1,53 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class RestockItemCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_itemDoesNotExist_throwsException() {
+ RestockItemCommand command = new RestockItemCommand(
+ "NonexistentItem", Optional.empty(), 10);
+
+ assertThrows(PillException.class, () -> command.execute(itemMap, storage)
+ );
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ RestockItemCommand command = new RestockItemCommand(
+ "test", Optional.empty(), 10);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/SetCostCommandTest.java b/src/test/java/seedu/pill/command/SetCostCommandTest.java
new file mode 100644
index 0000000000..e04a0dce4f
--- /dev/null
+++ b/src/test/java/seedu/pill/command/SetCostCommandTest.java
@@ -0,0 +1,50 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SetCostCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_itemDoesNotExist_throwsException() {
+ SetCostCommand command = new SetCostCommand("NonexistentItem", 10.0);
+
+ assertThrows(PillException.class, () -> command.execute(itemMap, storage)
+ );
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ SetCostCommand command = new SetCostCommand("test", 1.0);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/SetPriceCommandTest.java b/src/test/java/seedu/pill/command/SetPriceCommandTest.java
new file mode 100644
index 0000000000..48de51cc68
--- /dev/null
+++ b/src/test/java/seedu/pill/command/SetPriceCommandTest.java
@@ -0,0 +1,51 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SetPriceCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_itemDoesNotExist_throwsException() {
+ SetPriceCommand command = new SetPriceCommand("NonexistentItem", 10.0);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ SetPriceCommand command = new SetPriceCommand("test", 1.0);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/StockCheckCommandTest.java b/src/test/java/seedu/pill/command/StockCheckCommandTest.java
new file mode 100644
index 0000000000..175e2db1ad
--- /dev/null
+++ b/src/test/java/seedu/pill/command/StockCheckCommandTest.java
@@ -0,0 +1,142 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StockCheckCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ void stockCheck_emptyInventory_printsInfoMessage() throws PillException {
+ StockCheckCommand command = new StockCheckCommand("10");
+ command.execute(itemMap, storage);
+
+ assertEquals("", outputStream.toString(),
+ "Empty inventory should not produce output");
+ }
+
+ @Test
+ void stockCheck_noItemsBelowThreshold_printsInfoMessage() throws PillException {
+ // Setup
+ itemMap.addItem(new Item("med1", 20));
+ itemMap.addItem(new Item("med2", 30));
+ outputStream.reset();
+
+ // Execute
+ StockCheckCommand command = new StockCheckCommand("10");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items that have quantity less than or equal to 10:"
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void stockCheck_itemsBelowThreshold_listsItems() throws PillException {
+ // Setup
+ itemMap.addItem(new Item("lowMed", 5));
+ itemMap.addItem(new Item("highMed", 20));
+ outputStream.reset();
+
+ // Execute
+ StockCheckCommand command = new StockCheckCommand("10");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that need to be restocked (less than or equal to 10):"
+ + System.lineSeparator()
+ + "1. lowMed: 5 in stock" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void stockCheck_multipleItemsBelowThreshold_listsAllItems() throws PillException {
+ // Setup
+ itemMap.addItem(new Item("med1", 5));
+ itemMap.addItem(new Item("med2", 8));
+ itemMap.addItem(new Item("med3", 20));
+ outputStream.reset();
+
+ // Execute
+ StockCheckCommand command = new StockCheckCommand("10");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that need to be restocked (less than or equal to 10):"
+ + System.lineSeparator()
+ + "1. med1: 5 in stock" + System.lineSeparator()
+ + "2. med2: 8 in stock" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void stockCheck_itemsEqualToThreshold_included() throws PillException {
+ // Setup
+ itemMap.addItem(new Item("medAtThreshold", 10));
+ outputStream.reset();
+
+ // Execute
+ StockCheckCommand command = new StockCheckCommand("10");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "Listing all items that need to be restocked (less than or equal to 10):"
+ + System.lineSeparator()
+ + "1. medAtThreshold: 10 in stock" + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void stockCheck_nonNumericThreshold_throwsException() {
+ assertThrows(NumberFormatException.class,
+ () -> new StockCheckCommand("abc"));
+ }
+
+ @Test
+ void stockCheck_zeroThreshold_listsNoItems() throws PillException {
+ // Setup
+ itemMap.addItem(new Item("med", 5));
+ outputStream.reset();
+
+ // Execute
+ StockCheckCommand command = new StockCheckCommand("0");
+ command.execute(itemMap, storage);
+
+ String expectedOutput = "There are no items that have quantity less than or equal to 0:"
+ + System.lineSeparator();
+ assertEquals(expectedOutput, outputStream.toString());
+ }
+
+ @Test
+ void isExit_returnsAlwaysFalse() {
+ StockCheckCommand command = new StockCheckCommand("10");
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/TransactionHistoryCommandTest.java b/src/test/java/seedu/pill/command/TransactionHistoryCommandTest.java
new file mode 100644
index 0000000000..f81ed42c68
--- /dev/null
+++ b/src/test/java/seedu/pill/command/TransactionHistoryCommandTest.java
@@ -0,0 +1,79 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.TransactionManager;
+import seedu.pill.util.Transaction;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Unit tests for TransactionHistoryCommand
+ */
+public class TransactionHistoryCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_validDateRange_showsTransactions() throws PillException {
+ // Create a transaction
+ transactionManager.createTransaction("Paracetamol", 10, null,
+ Transaction.TransactionType.INCOMING, "Past transaction", null);
+
+ LocalDate start = LocalDate.now().minusDays(1);
+ LocalDate end = LocalDate.now().plusDays(1);
+
+ TransactionHistoryCommand command = new TransactionHistoryCommand(start, end, transactionManager);
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Paracetamol"));
+ assertTrue(output.contains("10"));
+ }
+
+ @Test
+ public void execute_emptyDateRange_printsNothing() throws PillException {
+ LocalDate now = LocalDate.now();
+ TransactionHistoryCommand command = new TransactionHistoryCommand(now, now, transactionManager);
+
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.isEmpty());
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ LocalDate now = LocalDate.now();
+ TransactionHistoryCommand command = new TransactionHistoryCommand(now, now, transactionManager);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/TransactionsCommandTest.java b/src/test/java/seedu/pill/command/TransactionsCommandTest.java
new file mode 100644
index 0000000000..3116aa92eb
--- /dev/null
+++ b/src/test/java/seedu/pill/command/TransactionsCommandTest.java
@@ -0,0 +1,71 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Transaction;
+import seedu.pill.util.TransactionManager;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for TransactionsCommand
+ */
+public class TransactionsCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_emptyTransactions_printsEmptyMessage() throws PillException {
+ TransactionsCommand command = new TransactionsCommand(transactionManager);
+ command.execute(itemMap, storage);
+ assertTrue(outputStream.toString().trim().contains("No transactions found"));
+ }
+
+ @Test
+ public void execute_withTransactions_listsAllTransactions() throws PillException {
+ // Create a transaction by adding an incoming transaction
+ transactionManager.createTransaction("Paracetamol", 10, null,
+ Transaction.TransactionType.INCOMING, "Test transaction", null);
+
+ TransactionsCommand command = new TransactionsCommand(transactionManager);
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Paracetamol"));
+ assertTrue(output.contains("10"));
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ TransactionsCommand command = new TransactionsCommand(transactionManager);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/UseItemCommandTest.java b/src/test/java/seedu/pill/command/UseItemCommandTest.java
new file mode 100644
index 0000000000..24f11437e2
--- /dev/null
+++ b/src/test/java/seedu/pill/command/UseItemCommandTest.java
@@ -0,0 +1,56 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.Item;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for UseItemCommand class.
+ */
+public class UseItemCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ }
+
+ public void execute_usePartialQuantityFromSingleItem_success() throws PillException {
+ // Arrange
+ Item item = new Item("Panadol", 10, LocalDate.now().plusDays(30));
+ itemMap.addItem(item);
+ UseItemCommand command = new UseItemCommand("Panadol", 3);
+
+ // Act
+ command.execute(itemMap, storage);
+
+ // Assert
+ assertEquals(7, itemMap.stockCount("Panadol"));
+ }
+
+ @Test
+ public void execute_useNonExistentItem_throwsPillException() {
+ // Arrange
+ UseItemCommand command = new UseItemCommand("NonExistentItem", 1);
+
+ // Act & Assert
+ assertThrows(PillException.class, () -> command.execute(itemMap, storage));
+ }
+
+ @Test
+ public void isExit_returnsFalse() {
+ UseItemCommand command = new UseItemCommand("AnyItem", 1);
+ assertFalse(command.isExit());
+ }
+}
diff --git a/src/test/java/seedu/pill/command/ViewOrdersCommandTest.java b/src/test/java/seedu/pill/command/ViewOrdersCommandTest.java
new file mode 100644
index 0000000000..ac03db789a
--- /dev/null
+++ b/src/test/java/seedu/pill/command/ViewOrdersCommandTest.java
@@ -0,0 +1,66 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Order;
+import seedu.pill.util.TransactionManager;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Unit tests for ViewOrdersCommand
+ */
+public class ViewOrdersCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ @Test
+ public void execute_withOrders_listsPendingOrders() throws PillException {
+ // Create an order with some items
+ ItemMap orderItems = new ItemMap();
+ orderItems.addItemSilent(new Item("Paracetamol", 10));
+ Order order = transactionManager.createOrder(Order.OrderType.PURCHASE, orderItems, "Test order");
+
+ ViewOrdersCommand command = new ViewOrdersCommand(transactionManager);
+ command.execute(itemMap, storage);
+
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Paracetamol"));
+ assertTrue(output.contains("10 in stock"));
+ assertTrue(output.contains("Test order"));
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ ViewOrdersCommand command = new ViewOrdersCommand(transactionManager);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/VisualizeCostCommandTest.java b/src/test/java/seedu/pill/command/VisualizeCostCommandTest.java
new file mode 100644
index 0000000000..e2255b952f
--- /dev/null
+++ b/src/test/java/seedu/pill/command/VisualizeCostCommandTest.java
@@ -0,0 +1,69 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Visualizer;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for VisualizeCostCommand
+ */
+public class VisualizeCostCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private Visualizer visualizer;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ visualizer = new Visualizer(new ArrayList<>());
+ }
+
+ @Test
+ public void execute_emptyItemMap_throwsException() {
+ VisualizeCostCommand command = new VisualizeCostCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void execute_itemsWithNoCost_throwsException() {
+ itemMap.addItemSilent(new Item("Paracetamol", 10));
+ VisualizeCostCommand command = new VisualizeCostCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ VisualizeCostCommand command = new VisualizeCostCommand(visualizer);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/VisualizeCostPriceCommandTest.java b/src/test/java/seedu/pill/command/VisualizeCostPriceCommandTest.java
new file mode 100644
index 0000000000..f94f8fd972
--- /dev/null
+++ b/src/test/java/seedu/pill/command/VisualizeCostPriceCommandTest.java
@@ -0,0 +1,66 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Visualizer;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class VisualizeCostPriceCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private Visualizer visualizer;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ visualizer = new Visualizer(new ArrayList<>());
+ }
+
+ @Test
+ public void execute_emptyItemMap_throwsException() {
+ VisualizeCostPriceCommand command = new VisualizeCostPriceCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void execute_itemsWithNoCostOrPrice_throwsException() {
+ itemMap.addItemSilent(new Item("Paracetamol", 10));
+ VisualizeCostPriceCommand command = new VisualizeCostPriceCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ VisualizeCostPriceCommand command = new VisualizeCostPriceCommand(visualizer);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/VisualizePriceCommandTest.java b/src/test/java/seedu/pill/command/VisualizePriceCommandTest.java
new file mode 100644
index 0000000000..9f3c48c99d
--- /dev/null
+++ b/src/test/java/seedu/pill/command/VisualizePriceCommandTest.java
@@ -0,0 +1,66 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Visualizer;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class VisualizePriceCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private Visualizer visualizer;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ visualizer = new Visualizer(new ArrayList<>());
+ }
+
+ @Test
+ public void execute_emptyItemMap_throwsException() {
+ VisualizePriceCommand command = new VisualizePriceCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void execute_itemsWithNoPrice_throwsException() {
+ itemMap.addItemSilent(new Item("Paracetamol", 10));
+ VisualizePriceCommand command = new VisualizePriceCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ VisualizePriceCommand command = new VisualizePriceCommand(visualizer);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/command/VisualizeStockCommandTest.java b/src/test/java/seedu/pill/command/VisualizeStockCommandTest.java
new file mode 100644
index 0000000000..4570a8d70b
--- /dev/null
+++ b/src/test/java/seedu/pill/command/VisualizeStockCommandTest.java
@@ -0,0 +1,55 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Visualizer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class VisualizeStockCommandTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private Visualizer visualizer;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ visualizer = new Visualizer(new ArrayList<>());
+ }
+
+ @Test
+ public void execute_emptyItemMap_throwsException() {
+ VisualizeStockCommand command = new VisualizeStockCommand(visualizer);
+
+ assertThrows(PillException.class, () -> {
+ command.execute(itemMap, storage);
+ });
+ }
+
+ @Test
+ public void isExit_returnsAlwaysFalse() {
+ VisualizeStockCommand command = new VisualizeStockCommand(visualizer);
+ assertFalse(command.isExit());
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/util/DateTimeTest.java b/src/test/java/seedu/pill/util/DateTimeTest.java
new file mode 100644
index 0000000000..6efa7d199e
--- /dev/null
+++ b/src/test/java/seedu/pill/util/DateTimeTest.java
@@ -0,0 +1,134 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+
+public class DateTimeTest {
+
+ @Test
+ public void constructor_noArguments_createsCurrentTime() {
+ DateTime dateTime = new DateTime();
+ LocalDateTime now = LocalDateTime.now();
+
+ // Allow 1 second difference to account for test execution time
+ long diffInSeconds = ChronoUnit.SECONDS.between(dateTime.getDateTime(), now);
+ assertTrue(Math.abs(diffInSeconds) <= 1);
+ }
+
+ @Test
+ public void constructor_withLocalDateTime_storesCorrectly() {
+ LocalDateTime testDateTime = LocalDateTime.of(2024, 1, 1, 12, 0);
+ DateTime dateTime = new DateTime(testDateTime);
+ assertEquals(testDateTime, dateTime.getDateTime());
+ }
+
+ @Test
+ public void getFormattedDateTime_customFormat_returnsCorrectFormat() {
+ LocalDateTime testDateTime = LocalDateTime.of(2024, 1, 1, 12, 30, 45);
+ DateTime dateTime = new DateTime(testDateTime);
+ assertEquals("2024-01-01 12:30:45", dateTime.getFormattedDateTime("yyyy-MM-dd HH:mm:ss"));
+ assertEquals("01/01/2024", dateTime.getFormattedDateTime("dd/MM/yyyy"));
+ }
+
+ @Test
+ public void getFormattedDate_returnsCorrectFormat() {
+ LocalDateTime testDateTime = LocalDateTime.of(2024, 1, 1, 12, 30, 45);
+ DateTime dateTime = new DateTime(testDateTime);
+ assertEquals("2024-01-01", dateTime.getFormattedDate());
+ }
+
+ @Test
+ public void getFormattedTime_returnsCorrectFormat() {
+ LocalDateTime testDateTime = LocalDateTime.of(2024, 1, 1, 12, 30, 45);
+ DateTime dateTime = new DateTime(testDateTime);
+ assertEquals("12:30:45", dateTime.getFormattedTime());
+ }
+
+ @Test
+ public void compareTo_withDifferentDates_returnsCorrectOrder() {
+ DateTime earlier = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime later = new DateTime(LocalDateTime.of(2024, 1, 2, 12, 0));
+
+ assertTrue(earlier.compareTo(later) < 0);
+ assertTrue(later.compareTo(earlier) > 0);
+ assertEquals(0, earlier.compareTo(new DateTime(earlier.getDateTime())));
+ }
+
+ @Test
+ public void isAfter_withDifferentDates_returnsCorrectBoolean() {
+ DateTime earlier = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime later = new DateTime(LocalDateTime.of(2024, 1, 2, 12, 0));
+
+ assertFalse(earlier.isAfter(later));
+ assertTrue(later.isAfter(earlier));
+ }
+
+ @Test
+ public void isBefore_withDifferentDates_returnsCorrectBoolean() {
+ DateTime earlier = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime later = new DateTime(LocalDateTime.of(2024, 1, 2, 12, 0));
+
+ assertTrue(earlier.isBefore(later));
+ assertFalse(later.isBefore(earlier));
+ }
+
+ @Test
+ public void getDaysUntil_withDifferentDates_returnsCorrectDays() {
+ DateTime start = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime end = new DateTime(LocalDateTime.of(2024, 1, 10, 12, 0));
+
+ assertEquals(9, start.getDaysUntil(end));
+ assertEquals(-9, end.getDaysUntil(start));
+ }
+
+ @Test
+ public void isExpired_withDifferentDates_returnsCorrectBoolean() {
+ DateTime current = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime expired = new DateTime(LocalDateTime.of(2023, 12, 31, 12, 0));
+ DateTime notExpired = new DateTime(LocalDateTime.of(2024, 1, 2, 12, 0));
+
+ assertTrue(current.isExpired(expired));
+ assertFalse(current.isExpired(notExpired));
+ }
+
+ @Test
+ public void getDaysUntilExpiration_withDifferentDates_returnsCorrectDays() {
+ DateTime current = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ DateTime expiration = new DateTime(LocalDateTime.of(2024, 1, 10, 12, 0));
+
+ assertEquals(9, current.getDaysUntilExpiration(expiration));
+ }
+
+ @Test
+ public void isWithinRefillPeriod_withDifferentScenarios_returnsCorrectBoolean() {
+ DateTime baseDate = new DateTime(LocalDateTime.now().minusDays(10));
+
+ // Test with 5 days refill period (should return true as base date is 10 days ago)
+ assertTrue(baseDate.isWithinRefillPeriod(5));
+
+ // Test with 15 days refill period (should return false as base date is only 10 days ago)
+ assertFalse(baseDate.isWithinRefillPeriod(15));
+ }
+
+ @Test
+ public void toString_returnsCorrectFormat() {
+ LocalDateTime testDateTime = LocalDateTime.of(2024, 1, 1, 12, 30, 45);
+ DateTime dateTime = new DateTime(testDateTime);
+ assertEquals("2024-01-01 12:30:45", dateTime.toString());
+ }
+
+ @Test
+ public void setDateTime_changesDateTime() {
+ DateTime dateTime = new DateTime(LocalDateTime.of(2024, 1, 1, 12, 0));
+ LocalDateTime newDateTime = LocalDateTime.of(2024, 1, 2, 12, 0);
+
+ dateTime.setDateTime(newDateTime);
+ assertEquals(newDateTime, dateTime.getDateTime());
+ }
+}
diff --git a/src/test/java/seedu/pill/util/GetExpiredItemsTest.java b/src/test/java/seedu/pill/util/GetExpiredItemsTest.java
new file mode 100644
index 0000000000..e216ab6446
--- /dev/null
+++ b/src/test/java/seedu/pill/util/GetExpiredItemsTest.java
@@ -0,0 +1,74 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GetExpiredItemsTest {
+ @Test
+ public void getExpiredEmptyTest() {
+ ItemMap items = new ItemMap();
+ ItemMap expiredItems = items.getExpiringItems(LocalDate.now());
+ assertTrue(expiredItems.isEmpty());
+ }
+
+ @Test
+ public void getExpiredTestNoDate() {
+ ItemMap items = new ItemMap();
+ Item item1 = new Item("a", 5);
+ items.addItem(item1);
+ ItemMap expiredItems = items.getExpiringItems(LocalDate.now());
+ assertTrue(expiredItems.isEmpty());
+ }
+
+ @Test
+ public void getExpiredTestSimpleExpired() {
+ Item item1 = new Item("a", 5, LocalDate.now().plusDays(-1));
+ Item item2 = new Item("a", 2, LocalDate.now().plusDays(1));
+ Item item3 = new Item("a", 3, LocalDate.now().plusDays(2));
+ ItemMap items = new ItemMap();
+ items.addItem(item1);
+ items.addItem(item2);
+ items.addItem(item3);
+ ItemMap expectedItems = new ItemMap();
+ expectedItems.addItem(item1);
+ ItemMap expiredItems = items.getExpiringItems(LocalDate.now());
+ assertEquals(expectedItems, expiredItems);
+ }
+
+ @Test
+ public void getExpiredTestSimpleNotExpired() {
+ Item item1 = new Item("a", 5, LocalDate.now().plusDays(1));
+ Item item2 = new Item("a", 2, LocalDate.now().plusDays(2));
+ Item item3 = new Item("a", 3, LocalDate.now().plusDays(3));
+ ItemMap items = new ItemMap();
+ items.addItem(item1);
+ items.addItem(item2);
+ items.addItem(item3);
+ ItemMap expiredItems = items.getExpiringItems(LocalDate.now());
+ assertTrue(expiredItems.isEmpty());
+ }
+
+ @Test
+ public void getExpiredTestMixed() {
+ Item item1 = new Item("a", 5, LocalDate.now().plusDays(-22));
+ Item item2 = new Item("b", 6, LocalDate.now().plusDays(-2));
+ Item item3 = new Item("c", 4, LocalDate.now().plusDays(3));
+ Item item4 = new Item("a", 8, LocalDate.now().plusDays(4));
+ Item item5 = new Item("b", 2, LocalDate.now().plusDays(5));
+ ItemMap items = new ItemMap();
+ items.addItem(item1);
+ items.addItem(item2);
+ items.addItem(item3);
+ items.addItem(item4);
+ items.addItem(item5);
+ ItemMap expectedItems = new ItemMap();
+ expectedItems.addItem(item1);
+ expectedItems.addItem(item2);
+ ItemMap expiredItems = items.getExpiringItems(LocalDate.now());
+ assertEquals(expectedItems, expiredItems);
+ }
+}
diff --git a/src/test/java/seedu/pill/util/LoadDataTest.java b/src/test/java/seedu/pill/util/LoadDataTest.java
new file mode 100644
index 0000000000..66d190f908
--- /dev/null
+++ b/src/test/java/seedu/pill/util/LoadDataTest.java
@@ -0,0 +1,43 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.ExceptionMessages;
+import seedu.pill.exceptions.PillException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class LoadDataTest {
+ @Test
+ public void loadLineSimplePasses() throws PillException {
+ String data = "Bandages,20";
+ Item expectedItem = new Item("Bandages", 20);
+ Storage storage = new Storage();
+ Item item = storage.loadLine(data);
+ assertEquals(expectedItem, item);
+ }
+
+ @Test
+ public void loadLineCorruptThrowsException() {
+ String data = "Bandages,20,5";
+ Storage storage = new Storage();
+ try {
+ Item item = storage.loadLine(data);
+ fail();
+ } catch (PillException e) {
+ assertEquals(ExceptionMessages.PARSE_DATE_ERROR.getMessage(), e.getMessage());
+ }
+ }
+
+ @Test
+ public void loadLineInvalidQuantityThrowsException() {
+ String data = "Bandages,20a";
+ Storage storage = new Storage();
+ try {
+ Item item = storage.loadLine(data);
+ fail();
+ } catch (PillException e) {
+ assertEquals(ExceptionMessages.INVALID_QUANTITY_FORMAT.getMessage(), e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/seedu/pill/util/OrderTest.java b/src/test/java/seedu/pill/util/OrderTest.java
new file mode 100644
index 0000000000..68190d18bb
--- /dev/null
+++ b/src/test/java/seedu/pill/util/OrderTest.java
@@ -0,0 +1,95 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class OrderTest {
+ private Order purchaseOrder;
+ private Order dispenseOrder;
+ private final String testNotes = "Test order notes";
+
+ @BeforeEach
+ void setUp() {
+ purchaseOrder = new Order(Order.OrderType.PURCHASE, testNotes);
+ dispenseOrder = new Order(Order.OrderType.DISPENSE, testNotes);
+ }
+
+ @Test
+ void constructor_createsOrderWithCorrectInitialState() {
+ // Test purchase order
+ assertNotNull(purchaseOrder.getId(), "Order ID should not be null");
+ assertEquals(Order.OrderType.PURCHASE, purchaseOrder.getType(), "Order type should be PURCHASE");
+ assertEquals(Order.OrderStatus.PENDING, purchaseOrder.getStatus(), "Initial status should be PENDING");
+ assertEquals(testNotes, purchaseOrder.getNotes(), "Notes should match constructor argument");
+ assertTrue(purchaseOrder.getItems().isEmpty(), "Initial items list should be empty");
+ assertNull(purchaseOrder.getFulfillmentTime(), "Initial fulfillment time should be null");
+
+ // Test creation timestamp
+ LocalDateTime now = LocalDateTime.now();
+ long timeDiff = ChronoUnit.SECONDS.between(purchaseOrder.getCreationTime(), now);
+ assertTrue(Math.abs(timeDiff) <= 1, "Creation time should be within 1 second of current time");
+
+ // Test dispense order
+ assertEquals(Order.OrderType.DISPENSE, dispenseOrder.getType(), "Order type should be DISPENSE");
+ }
+
+ @Test
+ void fulfill_updatesOrderStatusAndTime() {
+ purchaseOrder.fulfill();
+
+ assertEquals(Order.OrderStatus.FULFILLED, purchaseOrder.getStatus(),
+ "Status should be FULFILLED after fulfilling");
+ assertNotNull(purchaseOrder.getFulfillmentTime(),
+ "Fulfillment time should be set");
+
+ LocalDateTime now = LocalDateTime.now();
+ long timeDiff = ChronoUnit.SECONDS.between(purchaseOrder.getFulfillmentTime(), now);
+ assertTrue(Math.abs(timeDiff) <= 1,
+ "Fulfillment time should be within 1 second of current time");
+ }
+
+ @Test
+ void cancel_updatesOrderStatus() {
+ purchaseOrder.cancel();
+
+ assertEquals(Order.OrderStatus.CANCELLED, purchaseOrder.getStatus(),
+ "Status should be CANCELLED after cancelling");
+ assertNull(purchaseOrder.getFulfillmentTime(),
+ "Fulfillment time should still be null after cancelling");
+ }
+
+ @Test
+ void multipleStatusChanges_handleCorrectly() {
+ // Test cancel then fulfill
+ Order order1 = new Order(Order.OrderType.PURCHASE, "Test order 1");
+ order1.cancel();
+ order1.fulfill();
+ assertEquals(Order.OrderStatus.FULFILLED, order1.getStatus(),
+ "Should allow fulfilling a cancelled order");
+
+ // Test fulfill then cancel
+ Order order2 = new Order(Order.OrderType.PURCHASE, "Test order 2");
+ order2.fulfill();
+ order2.cancel();
+ assertEquals(Order.OrderStatus.CANCELLED, order2.getStatus(),
+ "Should allow cancelling a fulfilled order");
+ }
+
+ @Test
+ void orderEquality_idBasedComparison() {
+ Order order1 = new Order(Order.OrderType.PURCHASE, "Test order");
+ Order order2 = new Order(Order.OrderType.PURCHASE, "Test order");
+
+ assertNotEquals(order1.getId(), order2.getId(),
+ "Different orders should have different IDs");
+ }
+}
diff --git a/src/test/java/seedu/pill/util/ParserTest.java b/src/test/java/seedu/pill/util/ParserTest.java
new file mode 100644
index 0000000000..82562eb505
--- /dev/null
+++ b/src/test/java/seedu/pill/util/ParserTest.java
@@ -0,0 +1,114 @@
+package seedu.pill.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+import seedu.pill.util.ItemMap;
+import seedu.pill.util.Storage;
+import seedu.pill.util.Parser;
+import seedu.pill.util.TransactionManager;
+import seedu.pill.util.Ui;
+import seedu.pill.util.Item;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ParserTest {
+ private ItemMap itemMap;
+ private Storage storage;
+ private TransactionManager transactionManager;
+ private Ui ui;
+ private Parser parser;
+ private ByteArrayOutputStream outputStream;
+ private PrintStream printStream;
+ private final PrintStream standardOut = System.out;
+
+ @BeforeEach
+ public void setUp() {
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ ui = new Ui(itemMap);
+ parser = new Parser(itemMap, storage, transactionManager, ui);
+ outputStream = new ByteArrayOutputStream();
+ printStream = new PrintStream(outputStream);
+ System.setOut(printStream);
+ }
+
+ // Basic Command Tests
+ @Test
+ public void parseCommand_exitCommand_setsExitFlag() {
+ parser.parseCommand("exit");
+ assertTrue(parser.getExitFlag());
+ }
+
+ @Test
+ public void parseCommand_helpCommand_executesSuccessfully() {
+ parser.parseCommand("help");
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Available commands"));
+ }
+
+ @Test
+ public void parseCommand_listCommand_executesSuccessfully() {
+ parser.parseCommand("list");
+ // Verify expected output
+ }
+
+ // Delete Command Tests
+ @Test
+ public void parseCommand_validDeleteCommand_deletesItem() throws PillException {
+ itemMap.addItemSilent(new Item("Paracetamol", 10, LocalDate.parse("2024-12-31")));
+ parser.parseCommand("delete Paracetamol 2024-12-31");
+ assertTrue(itemMap.getItemByNameAndExpiry("Paracetamol",
+ Optional.of(LocalDate.parse("2024-12-31"))) == null);
+ }
+
+ // Transaction Command Tests
+ @Test
+ public void parseCommand_listTransactions_executesSuccessfully() {
+ parser.parseCommand("transactions");
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("No transactions found"));
+ }
+
+ @Test
+ public void parseCommand_validTransactionHistory_showsHistory() {
+ parser.parseCommand("transaction-history 2024-01-01T00:00:00 2024-12-31T23:59:59");
+ // Verify output
+ }
+
+ // Visualization Command Tests
+ @Test
+ public void parseCommand_visualizationCommands_executeSuccessfully() {
+ parser.parseCommand("visualize-price");
+ parser.parseCommand("visualize-cost");
+ parser.parseCommand("visualize-stock");
+ parser.parseCommand("visualize-cost-price");
+ }
+
+ // Error Cases
+ @Test
+ public void parseCommand_invalidCommand_throwsPillException() {
+ parser.parseCommand("invalidcommand");
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Invalid command, please try again."));
+ }
+
+ @Test
+ public void parseCommand_emptyCommand_throwsPillException() {
+ parser.parseCommand("");
+ String output = outputStream.toString().trim();
+ assertTrue(output.contains("Invalid command, please try again."));
+ }
+
+ @AfterEach
+ public void restoreSystemOut() {
+ System.setOut(standardOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/util/StringMatcherTest.java b/src/test/java/seedu/pill/util/StringMatcherTest.java
new file mode 100644
index 0000000000..0f879e57bc
--- /dev/null
+++ b/src/test/java/seedu/pill/util/StringMatcherTest.java
@@ -0,0 +1,115 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class StringMatcherTest {
+
+ @Test
+ public void levenshteinDistance_identicalStrings_returnsZero() {
+ assertEquals(0, StringMatcher.levenshteinDistance("hello", "hello"));
+ assertEquals(0, StringMatcher.levenshteinDistance("", ""));
+ assertEquals(0, StringMatcher.levenshteinDistance("12345", "12345"));
+ }
+
+ @Test
+ public void levenshteinDistance_singleCharacterDifference_returnsOne() {
+ // Substitution
+ assertEquals(1, StringMatcher.levenshteinDistance("cat", "hat"));
+ // Deletion
+ assertEquals(1, StringMatcher.levenshteinDistance("cats", "cat"));
+ // Insertion
+ assertEquals(1, StringMatcher.levenshteinDistance("cat", "cats"));
+ }
+
+ @Test
+ public void levenshteinDistance_multipleCharacterDifferences_returnsCorrectDistance() {
+ assertEquals(1, StringMatcher.levenshteinDistance("hello", "hallo")); // One substitution
+ assertEquals(3, StringMatcher.levenshteinDistance("kitten", "sitting")); // Multiple operations
+ assertEquals(3, StringMatcher.levenshteinDistance("saturday", "sunday")); // Multiple operations
+ }
+
+ @Test
+ public void levenshteinDistance_differentLengthStrings_returnsCorrectDistance() {
+ assertEquals(6, StringMatcher.levenshteinDistance("book", "bookkeeper")); // 6 insertions
+ assertEquals(4, StringMatcher.levenshteinDistance("", "test")); // 4 insertions
+ assertEquals(5, StringMatcher.levenshteinDistance("hello", "")); // 5 deletions
+ }
+
+ @Test
+ public void levenshteinDistance_caseSensitive_respectsCase() {
+ assertEquals(4, StringMatcher.levenshteinDistance("TEST", "test")); // 4 case changes
+ assertEquals(1, StringMatcher.levenshteinDistance("Hello", "hello")); // 1 case change
+ }
+
+ // Rest of the test cases remain the same...
+ @Test
+ public void findClosestMatch_exactMatch_returnsMatch() {
+ List validStrings = Arrays.asList("help", "add", "delete");
+ assertEquals("help", StringMatcher.findClosestMatch("help", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_closeMatch_returnsClosestString() {
+ List validStrings = Arrays.asList("help", "add", "delete");
+ assertEquals("help", StringMatcher.findClosestMatch("halp", validStrings));
+ assertEquals("help", StringMatcher.findClosestMatch("helpp", validStrings));
+ assertEquals("add", StringMatcher.findClosestMatch("ad", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_noCloseMatch_returnsNull() {
+ List validStrings = Arrays.asList("help", "add", "delete");
+ assertNull(StringMatcher.findClosestMatch("xxxxxxxx", validStrings));
+ assertNull(StringMatcher.findClosestMatch("completely-different", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_emptyInput_returnsNull() {
+ List validStrings = Arrays.asList("help", "add", "delete");
+ assertNull(StringMatcher.findClosestMatch("", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_emptyValidStrings_returnsNull() {
+ assertNull(StringMatcher.findClosestMatch("help", Collections.emptyList()));
+ }
+
+ @Test
+ public void findClosestMatch_caseInsensitive_returnsCaseInsensitiveMatch() {
+ List validStrings = Arrays.asList("Help", "ADD", "delete");
+ assertEquals("Help", StringMatcher.findClosestMatch("help", validStrings));
+ assertEquals("Help", StringMatcher.findClosestMatch("HELP", validStrings));
+ assertEquals("ADD", StringMatcher.findClosestMatch("add", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_multipleCloseMatches_returnsFirst() {
+ List validStrings = Arrays.asList("help", "heap", "heal");
+ // "help" and "heap" are both distance 1 from "hepp", should return "help" as it's first in list
+ assertEquals("help", StringMatcher.findClosestMatch("hepp", validStrings));
+ }
+
+ @Test
+ public void findClosestMatch_specialCharacters_handlesCorrectly() {
+ List validStrings = Arrays.asList("user-input", "user_input", "user.input");
+ assertEquals("user-input", StringMatcher.findClosestMatch("userinput", validStrings));
+ assertEquals("user-input", StringMatcher.findClosestMatch("user input", validStrings));
+ }
+
+ @Test
+ public void levenshteinDistance_specialCases_handlesCorrectly() {
+ // Test with spaces
+ assertEquals(1, StringMatcher.levenshteinDistance("hello world", "hello world"));
+ // Test with numbers
+ assertEquals(1, StringMatcher.levenshteinDistance("test123", "test124"));
+ // Test with special characters
+ assertEquals(1, StringMatcher.levenshteinDistance("user-input", "user_input"));
+ }
+}
diff --git a/src/test/java/seedu/pill/util/TransactionManagerTest.java b/src/test/java/seedu/pill/util/TransactionManagerTest.java
new file mode 100644
index 0000000000..f51df3633f
--- /dev/null
+++ b/src/test/java/seedu/pill/util/TransactionManagerTest.java
@@ -0,0 +1,701 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import seedu.pill.exceptions.PillException;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDate;
+import java.util.List;
+
+//@@author philip1304
+
+class TransactionManagerTest {
+ private TransactionManager transactionManager;
+ private ItemMap itemMap;
+ private ByteArrayOutputStream outContent; // Changed variable name to match usage
+ private final PrintStream originalOut = System.out;
+ private Storage storage;
+
+ @BeforeEach
+ void setUp() {
+ outContent = new ByteArrayOutputStream(); // Initialize here
+ System.setOut(new PrintStream(outContent));
+ itemMap = new ItemMap();
+ storage = new Storage();
+ transactionManager = new TransactionManager(itemMap, storage);
+ }
+
+ @Test
+ void createTransaction_insufficientStock_throwsException() {
+ // Arrange
+ String itemName = "Aspirin";
+ int initialQuantity = 50;
+ int decreaseQuantity = 100;
+ LocalDate expiryDate = null;
+
+ assertThrows(PillException.class, () -> {
+ transactionManager.createTransaction(
+ itemName,
+ decreaseQuantity,
+ expiryDate,
+ Transaction.TransactionType.OUTGOING,
+ "Test insufficient",
+ null
+ );
+ });
+ }
+
+ @Test
+ void createTransaction_withOrder_associatesCorrectly() throws PillException {
+ // Arrange
+ Order order = transactionManager.createOrder(Order.OrderType.PURCHASE, new ItemMap(), "Test order");
+ String itemName = "Aspirin";
+ int quantity = 100;
+ LocalDate expiryDate = null;
+
+ // Act
+ Transaction transaction = transactionManager.createTransaction(
+ itemName,
+ quantity,
+ expiryDate,
+ Transaction.TransactionType.INCOMING,
+ "Test with order",
+ order
+ );
+
+ // Assert
+ assertNotNull(transaction);
+ assertEquals(order, transaction.getAssociatedOrder());
+ }
+
+ @Test
+ void createOrder_purchaseOrder_createsSuccessfully() {
+ // Arrange
+ String notes = "Test purchase order";
+
+ // Act
+ Order order = transactionManager.createOrder(Order.OrderType.PURCHASE, new ItemMap(), notes);
+
+ // Assert
+ assertNotNull(order);
+ assertEquals(Order.OrderType.PURCHASE, order.getType());
+ assertEquals(notes, order.getNotes());
+ assertEquals(Order.OrderStatus.PENDING, order.getStatus());
+ }
+
+ @Test
+ void createOrder_dispenseOrder_createsSuccessfully() {
+ // Arrange
+ String notes = "Test dispense order";
+
+ // Act
+ Order order = transactionManager.createOrder(Order.OrderType.DISPENSE, new ItemMap(), notes);
+
+ // Assert
+ assertNotNull(order);
+ assertEquals(Order.OrderType.DISPENSE, order.getType());
+ assertEquals(notes, order.getNotes());
+ assertEquals(Order.OrderStatus.PENDING, order.getStatus());
+ }
+
+ @Test
+ void fulfillOrder_nonPendingOrder_throwsException() throws PillException {
+ // Arrange
+ Order order = transactionManager.createOrder(Order.OrderType.PURCHASE, new ItemMap(), "Test order");
+ order.fulfill(); // Change status to FULFILLED
+
+ // Act & Assert
+ assertThrows(PillException.class, () -> transactionManager.fulfillOrder(order));
+ }
+
+ @Test
+ void getTransactions_returnsCorrectTransactions() throws PillException {
+ // Arrange
+ transactionManager.createTransaction("Aspirin", 100, null,
+ Transaction.TransactionType.INCOMING, "First", null);
+ transactionManager.createTransaction("Bandage", 50, null,
+ Transaction.TransactionType.INCOMING, "Second", null);
+
+ // Act
+ List transactions = transactionManager.getTransactions();
+
+ // Assert
+ assertEquals(2, transactions.size());
+ assertEquals("Aspirin", transactions.get(0).getItemName());
+ assertEquals("Bandage", transactions.get(1).getItemName());
+ }
+
+ @Test
+ void getOrders_returnsCorrectOrders() {
+ // Arrange
+ transactionManager.createOrder(Order.OrderType.PURCHASE, new ItemMap(), "First order");
+ transactionManager.createOrder(Order.OrderType.DISPENSE, new ItemMap(), "Second order");
+
+ // Act
+ List orders = transactionManager.getOrders();
+
+ // Assert
+ assertEquals(2, orders.size());
+ assertEquals(Order.OrderType.PURCHASE, orders.get(0).getType());
+ assertEquals(Order.OrderType.DISPENSE, orders.get(1).getType());
+ }
+
+ @Test
+ void getTransactionHistory_returnsTransactionsInTimeRange() throws PillException {
+ // Arrange
+ LocalDate startDate = LocalDate.now();
+
+ transactionManager.createTransaction("Aspirin", 100, null,
+ Transaction.TransactionType.INCOMING, "First", null);
+ transactionManager.createTransaction("Bandage", 50, null,
+ Transaction.TransactionType.INCOMING, "Second", null);
+
+ LocalDate endDate = LocalDate.now();
+
+ // Act
+ List transactions = transactionManager.getTransactionHistory(startDate, endDate);
+
+ // Assert
+ assertEquals(2, transactions.size());
+ assertTrue(transactions.stream()
+ .allMatch(t -> !t.getTimestamp().toLocalDate().isBefore(startDate) && !t.getTimestamp().toLocalDate()
+ .isAfter(endDate)));
+ }
+
+ @Test
+ void getTransactionHistory_exactTimeRange_returnsMatchingTransactions() throws PillException {
+ // Arrange
+ LocalDate start = LocalDate.now();
+ Transaction first = transactionManager.createTransaction(
+ "Aspirin", 100, null,
+ Transaction.TransactionType.INCOMING, "First", null);
+ Transaction second = transactionManager.createTransaction(
+ "Bandage", 50, null,
+ Transaction.TransactionType.INCOMING, "Second", null);
+ LocalDate end = LocalDate.now();
+
+ // Act
+ List transactions = transactionManager.getTransactionHistory(
+ first.getTimestamp().toLocalDate(), second.getTimestamp().toLocalDate());
+
+ // Assert
+ assertEquals(2, transactions.size());
+ assertTrue(transactions.contains(first));
+ assertTrue(transactions.contains(second));
+ }
+
+ @Test
+ void getTransactionHistory_exactBoundaryTimes_includesTransactions() throws PillException {
+ // Arrange
+ Transaction first = transactionManager.createTransaction(
+ "First", 100, null,
+ Transaction.TransactionType.INCOMING, "Boundary start", null);
+ LocalDate startAndEnd = first.getTimestamp().toLocalDate(); // Use exact timestamp
+
+ // Act
+ List transactions = transactionManager.getTransactionHistory(startAndEnd, startAndEnd);
+
+ // Assert
+ assertEquals(1, transactions.size());
+ assertTrue(transactions.contains(first));
+ }
+
+ @Test
+ void getTransactionHistory_endBeforeStart_returnsEmptyList() throws PillException {
+ // Arrange
+ Transaction transaction = transactionManager.createTransaction(
+ "Test", 100, null,
+ Transaction.TransactionType.INCOMING, "Test", null);
+ LocalDate later = LocalDate.now();
+ LocalDate earlier = later.minusDays(1);
+
+ // Act
+ List transactions = transactionManager.getTransactionHistory(later, earlier);
+
+ // Assert
+ assertTrue(transactions.isEmpty());
+ }
+
+ @Test
+ void listTransactionHistory_printsFormattedTransactions() throws PillException {
+ // Arrange
+ LocalDate today = LocalDate.now();
+ LocalDate start = today.minusDays(1);
+ LocalDate end = today.plusDays(1);
+
+ // First create an incoming transaction to add stock
+ transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "First transaction",
+ null
+ );
+
+ // Create an incoming transaction for Bandage first
+ transactionManager.createTransaction(
+ "Bandage",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Stock addition",
+ null
+ );
+
+ // Now we can create an outgoing transaction for Bandage since we have stock
+ transactionManager.createTransaction(
+ "Bandage",
+ 50,
+ null,
+ Transaction.TransactionType.OUTGOING,
+ "Second transaction",
+ null
+ );
+
+ // Clear any previous output
+ outContent.reset();
+
+ // Act
+ transactionManager.listTransactionHistory(start, end);
+
+ // Assert
+ String output = outContent.toString();
+
+ // Verify numbering and content expectations
+ assertTrue(output.contains("1. "), "Output should contain first item numbering");
+ assertTrue(output.contains("2. "), "Output should contain second item numbering");
+ assertTrue(output.contains("3. "), "Output should contain third item numbering");
+
+ // Verify all transaction details are present
+ assertTrue(output.contains("Aspirin"), "Output should contain Aspirin");
+ assertTrue(output.contains("100"), "Output should contain quantity 100");
+ assertTrue(output.contains("INCOMING"), "Output should contain INCOMING type");
+ assertTrue(output.contains("First transaction"), "Output should contain first transaction notes");
+
+ assertTrue(output.contains("Bandage"), "Output should contain Bandage");
+ assertTrue(output.contains("50"), "Output should contain quantity 50");
+ assertTrue(output.contains("OUTGOING"), "Output should contain OUTGOING type");
+ assertTrue(output.contains("Second transaction"), "Output should contain second transaction notes");
+
+ // Count the number of transactions
+ long transactionCount = output.lines().count();
+ assertEquals(3, transactionCount, "Should have exactly 3 transactions listed");
+ }
+
+ @Test
+ void getTransactionHistory_withinRange_returnsMatchingTransactions() throws PillException {
+ // Arrange
+ LocalDate start = LocalDate.now();
+ LocalDate end = LocalDate.now().plusDays(2);
+
+ Transaction transaction = transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Test transaction",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(start, end);
+
+ // Assert
+ assertEquals(1, result.size());
+ assertTrue(result.contains(transaction));
+ }
+
+ @Test
+ void getTransactionHistory_outsideRange_returnsEmptyList() throws PillException {
+ // Arrange
+ LocalDate futureStart = LocalDate.now().plusDays(5);
+ LocalDate futureEnd = LocalDate.now().plusDays(10);
+
+ transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Test transaction",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(futureStart, futureEnd);
+
+ // Assert
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void getTransactionHistory_exactlyOnStartDate_includesTransaction() throws PillException {
+ // Arrange
+ LocalDate today = LocalDate.now();
+
+ Transaction transaction = transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Test transaction",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(today, today.plusDays(1));
+
+ // Assert
+ assertEquals(1, result.size());
+ assertTrue(result.contains(transaction));
+ }
+
+ @Test
+ void getTransactionHistory_exactlyOnEndDate_includesTransaction() throws PillException {
+ // Arrange
+ LocalDate today = LocalDate.now();
+
+ Transaction transaction = transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Test transaction",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(today.minusDays(1), today);
+
+ // Assert
+ assertEquals(1, result.size());
+ assertTrue(result.contains(transaction));
+ }
+
+ @Test
+ void getTransactionHistory_endDateBeforeStartDate_returnsEmptyList() throws PillException {
+ // Arrange
+ LocalDate start = LocalDate.now();
+ LocalDate end = start.minusDays(1);
+
+ transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Test transaction",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(start, end);
+
+ // Assert
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void getTransactionHistory_multipleTransactions_returnsCorrectSubset() throws PillException {
+ // Arrange
+ LocalDate start = LocalDate.now();
+ LocalDate end = start.plusDays(1);
+
+ // Create transaction within range
+ Transaction inRangeTransaction = transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "In range",
+ null
+ );
+
+ // Add stock for outgoing transaction
+ transactionManager.createTransaction(
+ "Bandage",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Setup stock",
+ null
+ );
+
+ // Create another transaction within range
+ Transaction alsoInRangeTransaction = transactionManager.createTransaction(
+ "Bandage",
+ 50,
+ null,
+ Transaction.TransactionType.OUTGOING,
+ "Also in range",
+ null
+ );
+
+ // Act
+ List result = transactionManager.getTransactionHistory(start, end);
+
+ // Assert
+ assertEquals(3, result.size());
+ assertTrue(result.contains(inRangeTransaction));
+ assertTrue(result.contains(alsoInRangeTransaction));
+ }
+
+ @Test
+ void fulfillOrder_purchaseOrder_createsIncomingTransactionsAndFulfills() throws PillException {
+ // Arrange
+ ItemMap itemsToOrder = new ItemMap();
+ Item item1 = new Item("Aspirin", 100);
+ Item item2 = new Item("Bandage", 50);
+ itemsToOrder.addItem(item1);
+ itemsToOrder.addItem(item2);
+
+ Order purchaseOrder = transactionManager.createOrder(
+ Order.OrderType.PURCHASE,
+ itemsToOrder,
+ "Test purchase order"
+ );
+
+ // Act
+ transactionManager.fulfillOrder(purchaseOrder);
+
+ // Assert
+ assertEquals(Order.OrderStatus.FULFILLED, purchaseOrder.getStatus());
+ List transactions = transactionManager.getTransactions();
+ assertEquals(2, transactions.size());
+
+ // Verify first transaction
+ Transaction firstTransaction = transactions.get(0);
+ assertEquals("Aspirin", firstTransaction.getItemName());
+ assertEquals(100, firstTransaction.getQuantity());
+ assertEquals(Transaction.TransactionType.INCOMING, firstTransaction.getType());
+ assertEquals("Order fulfillment", firstTransaction.getNotes());
+ assertEquals(purchaseOrder, firstTransaction.getAssociatedOrder());
+
+ // Verify second transaction
+ Transaction secondTransaction = transactions.get(1);
+ assertEquals("Bandage", secondTransaction.getItemName());
+ assertEquals(50, secondTransaction.getQuantity());
+ assertEquals(Transaction.TransactionType.INCOMING, secondTransaction.getType());
+ assertEquals("Order fulfillment", secondTransaction.getNotes());
+ assertEquals(purchaseOrder, secondTransaction.getAssociatedOrder());
+ }
+
+ @Test
+ void fulfillOrder_dispenseOrder_createsOutgoingTransactionsAndFulfills() throws PillException {
+ // Arrange
+ // First add stock for items
+ transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Setup stock",
+ null
+ );
+ transactionManager.createTransaction(
+ "Bandage",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Setup stock",
+ null
+ );
+
+ // Clear previous transactions to make verification easier
+ outContent.reset();
+
+ // Create dispense order
+ ItemMap itemsToDispense = new ItemMap();
+ Item item1 = new Item("Aspirin", 50);
+ Item item2 = new Item("Bandage", 30);
+ itemsToDispense.addItem(item1);
+ itemsToDispense.addItem(item2);
+
+ Order dispenseOrder = transactionManager.createOrder(
+ Order.OrderType.DISPENSE,
+ itemsToDispense,
+ "Test dispense order"
+ );
+
+ // Act
+ transactionManager.fulfillOrder(dispenseOrder);
+
+ assertEquals(Order.OrderStatus.FULFILLED, dispenseOrder.getStatus());
+ List orderTransactions = transactionManager.getTransactions()
+ .stream().filter(t -> t.getAssociatedOrder() == dispenseOrder)
+ .toList();
+ assertEquals(2, orderTransactions.size());
+
+ // Verify first transaction
+ Transaction firstTransaction = orderTransactions.get(0);
+ assertEquals("Aspirin", firstTransaction.getItemName());
+ assertEquals(50, firstTransaction.getQuantity());
+ assertEquals(Transaction.TransactionType.OUTGOING, firstTransaction.getType());
+ assertEquals("Order fulfillment", firstTransaction.getNotes());
+ assertEquals(dispenseOrder, firstTransaction.getAssociatedOrder());
+
+ // Verify second transaction
+ Transaction secondTransaction = orderTransactions.get(1);
+ assertEquals("Bandage", secondTransaction.getItemName());
+ assertEquals(30, secondTransaction.getQuantity());
+ assertEquals(Transaction.TransactionType.OUTGOING, secondTransaction.getType());
+ assertEquals("Order fulfillment", secondTransaction.getNotes());
+ assertEquals(dispenseOrder, secondTransaction.getAssociatedOrder());
+ }
+
+ @Test
+ void fulfillOrder_emptyOrder_completesFulfillment() throws PillException {
+ // Arrange
+ Order emptyOrder = transactionManager.createOrder(
+ Order.OrderType.PURCHASE,
+ new ItemMap(),
+ "Empty order"
+ );
+
+ // Act
+ transactionManager.fulfillOrder(emptyOrder);
+
+ // Assert
+ assertEquals(Order.OrderStatus.FULFILLED, emptyOrder.getStatus());
+ assertTrue(transactionManager.getTransactions().isEmpty());
+ }
+
+ @Test
+ void createTransaction_withExpiry_createsSuccessfully() throws PillException {
+ // Arrange
+ String itemName = "Aspirin";
+ int quantity = 100;
+ LocalDate expiryDate = LocalDate.now().plusMonths(6);
+
+ // Act
+ Transaction transaction = transactionManager.createTransaction(
+ itemName,
+ quantity,
+ expiryDate,
+ Transaction.TransactionType.INCOMING,
+ "Test with expiry",
+ null
+ );
+
+ // Assert
+ assertNotNull(transaction);
+ assertEquals(itemName, transaction.getItemName());
+ assertEquals(quantity, transaction.getQuantity());
+ assertEquals(Transaction.TransactionType.INCOMING, transaction.getType());
+ assertEquals("Test with expiry", transaction.getNotes());
+ assertNull(transaction.getAssociatedOrder());
+ }
+
+ @Test
+ void listTransactions_noTransactions_printsNoTransactionsMessage() {
+ // Arrange - no setup needed, assuming empty transaction list
+
+ // Act
+ transactionManager.listTransactions();
+
+ // Assert
+ String output = outContent.toString();
+ assertTrue(output.contains("No transactions found"));
+ }
+
+ @Test
+ void listOrders_noOrders_printsNoOrdersMessage() {
+ // Arrange - no setup needed, assuming empty order list
+
+ // Act
+ transactionManager.listOrders();
+
+ // Assert
+ String output = outContent.toString();
+ assertTrue(output.contains("No orders recorded..."));
+ }
+
+ @Test
+ void fulfillOrder_dispenseOrderWithInsufficientStock_throwsException() {
+ // Arrange
+ ItemMap itemsToDispense = new ItemMap();
+ Item item = new Item("Aspirin", 100); // More than available stock
+ itemsToDispense.addItem(item);
+
+ Order dispenseOrder = transactionManager.createOrder(
+ Order.OrderType.DISPENSE,
+ itemsToDispense,
+ "Test dispense order"
+ );
+
+ // Act & Assert
+ assertThrows(PillException.class, () -> transactionManager.fulfillOrder(dispenseOrder));
+ }
+
+ @Test
+ void createOrder_emptyNotes_createsOrderWithEmptyNotes() {
+ // Arrange
+ Order.OrderType type = Order.OrderType.PURCHASE;
+ ItemMap itemsToOrder = new ItemMap();
+
+ // Act
+ Order order = transactionManager.createOrder(type, itemsToOrder, "");
+
+ // Assert
+ assertNotNull(order);
+ assertEquals("", order.getNotes());
+ }
+
+ @Test
+ void listTransactions_printsFormattedTransactions() throws PillException {
+ // Arrange
+ transactionManager.createTransaction(
+ "Aspirin",
+ 100,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "First transaction",
+ null
+ );
+ transactionManager.createTransaction(
+ "Bandage",
+ 50,
+ null,
+ Transaction.TransactionType.INCOMING,
+ "Second transaction",
+ null
+ );
+
+ // Clear any previous output
+ outContent.reset();
+
+ // Act
+ transactionManager.listTransactions();
+
+ // Assert
+ String output = outContent.toString();
+
+ // Verify numbering and content expectations
+ assertTrue(output.contains("1. "), "Output should contain first item numbering");
+ assertTrue(output.contains("2. "), "Output should contain second item numbering");
+
+ // Verify all transaction details are present
+ assertTrue(output.contains("Aspirin"), "Output should contain Aspirin");
+ assertTrue(output.contains("100"), "Output should contain quantity 100");
+ assertTrue(output.contains("INCOMING"), "Output should contain INCOMING type");
+ assertTrue(output.contains("First transaction"), "Output should contain first transaction notes");
+
+ assertTrue(output.contains("Bandage"), "Output should contain Bandage");
+ assertTrue(output.contains("50"), "Output should contain quantity 50");
+ assertTrue(output.contains("INCOMING"), "Output should contain INCOMING type");
+ assertTrue(output.contains("Second transaction"), "Output should contain second transaction notes");
+ }
+
+ @AfterEach
+ void restoreSystemOut() {
+ System.setOut(originalOut);
+ }
+}
diff --git a/src/test/java/seedu/pill/util/TransactionTest.java b/src/test/java/seedu/pill/util/TransactionTest.java
new file mode 100644
index 0000000000..3046921163
--- /dev/null
+++ b/src/test/java/seedu/pill/util/TransactionTest.java
@@ -0,0 +1,180 @@
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.UUID;
+
+class TransactionTest {
+
+ @Test
+ void constructor_withValidInputs_createsTransaction() {
+ // Arrange
+ String itemName = "Aspirin";
+ int quantity = 100;
+ Transaction.TransactionType type = Transaction.TransactionType.INCOMING;
+ String notes = "Test transaction";
+ Order associatedOrder = new Order(Order.OrderType.PURCHASE, "Test order");
+
+ // Act
+ Transaction transaction = new Transaction(itemName, quantity, type, notes, associatedOrder);
+
+ // Assert
+ assertNotNull(transaction.getId());
+ assertEquals(itemName, transaction.getItemName());
+ assertEquals(quantity, transaction.getQuantity());
+ assertEquals(type, transaction.getType());
+ assertEquals(notes, transaction.getNotes());
+ assertEquals(associatedOrder, transaction.getAssociatedOrder());
+
+ // Verify timestamp is recent (within last second)
+ LocalDateTime now = LocalDateTime.now();
+ long timeDiff = ChronoUnit.SECONDS.between(transaction.getTimestamp(), now);
+ assertTrue(Math.abs(timeDiff) <= 1);
+ }
+
+ @Test
+ void constructor_withNullOrder_createsTransactionWithoutOrder() {
+ // Arrange
+ String itemName = "Bandages";
+ int quantity = 50;
+ Transaction.TransactionType type = Transaction.TransactionType.OUTGOING;
+ String notes = "Direct transaction";
+
+ // Act
+ Transaction transaction = new Transaction(itemName, quantity, type, notes, null);
+
+ // Assert
+ assertNotNull(transaction.getId());
+ assertEquals(itemName, transaction.getItemName());
+ assertEquals(quantity, transaction.getQuantity());
+ assertEquals(type, transaction.getType());
+ assertEquals(notes, transaction.getNotes());
+ assertNull(transaction.getAssociatedOrder());
+ }
+
+ @Test
+ void getId_returnsUniqueIds() {
+ // Arrange
+ Transaction transaction1 = new Transaction("Item1", 10,
+ Transaction.TransactionType.INCOMING, "Note1", null);
+ Transaction transaction2 = new Transaction("Item2", 20,
+ Transaction.TransactionType.OUTGOING, "Note2", null);
+
+ // Act & Assert
+ assertNotEquals(transaction1.getId(), transaction2.getId());
+ }
+
+ @Test
+ void getTimestamp_returnsCorrectTimestamp() {
+ // Arrange
+ LocalDateTime beforeCreation = LocalDateTime.now();
+ Transaction transaction = new Transaction("Item", 10,
+ Transaction.TransactionType.INCOMING, "Note", null);
+ LocalDateTime afterCreation = LocalDateTime.now();
+
+ // Act
+ LocalDateTime timestamp = transaction.getTimestamp();
+
+ // Assert
+ assertTrue(timestamp.isEqual(beforeCreation) || timestamp.isAfter(beforeCreation));
+ assertTrue(timestamp.isEqual(afterCreation) || timestamp.isBefore(afterCreation));
+ }
+
+ @Test
+ void toString_withoutOrder_returnsCorrectFormat() {
+ // Arrange
+ Transaction transaction = new Transaction("TestItem", 10,
+ Transaction.TransactionType.INCOMING, "Test note", null);
+
+ // Act
+ String result = transaction.toString();
+
+ // Assert
+ assertTrue(result.contains("TestItem"));
+ assertTrue(result.contains("10"));
+ assertTrue(result.contains("INCOMING"));
+ assertTrue(result.contains("Test note"));
+ assertFalse(result.contains("Order:"));
+ }
+
+ @Test
+ void toString_withOrder_returnsCorrectFormat() {
+ // Arrange
+ Order order = new Order(Order.OrderType.PURCHASE, "Test order");
+ Transaction transaction = new Transaction("TestItem", 10,
+ Transaction.TransactionType.INCOMING, "Test note", order);
+
+ // Act
+ String result = transaction.toString();
+
+ // Assert
+ assertTrue(result.contains("TestItem"));
+ assertTrue(result.contains("10"));
+ assertTrue(result.contains("INCOMING"));
+ assertTrue(result.contains("Test note"));
+ assertTrue(result.contains("Order: " + order.getId()));
+ }
+
+ @Test
+ void transactionTypes_haveCorrectValues() {
+ // Assert
+ assertEquals(2, Transaction.TransactionType.values().length);
+ assertArrayEquals(
+ new Transaction.TransactionType[] {
+ Transaction.TransactionType.INCOMING,
+ Transaction.TransactionType.OUTGOING
+ },
+ Transaction.TransactionType.values()
+ );
+ }
+
+ @Test
+ void multipleCalls_generateDifferentIds() {
+ // Arrange & Act
+ UUID[] ids = new UUID[100];
+ for (int i = 0; i < 100; i++) {
+ Transaction transaction = new Transaction("Item", 1,
+ Transaction.TransactionType.INCOMING, "Note", null);
+ ids[i] = transaction.getId();
+ }
+
+ // Assert
+ // Check that all IDs are unique
+ for (int i = 0; i < ids.length; i++) {
+ for (int j = i + 1; j < ids.length; j++) {
+ assertNotEquals(ids[i], ids[j],
+ "Generated UUIDs should be unique");
+ }
+ }
+ }
+
+ @Test
+ void transaction_withZeroQuantity_createsSuccessfully() {
+ // Arrange & Act
+ Transaction transaction = new Transaction("TestItem", 0,
+ Transaction.TransactionType.INCOMING, "Zero quantity test", null);
+
+ // Assert
+ assertEquals(0, transaction.getQuantity());
+ }
+
+ @Test
+ void transaction_withEmptyNotes_createsSuccessfully() {
+ // Arrange & Act
+ Transaction transaction = new Transaction("TestItem", 10,
+ Transaction.TransactionType.INCOMING, "", null);
+
+ // Assert
+ assertEquals("", transaction.getNotes());
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 892cb6cae7..22c437738d 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,9 +1,21 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
-
-What is your name?
-Hello James Gosling
+ . . . .. . . . . . . . . .:+++: .
+ :&&&&&&&&&&X ;&&&&&&&&&&&& . &&& .. .:&&: . .. . .+XXXXXXXXX:
+. :&&;.. ..&&&& . $&& &&& . .. . :&&: +X+;xXXXXXXX:
+ :&&; .. :&&X . . $&& . . &&& .. . .. :&&: . . . ;X+;xXXXXXXXX;
+. :&&; . .&&& . . $&& . &&& :&&:. . . . ..Xx;+XXXXXXXXx.
+ :&&; . ;&&+ . . $&& . . &&& . . . :&&: . .Xx;;XXXXXXXXX.
+ ..:&&X+++++$&&&x. $&& . . &&&. . :&&: . . .. .++::+xXXXXXXX:. ..
+ :&&&&&&&&&&. $&&.. . &&& . . :&&: . . .:+::;++++++xX;
+ :&&; . . $&& . . &&&. . . . :&&: . :++++++++++xx+
+. :&&; . . .... $&& . . .&&& .. :&&: .. . ++++++++++xx+
+ :&&; $&&. &&& .. .. :&&: . . .+++++++++xxx.
+ :&&; .. :&&&&&&&&&&&& ... &&&&&&&&&&&&&.. .:&&&&&&&&&&&&$ ++++++++xxx:
+ .XX. . . .XXXXXXXXXXXx .XXXXXXXXXXXXX. .XXXXXXXXXXXX+ ... .++++xxx; .. .
+ . . . . . . .. . . . . . . . . .. .. . .
+
+
+
+Hello! I'm PILL! How can I help you today?
+
+
+Bye. Hope to see you again soon!
diff --git a/text-ui-test/data/pill.txt b/text-ui-test/data/pill.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index f6ec2e9f95..ae3bc0a936 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -1 +1 @@
-James Gosling
\ No newline at end of file
+exit
\ No newline at end of file
diff --git a/text-ui-test/log/PillLog.log b/text-ui-test/log/PillLog.log
new file mode 100644
index 0000000000..9272f57ae4
--- /dev/null
+++ b/text-ui-test/log/PillLog.log
@@ -0,0 +1,16 @@
+Oct 21, 2024 10:39:17 PM seedu.pill.util.ItemMap
+INFO: New ItemMap instance created
+Oct 21, 2024 10:39:17 PM seedu.pill.util.ItemMap
+INFO: New ItemMap instance created
+Oct 21, 2024 10:39:17 PM seedu.pill.Pill run
+INFO: New Chatbot Conversation Created
+Oct 21, 2024 10:39:17 PM seedu.pill.Pill run
+INFO: Chatbot Conversation Ended
+Oct 21, 2024 10:39:52 PM seedu.pill.util.ItemMap
+INFO: New ItemMap instance created
+Oct 21, 2024 10:39:52 PM seedu.pill.util.ItemMap
+INFO: New ItemMap instance created
+Oct 21, 2024 10:39:52 PM seedu.pill.Pill run
+INFO: New Chatbot Conversation Created
+Oct 21, 2024 10:39:52 PM seedu.pill.Pill run
+INFO: Chatbot Conversation Ended
diff --git a/unused/OrderItem.java b/unused/OrderItem.java
new file mode 100644
index 0000000000..af9a8b8385
--- /dev/null
+++ b/unused/OrderItem.java
@@ -0,0 +1,166 @@
+//@@author philip1304-unused
+/*
+ * We ended up using regular Items in another ItemMap, rather than using a
+ * different type of item.
+ */
+package seedu.pill.util;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+/**
+ * Represents an item in an order with its quantity and optional expiry date.
+ * OrderItems are immutable to ensure data consistency throughout the order lifecycle.
+ */
+public class OrderItem {
+ private final String itemName;
+ private final int quantity;
+ private final Optional expiryDate;
+ private final double unitPrice;
+
+ /**
+ * Constructs a new OrderItem with the specified name and quantity.
+ *
+ * @param itemName - The name of the item
+ * @param quantity - The quantity ordered
+ * @throws IllegalArgumentException - If quantity is negative or item name is null/empty
+ */
+ public OrderItem(String itemName, int quantity) {
+ this(itemName, quantity, null, 0.0);
+ }
+
+ /**
+ * Constructs a new OrderItem with all properties specified.
+ *
+ * @param itemName - The name of the item
+ * @param quantity - The quantity ordered
+ * @param expiryDate - The expiry date of the item (can be null)
+ * @param unitPrice - The price per unit of the item
+ * @throws IllegalArgumentException - If quantity is negative, price is negative, or item name is null/empty
+ */
+ public OrderItem(String itemName, int quantity, LocalDate expiryDate, double unitPrice) {
+ if (itemName == null || itemName.trim().isEmpty()) {
+ throw new IllegalArgumentException("Item name cannot be null or empty");
+ }
+ if (quantity < 0) {
+ throw new IllegalArgumentException("Quantity cannot be negative");
+ }
+ if (unitPrice < 0) {
+ throw new IllegalArgumentException("Unit price cannot be negative");
+ }
+
+ this.itemName = itemName;
+ this.quantity = quantity;
+ this.expiryDate = Optional.ofNullable(expiryDate);
+ this.unitPrice = unitPrice;
+ }
+
+ /**
+ * Gets the name of the item.
+ *
+ * @return - The item name
+ */
+ public String getItemName() {
+ return itemName;
+ }
+
+ /**
+ * Gets the quantity ordered.
+ *
+ * @return - The quantity
+ */
+ public int getQuantity() {
+ return quantity;
+ }
+
+ /**
+ * Gets the expiry date of the item, if any.
+ *
+ * @return - An Optional containing the expiry date, or empty if no expiry date
+ */
+ public Optional getExpiryDate() {
+ return expiryDate;
+ }
+
+ /**
+ * Gets the unit price of the item.
+ *
+ * @return - The price per unit
+ */
+ public double getUnitPrice() {
+ return unitPrice;
+ }
+
+ /**
+ * Calculates the total price for this order item.
+ *
+ * @return - The total price (quantity * unit price)
+ */
+ public double getTotalPrice() {
+ return quantity * unitPrice;
+ }
+
+ /**
+ * Returns a string representation of this OrderItem.
+ * The string includes the item name, quantity, expiry date (if present),
+ * and pricing information (if unit price is greater than 0).
+ *
+ * @return - A formatted string containing the OrderItem's details
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(itemName)
+ .append(": ")
+ .append(quantity)
+ .append(" units");
+
+ expiryDate.ifPresent(date -> sb.append(", expires: ").append(date));
+
+ if (unitPrice > 0) {
+ sb.append(String.format(", unit price: $%.2f", unitPrice))
+ .append(String.format(", total: $%.2f", getTotalPrice()));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Compares this OrderItem with another object for equality.
+ * Two OrderItems are considered equal if they have the same item name,
+ * quantity, expiry date, and unit price.
+ *
+ * @param obj - The object to compare with this OrderItem
+ * @return - True if the objects are equal, false otherwise
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof OrderItem other)) {
+ return false;
+ }
+ return itemName.equals(other.itemName) &&
+ quantity == other.quantity &&
+ expiryDate.equals(other.expiryDate) &&
+ Double.compare(unitPrice, other.unitPrice) == 0;
+ }
+
+ /**
+ * Generates a hash code for this OrderItem.
+ * The hash code is calculated using all fields of the OrderItem
+ * to ensure it follows the contract with equals().
+ *
+ * @return - A hash code value for this OrderItem
+ */
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + itemName.hashCode();
+ result = 31 * result + quantity;
+ result = 31 * result + expiryDate.hashCode();
+ result = 31 * result + Double.hashCode(unitPrice);
+ return result;
+ }
+}
diff --git a/unused/OrderItemTest.java b/unused/OrderItemTest.java
new file mode 100644
index 0000000000..9f49ce7135
--- /dev/null
+++ b/unused/OrderItemTest.java
@@ -0,0 +1,371 @@
+//@@author philip1304-unused
+// Same reason given in OrderItem.java
+package seedu.pill.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+class OrderItemTest {
+
+ @Test
+ void constructor_basicConstructor_createsOrderItemSuccessfully() {
+ // Arrange & Act
+ OrderItem item = new OrderItem("Aspirin", 100);
+
+ // Assert
+ assertEquals("Aspirin", item.getItemName());
+ assertEquals(100, item.getQuantity());
+ assertEquals(Optional.empty(), item.getExpiryDate());
+ assertEquals(0.0, item.getUnitPrice());
+ }
+
+ @Test
+ void constructor_fullConstructor_createsOrderItemSuccessfully() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now().plusYears(1);
+
+ // Act
+ OrderItem item = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertEquals("Aspirin", item.getItemName());
+ assertEquals(100, item.getQuantity());
+ assertEquals(Optional.of(expiryDate), item.getExpiryDate());
+ assertEquals(5.99, item.getUnitPrice());
+ }
+
+ @Test
+ void constructor_nullItemName_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new OrderItem(null, 100));
+ }
+
+ @Test
+ void constructor_emptyItemName_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new OrderItem("", 100));
+ }
+
+ @Test
+ void constructor_blankItemName_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new OrderItem(" ", 100));
+ }
+
+ @Test
+ void constructor_negativeQuantity_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new OrderItem("Aspirin", -1));
+ }
+
+ @Test
+ void constructor_negativeUnitPrice_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new OrderItem("Aspirin", 100, null, -1.0));
+ }
+
+ @Test
+ void getTotalPrice_calculatesCorrectly() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100, null, 5.99);
+
+ // Act
+ double totalPrice = item.getTotalPrice();
+
+ // Assert
+ assertEquals(599.0, totalPrice, 0.001);
+ }
+
+ @Test
+ void getTotalPrice_withZeroQuantity_returnsZero() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 0, null, 5.99);
+
+ // Act
+ double totalPrice = item.getTotalPrice();
+
+ // Assert
+ assertEquals(0.0, totalPrice, 0.001);
+ }
+
+ @Test
+ void getTotalPrice_withZeroUnitPrice_returnsZero() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100, null, 0.0);
+
+ // Act
+ double totalPrice = item.getTotalPrice();
+
+ // Assert
+ assertEquals(0.0, totalPrice, 0.001);
+ }
+
+ @Test
+ void toString_withoutExpiryAndPrice_returnsBasicFormat() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100);
+
+ // Act
+ String result = item.toString();
+
+ // Assert
+ assertEquals("Aspirin: 100 units", result);
+ }
+
+ @Test
+ void toString_withExpiryDate_includesExpiry() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.of(2024, 12, 31);
+ OrderItem item = new OrderItem("Aspirin", 100, expiryDate, 0.0);
+
+ // Act
+ String result = item.toString();
+
+ // Assert
+ assertTrue(result.contains("expires: 2024-12-31"));
+ }
+
+ @Test
+ void toString_withUnitPrice_includesPricing() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100, null, 5.99);
+
+ // Act
+ String result = item.toString();
+
+ // Assert
+ assertTrue(result.contains("unit price: $5.99"));
+ assertTrue(result.contains("total: $599.00"));
+ }
+
+ @Test
+ void equals_sameValues_returnsTrue() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now().plusYears(1);
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertEquals(item1, item2);
+ }
+
+ @Test
+ void equals_differentValues_returnsFalse() {
+ // Arrange
+ OrderItem item1 = new OrderItem("Aspirin", 100, null, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 200, null, 5.99);
+ OrderItem item3 = new OrderItem("Paracetamol", 100, null, 5.99);
+ OrderItem item4 = new OrderItem("Aspirin", 100, null, 6.99);
+
+ // Assert
+ assertNotEquals(item1, item2); // Different quantity
+ assertNotEquals(item1, item3); // Different name
+ assertNotEquals(item1, item4); // Different price
+ }
+
+ @Test
+ void equals_null_returnsFalse() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100);
+
+ // Assert
+ assertNotEquals(null, item);
+ }
+
+ @Test
+ void equals_differentClass_returnsFalse() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100);
+ String other = "Aspirin";
+
+ // Assert
+ assertNotEquals(other, item);
+ }
+
+ @Test
+ void hashCode_sameValues_returnsSameHash() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now().plusYears(1);
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ void hashCode_differentValues_returnsDifferentHash() {
+ // Arrange
+ OrderItem item1 = new OrderItem("Aspirin", 100, null, 5.99);
+ OrderItem item2 = new OrderItem("Paracetamol", 100, null, 5.99);
+
+ // Assert
+ assertNotEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ void equals_sameObject_returnsTrue() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100, LocalDate.now(), 5.99);
+
+ // Assert
+ assertTrue(item.equals(item)); // Tests the this == obj condition
+ }
+
+ @Test
+ void equals_nullObject_returnsFalse() {
+ // Arrange
+ OrderItem item = new OrderItem("Aspirin", 100);
+
+ // Assert
+ assertFalse(item.equals(null)); // Tests null comparison
+ }
+
+ @Test
+ void equals_allFieldsSame_returnsTrue() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertTrue(item1.equals(item2)); // Tests all fields equality
+ assertTrue(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_differentItemName_returnsFalse() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Paracetamol", 100, expiryDate, 5.99);
+
+ // Assert
+ assertFalse(item1.equals(item2));
+ assertFalse(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_differentQuantity_returnsFalse() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 200, expiryDate, 5.99);
+
+ // Assert
+ assertFalse(item1.equals(item2));
+ assertFalse(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_differentExpiryDate_returnsFalse() {
+ // Arrange
+ LocalDate date1 = LocalDate.now();
+ LocalDate date2 = date1.plusDays(1);
+ OrderItem item1 = new OrderItem("Aspirin", 100, date1, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, date2, 5.99);
+
+ // Assert
+ assertFalse(item1.equals(item2));
+ assertFalse(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_differentUnitPrice_returnsFalse() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 6.99);
+
+ // Assert
+ assertFalse(item1.equals(item2));
+ assertFalse(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_sameValuesWithNullExpiryDate_returnsTrue() {
+ // Arrange
+ OrderItem item1 = new OrderItem("Aspirin", 100, null, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, null, 5.99);
+
+ // Assert
+ assertTrue(item1.equals(item2));
+ assertTrue(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_oneNullExpiryDate_returnsFalse() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, null, 5.99);
+
+ // Assert
+ assertFalse(item1.equals(item2));
+ assertFalse(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_veryCloseUnitPrices_handlesDoublePrecision() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ // Use exactly the same double value
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertTrue(item1.equals(item2));
+ assertTrue(item2.equals(item1)); // Tests symmetry
+ }
+
+ @Test
+ void equals_transitivityProperty_maintainsConsistency() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item3 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ assertTrue(item1.equals(item2));
+ assertTrue(item2.equals(item3));
+ assertTrue(item1.equals(item3)); // Tests transitivity
+ }
+
+ @Test
+ void equals_consistencyWithHashCode_maintainsContract() {
+ // Arrange
+ LocalDate expiryDate = LocalDate.now();
+ OrderItem item1 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, expiryDate, 5.99);
+
+ // Assert
+ // If two objects are equal, their hash codes must be equal
+ assertTrue(item1.equals(item2));
+ assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ void equals_multipleCallsSameObject_consistentResults() {
+ // Arrange
+ OrderItem item1 = new OrderItem("Aspirin", 100, LocalDate.now(), 5.99);
+ OrderItem item2 = new OrderItem("Aspirin", 100, LocalDate.now(), 5.99);
+
+ // Assert
+ // Multiple calls should return consistent results
+ boolean firstCall = item1.equals(item2);
+ boolean secondCall = item1.equals(item2);
+ boolean thirdCall = item1.equals(item2);
+
+ assertEquals(firstCall, secondCall);
+ assertEquals(secondCall, thirdCall);
+ }
+}
diff --git a/unused/TimestampIO.java b/unused/TimestampIO.java
new file mode 100644
index 0000000000..a8d9eed243
--- /dev/null
+++ b/unused/TimestampIO.java
@@ -0,0 +1,52 @@
+//@@author philip1304-unused
+/*
+ * We did not get around to integrating timestamps since we had too many bugs
+ * in our code and many other features that needed integrating at the same time.
+ */
+package seedu.pill.util;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Handles timestamped input/output operations for the Pill inventory management system.
+ * Provides methods to log both user inputs and system outputs with timestamps.
+ * Supports full Unicode character set for internationalization.
+ */
+public class TimestampIO {
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ /**
+ * Prints a timestamped output message.
+ * @param message - The message to be printed
+ */
+ public static void printOutput(String message) {
+ printTimestamped("OUT", message);
+ }
+
+ /**
+ * Logs a timestamped input message.
+ * @param input - The user input to be logged
+ */
+ public static void logInput(String input) {
+ printTimestamped("IN", input);
+ }
+
+ /**
+ * Prints a timestamped error message.
+ * @param error - The error message to be printed
+ */
+ public static void printError(String error) {
+ printTimestamped("ERR", error);
+ }
+
+ /**
+ * Helper method to print timestamped messages in a consistent format.
+ * @param type - The type of message (IN/OUT/ERR)
+ * @param message - The message content
+ */
+ private static void printTimestamped(String type, String message) {
+ String timestamp = LocalDateTime.now().format(formatter);
+ System.out.printf("[%s] %s: %s%n", timestamp, type, message);
+ }
+}
diff --git a/unused/TimestampIOTest.java b/unused/TimestampIOTest.java
new file mode 100644
index 0000000000..203c8f0e83
--- /dev/null
+++ b/unused/TimestampIOTest.java
@@ -0,0 +1,105 @@
+//@@author philip1304-unused
+// Same reason given in TimestampIO.java
+package seedu.pill.util;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TimestampIOTest {
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+
+ @BeforeEach
+ void setUp() {
+ System.setOut(new PrintStream(outContent));
+ }
+
+ @AfterEach
+ void restoreStreams() {
+ System.setOut(originalOut);
+ }
+
+ /**
+ * Verifies that a timestamped output string matches the expected format and content.
+ */
+ private boolean verifyTimestampedOutput(String output, String expectedMessageType, String expectedContent) {
+ // Check basic format
+ if (!output.startsWith("[") || output.length() < 21) {
+ return false;
+ }
+
+ try {
+ // Extract timestamp
+ String timestamp = output.substring(1, 20);
+ LocalDateTime outputTime = LocalDateTime.parse(timestamp, formatter);
+
+ // Verify timestamp is recent
+ LocalDateTime now = LocalDateTime.now();
+ long timeDiff = ChronoUnit.SECONDS.between(outputTime, now);
+ if (Math.abs(timeDiff) > 1) {
+ return false;
+ }
+
+ // Verify message format
+ String expectedFormat = String.format("[%s] %s: %s",
+ timestamp,
+ expectedMessageType,
+ expectedContent);
+
+ return output.equals(expectedFormat);
+
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Test
+ void printOutput_simpleMessage_formatsCorrectly() {
+ String message = "Test message";
+ TimestampIO.printOutput(message);
+ String output = outContent.toString().trim();
+ assertTrue(verifyTimestampedOutput(output, "OUT", message));
+ }
+
+ @Test
+ void printOutput_messageWithSpecialCharacters_formatsCorrectly() {
+ String message = "Test message with !@#$%^&*()";
+ TimestampIO.printOutput(message);
+ String output = outContent.toString().trim();
+ assertTrue(verifyTimestampedOutput(output, "OUT", message));
+ }
+
+ @Test
+ void printError_simpleError_formatsCorrectly() {
+ String error = "Test error";
+ TimestampIO.printError(error);
+ String output = outContent.toString().trim();
+ assertTrue(verifyTimestampedOutput(output, "ERR", error));
+ }
+
+ @Test
+ void printError_errorWithSpecialCharacters_formatsCorrectly() {
+ String error = "Test error with !@#$%^&*()";
+ TimestampIO.printError(error);
+ String output = outContent.toString().trim();
+ assertTrue(verifyTimestampedOutput(output, "ERR", error));
+ }
+
+ @Test
+ void output_withNewlines_preservesFormatting() {
+ String message = "Line 1\nLine 2\nLine 3";
+ TimestampIO.printOutput(message);
+ String output = outContent.toString().trim();
+ assertTrue(verifyTimestampedOutput(output, "OUT", message));
+ }
+}