diff --git a/.gitignore b/.gitignore index 2873e189e1..c8efcb9c4d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +# Local data and log files +/data/ +/log/ + +# Artifact manifest file +/META-INF/ \ No newline at end of file diff --git a/README.md b/README.md index e243ece764..d76453d27a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# Pharmacy Inventory & Logistics Ledger (PILL) This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. diff --git a/build.gradle b/build.gradle index ea82051fab..74aa189fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation 'org.knowm.xchart:xchart:3.8.2' } test { @@ -29,7 +30,7 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.pill.Pill") } shadowJar { @@ -41,6 +42,7 @@ checkstyle { toolVersion = '10.2' } -run{ +run { standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..4d98e075c4 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +Display | Name | Github Profile | Portfolio +--------|:-----------------:|:-----------------------------------------:|:---------: +![](./team/assets/github_cat.png) | Benjamin | [Github](https://github.com/yakultbottle) | [Portfolio](team/yakultbottle.md) +![](./team/assets/github_cat.png) | Yijian | [Github](https://github.com/yijiano) | [Portfolio](team/yijiano.md) +![](./team/assets/github_cat.png) | Nivedit | [Github](https://github.com/cnivedit) | [Portfolio](team/cnivedit.md) +![](./team/assets/github_cat.png) | Xinchi | [Github](https://github.com/cxc0418) | [Portfolio](team/cxc0418.md) +![](./team/assets/github_cat.png) | Philip | [Github](https://github.com/philip1304) | [Portfolio](team/philip1304.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..6858850de7 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,424 @@ # Developer Guide +## Table of Contents + +- [Developer Guide](#developer-guide) + - [Table of Contents](#table-of-contents) + - [Acknowledgements](#acknowledgements) + - [Design & Implementation](#design--implementation) + - [UI and I/O](#ui-and-io) + - [Commands](#commands) + - [Storage](#storage) + - [Item and ItemMap](#item-and-itemmap) + - [Orders and Transactions](#orders-and-transactions) + - [Visualizer](#visualizer) + - [Parser](#parser) + - [Exceptions](#exceptions) + - [Logging](#logging) + - [Product scope](#product-scope) + - [Target user profile](#target-user-profile) + - [Value proposition](#value-proposition) + - [User Stories](#user-stories) + - [Non-Functional Requirements](#non-functional-requirements) + - [Glossary](#glossary) + - [Instructions for Testing](#instructions-for-testing) + - [Manual Testing](#manual-testing) + - [JUnit Testing](#junit-testing) + - [Text UI Testing](#text-ui-testing) + - [Future Enhancements](#future-enhancements) + ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} + + +PILL uses the following tools for development: +1. [JUnit 5](https://junit.org/junit5/) - Used for testing. +2. [Gradle](https://gradle.org/) - Used for build automation. +3. [XChart](https://github.com/knowm/XChart) - A plotting library for visualizing data within the application. ## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +The project is designed using the Model-View-Controller (MVC) architecture, with the following components: + +1. **Model**: Contains the core logic of the application, including the Item, ItemMap, and Storage classes. +2. **View**: The user interface, which is implemented as a command-line interface (CLI). +3. **Controller**: The command classes, which interpret user input and call the appropriate methods from the Model and + Storage classes. +4. **Storage**: Handles the reading and writing of data to a CSV file. +5. **DateTime**: A utility class to handle date and time operations. +6. **Exceptions**: Custom exceptions to handle printing error messages in a neater way. +7. **PillLogger**: A centralized utility class to handle logging across the entire application. +8. **PillException**: A custom exception class to handle exceptions specific to the PILL application. +9. **Item**: A class to represent an item in the inventory. +10. **ItemMap**: A class to store items in a map-like structure. +11. **Command**: A class to represent a command that can be executed by the application. +12. **Parser**: A class to parse user input and return the corresponding command. +13. **Pill**: The main class that initializes the application and starts the CLI. + +### High-Level Overview + +The high-level overview of the project structure is as follows: + +High Level Overview of PILL + + + + +### UI and I/O + +The program uses a command-line interface (CLI) for interaction with the user. +It receives input as text commands, processes these commands, and provides feedback through the console. + +The UI consists of the following components: + +1. **Parser**: The Parser class is responsible for interpreting user input and returning the corresponding command. +2. **Command**: The Command class represents a command that can be executed by the application. +3. **Pill**: The main class that initializes the application and starts the CLI. + +The UI components work together to provide a seamless user experience, allowing users to interact with the application. + +### Commands + +Each user action (e.g. adding, deleting, or editing an item) is mapped to a specific command class. +Each specific class inherits from the abstract Command class. These classes handle +the logic for interpreting the input and calling the appropriate methods from the +ItemMap and Storage classes via the usage of polymorphism. + +Below is the overview for how Commands are executed from the perspective of the Parser class. + +![Command overview](diagrams/Command-Overview-SequenceDiagram.png) + +Example: + +``` +AddItemCommand command = new AddItemCommand(itemName, quantity, expiryDate); +command.execute(itemMap, storage); +``` + + + +#### AddItemCommand + +The AddItemCommand intialises an Item with the corresponding name, quantity, +and expiryDate. Then the ItemMap is checked as to whether an Item with exactly +the same name and expiryDate already exists in the ItemMap. If it does, the +corresponding item's quantity is updated to include the new Item's quantity as well. +Else, a new Item entry is added. Finally, the Storage is updated with the corresponding +item. + +Not depicted are the print calls to the System.out and Logger classes for brevity. + + + +### Storage + +**API**: Storage.java + +Entries are stored in Comma Separated Values(CSV) format. Fields read from left +to right are: **Item type**, **Quantity**, and **Expiry date**(optional). Items +may or may not have an expiry date, but all possess an Item type and a Quantity. + +Example of stored entries: + +``` +panadol,1 +panadol,30,2024-08-03 +panadol,20,2024-08-09 +bandage,34 +``` + +The Storage class depends on self-defined classes PillException, Item, and +ItemMap. While it has other dependencies, such as File and FileWriter from +the Java standard library, PillException is the only custom class it depends on. + +### Item and ItemMap + +The Item class has five private variables, a name, a quantity, an +expiry date, a cost and a price. An Item may or may not have an expiry date, so we store it +as an Optional, which handles empty values for us without using null. + +![](diagrams/Item-ClassDiagram.png) + +Quantity will always be a positive integer, and if no quantity is specified +in the constructor, the default value is 1. Similarly, if no value is provided +for expiry date, then it will be an Optional.empty(). + +The ItemMap class contains a key-value pair, implemented as a Map, from the +item name(String) to the item(TreeSet\) + +![](diagrams/ItemMap-ClassDiagram.png) + +Each TreeSet\ represents an item type, with each entry in the TreeSet +having a unique expiry date. The TreeSet then orders Items based on the +compareTo method overridden in the Item class, which sorts by the earliest +expiry date to the latest. Items with no expiry date, aka an expiry date of +Optional.empty(), will be the last entry in the TreeSet. + +The usage of TreeSet is to facilitate storing multiple batches of items with +different expiry dates and quantities, and to be able to extract items with the +soonest expiry date when taking out of storage. + + + +### Orders and Transactions + + + +#### Orders + +Orders are from the perspective of the Inventory, so purchases are items being +received into the inventory, and dispense refers to items going out of the inventory. +Each order is a collection of one or more items, and is associated to either of the order types: purchase or dispense. + + + +#### Transactions + +A transactions represents an inflow/outflow of items to/from the inventory. +Each transaction is associated with a single item and the corresponding order. +When a transaction is created, the inventory is updated to reflect this inflow/outflow by +invoking the `addItem` or `useItem` methods, depending on whether it is an incoming or outgoing transaction. + +#### Order Fulfillment + +An order is said to be fulfilled when the inflow/outflow of the items ordered have occurred. +Each time an order is fulfilled, a corresponding transaction is created for each individual item in the order and the +order is marked as fulfilled. + +#### TransactionManager +The `TransactionManager` class functions as the entry point for all `Order` and `Transaction` related functionalities. +`Pill` instantiates a `TransactionManager`, which handles these functionalities throughout the application. +`TransactionManager` keeps track of all created orders and transactions and handles the interactions within these +classes. + +The interaction between the different members of the `TransactionManager` class is better visualized below: + + + + + +### Visualizer + +The Visualizer class is the core class for handling the visualization of item data. +It leverages the XChart library to generate bar charts for different aspects +of the inventory. It is responsible for providing a graphical view +of item data, such as item prices, costs, and stock levels. This class +enhances the usability of the application by allowing users to better understand +and analyze their inventory data through visual representation. + +![](diagrams/Visualizer-ClassDiagram.png) + + + +### Parser + +The Parser class is responsible for interpreting and executing user commands +within the application. It translates raw user input into actionable commands, +checks for common errors, and ensures the commands are executed in a controlled +and predictable way. The class contains several helper methods that assist in +interpreting specific command formats, while the main parseCommand method +coordinates this process. + +#### Responsibilities +- Input Parsing: Parser takes a single line of user input, splits it into its + component parts (command and arguments), and identifies which command the + user intended to execute. +- Command Dispatching: Based on the parsed command, Parser creates the + appropriate command object (e.g., AddCommand, DeleteCommand, EditCommand) and + invokes its execute method. +- Error Handling: All exceptions related to parsing and validation (encapsulated + in PillException) are handled directly within Parser. This ensures that any + invalid input or command errors are handled without propagating out. + +#### Key Functionalities + +1. Command Recognition: + The first word of each input string determines the command type. For example, + “add” initiates the AddCommand, while “delete” initiates the DeleteCommand. + Commands can also support optional flags and arguments, parsed from the + remaining components of the input. + +2. Argument and Flag Parsing: + After identifying the command, Parser extracts and verifies additional + arguments, flags, and parameters. Specific commands (e.g., stock-check, + expiring) have strict requirements on the number and format of arguments, which + are validated before command execution. + +3. Command Execution: + For each recognized command, the corresponding command object is created and + its execute method is invoked with any necessary arguments. All command objects + receive items and storage as parameters, enabling them to interact with the + application’s data layer. + +4. Exception Management: + Each helper method that parses specific commands may throw a PillException + for invalid arguments, unsupported commands, or unexpected input formats. + These exceptions are caught within the parseCommand method, allowing Parser + to handle error messages consistently across the application. + +#### Assumptions + +Parser expects commands to follow a specific format. For instance, commands +like `add` require additional arguments, while commands like `list` and `exit` +require none. Only recognized commands will proceed to execution; any unrecognized +command results in a PillException. + + +### Exceptions + +All exceptions are of the PillException type, constructed with an +ExceptionMessages enum value to indicate specific error cases. The +ExceptionMessages enum provides predefined messages accessible through +getMessage(), ensuring consistent error descriptions across the application. +Most importantly, this keeps code of thrown exceptions readable. + +Example usage: + +``` +} catch (NumberFormatException e) { + throw new PillException(ExceptionMessages.INVALID_INDEX); +``` + + + +### Logging + +**API**: PillLogger.java + +Component Diagram for PillLogger + +The project uses the `java.util.logging` package for logging, with PillLogger serving as a centralized utility class to +handle logging across the entire application. PillLogger implements the singleton pattern by maintaining a single static +logger instance, which manages log creation, configuration, and output redirection. + +#### Key Components + +- File Output Configuration: The log level for file output is set by the `fileHandler.setLevel()` call, using + `Level.ALL` to + capture all events during execution. The log file, named according to the `FILE_NAME` attribute, is created in the + directory specified by `PATH`. + +- Console Output Configuration: Console output is managed by `consoleHandler.setLevel()`. To maintain a clean terminal + output for end-users, console logging is set to `Level.OFF` by default, ensuring it is suppressed unless required for + debugging. + +#### Resilience and Error Handling + +In the event of a failure in log file creation, PillLogger logs the error to the console and allows the application to +continue running. This design ensures the application’s functionality is not hindered by logging setup issues. + +#### API Access + +PillLogger exposes a single public method, `getLogger()`, which provides application-wide access to the singleton Logger +instance. Classes within the application use `getLogger()` to record events, without needing to set up or manage their +own +loggers. + + ## Product scope + ### Target user profile -{Describe the target user profile} +Pharmacy Inventory & Logistics Ledger (PILL) is designed for personnel responsible for managing and monitoring +pharmaceutical inventories. Target users include pharmacists, pharmacy cashiers, logistics in-charge personnel, and +regional managers in pharmacies or healthcare facilities, particularly those who prefer a command-line interface (CLI) +for its efficiency and simplicity. ### Value proposition -{Describe the value proposition: what problem does it solve?} +PILL provides a streamlined, user-friendly command-line interface (CLI) to manage pharmaceutical inventory with ease. +This minimalistic tool facilitates quick and efficient inventory operations, tailored to the requirements of pharmacy +staff and logistics managers who need to track stock, monitor expiry dates, and generate reports without unnecessary +complexity. ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|---------------------|-------------------------------------------|--------------------------------------------------------------| +| v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v1.0 | pharmacist | add new medicine stock | keep track of new arrivals | +| v1.0 | pharmacy cashier | remove items from stock | remove them from the record when they run out | +| v1.0 | logistics in-charge | list out all medicines currently stocked | get an overview of what the pharmacy has at the moment | +| v1.0 | regional manager | export all items as a human readable file | compare across multiple outlets | +| v1.0 | frequent user | find items provided a keyword | find relevant items without having to look through all items | +| v2.0 | logistics in-charge | list all expiring items | remember to clear them out | +| v2.0 | logistics in-charge | list all items that need to be restocked | place orders before items run out | ## Non-Functional Requirements -{Give non-functional requirements} +* Technical Requirements: Any *mainstream OS* with Java 17 or above installed. + Instructions for downloading Java 17 can be found + [here](https://www.oracle.com/sg/java/technologies/javase/jdk17-archive-downloads.html). +* Project Scope Constraints: The application should only be used for tracking. It is not meant to be involved in any + form of monetary transaction. +* Project Scope Constraints: Data storage is only to be performed locally. +* Quality Requirements: The application should be able to be used effectively by a novice with little experience with + CLIs. ## Glossary -* *glossary item* - Definition +- **Mainstream OS**: Windows, Linux, Unix, MacOS +- **CLI (Command-Line Interface)**: A text-based user interface used to interact with software by typing commands. +- **UI (User Interface)**: The components and layout of a software application + with which users interact, including visual elements like buttons, screens, + and command prompts that facilitate user interaction with the program. +- **I/O (Input/Output)**: The communication between a program and the external + environment, such as receiving data from the user (input) or displaying results + to the user (output). +- **JUnit**: A testing framework for Java that allows developers to write and run + repeatable automated tests. +- **Gradle**: A build automation tool for Java (and other languages) used to compile, test, and package applications. +- **Text UI Testing**: A form of testing where interactions with a command-line + interface are tested by simulating user input and validating the application’s text output. +- **Pharmaceutical Inventory**: Refers to the stock of medicines and other medical + supplies maintained by a pharmacy or healthcare facility for dispensing and logistics. +- **Expiry Date**: The date after which a pharmaceutical product is no longer considered safe or effective for use. +- **Restocking**: The process of replenishing inventory to ensure sufficient supply. + +## Instructions for Testing + +### Manual Testing + +View the [User Guide](UserGuide.md) for the full list of UI commands and +their related use case and expected outcomes. + +### JUnit Testing + +JUnit tests are written in the [test directory](../src/test/java/seedu/pill/) +and serve to test key methods part of the application. + +### Text UI Testing + +Files relating to Text UI Testing can be found [here](../text-ui-test/). + +To run the Text UI tests, navigate to the `text-ui-test` directory in the terminal. + +When running tests on a Windows system, run the following command from the +specified directory: + +``` +./runtest.bat +``` + +When running tests on a UNIX-based system, run the following command from the specified directory: + +``` +./runtest.sh +``` + +Outcomes of these tests are listed in the below code segment. + +``` +// Successfully passed all tests +All tests passed! -## Instructions for manual testing +// Tests failed: 1 +``` -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +## Future Enhancements +- Implement the ability to take in special characters in item names. +- Implement a feature to track the price of items. +- Implement a feature to track the cost of items. +- Implement a feature to track the profit of items. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..38934707b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,20 @@ -# Duke +# Pharmacy Inventory & Logistics Ledger (PILL) -{Give product intro here} +![Ui](assets/pill.png) -Useful links: +Pharmacy Inventory & Logistics Ledger (PILL) is a powerful Command Line Interface (CLI) application that streamlines pharmaceutical inventory management for small to medium pharmacies. It helps pharmacists and inventory managers prevent losses from expired medicines, optimize stock levels, and make data-driven purchasing decisions. + +Key features: +- Track inventory levels, costs, and expiry dates in real-time +- Get early warnings about medicines nearing expiry +- Generate insights through interactive data visualizations +- Manage supplier orders and customer transactions efficiently +- Export reports for compliance and business analysis + +PILL is designed for users who prefer keyboard-based interactions over GUI applications, offering faster data entry and retrieval through CLI commands. + + +## Useful links: * [User Guide](UserGuide.md) * [Developer Guide](DeveloperGuide.md) * [About Us](AboutUs.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..4ab2f48770 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,561 @@ -# User Guide +# Pharmacy Inventory & Logistics Ledger (PILL) User Guide + +**Version 2.1** + +1. [Introduction](#introduction) +2. [Important Usage Notes](#important-usage-notes) + - [Order of arguments](#order-of-arguments) + - [No special characters](#no-special-characters) + - [Expiry Date](#expiry-date) +3. [Features](#features) + - **General Commands** + - [Viewing Help: `help`](#viewing-help-help) + - [Listing All Items: `list`](#listing-all-items-list) + - [Exiting the Program: `exit`](#exiting-the-program-exit) + - **Item Management** + - [Adding New Item: `add`](#adding-new-item-add) + - [Deleting Existing Item: `delete`](#deleting-existing-item-delete) + - [Editing Existing Item: `edit`](#editing-existing-item-edit) + - [Finding Items: `find`](#finding-items-find) + - [Priority Removal of Items: `use`](#priority-removal-of-items-use) + - **Expiry Management** + - [List Expiring Items: `expiring`](#list-expiring-items-expiring) + - [List Expired Items: `expired`](#list-expired-items-expired) + - **Stock Management** + - [Query Existing Stock: `stock-check`](#query-existing-stock-stock-check) + - [Restock Specific Item: `restock`](#restock-specific-item-restock) + - [Restock All Items Below Threshold: `restock-all`](#restock-all-items-below-threshold-restock-all) + - **Price and Cost Management** + - [Set Item Cost: `cost`](#set-item-cost-cost) + - [Set Item Price: `price`](#set-item-price-price) + - **Visualization** + - [Visualize item prices: `visualize-price`](#visualize-item-prices-visualize-price) + - [Visualize item costs: `visualize-cost`](#visualize-item-costs-visualize-cost) + - [Visualize item stock: `visualize-stock`](#visualize-item-stock-visualize-stock) + - [Visualize item costs and prices: `visualize-cost-price`](#visualize-item-costs-and-prices-visualize-cost-price) + - **Order and Transaction Management** + - [Order Items: `order`](#order-items-order) + - [View All Orders: `view-orders`](#view-all-orders-view-orders) + - [Fulfill Order: `fulfill-order`](#fulfill-order-fulfill-order) + - [Viewing Transactions: `transactions`](#view-transactions-transactions) + - [Viewing Transaction History: `transaction-history`](#view-transactions-within-a-set-time-period-transaction-history) + ## Introduction -{Give a product intro} +Pharmacy Inventory & Logistics Ledger (PILL) is a powerful Command Line Interface (CLI) application that streamlines pharmaceutical inventory management for small to medium pharmacies. It helps pharmacists and inventory managers prevent losses from expired medicines, optimize stock levels, and make data-driven purchasing decisions. + +Key features: +- Track inventory levels, costs, and expiry dates in real-time +- Get early warnings about medicines nearing expiry +- Generate insights through interactive data visualizations +- Manage supplier orders and customer transactions efficiently +- Export reports for compliance and business analysis + +PILL is designed for users who prefer keyboard-based interactions over GUI applications, offering faster data entry and retrieval through CLI commands. + + + + +--- + +## Important Usage Notes + +### Order of arguments + +Our commands **do not** allow arguments in any order. Please read the documentation for the +order in which arguments should follow a command. + +### No special characters + +Our application **does not support special characters** for any input. + +Preferably, please keep to **lowercase, alphanumeric characters** for command inputs. + +### Expiry Date + +Under most commands, items with the same name but different expiry dates are treated as **different items**. Be very careful when reading command instructions: +- **Include the expiry date** with the item name for commands where it is required to ensure the correct item entry is processed. +- Always double-check the item's expiry date before executing commands like `delete` or `edit` to avoid unintentional modifications or deletions. + + + +## Features + +### Viewing Help: `help` + +Displays a list of all available commands and their descriptions. + +**Format**: `help (COMMAND_NAME) (-v)` + +- Optional: `COMMAND_NAME` specifies the command to display help for. +- Optional: `-v` flag to display verbose help for the specified command. + + + +--- + +### Adding New Item: `add` + +Adds a new item to the inventory, specifying its name and quantity. + +**Format**: `add NAME (QUANTITY) (EXPIRY_DATE)` + +- Optional: `QUANTITY` specifies the quantity of the item to add. Defaults to 1. +- Optional: `EXPIRY_DATE` specifies the expiry date of the item in `YYYY-MM-DD` format. + +**Sample Output**: + +`> add Aspirin 100 2024-05-24` + +``` +Added the following item to the inventory: +Aspirin: 100 in stock, expiring: 2024-05-24 +``` + +--- + +### Listing All Items: `list` + +Displays a list of all items currently stored in the inventory, including their names and quantities. + +**Format**: `list` + +**Sample Output**: + +`> list` + +``` +Listing all items: +1. candles: 900 in stock, cost: $110.0 +2. can: 900 in stock +3. panadol: 999990 in stock, expiring: 2024-05-16 +4. panadol: 1000 in stock +5. syringe: 100 in stock +6. cans: 10 in stock +7. Aspirin: 100 in stock, expiring: 2024-05-24 +``` + + + +--- + +### Deleting Existing Item: `delete` + +The `delete` command is used to remove an existing item entry from the inventory. The behavior of this +command depends on whether the item has an associated expiry date. + +**Format**: `delete NAME (EXPIRY_DATE)` + +- `NAME` of the item you wish to delete. +- Optional: `EXPIRY_DATE` is a parameter in the `YYYY-MM-DD` format that **must** be provided if the item you want to delete has an expiry date. + +**Command Behavior**: + +- If an item does not have an expiry date, you can delete it using only the NAME. +- If an item has an expiry date, you must specify the EXPIRY_DATE to delete the correct entry. +- If you attempt to delete an item with an expiry date but do not provide the EXPIRY_DATE, the system will return an "Item not found" error. + +**Sample Output**: + +`> delete cans` + +``` +Deleted the following item from the inventory: +cans: 10 in stock +``` + +`> delete pear 2010-12-12` + +``` +Deleted the following item from the inventory: +pear: 20 in stock, expiring: 2010-12-12 +``` + +**Notes**: +- Always provide the `EXPIRY_DATE` when deleting items that have expiry dates to ensure the correct entry is removed. +- Use the `list` command to view all items and their expiry dates before attempting to delete. + +--- +### Editing Existing Item: `edit` + +The `edit` command is used to **update the quantity** of an existing item entry in the inventory. This does not modify the expiry date of the item. + +The behavior of this command depends on whether the item has an associated expiry date. + +**Format**: `edit NAME QUANTITY (EXPIRY_DATE)` + +- `NAME` of the item you wish to edit. +- `QUANTITY` to update to for the specified item. +- Optional: `EXPIRY_DATE` is a parameter in the `YYYY-MM-DD` format that must be provided if the item has an expiry date. + +**Command Behavior**: + +- Editing Items Without an Expiry Date: + - If an item does not have an expiry date, you can edit it using only the `NAME` and `QUANTITY`. + - Example: `edit NAME QUANTITY` +- Editing Items With an Expiry Date: + - If an item has an expiry date, you must specify the `EXPIRY_DATE` to edit the correct entry. + - If you attempt to edit an item with an expiry date but do not provide the `EXPIRY_DATE`, the system will return an "Item not found" error. + - Example: `edit NAME QUANTITY EXPIRY_DATE` +- Handling Errors: + - If the specified item does not exist in the inventory, or the provided details do not match an existing entry, the system will return an "Item not found" error. + +**Sample Output**: + +- `> edit Panadol 20` + +``` +Edited item: Panadol: 20 in stock +``` + +- `> edit Zyrtec 30 2025-02-03` + +``` +Edited item: Zyrtec: 30 in stock, expiring: 2025-02-03 +``` +--- + +### Finding Items: `find` + +Finds all items in the inventory that match a specified name or partial name. + +**Format**: `find KEYWORD` + +**Sample Output**: + +`> find Panadol` + +``` +Listing all items: + +1. Panadol: 20 in stock, expiring: 2024-12-31 +2. Big Panadol: 50 in stock +``` + +**Notes**: + +- The `find` command is **not** case-sensitive. This means that `find PANADOL` and `find panadol` will yield the same results. +- Use `find` to quickly locate items, especially when you only remember part of the name. + +--- +### List Expiring Items: `expiring` + +Displays all items that will expire before a specified date. + +**Format**: `expiring EXPIRY_DATE` + +- The `EXPIRY_DATE` must be in `YYYY-MM-DD` format. + +**Sample Output**: + +`> expiring 2024-12-31` +``` +Listing all items expiring before 2024-12-31: + +1. Panadol: 99 in stock, expiring: 2024-12-12 +``` + +--- + +### List Expired Items: `expired` + +Lists all items that have expired as of the current date. + +**Format**: `expired` + +**Sample Output**: + +`> expired` +``` +Listing all items that have expired: + +1. Ibuprofen: 10 in stock, expiring: 2023-11-01 +2. Aspirin: 5 in stock, expiring: 2023-10-10 +``` +--- +### Query Existing Stock: `stock-check` + +Displays all items that have stock levels below a specified threshold. + +**Format**: `stock-check THRESHOLD` + +- `THRESHOLD` is the minimum stock level for listing items. + +**Sample Output**: + +`> stock-check 50` +``` +Listing all items that need to be restocked (less than 50): + +Ibuprofen: 10 in stock +Aspirin: 5 in stock +``` + +--- +### Set Item Cost: `cost` + +Sets the cost for a specified item, applied to all entries with the same name, regardless of expiry date. + +**Format**: `cost ITEM_NAME AMOUNT` + +**Sample Output**: + +`> cost Panadol 15` + +``` +Set cost of Panadol to $15.00. +``` +--- +### Set Item Price: `price` + +Sets the selling price for a specified item, applied to all entries with the same name, regardless of expiry date. + +**Format**: `price ITEM_NAME AMOUNT` + +**Sample Output**: + +`> price Panadol 20` + +``` +Set price of Panadol to $20.00. +``` +--- +### Visualize item prices: `visualize-price` + +The visualize-price command will display a chart showing the prices of all items in the inventory. Each bar represents the price of an item, and items are labeled with their names and expiry dates (if applicable). + +**Format**: `visualize-price` + +--- +### Visualize item costs: `visualize-cost` + +This command will display a chart showing the costs of all items in the inventory. Each bar represents the cost of an item, and items are labeled with their names and expiry dates (if applicable). + +**Format**: `visualize-cost` + +--- +### Visualize item stock: `visualize-stock` + +This command will display a chart showing the quantity of items in stock. Each bar represents the stock level of an item, and items are labeled with their names and expiry dates (if applicable). + +**Format**: `visualize-stock` + +--- +### Visualize item costs and prices: `visualize-cost-price` + +This command will display a chart comparing the costs and prices of all items. Each item will have two bars: one for cost and one for price, labeled with the item name and expiry date (if applicable). + +**Format**: `visualize-cost-price` + +--- +### Restock Specific Item: `restock` + +The restock command allows you to restock a specific item to a desired quantity if it is below the specified stock level +, displaying the restock cost. The behavior of the command differs based on whether or not an expiry date is provided. + +**Format**: `restock ITEM_NAME (EXPIRY_DATE) QUANTITY` +- `ITEM_NAME` of the Item you wish to restock. +- Optional: `(EXPIRY_DATE)` is a parameter in the `YYYY-MM-DD` format. This specifies which item entry to restock if there are multiple entries with the same item name but different expiry dates. +- `QUANTITY` is the new stock quantity. + +**Command Behavior**: +- The `EXPIRY_DATE` is mandatory when restocking items that have expiry dates. You must specify the expiry date explicitly, even if only one entry with that expiry date exists. +- If an item **does not have an expiry date**, the command will restock that entry without needing an expiry date. +- If you attempt to restock an item with an expiry date but fail to provide the `EXPIRY_DATE`, the system will display an "Item not found" error. + +**Sample Output**: + +`> restock Panadol 2024-12-31 100` + +``` +Restocked Item: Panadol, Current Stock: 1, New Stock: 100, Total Restock Cost: $1485.00 +``` + +--- + +### Restock All Items Below Threshold: `restock-all` + +Restocks all items with a quantity below a specified threshold to that threshold, listing each item's restock cost. + +**Format**: `restock-all THRESHOLD` + +**Sample Output**: + +`> restock-all 50` + +``` +Item: Ibuprofen, Current Stock: 10, New Stock: 50, Restock Cost: $40.00 +Total Restock Cost for all items below threshold 50: $40.00 +``` +--- + +### Priority Removal of Items `use` + +Priority removal of items from the list, starting with the earliest expiry date. + +**Format**: `use ITEM_NAME QUANTITY` + +**Sample Output**: + +`> use panadol 100` + +``` +Deleted the following item from the inventory: +panadol: 90 in stock, expiring: 2023-05-17 +Edited item: panadol: 999990 in stock, expiring: 2024-05-16 +Partially used item with expiry date 2024-05-16 (reduced from 1000000 to 999990): +panadol: 999990 in stock, expiring: 2024-05-16 +``` + +--- + +### Order Items: `order` + +Creates a new purchase or dispense order. + +**Format**: `order ORDER_TYPE ITEM_COUNT ("NOTES")` +This is followed by `ITEM_COUNT` number of lines of `ITEM_NAME (QUANTITY) (EXPIRY_DATE)` + +- `ORDER_TYPE` is either `purchase` or `dispense` +- Optional: `"NOTES"` attached to the order + +**Notes**: +- `NOTES` are optional and in quotes, and don't detect anything after the quotation. + The quotation marks recognised go by the first and last quotation marks after the + `ORDER_TYPE` and `ITEM_COUNT`. + - For example: `order purchase 4 "restock on panadol" not recognised` + - The `not recognised` portion is **ignored** by the program. + - For example: `order dispense 1 "sell some "zyrtec" to "bob" on tuesday"` + - `NOTES` will be `sell some "zyrtec" to "bob" on tuesday` +- Expiry dates are ignored for `dispense` orders. They are only valid for `purchase` + orders, since when items are dispensed from the inventory, the item with the soonest + expiry date will always be used first. + - A warning will be printed that expiry dates are ignored for dispense orders for the + first item with a specified expiry date for that order: + `Expiry dates will be ignored for dispense orders` + +**Sample Output**: + +``` +> order purchase 2 +syringe 100 +cans 10 +``` + +``` +Order placed! Listing order details +UUID: cec43f38-5c63-40b6-8964-00f8b4225c17 +Type: PURCHASE +Creation Time: 2024-11-08T00:06:30.735047100 +Fulfillment Time: null +Status: PENDING +Notes: null +Items: +1. syringe: 100 in stock +2. cans: 10 in stock +``` + +``` +> order dispense 1 "Big sale due for Thursday" +panadol 100 +``` + +``` +Order placed! Listing order details +UUID: 13e3cd21-195b-4d04-9783-e8addd42c537 +Type: DISPENSE +Creation Time: 2024-11-12T00:18:46.299344700 +Fulfillment Time: null +Status: PENDING +Notes: Big sale due for Thursday +Items: +1. panadol: 100 in stock +``` + +--- +### View All Orders `view-orders` + +Lists all orders. + +**Format**: `view-orders` + +**Sample Output**: + +`> view-orders` + +``` +1. UUID: cec43f38-5c63-40b6-8964-00f8b4225c17 +Type: PURCHASE +Creation Time: 2024-11-08T00:06:30.735047100 +Fulfillment Time: null +Status: PENDING +Notes: null +Items: +1. syringe: 100 in stock +2. cans: 10 in stock + +2. UUID: b213353f-31d3-46db-a4e7-b2ee7542d18e +Type: DISPENSE +Creation Time: 2024-11-08T00:08:21.386002300 +Fulfillment Time: null +Status: PENDING +Notes: null +Items: +1. bottle: 10 in stock +``` + +--- +### Fulfill Order: `fulfill-order` + +Completes an order by adding/removing items from the inventory. + +The action taken depends on the order type (purchase/dispense) and the items in the order. + +**Format**: `fulfill-order ORDER_UUID` + +**Sample Output**: + +`> fulfill-order cec43f38-5c63-40b6-8964-00f8b4225c17` + +``` +Added the following item to the inventory: +syringe: 100 in stock +Added the following item to the inventory: +cans: 10 in stock +``` -## Quick Start +--- +### View Transactions: `transactions` -{Give steps to get started quickly} +Displays all fulfilled orders made in the system. -1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +**Format**: `transactions` -## Features +--- +### View Transactions (within a set time period): `transaction-history` -{Give detailed description of each feature} +Displays all transactions made within a specified time period. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +**Format**: `transaction-history START_DATE END_DATE` -Format: `todo n/TODO_NAME d/DEADLINE` +### Exiting the Program: `exit` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Exits the program. -Example of usage: +**Format**: `exit` -`todo n/Write the rest of the User Guide d/next week` +--- -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +### Saving the Data -## FAQ +The system automatically saves any changes to the inventory to the hard disk after commands that modify the data (e.g., `add`, `delete`). The data is also saved upon using the `exit` command. There is no need to manually save changes. -**Q**: How do I transfer my data to another computer? -**A**: {your answer here} +--- -## Command Summary +### Editing the Data File -{Give a 'cheat sheet' of commands here} +Inventory data is stored in a `.txt` file (in `csv` format). -* Add todo `todo n/TODO_NAME d/DEADLINE` +Users can edit this file manually if necessary. diff --git a/docs/assets/pill.png b/docs/assets/pill.png new file mode 100644 index 0000000000..1bca09f080 Binary files /dev/null and b/docs/assets/pill.png differ diff --git a/docs/diagrams/AddItemCommand-SequenceDiagram.png b/docs/diagrams/AddItemCommand-SequenceDiagram.png new file mode 100644 index 0000000000..459aacdca3 Binary files /dev/null and b/docs/diagrams/AddItemCommand-SequenceDiagram.png differ diff --git a/docs/diagrams/AddItemCommand-SequenceDiagram.puml b/docs/diagrams/AddItemCommand-SequenceDiagram.puml new file mode 100644 index 0000000000..16b389a4d6 --- /dev/null +++ b/docs/diagrams/AddItemCommand-SequenceDiagram.puml @@ -0,0 +1,80 @@ +@startuml + +hide footbox +skinparam sequenceReferenceBackgroundColor #f7807c + +participant ":AddItemCommand" as AddItemCommand #E0BBE4 +participant "newItem:Item" as newItem #95E1D3 +participant ":ItemMap" as ItemMap #F9FBCB +participant "oldSet:TreeSet" as oldTreeSet #67E072 +participant "newSet:TreeSet" as newTreeSet #67E072 +participant "oldItem:Item" as oldItem #95E1D3 +participant ":Storage" as Storage #FFABAB + +-> AddItemCommand : execute(items, storage) +activate AddItemCommand + +AddItemCommand -> newItem : newItem(itemName, quantity, expiryDate) +activate newItem +return item +deactivate newItem + +AddItemCommand -> ItemMap : addItem(item) +activate ItemMap + +ItemMap -> newItem : <> +activate newItem +return <> +deactivate newItem + +alt item with same name exists + ItemMap -> ItemMap : get(name) + activate ItemMap + return itemSet + + loop until item with same expiry date \nis found or end of array is reached + opt item has same expiry date + ItemMap -> oldItem : setQuantity(newQuantity) + activate oldItem + return + deactivate oldItem + end + end + + opt item with same expiry \ndate not found + ItemMap -> oldTreeSet : add(item) + activate oldTreeSet + return + deactivate oldTreeSet + end + + +else else + ItemMap -> newTreeSet ** : TreeSet<>() + activate newTreeSet + return itemSet + deactivate newTreeSet + ItemMap -> newTreeSet : add(item) + activate newTreeSet + return + deactivate newTreeSet + ItemMap -> ItemMap : put(name, itemSet) + activate ItemMap + return + ||10|| +end + +ItemMap --> AddItemCommand +deactivate ItemMap + +AddItemCommand -> Storage : saveItem(item) +activate Storage +|||||| +return +deactivate Storage + +<-- AddItemCommand +deactivate AddItemCommand +destroy AddItemCommand + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Command-Overview-SequenceDiagram.png b/docs/diagrams/Command-Overview-SequenceDiagram.png new file mode 100644 index 0000000000..ec0d699be4 Binary files /dev/null and b/docs/diagrams/Command-Overview-SequenceDiagram.png differ diff --git a/docs/diagrams/Command-Overview-SequenceDiagram.puml b/docs/diagrams/Command-Overview-SequenceDiagram.puml new file mode 100644 index 0000000000..133ba94db4 --- /dev/null +++ b/docs/diagrams/Command-Overview-SequenceDiagram.puml @@ -0,0 +1,24 @@ +@startuml + +'hide footbox +skinparam sequenceReferenceBackgroundColor #f7807c + +hide footbox + +actor Parser +participant ":Command" as Command #E0BBE4 + +activate Parser +Parser -> Command ** : Command(itemName, quantity, expiryDate) +activate Command +return command + +Parser -> Command : execute(items, storage) +activate Command + +...Command specific execution runs... + +return +destroy Command + +@enduml \ No newline at end of file diff --git a/docs/diagrams/High-Level-Overview.png b/docs/diagrams/High-Level-Overview.png new file mode 100644 index 0000000000..7d499cdbac Binary files /dev/null and b/docs/diagrams/High-Level-Overview.png differ diff --git a/docs/diagrams/High-Level-Overview.puml b/docs/diagrams/High-Level-Overview.puml new file mode 100644 index 0000000000..4c341d1697 --- /dev/null +++ b/docs/diagrams/High-Level-Overview.puml @@ -0,0 +1,71 @@ +``` plantuml +@startuml + +hide circle +skinparam packageStyle rectangle + +left to right direction + +package "pill" { + class Pill + + package "util" { + class Ui + class Parser + class StringMatcher + class Printer + class Storage + class ItemMap + class Item + class DateTime + class PillLogger + class TransactionManager + class Order + class Transaction + class Visualizer + } + + package exceptions { + class PillException + class ExceptionMessages + } + package command { + class Command + } + + + + Pill --> Ui + Pill -> Printer + + + Ui --> Parser + Ui --> ItemMap + Ui --> Storage + ItemMap --> Item + + + Parser -> command + Parser -up> StringMatcher + Parser --> DateTime + StringMatcher -> Parser + Parser -> PillLogger + command --> exceptions + command --> TransactionManager + command --> Visualizer + command -> PillLogger + Parser --> exceptions + + ItemMap -> Storage + TransactionManager --> Storage + TransactionManager ---> Order + TransactionManager ---> Transaction + + PillException -left> ExceptionMessages + +} + + + +@enduml +``` \ No newline at end of file diff --git a/docs/diagrams/Item-ClassDiagram.png b/docs/diagrams/Item-ClassDiagram.png new file mode 100644 index 0000000000..84080f8c50 Binary files /dev/null and b/docs/diagrams/Item-ClassDiagram.png differ diff --git a/docs/diagrams/Item-ClassDiagram.puml b/docs/diagrams/Item-ClassDiagram.puml new file mode 100644 index 0000000000..600f71e53a --- /dev/null +++ b/docs/diagrams/Item-ClassDiagram.puml @@ -0,0 +1,23 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circles + +interface Comparable << interface >> +Comparable <|-[dashed]- Item + +class Item { + - name: String + - quantity: int = 1 + - expiryDate: Optional + + + getName() : String + + getQuantity() : int + + getExpiryDate() : Optional + + compareTo(other: Item) : int +} + +note right of Item : quantity defaults to 1 if not specified +note bottom of Item : Item compares to other \nItems based on expiryDate + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ItemMap-ClassDiagram.png b/docs/diagrams/ItemMap-ClassDiagram.png new file mode 100644 index 0000000000..d0562c3834 Binary files /dev/null and b/docs/diagrams/ItemMap-ClassDiagram.png differ diff --git a/docs/diagrams/ItemMap-ClassDiagram.puml b/docs/diagrams/ItemMap-ClassDiagram.puml new file mode 100644 index 0000000000..d8465acdf6 --- /dev/null +++ b/docs/diagrams/ItemMap-ClassDiagram.puml @@ -0,0 +1,27 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circles + +class ItemMap { + + addItem(newItem: Item) + + deleteItem(name: String, expiryDate: Optional) + + editItem(updatedItem: Item) + + listItems() + + listExpiringItems() + + listToRestock() + + findItem(itemName: String) : ItemMap + + getExpiringItems(itemName: String) : ItemMap +} + +ItemMap "1" *-- "1" Map : contains > + +interface Map << interface >> { + itemName: String + item: TreeSet +} + +note right of Map : Maps key(itemName) to value(item)\nHas any amount of key-value pairs +note bottom of Map : Stored as a Map, intialised as a LinkedHashMap + +@enduml \ No newline at end of file diff --git a/docs/diagrams/PillLogger.png b/docs/diagrams/PillLogger.png new file mode 100644 index 0000000000..be76dc2d7d Binary files /dev/null and b/docs/diagrams/PillLogger.png differ diff --git a/docs/diagrams/PillLogger.puml b/docs/diagrams/PillLogger.puml new file mode 100644 index 0000000000..af4f21e848 --- /dev/null +++ b/docs/diagrams/PillLogger.puml @@ -0,0 +1,26 @@ +@startuml + +package "Pharmacy Inventory & Logistics Ledger" { + [Commands] <> + [Utils] <> + [Pill] <> + [PillLogger] <> +} + +[JavaLoggingAPI] <> + +[Commands] ..|> [PillLogger] : uses getLogger() +[Utils] ..|> [PillLogger] : uses getLogger() +[Pill] ..|> [PillLogger] : uses getLogger() +[PillLogger] ..> [JavaLoggingAPI] : utilizes + +note right of PillLogger + Centralized logging utility using + singleton pattern for unified log management +end note + +note right of JavaLoggingAPI + java.util.logging package +end note + +@enduml diff --git a/docs/diagrams/TransactionManagement-ClassDiagram.png b/docs/diagrams/TransactionManagement-ClassDiagram.png new file mode 100644 index 0000000000..48f3e7def2 Binary files /dev/null and b/docs/diagrams/TransactionManagement-ClassDiagram.png differ diff --git a/docs/diagrams/TransactionManagement-ClassDiagram.puml b/docs/diagrams/TransactionManagement-ClassDiagram.puml new file mode 100644 index 0000000000..bd3ffea9d9 --- /dev/null +++ b/docs/diagrams/TransactionManagement-ClassDiagram.puml @@ -0,0 +1,32 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circles + +class TransactionManager { + - itemMap : ItemMap +} + +class Transaction { + - id: UUID + - itemName: String + - quantity: int + - type: TransactionType + - timestamp: LocalDateTime + - notes: String +} +class Order { + - id: UUID + - type: OrderType + - creationTime: LocalDateTime + - fulfillmentTime: LocalDateTime + - status: OrderStatus + - items: ItemMap + - notes: String +} + +TransactionManager -- "*" Transaction : manages > +TransactionManager - "*" Order : processes > +Transaction - "1" Order : tracks > + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Visualizer-ClassDiagram.png b/docs/diagrams/Visualizer-ClassDiagram.png new file mode 100644 index 0000000000..93fdeddb37 Binary files /dev/null and b/docs/diagrams/Visualizer-ClassDiagram.png differ diff --git a/docs/team/assets/github_cat.png b/docs/team/assets/github_cat.png new file mode 100644 index 0000000000..e853a793a7 Binary files /dev/null and b/docs/team/assets/github_cat.png differ diff --git a/docs/team/cnivedit.md b/docs/team/cnivedit.md new file mode 100644 index 0000000000..a2c2a77512 --- /dev/null +++ b/docs/team/cnivedit.md @@ -0,0 +1,109 @@ +# Chittazhi Nivedit Nandakumar - Project Portfolio Page + +## Overview + +Pharmacy Inventory & Logistics Ledger (PILL) is a desktop application that allows +pharmacists to keep track of and manage medicinal inventory. + +PILL is fully written in Java, and users can interact with the application using +Command Line Interface (CLI). + +### Summary of Contributions + +- **New Feature**: Added feature to fetch expired and expiring items + ([#113](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/113)), ([#114](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/114)). + - What it does: Filters and lists expired items and items expiring after the provided cut-off date to the user. + - Justification: Allows user to retrieve and view expired items without reading through the entire inventory. + Allows for easy tracking of items that are expiring soon. + - Highlights: Each item in the inventory item map is iterated over and each batch of the item + (entry of same item with different expiry date) is checked against the current date or the cut-off date depending + on whether the `expired` or the `expiring` command was used. +- **New Feature**: Create commands and integrate order related features including placing, viewing and fulfilling + orders, viewing transactions and querying transaction history over a specified date-range + ([#142](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/142)). + - What it does: Enables user to place, view and fulfill orders. Users may also view transactions and transaction + history over a specified date range. + - Justification: Allows for easy tracking of orders and transactions. + - Highlights: Replicates real life inventory functioning where the inventory is controlled by orders that are + placed in bulk. +- **General Contributions**: Created PillLogger class ([#103](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/103)). + - What it does: Responsible for console and file logging. + - Justification: Logging is essential to understand and track the behaviour of the application. + - Highlights: Singleton PillLogger class acts as a single entity responsible for all logging purposes. +- **New Feature**: Implemented Data Loading ([#68](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/68)). + - What it does: Loads data stored in the local CSV file. + - Justification: Enables loading of previously saved data. + - Highlights: Loads any data on disk, skips corrupt lines. +- **New Feature**: Added ListCommand class + - What it does: The class is responsible for handling the execution of the list + command ([#7](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/7)). + - Justification: Abstraction of the list command, inheriting from the base command class. + - Highlights: Receives ItemMap instance and lists items. +- **New Feature**: Added FindItemCommand class + - What it does: The class is responsible for handling the execution of the find + command ([#20](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/20)). + - Justification: Abstraction of the find command, inheriting from the base command class. + - Highlights: Receives item name to search for and executes the command. +- **Code contributed**: + [RepoSense link](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=cnivedit&breakdown=true) +- **Project Management**: + - Helped maintain some issues, opening + [#69](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/69), + [#71](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/71), + [#143](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/143), + [#145](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/145), + [#146](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/146), + [#163](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/163), + [#239](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/239) + +- **Testing**: + - ListCommand ([#49](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/49)) + - FindCommand ([#51](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/51)) + - getExpiringItems ([#113](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/113)) + - loadLine ([#68](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/68)) + +- **Documentation**: + - Developer Guide + - Added the following sections + - Orders and Transactions ([#241](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/241)) + - AddItem command sequence diagram ([#124](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/124)) + - Helped update high level overview previously created by @yijiano + ([#249](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/249)) + - Logging ([#121](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/121)) + - Target User Profile ([#115](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/115)) + - Value Proposition ([#115](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/115)) + - User Stories ([#115](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/115)) + - User Guide + - Fixed inaccurate order command format ([#224](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/224)) + - Update fulfill command usage ([#235](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/235)) + +- **Community**: + - PRs reviewed(with non-trivial comments): + [#56](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/56), + [#72](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/72), + [#106](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/106), + [#122](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/122), + [#125](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/125), + [#164](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/164), + [#168](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/168), + [#223](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/223), + [#232](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/232), + [#233](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/233), + [#240](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/240), + [#243](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/243) + +
+ +- **Extracts from DG**: + +Contributed: + + + + + +Component Diagram for PillLogger + +Updated: + +High Level Overview of PILL diff --git a/docs/team/cxc0418.md b/docs/team/cxc0418.md new file mode 100644 index 0000000000..e3404c93fb --- /dev/null +++ b/docs/team/cxc0418.md @@ -0,0 +1,47 @@ +# Chen Xinchi - Project Portfolio Page + +## Overview +Pharmacy Inventory & Logistics Ledger (PILL) is a desktop application that allows pharmacists to keep track of and manage medicinal inventory. + +PILL is fully written in Java, and users can interact with the application using Command Line Interface (CLI). + +### Summary of Contributions + +- **New Feature**: Added `AddItemCommand`, `DeleteItemCommand`, and `EditItemCommand` + - **What it does**: Implements commands to add, delete, and edit items in the inventory. Each command has enhanced parsing logic to support multi-word item names and item names ending with integers (e.g., `add iPhone 16 999 2020-12-12`). + - **Justification**: These commands are crucial for allowing pharmacists to manage inventory items efficiently, with flexible input handling that mirrors real-world scenarios. + - **Highlights**: Developing the parsing logic was complex, especially distinguishing between item names, quantities, and expiry dates. Ensured the commands handle edge cases and invalid input gracefully. + +- **New Feature**: Enhanced Cost and Price Management, added `cost` and `price` Commands + - **What it does**: Implements features for setting and updating the cost and price of items, ensuring these values are formatted. + - **Justification**: Accurate cost and price representation is essential for financial management in a pharmacy. + - **Highlights**: Added comprehensive validation for input and adjusted methods to ensure consistent formatting. Fixed related bugs to improve the overall reliability of these features. + +- **New Feature**: Implemented `Restock` and `RestockAll` Commands + - **What it does**: Automates the restocking process for items below a certain stock level, either for a specific item or all items in the inventory. + - **Justification**: Helps pharmacists efficiently manage inventory and prevent stockouts by identifying items that need restocking. + - **Highlights**: Developed logic to calculate restocking costs and handle cases where items have no cost set. Improved usability with clear user feedback. + +- **New Feature**: Developed the `Visualizer` Class for Data Visualization, added `visualize-cost`, `visualize-price`, `visualize-cost-price` and `visualize-stock` Commands + - **What it does**: Creates graphical charts using the XChart library to visualize item data, including prices, costs, and stock levels. Additionally, added a feature to compare costs and prices side-by-side. + - **Justification**: Enhances user experience by providing visual insights into inventory data, making it easier for pharmacists to understand and manage their stock efficiently. + - **Highlights**: The development involved processing item data to handle expiry dates properly and ensuring that charts were clear and intuitive. + +- **Code contributed**: [RepoSense link](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=cxc0418&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=zoom&zA=cxc0418&zR=AY2425S1-CS2113-W14-4%2Ftp%5Bmaster%5D&zACS=163.78344370860927&zS=2024-09-20&zFS=&zU=2024-11-07&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false) + +- **Project Management**: + - Helped maintain some issues, opening + [#165](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/165) + [#226](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/226) + +- **Documentation**: + - **User Guide**: + - Authored the complete v1.0 version and made significant updates for v2.0, adding sections for commands like `find`, `list`, `expiring`, `expired`, `stock-check`, `cost`, `price`, `restock`, `restock-all`, `visualize-cost`, `visualize-price`, `visualize-cost-price` and `visualize-stock`. ([#134](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/134)) + - **Developer Guide**: + - Updated some details for DG like `Visualizer`. + +- **Community**: + - PRs reviewed (with non-trivial comments): + - [#135](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/165) + - [#160](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/165) + - [#161](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/165) diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/philip1304.md b/docs/team/philip1304.md new file mode 100644 index 0000000000..8149b16ecb --- /dev/null +++ b/docs/team/philip1304.md @@ -0,0 +1,93 @@ +# Philip Chang - Project Portfolio Page + +## Overview +Pharmacy Inventory & Logistics Ledger (PILL) is a desktop application that allows pharmacists to keep track of and manage medicinal inventory. + +PILL is fully written in Java, and users can interact with the application using Command Line Interface (CLI). + +### Summary of Contributions +- **New Feature**: `HelpCommand` + - **What it does**: Implements command to enable the user to ask the program for help. + Every available command has its own help command, with optional verbose to provide the user with more detailed help information. + - **Justification**: It is incredibly crucial for the user to ask the program for help on the go, + especially if they do not have the User Guide at hand. + - **Highlights**: Making sure every command had its own simple and detailed help command was a little + mind-numbing, but it was an important feature that our program needed to have. + Writing the code so that the printed help information was consistent and looked tidy was + also a slow but important task. + +- **New Feature**: `DateTime` + - **What it does**: Allows the program to provide accurate and consistently formatted date & time information + to the user. + - **Justification**: Medicine must be stored properly with expiry dates that are properly kept track of. It is also + imperative that all actions are accurately logged and timestamped in order for a pharmacy and + its staff to help its customers as best as possible. + - **Highlights**: Implemented a robust and flexible DateTime wrapper class that works across the entire program, + providing a consistent way to work with dates and times. + +- **New Feature**: `Order` + - **What it does**: Represents an order in the inventory. An order can either be a purchase order or a dispense + order. + - **Justification**: It is a critical component of the program which allows the user to formally track and process + both incoming stock and outgoing items via orders. It provides a structured way to manage + inventory changes and ensures consistency. + - **Highlights**: Designed & implemented the Order class to support both purchasing and dispensing orders. + +- **New Feature**: `StringMatcher` + - **What it does**: Provides utility methods for string matching and comparison, which is particularly useful for + handling user input and providing helpful suggestions. + - **Justification**: The Pill application needs to be able to handle a variety of user inputs, including commands + and item names. By providing robust string matching capabilities, the application can offer + useful suggestions to the user, improving the overall user experience and reducing errors. + - **Highlights**: Implemented the Levenshtein distance algorithm to calculate the similarity between two strings, + which is used to find the closest matching command or item name when the user input doesn't + exactly match what the application expects. This helps the application provide helpful suggestions + to the user, even if they make minor typos or mistakes in their input. + +- **New Feature**: `Transaction` + - **What it does**: Represents a single transaction within the inventory management system, recording changes to the + inventory either through incoming stock (purchases) or outgoing stock (dispensing). + - **Justification**: Maintaining a complete audit trail of all inventory changes is essential for a pharmacy + management system. The Transaction class provides a structured way to record these changes, + including the item name, quantity, transaction type, timestamp, and any associated order + information. + - **Highlights**: The Transaction class includes unique identifiers, timestamps, and properties to capture the + details of each inventory transaction. It supports two main transaction types: incoming + and outgoing. Transactions can be associated with a specific order, + providing a clear link between orders and the resulting inventory changes. + +- **New Feature**: `TransactionManager` + - **What it does**: Manages all transactions and orders in the inventory management system, serving as the central + point for handling inventory movements, both incoming and outgoing + transactions, as well as managing order creation and fulfillment. + - **Justification**: The TransactionManager is a crucial component of the Pill application, as it ensures data + consistency between the actual inventory state and all related transactions and orders. By + centralizing these responsibilities, the application can maintain a complete audit trail and + provide reliable inventory management capabilities. + - **Highlights**: The TransactionManager provides methods to create new transactions, either incoming + or outgoing, and associates them with the relevant order if applicable. It also + handles the creation and fulfillment of purchase and dispense orders, updating the inventory + accordingly. The TransactionManager maintains a comprehensive transaction history and order list, + which can be retrieved and listed by the application. + +- **Code contributed**: [RepoSense link](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=philip1304&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +- **Project Management**: + - **Testing**: Wrote comprehensive unit tests for the classes I worked on (HelpCommand, DateTime, Order, + StringMatcher, Transaction, TransactionManager). + - Helped maintain some issues, working on: + [#9](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/9) + [#12](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/12) + [#16](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/16) + [#17](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/17) + [#33](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/33) + [#37](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/37) + [#59](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/59) + [#62](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/62) + [#90](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/90) + [#94](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/94) + - Helped maintain some issues, opening: + [#250](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/250) + +- **Community**: + - Reported bugs for other teams in the class. diff --git a/docs/team/yakultbottle.md b/docs/team/yakultbottle.md new file mode 100644 index 0000000000..cad88a9de6 --- /dev/null +++ b/docs/team/yakultbottle.md @@ -0,0 +1,68 @@ +# Lim Wei Ming, Benjamin - Project Portfolio Page + +## Overview +Pharmacy Inventory & Logistics Ledger (PILL) is a desktop application that allows +pharmacists to keep track of and manage medicinal inventory. + +PILL is fully written in Java, and users can interact with the application using +Command Line Interface (CLI). + +### Summary of Contributions + +- **New Feature**: Added an expiry date field for items + - What it does: Items added can have an expiry date. When removed, the item + with the earliest expiry date is automatically removed first. + - Justification: Managing expiry dates of medicinal inventory is difficult, as + different batches of medicine might have different expiry dates. + - Highlights: Not all medicinal items might have an expiry date, such as bandages. + As such, implementation was more challenging, as most commands that used Items + had to be refactored, and to organise items by expiry date, a compareTo method + had to be defined for Item. +- **New Feature**: Added Use command with priority removal for items + - What it does: Uses items in inventory with priority for items with the soonest expiry date. + - Justification: When using items, the items that are the closest to expiry should be used first, + to allow for the rest of the inventory to last as long as possible. + - Highlights: Needed to implement a TreeSet of Items, by defining its compareTo method, + such that the Items were sorted according to their expiry date, and could be + extracted from the ItemMap easily. +- **New Feature**: Added the ability to save data + - What it does: Automatically saves added items into a human-readable file. + - Justification: This feature improves the product significantly because + a user might want to refer to the stock list of items even when the product + is offline. + - Highlights: Save Data is stored in CSV format, and storage is done automatically + after any command edits the inventory list. The Save Data is able to take in commas + `,` as well, by escaping the comma character when saving and unescaping it when loading. +- **Code contributed**: [RepoSense link](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=yakultbottle&breakdown=true) +- **Project Management**: + - Helped do consistent bug-testing during the whole project + [#73](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/73), + [#74](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/74), + [#101](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/101), + [#150](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/150), + [#153](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/153), + [#154](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/154), + [#155](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/155), + [#156](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/156), + [#157](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/157), + [#166](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/166) + + +- **Documentation**: + - User Guide: + - Added documentation for `edit` feature [#126](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/126) + - Updated documentation for `OrderCommand` [#240](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/240) + - Developer Guide: + - Added implementation details of `Item` and `ItemMap`, including class diagrams for both. + Example diagram for ItemMap below + ItemMap Class Diagram + - Added implementation details of `Parser` and `Exceptions` + - Helped polish sequence diagram for `AddItemCommand`, splitting up previous diagram created + by Nivedit [#229](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/229) + - Added Glossary details and minor nits [#232](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/232) +- **Community**: + - PRs reviewed(with non-trivial comments): + [#102](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/102), + [#106](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/106), + [#139](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/139), + [#231](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/231) diff --git a/docs/team/yijiano.md b/docs/team/yijiano.md new file mode 100644 index 0000000000..aa68c41e6d --- /dev/null +++ b/docs/team/yijiano.md @@ -0,0 +1,117 @@ +# Zhang Yijian - Project Portfolio Page + +## Overview +Pharmacy Inventory & Logistics Ledger (PILL) is a desktop application that allows +pharmacists to keep track of and manage medicinal inventory. + +PILL is fully written in Java, and users can interact with the application using +Command Line Interface (CLI). + +### Summary of Contributions + +- **New Feature**: Added User Interface for the application ([#39](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/39)). + - What it does: The Ui class is responsible for handling the user interface of the application. + - Justification: Abstraction of the user interface from the main logic of the application. + - Highlights: ASCII Art, welcome message, exit message, error messages and command prompt. +- **New Feature**: Added parser for commands. ([#39](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/39)). + - What it does: Abstracts the parsing of user input into commands. + - Justification: Separation of concerns between user input and command execution. + - Highlights: Handles all exceptions and errors within the Parser class, allowing for cleaner code in the main logic. +- **New Feature**: Added `stock-check` command ([#106](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/106)). + - What it does: Queries for the items in the inventory that are below a threshold quantity determined by the user input. + - Justification: Allows for easy tracking of items that are running low in stock. + - Highlights: Allows for Ui to prompt user to restock relevant items upon launch. Allows for other commands to determine which items are running low. +- **New Feature**: Added tests for various commands ([#122](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/122)). + - What it does: Tests the functionality of the commands in the application. + - Justification: Ensures that the commands are robust and working as intended. + - Highlights: Success cases, failure cases and various edge cases and scenarios are tested to ensure that the commands are working as intended. +- **General Contributions**: Refactor ItemList to ItemMap ([#79](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/79)). + - What it does: Allows for input of duplicate item names with different expiry dates. + - Justification: Differentiates between items with the same name but different expiry dates. This is common occurrence in real pharmacies that deal with different batches of stock. + - Highlights: Allows for more accurate tracking of stock levels through using expiry dates. +- **General Contributions**: Test cases for several commands ([#122](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/122), [#233](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/233)) +- **Code contributed**: [RepoSense link](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=yijiano&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=yijiano&tabRepo=AY2425S1-CS2113-W14-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +- **Project Management**: + - Creation of the fork and pull request to the original repository: [PR Link](https://github.com/nus-cs2113-AY2425S1/tp/pull/28) + - Helped maintain some issues, opening + [#5](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/5), + [#6](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/6), + [#7](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/7), + [#8](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/8), + [#9](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/9), + [#12](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/12), + [#13](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/13), + [#14](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/14), + [#15](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/15), + [#16](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/16), + [#17](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/17), + [#18](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/18), + [#19](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/19), + [#20](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/20), + [#23](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/23), + [#24](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/24), + [#25](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/25), + [#26](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/26), + [#27](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/27), + [#28](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/28), + [#29](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/29), + [#30](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/30), + [#31](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/31), + [#32](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/32), + [#33](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/33), + [#34](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/34), + [#35](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/35), + [#36](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/36), + [#37](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/37), + [#38](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/38), + [#40](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/40), + [#41](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/41), + [#47](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/47), + [#59](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/59), + [#60](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/60), + [#61](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/61), + [#62](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/62), + [#66](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/66), + [#77](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/77), + [#78](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/78), + [#80](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/80), + [#82](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/82), + [#83](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/83), + [#84](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/84), + [#85](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/85), + [#86](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/86), + [#87](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/87), + [#88](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/88), + [#89](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/89), + [#90](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/90), + [#91](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/91), + [#92](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/92), + [#93](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/93), + [#94](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/94), + [#95](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/95), + [#107](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/107), + [#108](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/108), + [#109](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/109), + [#120](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/120), + [#138](https://github.com/AY2425S1-CS2113-W14-4/tp/issues/138) + +- **Documentation**: + - Developer Guide ([#123](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/123)): + - Added the following sections + - Contents + - Acknowledgements + - Design & Implementation + - High-Level Overview + - UI + - Glossary + - Testing Instructions +- **Community**: + - PRs reviewed(with non-trivial comments): + [#46](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/46), + [#96](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/96), + [#99](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/99), + [#100](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/100), + [#104](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/104), + [#111](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/113), + [#113](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/113), + [#126](https://github.com/AY2425S1-CS2113-W14-4/tp/pull/126) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/main/java/seedu/pill/Pill.java b/src/main/java/seedu/pill/Pill.java new file mode 100644 index 0000000000..1b31e25edd --- /dev/null +++ b/src/main/java/seedu/pill/Pill.java @@ -0,0 +1,46 @@ +package seedu.pill; + +import seedu.pill.util.ItemMap; +import seedu.pill.util.Parser; +import seedu.pill.util.Printer; +import seedu.pill.util.Storage; +import seedu.pill.util.Ui; +import seedu.pill.util.PillLogger; +import seedu.pill.util.TransactionManager; + +import seedu.pill.exceptions.PillException; + +import java.util.logging.Logger; + +public final class Pill { + private static final Storage storage = new Storage(); + private static ItemMap items = new ItemMap(); + private static final Ui ui = new Ui(items); + private static final Logger logger = PillLogger.getLogger(); + private static TransactionManager transactionManager; + private static Parser parser = new Parser(items, storage, transactionManager, ui); + + /** + * Runs the main loop of the Pill chatbot. + */ + public void run() { + items = storage.loadData(); + transactionManager = new TransactionManager(items, storage); + Printer.printInitMessage(items, 50); + parser = new Parser(items, storage, transactionManager, ui); + logger.info("New Chatbot Conversation Created"); + while (!parser.getExitFlag()) { + String line = ui.getInput(); + parser.parseCommand(line); + } + Printer.printExitMessage(); + logger.info("Chatbot Conversation Ended"); + } + + /** + * Main method to run the Pill bot. + */ + public static void main(String[] args) throws PillException{ + new Pill().run(); + } +} diff --git a/src/main/java/seedu/pill/command/AddItemCommand.java b/src/main/java/seedu/pill/command/AddItemCommand.java new file mode 100644 index 0000000000..7fc3a6961a --- /dev/null +++ b/src/main/java/seedu/pill/command/AddItemCommand.java @@ -0,0 +1,39 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.time.LocalDate; + +public class AddItemCommand extends Command { + private final String itemName; + private final int quantity; + private final LocalDate expiryDate; + + public AddItemCommand(String itemName, int quantity) { + this.itemName = itemName.toLowerCase(); + this.quantity = quantity; + this.expiryDate = null; + } + + public AddItemCommand(String itemName, int quantity, LocalDate expiryDate) { + this.itemName = itemName.toLowerCase(); + this.quantity = quantity; + this.expiryDate = expiryDate; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + Item item = new Item(itemName, quantity, expiryDate); + itemMap.addItem(item); + storage.saveItem(item); + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/Command.java b/src/main/java/seedu/pill/command/Command.java new file mode 100644 index 0000000000..ff63aae616 --- /dev/null +++ b/src/main/java/seedu/pill/command/Command.java @@ -0,0 +1,29 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +/** + * Represents a command that can be executed. + * Each command is responsible for performing a specific action in the system. + */ +public abstract class Command { + + /** + * Executes the command with the specified item list. + * + * @param itemMap The item list to be manipulated by the command. + */ + public abstract void execute(ItemMap itemMap, Storage storage) throws PillException; + + /** + * Determines whether this command will exit the application. + * Overridden only by the ByeCommand. + * + * @return false, as most commands do not exit the application. + */ + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/DeleteItemCommand.java b/src/main/java/seedu/pill/command/DeleteItemCommand.java new file mode 100644 index 0000000000..dc662b23a8 --- /dev/null +++ b/src/main/java/seedu/pill/command/DeleteItemCommand.java @@ -0,0 +1,36 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.time.LocalDate; + +public class DeleteItemCommand extends Command { + private final String itemName; + private final LocalDate expiryDate; + + public DeleteItemCommand(String itemName) { + this.itemName = itemName.toLowerCase(); + this.expiryDate = null; + } + + public DeleteItemCommand(String itemName, LocalDate expiryDate) { + this.itemName = itemName.toLowerCase(); + this.expiryDate = expiryDate; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + // Looks stupid, but this way I don't handle Optionals in this class + Item item = new Item(itemName, 0, expiryDate); + itemMap.deleteItem(item.getName(), item.getExpiryDate()); + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/EditItemCommand.java b/src/main/java/seedu/pill/command/EditItemCommand.java new file mode 100644 index 0000000000..53a019c0dd --- /dev/null +++ b/src/main/java/seedu/pill/command/EditItemCommand.java @@ -0,0 +1,38 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.time.LocalDate; + +public class EditItemCommand extends Command { + private final String itemName; + private final int newQuantity; + private final LocalDate expiryDate; + + public EditItemCommand(String itemName, int newQuantity) { + this.itemName = itemName.toLowerCase(); + this.newQuantity = newQuantity; + this.expiryDate = null; + } + + public EditItemCommand(String itemName, int newQuantity, LocalDate expiryDate) { + this.itemName = itemName.toLowerCase(); + this.newQuantity = newQuantity; + this.expiryDate = expiryDate; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + Item item = new Item(itemName, newQuantity, expiryDate); + itemMap.editItem(item); + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/ExpiredCommand.java b/src/main/java/seedu/pill/command/ExpiredCommand.java new file mode 100644 index 0000000000..7cfd5885b7 --- /dev/null +++ b/src/main/java/seedu/pill/command/ExpiredCommand.java @@ -0,0 +1,20 @@ +package seedu.pill.command; + +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.time.LocalDate; + +public class ExpiredCommand extends Command { + public ExpiredCommand() {} + + @Override + public void execute(ItemMap itemMap, Storage storage) { + itemMap.listExpiringItems(LocalDate.now()); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/ExpiringCommand.java b/src/main/java/seedu/pill/command/ExpiringCommand.java new file mode 100644 index 0000000000..1e09ac53d6 --- /dev/null +++ b/src/main/java/seedu/pill/command/ExpiringCommand.java @@ -0,0 +1,24 @@ +package seedu.pill.command; + +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.time.LocalDate; + +public class ExpiringCommand extends Command { + private LocalDate cutOffDate; + + public ExpiringCommand(LocalDate cutOffDate) { + this.cutOffDate = cutOffDate; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) { + itemMap.listExpiringItems(cutOffDate); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/FindCommand.java b/src/main/java/seedu/pill/command/FindCommand.java new file mode 100644 index 0000000000..c996051efa --- /dev/null +++ b/src/main/java/seedu/pill/command/FindCommand.java @@ -0,0 +1,31 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + + +public class FindCommand extends Command { + private final String itemName; + + public FindCommand(String itemName) { + this.itemName = itemName.toLowerCase(); + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + ItemMap foundItems = itemMap.findItem(itemName); + if (foundItems.isEmpty()) { + throw new PillException(ExceptionMessages.ITEM_NOT_FOUND_ERROR); + } else { + ListCommand listCommand = new ListCommand(); + listCommand.execute(foundItems, storage); + } + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/FulfillCommand.java b/src/main/java/seedu/pill/command/FulfillCommand.java new file mode 100644 index 0000000000..9b8ae1e23e --- /dev/null +++ b/src/main/java/seedu/pill/command/FulfillCommand.java @@ -0,0 +1,22 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Order; +import seedu.pill.util.Storage; +import seedu.pill.util.TransactionManager; + +public class FulfillCommand extends Command { + private Order order; + private TransactionManager transactionManager; + + public FulfillCommand(Order order, TransactionManager transactionManager) { + this.order = order; + this.transactionManager = transactionManager; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + transactionManager.fulfillOrder(order); + } +} diff --git a/src/main/java/seedu/pill/command/HelpCommand.java b/src/main/java/seedu/pill/command/HelpCommand.java new file mode 100644 index 0000000000..a074dabc21 --- /dev/null +++ b/src/main/java/seedu/pill/command/HelpCommand.java @@ -0,0 +1,638 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.PillLogger; +import seedu.pill.util.Storage; +import seedu.pill.util.StringMatcher; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +/** + * Represents a command that displays help information about available commands in the Pill application. + * This command can show both general help information for all commands and detailed help for specific commands. + * It supports a verbose mode that provides additional examples and detailed usage information. + */ +public class HelpCommand extends Command { + private static final Logger logger = PillLogger.getLogger(); + private static final List VALID_COMMANDS = Arrays.asList( + "exit", "add", "delete", "edit", "find", "help", "list", + "stock-check", "expired", "expiring", "cost", "price", + "restock-all", "restock", "use", "order", "view-orders", + "fulfill-order", "transactions", "transaction-history", + "visualize-price", "visualize-cost", "visualize-stock", "visualize-cost-price" + ); + + private final String commandName; + private final boolean verbose; + + /** + * Constructs a new HelpCommand with the specified command input and verbosity setting. + * + * @param commandInput - The name of the command to get help for, or null for general help. + * If the input contains spaces, only the first word is considered as the command. + * @param verbose - Whether to display detailed help information with examples (true) or basic help (false). + */ + public HelpCommand(String commandInput, boolean verbose) { + if (commandInput != null) { + String[] parts = commandInput.split("\\s+", 2); + this.commandName = parts.length > 0 ? parts[0].toLowerCase() : null; + } else { + this.commandName = null; + } + this.verbose = verbose; + logger.info("Created HelpCommand for command: " + commandName + " with verbose mode: " + verbose); + } + + /** + * Executes the help command by displaying appropriate help information. + * If no specific command is specified, shows general help for all commands. + * If a specific command is specified, shows detailed help for that command. + * In verbose mode, includes additional examples and detailed usage information. + * + * @param itemMap - The current inventory of items (not used by this command but required by interface). + * @param storage - The storage manager (not used by this command but required by interface). + * @throws PillException - If there is an error executing the help command. + */ + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + assert itemMap != null : "ItemList cannot be null"; + assert storage != null : "Storage cannot be null"; + logger.info("Executing HelpCommand"); + + if (commandName == null || commandName.isEmpty()) { + showGeneralHelp(); + } else { + showSpecificHelp(commandName); + } + } + + /** + * Displays general help information for all available commands. + * Lists each command with a brief description of its function. + */ + private void showGeneralHelp() { + logger.info("Showing general help information"); + + System.out.println("Available commands:"); + + System.out.println("\nItem Management:"); + System.out.println(" add - Adds a new item to the list"); + System.out.println(" delete - Deletes an item from the list"); + System.out.println(" edit - Edits an item in the list"); + System.out.println(" find - Finds all items with the same keyword"); + System.out.println(" expired - Lists all items that have expired"); + System.out.println(" expiring - Lists items expiring before a specified date"); + System.out.println(" list - Lists all items"); + System.out.println(" stock-check - Lists all items that need to be restocked"); + System.out.println(" restock - Restocks a specified item " + + "with an optional expiry date and quantity"); + System.out.println(" restock-all - Restocks all items below a specified threshold"); + System.out.println(" use - Priority removal of items from the list, " + + "starting with earliest expiry date"); + + System.out.println("\nVisualization:"); + System.out.println(" visualize-price - Visualizes item prices as a bar chart"); + System.out.println(" visualize-cost - Visualizes item costs as a bar chart"); + System.out.println(" visualize-stock - Visualizes item stocks as a bar chart"); + System.out.println(" visualize-cost-price - Visualizes item costs and prices side-by-side as a bar chart"); + + System.out.println("\nPrice and Cost Management:"); + System.out.println(" cost - Sets the cost for a specified item"); + System.out.println(" price - Sets the selling price for a specified item"); + + System.out.println("\nOrder Management:"); + System.out.println(" order - Creates a new purchase or dispense order"); + System.out.println(" fulfill-order - Processes and completes a pending order"); + System.out.println(" view-orders - Lists all orders"); + + System.out.println("\nTransaction Management:"); + System.out.println(" transactions - Views all transactions"); + System.out.println(" transaction-history - Views transaction history in a given time period"); + + System.out.println("\nOther Commands:"); + System.out.println(" help - Shows this help message"); + System.out.println(" exit - Exits the program"); + + System.out.println("\nType 'help ' for more information on a specific command."); + System.out.println("Type 'help -v' for verbose output with examples."); + } + + /** + * Displays help information for a specific command. + * If the command is not recognized, attempts to find and suggest similar commands. + * + * @param command - The name of the command to show help for. + */ + private void showSpecificHelp(String command) { + assert command != null : "Command cannot be null"; + logger.info("Showing specific help for command: " + command); + + switch (command.toLowerCase()) { + // Item Management + case "add": + showAddHelp(); + break; + case "delete": + showDeleteHelp(); + break; + case "edit": + showEditHelp(); + break; + case "find": + showFindHelp(); + break; + case "expired": + showExpiredHelp(); + break; + case "expiring": + showExpiringHelp(); + break; + case "list": + showListHelp(); + break; + case "stock-check": + showStockCheckHelp(); + break; + case "restock": + showRestockHelp(); + break; + case "restock-all": + showRestockAllHelp(); + break; + case "use": + showUseHelp(); + break; + // Visualization + case "visualize-price": + showVisualizePriceHelp(); + break; + case "visualize-cost": + showVisualizeCostHelp(); + break; + case "visualize-stock": + showVisualizeStockHelp(); + break; + case "visualize-cost-price": + showVisualizeCostPriceHelp(); + break; + // Price and Cost Management + case "cost": + showCostHelp(); + break; + case "price": + showPriceHelp(); + break; + // Order Management + case "order": + showOrderHelp(); + break; + case "fulfill-order": + showFulfillOrderHelp(); + break; + case "view-orders": + showViewOrdersHelp(); + break; + // Transaction Management + case "transactions": + showTransactionsHelp(); + break; + case "transaction-history": + showTransactionHistoryHelp(); + break; + // Other Commands + case "help": + showHelpHelp(); + break; + case "exit": + showExitHelp(); + break; + + default: + String closestMatch = StringMatcher.findClosestMatch(command, VALID_COMMANDS); + if (closestMatch != null) { + System.out.println("Did you mean: " + closestMatch + "?"); + showSpecificHelp(closestMatch); + } else { + suggestSimilarCommand(command); + } + } + } + + /** + * Suggests similar commands when an unknown command is entered. + * Uses string matching to find the closest matching valid command. + * + * @param command - The unknown command entered by the user. + */ + private void suggestSimilarCommand(String command) { + logger.info("Suggesting similar command for: " + command); + + System.out.println("Unknown command: " + command); + String closestMatch = StringMatcher.findClosestMatch(command, VALID_COMMANDS); + if (closestMatch != null) { + System.out.println("Did you mean: " + closestMatch + "?"); + System.out.println("Type 'help " + closestMatch + "' for more information on this command."); + } else { + System.out.println("No similar command found."); + } + + System.out.println("\nAvailable commands: " + String.join(", ", VALID_COMMANDS)); + System.out.println("Type 'help ' for more information on a specific command."); + } + + /** + * Prints detailed information about the 'visualize-price' command. + */ + private void showVisualizePriceHelp() { + System.out.println("visualize-price: Displays a bar chart of item prices."); + if (verbose) { + System.out.println("Usage: visualize-price"); + System.out.println("\nExample:"); + System.out.println(" visualize-price"); + System.out.println(" This will display a chart of item prices."); + } + } + + /** + * Prints detailed information about the 'visualize-cost' command. + */ + private void showVisualizeCostHelp() { + System.out.println("visualize-cost: Displays a bar chart of item costs."); + if (verbose) { + System.out.println("Usage: visualize-cost"); + System.out.println("\nExample:"); + System.out.println(" visualize-cost"); + System.out.println(" This will display a chart of item costs."); + } + } + + /** + * Prints detailed information about the 'visualize-stock' command. + */ + private void showVisualizeStockHelp() { + System.out.println("visualize-stock: Displays a bar chart of item stocks."); + if (verbose) { + System.out.println("Usage: visualize-stock"); + System.out.println("\nExample:"); + System.out.println(" visualize-stock"); + System.out.println(" This will display a chart of item stocks."); + } + } + + /** + * Prints detailed information about the 'visualize-cost-price' command. + */ + private void showVisualizeCostPriceHelp() { + System.out.println("visualize-cost-price: Displays a bar chart comparing item costs and prices side-by-side."); + if (verbose) { + System.out.println("Usage: visualize-cost-price"); + System.out.println("\nExample:"); + System.out.println(" visualize-cost-price"); + System.out.println(" This will display a chart comparing item costs and prices for each item."); + } + } + + /** + * Prints detailed information about the 'use' command. + */ + private void showUseHelp() { + logger.fine("Showing help information for 'use' command"); + + System.out.println("use: Priority removal of items from the list, " + + "starting with the earliest expiry date."); + if (verbose) { + System.out.println("Usage: use "); + System.out.println(" - Name of the item"); + System.out.println("\nExample:"); + System.out.println(" use Aspirin"); + } + System.out.println("\nCorrect input format: use "); + } + + /** + * Prints detailed information about the 'find' command. + */ + private void showFindHelp() { + logger.fine("Showing help information for 'find' command"); + + System.out.println("find: Finds all items with the same keyword."); + if (verbose) { + System.out.println("Usage: find "); + System.out.println(" - Keyword to search for in item names"); + System.out.println("\nExample:"); + System.out.println(" find Aspirin"); + } + System.out.println("\nCorrect input format: find "); + } + + /** + * Prints detailed information about the 'restock' command. + */ + private void showRestockHelp() { + System.out.println("restock: Restocks a specified item with an optional expiry date and quantity."); + if (verbose) { + System.out.println("Usage: restock [expiry-date] "); + System.out.println(" - The name of the item to restock."); + System.out.println(" [expiry-date] - Optional. The expiry date of the item in yyyy-MM-dd format."); + System.out.println(" [quantity] - Optional. The quantity to restock up to. Defaults to 50."); + System.out.println("\nExamples:"); + System.out.println(" restock apple 100"); + System.out.println(" restock orange 2025-12-12 50"); + } + } + + /** + * Prints detailed information about the 'restockall' command. + */ + private void showRestockAllHelp() { + System.out.println("restock-all: Restocks all items below a specified threshold."); + if (verbose) { + System.out.println("Usage: restockall [threshold]"); + System.out.println(" [threshold] - Optional. The minimum quantity for restocking. Defaults to 50."); + System.out.println("\nExample:"); + System.out.println(" restockall 100"); + } + } + + /** + * Prints detailed information about the 'cost' command. + */ + private void showCostHelp() { + System.out.println("cost: Sets the cost for a specified item."); + if (verbose) { + System.out.println("Usage: cost "); + System.out.println(" - The name of the item."); + System.out.println(" - The cost value to set for the item."); + System.out.println("\nExample:"); + System.out.println(" cost apple 20.0"); + } + } + + /** + * Prints detailed information about the 'price' command. + */ + private void showPriceHelp() { + System.out.println("price: Sets the selling price for a specified item."); + if (verbose) { + System.out.println("Usage: price "); + System.out.println(" - The name of the item."); + System.out.println(" - The price value to set for the item."); + System.out.println("\nExample:"); + System.out.println(" price apple 30.0"); + } + } + + + /** + * Prints detailed information about the 'help' command. + */ + private void showHelpHelp() { + logger.fine("Showing help information for 'help' command"); + + System.out.println("help: Shows help information about available commands."); + if (verbose) { + System.out.println("Usage: help [command] [-v]"); + System.out.println(" [command] - Optional. Specify a command to get detailed help."); + System.out.println(" [-v] - Optional. Show verbose output with examples."); + System.out.println("\nExamples:"); + System.out.println(" help"); + System.out.println(" help add"); + System.out.println(" help add -v"); + } + } + + /** + * Prints detailed information about the 'add' command. + */ + private void showAddHelp() { + logger.fine("Showing help information for 'add' command"); + + System.out.println("add: Adds a new item to the inventory."); + if (verbose) { + System.out.println("Usage: add "); + System.out.println(" - Name of the item"); + System.out.println(" [quantity] - Optional: Initial quantity of the item (default 1)"); + System.out.println(" [expiry] - Optional: Expiry date of the item in yyyy-MM-dd format"); + System.out.println("\nExample:"); + System.out.println(" add Aspirin 100 2024-05-24"); + } + System.out.println("\nCorrect input format: add [quantity] [expiry]"); + } + + /** + * Prints detailed information about the 'delete' command. + */ + private void showDeleteHelp() { + logger.fine("Showing help information for 'delete' command"); + + System.out.println("delete: Removes an item from the inventory."); + if (verbose) { + System.out.println("Usage: delete "); + System.out.println(" - Name of the item to delete (as shown in the list)"); + System.out.println(" - Expiry date of the item in yyyy/MM/dd format."); + System.out.println("\nExample:"); + System.out.println(" delete Aspirin 2024-05-24"); + } + System.out.println("\nCorrect input format: delete "); + } + + /** + * Prints detailed information about the 'edit' command. + */ + private void showEditHelp() { + logger.fine("Showing help information for 'edit' command"); + + System.out.println("edit: Edits the item in the inventory."); + if (verbose) { + System.out.println("Usage: edit "); + System.out.println(" - Name of the item to edit (as shown in the list)"); + System.out.println(" - New quantity of the item"); + System.out.println(" - Expiry date of the item in yyyy-MM-dd format"); + System.out.println("\nExample:"); + System.out.println(" edit Aspirin 100 2024-05-24"); + } + System.out.println("\nCorrect input format: edit "); + } + + /** + * Prints detailed information about the 'expired' command. + */ + private void showExpiredHelp() { + logger.fine("Showing help information for 'expired' command"); + + System.out.println("expired: Lists all items that have expired as of today."); + if (verbose) { + System.out.println("Usage: expired"); + System.out.println(" Shows all items with expiry dates before today's date"); + System.out.println("\nExample:"); + System.out.println(" expired"); + System.out.println(" This will show all items that have passed their expiry date"); + } + } + + /** + * Prints detailed information about the 'expiring' command. + */ + private void showExpiringHelp() { + logger.fine("Showing help information for 'expiring' command"); + + System.out.println("expiring: Lists all items that will expire before a specified date."); + if (verbose) { + System.out.println("Usage: expiring "); + System.out.println(" - The cutoff date in yyyy-MM-dd format"); + System.out.println(" Shows all items with expiry dates before the specified date"); + System.out.println("\nExample:"); + System.out.println(" expiring 2024-12-31"); + System.out.println(" This will show all items expiring before December 31, 2024"); + } + System.out.println("\nCorrect input format: expiring yyyy-MM-dd"); + } + + /** + * Prints detailed information about the 'list' command. + */ + private void showListHelp() { + logger.fine("Showing help information for 'list' command"); + + System.out.println("list: Displays all items in the inventory."); + if (verbose) { + System.out.println("Usage: list"); + System.out.println("\nExample:"); + System.out.println(" list"); + } + } + + /** + * Prints detailed information about the 'order' command. + */ + private void showOrderHelp() { + logger.fine("Showing help information for 'order' command"); + + System.out.println("order: Creates a new purchase or dispense order."); + if (verbose) { + System.out.println("Usage:"); + System.out.println("order "); + System.out.println(" "); + System.out.println("[item2 quantity2]"); + System.out.println("..."); + System.out.println("[-n \"notes\"]"); + System.out.println(); + System.out.println(" - Type of order: 'purchase' or 'dispense'"); + System.out.println(" - Name of item to order"); + System.out.println(" - Quantity of the item"); + System.out.println(" -n - Optional notes about the order"); + System.out.println("\nExamples:"); + System.out.println(" order purchase 2"); + System.out.println(" Aspirin 100"); + System.out.println(" Bandages 50"); + System.out.println(" -n \"Monthly stock replenishment\""); + System.out.println(); + System.out.println(" order dispense 1"); + System.out.println(" Paracetamol 20"); + System.out.println(" -n \"Emergency room request\""); + } + } + + /** + * Prints detailed information about the 'fulfill-order' command. + */ + private void showFulfillOrderHelp() { + logger.fine("Showing help information for 'fulfill-order' command"); + + System.out.println("fulfill-order: Processes and completes a pending order."); + if (verbose) { + System.out.println("Usage: fulfill-order "); + System.out.println(" - The unique identifier of the order to fulfill"); + System.out.println("\nExample:"); + System.out.println(" fulfill-order 123e4567-e89b-12d3-a456-556642440000"); + System.out.println("\nNote: This will create the necessary transactions and update inventory levels"); + } + } + + /** + * Prints detailed information about the 'list-orders' command. + */ + private void showViewOrdersHelp() { + logger.fine("Showing help information for 'view-orders' command"); + + System.out.println("view-orders: Lists all orders."); + if (verbose) { + System.out.println("Usage: view-orders"); + System.out.println("\nExamples:"); + System.out.println(" view-orders"); + } + } + + /** + * Prints detailed information about the 'transactions' command. + */ + private void showTransactionsHelp() { + logger.fine("Showing help information for 'transactions' command"); + + System.out.println("transactions: Views all transactions."); + if (verbose) { + System.out.println("Usage: transactions"); + System.out.println("\nExamples:"); + System.out.println(" transactions"); + } + } + + /** + * Prints detailed information about the 'transaction-history' command. + */ + private void showTransactionHistoryHelp() { + logger.fine("Showing help information for 'transaction-history' command"); + + System.out.println("transaction-history: Views transaction history in a given time period"); + if (verbose) { + System.out.println("Usage: transaction-history "); + System.out.println(" - Show transactions from this date (yyyy-MM-dd)"); + System.out.println(" - Show transactions until this date (yyyy-MM-dd)"); + System.out.println("\nExamples:"); + System.out.println(" transaction-history 2024-01-01 2024-12-31"); + } + } + + /** + * Prints detailed information about the 'stock-check' command. + */ + private void showStockCheckHelp() { + logger.fine("Showing help information for 'stock-check' command"); + + System.out.println("stock-check: Displays all items in the inventory that need to be restocked."); + if (verbose) { + System.out.println("Usage: stock-check "); + System.out.println(" - Items with strictly less than this quantity will be printed."); + System.out.println("\nExample:"); + System.out.println(" stock-check 100"); + } + System.out.println("\nCorrect input format: stock-check "); + } + + /** + * Prints detailed information about the 'exit' command. + */ + private void showExitHelp() { + logger.fine("Showing help information for 'exit' command"); + + System.out.println("exit: Exits the program."); + if (verbose) { + System.out.println("Usage: exit"); + System.out.println("\nExample:"); + System.out.println(" exit"); + } + } + + /** + * Determines whether this command will exit the application. + * + * @return - false as the help command does not exit the application. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/ListCommand.java b/src/main/java/seedu/pill/command/ListCommand.java new file mode 100644 index 0000000000..3f6a1f80bc --- /dev/null +++ b/src/main/java/seedu/pill/command/ListCommand.java @@ -0,0 +1,18 @@ +package seedu.pill.command; + +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +public class ListCommand extends Command{ + public ListCommand() {} + + @Override + public void execute(ItemMap itemMap, Storage storage) { + itemMap.listItems(); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/OrderCommand.java b/src/main/java/seedu/pill/command/OrderCommand.java new file mode 100644 index 0000000000..6bc506fed0 --- /dev/null +++ b/src/main/java/seedu/pill/command/OrderCommand.java @@ -0,0 +1,28 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.TransactionManager; +import seedu.pill.util.Order; +import seedu.pill.util.Storage; + + +public class OrderCommand extends Command{ + private TransactionManager transactionManager; + private ItemMap itemsToOrder; + private Order.OrderType orderType; + private String notes; + + public OrderCommand(ItemMap itemsToOrder, TransactionManager transactionManager, + Order.OrderType orderType, String notes) { + this.itemsToOrder = itemsToOrder; + this.transactionManager = transactionManager; + this.orderType = orderType; + this.notes = notes; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + transactionManager.createOrder(orderType, itemsToOrder, notes); + } +} diff --git a/src/main/java/seedu/pill/command/RestockAllCommand.java b/src/main/java/seedu/pill/command/RestockAllCommand.java new file mode 100644 index 0000000000..817339f94e --- /dev/null +++ b/src/main/java/seedu/pill/command/RestockAllCommand.java @@ -0,0 +1,49 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.util.ArrayList; +import java.util.List; + +/** + * Command to restock all items below a specified stock level. + */ +public class RestockAllCommand extends Command { + private final int threshold; + + public RestockAllCommand(int threshold) { + this.threshold = threshold; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + List itemsToRestock = new ArrayList<>(); + double totalRestockCost = 0; + + for (Item item : itemMap.getAllItems()) { + if (item.getQuantity() < threshold) { + int currentStock = item.getQuantity(); + int restockAmount = threshold - currentStock; + double itemRestockCost = item.getCost() * restockAmount; + + totalRestockCost += itemRestockCost; + item.setQuantity(threshold); + + itemsToRestock.add(item); + System.out.printf("Item: %s, Current Stock: %d, New Stock: %d, Restock Cost: $%.2f%n", + item.getName(), currentStock, threshold, itemRestockCost); + } + } + + System.out.printf("Total Restock Cost for all items below threshold %d: $%.2f%n", threshold, totalRestockCost); + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/RestockItemCommand.java b/src/main/java/seedu/pill/command/RestockItemCommand.java new file mode 100644 index 0000000000..d8ea98847c --- /dev/null +++ b/src/main/java/seedu/pill/command/RestockItemCommand.java @@ -0,0 +1,57 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +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 java.util.Optional; + +/** + * Command to restock a specific item. + */ +public class RestockItemCommand extends Command { + private final String itemName; + private final Optional expiryDate; + private final int quantity; + + public RestockItemCommand(String itemName, Optional expiryDate, int quantity) { + this.itemName = itemName; + this.expiryDate = expiryDate; + this.quantity = quantity; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + Item itemToRestock = itemMap.getItemByNameAndExpiry(itemName, expiryDate); + + if (itemToRestock == null) { + throw new PillException(ExceptionMessages.ITEM_NOT_FOUND_ERROR); + } + + int currentStock = itemToRestock.getQuantity(); + + if (currentStock >= quantity) { + System.out.printf("Item %s already has sufficient stock: %d (No restock needed)%n", + itemName, currentStock); + return; + } + + int restockAmount = quantity - currentStock; + double restockCost = itemToRestock.getCost() * restockAmount; + + itemToRestock.setQuantity(quantity); + + System.out.printf("Restocked Item: %s, Current Stock: %d, New Stock: %d, Total Restock Cost: $%.2f%n", + itemName, currentStock, itemToRestock.getQuantity(), restockCost); + + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/SetCostCommand.java b/src/main/java/seedu/pill/command/SetCostCommand.java new file mode 100644 index 0000000000..b04af38387 --- /dev/null +++ b/src/main/java/seedu/pill/command/SetCostCommand.java @@ -0,0 +1,49 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.text.DecimalFormat; + +/** + * Command to set the cost of all items with a specified name. + */ +public class SetCostCommand extends Command { + private static final DecimalFormat decimalFormat = new DecimalFormat("#0.00"); + private final String itemName; + private final double cost; + + public SetCostCommand(String itemName, double cost) { + this.itemName = itemName.toLowerCase(); + this.cost = cost; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + boolean itemFound = false; + boolean msgIsPrinted = false; + + for (Item item : itemMap.getItemsByName(itemName)) { + item.setCost(cost); + if (!msgIsPrinted) { + System.out.println("Set cost of " + itemName + " to $" + decimalFormat.format(cost) + "."); + msgIsPrinted = true; + } + itemFound = true; + } + + if (!itemFound) { + throw new PillException(ExceptionMessages.ITEM_NOT_FOUND_ERROR); + } + + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/SetPriceCommand.java b/src/main/java/seedu/pill/command/SetPriceCommand.java new file mode 100644 index 0000000000..5bdc39b4a0 --- /dev/null +++ b/src/main/java/seedu/pill/command/SetPriceCommand.java @@ -0,0 +1,49 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.Item; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +import java.text.DecimalFormat; + +/** + * Command to set the price of all items with a specified name. + */ +public class SetPriceCommand extends Command { + private static final DecimalFormat decimalFormat = new DecimalFormat("#0.00"); + private final String itemName; + private final double price; + + public SetPriceCommand(String itemName, double price) { + this.itemName = itemName.toLowerCase(); + this.price = price; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + boolean itemFound = false; + boolean msgIsPrinted = false; + + for (Item item : itemMap.getItemsByName(itemName)) { + item.setPrice(price); + if (!msgIsPrinted) { + System.out.println("Set price of " + itemName + " to $" + decimalFormat.format(price) + "."); + msgIsPrinted = true; + } + itemFound = true; + } + + if (!itemFound) { + throw new PillException(ExceptionMessages.ITEM_NOT_FOUND_ERROR); + } + + storage.saveItemMap(itemMap); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/StockCheckCommand.java b/src/main/java/seedu/pill/command/StockCheckCommand.java new file mode 100644 index 0000000000..950579f929 --- /dev/null +++ b/src/main/java/seedu/pill/command/StockCheckCommand.java @@ -0,0 +1,22 @@ +package seedu.pill.command; + +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +public class StockCheckCommand extends Command{ + private final int threshold; + + public StockCheckCommand(String threshold) { + this.threshold = Integer.parseInt(threshold); + } + + @Override + public void execute(ItemMap itemMap, Storage storage) { + itemMap.listItemsToRestock(this.threshold); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/TransactionHistoryCommand.java b/src/main/java/seedu/pill/command/TransactionHistoryCommand.java new file mode 100644 index 0000000000..d46af52d4f --- /dev/null +++ b/src/main/java/seedu/pill/command/TransactionHistoryCommand.java @@ -0,0 +1,24 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.TransactionManager; + +import java.time.LocalDate; + +public class TransactionHistoryCommand extends Command { + private TransactionManager transactionManager; + private LocalDate start; + private LocalDate end; + + public TransactionHistoryCommand(LocalDate start, LocalDate end, TransactionManager transactionManager) { + this.start = start; + this.end = end; + this.transactionManager = transactionManager; + } + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + transactionManager.listTransactionHistory(start, end); + } +} diff --git a/src/main/java/seedu/pill/command/TransactionsCommand.java b/src/main/java/seedu/pill/command/TransactionsCommand.java new file mode 100644 index 0000000000..7f0eafa61b --- /dev/null +++ b/src/main/java/seedu/pill/command/TransactionsCommand.java @@ -0,0 +1,19 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.TransactionManager; + +public class TransactionsCommand extends Command{ + private TransactionManager transactionManager; + + public TransactionsCommand(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + transactionManager.listTransactions(); + } +} diff --git a/src/main/java/seedu/pill/command/UseItemCommand.java b/src/main/java/seedu/pill/command/UseItemCommand.java new file mode 100644 index 0000000000..0c6787d1c9 --- /dev/null +++ b/src/main/java/seedu/pill/command/UseItemCommand.java @@ -0,0 +1,49 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; + +/** + * A command that uses (consumes) a specified quantity of items from the inventory. + * This command updates both the in-memory item map and persists changes to storage. + */ +public class UseItemCommand extends Command { + private final String itemName; + private final int quantityToUse; + + /** + * Creates a new command to use (consume) items from inventory. + * + * @param itemName the name of the item to use + * @param quantityToUse the quantity of the item to consume + */ + public UseItemCommand(String itemName, int quantityToUse) { + this.itemName = itemName.toLowerCase(); + this.quantityToUse = quantityToUse; + } + + /** + * Executes the use item command by consuming the specified quantity of items + * and saving the updated inventory to storage. + * + * @param itemMap the inventory to update + * @param storage the storage system to save changes to + * @throws PillException if the operation fails due to insufficient stock or invalid item name + */ + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + itemMap.useItem(this.itemName, this.quantityToUse); + storage.saveItemMap(itemMap); + } + + /** + * Indicates whether this command should terminate the program. + * + * @return false as this command does not exit the program + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/seedu/pill/command/ViewOrdersCommand.java b/src/main/java/seedu/pill/command/ViewOrdersCommand.java new file mode 100644 index 0000000000..6f5cd37b20 --- /dev/null +++ b/src/main/java/seedu/pill/command/ViewOrdersCommand.java @@ -0,0 +1,19 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.TransactionManager; + +public class ViewOrdersCommand extends Command { + private TransactionManager transactionManager; + + public ViewOrdersCommand(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void execute(ItemMap itemMap, Storage storage) throws PillException { + transactionManager.listOrders(); + } +} diff --git a/src/main/java/seedu/pill/command/VisualizeCostCommand.java b/src/main/java/seedu/pill/command/VisualizeCostCommand.java new file mode 100644 index 0000000000..1b46682b49 --- /dev/null +++ b/src/main/java/seedu/pill/command/VisualizeCostCommand.java @@ -0,0 +1,29 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.Visualizer; + +/** + * Handles the visualization of item costs. + */ +public class VisualizeCostCommand extends Command { + private final Visualizer visualizer; + + public VisualizeCostCommand(Visualizer visualizer) { + this.visualizer = visualizer; + } + + @Override + public void execute(ItemMap items, Storage storage) throws PillException { + visualizer.setItems(items.getItemsAsArrayList()); + + try{ + visualizer.drawCostChart(); + } catch (Exception e) { + throw new PillException(ExceptionMessages.NOTHING_TO_VISUALIZE); + } + } +} diff --git a/src/main/java/seedu/pill/command/VisualizeCostPriceCommand.java b/src/main/java/seedu/pill/command/VisualizeCostPriceCommand.java new file mode 100644 index 0000000000..ed625eb178 --- /dev/null +++ b/src/main/java/seedu/pill/command/VisualizeCostPriceCommand.java @@ -0,0 +1,29 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.Visualizer; + +/** + * Handles the visualization of item costs and prices. + */ +public class VisualizeCostPriceCommand extends Command { + private final Visualizer visualizer; + + public VisualizeCostPriceCommand(Visualizer visualizer) { + this.visualizer = visualizer; + } + + @Override + public void execute(ItemMap items, Storage storage) throws PillException { + visualizer.setItems(items.getItemsAsArrayList()); + + try{ + visualizer.drawCostPriceChart(); + } catch (Exception e) { + throw new PillException(ExceptionMessages.NOTHING_TO_VISUALIZE); + } + } +} diff --git a/src/main/java/seedu/pill/command/VisualizePriceCommand.java b/src/main/java/seedu/pill/command/VisualizePriceCommand.java new file mode 100644 index 0000000000..88fca4c5d4 --- /dev/null +++ b/src/main/java/seedu/pill/command/VisualizePriceCommand.java @@ -0,0 +1,29 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.Visualizer; + +/** + * Handles the visualization of item prices. + */ +public class VisualizePriceCommand extends Command { + private final Visualizer visualizer; + + public VisualizePriceCommand(Visualizer visualizer) { + this.visualizer = visualizer; + } + + @Override + public void execute(ItemMap items, Storage storage) throws PillException { + visualizer.setItems(items.getItemsAsArrayList()); + + try{ + visualizer.drawPriceChart(); + } catch (Exception e) { + throw new PillException(ExceptionMessages.NOTHING_TO_VISUALIZE); + } + } +} diff --git a/src/main/java/seedu/pill/command/VisualizeStockCommand.java b/src/main/java/seedu/pill/command/VisualizeStockCommand.java new file mode 100644 index 0000000000..cc2565571b --- /dev/null +++ b/src/main/java/seedu/pill/command/VisualizeStockCommand.java @@ -0,0 +1,29 @@ +package seedu.pill.command; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; +import seedu.pill.util.ItemMap; +import seedu.pill.util.Storage; +import seedu.pill.util.Visualizer; + +/** + * Handles the visualization of item stocks. + */ +public class VisualizeStockCommand extends Command { + private final Visualizer visualizer; + + public VisualizeStockCommand(Visualizer visualizer) { + this.visualizer = visualizer; + } + + @Override + public void execute(ItemMap items, Storage storage) throws PillException { + visualizer.setItems(items.getItemsAsArrayList()); + + try{ + visualizer.drawStockChart(); + } catch (Exception e) { + throw new PillException(ExceptionMessages.NOTHING_TO_VISUALIZE); + } + } +} diff --git a/src/main/java/seedu/pill/exceptions/ExceptionMessages.java b/src/main/java/seedu/pill/exceptions/ExceptionMessages.java new file mode 100644 index 0000000000..10128578aa --- /dev/null +++ b/src/main/java/seedu/pill/exceptions/ExceptionMessages.java @@ -0,0 +1,57 @@ +package seedu.pill.exceptions; + +public enum ExceptionMessages { + INVALID_COMMAND ("Invalid command, please try again." + + "\nType the 'help' command for a list of valid commands."), + SAVE_ERROR ("Error saving to file, please try again."), + LOAD_ERROR ("Error loading saved data"), + INVALID_LINE_FORMAT ("File corrupted. Ignoring invalid line format..."), + INVALID_QUANTITY ("Quantity is invalid, please try again"), + INVALID_QUANTITY_FORMAT ("Quantity provided is not a number, please try again."), + INVALID_ADD_COMMAND ("Invalid Add command format..."), + INVALID_DELETE_COMMAND ("Invalid Delete command format..."), + INVALID_EDIT_COMMAND ("Invalid Edit command format..."), + INVALID_USE_COMMAND ("Invalid Use command format..."), + STOCK_UNDERFLOW ("Trying to use more items than is available, please try again."), + NO_ITEM_ERROR ("No item with that name in inventory, please try again."), + PARSE_DATE_ERROR ("Date provided is in the wrong format, please try again."), + ITEM_NOT_FOUND_ERROR ("Item not found..."), + TOO_MANY_ARGUMENTS ("Too many arguments. Please type 'help' for accepted commands."), + INVALID_STOCKCHECK_COMMAND ("Invalid stock-check command format..."), + INVALID_COST_COMMAND ("Invalid Cost command format..."), + INVALID_PRICE_COMMAND ("Invalid Price command format..."), + INVALID_RESTOCKALL_COMMAND ("Invalid restock-all command format..."), + INVALID_RESTOCK_COMMAND ("Invalid restock item command format..."), + TRANSACTION_ERROR ("Error creating transaction"), + INVALID_FULFILL_COMMAND ("Invalid fulfillment command format..."), + INVALID_TRANSACTION_HISTORY_COMMAND ("Invalid transaction history command format..."), + INVALID_DATETIME_FORMAT ("Invalid datetime format, please try again."), + INVALID_ORDER ("Order not found..."), + INVALID_INDEX ("Index out of bounds, please try again."), + INVALID_ORDER_COMMAND ("Invalid order command format..."), + INVALID_ITEM_FORMAT ("Invalid item format..."), + ORDER_NOT_PENDING ("Order already fulfilled..."), + INVALID_DATE_FORMAT ("Invalid expiry date format. Please try again..."), + NOTHING_TO_VISUALIZE ("There is nothing to visualize..."); + + + private final String message; + + /** + * Constructor for ExceptionMessages. + * + * @param message The message of the exception. + */ + ExceptionMessages(String message) { + this.message = message; + } + + /** + * Gets the message of the exception. + * + * @return The message of the exception. + */ + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/seedu/pill/exceptions/PillException.java b/src/main/java/seedu/pill/exceptions/PillException.java new file mode 100644 index 0000000000..857f2f78d9 --- /dev/null +++ b/src/main/java/seedu/pill/exceptions/PillException.java @@ -0,0 +1,21 @@ +package seedu.pill.exceptions; + +public class PillException extends Exception { + /** + * Constructor for PillException. + * + * @param message The cause of the exception using enum {@link ExceptionMessages}. + */ + public PillException(ExceptionMessages message) { + super(message.getMessage()); + } + + /** + * Prints the exception message. + * + * @param e The exception message to be printed. + */ + public static void printException(PillException e) { + System.out.println(e.getMessage()); + } +} diff --git a/src/main/java/seedu/pill/util/DateTime.java b/src/main/java/seedu/pill/util/DateTime.java new file mode 100644 index 0000000000..a62ad0ffc7 --- /dev/null +++ b/src/main/java/seedu/pill/util/DateTime.java @@ -0,0 +1,163 @@ +package seedu.pill.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +/** + * A wrapper class for LocalDateTime that provides additional utility methods for date and time operations. + * This class is used for handling dates, times, and datetime operations in the Pill application. + * It implements Comparable to allow for chronological ordering of DateTime instances. + */ +public class DateTime implements Comparable { + private LocalDateTime dateTime; + + /** + * Constructs a new DateTime instance set to the current date and time. + */ + public DateTime() { + this.dateTime = LocalDateTime.now(); + } + + /** + * Constructs a new DateTime instance with the specified LocalDateTime. + * + * @param dateTime - The LocalDateTime to initialize this DateTime with + */ + public DateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + /** + * Returns the underlying LocalDateTime instance. + * + * @return - The LocalDateTime instance representing this DateTime + */ + public LocalDateTime getDateTime() { + return dateTime; + } + + /** + * Sets the underlying LocalDateTime instance. + * + * @param dateTime - The new LocalDateTime to set + */ + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + /** + * Formats the datetime using the specified format pattern. + * + * @param format - The format pattern to use (following DateTimeFormatter patterns) + * @return - A string representation of the datetime in the specified format + */ + public String getFormattedDateTime(String format) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); + return dateTime.format(formatter); + } + + /** + * Returns the date component formatted as "yyyy-MM-dd". + * + * @return - A string representation of the date + */ + public String getFormattedDate() { + return getFormattedDateTime("yyyy-MM-dd"); + } + + /** + * Returns the time component formatted as "HH:mm:ss". + * + * @return - A string representation of the time + */ + public String getFormattedTime() { + return getFormattedDateTime("HH:mm:ss"); + } + + /** + * Compares this DateTime with another DateTime for ordering. + * The comparison is based on chronological order. + * + * @param other - The DateTime to compare with + * @return - A negative integer if this DateTime is earlier, zero if they're equal, + * or a positive integer if this DateTime is later + */ + @Override + public int compareTo(DateTime other) { + return this.dateTime.compareTo(other.getDateTime()); + } + + /** + * Checks if this DateTime is chronologically after the specified DateTime. + * + * @param other - The DateTime to compare with + * @return - True if this DateTime is after the other DateTime, false otherwise + */ + public boolean isAfter(DateTime other) { + return this.compareTo(other) > 0; + } + + /** + * Checks if this DateTime is chronologically before the specified DateTime. + * + * @param other - The DateTime to compare with + * @return - True if this DateTime is before the other DateTime, false otherwise + */ + public boolean isBefore(DateTime other) { + return this.compareTo(other) < 0; + } + + /** + * Calculates the number of days between this DateTime and another DateTime. + * + * @param other - The DateTime to calculate the difference to + * @return - The number of days between the two DateTimes + */ + public long getDaysUntil(DateTime other) { + return ChronoUnit.DAYS.between(this.dateTime, other.getDateTime()); + } + + /** + * Checks if this DateTime has passed the expiration DateTime. + * + * @param expirationDate - The DateTime representing the expiration date + * @return - True if this DateTime is after the expiration date, false otherwise + */ + public boolean isExpired(DateTime expirationDate) { + return this.isAfter(expirationDate); + } + + /** + * Calculates the number of days until the expiration date. + * + * @param expirationDate - The DateTime representing the expiration date + * @return - The number of days until expiration + */ + public long getDaysUntilExpiration(DateTime expirationDate) { + return getDaysUntil(expirationDate); + } + + /** + * Checks if this DateTime falls within the refill period. + * A DateTime is within the refill period if the current time is after + * this DateTime plus the specified number of days. + * + * @param daysBeforeRefill - The number of days to consider for the refill period + * @return - True if within the refill period, false otherwise + */ + public boolean isWithinRefillPeriod(int daysBeforeRefill) { + LocalDateTime refillDate = this.dateTime.plusDays(daysBeforeRefill); + return LocalDateTime.now().isAfter(refillDate); + } + + /** + * Returns a string representation of this DateTime in the format "yyyy-MM-dd HH:mm:ss". + * + * @return - A string representation of this DateTime + */ + @Override + public String toString() { + return getFormattedDateTime("yyyy-MM-dd HH:mm:ss"); + } +} diff --git a/src/main/java/seedu/pill/util/Item.java b/src/main/java/seedu/pill/util/Item.java new file mode 100644 index 0000000000..513bb7106e --- /dev/null +++ b/src/main/java/seedu/pill/util/Item.java @@ -0,0 +1,114 @@ +package seedu.pill.util; + +import java.time.LocalDate; +import java.util.Optional; + +/** + * Represents an item in the inventory. + */ +public class Item implements Comparable { + private String name; + private int quantity; + private Optional expiryDate; + private double cost; + private double price; + + public Item(String name, int quantity) { + this(name, quantity, null, 0, 0); + } + + public Item(String name, int quantity, LocalDate expiryDate) { + this(name, quantity, expiryDate, 0, 0); + } + + public Item(String name, int quantity, LocalDate expiryDate, double cost, double price) { + this.name = name; + this.quantity = quantity; + this.expiryDate = Optional.ofNullable(expiryDate); + this.cost = cost; + this.price = price; + } + + public String getName() { + return name; + } + + public int getQuantity() { + return quantity; + } + + public Optional getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDate expiryDate) { + this.expiryDate = Optional.ofNullable(expiryDate); + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public double getCost() { + return cost; + } + + public void setCost(double cost) { + this.cost = cost; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + /** + * Compares this {@code Item} with another {@code Item} based on their expiry dates. + * The comparison is in ascending order, meaning items with sooner expiry dates will come first. + * + * @param other The {@code Item} to be compared to this {@code Item}. + * @return A negative integer if this item expires sooner than the other; + * a positive integer if this item expires later than the other; + * zero if both items have the same expiry date. + */ + @Override + public int compareTo(Item other) { + return this.getExpiryDate() + .map(thisDate -> other.getExpiryDate() + .map(otherDate -> thisDate.compareTo(otherDate)) + .orElse(-1)) + .orElseGet(() -> other.getExpiryDate().map(x -> 1).orElse(0)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append(": ").append(quantity).append(" in stock"); + expiryDate.ifPresent(date -> sb.append(", expiring: ").append(date)); + if (cost > 0) { + sb.append(", cost: $").append(String.format("%.2f", cost)); + } + if (price > 0) { + sb.append(", price: $").append(String.format("%.2f", price)); + } + return sb.toString(); + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Item item) { + return name.equals(item.getName()) && quantity == item.getQuantity() + && expiryDate.equals(item.getExpiryDate()) + && Double.compare(cost, item.cost) == 0 + && Double.compare(price, item.price) == 0; + } + return false; + } +} diff --git a/src/main/java/seedu/pill/util/ItemMap.java b/src/main/java/seedu/pill/util/ItemMap.java new file mode 100644 index 0000000000..4c28b77ba9 --- /dev/null +++ b/src/main/java/seedu/pill/util/ItemMap.java @@ -0,0 +1,626 @@ +package seedu.pill.util; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; + +import java.time.LocalDate; +import java.util.Map; +import java.util.ArrayList; +import java.util.TreeSet; +import java.util.Iterator; +import java.util.Optional; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Logger; +import java.util.stream.IntStream; + +/** + * Represents a list of items and provides methods to add, delete, list, and edit items. + */ +public class ItemMap implements Iterable>> { + private static final Logger LOGGER = PillLogger.getLogger(); + Map> items; + + /** + * Constructor for ItemMap. + * Initializes the internal Map to store items. + */ + public ItemMap() { + this.items = new LinkedHashMap<>(); + LOGGER.info("New ItemMap instance created"); + } + + public boolean isEmpty() { + return this.items.isEmpty(); + } + + @Override + public Iterator>> iterator() { + return items.entrySet().iterator(); + } + + /** + * Compares this ItemMap to the specified object for equality. + * + *

This method returns {@code true} if and only if the specified object + * is also an ItemMap and both ItemMaps contain the same key-value pairs, + * where keys are strings and values are sets of Item objects. The equality + * of Item objects is determined by their own {@link Item#equals(Object)} + * method.

+ * + * @param obj the object to be compared for equality with this ItemMap + * @return {@code true} if the specified object is equal to this ItemMap; + * {@code false} otherwise + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ItemMap itemMap) { + return this.items.equals(itemMap.items); + } + return false; + } + + /** + * Adds a new item to the list. + * + * @param newItem The item to be added. + */ + public void addItem(Item newItem) { + String name = newItem.getName().toLowerCase(); + int quantity = newItem.getQuantity(); + Optional expiryDate = newItem.getExpiryDate(); + + assert name != null && !name.trim().isEmpty() : "Item name cannot be null or empty"; + assert quantity > 0 : "Quantity must be positive"; + + if (name == null || name.trim().isEmpty() || quantity <= 0) { + LOGGER.warning("Attempt to add invalid item: name=" + name + ", quantity=" + quantity); + System.out.println("Invalid item name or quantity."); + return; + } + + // If the item name exists, check for items with the same expiry date + if (items.containsKey(name)) { + TreeSet itemSet = items.get(name); + boolean itemUpdated = false; + + // Check if an item with the same expiry date already exists + for (Item item : itemSet) { + if (item.getExpiryDate().equals(expiryDate)) { + int newQuantity = item.getQuantity() + quantity; + item.setQuantity(newQuantity); + itemUpdated = true; + expiryDate.ifPresentOrElse( + expiry -> { + LOGGER.info("Updated existing item with expiry date: " + item); + System.out.println("Item already exists with the same expiry date. Updated quantity: \n" + + item); + }, + () -> { + LOGGER.info("Updated existing item with no expiry date: " + item); + System.out.println("Item already exists with no expiry date. Updated quantity: \n" + + item); + } + ); + break; + } + } + + // If no item with the same expiry date, add a new one + if (!itemUpdated) { + itemSet.add(newItem); + LOGGER.info("Added new item with different expiry date: " + newItem); + System.out.println("Added new item with a different expiry date: \n" + + newItem); + } + } else { + // If the item doesn't exist, create a new list for the item and add it + TreeSet itemSet = new TreeSet<>(); + itemSet.add(newItem); + items.put(name, itemSet); + LOGGER.info("Added new item: " + newItem); + System.out.println("Added the following item to the inventory: \n" + + newItem); + } + } + + /** + * Adds a new item to the list. Does not print any output. + * + * @param newItem The item to be added. + */ + public void addItemSilent(Item newItem) { + String name = newItem.getName().toLowerCase(); + int quantity = newItem.getQuantity(); + Optional expiryDate = newItem.getExpiryDate(); + + assert name != null && !name.trim().isEmpty() : "Item name cannot be null or empty"; + assert quantity > 0 : "Quantity must be positive"; + + if (name == null || name.trim().isEmpty() || quantity <= 0) { + LOGGER.warning("Attempt to silently add invalid item: name=" + name + ", quantity=" + quantity); + return; + } + + if (items.containsKey(name)) { + TreeSet itemSet = items.get(name); + boolean itemUpdated = false; + for (Item item : itemSet) { + if (item.getExpiryDate().equals(expiryDate)) { + int newQuantity = item.getQuantity() + quantity; + item.setQuantity(newQuantity); + itemUpdated = true; + LOGGER.fine("Silently updated existing item: " + name + ", new quantity: " + newQuantity); + break; + } + } + if (!itemUpdated) { + itemSet.add(newItem); + LOGGER.fine("Silently added new item: " + newItem); + } + } else { + TreeSet itemSet = new TreeSet<>(); + itemSet.add(newItem); + items.put(name, itemSet); + LOGGER.fine("Silently added new item: " + newItem); + } + } + + /** + * Deletes an item from the list by its name. + * + * @param itemName The name of the item to be deleted. + * @param expiryDate The date of the item to be deleted. + */ + public void deleteItem(String itemName, Optional expiryDate) { + String name = itemName.toLowerCase(); + assert name != null : "Item name cannot be null"; + + if (name == null || name.trim().isEmpty()) { + LOGGER.warning("Attempt to delete item with invalid name: " + name); + System.out.println("Invalid item name."); + return; + } + + TreeSet itemSet = items.get(name); + if (itemSet != null) { + Item dummyItem = expiryDate.map(ex -> new Item(name.toLowerCase(), 0, ex)) + .orElse(new Item(name, 0)); + Item removedItem = itemSet.ceiling(dummyItem); + if (removedItem != null && removedItem.getExpiryDate().equals(expiryDate)) { + itemSet.remove(removedItem); + LOGGER.info("Deleted item: " + removedItem); + System.out.println("Deleted the following item from the inventory: \n" + + removedItem); + if (itemSet.isEmpty()) { + items.remove(name); + } + } else { + LOGGER.warning("Attempt to delete non-existent item: " + removedItem); + System.out.println("Item not found: " + removedItem); + } + } else { + LOGGER.warning("Attempt to delete non-existent item: " + name); + System.out.println("Item not found: " + name); + } + } + + /** + * Edits an item by its name and expiry date. + * + * @param updatedItem The updated item that has a new quantity. + */ + public void editItem(Item updatedItem) { + String name = updatedItem.getName().toLowerCase(); + int quantity = updatedItem.getQuantity(); + Optional expiryDate = updatedItem.getExpiryDate(); + + assert name != null && !name.trim().isEmpty() : "Item name cannot be null or empty"; + assert quantity > 0 : "Quantity must be positive"; + + if (name == null || name.trim().isEmpty() || quantity <= 0) { + LOGGER.warning("Attempt to edit item with invalid parameters: name=" + name + ", quantity=" + quantity); + System.out.println("Invalid item name or quantity."); + return; + } + + TreeSet itemSet = items.get(name); + boolean isUpdated = false; + if (itemSet != null) { + for (Item item : itemSet) { + if (item.getExpiryDate().equals(expiryDate)) { + item.setQuantity(quantity); + isUpdated = true; + } + } + if (isUpdated) { + LOGGER.info("Edited item: " + updatedItem); + System.out.println("Edited item: " + updatedItem); + } else { + LOGGER.warning("Attempt to edit non-existent item: " + updatedItem); + System.out.println("Item not found: " + updatedItem); + } + } else { + LOGGER.warning("Attempt to edit non-existent item: " + name); + System.out.println("Item not found: " + name); + } + } + + /** + * Lists all the items in the inventory. + */ + public void listItems() { + if (items.isEmpty()) { + LOGGER.info("Attempted to list items, but inventory is empty"); + System.out.println("The inventory is empty."); + return; + } + LOGGER.info("Listing all items in inventory"); + System.out.println("Listing all items:"); + int index = 1; + for (Map.Entry> entry : items.entrySet()) { + TreeSet itemSet = entry.getValue(); + for (Item item : itemSet) { + System.out.println(index + ". " + item.toString()); + index++; + } + } + } + + /** + * Lists all items in this {@code ItemMap} that have expired or are expiring before the specified cutoff date. + *

+ * Retrieves expired or expiring items from {@link #getExpiringItems(LocalDate)}, and logs a message + * if there are no items meeting the specified date criteria. If there are items that have expired + * (when {@code cutOffDate} is today) or will expire before {@code cutOffDate} (for a future date), + * the method logs and prints a message listing these items. + *

+ * + * @param cutOffDate the date against which item expiry is checked. If the date is today, + * the method lists items that have expired. If the date is in the future, + * it lists items expiring before the cutoff date. + */ + public void listExpiringItems(LocalDate cutOffDate) { + ItemMap expiringItems = this.getExpiringItems(cutOffDate); + if (expiringItems.isEmpty()) { + if (cutOffDate.isEqual(LocalDate.now())) { + LOGGER.info("There are no items that have expired."); + System.out.println("There are no items that have expired."); + } else { + LOGGER.info("There are no items expiring before " + cutOffDate + "."); + System.out.println("There are no items expiring before " + cutOffDate + "."); + } + return; + } + if (cutOffDate.isEqual(LocalDate.now())) { + LOGGER.info("Listing all items that have expired"); + System.out.println("Listing all items that have expired"); + } else { + LOGGER.info("Listing all items expiring before " + cutOffDate); + System.out.println("Listing all items expiring before " + cutOffDate); + } + List itemList = expiringItems.items.values().stream() + .flatMap(Collection::stream) + .toList(); + + IntStream.range(0, itemList.size()) + .forEach(i -> System.out.println((i + 1) + ". " + itemList.get(i))); + + System.out.println(); + } + + /** + * Lists all the items in the inventory for restock command, given a threshold value. + * Prints all items with quantity strictly less than threshold. + * + * @param threshold The minimum number of items before it is deemed to require replenishment. + */ + public void listItemsToRestock(int threshold){ + try { + if (items.isEmpty()) { + LOGGER.info("Attempted to list items, but inventory is empty"); + return; + } + + if (threshold < 0) { + throw new PillException(ExceptionMessages.INVALID_QUANTITY); + } + + List filteredItems = items.values().stream() + .flatMap(TreeSet::stream) + .filter(item -> item.getQuantity() <= threshold) + .toList(); + + if (filteredItems.isEmpty()) { + LOGGER.info(String.format("There are no items that have quantity less than or equal to %d.", + threshold)); + System.out.printf("There are no items that have quantity less than or equal to %d:%n", threshold); + + } else { + LOGGER.info(String.format("Listing all items that need to be restocked (less than or equal to %d):", + threshold)); + System.out.printf("Listing all items that need to be restocked (less than or equal to %d):%n", + threshold); + IntStream.rangeClosed(1, filteredItems.size()) + .forEach(i -> System.out.println(i + ". " + filteredItems.get(i - 1).toString())); + } + + } catch (PillException e) { + LOGGER.severe(e.getMessage()); + PillException.printException(e); + } + } + + /** + * Finds an item in the list. + * + * @param itemName The name of the item. + */ + public ItemMap findItem(String itemName) { + assert itemName != null : "Item name cannot be null"; + + ItemMap foundItems = new ItemMap(); + if (itemName == null || itemName.trim().isEmpty()) { + LOGGER.warning("Attempt to find item with null or empty name"); + return foundItems; + } + LOGGER.info("Searching for items containing: " + itemName); + for (Map.Entry> entry : items.entrySet()) { + TreeSet itemSet = entry.getValue(); + if (itemSet.first().getName().toLowerCase().contains(itemName.toLowerCase())) { + for (Item item : itemSet) { + foundItems.addItemSilent(item); + } + } + } + LOGGER.info("Found " + foundItems.items.size() + " items matching: " + itemName); + return foundItems; + } + + /** + * Retrieves all items that expire before the cutOffDate from the item map. + * + *

This method iterates through all items in the item map and checks each item's expiry date. + * If the expiry date is before the cut off date, the item is added to a new {@code ItemMap} + * containing only the expiring items.

+ * + * @param cutOffDate date before which all items are considered to be expiring + * @return an {@code ItemMap} containing all items that are expiring. + */ + public ItemMap getExpiringItems(LocalDate cutOffDate) { + ItemMap expiringItems = new ItemMap(); + for (Map.Entry> entry : items.entrySet()) { + TreeSet itemSet = entry.getValue(); + itemSet.stream() + .flatMap(item -> item.getExpiryDate().stream() + .filter(expiry -> expiry.isBefore(cutOffDate)) + .map(expiry -> item)) + .forEach(expiringItems::addItemSilent); + } + return expiringItems; + } + + /** + * Uses a specified quantity of items with the given name, consuming from the earliest expiring items first. + * If the quantity to use equals or exceeds an item's quantity, that item is deleted. + * If the quantity to use is less than an item's quantity, the item's quantity is reduced. + * + * @param itemName the name of the item to use + * @param quantityToUse the quantity of the item to consume + * @throws PillException if: + * - the requested quantity exceeds the total available stock for the item + * - the specified item name does not exist in the inventory + */ + public void useItem(String itemName, int quantityToUse) throws PillException { + itemName = itemName.toLowerCase(); + if (quantityToUse > this.stockCount(itemName)) { + LOGGER.warning("Attempt to use more items than available: name=" + itemName + + ", requested=" + quantityToUse + + ", available=" + this.stockCount(itemName)); + throw new PillException(ExceptionMessages.STOCK_UNDERFLOW); + } + + int originalQuantity = quantityToUse; // Store for logging purposes + while (quantityToUse > 0) { + // throws PillException if no key-value pair for itemName exists + Item itemToUse = this.getSoonestExpiringItem(itemName); + + if (itemToUse.getQuantity() == quantityToUse) { + quantityToUse = 0; + this.deleteItem(itemName, itemToUse.getExpiryDate()); + + itemToUse.getExpiryDate().ifPresentOrElse( + expiry -> { + LOGGER.info("Completely used item with expiry date: " + itemToUse); + System.out.println("Completely used item with expiry date " + expiry + ": \n" + itemToUse); + }, + () -> { + LOGGER.info("Completely used item: " + itemToUse); + System.out.println("Completely used item: \n" + itemToUse); + } + ); + } else if (itemToUse.getQuantity() > quantityToUse) { + int oldQuantity = itemToUse.getQuantity(); + itemToUse.setQuantity(oldQuantity - quantityToUse); + this.editItem(itemToUse); + quantityToUse = 0; + + itemToUse.getExpiryDate().ifPresentOrElse( + expiry -> { + LOGGER.info("Partially used item with expiry date: " + itemToUse + + " (reduced from " + oldQuantity + " to " + itemToUse.getQuantity() + ")"); + System.out.println("Partially used item with expiry date " + expiry + + " (reduced from " + oldQuantity + " to " + itemToUse.getQuantity() + "): \n" + + itemToUse); + }, + () -> { + LOGGER.info("Partially used item with no expiry date: " + itemToUse + + " (reduced from " + oldQuantity + " to " + itemToUse.getQuantity() + ")"); + System.out.println("Partially used item with no expiry date" + + " (reduced from " + oldQuantity + " to " + itemToUse.getQuantity() + "): \n" + + itemToUse); + } + ); + } else { + quantityToUse -= itemToUse.getQuantity(); + this.deleteItem(itemName, itemToUse.getExpiryDate()); + + itemToUse.getExpiryDate().ifPresentOrElse( + expiry -> { + LOGGER.info("Completely used item with expiry date " + expiry + + "(more remaining to use): " + itemToUse); + }, + () -> { + // below shouldn't be reachable + LOGGER.warning("Completely used all items, but still need to use more."); + } + ); + } + } + } + + /** + * Retrieves the item with the soonest expiry date for the specified item name. + * + * @param itemName the name of the item to retrieve + * @return the {@code Item} with the soonest expiry date + * @throws PillException If the input string is invalid or there is no such key-value mapping in ItemMap + */ + public Item getSoonestExpiringItem(String itemName) throws PillException { + assert itemName != null : "Item name cannot be null"; + + if (itemName == null || itemName.trim().isEmpty()) { + throw new PillException(ExceptionMessages.INVALID_COMMAND); + } + + TreeSet item = this.items.get(itemName); + + if (item == null) { + throw new PillException(ExceptionMessages.NO_ITEM_ERROR); + } + + return item.first(); + } + + /** + * Calculates the total quantity in stock for the specified item name. + *

+ * Iterates over all instances of the item to aggregate quantities + * from each entry, where each {@code Item} represents a distinct batch + * with an associated quantity. + *

+ * + * @param itemName the name of the item to query + * @return the total quantity in stock for the specified item; returns 0 if + * the item does not exist or the name is invalid + */ + public int stockCount(String itemName) { + assert itemName != null : "Item name cannot be null"; + + if (itemName == null || itemName.trim().isEmpty()) { + return 0; + } + + TreeSet item = this.items.get(itemName); + + if (item == null) { + return 0; + } + + int totalQuantity = 0; + for (Item currentItem : item) { + totalQuantity += currentItem.getQuantity(); + } + + return totalQuantity; + } + + /** + * Returns the total number of items in the map. + * This counts each individual item, including those with different expiry dates. + * + * @return The total number of items in the ItemMap. + */ + public int size() { + int totalSize = 0; + for (TreeSet itemSet : items.values()) { + totalSize += itemSet.size(); + } + return totalSize; + } + + /** + * Returns a list of all items in the ItemMap. + * + * @return A list containing all items in the inventory. + */ + public List getAllItems() { + List allItems = new ArrayList<>(); + for (TreeSet itemSet : items.values()) { + allItems.addAll(itemSet); + } + return allItems; + } + + + /** + * Returns a collection of all items with the specified name. + * If no items are found, returns an empty collection. + * + * @param itemName The name of the items to retrieve. + * @return A collection of items with the specified name, or an empty collection if none are found. + */ + public Collection getItemsByName(String itemName) { + return items.containsKey(itemName) ? new TreeSet<>(items.get(itemName)) : Collections.emptySet(); + } + + /** + * Retrieves an item by its name and optional expiry date. + * + * @param itemName The name of the item. + * @param expiryDate An optional expiry date. If provided, it will look for an item with this expiry date. + * @return The item with the specified name and expiry date, or null if not found. + */ + public Item getItemByNameAndExpiry(String itemName, Optional expiryDate) { + TreeSet itemSet = items.get(itemName); + if (itemSet == null) { + return null; + } + + for (Item item : itemSet) { + if (expiryDate.isPresent()) { + if (item.getExpiryDate().equals(expiryDate)) { + return item; + } + } else if (item.getExpiryDate().isEmpty()) { + return item; + } + } + return null; + } + + /** + * Returns a flat list of all items in the inventory, for use in external classes like Visualizer. + * + * @return An ArrayList containing all items in the inventory. + */ + public ArrayList getItemsAsArrayList() { + return new ArrayList<>(getAllItems()); + } + + + /** + * Returns the set of items with the given name. + * + * @param itemName The name of the item to retrieve. + * @return A TreeSet of items with the given name, or null if the item does not exist. + */ + public TreeSet get(String itemName) { + return items.getOrDefault(itemName, new TreeSet<>()); + } +} diff --git a/src/main/java/seedu/pill/util/Order.java b/src/main/java/seedu/pill/util/Order.java new file mode 100644 index 0000000000..5f0797de5f --- /dev/null +++ b/src/main/java/seedu/pill/util/Order.java @@ -0,0 +1,193 @@ +package seedu.pill.util; + +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.Map; +import java.util.TreeSet; + +/** + * Represents an order in the inventory management system. + * An order is a collection of items that are either being received into inventory (purchase orders) + * or being dispensed out of inventory (dispense orders). Each order has a unique identifier, + * tracks its creation and fulfillment times, maintains a status, and can include notes. + */ +public class Order { + private final UUID id; + private final OrderType type; + private final LocalDateTime creationTime; + private LocalDateTime fulfillmentTime; + private OrderStatus status; + private final ItemMap items; + private final String notes; + + /** + * Defines the types of orders that can be created in the system. + * PURCHASE - orders represent incoming inventory from suppliers. + * DISPENSE - orders represent outgoing inventory to end users or departments. + */ + public enum OrderType { + PURCHASE, + DISPENSE + } + + /** + * Defines the possible states of an order in the system. + * PENDING - indicates the order is awaiting processing. + * FULFILLED - indicates the order has been successfully processed and completed. + * CANCELLED - indicates the order was terminated before completion. + */ + public enum OrderStatus { + PENDING, + FULFILLED, + CANCELLED + } + + /** + * Creates a new Order with the specified type and notes. + * The order is automatically assigned a unique identifier and initialized + * with a PENDING status and the current timestamp. + * + * @param type - The type of order (PURCHASE or DISPENSE) + * @param notes - Additional information or comments about the order + */ + public Order(OrderType type, String notes) { + this.id = UUID.randomUUID(); + this.type = type; + this.creationTime = LocalDateTime.now(); + this.status = OrderStatus.PENDING; + this.items = new ItemMap(); + this.notes = notes; + } + + /** + * Creates a new Order with the specified type and notes. + * The order is automatically assigned a unique identifier and initialized + * with a PENDING status and the current timestamp. + * + * @param type - The type of order (PURCHASE or DISPENSE) + * @param notes - Additional information or comments about the order + */ + public Order(OrderType type, ItemMap itemsToOrder, String notes) { + this.id = UUID.randomUUID(); + this.type = type; + this.creationTime = LocalDateTime.now(); + this.status = OrderStatus.PENDING; + this.items = itemsToOrder; + this.notes = notes; + } + + /** + * Adds an item to this order with the specified name and quantity. + * Multiple items can be added to a single order. + * + * @param item - The item to add + */ + public void addItem(Item item) { + items.addItemSilent(item); + } + + /** + * Marks this order as fulfilled, updating its status and recording + * the fulfillment timestamp. + */ + public void fulfill() { + this.status = OrderStatus.FULFILLED; + this.fulfillmentTime = LocalDateTime.now(); + } + + /** + * Marks this order as cancelled, preventing it from being fulfilled. + */ + public void cancel() { + this.status = OrderStatus.CANCELLED; + } + + /** + * Returns the unique identifier for this order. + * + * @return - The UUID assigned to this order + */ + public UUID getId() { + return id; + } + + /** + * Returns the type of this order (PURCHASE or DISPENSE). + * + * @return - The OrderType of this order + */ + public OrderType getType() { + return type; + } + + /** + * Returns the timestamp when this order was created. + * + * @return - The creation timestamp + */ + public LocalDateTime getCreationTime() { + return creationTime; + } + + /** + * Returns the timestamp when this order was fulfilled, if applicable. + * + * @return - The fulfillment timestamp, or null if the order is not fulfilled + */ + public LocalDateTime getFulfillmentTime() { + return fulfillmentTime; + } + + /** + * Returns the current status of this order. + * + * @return - The current OrderStatus + */ + public OrderStatus getStatus() { + return status; + } + + /** + * Returns a copy of the list of items in this order. + * The returned list is a new ArrayList to prevent modification of the original items. + * + * @return - A new ArrayList containing the OrderItems in this order + */ + public ItemMap getItems() { + return items; + } + + /** + * Returns the notes associated with this order. + * + * @return - The notes string provided during order creation + */ + public String getNotes() { + return notes; + } + /** + * Lists the details of the order, including UUID, type, creation time, fulfillment time, + * status, notes, and the items in the order with a serial number for each item. + * + *

This method prints the order's metadata such as UUID, type, creation time, fulfillment time, + * status, and notes. It then iterates through the items in the order, printing each item's details + * with a serial number (starting from 1) to differentiate the items within the order.

+ */ + public void listItems() { + int index = 1; + System.out.println("UUID: " + id); + System.out.println("Type: " + type); + System.out.println("Creation Time: " + creationTime); + System.out.println("Fulfillment Time: " + fulfillmentTime); + System.out.println("Status: " + status); + System.out.println("Notes: " + notes); + System.out.println("Items: "); + for (Map.Entry> entry : items.items.entrySet()) { + TreeSet itemSet = entry.getValue(); + for (Item item : itemSet) { + System.out.println(index + ". " + item.toString()); + index++; + } + } + } +} diff --git a/src/main/java/seedu/pill/util/Parser.java b/src/main/java/seedu/pill/util/Parser.java new file mode 100644 index 0000000000..3b67900594 --- /dev/null +++ b/src/main/java/seedu/pill/util/Parser.java @@ -0,0 +1,875 @@ +package seedu.pill.util; + +import seedu.pill.command.HelpCommand; +import seedu.pill.command.AddItemCommand; +import seedu.pill.command.DeleteItemCommand; +import seedu.pill.command.EditItemCommand; +import seedu.pill.command.ExpiredCommand; +import seedu.pill.command.ExpiringCommand; +import seedu.pill.command.FindCommand; +import seedu.pill.command.ListCommand; +import seedu.pill.command.RestockAllCommand; +import seedu.pill.command.RestockItemCommand; +import seedu.pill.command.SetCostCommand; +import seedu.pill.command.SetPriceCommand; +import seedu.pill.command.StockCheckCommand; +import seedu.pill.command.UseItemCommand; +import seedu.pill.command.TransactionHistoryCommand; +import seedu.pill.command.OrderCommand; +import seedu.pill.command.FulfillCommand; +import seedu.pill.command.ViewOrdersCommand; +import seedu.pill.command.TransactionsCommand; +import seedu.pill.command.VisualizeCostCommand; +import seedu.pill.command.VisualizePriceCommand; +import seedu.pill.command.VisualizeStockCommand; +import seedu.pill.command.VisualizeCostPriceCommand; + +import seedu.pill.util.Order.OrderType; + +import seedu.pill.exceptions.ExceptionMessages; +import seedu.pill.exceptions.PillException; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class Parser { + private boolean exitFlag = false; + private final ItemMap items; + private final Storage storage; + private final TransactionManager transactionManager; + private final Ui ui; + private final Visualizer visualizer; + + /** + * Public constructor for Parser. + */ + public Parser(ItemMap items, Storage storage, TransactionManager transactionManager, Ui ui) { + this.items = items; + this.storage = storage; + this.transactionManager = transactionManager; + this.ui = ui; + this.visualizer = new Visualizer(items.getItemsAsArrayList()); + } + + /** + * Processes the user's command. + * + * @param input The user's input command from the scanner. + */ + public void parseCommand(String input) { + try { + input = input.trim(); + String[] splitInput = input.split("\\s+"); + String commandString = splitInput[0].toLowerCase(); + String argument = splitInput.length > 1 ? splitInput[1] : null; + String flagStr = splitInput.length > 2 ? splitInput[2] : ""; + String arguments = String.join(" ", Arrays.copyOfRange(splitInput, 1, splitInput.length)); + + switch (commandString) { + case "exit": + this.exitFlag = true; + visualizer.closeCharts(); + break; + case "add": + parseAddItemCommand(arguments).execute(this.items, this.storage); + break; + case "delete": + parseDeleteItemCommand(arguments).execute(this.items, this.storage); + break; + case "edit": + parseEditItemCommand(arguments).execute(this.items, this.storage); + break; + case "find": + new FindCommand(arguments).execute(this.items, this.storage); + break; + case "help": + boolean flag = flagStr.equals("-v"); + new HelpCommand(argument, flag).execute(this.items, this.storage); + break; + case "list": + if (splitInput.length > 1) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + new ListCommand().execute(this.items, this.storage); + break; + case "stock-check": + if (splitInput.length != 2) { + throw new PillException(ExceptionMessages.INVALID_STOCKCHECK_COMMAND); + } + if (!argument.matches("\\d+")) { + throw new PillException(ExceptionMessages.INVALID_STOCKCHECK_COMMAND); + } + new StockCheckCommand(argument).execute(this.items, this.storage); + break; + case "expired": + if (splitInput.length > 1) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + new ExpiredCommand().execute(this.items, this.storage); + break; + case "expiring": + if (splitInput.length > 2) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + if (!this.isValidDate(arguments)) { + throw new PillException(ExceptionMessages.PARSE_DATE_ERROR); + } + LocalDate expiryDate = parseExpiryDate(arguments); + new ExpiringCommand(expiryDate).execute(this.items, this.storage); + break; + case "cost": + parseSetCostCommand(arguments).execute(this.items, this.storage); + break; + case "price": + parseSetPriceCommand(arguments).execute(this.items, this.storage); + break; + case "restock-all": + parseRestockAllCommand(arguments).execute(this.items, this.storage); + break; + case "restock": + parseRestockItemCommand(arguments).execute(this.items, this.storage); + break; + case "visualize-price": + new VisualizePriceCommand(visualizer).execute(this.items, this.storage); + break; + case "visualize-cost": + new VisualizeCostCommand(visualizer).execute(this.items, this.storage); + break; + case "visualize-stock": + new VisualizeStockCommand(visualizer).execute(this.items, this.storage); + break; + case "visualize-cost-price": + new VisualizeCostPriceCommand(visualizer).execute(this.items, this.storage); + break; + case "use": + parseUseItemCommand(arguments).execute(this.items, this.storage); + break; + case "order": + parseOrderCommand(arguments).execute(this.items, this.storage); + break; + case "view-orders": + new ViewOrdersCommand(transactionManager).execute(this.items, this.storage); + break; + case "transactions": + if (splitInput.length > 1) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + new TransactionsCommand(transactionManager).execute(this.items, this.storage); + break; + case "transaction-history": + parseTransactionHistoryCommand(arguments).execute(this.items, this.storage); + break; + case "fulfill-order": + parseFulfillCommand(arguments).execute(this.items, this.storage); + break; + default: + throw new PillException(ExceptionMessages.INVALID_COMMAND); + } + } catch (PillException e) { + PillException.printException(e); + } + } + + /** + * Parses the arguments provided to create a {@link FulfillCommand} instance, which is used to fulfill an order. + * + * @param arguments The command input containing the order UUID to be fulfilled. + * @return A {@link FulfillCommand} instance that contains the order and transaction manager. + * @throws PillException if the input contains too many arguments, is empty, cannot be parsed as a number, + * or if the specified order UUID is invalid. + * + */ + private FulfillCommand parseFulfillCommand(String arguments) throws PillException { + String[] commandArguments = arguments.split("\\s+"); + if (commandArguments.length > 1) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + if (commandArguments.length == 0 || arguments.isEmpty()) { + throw new PillException(ExceptionMessages.INVALID_FULFILL_COMMAND); + } + try { + List orders = transactionManager.getOrders(); + String orderToFetch = commandArguments[0]; + Order order = orders.stream() + .filter(orderInfo -> orderInfo.getId().toString().equals(orderToFetch)) + .findFirst() + .orElse(null); + if (order == null) { + throw new PillException(ExceptionMessages.INVALID_ORDER); + } + return new FulfillCommand(order, transactionManager); + } catch (IndexOutOfBoundsException e) { + throw new PillException(ExceptionMessages.INVALID_ORDER); + } + } + + /** + * Parses the arguments provided to create a {@link TransactionHistoryCommand} instance, + * which retrieves the transaction history for a specified date range. + * + * @param arguments The command input containing either one or two date-time arguments + * representing the start (and optionally the end) of the date range. + * @return A {@link TransactionHistoryCommand} instance that contains the specified + * date range and transaction manager. + * @throws PillException if the input contains too many arguments, is empty, + * has an invalid date-time format. + * + */ + private TransactionHistoryCommand parseTransactionHistoryCommand(String arguments) throws PillException { + String[] commandArguments = arguments.split("\\s+"); + LocalDate start; + LocalDate end; + if (commandArguments.length > 2) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } + if (commandArguments.length == 0) { + throw new PillException(ExceptionMessages.INVALID_TRANSACTION_HISTORY_COMMAND); + } else if (commandArguments.length == 1) { + try { + start = LocalDate.parse(commandArguments[0]); + return new TransactionHistoryCommand(start, LocalDate.now(), transactionManager); + } catch (DateTimeParseException e) { + throw new PillException(ExceptionMessages.INVALID_DATETIME_FORMAT); + } + } else { + try { + start = LocalDate.parse(commandArguments[0]); + end = LocalDate.parse(commandArguments[1]); + return new TransactionHistoryCommand(start, end, transactionManager); + } catch (DateTimeParseException e) { + throw new PillException(ExceptionMessages.INVALID_DATETIME_FORMAT); + } + } + } + + /** + * Parses the arguments provided to create an {@link OrderCommand} instance, which handles + * ordering items for either purchasing or dispensing, based on the specified order type. + * + * @param arguments The command input containing the order type (either "PURCHASE" or "DISPENSE") + * and the number of items to order. + * @return An {@link OrderCommand} instance that contains the specified items to order, + * transaction manager, and order type. + * @throws PillException if the input contains too many arguments, is empty, has an invalid order type, + * or if item details are incorrectly formatted. + * + */ + private OrderCommand parseOrderCommand(String arguments) throws PillException { + String commandArguments; + String notes = null; + + if (arguments.contains("\"")) { + // Split at the first quotation mark + int quoteStart = arguments.indexOf("\""); + + // Extract the non-quoted section (order type and quantity) + commandArguments = arguments.substring(0, quoteStart).trim(); + + // Extract the quoted section as notes + int quoteEnd = arguments.lastIndexOf("\""); + if (quoteEnd == quoteStart) { + throw new PillException(ExceptionMessages.INVALID_ORDER_COMMAND); // Unbalanced quotes + } + notes = arguments.substring(quoteStart + 1, quoteEnd); + } else { + // No notes, so the entire input is order type and quantity + commandArguments = arguments.trim(); + } + + String[] orderTypeAndQuantity = commandArguments.split("\\s+"); + + if (orderTypeAndQuantity.length > 2) { + throw new PillException(ExceptionMessages.TOO_MANY_ARGUMENTS); + } else if (orderTypeAndQuantity.length < 2) { + throw new PillException(ExceptionMessages.INVALID_ORDER_COMMAND); + } + + ItemMap itemsToOrder = new ItemMap(); + OrderType orderType = null; + int numberOfItems = parseQuantity(orderTypeAndQuantity[1]); + + if (orderTypeAndQuantity[0].equalsIgnoreCase("PURCHASE")) { + orderType = OrderType.PURCHASE; + } else if (orderTypeAndQuantity[0].equalsIgnoreCase("DISPENSE")) { + orderType = OrderType.DISPENSE; + } else { + throw new PillException(ExceptionMessages.INVALID_ORDER_COMMAND); + } + + boolean hasBeenPrinted = false; + for (int i = 0; i < numberOfItems; i++) { + String userInput = ui.getRawInput(); + String[] itemArguments = userInput.split("\\s+"); + if (itemArguments.length < 2) { + throw new PillException(ExceptionMessages.INVALID_ITEM_FORMAT); + } + try { + Item item = parseItem(itemArguments); + if (!item.getExpiryDate().equals(Optional.empty()) + && orderType.equals(OrderType.DISPENSE)) { + item.setExpiryDate(null); + + if (!hasBeenPrinted) { + System.out.println("Expiry dates will be ignored for dispense orders"); + hasBeenPrinted = true; + } + } + itemsToOrder.addItemSilent(item); + } catch (PillException e) { + throw new PillException(ExceptionMessages.INVALID_ITEM_FORMAT); + } + } + + return new OrderCommand(itemsToOrder, transactionManager, orderType, notes); + } + /** + * Parses an array of item arguments and returns an {@code Item} object. + * The item arguments array is expected to contain details such as item name, quantity, and optional expiry date. + * This method validates the format of the item arguments and ensures that only one date is included and + * positioned correctly as the last element if present. + * + *

The parsing logic follows these rules: + *

    + *
  • If the quantity is specified, it should be a numeric value located at the end or second to last position. + *
  • + *
  • If an expiry date is specified, it should be a valid date string located at the last position in the + * array.
  • + *
  • If no quantity is specified, it defaults to "1".
  • + *
  • If no date is specified, it defaults to {@code null}.
  • + *
+ * + * @param itemArguments An array of strings containing the arguments for the item. + * It may include an item name, quantity, and an optional expiry date. + * @return An {@code Item} object constructed from the parsed item arguments. + * @throws PillException If there are multiple dates, an invalid date format, or if the arguments are in an invalid + * format. + */ + public Item parseItem(String[] itemArguments) throws PillException { + Integer quantityIndex = null; + Integer dateIndex = null; + + for (int j = 0; j < itemArguments.length; j++) { + String currentArgument = itemArguments[j]; + + if (isValidDate(currentArgument)) { + if (dateIndex != null) { + throw new PillException(ExceptionMessages.INVALID_ITEM_FORMAT); + } + dateIndex = j; + + if (j != itemArguments.length - 1) { + throw new PillException(ExceptionMessages.INVALID_ITEM_FORMAT); + } + } + + if (isANumber(currentArgument)) { + quantityIndex = j; + } + } + + String itemName; + String quantityStr = null; + String expiryDateStr = null; + + if (quantityIndex != null && quantityIndex == itemArguments.length - 1) { + quantityStr = itemArguments[quantityIndex]; + expiryDateStr = null; + itemName = buildItemName(itemArguments, 0, quantityIndex); + } else if (quantityIndex != null && quantityIndex == itemArguments.length - 2 + && isValidDate(itemArguments[quantityIndex + 1])) { + quantityStr = itemArguments[quantityIndex]; + expiryDateStr = itemArguments[quantityIndex + 1]; + itemName = buildItemName(itemArguments, 0, quantityIndex); + } else if (dateIndex != null && dateIndex == itemArguments.length - 1) { + expiryDateStr = itemArguments[dateIndex]; + quantityStr = "1"; + itemName = buildItemName(itemArguments, 0, dateIndex); + } else { + quantityStr = "1"; + expiryDateStr = null; + itemName = buildItemName(itemArguments, 0, itemArguments.length); + } + + return new Item(itemName, parseQuantity(quantityStr), parseExpiryDate(expiryDateStr)); + } + + /** + * Parses the `restockall` command with an optional threshold. + * + * @param arguments The user's input after the `restockall` command. + * @return A `RestockAllCommand` with the parsed threshold. + * @throws PillException If the input format is invalid. + */ + private RestockAllCommand parseRestockAllCommand(String arguments) throws PillException { + int threshold = 50; + if (!arguments.isEmpty()) { + if (!isANumber(arguments)) { + throw new PillException(ExceptionMessages.INVALID_RESTOCKALL_COMMAND); + } + threshold = Integer.parseInt(arguments); + if (threshold <= 0) { + throw new PillException(ExceptionMessages.INVALID_QUANTITY); + } + } + return new RestockAllCommand(threshold); + } + + /** + * Parses the `restock` command for a specific item, with an optional expiry date and quantity. + * + * @param arguments The user's input after the `restock` command. + * @return A `RestockItemCommand` with the parsed item name, expiry date, and quantity. + * @throws PillException If the input format is invalid. + */ + private RestockItemCommand parseRestockItemCommand(String arguments) throws PillException { + String[] splitArguments = arguments.split("\\s+"); + if (splitArguments.length < 2) { + throw new PillException(ExceptionMessages.INVALID_RESTOCK_COMMAND); + } + + String itemName; + Optional 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)); + } +}