diff --git a/.gitignore b/.gitignore index 2873e189e1..2fe76deada 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +/text-ui-test/data/*.txt +/data/*.txt diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..fd00eb8b10 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: seedu.clirental.CliRental + diff --git a/build.gradle b/build.gradle index ea82051fab..1c51e7628b 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,9 @@ 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' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3' + testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' + testImplementation 'org.mockito:mockito-inline:4.11.0' } test { @@ -29,11 +32,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.clirental.CliRental") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("CliRental") archiveClassifier.set("") } @@ -42,5 +45,6 @@ checkstyle { } run{ + enableAssertions = true standardInput = System.in } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..c4921cf17a 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,22 @@ # About us + +Display | Name | Github Profile | Portfolio +--------|:-----:|:------------------------------------:|:---------: +![](./team/Khanh.jpg) | Khanh | [Github](https://github.com/tkhahns) | [Portfolio](./team/tkhahns.md) + +Display | Name | Github Profile | Portfolio +--------|:--------:|:--------------------------------------:|:---------: +![](./team/Rex.png) | Rex Koh | [Github](https://github.com/rexkoh425) | [Portfolio](./team/rexkoh425.md) + 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) +![](./team/Ryan.jpeg) | Ryan Fong | [Github](https://github.com/CT9ARyan) | [Portfolio](./team/ct9aryan.md) + +Display | Name | Github Profile | Portfolio +--------|:-------:|:--------------------------------------:|:---------: +![](./team/Aaron.jpg) | Liu Hao | [Github](https://github.com/AaronZZ10) | [Portfolio](./team/aaronzz10.md) + +Display | Name | Github Profile | Portfolio +--------|:-----------:|:--------------------------------------:|:---------: +![](./team/Kenneth.jpg) | Kenneth Tan | [Github](https://github.com/SemiColonKen) | [Portfolio](./team/semicolonken.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..a01c323e48 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,359 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +No reused/adapted ideas, code, documentation, and third-party libraries was used in the project. + +## Limitations of PlantUML + +1) Circle with capitalised first letter is shown in class diagram beside the class name e.g. C when class is +called Customer. +2) Other software might be used by the team if plantUML is unable to display what they want, explaining difference +in diagrams across the team ## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Architecture diagram + +The following is our overall architecture diagram for our whole project. To reduce the size of the overall diagram , only class +names are included. + +![Local Image](images/ArchitectureDiagram.drawio.png) + + +--- +### Adding a customer + +### Implementation: +The following sequence diagram will illustrate the sequence of events of a **valid** `add-user` operation. Customer +details like age and contact number are stored which are useful when the rental company would like to contact +the customer and creating transactions. + +### Sequence diagram +![Local Image](images/AddCustomerSequence.png) + +### Removing a car + +The following sequence diagram illustrates the sequence of events after the user executes +the `remove-car` command. + +![](./images/RyanRemoveCarDiagram.png) + +### Creating Car file and loading file + +### High-level steps + +1) Parse the input into its parameters and extract the content. +2) Create the new Customer object if all parameters fit the format. +3) Add it to the current Customer ArrayList. + +--- +### Creating file and loading file at the start of the program + +### Implementation: +The following sequence diagram will explain the sequence of events for loading of the carData.txt which happens at the +start of the program. The operations involved for the other two files are very similar so we will use the example of +carData.txt. The carData.txt will be created if it does not exist at the start of program and its data will be +loaded if the file exist. + +### Sequence diagram +![Local Image](images/CarFileLoader.png) + +### Class diagram for All File-related Classes + +![Local Image](images/FileHandlerClassDiagram.png) + +### High-level steps + +1) File is created if it does not exist. +2) Each line in the file is scanned and checked that each parameter is correctly formatted. +3) The correct data is parsed into a new Car object. +4) Each Car object is placed into the ArrayList. + +### Rationale behind way of implementation: + +Similar to real-world applications, data are stored on the computer already and does not require the user to explicitly +load data from a specific location. It should be automatic and hassle-free. Preventing the corrupted data from entering +the system is also important as it might crash the program thus it is important to check the data before adding. + +### Alternatives considered: +* User can choose which file to load + * Cons + 1) Too complicated as there are too many possible file paths possible and would significant add to the complexity of + the program. +* User can choose whether to load data file and save when they want to. + * Pros + 1) User has more control over the version control of the data files. + * Cons + 1) User experience might decrease as they constantly have to save files themselves. + 2) Many important data can be lost if program crashes before saving of file which does not happen with + constant updating. + +--- +### Auto updating of car rental status feature + +### Implementation: + +Every car added to the car list will have a default rental status of **'Available'**. +Once a valid transaction has been made and added to the transaction list, the particular car, +identified by its **unique identifier (i.e. license plate number)**, will automatically be +marked as **'Rented'**. + +Given below is a step-by-step execution of the implementation: + +Step 1: The user launches the application and enters the `add-car ...` command to add a new +car to the list, specifying the model name, car license plate number and price of the car. + +> **Note**: If the command fails due to invalid parameters or format, +the car will not be added to the list. + +The following sequence diagram illustrates a **valid** `add-car` operation: + +![](./images/RyanAddCarDiagram.png) + +Step 2: The user enters the `list-cars` command to verify that the car has been successfully added +to the car list. + +Example: + +![](./images/list-cars-output-before.png) + + +Step 3: A customer decides to rent a car from the company. The user then uses the application to track +and record the transaction details. The user executes the `add-tx ...` command to add +a new transaction record, specifying details like the car license plate number. + +> **Note**: If the command fails due to invalid parameters or format, +the transaction will not be added to the list and the car rental status remains as **'Available'**. + +The following sequence diagram illustrates a **valid** `add-tx` operation: + +![](./images/RyanAddTxDiagram.png) + +Step 4: After adding the transaction, the rental status of the selected car will now be updated to **'Rented'**. +The user finally executes the `list-rented` command to list all rented out cars. The car that +was just rented out should appear in the list, together with other rented out cars (if any). + +Example: + +![](./images/list-rented-output.png) + +Step 5: **Optionally**, the user can also execute +the `list-cars` command to view the rental status of all +the cars. By doing so, the rental status of the car that was just rented out, +should now have a rental status of **'Rented'** instead of **'Available'**. + +Example: +![](./images/list-cars-output-after.png) +> **Note**: The rental status is now updated to **'Rented'**. +> (compare with output in **Step 2**) + +The following class diagram shows the interaction between the classes involved: + +![](./images/RyanClassDiagram.png) + +### Rationale behind way of implementation: +Modelled after the real world, the car is only rented out once the transaction (payment) is complete. +For our application, after adding a transaction, it signifies that the +transaction was successful and complete. As such, the car should be rented out after that. Therefore, +the car's rental status is automatically updated once a new transaction record is added. + +### Alternatives considered: +- **Alternative 1 (current choice):** Automatically update car rental status + - Pros: + - Improve effectiveness and efficiency (E&E) + - Easy to implement + - No need for additional commands + - Cons: + - Does not adhere to software design principles like Single +Responsibility principle (SRP) or Separation of Concerns principle (SOC). +- **Alternative 2:** Manually update car rental status + - Pros: + - Adheres to the software design principles and makes the code more OOP. + - Cons: + - Possibility that user might forget to update rental status + - Need to add new commands to update rental status (e.g. `mark-rented`) +--- +### Implementation of Transaction Completion Management and Retrieval Features + +To enhance the functionality of **CliRental**, we have implemented features that allow users to mark transactions as completed or uncompleted, list transactions based on their completion status, and find transactions by customer name. These features streamline the process of managing rental transactions, ensuring accurate tracking and easy retrieval of relevant data. + +#### **1. Overview of Features** + +- **Marking/Unmarking Transactions as Completed:** + - **Mark Completed:** Allows users to mark a specific transaction as completed, indicating that the rental process has been finalized. + - **Unmark Completed:** Enables users to revert a transaction's status from completed to uncompleted if needed. + +- **Listing Transactions:** + - **List Completed Transactions:** Displays all transactions that have been marked as completed. + - **List Uncompleted Transactions:** Shows all transactions that are still pending completion. + +- **Finding Transactions by Customer Name:** + - Facilitates the retrieval of all transactions associated with a specific customer, enabling efficient management and review. + +- **Add Users to the database** + - Allows transactions involving customers and keep track of user information. + +- **Listing Cars and status** + - Displays all the cars in the company and see if they are rented out or not. + +- **File saving** + - Saves all data regarding transactions, cars and customer preventing data loss. + - Real-time updates after every command. + - Loads file upon start of program without any hassle. + +#### **2. Design & Implementation** + +The implementation of these features is encapsulated within the `TransactionList` class, which manages all transaction-related operations. Below is a detailed breakdown of the design and implementation strategies employed. + +##### **a. Class-Level Design** + +**`Transaction` Class:** + +- **Responsibilities:** + - Represents a single car rental transaction, encapsulating details like transaction ID, car license plate, customer, rental duration, start and end dates, and completion status. + - Provides methods to manipulate transaction data, including setting a transaction ID, marking the transaction as completed, and generating string representations for display and file storage. + +- **Key Methods:** + - `isValidTxId(String transactionId)`: Validates the format of a transaction ID. + - `setTransactionId(String transactionId)`: Sets a unique transaction ID. + - `setCompleted(boolean completed)`: Marks the transaction as completed or incomplete. + - `toString()`: Provides a formatted string for displaying transaction details. + - `toFileString()`: Outputs a file-friendly string for persisting transaction data. + +- **Design Considerations:** + - **Data Integrity**: Ensures that the end date is calculated automatically based on the start date and duration, minimizing the risk of manual input errors. + - **Encapsulation**: Private fields with controlled access ensure data consistency, allowing only authorized modifications. + - **Readability**: The `formatDuration()` and `toString()` methods provide user-friendly representations of transaction data, making it easy to read and understand both on-screen and in storage. + +**`TransactionList` Class:** + +- **Responsibilities:** + - Manage the list of all transactions. + - Provide functionalities to add, remove, update, and retrieve transactions based on various criteria. + +- **Key Methods:** + - `markCompletedByTxId(String txId)`: Marks a transaction as completed. + - `unmarkCompletedByTxId(String txId)`: Marks a transaction as uncompleted. + - `printCompletedTransactions()`: Lists all completed transactions. + - `printUncompletedTransactions()`: Lists all uncompleted transactions. + - `findTxsByCustomer(String customer)`: Retrieves transactions associated with a specific customer. + +**Design Considerations:** + +- **Encapsulation:** The `transactionList` is maintained as a private static `ArrayList`, ensuring controlled access through public methods. +- **Data Integrity:** Assertions and exception handling are employed to maintain the integrity of transaction data during operations. + + +**`TransactionFile` Class:** + +- **Responsibilities:** + - Loads transaction data from transactionData.txt upon start of program. + - Updates transaction data in the file after every command. + +- **Key Methods:** + - `loadTransactionData()`: Loads transaction data from transactionData.txt to TransactionList. + - `scanLineAndAddTransaction()`: Scans the current line in the file and add it into TransactionList. + - `addTransactionWithParameters` : Create a Transaction object based on a list of parameters. + - `updateTransactionDataFile()` : Reads TransactionList and update TransactionList.txt accordingly. + +- **Design Considerations** + - `User Experience` : Users should not need to update the files manually after each command and should be automated + instead. + - `Maintainability` : When new parameters are introduced, only 3 methods and less than 10 lines of code + needs to be updated. + +--- ## Product scope ### Target user profile -{Describe the target user profile} +Our targeted users are car rental companies which handles rental transaction on a daily basis. More specifically , +CliRental will be targeted at workers working at the front of house , handling customers +and recording transactions. ### Value proposition -{Describe the value proposition: what problem does it solve?} +Our product, CliRental aims to allow quick adding of transactional data when renting out a car in a car rental +company/store. It also allows for the staff of the rental company to filter through the massive amount of transactions, +finding the transaction they are looking for easily with multiple filters. ## 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| +Here's a breakdown of the user stories divided into the versions as requested: + +--- + +### Version 1.0 + +| Version | As a ... | I want to ... | So that I can ... | +|---------|--------------------------------|----------------------------------------|-------------------------------------------------| +| v1.0 | car rental front-desk employee | know the status of all the cars | inform customers about availability | +| v1.0 | car rental front-desk employee | add customer details to our database | keep records for future transactions | +| v1.0 | car rental company manager | get an overview of all transactions | gauge how well the company is doing | +| v1.0 | car rental front-desk employee | add a new transaction record | keep track of transaction details | +| v1.0 | car rental front-desk employee | add new cars to the car database | have a wider range of cars to offer to customers| -## Non-Functional Requirements +--- -{Give non-functional requirements} +### Version 2.0 & 2.1 + +| Version | As a ... | I want to ... | So that I can ... | +|---------|-------------------------------|---------------------------------------------------------|----------------------------------------------------------| +| v2.0 | car rental front-desk employee | save all my data | ensure information will never be lost | +| v2.0 | forgetful car rental employee | view a help page containing all the commands | refer to it whenever I forget any commands | +| v2.0 | car rental administrator | search for all past transactions involving a specific customer | assess their rental history | +| v2.0 | car rental administrator | list all uncompleted transactions | follow up on active rentals | +| v2.0 | car rental administrator | list all completed transactions | review records of all past rentals | +| v2.0 | car rental administrator | remove all customers or transactions | clear out old data when no longer needed | +| v2.0 | car rental administrator | remove individual transactions | remove any unnecessary records individually | +| v2.0 | car rental company manager | list out all past and completed transactions | gauge how well the company is doing | +| v2.0 | car rental fleet manager | make changes to the car fleet | add or remove cars from the inventory | +| v2.0 | car rental fleet manager | categorize cars by their price range | allow customers to choose based on budget | + + + +| Version | As a ... | I want to ... | So that I can ... | +|---------|--------------------------------|----------------------------------------------------|---------------------------------------------------------| +| v2.1 | car rental fleet manager | add a new car to the list of available cars | provide customers with a wider selection | +| v2.1 | car rental fleet manager | list all available cars | quickly check which cars can be rented | +| v2.1 | car rental fleet manager | list all rented-out cars | view which cars are currently in use | +| v2.1 | car rental fleet manager | set a rental duration for each transaction | clarify each rental's timeline | +| v2.1 | car rental fleet manager | add or remove cars to match demand | manage the car inventory efficiently | +| v2.1 | car rental front-desk employee | complete transactions after the customer returns the car | finalize each rental transaction | +| v2.1 | car rental front-desk employee | save customer information | reuse information for returning customers without re-entry | +| v2.1 | car rental front-desk employee | add a new transaction record to the database | keep track of transaction details | +| v2.1 | car rental front-desk employee | remove a customer from the system | keep the customer list up-to-date | +| v2.1 | car rental front-desk employee | unmark a completed transaction | correct transaction records when needed | +| v2.1 | car rental maintenance staff | add information to a car | view and edit the details of a car | +| v2.1 | forgetful user | view a user guide | have a quick summary of important commands | + + +1. User can type fast, and prefer typing to mouse/voice commands. +2. Program can run on Mainstream OS. ## Glossary -* *glossary item* - Definition +* Mainstream OS: Windows, Linux, Unix, MacOS ## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +`Test case 1` : + +`details` : upon start of program, the data files should be created in your local computer at the same level +as your jar file.
+`check` : A folder called data should be created if it did not already exist. carData.txt, customerData.txt, +transactionData.txt should be created in the data folder as well if it does not already exist. + +`Test case 2` : + +`details` : Adding a user using `add-user /u john /a 30 /c 12345678`
+`check` : The command should return an error message saying that the format of contact number is wrong. Using +`87777777` for the contact number should allow you to add the user successfully now. + +`Test case 3` : + +`details` : Adding a user using `add-user /u bill /a 16 /c 87777777`
+`check` : The command should return an error message saying that the age is illegal to drive which is true +for most countries at age of 16. Our legal age is 18 thus changing the age to 18 and above but maximally 100 years old +will work now. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..c59879d970 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,9 @@ -# Duke +# CliRental -{Give product intro here} +Welcome to CliRental, a perfect desktop app dedicate to managing a car rental business involves handling large volumes +of data, making traditional pen-and-paper methods inefficient for managers. This app is designed specifically for car +rental managers, offering a Command Line Interface (CLI) that enables quick data entry and efficient tracking of +customers, cars, and transactions. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UML/AddCustomerSequence.puml b/docs/UML/AddCustomerSequence.puml new file mode 100644 index 0000000000..7d08113033 --- /dev/null +++ b/docs/UML/AddCustomerSequence.puml @@ -0,0 +1,72 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber +mainframe sd add-user +participant ":Parser" as Parser +participant ":CustomerParser" as CustomerParser +participant ":Customer" as Customer +participant ":CustomerException" as CustomerException +participant ":CustomerList" as CustomerList + +activate Parser +Parser -> CustomerParser : parseIntoCustomer(userInput : String) +activate CustomerParser + CustomerParser -> CustomerParser : isValidSequence(parameters : String[], userInput : String) + activate CustomerParser + loop parameter of parameters + opt userInput does not contain parameter + CustomerParser <-- CustomerParser : return false + end opt + end loop + + loop iterate from 1 to (length of parameters - 1) + opt parameters not in sequence + CustomerParser <-- CustomerParser : return false + end opt + end loop + + CustomerParser --> CustomerParser : return true + deactivate CustomerParser + + alt isValidSequence + CustomerParser -> CustomerParser : parseIntoCustomer(userInput : String) + activate CustomerParser + CustomerParser -> CustomerParser :parseParameterContents(String[] parameters, String userInput) + activate CustomerParser + CustomerParser --> CustomerParser : return contents + deactivate CustomerParser + create Customer + + CustomerParser -> Customer : + activate Customer + CustomerParser <-- Customer : return + deactivate Customer + CustomerParser --> CustomerParser : return new Customer + deactivate CustomerParser + else + CustomerParser -> CustomerException : addCustomerException() + activate CustomerException + note right of CustomerException + The static method creates + a new CustomerException + end note + CustomerException --> CustomerParser : return new CustomerException + deactivate CustomerException + Parser <-- CustomerParser : throw new CustomerException + end alt + Parser <-- CustomerParser : return new Customer + +deactivate CustomerParser + +Parser -> CustomerList : addCustomer(customer : Customer) +note right of CustomerList + Adds the new Customer to a ArrayList + and prints information to the user +end note +activate CustomerList + Parser <-- CustomerList : return +deactivate CustomerList +deactivate Parser +hide footbox +@enduml \ No newline at end of file diff --git a/docs/UML/CLiRental.puml b/docs/UML/CLiRental.puml new file mode 100644 index 0000000000..0144b86341 --- /dev/null +++ b/docs/UML/CLiRental.puml @@ -0,0 +1,38 @@ +@startuml +'https://plantuml.com/class-diagram + + + +class CliRental{ + main() : void + getName() : void + printGreeting() : void +} + +class Customer{ + <> NUMBER_OF_PARAMETERS : int = 4 + username : string + age : int + contactNumber : string + + Customer(string, int, string) + getUsername() : string + getContactNumber() : string + getAge() : int + setUsername(string) : Void + setAge(int) : void + setContactNumber(String contactNumber) : void + toString() : string + toFileString() : string +} + +class CustomerList{ + addCustomer(Customer) : void + addCustomerWithoutPrintingInfo(Customer) : void + removeCustomer(String) : void + getCustomers() : ArrayList + printCustomers() : void + customerListToFileString() : string +} +hide footbox +@enduml \ No newline at end of file diff --git a/docs/UML/CarFileLoader.puml b/docs/UML/CarFileLoader.puml new file mode 100644 index 0000000000..d71db2d4a6 --- /dev/null +++ b/docs/UML/CarFileLoader.puml @@ -0,0 +1,93 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber +participant ":FileHandler" as FileHandler +participant ":CarFile" as CarFile +participant ":Scanner" as scanner +participant "errorLines \n: ArrayList" as ArrayList +participant ":Car" as Car +participant ":CarList" as CarList + + -> FileHandler: createAndLoadFiles() +activate FileHandler + FileHandler -> CarFile : createCarFileIfNotExist() + activate CarFile + opt File does not exist + CarFile -> FileHandler : createNewFile() + FileHandler --> CarFile : return + end opt + CarFile --> FileHandler : return + deactivate CarFile + +FileHandler -> CarFile: loadCarDataIfExist() +activate CarFile + opt File exists + CarFile -> CarFile : loadCarData() + activate CarFile + + + create scanner + CarFile -> scanner : new scanner() + activate scanner + scanner --> CarFile : return + deactivate scanner + + create ArrayList + CarFile -> ArrayList: new ArrayList() + activate ArrayList + ArrayList --> CarFile : return + deactivate ArrayList + + loop For each line in file + CarFile -> scanner : hasNext() + activate scanner + opt next line exists + CarFile -> CarFile: scanLineAndAddCar() + activate CarFile + CarFile -> scanner : nextLine() + activate scanner + alt parameters.length != Car.NUMBER_OF_PARAMETER + CarFile -> ArrayList : add(line : int) + activate ArrayList + ArrayList --> CarFile : return + deactivate ArrayList + else + CarFile -> CarFile : addCarWithParameters(...) + opt parameters are correctly formatted + activate CarFile + CarFile -> Car : new Car() + activate Car + Car --> CarFile : return + deactivate Car + CarFile -> CarList : addCarWithoutPrintingInfo(car : Car) + activate CarList + CarList -> CarList : add(car : Car) + activate CarList + CarList --> CarList : return + deactivate CarList + CarList --> CarFile : return + deactivate CarList + end opt + CarFile --> CarFile : return + deactivate CarFile + end alt + scanner --> CarFile : return + deactivate scanner + CarFile --> CarFile : return + deactivate CarFile + end opt + scanner --> CarFile : return + deactivate scanner + end loop + + CarFile --> CarFile : return + deactivate CarFile + end opt +CarFile --> FileHandler : return +deactivate CarFile + +<-- FileHandler : return +deactivate FileHandler +hide footbox +@enduml \ No newline at end of file diff --git a/docs/UML/FileHandlerClassDiagram.puml b/docs/UML/FileHandlerClassDiagram.puml new file mode 100644 index 0000000000..6e8b87528a --- /dev/null +++ b/docs/UML/FileHandlerClassDiagram.puml @@ -0,0 +1,84 @@ +@startuml +'https://plantuml.com/class-diagram + +class FileHandler { + {field}{static} -DIR_NAME : String = "data" + {field}{static} -DATA_DIR : File + {method}{static} +getDataDir() : File + {method}{static} +getCarFile() : CarFile + {method}{static} +getCustomerFile() : CustomerFile + {method}{static} +getTransactionFile() : TransactionFile + {method}{static} +getDirName() : String + {method}{static} +createAndLoadFiles() : void + {method}{static} +createNewFile(filename : File) : void + {method}{static} +createFolderIfNotExist() : void + {method}{static} +updateFiles() : void + {method}{static} -createFolder() : void + {method}{static} +containEmptyParameter(parameters : String[]) : boolean +} + +class CarFile { + {field} -carDataFileName : String + {field} -carDataFilePath : String + {field} -carDataFile : File + {method} +getCarDataFilename() : String + {method} +addCarWithParameters(parameters : String[], errorLines : ArrayList, line : int) : void + {method} +updateCarDataFile() : void + {method} +createCarFileIfNotExist() : void + {method} +deleteCarFileIfExist() : void + {method} +loadCarData() : void + {method} +scanLineAndAddCar(scanner : Scanner, errorLines : ArrayList, line : int) : void + {method} +loadCarDataIfExist() : void + {method} +getAbsolutePath() : String +} + +class CustomerFile { + {field} -customerDataFileName : String + {field} -customerDataFilePath : String + {field} -customerDataFile : File + {method} +getCustomerDataFilename() : String + {method} +createCustomerFileIfNotExist() : void + {method} +loadCustomerData() : void + {method} +scanLineAndAddCustomer(scanner : Scanner, errorLines : ArrayList, line : int) : void + {method} +updateCustomerDataFile() : void + {method} +addCustomerWithParameters(parameters : String[], errorLines : ArrayList, line : int) : void + {method} +loadCustomerDataIfExist() : void + {method} +getAbsolutePath() : String +} + +class TransactionFile { + {field} -transactionDataFileName : String + {field} -transactionDataFilePath : String + {field} -transactionDataFile : File + {method} +getTransactionDataFilename() : String + {method} +createTransactionFileIfNotExist() : void + {method} +loadTransactionData() : void + {method} +scanLineAndAddTransaction(scanner : Scanner, errorLines : ArrayList, line : int) : void + {method} +addTransactionWithParameters(parameters : String[], errorLines : ArrayList, line : int) : void + {method} +loadTransactionDataIfExist() : void + {method} +updateTransactionDataFile() : void + {method} +getAbsolutePath() : String +} + +FileHandler --> "1" CarFile : "creates" +FileHandler --> "1" CustomerFile : "creates" +FileHandler --> "1" TransactionFile : "creates" + +CarFile ..> CarList : "edits" +CarFile ..> Car : "creates" +CarFile ..> FileHandler + +CustomerFile ..> CustomerList : "edits" +CustomerFile ..> FileHandler +CustomerFile ..> Customer : "creates" + +TransactionFile ..> TransactionList : "edits" +TransactionFile ..> Transaction : "creates" +TransactionFile ..> FileHandler + +note right of FileHandler + All classes other than FileHandler , TransactionFile , + CarFile , CustomerFile will be empty for sake of simplicity + and size of the class diagram +end note +@enduml \ No newline at end of file diff --git a/docs/UML/ListCarsSequence.puml b/docs/UML/ListCarsSequence.puml new file mode 100644 index 0000000000..a0263ee7c5 --- /dev/null +++ b/docs/UML/ListCarsSequence.puml @@ -0,0 +1,49 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber + +participant ":Parser" as Parser +participant ":CarList" as CarList +participant ":Car" as Car + +activate Parser +Parser -> CarList : printCarList() +activate CarList +alt carsList is not empty + loop For each Car in CarList + CarList -> Car : getModel() + activate Car + Car --> CarList : return model + deactivate Car + CarList -> Car : getLicensePlateNumber() + activate Car + Car --> CarList : return licensePlateNumber + deactivate Car + CarList -> CarList : formatPriceToTwoDp(price) + activate CarList + CarList -> Car : getPrice() + activate Car + Car --> CarList : return price + deactivate Car + CarList --> CarList : return formattedPrice + deactivate CarList + CarList -> Car : getRentedStatus() + activate Car + Car --> CarList : return rentedStatus + deactivate Car + CarList -> Car : getExpensiveStatus() + activate Car + Car --> CarList : return expensiveStatus + deactivate Car + CarList -> CarList : getMedianPrice() + activate CarList + CarList --> CarList : return median car price + deactivate CarList + end loop +end alt +Parser <-- CarList : return +deactivate CarList +deactivate Parser +hide footbox +@enduml \ No newline at end of file diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..b8a1511b27 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,579 @@ -# User Guide +# CliRental User Guide ## Introduction -{Give a product intro} +Clirental is a CLI-based application that allows car rental companies to track their customers, cars, and rental transactions. -## Quick Start +This application is useful to car rental companies that provide day-to-day rental service to their customers in helping them to manage their car fleet, customer information and rental transactions. + +Summary of Contents: +- [Quick start](#quick-start) +- [File Saving](#file-saving) +- [Features](#features) + - [Adding a user: `add-user`](#adding-a-user-to-the-database-add-user) + - [Adding a car: `add-car`](#adding-a-car-add-car) + - [Removing a user: `remove-user`](#removing-a-user-from-the-database-remove-user) + - [Removing all users: `remove-all-users`](#removing-all-users-from-the-database-remove-all-users) + - [Removing a car: `remove-car`](#removing-a-car-remove-car) + - [Removing all cars: `remove-all-cars`](#removing-all-cars-remove-all-cars) + - [Listing all users: `list-users`](#listing-all-cars-list-cars) + - [Listing all cars: `list-cars`](#listing-all-cars-list-cars) + - [Listing all rented-out cars: `list-rented`](#listing-all-rented-out-cars-list-rented) + - [Listing all available cars: `list-available`](#listing-all-available-cars-list-available) + - [Updating rental status of cars](#updating-rental-status-of-car) + - [Adding a transaction: `add-tx`](#adding-a-transaction-add-tx) + - [Removing a transaction: `remove-tx`](#removing-a-transaction-remove-tx) + - [Removing all transactions: `remove-all-txs`](#removing-all-transactions-remove-all-txs) + - [Listing all transactions: `list-txs`](#listing-all-transactions-list-txs) + - [Listing all completed transactions: `list-txs-completed`](#listing-all-completed-transactions-list-txs-completed) + - [Listing all uncompleted transactions: `list-txs-uncompleted`](#listing-all-uncompleted-transactions-list-txs-uncompleted) + - [Marking a transaction as complete: `mark-tx`](#marking-a-transaction-as-complete-mark-tx) + - [Unmarking a transaction as incomplete: `unmark-tx`](#unmarking-a-transaction-as-incomplete-unmark-tx) + - [Finding transactions under a Customer: `find-txs-by-customer`](#find-transactions-under-a-customer-find-txs-by-customer) + - [Displaying the help page: `help`](#displaying-the-help-page-help) + - [Exiting the program: `exit`](#exiting-the-program-exit) +- [FAQ](#faq) +- [Command Summary](#command-summary) -{Give steps to get started quickly} + +## Quick Start 1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `CliRental` from [here](https://github.com/AY2425S1-CS2113-T11-3/tp/releases). +3. Copy the jar file into an empty folder. +4. Open your terminal and navigate to the folder the jar file is placed in. +5. Run java -jar CliRental.jar and you can start using right away. + +## Features + +--- +### File Saving + +`Customer`, `Transaction`, and `Car` data will be saved in their respective files under the `data` directory. + + +#### `IMPORTANT NOTE / DISCLAIMER: ` + +* The file saving feature does not include the functionality of being able to add/edit data by editing the text files. +Please add/edit data via the command line using the commands given. +* Users must ensure any additional data follows the correct format. + +`If user do not follow instructions, following additional measures are placed.` +* Corrupted data will be flagged upon start of program , highlighting the rows of data which are wrong. Please correct +them or the corrupted lines will be flushed from the data files upon the `first correct command` given by the user. + + +**Filenames:** + +* `Car data`: `carData.txt` +* `Customer data`: `customerData.txt` +* `Transaction data`: `transactionData.txt` + +Format : +* `Car data` : `CAR MODEL | LICENSE PLATE | PRICE | RENTED | EXPENSIVE` +* `Customer data` : `NAME | AGE | PHONE NUMBER` +* `Transaction data` : `TRANSACTION ID | LICENSE PLATE | CUSTOMER NAME | RENTAL DURATION(DAYS) | +RENTAL START DATE | COMPLETED` + +Types : +* `Car data` : `STRING | STRING | DOUBLE | BOOLEAN | BOOLEAN` +* `Customer data` : `STRING | INT | STRING` +* `Transaction data` : `STRING | STRING | STRING | INT | LOCALDATE | BOOLEAN` + +**Example:** + +* `Car data` : `Toyota Corolla | SGM4932K | 120.0 | false | false` +* `Customer data` : `John | 22 | 90907638` +* `Transaction data` : `TX1 | SGM4932K | John | 30 | 17-10-2024 | false` + +Others : + +* Any string `not "true"` will be treated as `false` when it is placed in a BOOLEAN section. +* Loading of data follows constraints mentioned in the respective `add` commands. + +--- +## Features + +### Adding a User to the Database: `add-user` + +Adds a customer to the list of customers tracked by the car rental application. + +**Format:** `add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER]` + +* `CUSTOMER_NAME` : `STRING`. +* `AGE` : `INT` + * age should be >= 18 and <= 100. +* `CONTACT_NUMBER` : `STRING` + * `[8 DIGITS AND STARTS WITH 8 or 9]` + * `No space between digits` + * `E.g. 95382572` +* `/u` , `/a` , `/c` must be in sequence. + +**Example of usage:** +`add-user /u John /a 18 /c 95382572` + +**Sample Response:** +``` +add-user /u John /a 18 /c 95382572 +Customer added +Customer name : John +Age : 18 +Contact Number : 95382572 +``` +--- +### Removing a User from the Database: `remove-user` + +Removes a customer from the customer list. + +**Format:** `remove-user /u [CUSTOMER_NAME]` + +* `CUSTOMER_NAME` : `STRING`. +* `CUSTOMER_NAME` must match an existing customer in the database. +* `CUSTOMER_NAME` is not case-sensitive. 'John' and 'john' mean the same customer. + +**Example of usage:** +`remove-user /u John` + +**Sample Response:** +``` +John has been removed from customer list +``` +--- +### Removing all Users from the Database: `remove-all-users` + +Removes all customers from the customer list. + +**Format:** `remove-all-users` + +**Example of usage:** +`remove-all-user` + +**Sample Response:** +``` +All customers removed!!! +``` +--- +### Listing All Users: `list-users` + +Lists all customers in the customer list in this format for each customer: +Customer Name | Age | Contact Number + +**Format:** `list-users` + +**Sample Output:** +``` +Here are all the customers: +1) John | 18 | 95382572 +2) Alice | 25 | 81234567 +``` +If the list is **empty**: + +``` +Customer list is empty. + +``` +--- +### Adding a Car: `add-car` + +Adds a car to the car list. + +**Format:** `add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE]` + +- `/n`, `/c` and `/p` identifiers **must be** in the correct order. +- `LICENSE_PLATE_NUMBER` **must be** in the following format: `SXX####X`, where + - `X` is any letter from **A to Z**. + - `####` is any number from **1 to 9999**, **without any leading zeroes**. + - Starts with the letter **S**. +- `PRICE` must be a **non-negative, numeric value**. +- `PRICE` cannot exceed **10000**. +- `$` character not required for `PRICE`. + +**Example:** +`add-car /n Honda Civic /c SGE1234X /p 10000` + +**Sample output:** +``` +Car added to list +Car details: +Honda Civic | SGE1234X | $10000.00 | Available | Affordable | Median price: 10000.0 +``` +--- +### Removing a Car: `remove-car` + +Removes a car from the fleet based on the car's unique ID. + +**Format:** `remove-car /i [LICENSE_PLATE_NUMBER]` + +- `/i` identifier specifies the license plate number belonging to the car that is to be removed. +- `LICENSE_PLATE_NUMBER` must match an existing car in the database. + +**Example:** +`remove-car /i SGE1234X` + +**Sample output:** +``` +Car with license plate SGE1234X removed from list. +``` + +If the `LICENSE_PLATE_NUMBER` is not found: +``` +No car found with license plate [SGE1234X] +``` +--- +### Removing all Cars: `remove-all-cars` + +Removes all cars from the fleet. + +**Format:** `remove-all-cars` + +**Sample output:** +``` +All cars removed!!! +``` +### Listing All Cars: `list-cars` + +Lists all the cars owned by the company, sorted according to the price of renting the car for a day. +The format for each car in the list is: +Car Model | License PLate Number | Price of Rental (Per Day) | Availability (for Rental) | Price Category | +Median Price of Cars in Fleet + +**Format:** `list-cars` + +**Sample Output:** +``` +Here are the current cars in the company: +1) Honda Civic | SGE1234X | $1000.00 | Available | Affordable | Median price: 1000.0 +2) Toyota Vios | SKP890C | $2000.00 | Available | Expensive | Median price: 1000.0 +``` +If the list is **empty**: + +``` +Oops!! Car list is empty... +Use command to add a new car. +``` +--- +### Listing All Rented Out Cars: `list-rented` + +Lists all the cars that are currently rented out in this format for each car: +Car Model | License Plate Number | Price of Rental (Per day) + +**Format:** `list-rented` + +**Sample output:** +``` +Here are all the rented-out cars: +1) Honda Civic | SGE1234X | $100.00 +2) Toyota Camry | SKL4567M | $200.00 +3) Nissan Latio | SFT1190A | $300.00 +``` + +If the list is **empty**: +``` +No cars currently rented out... +``` +--- +### Listing All Available Cars: `list-available` + +Lists all available cars in the company in this format for each car: +Car Model | License Plate Number | Price of Rental (Per day) + +**Format:** `list-available` -## Features +**Sample output:** +``` +Here are all the available cars: +1) Mitsubishi Attrage | SGP7877N | $1500.00 +2) Honda Vezel | SLK9945F | $3400.00 +``` -{Give detailed description of each feature} +If the list is **empty**: +``` +There are no available cars at the moment... +``` +--- +### Updating Rental Status of Car -### Adding a todo: `todo` -Adds a new item to the list of todo items. +There is no need to manually update the rental status of a car. +The status will be updated automatically when a transaction record is: -Format: `todo n/TODO_NAME d/DEADLINE` +- Added +- Removed +- Marked as completed +- Marked as not completed -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +### Adding a Transaction: `add-tx` -Example of usage: +Adds a new rental transaction to the system. -`todo n/Write the rest of the User Guide d/next week` +To add transaction bearing either an existing license plate number and/or customer name, all previous transactions containing either both or one of the parameter must be either marked as completed or be removed from the transaction list. +It means that when a customer and a car are in a transaction, either that customer or that car cannot involve in other transactions, including transactions with different time periods (start date - end date), unless that transaction is marked as completed or be removed from the transaction list. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +**Format:** `add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: dd-MM-yyyy]` +- `/c` identifier specifies the license plate number of the car the customer wants to rent. +- `LICENSE_PLATE_NUMBER` must match an existing car in the database. This is unique as the program will not allow 2 cars to have the same license plate number. + + +- `/u` identifier specifies the name of the customer. +- `CUSTOMER_NAME` must match an existing customer in the database. This is unique as the program will not allow 2 customers to have the same name. + + +- `/d` identifier specifies the duration of the rental in days. +- `DURATION` must be an integer between 1 and 365 (inclusive). This allows the rental companies to handle rental transactions from 1 day to 365 days (a year). + + +- `/s` identifier specifies the start date of the rental. +- `START_DATE` is in the format of [dd-MM-yyyy], accepting integers input only. It must be a valid date in the calendar. + +**Example:** +`add-tx /c SZZ1579D /u John /d 15 /s 11-05-2025` + +**Sample Response:** +``` +Transaction added: +[ ] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +``` +--- +### Removing a Transaction: `remove-tx` + +Removes a specific rental transaction from the system based on the transaction ID. + +**Format:** `remove-tx /t [TRANSACTION_ID]` + +- `/t` identifier specifies the transaction ID to be removed. +- `TRANSACTION_ID` must begin with "TX" and match an existing transaction in the system. + +**Example:** +`remove-tx /t TX1` + +**Sample output:** + +``` +Transaction deleted: [ ] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +``` + + +If the `TRANSACTION_ID` is not found: +``` +Transaction not found +``` +--- + +### Removing All Transactions: `remove-all-txs` + +Removes all transactions from the system. + +**Format:** `remove-all-txs` + +**Sample output:** +``` +All transactions removed!!! +``` + +### Listing All Transactions: `list-txs` + +Displays all transactions stored in the system in this format for each transaction: +[ ] Transaction ID | License Plate Number | Customer Name | Duration of Rental (in day(s)) +Start Date | End Date +where +[ ] indicates that the transaction is ongoing or has been marked as uncompleted or +[X] indicates that the transaction is completed or has been marked as completed + +**Format:** `list-txs` + +**Sample output:** +``` +Here are all the transactions: +1) [ ] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +2) [X] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +``` + +If the list is **empty**: +``` +No transaction available. +``` + +--- +### Listing All Completed Transactions: `list-txs-completed` + +Displays all transactions that are marked as completed list in this format for each transaction: +[X] Transaction ID | License Plate Number | Customer Name | Duration of Rental (in day(s)) +Start Date | End Date +where [X] indicates that the transaction is completed or has been marked as completed + +**Format:** `list-txs-completed` + +**Sample output:** +``` +Here are all the transactions: +1) [X] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +2) [X] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +``` + +If the list is **empty**: +``` +Here are all the completed transactions: +No completed transaction available. +``` +### Listing All Uncompleted Transactions: `list-txs-uncompleted` + +Displays all transactions that are ongoing or are marked as uncompleted list in this format for each transaction: +[ ] Transaction ID | License Plate Number | Customer Name | Duration of Rental (in day(s)) +Start Date | End Date +where [ ] indicates that the transaction is ongoing or has been marked as uncompleted + +**Format:** `list-txs-uncompleted` + +**Sample output:** +``` +Here are all the transactions: +1) [ ] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +2) [ ] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +``` + +If the list is **empty**: +``` +Here are all the uncompleted transactions: +No uncompleted transaction available. +``` +### Marking a Transaction as Complete: `mark-tx` + +Marks a rental transaction as completed, indicating that the transaction is finalized. + +**Format:** `mark-tx /t [TRANSACTION_ID]` + +- `/t` identifier specifies the transaction ID to be marked as completed. +- `TRANSACTION_ID` must match an existing transaction in the system. + +**Example:** +`mark-tx /t TX1` + +**Sample output:** +``` +Transaction completed: [X] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +``` + +If the `TRANSACTION_ID` is not found: +``` +Transaction not found +``` +--- +### Unmarking a Transaction as Incomplete: `unmark-tx` + +Unmarks a rental transaction, indicating it is not yet completed. + +**Format:** `unmark-tx /t [TRANSACTION_ID]` + +- `/t` identifier specifies the transaction ID to be unmarked. +- `TRANSACTION_ID` must match an existing transaction in the system. + +**Example:** +`unmark-tx /t TX1` + +**Sample output:** +``` +Transaction set uncompleted: [ ] TX1 | SKL4567M | Alice | 7 days +Start Date: 15-12-2024 | End Date: 22-12-2024 +``` + +If the `TRANSACTION_ID` is not found: +``` +Transaction not found +``` +### Find Transactions under a Customer: `find-txs-by-customer` + +Finds all the transactions under a customer using the customer name as the search term. +The transactions are displayed in the same format as list-txs. + +**Format:** `find-txs-by-customer /u [CUSTOMER_NAME]` + +- `/u` identifier specifies the Customer Name. + +- `CUSTOMER_NAME` must partially match an existing customer in the customer list. This is not case-sensitive, 'john' and 'John' is the same, and 'john' input can search for the user 'John Doe', which contains 'John'. + +**Example:** +`find-txs-by-customer /u John` + +**Sample output:** +``` +Transaction(s) by John found: +[X] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +[ ] TX3 | SFT1190A | John | 4 days +Start Date: 22-12-2024 | End Date: 26-12-2024 +``` + +If the customer has no rental transaction: +``` +Transaction(s) by John found: +None +``` +### Displaying the help page: `help` + +Displays a help page containing all the commands, together with +its respective format and description. + +Format: `help` + +### Exiting the program: `exit` + +Exits the program and saves all data to the respective data text files. + +Format: `exit` + +--- ## FAQ -**Q**: How do I transfer my data to another computer? +`No questions to answer for now!!!` + +# Command Summary + +**`Customer` related commands:** + +| Action | Format | +|:-------------------------:|------------------------------------------------------------| +| **Add** customer | `add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER]` | +| **Remove** customer | `remove-user /u [CUSTOMER_NAME]` | +| **Remove all** customers | `remove-all-users` | +| **List all** customers | `list-users` | + +**`Car` related commands:** + +| Action | Format | +|:-----------------------:|----------------------------------------------------------------| +| **Add** car | `add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE]` | +| **Remove** car | `remove-car /i [LICENSE_PLATE_NUMBER]` | +| **Remove all** cars | `remove-all-cars` | +| **List all** cars | `list-cars` | +| **List rented** cars | `list-rented` | +| **List available** cars | `list-available` | -**A**: {your answer here} +**`Transaction` related commands:** -## Command Summary +| Action | Format | +|:--------------------------------------:|-------------------------------------------------------------------------------------------------| +| **Add** transaction | `add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: dd-MM-yyyy]` | +| **Remove** transaction | `remove-tx /t [TRANSACTION_ID]` | +| **Remove all** transactions | `remove-all-txs` | +| **List all** transactions | `list-txs` | +| **Mark** transaction as **complete** | `mark-tx /t [TRANSACTION_ID]` | +| **Mark** transaction as **incomplete** | `unmark-tx /t [TRANSACTION_ID]` | +| **List completed** transactions | `list-txs-completed` | +| **List uncompleted** transactions | `list-txs-uncompleted` | +| **Find** transactions **by customer** | `find-txs-by-customer /u [CUSTOMER_NAME]` | -{Give a 'cheat sheet' of commands here} +**Other useful commands:** -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Action | Format | +|:------------------:|--------| +| Show **help** page | `help` | +| **Exit** program | `exit` | diff --git a/docs/diagrams/KennethRemoveCustomerDiagram.puml b/docs/diagrams/KennethRemoveCustomerDiagram.puml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/diagrams/Overall.puml b/docs/diagrams/Overall.puml new file mode 100644 index 0000000000..0f1ecbf812 --- /dev/null +++ b/docs/diagrams/Overall.puml @@ -0,0 +1,105 @@ +@startuml +'https://plantuml.com/class-diagram + + +class CliRental { + +} + +class Parser { + +} +class CarParser{ + +} + +class TransactionParser{ + +} + +class Car { + +} + +class Transaction { + +} + +class CarList { + +} + +class TransactionList { + +} + +class Customer { + +} + +class CustomerList { + +} + +class CustomerParser { + +} + +class CustomerException { + +} + +class FileHandler { + +} + +class CarFile { + +} + +class CustomerFile { + +} + +class TransactionFile { + +} + +FileHandler --> "1" CarFile : "create" +FileHandler --> "1" CustomerFile : "create" +FileHandler --> "1" TransactionFile : "create" + +CarFile ..> CarList : "edit" +CarFile ..> Car : "create" +CarFile ..> FileHandler : "uses" + +CustomerFile ..> CustomerList : "edit" +CustomerFile ..> FileHandler : "uses" +CustomerFile ..> Customer : "create" + +TransactionFile ..> TransactionList : "edit" +TransactionFile ..> Transaction : "create" +TransactionFile ..> FileHandler : "uses" + +CliRental ..> Parser +CliRental ..> CustomerException + +Parser ..> CarParser +Parser ..> TransactionParser +Parser ..> CustomerParser + +CarParser -- Car: parse into Car object > +CarParser ..> CarList + +TransactionParser -- Transaction : parse into Transaction object > +TransactionParser ..> TransactionList + +CarList *-- "*" Car +TransactionList *-- "*" Transaction + +CustomerParser ..> Customer : "creates" +CustomerParser ..> CustomerException : "throws" + +CustomerList *-- "*" Customer : "contains" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/RyanAddCarDiagram.puml b/docs/diagrams/RyanAddCarDiagram.puml new file mode 100644 index 0000000000..2579f89e89 --- /dev/null +++ b/docs/diagrams/RyanAddCarDiagram.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!define BOX_COLOR #FFFFFF + +hide footbox +skinparam sequenceMessageAlign center + +box add-car\n BOX_COLOR +participant "CliRental" as CliRental +participant "<>\nParser" as Parser +participant "<>\nCarParser" as CarParser +participant "car :Car" as Car +participant "<>\nCarList" as CarList +end box + +CliRental -> Parser ++ : parse user input +Parser -> CarParser ++ : parse into Car object + +create Car +CarParser -> Car ++ +Car --> CarParser -- +CarParser --> Parser --: car : Car + +Parser -> CarList ++ : addCar(car) +CarList --> Parser --: car added to list +Parser --> CliRental --: exit status + +@enduml \ No newline at end of file diff --git a/docs/diagrams/RyanAddTxDiagram.puml b/docs/diagrams/RyanAddTxDiagram.puml new file mode 100644 index 0000000000..bde2dfba39 --- /dev/null +++ b/docs/diagrams/RyanAddTxDiagram.puml @@ -0,0 +1,33 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!define BOX_COLOR #FFFFFF + +hide footbox +skinparam { +sequenceMessageAlign center +} + +box add-tx\n BOX_COLOR +participant "CliRental" as CliRental +participant "<>\nParser" as Parser +participant "<>\nTransactionParser" as TransactionParser +participant "transaction :Transaction" as Transaction +participant "<>\nTransactionList" as TransactionList +end box + +CliRental -> Parser ++ : parse user input +Parser -> TransactionParser ++ : parse into Transaction object + +create Transaction +TransactionParser -> Transaction ++ +Transaction --> TransactionParser -- +TransactionParser --> Parser -- : transaction : Transaction + +Parser -> TransactionList ++ : addTx(transaction) + +TransactionList --> Parser --: transaction added to list and car mark as "rented" + +Parser --> CliRental --: exit status + +@enduml \ No newline at end of file diff --git a/docs/diagrams/RyanClassDiagram.puml b/docs/diagrams/RyanClassDiagram.puml new file mode 100644 index 0000000000..a36170dd9a --- /dev/null +++ b/docs/diagrams/RyanClassDiagram.puml @@ -0,0 +1,65 @@ +@startuml +'https://plantuml.com/class-diagram + +skinparam { +classAttributeIconSize 0 +defaultFontSize 25 +ArrowFontSize 25 +} + +class CliRental { +{method}{static} +main(args : String[]) : void +{method}... +} +class Parser { +{method}{static} +parse(userInput : String): boolean +{method}... +} +class CarParser <>{ +{method}{static} +parseIntoCar(userInput : String): Car +} +class TransactionParser <>{ +{method}{static} +parseIntoTransaction(userInput : String): Transaction +} +class Car <>{ +{field} -licensePlateNumber : String +{field} -isRented : boolean +... +{method} +getLicensePlateNumber() : String +{method} +markAsRented() : void +{method} +markAsAvailable() : void +{method} +isRented() : boolean +{method} +getRentedStatus() : String +{method}... +} +class Transaction <>{ +{field} -carLicensePlate : String +... +{method} +getCarLicensePlate() : String +{method}... +} +class CarList <>{ +{method}{static} +addCar(car : Car) : void +{method}{static} +markCarAsRented(carLicensePlateNumber : String) : void +{method}... +} +class TransactionList <>{ +{method}{static} +addTx(transaction : Transaction) : void +{method}... +} + +CliRental ..> Parser + +Parser ..> CarParser +Parser ..> TransactionParser + +CarParser -- Car: parse into Car object > +CarParser ..> CarList + +TransactionParser -- Transaction : parse into Transaction object > +TransactionParser ..> TransactionList + +CarList *-- "*" Car +TransactionList *-- "*" Transaction + +@enduml \ No newline at end of file diff --git a/docs/diagrams/RyanRemoveCarDiagram.puml b/docs/diagrams/RyanRemoveCarDiagram.puml new file mode 100644 index 0000000000..0539ffdb23 --- /dev/null +++ b/docs/diagrams/RyanRemoveCarDiagram.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/sequence-diagram + +hide footbox +skinparam sequenceMessageAlign left + +participant "CliRental" as CliRental +participant "<>\nParser" as Parser +participant "<>\nCarParser" as CarParser +participant "<>\nCarList" as CarList + +CliRental -> Parser ++ : parse user input +Parser -> CarParser ++ : parse into license plate number + +alt isValidFormat && isValidLicensePlateNumber + CarParser --> Parser : return license plate number + Parser -> CarList ++: remove car using license plate number + alt license plate number found in car list + CarList --> Parser : car removed from car list + else license plate number not found in car list + CarList --> Parser --: shows "No car found with license plate" message + end +else invalid format or license plate number +CarParser --> Parser --: CarException thrown +end + +Parser --> CliRental --: exit status + +@enduml \ No newline at end of file diff --git a/docs/images/AddCustomerSequence.png b/docs/images/AddCustomerSequence.png new file mode 100644 index 0000000000..f771ca9682 Binary files /dev/null and b/docs/images/AddCustomerSequence.png differ diff --git a/docs/images/ArchitectureDiagram.drawio.png b/docs/images/ArchitectureDiagram.drawio.png new file mode 100644 index 0000000000..1a9cf55fba Binary files /dev/null and b/docs/images/ArchitectureDiagram.drawio.png differ diff --git a/docs/images/CLiRental.png b/docs/images/CLiRental.png new file mode 100644 index 0000000000..23380bd3ed Binary files /dev/null and b/docs/images/CLiRental.png differ diff --git a/docs/images/CarFileLoader.png b/docs/images/CarFileLoader.png new file mode 100644 index 0000000000..3adc670d04 Binary files /dev/null and b/docs/images/CarFileLoader.png differ diff --git a/docs/images/FileHandlerClassDiagram.png b/docs/images/FileHandlerClassDiagram.png new file mode 100644 index 0000000000..13ef9cee43 Binary files /dev/null and b/docs/images/FileHandlerClassDiagram.png differ diff --git a/docs/images/ListCarsSequence.png b/docs/images/ListCarsSequence.png new file mode 100644 index 0000000000..e8c5fe93a9 Binary files /dev/null and b/docs/images/ListCarsSequence.png differ diff --git a/docs/images/Overall.png b/docs/images/Overall.png new file mode 100644 index 0000000000..b098acfa3d Binary files /dev/null and b/docs/images/Overall.png differ diff --git a/docs/images/RyanAddCarDiagram.png b/docs/images/RyanAddCarDiagram.png new file mode 100644 index 0000000000..738c1021f8 Binary files /dev/null and b/docs/images/RyanAddCarDiagram.png differ diff --git a/docs/images/RyanAddTxDiagram.png b/docs/images/RyanAddTxDiagram.png new file mode 100644 index 0000000000..3c6132349b Binary files /dev/null and b/docs/images/RyanAddTxDiagram.png differ diff --git a/docs/images/RyanClassDiagram.png b/docs/images/RyanClassDiagram.png new file mode 100644 index 0000000000..b30282f29c Binary files /dev/null and b/docs/images/RyanClassDiagram.png differ diff --git a/docs/images/RyanRemoveCarDiagram.png b/docs/images/RyanRemoveCarDiagram.png new file mode 100644 index 0000000000..a880772bfd Binary files /dev/null and b/docs/images/RyanRemoveCarDiagram.png differ diff --git a/docs/images/list-cars-output-after.png b/docs/images/list-cars-output-after.png new file mode 100644 index 0000000000..54ce38f5fd Binary files /dev/null and b/docs/images/list-cars-output-after.png differ diff --git a/docs/images/list-cars-output-before.png b/docs/images/list-cars-output-before.png new file mode 100644 index 0000000000..d324ca770f Binary files /dev/null and b/docs/images/list-cars-output-before.png differ diff --git a/docs/images/list-rented-output.png b/docs/images/list-rented-output.png new file mode 100644 index 0000000000..8817547be7 Binary files /dev/null and b/docs/images/list-rented-output.png differ diff --git a/docs/team/Aaron.jpg b/docs/team/Aaron.jpg new file mode 100644 index 0000000000..a3b59a531e Binary files /dev/null and b/docs/team/Aaron.jpg differ diff --git a/docs/team/Kenneth.jpg b/docs/team/Kenneth.jpg new file mode 100644 index 0000000000..f177a45b85 Binary files /dev/null and b/docs/team/Kenneth.jpg differ diff --git a/docs/team/Khanh.jpg b/docs/team/Khanh.jpg new file mode 100644 index 0000000000..75d9faf7e1 Binary files /dev/null and b/docs/team/Khanh.jpg differ diff --git a/docs/team/Rex.png b/docs/team/Rex.png new file mode 100644 index 0000000000..ad5fe0774e Binary files /dev/null and b/docs/team/Rex.png differ diff --git a/docs/team/Ryan.jpeg b/docs/team/Ryan.jpeg new file mode 100644 index 0000000000..5981c815a0 Binary files /dev/null and b/docs/team/Ryan.jpeg differ diff --git a/docs/team/Ryan.jpg b/docs/team/Ryan.jpg new file mode 100644 index 0000000000..fce9cb9e71 Binary files /dev/null and b/docs/team/Ryan.jpg differ diff --git a/docs/team/aaronzz10.md b/docs/team/aaronzz10.md new file mode 100644 index 0000000000..a2c8ab57c8 --- /dev/null +++ b/docs/team/aaronzz10.md @@ -0,0 +1,79 @@ +# Liu Hao's Project Portfolio Page + +## Project: CLIRental + +## Overview + +**CliRental** is a command-line application designed for car rental managers to efficiently +manage and track customers, cars, and rental transactions in a centralized platform. +Built to replace traditional pen-and-paper methods, CliRental provides a user-friendly, +reliable solution for handling large volumes of data, streamlining operations for managers +with quick data entry and tracking capabilities. Developed in Java with approximately +4,000 lines of code (LOC), this tool serves as an effective desktop app tailored to the +core needs of car rental businesses. + +## Features Implemented +**Feature 1** : Implement Transaction Management Commands + +- **What it does** : Introduces commands to manage rental transactions, including removing transactions, marking them as complete or incomplete, listing completed or uncompleted transactions, and finding transactions by customer. + +- **Justification**: Efficient transaction management is crucial for tracking the status of rentals, ensuring cars are available when needed, and maintaining accurate records for billing and reporting purposes. + +- **Highlights**: + - **remove-tx**: Allows users to delete specific transactions using the transaction ID. + - **mark-tx**: Enables marking a transaction as completed, indicating the rental period has ended. + - **unmark-tx**: Allows users to revert a transaction's status to incomplete if needed. + - **list-tx-completed**: Lists all transactions that have been marked as completed. + - **list-tx-uncompleted**: Displays all transactions that are still active or incomplete. + - **find-tx-by-customer**: Provides the ability to search for all transactions associated with a specific customer. + +**Feature 2** : Develop Comprehensive Unit Tests for Transaction Classes + +- **What it does** : Implements JUnit tests for all methods within the `Transaction` and `TransactionList` classes to ensure functionality and reliability. + +- **Justification**: Unit testing is essential for verifying that individual components of the application work as intended, facilitating maintenance, and preventing regressions during future development. + +- **Highlights**: + - **Transaction Class Tests**: Tests cover methods such as adding, removing, marking, and unmarking transactions, ensuring each function behaves correctly under various scenarios. + - **TransactionList Class Tests**: Verifies the integrity of the transaction list operations, including data consistency and correct handling of edge cases. + - **Automated Testing**: Integrates tests into the development workflow, enabling continuous verification of code changes and enhancing overall code quality. + +**Feature 3** : Implement Comprehensive Input/Output (IO) Testing + +- **What it does** : Conducts full IO testing for the application, verifying that commands entered via the CLI produce the expected outputs. Tests ensure that user interactions with features like transaction management and data listing work as designed and meet the application's functionality requirements. + +- **Justification**: IO testing is critical for confirming that the application responds correctly to user commands, providing reliable and expected results. This testing approach validates the end-to-end functionality of the application from the user's perspective, ensuring a smooth and predictable user experience. + +- **Highlights**: + - **Command Accuracy**: Tests cover all core commands, including transaction addition, removal, completion status toggling, and customer-specific searches, to confirm correct outputs for each operation. + - **End-to-End Verification**: Ensures that user commands execute successfully and return accurate, user-friendly information, maintaining data consistency throughout. + - **CLI Simulation**: Simulates actual CLI interactions to replicate real user scenarios, verifying the app’s robustness in handling various user inputs and sequences. + +## Code Contribution + +Code contributed: [AaronZZ10's Contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=aaronzz10&breakdown=true) + +## Documentation + +### User Guide (UG) Contributions +- Added documentation for features: + - `list-users` + - `remove-tx` + - `mark-tx` + - `unmark-tx` + - `list-txs-completed` + - `list-txs-uncompleted` + - `find-txs-by-customer` + +### Developer Guide (DG) Contributions +- Added overview for Transaction Completion Management and Retrieval Features +- Added class-level design for TransactionList and Transaction class + +## Community + +### Team-Based Task Contributions +- Improved code quality and documentation by organizing format, adding divider lines for readability, and creating a contents summary for the User Guide. +- Reviewed teammates' pull requests, providing feedback to enhance code quality and functionality. [#54](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/54), [#66](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/66), [#68](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/68) +- Approved pull requests as needed to support workflow and project progression. + + diff --git a/docs/team/ct9aryan.md b/docs/team/ct9aryan.md new file mode 100644 index 0000000000..528c875c94 --- /dev/null +++ b/docs/team/ct9aryan.md @@ -0,0 +1,118 @@ +# Ryan Fong's Project Portfolio Page + +## Project: CLIRental + +--- +## Overview + +**CliRental** is a **CLI-based application** designed to facilitate operations of car rental +companies. It **manages and keeps track of customers, cars, and rental transactions** all +in one place. Targeted specifically at **car rental managers**, it provides them with a +**user-friendly and effective tool** to execute these tasks efficiently, especially when +**handling large volumes of data.** + +--- +## Code Contribution + +Click [here](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=ct9aryan&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=CT9ARyan&tabRepo=AY2425S1-CS2113-T11-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +to view my code contributions for this project!! :) +--- + +## Features implemented + +### Feature 1: Implemented `Car` related operations. +- `add-car`: **Adds** a car to the car database, specifying details like + the car model, license plate number and the rental price. +- `remove-car`: **Removes** a car from the car database. +- `list-cars`: **Lists** out **all** cars in the database. +- `list-rented`: **Lists** out all currently **rented out** cars. +- `list-available`: **Lists** out all currently **available** cars. + +
+ +### Feature 2: Modelled the validity check of license plate number after the real world. + +**Background:** + +The standard vehicle registration number in Singapore follows the +following format: `SXX####X`, where **X** represents any letter excluding +**I** and **O** (to avoid confusion with numbers 1 and 0 respectively), and +**\####** represents any number from 1 to 9999 without leading zeroes. However, there are +also exceptions where some license plate numbers start with E and only contain 2 letter +prefixes, etc. + +**Actual Implementation:** + +In an attempt to make things simple and flexible yet still adhering closely to the +actual format, our application allowed **X** to be **ANY** letter, including **I** and **O**. +We also disregarded the other exceptions (e.g. license plate number starting with E). Other +than that, all other requirements of the standard license plate number format were +adhered to. + +**Summary of implementation:** + +Format: `SXX####X`, where **X** represents **ANY** letter and **\####** represents +any number from 1 to 9999, **without leading zeroes.** + +This check prevents the user from entering any random undesirable or +invalid strings as the license plate number. + +### Feature 3: Added the ability to automatically update the rental status of cars. + +- All newly added cars will have a rental status of `Available`. +- The rental status of the car will be updated after any one of the following actions have + been executed: + - Transaction record **added**. (`Available` -> `Rented`) + - Transaction record(s) **removed**. (`Rented` -> `Available`) + - Transaction marked as **completed**. (`Rented` -> `Available`) + - Transaction marked as **uncompleted**. (`Available` -> `Rented`) +- This feature omits the need for extra commands and implementation like `mark-rented` +and `mark-available` for instance, hence improving efficiency and effectiveness (E & E) +of the application. + +___ + +
+ +## Documentation + +### User Guide (UG) Contributions +- Added documentation for features: + - `add-car` + - `list-cars` + - `list-rented` + - `list-available` + - `list-tx` + - `Updating rental status of car` + - `help` + - `exit` +- Created **Command Summary section** and **Command Summary Tables** and + filled in tables based on assigned commands listed above. +- Added a **Summary of Contents** at the top of the UG which links to each section +for easy navigation. + +### Developer Guide (DG) Contributions +- Added implementation details for auto updating of car rental status feature. +- Added `removeCar` UML Sequence diagram. +- Added `addCar` UML Sequence diagram. +- Added `addTx` UML Sequence diagram. +- Added UML Class diagram for Car and Transaction classes. + +___ +## Community + +### Team-Based Tasks Contributions +- General code and documentation enhancements, tidying up the format and + neatness of the code. (e.g. adding divider lines after each output, adding summary of +contents for easier navigation in User Guide) +- Helping to review teammates' PR and give meaningful feedback and + comments whenever possible. +(Pull requests [#54](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/54), +[#64](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/64), +[#110](https://github.com/AY2425S1-CS2113-T11-3/tp/pull/110)) +- Helping to approve teammate's PR whenever necessary. +- Pointed out bugs and provided suggestions for improvements for + other teams' DG during the peer review exercise. +(Pull request [#10](https://github.com/nus-cs2113-AY2425S1/tp/pull/10)) + +--- 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/rexkoh425.md b/docs/team/rexkoh425.md new file mode 100644 index 0000000000..4dbb957c70 --- /dev/null +++ b/docs/team/rexkoh425.md @@ -0,0 +1,76 @@ +# Rex Koh - Project Portfolio Page + +## Project: CliRental + +CliRental is a perfect desktop app dedicate to managing a car rental business involves handling large volumes +of data, making traditional pen-and-paper methods inefficient for managers. This app is designed specifically for car +rental managers, offering a Command Line Interface (CLI) that enables quick data entry and efficient tracking of +customers, cars, and transactions. It is written in Java and has about 4k Loc. + +## Summary of Contributions + +### Features added + +`Feature 1` : Add the ability to add customers details into the database. + +* `What it does` : Allows user to store customer's name , age and contact number to our database. +* `Justification`: Customer details like their names are needed to link car rental transactions to the specific customer. +* `Highlights`: This command allows user to add customer which is the building blocks for our application. Without the +ability to add customers , the app would not achieve its intended use. + +`Feature 2` : Add the ability to list all cars in the database. + +* `What it does` : Allows user to get a list of all the cars in the database , giving the user a +good overview of the cars. +* `Justification` : User cannot know what car is available to add a transaction if the user does not have a way to view +all cars. The user is also not able know the current status of the car , whether it is rented or not as well. + +`Feature 3` : Save car, customer and transaction data into different text file. + +* `What it does` : The application automatically stores car, customer and transaction data into different text files and +automatically loads upon start of program. The predetermined text files are customerData.txt, carData.txt, +transactionData.txt. They will be loaded into a directory called data. If the data files are not created on the local +computer, the files and folder will be automatically created by the program upon running. +* `Justification`: data should be able to be stored on the local computer and read by the program. If the user +accidentally exits the program without any file saving , it would result in the user having to type every command again. +This is very troublesome. Furthermore, the automatic updating and saving of the file is convenient to the user as the +user does not have to do anything extra. +* `Highlights`: The feature requires constant maintenance when new parameters are added to the Customer, Car and +Transaction class. This maintenance requires good care to allow data loaded to be accurate as if the user had type it +via commands. The files are also seperated into multiple different files and classes for easy maintenance and lesser +coupling. Multiple Junit tests for each individual file class are implemented to ensure the correctness of data which +is a priority. + +### Code Contribution + +Click [here](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=rexkoh425&breakdown=true) +to view my code contributions for this project!! + +### Project management + +Managed releases v1.0 - v2.1 (3 releases) on GitHub.
+ +### Enhancements to existing features: + +1) Improved Junit test for CarFile , CustomerFile and TransactionFile.
+2) Achieve at least 80% coverage for Class, Method , Line and Branch for each class mentioned above.
+3) Improved error handling of loading data from file to prevent malicious data from crashing the program.
+4) Data loaded from file follows constraints as if the user had added the command through the command line.
+ +### Documentation: +#### User Guide: +Added documentation for the features e.g. add-user and list-cars.
+Added documentation to inform user of file loading.
+Added user stories for add-user , list-cars
+ +#### Developer Guide: +1) Added implementation details of the adding customer details e.g. AddCustomerSequence.png.
+2) Added implementation details of listing cars e.g. ListCarsSequence.png.
+3) Added implementation details of file saving e.g. CarFileLoader.png & FileHandlerClassDiagram.png.
+4) Updated NFR.
+5) Added overall architecture diagram.
+6) Include manual testing instructions.
+ +### Community: +PRs reviewed (with non-trivial review comments): #68, #95, #110.
+Reported bugs and suggestions for other teams in the class in peer DG review.
diff --git a/docs/team/semicolonken.md b/docs/team/semicolonken.md new file mode 100644 index 0000000000..2faf47ba83 --- /dev/null +++ b/docs/team/semicolonken.md @@ -0,0 +1,82 @@ +# Kenneth Tan - Project Portfolio Page + +## Project: CliRental + +CliRental is a straightforward, user-friendly application designed to help manage car rental operations efficiently. +Built with Java, this app allows users to track rental transactions, manage customer information, +and monitor car availability through a series of command-line commands. Key functionalities include +adding and removing customers, logging rental transactions, checking car availability, +and marking transactions as completed. Designed for quick access and ease of use, +this app is ideal for small to mid-sized rental services looking to streamline operations without a complex interface. + +## Summary of Contributions + +### Features added + +`Feature 1` : Add the ability to remove customers details from the database. + +* What it does : Allows user to remove a customer from our database. +* Justification: Customer details such as contact number can change, removal of customers allow the updated information +to be added afterward. It is also important to allow removal when details have been entered wrongly. +* Highlights: This command allows user to remove customer which is the building blocks for our application. Without the + ability to remove customers , the app would not achieve its intended use. + +`Feature 2` : Add the ability to add a rental transaction into the database + +* What it does : Allows user to add a rental transaction into the database using information from the car and the customer +* Justification: To allow management of rental transaction, it is essential to add a rental transaction into the database. +This allows the user to keep a record of all rental transactions that have occurred which can then be view later on. +* Highlights: This command allows user to add a rental transaction which is the part of the main feature for our +application, rental management Without the ability to add transaction, the app would not serve its purpose. +* Difficulty of task: It is tedious to make sure that the transactionID is only generated when a valid transaction +has been successfully added and the transactionID is generated incrementally but not randomly. There is a need to have a +counter to increment the ID but the counter will not be stored when the program exit as the program only stores the +transactionID. It is necessary to compare the transactionID from the transactionData store and +update the transactionCounter which was not stored to match the saved file. + + +### Enhancements to existing features: +* Worked with rexkoh425 to save Transaction ID +* Added case-insensitive features for some commands, e.g. remove-user and add-tx +* Added junittest for for customer removal + +### Documentation: +#### User Guide: +Added documentation for the features e.g. remove-user and list completed transactions. +### Adding a Transaction: `add-tx` + +Adds a new rental transaction to the system. + +To add transaction bearing either an existing license plate number and/or customer name, all previous transactions containing either both or one of the parameter must be either marked as completed or be removed from the transaction list. + +**Format:** `add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: dd-MM-yyyy]` + +- `/c` identifier specifies the license plate number of the car the customer wants to rent. +- `LICENSE_PLATE_NUMBER` must match an existing car in the database. This is unique as the program will not allow 2 cars to have the same license plate number. + + +- `/u` identifier specifies the name of the customer. +- `CUSTOMER_NAME` must match an existing customer in the database. This is unique as the program will not allow 2 customers to have the same name. + + +- `/d` identifier specifies the duration of the rental in days. +- `DURATION` must be an integer between 1 and 365 (inclusive). This allows the rental companies to handle rental transactions from 1 day to 365 days (a year). + + +- `/s` identifier specifies the start date of the rental. +- `START_DATE` is in the format of [dd-MM-yyyy], accepting integers input only. It must be a valid date in the calendar. + +**Example:** +`add-tx /c SZZ1579D /u John /d 15 /s 11-05-2025` + +**Sample Response:** +``` +Transaction added: +[ ] TX2 | SZZ1579D | John | 15 days +Start Date: 11-05-2025 | End Date: 26-05-2025 +``` +Added sample outputs to match the latest release + +### Code Contribution + +Code contributed: https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=SemiColonKen&breakdown=true diff --git a/docs/team/tkhahns.md b/docs/team/tkhahns.md new file mode 100644 index 0000000000..f0d1766063 --- /dev/null +++ b/docs/team/tkhahns.md @@ -0,0 +1,72 @@ +# Dao Trong Khanh - Project Portfolio Page + +## Project: CliRental + +**CLiRental** is a Command Line Interface (CLI) application tailored for car rental businesses, +providing an efficient solution for managing customers, cars, and rental transactions in a centralized, +user-friendly platform. Targeted at car rental managers who often deal with large volumes of data, +**CLiRental** replaces traditional, inefficient pen-and-paper methods with a fast, reliable, and straightforward desktop tool. +Developed in Java with approximately 4,000 lines of code (LOC), this application is built to handle the core needs of a car rental operation, +enabling managers to perform quick data entries and track essential business operations seamlessly. + +## Summary of Contributions + +### Features additions and enhancements + +#### 1. Help Command Implementation +- **What it does**: Provides a user-friendly `help` command that displays a complete list of available commands along with descriptions. +- **Justification**: This feature improves usability significantly by giving new users or those unfamiliar with the commands a quick reference guide, enhancing user efficiency and reducing the learning curve. +- **Highlights**: This feature is foundational for user support, making the system intuitive and accessible without external documentation. + +#### 2. Remove a Car +- **What it does**: Enables users to remove a specific car from the system, keeping the fleet information up-to-date and reflecting only available vehicles. +- **Justification**: This feature is essential for inventory management, allowing the business to accurately represent current fleet availability and prevent outdated or unavailable cars from being rented. +- **Highlights**: The feature was designed to streamline car management processes, particularly focusing on ease of use for front-desk employees who manage car listings daily. + +#### 3. Add Car with Statistical Calculations +- **What it does**: Enhances the `add-car` functionality by calculating the median price of all available cars and categorizing them based on predefined price ranges (e.g., affordable, expensive). +- **Justification**: This feature provides valuable insights into the pricing structure, allowing employees to offer budget-aligned options to customers and helping managers analyze pricing distribution. +- **Highlights**: This enhancement required integrating statistical calculations and data filtering into the `add-car` feature, expanding its functionality while maintaining simplicity for end-users. + +#### 4. Enhanced Transaction Constraints +- **What it does**: Modifies the Customer, Car, and Transaction classes to enforce a strict one-to-one relationship between customers and cars in each transaction. Prevents additional transactions involving the same customer or car unless the previous transaction is marked complete or deleted. +- **Justification**: Ensuring each customer can only rent one car at a time prevents conflicts and errors in transaction management, improving data integrity and operational accuracy. +- **Highlights**: This enhancement introduced transaction constraints across multiple classes, requiring in-depth changes to existing data handling and validation processes. This improvement affects future features that rely on transaction data consistency. + +#### 5. Remove-All Commands +- **What it does**: Adds `remove-all` commands for clearing all customer, car, and transaction records, providing an efficient means for data reset or cleanup. +- **Justification**: This feature enhances administrative control by allowing bulk deletion of outdated or unnecessary records, improving data management efficiency. +- **Highlights**: Implementing `remove-all` commands required handling data dependencies and ensuring complete data clearance across related classes without impacting system stability. + +#### 6. Comprehensive Test Coverage +- **What it does**: Expands test cases for all implemented features, aiming for high test coverage to validate feature reliability and accuracy under various conditions. +- **Justification**: Comprehensive testing improves system stability, reduces bugs, and ensures that each feature performs as expected, even in edge cases. +- **Highlights**: This required extensive testing and coverage analysis, ensuring the implemented features integrate seamlessly and handle diverse user inputs effectively. + +Each of these contributions enhances the system's functionality, usability, and reliability, providing significant value to both users and administrators. + +### Code Contributions + +- Click [tkhahns Contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=tkhahns&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=tkhahns&tabRepo=AY2425S1-CS2113-T11-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +to view my code contributions! + +### Project management +- Generate the project idea and come up with the initial project plan diagram +- Set up organization and team repository. +- Manage releases **v2.0** and **v2.1** on GitHub. +- Resolve internal GitHub issues and issues generated by other groups from the PE-DryRun. + +### Documentation: +#### User Guide: +- Add documentation for multiple features, especially `add` and `remove` commands. +- Finetune and fix any existing error regarding the User Guide formatting. +- Complete the list of missing features for **v2.0**. + +#### Developer Guide: +- Update the complete list of User Stories for the project. + +### Community: +- Review PRs with trivial and non-trivial code comments, manage GitHub PRs and commits. +- Helping to check, approve, and merge teammate's PR whenever necessary. +- Reported bugs and suggestions for other teams in peer reviews. +- General code and documentation enhancements, tidying up the format and neatness of the code. diff --git a/src/main/java/car/Car.java b/src/main/java/car/Car.java new file mode 100644 index 0000000000..ba32b7202c --- /dev/null +++ b/src/main/java/car/Car.java @@ -0,0 +1,86 @@ +package car; + +/** + * Represents a class containing attributes and methods pertaining to a car. + */ +public class Car { + + public static final int NUMBER_OF_PARAMETERS = 5; + private final String model; + private final String licensePlateNumber; + private final double price; + private boolean isRented; + private boolean isExpensive; + + public Car(String model, String licensePlateNumber, double price) { + this.model = model; + this.licensePlateNumber = licensePlateNumber; + this.price = price; + isRented = false; + isExpensive = false; + } + + public Car(String model, String licensePlateNumber, double price , boolean isRented , boolean isExpensive) { + this.model = model; + this.licensePlateNumber = licensePlateNumber; + this.price = price; + this.isRented = isRented; + this.isExpensive = isExpensive; + } + + public String getModel() { + return model; + } + + public String getLicensePlateNumber() { + return licensePlateNumber; + } + + public double getPrice() { + return price; + } + + public void markAsRented() { + isRented = true; + } + + public void markAsAvailable() { + isRented = false; + } + + public void markAsExpensive() { + isExpensive = true; + } + + public void markAsCheap() { + isExpensive = false; + } + + public boolean isRented() { + return isRented; + } + + public boolean isExpensive() { + return isExpensive; + } + + public String getExpensiveStatus(){ + if (isExpensive) { + return "Expensive"; + } else { + return "Affordable"; + } + } + + public String getRentedStatus(){ + if (isRented) { + return "Rented"; + } + return "Available"; + } + + public String toFileString(){ + return this.getModel() + " | " + this.getLicensePlateNumber() + + " | " + this.getPrice() + " | " + this.isRented() + " | " + this.isExpensive(); + } +} diff --git a/src/main/java/car/CarList.java b/src/main/java/car/CarList.java new file mode 100644 index 0000000000..05a4c06621 --- /dev/null +++ b/src/main/java/car/CarList.java @@ -0,0 +1,281 @@ +package car; + +import exceptions.CarException; + +import java.util.ArrayList; +import java.util.Comparator; + +/** + * Represents an ArrayList of cars storing Car objects. + */ +public class CarList { + + public static ArrayList carsList = new ArrayList<>(); + + public static ArrayList getCarsList() { + return carsList; + } + + public static void clearCarList(){ + carsList.clear(); + } + + /** + * Formats the specified price to 2 d.p. + * + * @param price Price to be formatted to 2 d.p. + * @return Price formatted to 2.d.p. + */ + public static String formatPriceToTwoDp(double price) { + assert price >= 0.00 : "ERROR.. Price cannot be negative!!"; + + String result = ""; + String integerPart = String.valueOf((int)price); + + double remainder = price - (int)price; + String formattedRemainder = String.format("%.2f", remainder); + String remainderPart = formattedRemainder.substring(1); + + result += (integerPart + remainderPart); + return result; + } + + /** + * Adds a Car to the car list. + *

+ * If the license plate number of the Car already exists in the car list, + * a CarException is thrown instead. + * + * @param car Car to be added. + * @throws CarException If the license plate number already exists in the list. + */ + public static void addCar(Car car) throws CarException { + if (isExistingLicensePlateNumber(car.getLicensePlateNumber())) { + throw CarException.duplicateLicensePlateNumber(); + } + + assert !isExistingLicensePlateNumber(car.getLicensePlateNumber()) : + "ERROR.. Cannot add car with same license plate number"; + carsList.add(car); + CarList.sortCarsByPrice(); + CarList.markCarAsExpensive(); + System.out.println("Car added to list"); + System.out.println("Car details:"); + System.out.println(car.getModel() + " | " + car.getLicensePlateNumber() + + " | $" + formatPriceToTwoDp(car.getPrice()) + " | " + car.getRentedStatus() + + " | " + car.getExpensiveStatus() + " | " + "Median price: " + getMedianPrice()); + } + + public static void addCarWithoutPrintingInfo(Car car) throws CarException { + + if (isExistingLicensePlateNumber(car.getLicensePlateNumber())) { + throw CarException.duplicateLicensePlateNumber(); + } + + carsList.add(car); + CarList.sortCarsByPrice(); + CarList.getMedianPrice(); + CarList.markCarAsExpensive(); + } + + /** + * Removes a Car from the car list. + * + * @param carLicensePlateNumber License plate number of Car to be removed. + */ + public static void removeCar(String carLicensePlateNumber) { + Car carToRemove = null; + + // Iterate through the list of cars to find the one with the given license plate + for (Car car : carsList) { + if (car.getLicensePlateNumber().equalsIgnoreCase(carLicensePlateNumber)) { + carToRemove = car; + break; + } + } + + // Remove the car if it exists + if (carToRemove != null) { + carsList.remove(carToRemove); + System.out.println("Car with license plate " + carLicensePlateNumber.toUpperCase() + " removed from list."); + } else { + System.out.println("No car found with license plate " + carLicensePlateNumber.toUpperCase()); + } + } + + public static void removeAllCars() { + carsList.clear(); + System.out.println("All cars removed!!!"); + } + + /** + * Prints a list of all current cars in the company. + *

+ * If the list is empty, prints out a message instead to inform user that car list is empty. + */ + public static void printCarList(){ + if (carsList.isEmpty()) { + System.out.println("Oops!! Car list is empty..." + + "\nUse command to add a new car."); + return; + } + + System.out.println("Here are the current cars in the company:"); + + for(int i = 0 ; i < carsList.size(); i++){ + Car car = carsList.get(i); + System.out.println( (i + 1) + ") " + car.getModel() + " | " + car.getLicensePlateNumber() + + " | $" + formatPriceToTwoDp(car.getPrice()) + " | " + car.getRentedStatus() + + " | " + car.getExpensiveStatus() + " | " + "Median price: " + getMedianPrice()); + } + } + + /** + * Prints a list of all rented out cars. + *

+ * If the list is empty, prints out a message instead to inform user that no cars + * are currently rented out. + */ + public static void printRentedCarsList() { + ArrayList rentedCarsList = getRentedCarsList(); + + if (rentedCarsList.isEmpty()) { + System.out.println("No cars currently rented out..."); + return; + } + + System.out.println("Here are all the rented out cars:"); + + int index = 1; + for (Car car : rentedCarsList) { + System.out.println(index + ") " + car.getModel() + " | " + car.getLicensePlateNumber() + + " | $" + formatPriceToTwoDp(car.getPrice())); + index++; + } + } + + /** + * Prints a list of all available cars. + *

+ * If the list is empty, prints out a message instead to inform user that there + * are no available cars currently. + */ + public static void printAvailableCarsList() { + ArrayList availableCarsList = getAvailableCarsList(); + + if (availableCarsList.isEmpty()) { + System.out.println("There are no available cars at the moment..."); + return; + } + + System.out.println("Here are all the available cars:"); + + int index = 1; + for (Car car : availableCarsList) { + System.out.println(index + ") " + car.getModel() + " | " + car.getLicensePlateNumber() + + " | $" + formatPriceToTwoDp(car.getPrice())); + index++; + } + } + + private static ArrayList getRentedCarsList() { + ArrayList rentedCars = new ArrayList<>(); + + for (Car car : carsList) { + if (car.isRented()) { + rentedCars.add(car); + } + } + return rentedCars; + } + + private static ArrayList getAvailableCarsList() { + ArrayList availableCars = new ArrayList<>(); + + for (Car car : carsList) { + if (!car.isRented()) { + availableCars.add(car); + } + } + return availableCars; + } + + /** + * Checks if the specified license plate number exists in the car list. + * + * @param licensePlateNumber License plate number to check. + * @return true if license plate number already exists, false otherwise. + */ + public static boolean isExistingLicensePlateNumber(String licensePlateNumber) { + for (Car car : carsList) { + if (car.getLicensePlateNumber().equalsIgnoreCase(licensePlateNumber)) { + return true; + } + } + return false; + } + + /** + * Marks a Car as rented. + * + * @param carLicensePlateNumber License plate number of to-be-marked Car. + */ + public static void markCarAsRented(String carLicensePlateNumber) { + for (Car car : carsList) { + if (car.getLicensePlateNumber().equals(carLicensePlateNumber)) { + car.markAsRented(); + break; + } + } + } + + /** + * Marks a Car as available. + * + * @param carLicensePlateNumber License plate number of to-be-marked Car. + */ + public static void markCarAsAvailable(String carLicensePlateNumber) { + for (Car car : carsList) { + if (car.getLicensePlateNumber().equals(carLicensePlateNumber)) { + car.markAsAvailable(); + break; + } + } + } + + public static String carListToFileString() { + StringBuilder carData = new StringBuilder(); + for (Car car : carsList) { + carData.append(car.toFileString()); + carData.append("\n"); + } + return carData.toString(); + } + + public static void sortCarsByPrice() { + carsList.sort(Comparator.comparingDouble(Car::getPrice)); + } + + public static double getMedianPrice() { + if (carsList.isEmpty()) { + return 0; + } + + int middleIndex = carsList.size() / 2; + if (carsList.size() % 2 == 0) { + // For even-sized lists, choose the lower middle element + middleIndex--; + } + return carsList.get(middleIndex).getPrice(); + } + + public static void markCarAsExpensive() { + for (Car car : carsList) { + if (car.getPrice() > getMedianPrice()) { + car.markAsExpensive(); + } else { + car.markAsCheap(); + } + } + } +} diff --git a/src/main/java/customer/Customer.java b/src/main/java/customer/Customer.java new file mode 100644 index 0000000000..ca014f97a7 --- /dev/null +++ b/src/main/java/customer/Customer.java @@ -0,0 +1,100 @@ +package customer; + +import exceptions.CustomerException; +import java.util.regex.Pattern; + +/** + * Represents customers of CliRental + */ +public class Customer { + + public static final int NUMBER_OF_PARAMETERS = 3; + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z ]+$"); + private static final Pattern VALID_CONTACT_PATTERN = Pattern.compile("^[89]\\d{7}$"); + // 8 digits, starts with 8 or 9 + + private String customerName; + private int age; + private String contactNumber; + + public Customer(String customerName, int age, String contactNumber) { + // Validate name format + if (isInvalidName(customerName)) { + throw CustomerException.invalidCustomerNameException(customerName); + } + // Validate age + if(age <= 17){ + throw CustomerException.invalidAgeException(); + }else if(age > 100){ + throw CustomerException.invalidMaxAgeException(); + } + + // Validate contact number format + if (isInvalidContactNumber(contactNumber)) { + throw CustomerException.invalidContactNumberException(); + } + + this.customerName = customerName; + this.age = age; + this.contactNumber = contactNumber; + } + + public String getCustomerName() { + return customerName; + } + + public String getContactNumber() { + return contactNumber; + } + + public int getAge() { + return age; + } + + public void setCustomerName(String customerName) { + if (isInvalidName(customerName)) { + throw CustomerException.invalidCustomerNameException(customerName); + } + this.customerName = customerName; + } + + public void setAge(int age) { + if(age <= 17){ + throw CustomerException.invalidAgeException(); + }else if(age > 100){ + throw CustomerException.invalidMaxAgeException(); + } + + this.age = age; + } + + public void setContactNumber(String contactNumber) { + if (isInvalidContactNumber(contactNumber)) { + throw CustomerException.invalidContactNumberException(); + } + this.contactNumber = contactNumber; + } + + public String toString() { + return this.getCustomerName() + " | " + + this.getAge() + " | " + + this.getContactNumber(); + } + + public String toFileString() { + return this.getCustomerName() + " | " + this.getAge() + " | " + this.getContactNumber(); + } + + public String toDisplayString() { + return "Customer name: " + getCustomerName() + "\nAge: " + getAge() + "\nContact Number: " + + getContactNumber(); + } + + private static boolean isInvalidName(String name) { + return !VALID_NAME_PATTERN.matcher(name).matches(); + } + + private static boolean isInvalidContactNumber(String contactNumber) { + return !VALID_CONTACT_PATTERN.matcher(contactNumber).matches(); + } +} diff --git a/src/main/java/customer/CustomerList.java b/src/main/java/customer/CustomerList.java new file mode 100644 index 0000000000..597d5b9ae8 --- /dev/null +++ b/src/main/java/customer/CustomerList.java @@ -0,0 +1,137 @@ +package customer; + +import exceptions.CustomerException; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class CustomerList { + + public static ArrayList customers = new ArrayList<>(); + + // Regex pattern to allow only alphabetic characters and spaces + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z ]+$"); + + public static void addCustomer(Customer customer) { + String customerName = customer.getCustomerName(); + + // Check for valid characters in the name + if (isInvalidName(customerName)) { + throw CustomerException.invalidCustomerNameException(customerName); + } + + // Check for duplicate customer name (case-insensitive) + if (isExistingCustomer(customerName)) { + throw CustomerException.duplicateCustomerNameException(customerName); + } + + customers.add(customer); + System.out.println("Customer added"); + System.out.println(customer.toDisplayString()); + } + + public static ArrayList getCustomerList() { + return customers; + } + + public static void addCustomerWithoutPrintingInfo(Customer customer) { + String customerName = customer.getCustomerName(); + + // Check for valid characters in the name + if (isInvalidName(customerName)) { + throw CustomerException.invalidCustomerNameException(customerName); + } + + // Check for duplicate customer name (case-insensitive) + if (isExistingCustomer(customerName)) { + throw CustomerException.duplicateCustomerNameException(customerName); + } + + customers.add(customer); + } + + public static void removeCustomer(String customerName) { + if (customers.isEmpty()) { + System.out.println("Customer list is empty. Nothing to remove"); + return; + } + + boolean customerFound = false; + + for (int i = 0; i < customers.size(); i++) { + Customer customer = customers.get(i); + if (customer.getCustomerName().equalsIgnoreCase(customerName)) { + customers.remove(i); + System.out.println("Customer Name: " + customer.getCustomerName() + + " has been removed from the customer list"); + customerFound = true; + break; + } + } + + if (!customerFound) { + System.out.println(customerName + " is not in the customer list. No removal done"); + } + } + + + public static void clearCustomerList() { + customers.clear(); + } + + public static void removeAllCustomers() { + clearCustomerList(); + System.out.println("All customers removed!!!"); + } + + public static ArrayList getCustomers() { + return customers; + } + + public static void printCustomers() { + if (CustomerList.getCustomers().isEmpty()) { + System.out.println("Customer list is empty."); + } else { + System.out.println("Here are all the customers:"); + for (int i = 0; i < customers.size(); i++) { + System.out.print((i + 1) + ") "); + Customer customer = customers.get(i); + System.out.println(customer.toString()); + } + } + } + + public static boolean isExistingCustomer(String customerName) { + for (Customer customer : customers) { + if (customer.getCustomerName().equalsIgnoreCase(customerName)) { + return true; + } + } + return false; + } + + public static String customerListToFileString() { + StringBuilder customerData = new StringBuilder(); + for (Customer customer : customers) { + customerData.append(customer.toFileString()); + customerData.append("\n"); + } + return customerData.toString(); + } + + public static String getCustomerNameIfExist(String inputName) { + for (Customer customer : customers) { + String customerName = customer.getCustomerName(); + if (customer.getCustomerName().equalsIgnoreCase(inputName)) { + return customerName; + } + } + return inputName; + } + + // Helper method to validate the customer's name + private static boolean isInvalidName(String name) { + return !VALID_NAME_PATTERN.matcher(name).matches(); + } +} + diff --git a/src/main/java/exceptions/CarException.java b/src/main/java/exceptions/CarException.java new file mode 100644 index 0000000000..992908831e --- /dev/null +++ b/src/main/java/exceptions/CarException.java @@ -0,0 +1,105 @@ +package exceptions; + +import java.util.ArrayList; + +/** + * Represents Car related exceptions. + */ +public class CarException extends RuntimeException{ + + private static final String ADD_CAR_FORMAT = "add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE]"; + private static final String LICENSE_PLATE_NUMBER_FORMAT = """ + + License plate number format: SXX####X + X -> Letters [A - Z], #### -> Numbers [1 - 9999]"""; + + public CarException(String message) { + super(message); + } + + /** + * Returns an exception when the add-car command is invalid. + * + * @return Exception with error message. + */ + public static CarException addCarException() { + String message = "Unable to add car. Refer to correct format below:\n" + + ADD_CAR_FORMAT; + return new CarException(message); + } + + /** + * Returns an exception when the specified price is negative. + * + * @return Exception with error message. + */ + public static CarException negativePrice() { + String message = "Price cannot be negative!! Try again..."; + return new CarException(message); + } + + /** + * Returns an exception when the specified price exceeds the price limit. + * The price limit is set to 10000. + * + * @return Exception with error message. + */ + public static CarException invalidPrice() { + String message = "Price exceeded limit of $10 000!! Try again..."; + return new CarException(message); + } + + /** + * Returns an exception when the specified license plate number is invalid. + * + * @return Exception with error message. + */ + public static CarException invalidLicensePlateNumber() { + String message = "Oops!! License Plate number is invalid...\n" + + LICENSE_PLATE_NUMBER_FORMAT; + return new CarException(message); + } + + /** + * Returns an exception when the specified license plate number in the + * add-car command already exists in the car list. + * + * @return Exception with error message. + */ + public static CarException duplicateLicensePlateNumber() { + String message = "Unable to add car.. License Plate number already exists!!"; + return new CarException(message); + } + + /** + * Returns an exception when the specified license plate number in the + * add-tx command cannot be found in the car list. + * + * @return Exception with error message. + */ + public static CarException licensePlateNumberNotFound() { + String message = "Car license plate number not found!!" + + System.lineSeparator() + "Use command to view list of available cars."; + return new CarException(message); + } + + /** + * Exception thrown if data in carData.txt does not fit pre-determined format. + * + * @param errorLines List of row numbers in the carData.txt file which the data format is wrong. + * @return Exception with message of which the row of data which are wrong. + */ + public static CarException invalidParameters(ArrayList errorLines){ + String message = "Car data do not match parameters requirements in " + errorLines.size() + " rows of data\n"; + message += "Rows are : "; + message += errorLines.toString(); + message = message + "\n"; + return new CarException(message); + } + + public static CarException carAlreadyInTransactionList() { + String message = "This car has been rented!"; + return new CarException(message); + } + +} diff --git a/src/main/java/exceptions/CliRentalException.java b/src/main/java/exceptions/CliRentalException.java new file mode 100644 index 0000000000..e4b4b81e4e --- /dev/null +++ b/src/main/java/exceptions/CliRentalException.java @@ -0,0 +1,10 @@ +package exceptions; + +public class CliRentalException extends Exception { + public CliRentalException(String message) { + super(message); + } + public static CliRentalException unknownCommand() { + return new CliRentalException("OOPS!!! Invalid command!"); + } +} diff --git a/src/main/java/exceptions/CustomerException.java b/src/main/java/exceptions/CustomerException.java new file mode 100644 index 0000000000..deb3ca3429 --- /dev/null +++ b/src/main/java/exceptions/CustomerException.java @@ -0,0 +1,89 @@ +package exceptions; + +import java.util.ArrayList; + +/** + * Customer related exceptions + */ +public class CustomerException extends RuntimeException { + + public static final String ADD_FORMAT = "add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER]"; + public static final String REMOVE_FORMAT = "remove-user /u [CUSTOMER_NAME]"; + + public CustomerException(String message) { + super(message); + } + + public void printErrorMessage(){ + System.out.println(getMessage()); + } + + /** + * Exception for the add-user command + */ + public static CustomerException addCustomerException(){ + return new CustomerException("Unable to add customer. Please follow: " + ADD_FORMAT); + } + + /** + * Exception thrown if data in customerData.txt does not fit pre-determined format. + * + * @param errorLines List of row numbers in the customerData.txt file which the data format is wrong. + * @return Exception with message of which the row of data which are wrong. + */ + public static CustomerException invalidParameters(ArrayList errorLines){ + String message = "Customer data do not match parameters requirements in " + + errorLines.size() + " rows of data\n"; + message += "Rows are : "; + message += errorLines.toString(); + message = message + "\n"; + return new CustomerException(message); + } + + public static CustomerException removeCustomerException(){ + return new CustomerException("Unable to remove customer. Please follow: " + REMOVE_FORMAT); + } + + public static CustomerException missingNameWhenRemoving(){ + return new CustomerException("Please enter customer name for removal."); + } + + public static CustomerException customerAlreadyInTransactionList() { + return new CustomerException("Customer has rented a car."); + } + + public static CustomerException invalidContactNumberException(){ + return new CustomerException("Invalid contact number. Format for contact number is [8 DIGITS AND " + + "STARTS WITH 8 OR 9]"); + } + + public static CustomerException invalidAgeException(){ + return new CustomerException("Illegal driver!! Age should be more than 17!!"); + } + + public static CustomerException invalidMaxAgeException(){ + return new CustomerException("This age is not safe to drive!! Too old!!"); + } + + /** + * Exception thrown when the customer name already exists (case-insensitive). + * + * @param customerName The name of the customer that already exists. + * @return Exception with a message specifying the duplicate name. + */ + public static CustomerException duplicateCustomerNameException(String customerName) { + return new CustomerException("The customer name \"" + customerName + "\" already exists. " + + "Customer names must be unique and are case-insensitive."); + } + + /** + * Exception thrown when the customer name contains invalid characters. + * + * @param customerName The invalid name entered. + * @return Exception with a message specifying the invalid characters. + */ + public static CustomerException invalidCustomerNameException(String customerName) { + return new CustomerException("The customer name \"" + customerName + "\" contains invalid characters. " + + "Only alphabetic characters and spaces are allowed."); + } +} diff --git a/src/main/java/exceptions/TransactionException.java b/src/main/java/exceptions/TransactionException.java new file mode 100644 index 0000000000..647b0f34fe --- /dev/null +++ b/src/main/java/exceptions/TransactionException.java @@ -0,0 +1,24 @@ +package exceptions; + +import java.util.ArrayList; + +public class TransactionException extends RuntimeException { + public TransactionException(String message) { + super(message); + } + + /** + * Exception thrown if data in transactionData.txt does not fit pre-determined format. + * + * @param errorLines List of row numbers in the transactionData.txt file which the data format is wrong. + * @return Exception with message of which the row of data which are wrong. + */ + public static TransactionException invalidParameters(ArrayList errorLines){ + String message = "Transaction data do not match parameters requirements in " + + errorLines.size() + " rows of data" + System.lineSeparator(); + message += "Rows are : "; + message += errorLines.toString(); + message = message + "\n"; + return new TransactionException(message); + } +} diff --git a/src/main/java/file/CarFile.java b/src/main/java/file/CarFile.java new file mode 100644 index 0000000000..904e663ce9 --- /dev/null +++ b/src/main/java/file/CarFile.java @@ -0,0 +1,150 @@ +package file; + +import car.Car; +import car.CarList; +import exceptions.CarException; +import exceptions.CustomerException; +import exceptions.TransactionException; +import parser.CarParser; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Handles file operation for the car. + */ +public class CarFile { + + private final String carDataFileName; + private final String carDataFilePath; + private final File carDataFile; + + public CarFile(){ + this.carDataFileName = "carData.txt"; + this.carDataFilePath = FileHandler.getDirName() + "/" + carDataFileName; + this.carDataFile = new File(carDataFilePath); + } + + public CarFile(String filename){ + this.carDataFileName = filename; + this.carDataFilePath = FileHandler.getDirName() + "/" + carDataFileName; + this.carDataFile = new File(carDataFilePath); + } + + public String getCarDataFilename() { + return this.carDataFileName; + } + + /** + * Add car object to the list according to the parameters. + * + * @param parameters parameters of the Car object. + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this car data is at in carData.txt. + */ + public void addCarWithParameters(String[] parameters, ArrayList errorLines, int line) { + assert parameters.length == Car.NUMBER_OF_PARAMETERS : "wrong no. of parameter"; + + try { + if(FileHandler.containEmptyParameter(parameters)){ + throw new CarException(""); + } + String model = parameters[0]; + String licensePlateNumber = parameters[1]; + double price = Double.parseDouble(parameters[2]); + boolean isRented = Boolean.parseBoolean(parameters[3]); + boolean isExpensive = Boolean.parseBoolean(parameters[4]); + if(!CarParser.isValidLicensePlateNumber(licensePlateNumber) || !CarParser.isValidPrice(price) || + CarList.isExistingLicensePlateNumber(licensePlateNumber)){ + throw new CarException(""); + } + Car car = new Car(model, licensePlateNumber, price, isRented , isExpensive); + CarList.addCarWithoutPrintingInfo(car); + CarList.sortCarsByPrice(); + CarList.markCarAsExpensive(); + + } catch(NumberFormatException | CarException | CustomerException | TransactionException e) { + + errorLines.add(line); + } + } + + /** + * Reads the current car list and updates carData.txt file. + * + * @throws IOException File does not exist. + */ + public void updateCarDataFile() throws IOException { + FileWriter fw = new FileWriter(carDataFile); + String textToAdd = CarList.carListToFileString(); + fw.write(textToAdd); + fw.close(); + } + + public void createCarFileIfNotExist(){ + if(!carDataFile.exists()){ + FileHandler.createNewFile(carDataFile); + } + } + + /** + * Reads every line in the carData.txt file and add it to the current car list. + * + * @throws FileNotFoundException if carData.txt does not exist. + * @throws CarException if there is corruption in file data. + */ + public void loadCarData() throws FileNotFoundException, CarException { + Scanner scanner = new Scanner(carDataFile); + ArrayList errorLines = new ArrayList<>(); + int line = 1; + while (scanner.hasNext()) { + scanLineAndAddCar(scanner, errorLines, line); + line ++; + } + CarList.sortCarsByPrice(); + CarList.markCarAsExpensive(); + if(!errorLines.isEmpty()) { + throw CarException.invalidParameters(errorLines); + } + } + + /** + * Scans the current line and add data to current car list. + * + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this car data is at in carData.txt. + */ + public void scanLineAndAddCar(Scanner scanner, ArrayList errorLines, int line) { + String input = scanner.nextLine(); + String[] parameters = input.split(" \\| "); + if(parameters.length != Car.NUMBER_OF_PARAMETERS){ + errorLines.add(line); + }else{ + addCarWithParameters(parameters, errorLines, line); + } + } + + /** + * Loads data from carData.txt if the file exist. + */ + public void loadCarDataIfExist(){ + try { + this.loadCarData(); + } catch (FileNotFoundException e) { + System.out.println("carData.txt not found in data directory. Please try again"); + } catch (CarException e) { + System.out.println(e.getMessage()); + } + } + + public boolean isFileExist(){ + return carDataFile.exists(); + } + + public String getAbsolutePath(){ + return carDataFile.getAbsolutePath(); + } +} diff --git a/src/main/java/file/CustomerFile.java b/src/main/java/file/CustomerFile.java new file mode 100644 index 0000000000..ace1f8f3a4 --- /dev/null +++ b/src/main/java/file/CustomerFile.java @@ -0,0 +1,144 @@ +package file; + +import customer.Customer; +import customer.CustomerList; +import exceptions.CarException; +import exceptions.CustomerException; +import exceptions.TransactionException; +import parser.CustomerParser; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Handles file operation for the customer. + */ +public class CustomerFile { + private final String customerDataFileName; + private final String customerDataFilePath ; + private final File customerDataFile; + + public CustomerFile(){ + this.customerDataFileName = "customerData.txt"; + this.customerDataFilePath = FileHandler.getDirName() + "/" + customerDataFileName; + this.customerDataFile = new File(customerDataFilePath); + } + + public CustomerFile(String filename){ + this.customerDataFileName = filename; + this.customerDataFilePath = FileHandler.getDirName() + "/" + customerDataFileName; + this.customerDataFile = new File(customerDataFilePath); + } + + public String getCustomerDataFilename() { + return this.customerDataFileName; + } + + public void createCustomerFileIfNotExist(){ + if(!this.customerDataFile.exists()){ + FileHandler.createNewFile(this.customerDataFile); + } + } + + /** + * Reads every line in the customerData.txt file and add it to the current customer list. + * + * @throws FileNotFoundException if customerData.txt does not exist. + * @throws CustomerException if there is corruption in file data. + */ + public void loadCustomerData() throws FileNotFoundException, CustomerException { + if(this.customerDataFile.exists()){ + Scanner scanner = new Scanner(this.customerDataFile); + ArrayList errorLines = new ArrayList<>(); + int line = 1; + while (scanner.hasNext()) { + scanLineAndAddCustomer(scanner, errorLines, line); + line ++; + } + if(!errorLines.isEmpty()) { + throw CustomerException.invalidParameters(errorLines); + } + } + } + + /** + * Scans the current line and add data to current customer list. + * + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this customer data is at in customerData.txt. + */ + public void scanLineAndAddCustomer(Scanner scanner, ArrayList errorLines, int line) { + String input = scanner.nextLine(); + String[] parameters = input.split(" \\| "); + if(parameters.length != Customer.NUMBER_OF_PARAMETERS){ + errorLines.add(line); + }else{ + addCustomerWithParameters(parameters, errorLines ,line); + } + } + + /** + * Reads the current customer list and updates customerData.txt file. + * + * @throws IOException File does not exist. + */ + public void updateCustomerDataFile() throws IOException { + FileWriter fw = new FileWriter(this.customerDataFile); + String textToAdd = CustomerList.customerListToFileString(); + fw.write(textToAdd); + fw.close(); + } + + /** + * Add customer object to the list according to the parameters. + * + * @param parameters parameters of the Customer object. + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this customer data is at in customerData.txt. + */ + public void addCustomerWithParameters(String[] parameters, ArrayList errorLines , int line) { + assert parameters.length == Customer.NUMBER_OF_PARAMETERS : "wrong no. of parameter"; + + try { + if (FileHandler.containEmptyParameter(parameters)) { + throw new CustomerException(""); + } + String customerName = parameters[0]; + int age = Integer.parseInt(parameters[1]); + String contactNumber = parameters[2]; + + if(!CustomerParser.isValidContactNumber(contactNumber) || age <= 17 || age > 100){ + throw new CustomerException(""); + } + + Customer customer = new Customer(customerName , age , contactNumber); + CustomerList.addCustomerWithoutPrintingInfo(customer); + }catch(NumberFormatException | CustomerException | TransactionException | CarException e) { + errorLines.add(line); + } + } + + /** + * Loads data from customerData.txt if the file exist. + */ + public void loadCustomerDataIfExist(){ + try { + loadCustomerData(); + } catch (FileNotFoundException e) { + System.out.println("customerData.txt not found in data directory. Please try again"); + } catch (CustomerException e) { + System.out.println(e.getMessage()); + } + } + + public boolean isFileExist(){ + return customerDataFile.exists(); + } + + public String getAbsolutePath(){ + return this.customerDataFile.getAbsolutePath(); + } +} diff --git a/src/main/java/file/FileHandler.java b/src/main/java/file/FileHandler.java new file mode 100644 index 0000000000..18c1c16196 --- /dev/null +++ b/src/main/java/file/FileHandler.java @@ -0,0 +1,132 @@ +package file; + +import java.io.IOException; +import java.io.File; + +/** + * Handles general file operations. + */ +public class FileHandler { + + private static final String DIR_NAME = "data"; + private static final File DATA_DIR = new File(DIR_NAME); + private static final CarFile carFile = new CarFile(); + private static final CustomerFile customerFile = new CustomerFile(); + private static final TransactionFile transactionFile = new TransactionFile(); + + public FileHandler(){ + + } + + public static CarFile getCarFile() { + return carFile; + } + + public static CustomerFile getCustomerFile() { + return customerFile; + } + + public static TransactionFile getTransactionFile() { + return transactionFile; + } + + public static File getDataDir() { + return DATA_DIR; + } + + public static String getDirName() { + return DIR_NAME; + } + + /** + * Creates and load files to store customer, transaction and car data. + */ + public static void createAndLoadFiles(){ + createFolderIfNotExist(); + carFile.createCarFileIfNotExist(); + customerFile.createCustomerFileIfNotExist(); + transactionFile.createTransactionFileIfNotExist(); + carFile.loadCarDataIfExist(); + customerFile.loadCustomerDataIfExist(); + transactionFile.loadTransactionDataIfExist(); + + assert carFile.isFileExist() : "Car file does not exist."; + assert customerFile.isFileExist() : "Customer file does not exist."; + assert transactionFile.isFileExist() : "Transaction file does not exist."; + } + + /** + * Creates a new file according to filepath. + * + * @param filename The name of the file to create. + */ + public static void createNewFile(File filename) { + + try { + if (filename.createNewFile()) { + System.out.println("File created successfully: " + filename.getName()); + } else { + System.out.println("File already exists."); + } + + } catch (IOException exception) { + System.err.println("Failed to create file: " + exception.getMessage()); + } + } + + /** + * Creates a directory called data to store data files if it does not exist. + */ + public static void createFolderIfNotExist(){ + + if (!DATA_DIR.isDirectory()) { + System.out.println(DIR_NAME + " does not exist. Creating it now......."); + createFolder(); + } + assert DATA_DIR.isDirectory() : "Data directory was not created successfully."; + } + + /** + * Creates the data directory if it does not exist + */ + private static void createFolder() { + + boolean isCreated = DATA_DIR.mkdirs(); + + if (isCreated) { + System.out.println(DIR_NAME + " directory created successfully."); + } else { + System.out.println(DIR_NAME + " directory already exists or failed to create."); + } + } + + public static boolean containEmptyParameter(String[] parameters){ + for (String parameter : parameters) { + if (parameter.trim().isEmpty()) { + return true; + } + } + return false; + } + + /** + * Reads customer , transaction and car list and refreshes the data file accordingly. + */ + public static void updateFiles(){ + try { + carFile.updateCarDataFile(); + } catch (IOException e) { + System.out.println("Unable to update " + carFile.getCarDataFilename()); + } + try { + customerFile.updateCustomerDataFile(); + } catch (IOException e) { + System.out.println("Unable to update " + customerFile.getCustomerDataFilename()); + } + try { + transactionFile.updateTransactionDataFile(); + } catch (IOException e) { + System.out.println("Unable to update " + transactionFile.getTransactionDataFilename()); + } + } +} diff --git a/src/main/java/file/TransactionFile.java b/src/main/java/file/TransactionFile.java new file mode 100644 index 0000000000..97c17d5b9b --- /dev/null +++ b/src/main/java/file/TransactionFile.java @@ -0,0 +1,171 @@ +package file; + +import car.CarList; +import customer.CustomerList; +import exceptions.CarException; +import exceptions.CustomerException; +import exceptions.TransactionException; +import parser.CarParser; +import transaction.Transaction; +import transaction.TransactionList; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +import static parser.TransactionParser.dateTimeFormatter; + +/** + * Handles file operation for the transaction. + */ +public class TransactionFile { + + private final String transactionDataFileName; + private final String transactionDataFilePath ; + private final File transactionDataFile; + + public TransactionFile(){ + this.transactionDataFileName = "transactionData.txt"; + this.transactionDataFilePath = FileHandler.getDirName() + "/" + transactionDataFileName; + this.transactionDataFile = new File(transactionDataFilePath); + } + + public TransactionFile(String filename){ + this.transactionDataFileName = filename; + this.transactionDataFilePath = FileHandler.getDirName() + "/" + transactionDataFileName; + this.transactionDataFile = new File(transactionDataFilePath); + } + + public String getTransactionDataFilename() { + return transactionDataFileName; + } + + public void createTransactionFileIfNotExist(){ + if(!transactionDataFile.exists()){ + FileHandler.createNewFile(transactionDataFile); + } + } + + /** + * Reads every line in the transactionData.txt file and add it to the current transaction list. + * + * @throws FileNotFoundException if transactionData.txt does not exist. + * @throws TransactionException if there is corruption in file data. + */ + public void loadTransactionData() throws FileNotFoundException, TransactionException { + if(transactionDataFile.exists()){ + Scanner scanner = new Scanner(transactionDataFile); + ArrayList errorLines = new ArrayList<>(); + int line = 1; + + while (scanner.hasNext()) { + scanLineAndAddTransaction(scanner, errorLines, line); + line ++; + } + + if (!errorLines.isEmpty()) { + throw TransactionException.invalidParameters(errorLines); + } + } + } + + /** + * Loads data from transactionData.txt if the file exist. + */ + public void loadTransactionDataIfExist(){ + try { + loadTransactionData(); + } catch (FileNotFoundException e) { + System.out.println("transactionData.txt not found in data directory. Please try again"); + } catch (TransactionException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Scans the current line and add data to current transaction list. + * + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this transaction data is at in transactionData.txt. + */ + public void scanLineAndAddTransaction(Scanner scanner, ArrayList errorLines, int line) { + String input = scanner.nextLine(); + String[] parameters = input.split(" \\| "); + + if (parameters.length != Transaction.NUMBER_OF_PARAMETERS) { + errorLines.add(line); + }else{ + addTransactionWithParameters(parameters , errorLines, line); + } + } + + /** + * Add transaction object to the list according to the parameters + * + * @param parameters parameters of the Transaction object. + * @param errorLines list of rows of data which are wrong so far. + * @param line current line number which this transaction data is at in transactionData.txt. + */ + public void addTransactionWithParameters(String[] parameters , ArrayList errorLines , int line) { + assert parameters.length == Transaction.NUMBER_OF_PARAMETERS : "wrong no. of parameter"; + + try { + + String transactionId = parameters[0]; + + if (!Transaction.isValidTxId(transactionId) || FileHandler.containEmptyParameter(parameters)) { + throw new TransactionException(""); + } + + int idNumber = Integer.parseInt(transactionId.substring(2)); + TransactionList.setTxCounter(idNumber); + String carLicensePlate = parameters[1]; + String borrowerName = parameters[2]; + + if(!CarParser.isValidLicensePlateNumber(carLicensePlate) || + !CarList.isExistingLicensePlateNumber(carLicensePlate)){ + throw new CarException(""); + } + + if(!CustomerList.isExistingCustomer(borrowerName)){ + throw new CustomerException(""); + } + + int duration = Integer.parseInt(parameters[3]); + LocalDate startDate = LocalDate.parse(parameters[4], dateTimeFormatter); + boolean isCompleted = Boolean.parseBoolean(parameters[5]); + Transaction transaction = new Transaction(transactionId , carLicensePlate, borrowerName, duration, + startDate, isCompleted); + + TransactionList.addTxWithoutPrintingInfo(transaction); + + }catch (NumberFormatException | DateTimeParseException | CarException | TransactionException | + CustomerException e){ + errorLines.add(line); + } + } + + /** + * Reads the current transaction list and updates transactionData.txt file. + * + * @throws IOException File does not exist. + */ + public void updateTransactionDataFile() throws IOException { + FileWriter fw = new FileWriter(transactionDataFile); + String textToAdd = TransactionList.transactionListToFileString(); + fw.write(textToAdd); + fw.close(); + } + + public String getAbsolutePath(){ + return transactionDataFile.getAbsolutePath(); + } + + public boolean isFileExist(){ + return transactionDataFile.exists(); + } +} diff --git a/src/main/java/parser/CarParser.java b/src/main/java/parser/CarParser.java new file mode 100644 index 0000000000..27857dbd33 --- /dev/null +++ b/src/main/java/parser/CarParser.java @@ -0,0 +1,250 @@ +package parser; + +import car.Car; +import exceptions.CarException; + +/** + * Represents a Parser to parse the user command into a Car object. + */ +public class CarParser { + + private static final String[] ADD_CAR_PARAMETERS = {"/n", "/c", "/p"}; + /** Number of chars to offset to obtain start index of parameters in add-car command */ + private static final int ADD_CAR_PARAMETERS_OFFSET = 2; + private static final int MIN_LICENSE_PLATE_NUMBER_LENGTH = 5; + private static final int MAX_LICENSE_PLATE_NUMBER_LENGTH = 8; + private static final int MAXIMUM_CAR_PRICE = 10000; + + /** + * Parses the add-car user command into a Car object. + *

+ * If all the parameters in the add-car command are valid, a + * new Car object is created and returned. + * + * @param userInput Full command entered by user. + * @return Car object. + * @throws CarException If license plate number or price of Car is invalid. + * @throws NumberFormatException If price is not a numeric value. + */ + public static Car parseIntoCar(String userInput) throws CarException, NumberFormatException{ + userInput = userInput.trim(); + + if (!isValidFormat(userInput)) { + throw CarException.addCarException(); + } + + String carModel = extractCarModel(userInput).trim(); + + String carLicensePlateNumber = extractCarLicensePlateNumber(userInput).trim(); + if (!isValidLicensePlateNumber(carLicensePlateNumber)) { + throw CarException.invalidLicensePlateNumber(); + } + + String carPriceString = extractCarPrice(userInput).trim(); + if (!isParseableToDouble(carPriceString)) { + throw new NumberFormatException("Price must be a numeric value!!"); + } + + double carPrice = Double.parseDouble(carPriceString); + if (!isValidPrice(carPrice)) { + if (carPrice < 0.00) { + throw CarException.negativePrice(); + } else { + throw CarException.invalidPrice(); + } + } + + assert carPrice >= 0.00 : "ERROR.. Car price is negative!!"; + assert carPrice <= 10000 : "ERROR.. Car price exceeded limit of $10 000!!"; + double formattedCarPrice = Double.parseDouble(String.format("%.2f", carPrice)); + + return new Car(carModel, carLicensePlateNumber, formattedCarPrice); + } + + /** + * Extracts the name of the car model from the add-car command. + * + * @param userInput Full command entered by user. + * @return Car model name. + */ + private static String extractCarModel(String userInput) { + int startIndexOfCarModel = userInput.indexOf(ADD_CAR_PARAMETERS[0]) + ADD_CAR_PARAMETERS_OFFSET; + int endIndexOfCarModel = userInput.indexOf(ADD_CAR_PARAMETERS[1]); + + String carModel = userInput.substring(startIndexOfCarModel, endIndexOfCarModel); + if (carModel.trim().isEmpty()) { + throw new CarException("Car model missing!!"); + } + + return carModel; + } + + /** + * Extracts the license plate number of the car from the add-car command. + * + * @param userInput Full command entered by user. + * @return License plate number of car. + */ + private static String extractCarLicensePlateNumber(String userInput) { + //dsa + int startIndexOfLicensePlateNumber = userInput.indexOf(ADD_CAR_PARAMETERS[1]) + + ADD_CAR_PARAMETERS_OFFSET; + int endIndexOfLicensePlateNumber = userInput.indexOf(ADD_CAR_PARAMETERS[2]); + + String carLicensePlateNumber = userInput.substring(startIndexOfLicensePlateNumber, + endIndexOfLicensePlateNumber); + if (carLicensePlateNumber.trim().isEmpty()) { + throw new CarException("License plate number missing!!"); + } + + return carLicensePlateNumber.toUpperCase(); + } + + /** + * Extracts the price of the car from the add-car command. + * + * @param userInput Full command entered by user. + * @return Price of car. + */ + private static String extractCarPrice(String userInput) throws NumberFormatException{ + int startIndexOfPrice = userInput.indexOf(ADD_CAR_PARAMETERS[2]) + ADD_CAR_PARAMETERS_OFFSET; + + String carPrice = userInput.substring(startIndexOfPrice).trim(); + if (carPrice.isEmpty()) { + throw new CarException("Car price missing!!"); + } + + return carPrice; + } + + /** + * Checks if car price can be parsed to a double type value. + * + * @param carPriceString String representation of car price. + * @return true if String can be parsed + * to a double type, false otherwise. + */ + private static boolean isParseableToDouble(String carPriceString) { + try{ + Double.parseDouble(carPriceString); + } catch (NumberFormatException e) { + return false; + } + + return true; + } + + /** + * Checks if the format of the add-car command is valid. + *

+ * A valid format means that all parameter specifiers must be + * included and in the correct order. + * + * @param userInput Full command entered by user. + * @return true if format is valid, false otherwise. + */ + public static boolean isValidFormat(String userInput) { + for (String param : ADD_CAR_PARAMETERS) { + if (!userInput.contains(param)) { + return false; + } + } + + for (int i = 0; i < ADD_CAR_PARAMETERS.length - 1; i++) { + if (userInput.indexOf(ADD_CAR_PARAMETERS[i]) > userInput.indexOf(ADD_CAR_PARAMETERS[i+1])) { + return false; + } + } + + return true; + } + + /** + * Checks if price entered by user is valid. + *

+ * A valid price must be non-negative and less than 10 000. + * + * @param price Price of car specified by user. + * @return true if price is valid, false otherwise. + */ + public static boolean isValidPrice(double price) throws CarException{ + return !(price < 0.00 || price > MAXIMUM_CAR_PRICE); + } + + /** + * Checks if license plate number entered by user is valid. + *

+ * A valid license plate number must start with "S" and have a length of 5 to 8 characters. + *

+ * The license plate number must also conform to the following format: SXX####X, where + *

+ * X represents any letter from A to Z. + *

+ * #### represents any number from 1 to 9999. + * + * @param licensePlateNumber License plate number of car specified by user. + * @return true if license plate number is valid, false otherwise. + */ + public static boolean isValidLicensePlateNumber(String licensePlateNumber) { + licensePlateNumber = licensePlateNumber.toUpperCase(); + + if (!licensePlateNumber.startsWith("S") || + licensePlateNumber.length() < MIN_LICENSE_PLATE_NUMBER_LENGTH || + licensePlateNumber.length() > MAX_LICENSE_PLATE_NUMBER_LENGTH) { + return false; + } + + assert licensePlateNumber.startsWith("S") : "ERROR.. All license plate numbers MUST start with S"; + + char[] licensePlateNumberChars = licensePlateNumber.toCharArray(); + int licensePlateNumberLength = licensePlateNumber.length(); + // Example: SGD1234X + for (int i = 1; i < licensePlateNumberLength; i++) { + // Checks if second, third and last char are (uppercase) letters. + if (i <= 2 || i == licensePlateNumberLength - 1) { + if (licensePlateNumberChars[i] < 'A' || licensePlateNumberChars[i] > 'Z') { + return false; + } + } else { + // Checks if middle chars are numbers. + if (licensePlateNumberChars[i] < '0' || licensePlateNumberChars[i] > '9') { + return false; + } + // Checks if first number is 0 + if (licensePlateNumberChars[3] == '0') { + return false; + } + } + } + + assert licensePlateNumberChars[3] != '0' : "ERROR.. License plate number cannot start with 0"; + return true; + } + + public static String parseCarLicenseForRemoval(String userInput) throws CarException { + userInput = userInput.trim(); + + String licensePlateNumber = extractLicensePlateForRemoval(userInput).trim(); + + if (!isValidLicensePlateNumber(licensePlateNumber)) { + throw CarException.invalidLicensePlateNumber(); + } + + return licensePlateNumber; + } + + private static String extractLicensePlateForRemoval(String userInput) throws CarException{ + String[] splitInput = userInput.split(" "); + + if (splitInput.length != 3) { + throw new CarException("Invalid format for removing a car. Use: remove-car /i [LICENSE_PLATE_NUMBER]"); + } + + String licensePlateNumberIdentifier = splitInput[1]; + if (!licensePlateNumberIdentifier.equals("/i")) { + throw new CarException("Invalid parameter identifier. Use: remove-car /i [LICENSE_PLATE_NUMBER]"); + } + + return splitInput[2]; // Expecting the license plate number to be the second argument + } +} diff --git a/src/main/java/parser/CustomerParser.java b/src/main/java/parser/CustomerParser.java new file mode 100644 index 0000000000..d0a3eb84df --- /dev/null +++ b/src/main/java/parser/CustomerParser.java @@ -0,0 +1,123 @@ +package parser; + +import customer.Customer; +import exceptions.CustomerException; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CustomerParser { + + private static final String ADD_CUSTOMER_COMMAND = "add-user"; + + /** + * Creates new customer object based on user input. + * + * @throws CustomerException if input is not compliant with format. + * @throws NumberFormatException if the age and contact content are not integer string. + */ + public static Customer parseIntoCustomer(String userInput) throws CustomerException, NumberFormatException { + userInput = userInput.substring(ADD_CUSTOMER_COMMAND.length()).trim(); + String[] parameters = { "/u" , "/a" , "/c"}; + String[] parameterContents; + + if(isValidSequence(parameters, userInput)){ + parameterContents = parseParameterContents(parameters, userInput); + } else{ + throw CustomerException.addCustomerException(); + } + + if(!isValidContactNumber(parameterContents[2])){ + throw CustomerException.invalidContactNumberException(); + } + + String customerName = parameterContents[0]; + int age = Integer.parseInt(parameterContents[1]); + + if(age <= 17){ + throw CustomerException.invalidAgeException(); + }else if(age > 100){ + throw CustomerException.invalidMaxAgeException(); + } + + String contactNumber = parameterContents[2]; + assert !customerName.isEmpty() : "Customer name should not be empty."; + assert isValidContactNumber(contactNumber) : "Invalid contact number format."; + return new Customer(customerName , age, contactNumber ); + } + + /** + * Parses the user input according to the parameters based on a fixed sequence. + * + * @param parameters Sequence to parse the input. + * @return content of each parameter in sequence. + * @throws CustomerException when any parameters are empty. + */ + public static String[] parseParameterContents(String[] parameters, String userInput) throws CustomerException { + + String[] contents = new String[parameters.length]; + + for(int i = 0; i < parameters.length - 1; i++){ + int indexOfBeforeParameter = userInput.indexOf(parameters[i]); + int indexOfAfterParameter = userInput.indexOf(parameters[i+1]); + int endOfBeforeParameter = indexOfBeforeParameter + parameters[i].length(); + contents[i] = userInput.substring(endOfBeforeParameter, indexOfAfterParameter).trim(); + } + + int indexOfLastParameter = userInput.indexOf(parameters[parameters.length - 1]); + int endOfLastParameter = indexOfLastParameter + parameters[parameters.length - 1].length(); + contents[parameters.length - 1] = userInput.substring(endOfLastParameter).trim(); + + for(int i = 0; i < parameters.length; i++) { + if(contents[i].isEmpty()){ + throw CustomerException.addCustomerException(); + } + } + + return contents; + } + + /** + * Checks if the string is a valid contact number. + */ + public static boolean isValidContactNumber(String contactNumber) { + String regex = "^[89]\\d{7}$"; // Singapore phone number + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(contactNumber); + return matcher.matches(); + } + + /** + * Checks if the parameters exist and is in sequence. + * + * @param parameters parameters which the input should have , in sequence. + */ + private static boolean isValidSequence(String[] parameters, String userInput){ + + for (String parameter : parameters) { + if (!userInput.contains(parameter)) { + return false; + } + } + + for(int i = 1 ; i < parameters.length ; i++){ + if(userInput.indexOf(parameters[i]) < userInput.indexOf(parameters[i-1])){ + return false; + } + } + + return true; + } + + public static String parseCustomerForRemoval(String userInput) throws CustomerException { + + String[] words = userInput.split("\\s+", 3); + if (words.length < 2 || !Objects.equals(words[1], "/u")) { + throw CustomerException.removeCustomerException(); + } else if (words.length != 3) { + throw CustomerException.missingNameWhenRemoving(); + } else { + return words[2]; // assuming input format is: remove-user + } + } +} diff --git a/src/main/java/parser/HelpParser.java b/src/main/java/parser/HelpParser.java new file mode 100644 index 0000000000..49864aba49 --- /dev/null +++ b/src/main/java/parser/HelpParser.java @@ -0,0 +1,41 @@ +package parser; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class HelpParser { + + // Command descriptions stored in a map + private static final Map commands = new LinkedHashMap<>(); + + static { + commands.put("help", "Provides a list of all commands and their descriptions."); + commands.put("add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER]", "Adds a new customer to the system."); + commands.put("remove-user /u [CUSTOMER_NAME]", "Removes a customer from the system."); + commands.put("remove-all-users", "Remove all customers."); + commands.put("list-users", "Lists all customers."); + commands.put("add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE]", "Adds a new car to the fleet."); + commands.put("remove-car /i [LICENSE_PLATE_NUMBER]", "Removes a car from the fleet."); + commands.put("list-cars", "Lists all cars."); + commands.put("list-rented", "Lists all rented-out cars."); + commands.put("list-available", "Lists all available cars."); + commands.put("remove-all-cars", "Remove all existing cars in the cars list."); + commands.put("add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: ]", + "Adds a new rental transaction."); + commands.put("mark-tx /t [TRANSACTION_ID]", "Marks a rental transaction completed."); + commands.put("unmark-tx /t [TRANSACTION_ID]", "Unmark a rental transaction."); + commands.put("remove-tx /t [TRANSACTION_ID]", "Removes an existing rental transaction."); + commands.put("remove-all-txs", "Removes transactions history"); + commands.put("list-txs", "Lists all transactions."); + commands.put("list-txs-completed", "Lists all completed transactions."); + commands.put("list-txs-uncompleted", "Lists all uncompleted transactions."); + commands.put("find-txs-by-customer /u [CUSTOMER_NAME]", "Finds transactions by a customer's name."); + commands.put("exit", "Exits the program."); + } + + // Method to parse and handle the "help" command + public static void parseHelpCommand() { + System.out.println("Available Commands:"); + commands.forEach((command, description) -> System.out.println(command + " - " + description)); + } +} diff --git a/src/main/java/parser/Parser.java b/src/main/java/parser/Parser.java new file mode 100644 index 0000000000..da3236798a --- /dev/null +++ b/src/main/java/parser/Parser.java @@ -0,0 +1,149 @@ +package parser; + +import car.Car; +import car.CarList; +import customer.Customer; +import customer.CustomerList; +import transaction.Transaction; +import exceptions.CliRentalException; +import transaction.TransactionList; + +import java.util.Scanner; + +public class Parser { + + public static Scanner scanner = new Scanner(System.in); + public static final String ADD_TRANSACTION_COMMAND = "add-tx"; + private static final String HELP_COMMAND = "help"; + private static final String ADD_CUSTOMER_COMMAND = "add-user"; + private static final String REMOVE_CUSTOMER_COMMAND = "remove-user"; + private static final String LIST_USERS_COMMAND = "list-users"; + private static final String REMOVE_ALL_CUSTOMERS_COMMAND = "remove-all-users"; + private static final String ADD_CAR_COMMAND = "add-car"; + private static final String REMOVE_CAR_COMMAND = "remove-car"; + private static final String REMOVE_ALL_CARS_COMMAND = "remove-all-cars"; + private static final String LIST_CARS_COMMAND = "list-cars"; + private static final String LIST_RENTED_CARS_COMMAND = "list-rented"; + private static final String LIST_AVAILABLE_CARS_COMMAND = "list-available"; + private static final String REMOVE_TRANSACTION_COMMAND = "remove-tx"; + private static final String REMOVE_ALL_TRANSACTIONS_COMMAND = "remove-all-txs"; + private static final String MARK_TRANSACTION_COMMAND = "mark-tx"; + private static final String UNMARK_TRANSACTION_COMMAND = "unmark-tx"; + private static final String LIST_ALL_TRANSACTIONS = "list-txs"; + private static final String LIST_COMPLETED_TRANSACTIONS = "list-txs-completed"; + private static final String LIST_UNCOMPLETED_TRANSACTIONS = "list-txs-uncompleted"; + private static final String FIND_TRANSACTIONS_BY_CUSTOMER_COMMAND = "find-txs-by-customer"; + private static final String EXIT_COMMAND = "exit"; + + public static void printDividerLine() { + System.out.println("_".repeat(60)); + } + + public static String getUserInput(){ + System.out.println("What would you like to do?"); + printDividerLine(); + + return scanner.nextLine().trim(); + } + + public static boolean parse(String userInput) throws CliRentalException { + String[] words = userInput.split(" ",2); + String command = words[0].toLowerCase(); + + switch (command) { + case HELP_COMMAND: + HelpParser.parseHelpCommand(); + return false; + case ADD_CUSTOMER_COMMAND: + Customer customer = CustomerParser.parseIntoCustomer(userInput); + CustomerList.addCustomer(customer); + return false; + case REMOVE_CUSTOMER_COMMAND: + if (CustomerList.getCustomers().isEmpty()) { + System.out.println("Customer list is empty. No customer to remove."); + return false; + } + String customerName = CustomerParser.parseCustomerForRemoval(userInput); + CustomerList.removeCustomer(customerName); + return false; + case LIST_USERS_COMMAND: + CustomerList.printCustomers(); + return false; + case REMOVE_ALL_CUSTOMERS_COMMAND: + CustomerList.removeAllCustomers(); + return false; + case ADD_CAR_COMMAND: + try { + Car car = CarParser.parseIntoCar(userInput); + CarList.addCar(car); + } catch (NumberFormatException e) { + System.out.println(e.getMessage()); + } + return false; + case REMOVE_CAR_COMMAND: + try { + String carLicensePlateNumber = CarParser.parseCarLicenseForRemoval(userInput); + CarList.removeCar(carLicensePlateNumber); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return false; + case REMOVE_ALL_CARS_COMMAND: + try { + CarList.removeAllCars(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return false; + case LIST_CARS_COMMAND: + CarList.printCarList(); + return false; + case LIST_RENTED_CARS_COMMAND: + CarList.printRentedCarsList(); + return false; + case LIST_AVAILABLE_CARS_COMMAND: + CarList.printAvailableCarsList(); + return false; + case ADD_TRANSACTION_COMMAND: + try { + Transaction transaction = TransactionParser.parseIntoTransaction(userInput); + TransactionList.addTx(transaction); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + return false; + case REMOVE_TRANSACTION_COMMAND: + TransactionParser.parseRemoveTx(userInput); + return false; + case REMOVE_ALL_TRANSACTIONS_COMMAND: + try { + TransactionList.removeAllTxs(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return false; + case MARK_TRANSACTION_COMMAND: + TransactionParser.parseMarkCompleted(userInput); + return false; + case UNMARK_TRANSACTION_COMMAND: + TransactionParser.parseUnmarkCompleted(userInput); + return false; + case LIST_ALL_TRANSACTIONS: + TransactionList.printAllTransactions(); + return false; + case LIST_COMPLETED_TRANSACTIONS: + TransactionList.printCompletedTransactions(); + return false; + case LIST_UNCOMPLETED_TRANSACTIONS: + TransactionList.printUncompletedTransactions(); + return false; + case FIND_TRANSACTIONS_BY_CUSTOMER_COMMAND: + TransactionParser.parseFindTxsByCustomer(userInput); + return false; + case EXIT_COMMAND: + return true; + default: + throw CliRentalException.unknownCommand(); + } + } +} diff --git a/src/main/java/parser/TransactionParser.java b/src/main/java/parser/TransactionParser.java new file mode 100644 index 0000000000..f767c92364 --- /dev/null +++ b/src/main/java/parser/TransactionParser.java @@ -0,0 +1,178 @@ +package parser; + +import car.CarList; +import customer.CustomerList; +import exceptions.CarException; +import transaction.Transaction; +import transaction.TransactionList; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import static parser.Parser.ADD_TRANSACTION_COMMAND; + +/** + * Parses user commands into Transaction objects or performs actions on TransactionList. + */ +public class TransactionParser { + + public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + public static final String ADD_TRANSACTION_FORMAT = "add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] " + + "/d [DURATION] /s [START_DATE dd-MM-yyyy]"; + public static final String FIND_TRANSACTION_BY_CUSTOMER_FORMAT = "find-txs-by-customer /u [CUSTOMER_NAME]"; + public static final String REMOVE_TRANSACTION_FORMAT = "remove-tx /t [TRANSACTION_ID]"; + public static final String MARK_TRANSACTION_FORMAT = "mark-tx /t [TRANSACTION_ID]"; + public static final String UNMARK_TRANSACTION_FORMAT = "unmark-tx /t [TRANSACTION_ID]"; + + private static final String LICENSE_PLATE_PARAM = "/c"; + private static final String CUSTOMER_NAME_PARAM = "/u"; + private static final String DURATION_PARAM = "/d"; + private static final String START_DATE_PARAM = "/s"; + + /** + * Parses a user input command into a Transaction object. + * + * @param userInput the user input command + * @return a Transaction object based on parsed input + * @throws IllegalArgumentException if the command format or parameters are invalid + */ + public static Transaction parseIntoTransaction(String userInput) throws IllegalArgumentException { + userInput = userInput.substring(ADD_TRANSACTION_COMMAND.length()).trim(); + + String[] parameters = { LICENSE_PLATE_PARAM, CUSTOMER_NAME_PARAM, DURATION_PARAM, START_DATE_PARAM }; + String[] parameterContents; + + if (!isValidSequence(parameters, userInput)) { + throw new IllegalArgumentException("Invalid command format for adding a transaction. Refer to the format: " + + ADD_TRANSACTION_FORMAT); + } + + parameterContents = parseParameterContents(parameters, userInput); + + String carLicensePlate = parameterContents[0].toUpperCase(); + validateLicensePlate(carLicensePlate); + + String customerName = parameterContents[1]; + customerName = validateCustomerName(customerName); + + int duration = parseAndValidateDuration(parameterContents[2]); + + LocalDate startDate = parseAndValidateStartDate(parameterContents[3]); + + return new Transaction(carLicensePlate, customerName, duration, startDate); + } + + private static void validateLicensePlate(String licensePlate) throws CarException { + if (!CarParser.isValidLicensePlateNumber(licensePlate)) { + throw CarException.invalidLicensePlateNumber(); + } + if (!CarList.isExistingLicensePlateNumber(licensePlate)) { + throw CarException.licensePlateNumberNotFound(); + } + } + + private static String validateCustomerName(String customerName) { + if (!CustomerList.isExistingCustomer(customerName)) { + throw new IllegalArgumentException("Customer " + customerName + " does not exist."); + } + return CustomerList.getCustomerNameIfExist(customerName); + } + + private static int parseAndValidateDuration(String durationStr) { + try { + int duration = Integer.parseInt(durationStr); + if (duration < 1 || duration > 365) { + throw new IllegalArgumentException("Invalid duration. Duration must be between 1 and 365 days."); + } + return duration; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid duration format. Duration must be an integer."); + } + } + + private static LocalDate parseAndValidateStartDate(String dateStr) { + try { + return LocalDate.parse(dateStr, dateTimeFormatter); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Invalid date format for startDate. Expected format: dd-MM-yyyy."); + } + } + + private static boolean isValidSequence(String[] parameters, String userInput) { + StringBuilder patternBuilder = new StringBuilder(); + + for (int i = 0; i < parameters.length; i++) { + patternBuilder.append(parameters[i]).append("\\s+"); + + if (i < parameters.length - 1) { + patternBuilder.append("([^/]+?)\\s+"); + } else { + patternBuilder.append("(.+)$"); + } + } + + return userInput.matches(patternBuilder.toString()); + } + + private static String[] parseParameterContents(String[] parameters, String userInput) { + String[] contents = new String[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + int startIndex = userInput.indexOf(parameters[i]) + parameters[i].length(); + int endIndex = userInput.length(); + for (int j = i + 1; j < parameters.length; j++) { + int nextParamIndex = userInput.indexOf(parameters[j], startIndex); + if (nextParamIndex != -1) { + endIndex = nextParamIndex; + break; + } + } + + contents[i] = userInput.substring(startIndex, endIndex).trim(); + + if (contents[i].isEmpty()) { + throw new IllegalArgumentException("Missing value for parameter: " + parameters[i]); + } + } + return contents; + } + + public static void parseFindTxsByCustomer(String userInput) { + String[] words = userInput.split(" ", 3); + if (words.length < 3 || !words[1].equals(CUSTOMER_NAME_PARAM)) { + System.out.println("Unable to search for transaction. Refer to correct format: " + + FIND_TRANSACTION_BY_CUSTOMER_FORMAT); + return; + } + TransactionList.findTxsByCustomer(words[2]); + } + + public static void parseRemoveTx(String userInput) { + String[] words = userInput.split(" ", 3); + if (words.length < 3 || !words[1].equals("/t") || !words[2].toLowerCase().startsWith("tx")) { + System.out.println("Unable to remove transaction. Refer to correct format: " + REMOVE_TRANSACTION_FORMAT); + return; + } + TransactionList.removeTxByTxId(words[2].toLowerCase()); + } + + public static void parseMarkCompleted(String userInput) { + String[] words = userInput.split(" ", 3); + if (words.length < 3 || !words[1].equals("/t") || !words[2].toLowerCase().startsWith("tx")) { + System.out.println("Unable to mark transaction. Refer to correct format: " + MARK_TRANSACTION_FORMAT); + return; + } + TransactionList.markCompletedByTxId(words[2].toLowerCase()); + } + + public static void parseUnmarkCompleted(String userInput) { + String[] words = userInput.split(" ", 3); + if (words.length < 3 || !words[1].equals("/t") || !words[2].toLowerCase().startsWith("tx")) { + System.out.println("Unable to unmark transaction. Refer to correct format: " + UNMARK_TRANSACTION_FORMAT); + return; + } + TransactionList.unmarkCompletedByTxId(words[2].toLowerCase()); + } +} diff --git a/src/main/java/seedu/clirental/CliRental.java b/src/main/java/seedu/clirental/CliRental.java new file mode 100644 index 0000000000..ab054a6abf --- /dev/null +++ b/src/main/java/seedu/clirental/CliRental.java @@ -0,0 +1,57 @@ +package seedu.clirental; + +import exceptions.CarException; +import exceptions.CliRentalException; +import exceptions.CustomerException; +import parser.HelpParser; +import file.FileHandler; +import parser.Parser; + +public class CliRental { + /** + * Main entry-point for the java.clirental.CliRental application. + */ + public static void main(String[] args) { + printGreetings(); + FileHandler.createAndLoadFiles(); + + boolean isExit = false; + + while (!isExit) { + try { + String action = Parser.getUserInput(); + + if (Parser.parse(action)) { + isExit = true; + } + + FileHandler.updateFiles(); + Parser.printDividerLine(); + + } catch (CustomerException exception) { + exception.printErrorMessage(); + } catch (NumberFormatException exception) { + System.out.println("Unable to parse input"); + } catch (CarException | CliRentalException e) { + System.out.println(e.getMessage()); + } + } + System.out.println("Goodbye!"); + } + + public static void printGreetings(){ + String logo = + """ + ______ __ _ ____ __ __ + / ____/ / / (_) / __ \\ ___ ____ / /_ ____ _ / / + / / / / / / / /_/ / / _ \\ / __ \\ / __/ / __ `/ / / + / /___ / / / / / _, _/ / __/ / / / // /_ / /_/ / / / + \\____/ /_/ /_/ /_/ |_| \\___/ /_/ /_/ \\__/ \\__,_/ /_/ + """; + System.out.println(logo); + System.out.println("Hello, thank you for choosing our car rental management program CliRental"); + Parser.printDividerLine(); + HelpParser.parseHelpCommand(); + Parser.printDividerLine(); + } +} 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/transaction/Transaction.java b/src/main/java/transaction/Transaction.java new file mode 100644 index 0000000000..78aec46c60 --- /dev/null +++ b/src/main/java/transaction/Transaction.java @@ -0,0 +1,135 @@ +package transaction; + +import java.time.LocalDate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static parser.TransactionParser.dateTimeFormatter; + +/** + * Represents a transaction associated with a car rental. + */ +public class Transaction { + public static final int NUMBER_OF_PARAMETERS = 6; + private String transactionId; + private final String carLicensePlate; + private final String customer; + private final int duration; + private final LocalDate startDate; + private final LocalDate endDate; + private boolean isCompleted; + + /** + * Constructor for creating a new transaction. + * + * @param carLicensePlate the license plate of the car + * @param customer the customer associated with the transaction + * @param duration the rental duration in days + * @param startDate the start date of the rental period + */ + public Transaction(String carLicensePlate, String customer, int duration, LocalDate startDate) { + this(null, carLicensePlate, customer, duration, startDate, false); + } + + /** + * Constructor for loading an existing transaction. + * + * @param transactionId the unique identifier for the transaction + * @param carLicensePlate the license plate of the car + * @param customer the customer associated with the transaction + * @param duration the rental duration in days + * @param startDate the start date of the rental period + * @param isCompleted the completion status of the transaction + */ + public Transaction(String transactionId, String carLicensePlate, String customer, int duration, + LocalDate startDate, boolean isCompleted) { + this.transactionId = transactionId; + this.carLicensePlate = carLicensePlate; + this.customer = customer; + this.duration = duration; + this.startDate = startDate; + this.endDate = startDate.plusDays(duration); + this.isCompleted = isCompleted; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public int getDuration() { + return duration; + } + + public LocalDate getStartDate() { + return startDate; + } + + public String getTransactionId() { + return transactionId; + } + + public String getCustomer() { + return customer; + } + + public String getCarLicensePlate() { + return carLicensePlate; + } + + public boolean isCompleted() { + return isCompleted; + } + + public void setCompleted(boolean completed) { + isCompleted = completed; + } + + /** + * Converts the duration to a human-readable string. + * + * @return formatted duration as a string + */ + private String formatDuration() { + return duration == 1 ? "1 day" : duration + " days"; + } + + /** + * Validates the format of a transaction ID. + * + * @param transactionId the transaction ID to validate + * @return true if the transaction ID is valid, false otherwise + */ + public static boolean isValidTxId(String transactionId) { + String regex = "TX([1-9]\\d*)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(transactionId); + return matcher.matches(); + } + + /** + * Provides a string representation of the transaction. + * + * @return the string representation + */ + @Override + public String toString() { + String formattedStartDate = startDate.format(dateTimeFormatter); + String formattedEndDate = endDate.format(dateTimeFormatter); + String status = isCompleted ? "[X]" : "[ ]"; + + return String.format("%s %s | %s | %s | %s%nStart Date: %s | End Date: %s", + status, transactionId, carLicensePlate, customer, formatDuration(), + formattedStartDate, formattedEndDate); + } + + /** + * Returns a string representation of the transaction formatted for file storage. + * + * @return formatted string for file storage + */ + public String toFileString() { + return String.format("%s | %s | %s | %d | %s | %b", + transactionId, carLicensePlate, customer, duration, + startDate.format(dateTimeFormatter), isCompleted); + } +} diff --git a/src/main/java/transaction/TransactionList.java b/src/main/java/transaction/TransactionList.java new file mode 100644 index 0000000000..9380795b06 --- /dev/null +++ b/src/main/java/transaction/TransactionList.java @@ -0,0 +1,373 @@ +package transaction; + +import car.CarList; +import customer.Customer; +import customer.CustomerList; +import exceptions.CarException; +import exceptions.CustomerException; +import parser.CarParser; + +import java.util.ArrayList; + +/** + * Manages a list of car rental transactions. + * Provides methods to add, remove, and search transactions, as well as mark transactions as completed. + */ +public class TransactionList { + + // Stores all transactions + private static final ArrayList transactionList = new ArrayList<>(); + private static int txCounter = 1; + + /** + * Sets the transaction counter if the provided counter is greater than the current counter. + * + * @param counter the new transaction counter value + */ + public static void setTxCounter(int counter) { + if (counter > txCounter) { + txCounter = counter; + } + } + + /** + * Adds a transaction to the transaction list and marks the car as rented. + * + * @param transaction the transaction to add + * @throws CarException if the car license plate is invalid or already in use + * @throws CustomerException if the customer is already in the transaction list + */ + public static void addTx(Transaction transaction) throws CarException { + assert transaction != null : "Transaction to add should not be null."; + + String licensePlateNumber = transaction.getCarLicensePlate(); + String uniqueCustomer = transaction.getCustomer(); + assert licensePlateNumber != null : "License plate number should not be null."; + assert uniqueCustomer != null : "Customer should not be null."; + + if (!CarParser.isValidLicensePlateNumber(licensePlateNumber)) { + throw CarException.invalidLicensePlateNumber(); + } + + if (!CarList.isExistingLicensePlateNumber(licensePlateNumber)) { + throw CarException.licensePlateNumberNotFound(); + } + + if (isCustomerInTransactionList(uniqueCustomer)) { + throw CustomerException.customerAlreadyInTransactionList(); + } + + if (isCarInTransactionList(licensePlateNumber)) { + throw CarException.carAlreadyInTransactionList(); + } + + String newTransactionId = "TX" + txCounter++; + transaction.setTransactionId(newTransactionId); + transactionList.add(transaction); + + assert transactionList.contains(transaction) : "Transaction was not added to the list."; + + CarList.markCarAsRented(licensePlateNumber); + System.out.println("Transaction added:"); + System.out.println(transaction); + } + + /** + * Adds a transaction without printing information. + * + * @param transaction the transaction to add + * @throws CustomerException if the customer is already in the transaction list + * @throws CarException if the car license plate is already in the transaction list + */ + public static void addTxWithoutPrintingInfo(Transaction transaction) throws CustomerException, CarException { + assert transaction != null : "Transaction to add should not be null."; + + String licensePlateNumber = transaction.getCarLicensePlate(); + String uniqueCustomer = transaction.getCustomer(); + + if (isCustomerInTransactionList(uniqueCustomer)) { + throw CustomerException.customerAlreadyInTransactionList(); + } + + if (!CarParser.isValidLicensePlateNumber(licensePlateNumber)) { + throw CarException.invalidLicensePlateNumber(); + } + + if (!CarList.isExistingLicensePlateNumber(licensePlateNumber)) { + throw CarException.licensePlateNumberNotFound(); + } + + String newTransactionId = "TX" + txCounter++; + transaction.setTransactionId(newTransactionId); + + if (isCarInTransactionList(licensePlateNumber)) { + throw CarException.carAlreadyInTransactionList(); + } + + transactionList.add(transaction); + CarList.markCarAsRented(licensePlateNumber); + assert transactionList.contains(transaction) : "Transaction was not added to the list."; + } + + /** + * Checks if a customer already exists in the transaction list. + * + * @param customer the customer name to check + * @return true if the customer exists in the transaction list, otherwise false + */ + private static boolean isCustomerInTransactionList(String customer) { + for (Transaction transaction : transactionList) { + if (transaction.getCustomer().equalsIgnoreCase(customer) && !transaction.isCompleted()) { + return true; + } + } + return false; + } + + /** + * Checks if a car with a specific license plate is already in the transaction list. + * + * @param licensePlateNumber the license plate to check + * @return true if the car is in the transaction list, otherwise false + */ + private static boolean isCarInTransactionList(String licensePlateNumber) { + for (Transaction transaction : transactionList) { + if (transaction.getCarLicensePlate().equalsIgnoreCase(licensePlateNumber) && !transaction.isCompleted()) { + return true; + } + } + return false; + } + + /** + * Prints all transactions in the list. + */ + public static void printAllTransactions() { + int index = 1; + + if (transactionList.isEmpty()) { + System.out.println("No transaction available."); + return; + } + + System.out.println("Here are all the transactions:"); + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + System.out.println(index + ") " + transaction); + index++; + } + } + + /** + * Prints all completed transactions. + */ + public static void printCompletedTransactions() { + int index = 1; + + if (transactionList.isEmpty()) { + System.out.println("No transaction available."); + return; + } + + System.out.println("Here are all the completed transactions:"); + + boolean containsCompletedTx = false; + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (transaction.isCompleted()) { + System.out.println(index + ") " + transaction); + index++; + containsCompletedTx = true; + } + } + + if (!containsCompletedTx) { + System.out.println("No completed transaction available."); + } + } + + /** + * Prints all uncompleted transactions. + */ + public static void printUncompletedTransactions() { + int index = 1; + + if (transactionList.isEmpty()) { + System.out.println("No transaction available."); + return; + } + + boolean containsUncompletedTx = false; + + System.out.println("Here are all the uncompleted transactions:"); + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (!transaction.isCompleted()) { + System.out.println(index + ") " + transaction); + index++; + containsUncompletedTx = true; + } + } + + if (!containsUncompletedTx) { + System.out.println("No uncompleted transaction available."); + } + } + + /** + * Removes a transaction by its transaction ID. + * + * @param txId the transaction ID to remove + */ + public static void removeTxByTxId(String txId) { + assert txId != null : "Transaction ID to remove should not be null."; + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (transaction.getTransactionId() != null && transaction.getTransactionId().equalsIgnoreCase(txId)) { + System.out.println("Transaction deleted: " + transaction); + transactionList.remove(transaction); + CarList.markCarAsAvailable(transaction.getCarLicensePlate()); + assert !transactionList.contains(transaction) : "Transaction was not removed from the list."; + return; + } + } + System.out.println("Transaction not found"); + } + + /** + * Removes all transactions from the list and resets the counter. + */ + public static void removeAllTxs() { + for (Transaction transaction : transactionList) { + CarList.markCarAsAvailable(transaction.getCarLicensePlate()); + } + transactionList.clear(); + clearTxCounter(); + System.out.println("All transactions removed!!!"); + } + + /** + * Finds transactions by customer name. + * + * @param customer the customer name to search for + */ + public static void findTxsByCustomer(String customer) { + assert customer != null : "Customer name to find transactions should not be null."; + + // Check if the customer exists in CustomerList + boolean customerExists = false; + String foundCustomer = ""; + for (Customer cust : CustomerList.getCustomerList()) { + assert cust != null : "Customer in the list should not be null."; + if (cust.getCustomerName().toLowerCase().contains(customer.toLowerCase())) { + customerExists = true; + foundCustomer = cust.getCustomerName(); + break; + } + } + + if (!customerExists) { + System.out.println("User " + customer + " was not found"); + return; + } + + // Search for transactions by the specified customer + boolean found = false; + System.out.println("Transaction(s) by " + foundCustomer + " found:"); + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (transaction.getCustomer().toLowerCase().contains(customer.toLowerCase())) { + found = true; + System.out.println(transaction); + } + } + + if (!found) { + System.out.println("None"); + } + } + + /** + * Marks a transaction as completed by its transaction ID. + * + * @param txId the transaction ID to mark as completed + */ + public static void markCompletedByTxId(String txId) { + assert txId != null : "Transaction ID to mark as completed should not be null."; + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (transaction.getTransactionId() != null && transaction.getTransactionId().equalsIgnoreCase(txId)) { + transaction.setCompleted(true); + CarList.markCarAsAvailable(transaction.getCarLicensePlate()); + System.out.println("Transaction completed: " + transaction); + assert transaction.isCompleted() : "Transaction was not marked as completed."; + return; + } + } + System.out.println("Transaction not found"); + } + + /** + * Unmarks a transaction as completed by its transaction ID. + * + * @param txId the transaction ID to unmark as completed + */ + public static void unmarkCompletedByTxId(String txId) { + assert txId != null : "Transaction ID to unmark as completed should not be null."; + + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + if (transaction.getTransactionId() != null && transaction.getTransactionId().equalsIgnoreCase(txId)) { + transaction.setCompleted(false); + CarList.markCarAsRented(transaction.getCarLicensePlate()); + System.out.println("Transaction set uncompleted: " + transaction); + assert !transaction.isCompleted() : "Transaction was not unmarked as completed."; + return; + } + } + System.out.println("Transaction not found"); + } + + /** + * Converts the transaction list to a formatted string for file storage. + * + * @return formatted string representing all transactions + */ + public static String transactionListToFileString() { + StringBuilder transactionData = new StringBuilder(); + for (Transaction transaction : transactionList) { + assert transaction != null : "Transaction in the list should not be null."; + transactionData.append(transaction.toFileString()); + transactionData.append(System.lineSeparator()); + } + return transactionData.toString(); + } + + /** + * Retrieves the transaction list. + * + * @return the list of transactions + */ + public static ArrayList getTransactionList() { + return transactionList; + } + + /** + * Clears the transaction list. + */ + public static void clearTransactionList() { + transactionList.clear(); + } + + /** + * Resets the transaction counter to 1. + */ + public static void clearTxCounter() { + txCounter = 1; + } +} diff --git a/src/test/java/car/CarListTest.java b/src/test/java/car/CarListTest.java new file mode 100644 index 0000000000..c3565bc8f5 --- /dev/null +++ b/src/test/java/car/CarListTest.java @@ -0,0 +1,101 @@ +package car; + +import exceptions.CarException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +public class CarListTest { + + @Test + public void formatPriceToTwoDp_validPriceMoreThanOrEqualToZero_priceFormattedToTwoDp() { + assertEquals("123.08", CarList.formatPriceToTwoDp(123.08)); + assertEquals("123.40", CarList.formatPriceToTwoDp(123.4)); + assertEquals("123.46", CarList.formatPriceToTwoDp(123.456)); + assertEquals("123.00", CarList.formatPriceToTwoDp(123.0000000002)); + assertEquals("123.00", CarList.formatPriceToTwoDp(123)); + } + + @Test + public void addCar_validCarObject_carAddedToList() { + Car car = new Car("Toyota Camry", "SLK12D", 180); + Car car1 = new Car("Nissan Qashqai", "SKA88X", 250); + + CarList.addCar(car); + CarList.addCar(car1); + + assertEquals(2, CarList.getCarsList().size()); + assertEquals(car, CarList.getCarsList().get(0)); + assertEquals(car1, CarList.getCarsList().get(1)); + + CarList.getCarsList().clear(); + } + + @Test + public void addCar_duplicateLicensePlateNumber_carExceptionThrown() throws CarException{ + Car car = new Car("BYD seal", "SND26F", 2500); + CarList.addCar(car); + + Car car1 = new Car("BYD Atto", "SND26F", 1500); + assertThrows(CarException.class, () -> CarList.addCar(car1)); + + CarList.getCarsList().clear(); + } + + @Test + public void removeCar_carRemovedFromList() { + Car car = new Car("Audi TT", "SCD99S", 6800); + Car car1 = new Car("BMW M3", "SCE19N", 4500); + Car car2 = new Car("Mercedes CLA", "SPL5569V", 5800); + + CarList.addCar(car); + CarList.addCar(car1); + CarList.addCar(car2); + + // After adding Car objects and before removing a car + assertEquals(3, CarList.getCarsList().size()); + + // License plate number provided not found in list + CarList.removeCar("SBA1111W"); + assertEquals(3, CarList.getCarsList().size()); + + // License plate number provided found in list + CarList.removeCar("SCE19N"); + assertEquals(2, CarList.getCarsList().size()); + + CarList.getCarsList().clear(); + } + + @Test + public void markCarAsRented_existingLicensePlateNumber_carMarkedAsRented() { + Car car = new Car("Ford Focus", "SKP77Y", 520); + CarList.addCar(car); + + CarList.markCarAsRented("SKP77Y"); + assertTrue(car.isRented()); + + CarList.getCarsList().clear(); + } + + @Test + public void markCarAsAvailable_existingLicensePlateNumber_carMarkedAsAvailable() { + Car car = new Car("Suzuki swift", "SJP9982R", 260); + CarList.addCar(car); + + CarList.markCarAsAvailable("SJP9982R"); + assertFalse(car.isRented()); + + CarList.getCarsList().clear(); + } + + @Test + public void testCarList() { + CarList.addCar(new Car("Honda Civic" , "SGE1234T" , 123)); + CarList.addCar(new Car("Hyundai" , "SFR124T" , 133)); + CarList.printCarList(); + } +} diff --git a/src/test/java/car/CarTest.java b/src/test/java/car/CarTest.java new file mode 100644 index 0000000000..b5fd1c002b --- /dev/null +++ b/src/test/java/car/CarTest.java @@ -0,0 +1,42 @@ +package car; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CarTest { + + @Test + public void testCarGetters_validParams() { + Car car = new Car("Toyota 86", "SJX1234D", 120); + assertEquals("Toyota 86", car.getModel()); + assertEquals("SJX1234D", car.getLicensePlateNumber()); + assertEquals(120, car.getPrice()); + assertFalse(car.isRented()); + assertEquals("Available", car.getRentedStatus()); + + Car car1 = new Car("Honda Fit", "SGE1234X", 50.69); + assertEquals("Honda Fit", car1.getModel()); + assertEquals("SGE1234X", car1.getLicensePlateNumber()); + assertEquals(50.69, car1.getPrice()); + assertFalse(car1.isRented()); + assertEquals("Available", car1.getRentedStatus()); + } + + @Test + public void markAsRented_validCarObject_carMarkedAsRented() { + Car car = new Car("Subaru BRZ", "SDC443M", 123.45); + car.markAsRented(); + assertTrue(car.isRented()); + assertEquals("Rented", car.getRentedStatus()); + } + + @Test + public void markAsAvailable_validCarObject_carMarkedAsAvailable() { + Car car = new Car("Toyota Corolla", "SNE12T", 55.99); + car.markAsAvailable(); + assertFalse(car.isRented()); + assertEquals("Available", car.getRentedStatus()); + } +} diff --git a/src/test/java/customer/CustomerListTest.java b/src/test/java/customer/CustomerListTest.java new file mode 100644 index 0000000000..312df4d47b --- /dev/null +++ b/src/test/java/customer/CustomerListTest.java @@ -0,0 +1,153 @@ +package customer; + +import exceptions.CustomerException; +import org.junit.jupiter.api.BeforeEach; +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 static org.junit.jupiter.api.Assertions.assertThrows; + +public class CustomerListTest { + + @BeforeEach + void setUp() { + CustomerList.removeAllCustomers(); + } + + @Test + public void testAddCustomer() { + Customer customer1 = new Customer("John", 18, "88414916"); + Customer customer2 = new Customer("Mary", 20, "88411416"); + CustomerList.addCustomer(customer1); + CustomerList.addCustomer(customer2); + assertEquals(2, CustomerList.getCustomers().size()); + assertEquals(customer1, CustomerList.getCustomers().get(0)); + assertEquals(customer2, CustomerList.getCustomers().get(1)); + } + + @Test + public void testAddCustomerWithDuplicateName() { + Customer customer1 = new Customer("John", 18, "88414916"); + Customer customer2 = new Customer("john", 20, "88411416"); + CustomerList.addCustomer(customer1); + assertThrows(CustomerException.class, () -> { + CustomerList.addCustomer(customer2); + }); + } + + @Test + public void testAddCustomerWithInvalidCharactersInName() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John@Doe", 25, "92345678"); + }); + + assertEquals("The customer name \"John@Doe\" contains invalid characters. " + + "Only alphabetic characters and spaces are allowed.", exception.getMessage()); + } + + @Test + public void testAddCustomerWithInvalidCharactersInNameUnderscore() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John_Doe", 25, "92345678"); + }); + + assertEquals("The customer name \"John_Doe\" contains invalid characters. " + + "Only alphabetic characters and spaces are allowed.", exception.getMessage()); + } + + @Test + public void testAddCustomerWithInvalidCharactersInNameWithNumbers() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John123", 25, "92345678"); + }); + + assertEquals("The customer name \"John123\" contains invalid characters. " + + "Only alphabetic characters and spaces are allowed.", exception.getMessage()); + } + + @Test + public void testAddCustomerWithInvalidContactNumberTooShort() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John Doe", 25, "12345"); + }); + + assertEquals("Invalid contact number. Format for contact number is [8 DIGITS AND STARTS WITH 8 OR 9]", + exception.getMessage()); + } + + @Test + public void testAddCustomerWithInvalidContactNumberTooLong() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John Doe", 25, "12345678901"); + }); + + assertEquals("Invalid contact number. Format for contact number is [8 DIGITS AND STARTS WITH 8 OR 9]", + exception.getMessage()); + } + + @Test + public void testAddCustomerWithInvalidContactNumberStartingWithInvalidDigit() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John Doe", 25, "71234567"); + }); + + assertEquals("Invalid contact number. Format for contact number is [8 DIGITS AND STARTS WITH 8 OR 9]", + exception.getMessage()); + } + + @Test + public void testAddCustomerWithValidName() { + Customer customer = new Customer("John Doe", 25, "92345678"); + CustomerList.addCustomer(customer); + assertEquals(1, CustomerList.getCustomers().size()); + assertEquals("John Doe", CustomerList.getCustomers().get(0).getCustomerName()); + } + + @Test + public void testRemoveCustomer_existingCustomer() { + Customer customer = new Customer("John Doe", 30, "91234567"); + CustomerList.addCustomerWithoutPrintingInfo(customer); + + CustomerList.removeCustomer("John Doe"); + + int customerListLength = CustomerList.getCustomers().size(); + assertFalse(CustomerList.isExistingCustomer("John Doe")); + assertEquals(0, customerListLength, "Customer list should be empty after removal."); + } + + @Test + public void testRemoveCustomer_nonExistingCustomer() { + Customer customer = new Customer("Jane Doe", 28, "81234567"); + CustomerList.addCustomerWithoutPrintingInfo(customer); + + CustomerList.removeCustomer("John Smith"); + + int customerListLength = CustomerList.getCustomers().size(); + assertTrue(CustomerList.isExistingCustomer("Jane Doe")); + assertFalse(CustomerList.isExistingCustomer("John Smith")); + assertEquals(1, customerListLength, "Customer list should contain 1 customer after non-existent removal."); + } + + @Test + public void testRemoveCustomer_caseInsensitive() { + Customer customer = new Customer("Alice Smith", 35, "91234567"); + CustomerList.addCustomerWithoutPrintingInfo(customer); + + CustomerList.removeCustomer("alice smith"); + + int customerListLength = CustomerList.getCustomers().size(); + assertFalse(CustomerList.isExistingCustomer("Alice Smith")); + assertEquals(0, customerListLength, "Customer list should be empty after removal."); + } + + + + @Test + public void testRemoveCustomer_emptyList() { + CustomerList.removeCustomer("Non Existent Customer"); + int customerListLength = CustomerList.getCustomers().size(); + assertEquals(0, customerListLength, "Customer list should remain empty when there is no customer."); + } +} diff --git a/src/test/java/customer/CustomerTest.java b/src/test/java/customer/CustomerTest.java new file mode 100644 index 0000000000..b01b7fbcc8 --- /dev/null +++ b/src/test/java/customer/CustomerTest.java @@ -0,0 +1,87 @@ +package customer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import exceptions.CustomerException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CustomerTest { + @BeforeEach + void setUp() { + CustomerList.removeAllCustomers(); + } + + @Test + public void testValidCustomerCreation() { + Customer customer = new Customer("John Doe", 25, "92345678"); + assertEquals("John Doe", customer.getCustomerName()); + assertEquals(25, customer.getAge()); + assertEquals("92345678", customer.getContactNumber()); + } + + @Test + public void testAddCustomerWithInvalidAge() { + CustomerException exception = assertThrows(CustomerException.class, () -> { + new Customer("John Doe", 17, "92345678"); // Age is 17, which is invalid + }); + + assertEquals("Illegal driver!! Age should be more than 17!!", exception.getMessage()); + } + + @Test + public void testSetInvalidAge() { + Customer customer = new Customer("John Doe", 25, "92345678"); + + CustomerException exception = assertThrows(CustomerException.class, () -> { + customer.setAge(16); // Attempt to set an invalid age + }); + + assertEquals("Illegal driver!! Age should be more than 17!!", exception.getMessage()); + } + + @Test + public void testAddCustomerWithValidAge() { + Customer customer = new Customer("Jane Doe", 18, "92345678"); // Minimum valid age + assertEquals(18, customer.getAge()); + } + + @Test + public void testCustomerGetters() { + Customer customer = new Customer("John", 18, "97193723"); + assertEquals("John", customer.getCustomerName()); + assertEquals(18, customer.getAge()); + assertEquals("97193723", customer.getContactNumber()); + } + + @Test + public void testSetters() { + Customer customer = new Customer("John", 18, "97193723"); + customer.setCustomerName("Mary"); + customer.setAge(20); + customer.setContactNumber("97529752"); + assertEquals("Mary", customer.getCustomerName()); + assertEquals(20, customer.getAge()); + assertEquals("97529752", customer.getContactNumber()); + } + + @Test + public void testInvalidNameWithSpecialCharacters() { + assertThrows(CustomerException.class, () -> { + new Customer("John@Doe", 25, "91234567"); + }); + } + + @Test + public void testInvalidNameWithNumbers() { + assertThrows(CustomerException.class, () -> { + new Customer("John123", 25, "91234567"); + }); + } + + @Test + public void testValidNameWithSpaces() { + Customer customer = new Customer("John Doe", 30, "92345678"); + assertEquals("John Doe", customer.getCustomerName()); + } +} diff --git a/src/test/java/file/CarFileTest.java b/src/test/java/file/CarFileTest.java new file mode 100644 index 0000000000..5c4bfc491f --- /dev/null +++ b/src/test/java/file/CarFileTest.java @@ -0,0 +1,198 @@ +package file; + +import car.Car; +import car.CarList; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.io.FileWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CarFileTest { + + private static final ArrayList filenames = new ArrayList<>(); + + @BeforeEach + void setUp() { + CarList.clearCarList(); + FileHandler.createFolderIfNotExist(); + } + + @AfterAll + static void tearDown() { + for (String filename : filenames) { + File file = new File(filename); + file.delete(); + } + } + + // Helper method to validate car attributes + private void validateCar(Car car, String expectedModel, String expectedLicensePlate, double expectedPrice, + boolean isRented, boolean isExpensive) { + assertEquals(expectedModel, car.getModel()); + assertEquals(expectedLicensePlate, car.getLicensePlateNumber()); + assertEquals(expectedPrice, car.getPrice(), 0.0); + assertEquals(isRented, car.isRented()); + assertEquals(isExpensive, car.isExpensive()); + } + + @Test + public void testGetCarDataFilename() { + CarFile carFile = new CarFile(); + assertEquals("carData.txt", carFile.getCarDataFilename()); + } + + private static ArrayList inputTestCases() { + CarFile carFile = new CarFile("carData1.txt"); + int line = 1; + ArrayList errorLines = new ArrayList<>(); + String[] parameters = {"Corolla123", "SGM1432K", "1.0", "false", "false"}; + carFile.addCarWithParameters(parameters, errorLines, line); + line++; + parameters = new String[]{"Toyota", "SGK6200F", "0", "false", "true"}; + carFile.addCarWithParameters(parameters, errorLines, line); + line++; + parameters = new String[]{"Toyota Cor", "SGK4932K", "jph", "false", "true"}; + carFile.addCarWithParameters(parameters, errorLines, line); + filenames.add(carFile.getAbsolutePath()); + return errorLines; + } + + @Test + void testAddCarWithParameters() { + ArrayList errorLines = inputTestCases(); + assertEquals(2, CarList.getCarsList().size()); + + Car car1 = CarList.getCarsList().get(0); + validateCar(car1, "Toyota", "SGK6200F", 0.0, false, false); + + Car car2 = CarList.getCarsList().get(1); + validateCar(car2, "Corolla123", "SGM1432K", 1.0, false, true); + + // Check for errors in the line + assertTrue(errorLines.size() == 1 && errorLines.get(0) == 3); + } + + @Test + void testUpdateCarDataFile() { + CarFile carFile = new CarFile("carData2.txt"); + File testFile = new File(carFile.getAbsolutePath()); + carFile.createCarFileIfNotExist(); + assertTrue(testFile.exists()); + + // Add cars to the list + CarList.addCarWithoutPrintingInfo(new Car("Toyota Corolla", "SGM4932K", 10)); + CarList.addCarWithoutPrintingInfo(new Car("Toyota Cor", "S4932K", 1)); + CarList.addCarWithoutPrintingInfo(new Car("Toyota", "SGK", 0)); + + try { + carFile.updateCarDataFile(); + } catch (IOException e) { + assert false; + } + + String[] lines = { + "Toyota | SGK | 0.0 | false | false", + "Toyota Cor | S4932K | 1.0 | false | false", + "Toyota Corolla | SGM4932K | 10.0 | false | true" + }; + + try (Scanner scanner = new Scanner(testFile)) { + int i = 0; + while (scanner.hasNext()) { + assertEquals(lines[i], scanner.nextLine()); + i++; + } + } catch (FileNotFoundException e) { + assert false; + } + + filenames.add(carFile.getAbsolutePath()); + } + + @Test + void testLoadCarDataIfExist() { + CarFile carFile = new CarFile("carData3.txt"); + File testFile = new File(carFile.getAbsolutePath()); + carFile.createCarFileIfNotExist(); + + try (FileWriter fw = new FileWriter(testFile)) { + String textToAdd = + "Toyota Corolla | SGM4932K | 10 | false | false\n" + + "Toyota Cor | SGM4933K | 1 | false | false\n" + + "Toyota Corolla | SGM4933K | 10 | false | false\n" + + "Toyota | SGK1234F | 0 | false | false\n" + + "Toyota | SGK1234F | 0 | false | false\n" + + " | | | | \n" + + "SGE1234X | 10000.0 | false | false\n" + + " | SGE1234X | 10000.0 | false | false\n" + + "Honda Civic | | 10000.0 | false | false\n" + + "Honda Civic | SGE1234X | | false | false\n" + + "Honda Civic | SGE1234X | 10000.0 | | false\n" + + "Honda Civic | SGE1234X | 10000.0 | false |\n" + + "Honda Civic | SGE1234X | -10000.0 | false | false\n" + + "Honda Civic | SGE1234X | 10000.0 | false |\n"; + fw.write(textToAdd); + } catch (IOException e) { + assert false; + } + + carFile.loadCarDataIfExist(); + assertEquals(3, CarList.getCarsList().size()); + + // Validate the cars loaded from the file + Car car1 = CarList.getCarsList().get(0); + validateCar(car1, "Toyota", "SGK1234F", 0.0, false, false); + + Car car2 = CarList.getCarsList().get(1); + validateCar(car2, "Toyota Cor", "SGM4933K", 1.0, false, false); + + Car car3 = CarList.getCarsList().get(2); + validateCar(car3, "Toyota Corolla", "SGM4932K", 10.0, false, true); + + filenames.add(carFile.getAbsolutePath()); + } + + @Test + void scanLineAndAddCar() { + CarFile carFile = new CarFile("carData4.txt"); + File testFile = new File(carFile.getAbsolutePath()); + carFile.createCarFileIfNotExist(); + + try (FileWriter fw = new FileWriter(testFile)) { + String textToAdd = "Toyota Corolla | SGM4932K | 120.0 | false | false"; + fw.write(textToAdd); + } catch (IOException e) { + assert false; + } + + int line = 1; + ArrayList errorLines = new ArrayList<>(); + try (Scanner scanner = new Scanner(testFile)) { + carFile.scanLineAndAddCar(scanner, errorLines, line); + } catch (FileNotFoundException e) { + assert false; + } + + Car car1 = CarList.getCarsList().get(0); + validateCar(car1, "Toyota Corolla", "SGM4932K", 120.0, false, false); + + filenames.add(carFile.getAbsolutePath()); + } + + @Test + void testCreateCarFileIfNotExist() { + CarFile carFile = new CarFile("carData5.txt"); + File testFile = new File(carFile.getAbsolutePath()); + carFile.createCarFileIfNotExist(); + + assertTrue(testFile.exists()); + filenames.add(carFile.getAbsolutePath()); + } +} diff --git a/src/test/java/file/CustomerFileTest.java b/src/test/java/file/CustomerFileTest.java new file mode 100644 index 0000000000..5f0df3ba24 --- /dev/null +++ b/src/test/java/file/CustomerFileTest.java @@ -0,0 +1,173 @@ +package file; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import customer.Customer; +import customer.CustomerList; +import java.util.ArrayList; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Scanner; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CustomerFileTest { + + private static final ArrayList filenames = new ArrayList<>(); + + @BeforeEach + void setUp() { + CustomerList.removeAllCustomers(); + FileHandler.createFolderIfNotExist(); + } + + @AfterAll + static void tearDown() { + for (String filename : filenames) { + File file = new File(filename); + file.delete(); + } + } + + private void validateCustomer(Customer customer, String expectedName, int expectedAge, String expectedContact) { + assertEquals(expectedName, customer.getCustomerName()); + assertEquals(expectedAge, customer.getAge()); + assertEquals(expectedContact, customer.getContactNumber()); + } + + @Test + public void testGetCustomerDataFilename() { + CustomerFile customerFile = new CustomerFile(); + assertEquals("customerData.txt", customerFile.getCustomerDataFilename()); + } + + @Test + void testAddCustomerWithParameters() { + CustomerFile customerFile = new CustomerFile("customerData1.txt"); + int line = 1; + ArrayList errorLines = new ArrayList<>(); + + String[] parameters = {"John Doe", "30", "83456789"}; + customerFile.addCustomerWithParameters(parameters, errorLines, line); + line++; + + parameters = new String[]{"Jane Doe", "abc", "87654321"}; + customerFile.addCustomerWithParameters(parameters, errorLines, line); + + assertEquals(1, CustomerList.getCustomerList().size()); + assertEquals(1, errorLines.size()); + + + Customer customer = CustomerList.getCustomerList().get(0); + validateCustomer(customer, "John Doe", 30, "83456789"); + + assertTrue(errorLines.contains(2)); + } + + @Test + void testUpdateCustomerDataFile() { + CustomerFile customerFile = new CustomerFile("customerData2.txt"); + File testFile = new File(customerFile.getAbsolutePath()); + customerFile.createCustomerFileIfNotExist(); + assertTrue(testFile.exists()); + + CustomerList.addCustomerWithoutPrintingInfo(new Customer("John Doe", 30, "83456789")); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("Jane Smith", 25, "87654321")); + + try { + customerFile.updateCustomerDataFile(); + } catch (IOException e) { + assert false; + } + + String[] expectedLines = { + "John Doe | 30 | 83456789", + "Jane Smith | 25 | 87654321" + }; + + try (Scanner scanner = new Scanner(testFile)) { + int i = 0; + while (scanner.hasNext()) { + assertEquals(expectedLines[i], scanner.nextLine()); + i++; + } + } catch (IOException e) { + assert false; + } + + filenames.add(customerFile.getAbsolutePath()); + } + + @Test + void testLoadCustomerDataIfExist() { + CustomerFile customerFile = new CustomerFile("customerData3.txt"); + File testFile = new File(customerFile.getAbsolutePath()); + customerFile.createCustomerFileIfNotExist(); + + try (FileWriter fw = new FileWriter(testFile)) { + String data = "John Doe | 30 | 83456789\n"; + data += "Jane Smith | 25 | 87654321\n"; + data += "Jane Smith | 25 | 87654321\n"; + data += "john | 10 | +151519515\n"; + data += "john | 18 | 851519515\n"; + data += "john | 18 | 75151915\n"; + data += "john | 18 | +515\n"; + data += "john | 8575715\n"; + data += "john | 18\n"; + data += " | 18 | 151519515\n"; + data += " | | \n"; + fw.write(data); + } catch (IOException e) { + assert false; + } + + customerFile.loadCustomerDataIfExist(); + assertEquals(2, CustomerList.getCustomerList().size()); + + Customer customer1 = CustomerList.getCustomerList().get(0); + validateCustomer(customer1, "John Doe", 30, "83456789"); + + Customer customer2 = CustomerList.getCustomerList().get(1); + validateCustomer(customer2, "Jane Smith", 25, "87654321"); + + filenames.add(customerFile.getAbsolutePath()); + } + + @Test + void testCreateCustomerFileIfNotExist() { + CustomerFile customerFile = new CustomerFile("customerData5.txt"); + File testFile = new File(customerFile.getAbsolutePath()); + customerFile.createCustomerFileIfNotExist(); + assertTrue(testFile.exists()); + filenames.add(customerFile.getAbsolutePath()); + } + + @Test + void testScanLineAndAddCustomer() { + CustomerFile customerFile = new CustomerFile("customerData4.txt"); + File testFile = new File(customerFile.getAbsolutePath()); + customerFile.createCustomerFileIfNotExist(); + + try (FileWriter fw = new FileWriter(testFile)) { + String customerData = "John Doe | 30 | 83456789"; + fw.write(customerData); + } catch (IOException e) { + assert false; + } + + int line = 1; + ArrayList errorLines = new ArrayList<>(); + try (Scanner scanner = new Scanner(testFile)) { + customerFile.scanLineAndAddCustomer(scanner, errorLines, line); + } catch (IOException e) { + assert false; + } + + Customer customer = CustomerList.getCustomerList().get(0); + validateCustomer(customer, "John Doe", 30, "83456789"); + + filenames.add(customerFile.getAbsolutePath()); + } +} diff --git a/src/test/java/file/FileHandlerTest.java b/src/test/java/file/FileHandlerTest.java new file mode 100644 index 0000000000..27b8256fda --- /dev/null +++ b/src/test/java/file/FileHandlerTest.java @@ -0,0 +1,40 @@ +package file; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FileHandlerTest { + + @Test + void getDirName() { + assertEquals("data", FileHandler.getDirName()); + } + + @Test + void createAndLoadFiles() { + FileHandler.createAndLoadFiles(); + CarFile carFile1 = FileHandler.getCarFile(); + CustomerFile customerFile1 = FileHandler.getCustomerFile(); + TransactionFile transactionFile1 = FileHandler.getTransactionFile(); + File carfile = new File(carFile1.getAbsolutePath()); + File customerfile = new File(customerFile1.getAbsolutePath()); + File transactionfile = new File(transactionFile1.getAbsolutePath()); + assertTrue(FileHandler.getDataDir().exists()); + assertTrue(carfile.exists()); + assertTrue(customerfile.exists()); + assertTrue(transactionfile.exists()); + carfile.delete(); + customerfile.delete(); + transactionfile.delete(); + } + + @Test + void createFolderIfNotExist() { + FileHandler.createFolderIfNotExist(); + assertTrue(FileHandler.getDataDir().exists()); + } +} diff --git a/src/test/java/file/TransactionFileTest.java b/src/test/java/file/TransactionFileTest.java new file mode 100644 index 0000000000..cb2edb39b1 --- /dev/null +++ b/src/test/java/file/TransactionFileTest.java @@ -0,0 +1,227 @@ +package file; + +import car.Car; +import car.CarList; +import customer.Customer; +import customer.CustomerList; +import org.junit.jupiter.api.Test; +import transaction.Transaction; +import transaction.TransactionList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import java.io.File; +import java.io.FileWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Scanner; + +class TransactionFileTest { + + private static final ArrayList filenames = new ArrayList<>(); + + @BeforeEach + void setUp() { + TransactionList.clearTransactionList(); + FileHandler.createFolderIfNotExist(); + TransactionList.clearTxCounter(); + CarList.clearCarList(); + CustomerList.clearCustomerList(); + } + + @AfterAll + static void tearDown() { + for (String filename : filenames) { + File file = new File(filename); + file.delete(); + } + } + + private void validateTransaction(Transaction transaction, String expectedId, String expectedCarPlate + , String expectedCustomer, int expectedDuration, LocalDate expectedStartDate + , boolean expectedCompletionStatus) { + assertEquals(expectedId, transaction.getTransactionId()); + assertEquals(expectedCarPlate, transaction.getCarLicensePlate()); + assertEquals(expectedCustomer, transaction.getCustomer()); + assertEquals(expectedDuration, transaction.getDuration()); + assertEquals(expectedStartDate, transaction.getStartDate()); + assertEquals(expectedCompletionStatus, transaction.isCompleted()); + } + + // Helper method to set up test data for adding transactions + private static ArrayList inputTestCases() { + TransactionFile transactionFile = new TransactionFile("transactionData1.txt"); + int line = 1; + + CarList.addCarWithoutPrintingInfo(new Car("1", "SJA9173C", 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1", "SJX1234D", 0.01)); + + CustomerList.addCustomerWithoutPrintingInfo(new Customer("John Doe", 18, "83468393")); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("Jane me", 18, "83468393")); + + ArrayList errorLines = new ArrayList<>(); + String[] parameters = {"TX1", "SJA9173C", "John Doe", "5", "08-11-2024", "false"}; + transactionFile.addTransactionWithParameters(parameters, errorLines, line); + line++; + parameters = new String[]{"TX2", "SJX1234D", "Jane me", "3", "10-10-2024", "true"}; + transactionFile.addTransactionWithParameters(parameters, errorLines, line); + line++; + parameters = new String[]{"TX3", "SJE8720G", "Alice", "seven", "15-09-2024", "false"}; + transactionFile.addTransactionWithParameters(parameters, errorLines, line); + filenames.add(transactionFile.getAbsolutePath()); + + return errorLines; + } + + @Test + public void testGetTransactionDataFilename() { + TransactionFile transactionFile = new TransactionFile(); + assertEquals("transactionData.txt", transactionFile.getTransactionDataFilename()); + } + + @Test + void testAddTransactionWithParameters() { + ArrayList errorLines = inputTestCases(); + assertEquals(2, TransactionList.getTransactionList().size()); + + Transaction transaction1 = TransactionList.getTransactionList().get(0); + validateTransaction(transaction1, "TX1", "SJA9173C", "John Doe" + , 5, LocalDate.of(2024, 11, 8), false); + + Transaction transaction2 = TransactionList.getTransactionList().get(1); + validateTransaction(transaction2, "TX2", "SJX1234D", "Jane me" + , 3, LocalDate.of(2024, 10, 10), true); + + if (errorLines.size() == 1 && errorLines.get(0) == 3) { + assertTrue(true); + } else { + assert false; + } + } + + @Test + void testUpdateTransactionDataFile() { + TransactionFile transactionFile = new TransactionFile("transactionData2.txt"); + File testFile = new File(transactionFile.getAbsolutePath()); + transactionFile.createTransactionFileIfNotExist(); + assertTrue(testFile.exists()); + + CarList.addCarWithoutPrintingInfo(new Car("1", "SJA9173C", 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1", "SJX1234D", 0.01)); + TransactionList.addTxWithoutPrintingInfo(new Transaction("TX1", "SJX1234D" + , "John", 5, LocalDate.of(2024, 11, 8), false)); + TransactionList.addTxWithoutPrintingInfo(new Transaction("TX2", "SJA9173C" + , "Jane", 3, LocalDate.of(2024, 10, 10), true)); + + try { + transactionFile.updateTransactionDataFile(); + } catch (IOException e) { + assert false; + } + + String[] expectedLines = { + "TX1 | SJX1234D | John | 5 | 08-11-2024 | false", + "TX2 | SJA9173C | Jane | 3 | 10-10-2024 | true" + }; + + try (Scanner scanner = new Scanner(testFile)) { + int i = 0; + while (scanner.hasNext()) { + assertEquals(expectedLines[i], scanner.nextLine()); + i++; + } + } catch (FileNotFoundException e) { + assert false; + } + + filenames.add(transactionFile.getAbsolutePath()); + } + + @Test + void testLoadTransactionDataIfExist() { + TransactionFile transactionFile = new TransactionFile("transactionData3.txt"); + File testFile = new File(transactionFile.getAbsolutePath()); + transactionFile.createTransactionFileIfNotExist(); + + CarList.addCarWithoutPrintingInfo(new Car("1", "SJX1234D", 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1", "SJE8720G", 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1", "SJA9173C", 0.01)); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("John", 18 + , "83468393")); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("Jane", 18 + , "83468393")); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("Alice", 18 + , "83468393")); + + try (FileWriter fw = new FileWriter(testFile)) { + String textToAdd = "TX1 | SJX1234D | John | 5 | 08-11-2024 | false\n"; + textToAdd += "TX2 | SJE8720G | Jane | 3 | 10-10-2024 | false\n"; + textToAdd += "TX3 | SJA9173C | Jane | 3 | 10-10-2024 | true\n"; + textToAdd += "TX3 | SJA9173C | Alice | 7 | 15-09-2024 | false\n"; + fw.write(textToAdd); + } catch (IOException e) { + assert false; + } + + transactionFile.loadTransactionDataIfExist(); + assertEquals(3, TransactionList.getTransactionList().size()); + + Transaction transaction1 = TransactionList.getTransactionList().get(0); + validateTransaction(transaction1, "TX1", "SJX1234D", "John" + , 5, LocalDate.of(2024, 11, 8), false); + + Transaction transaction2 = TransactionList.getTransactionList().get(1); + validateTransaction(transaction2, "TX2", "SJE8720G", "Jane" + , 3, LocalDate.of(2024, 10, 10), false); + + Transaction transaction3 = TransactionList.getTransactionList().get(2); + validateTransaction(transaction3, "TX3", "SJA9173C", "Alice" + , 7, LocalDate.of(2024, 9, 15), false); + + filenames.add(transactionFile.getAbsolutePath()); + } + + @Test + void testScanLineAndAddTransaction() { + TransactionFile transactionFile = new TransactionFile("transactionData4.txt"); + File testFile = new File(transactionFile.getAbsolutePath()); + transactionFile.createTransactionFileIfNotExist(); + CarList.addCarWithoutPrintingInfo(new Car("1", "SJX1234D", 0.01)); + CustomerList.addCustomerWithoutPrintingInfo(new Customer("John", 18 + , "83468393")); + + try (FileWriter fw = new FileWriter(testFile)) { + String textToAdd = "TX1 | SJX1234D | John | 5 | 08-11-2024 | false"; + fw.write(textToAdd); + } catch (IOException e) { + assert false; + } + + int line = 1; + ArrayList errorLines = new ArrayList<>(); + try (Scanner scanner = new Scanner(testFile)) { + transactionFile.scanLineAndAddTransaction(scanner, errorLines, line); + } catch (FileNotFoundException e) { + assert false; + } + + Transaction transaction1 = TransactionList.getTransactionList().get(0); + validateTransaction(transaction1, "TX1", "SJX1234D", "John" + , 5, LocalDate.of(2024, 11, 8), false); + + filenames.add(transactionFile.getAbsolutePath()); + } + + @Test + void testCreateTransactionFileIfNotExist() { + TransactionFile transactionFile = new TransactionFile("transactionData5.txt"); + File testFile = new File(transactionFile.getAbsolutePath()); + transactionFile.createTransactionFileIfNotExist(); + + assertTrue(testFile.exists()); + filenames.add(transactionFile.getAbsolutePath()); + } +} diff --git a/src/test/java/file/testFileForCar.txt b/src/test/java/file/testFileForCar.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/java/file/testFileForCustomer.txt b/src/test/java/file/testFileForCustomer.txt new file mode 100644 index 0000000000..730dd14fa2 --- /dev/null +++ b/src/test/java/file/testFileForCustomer.txt @@ -0,0 +1,10 @@ +john | 18 | 91519515 +john | 18 | 91519515 +john | 10 | +151519515 +john | 18 | 851519515 +john | 18 | 75151915 +john | 18 | +515 +john | 8575715 +john | 18 +18 | 151519515 + | | \ No newline at end of file diff --git a/src/test/java/file/testFileForTransaction.txt b/src/test/java/file/testFileForTransaction.txt new file mode 100644 index 0000000000..f274962862 --- /dev/null +++ b/src/test/java/file/testFileForTransaction.txt @@ -0,0 +1,17 @@ +TX1 | SGM4932K | John | 30 | 10-10-2024 | false +TX1 | SGM4932K | John | 30 | 17-13-2024 | false +TX | SGM4932K | John | 30 | 17-13-2024 | false +TX1 | SGM4932K | John | 30 | -2024 | false +TX1 | SGM4932K | John | 12 | 17-13-2024 | false +TX1 | SGM | John | 30 | 17-13-2024 | false +TX1 | SGM4932K | John | 30 | 17-13-2024 | fale +TX1 | SGM4932K | John | 30 | | fale +TX1 | SGM4932K | John | 30 | fale +SGM4932K | John | 30 | 10-10-2024 | false +TX1 | John | 30 | 10-10-2024 | false +TX1 | SGM4932K | 30 | 10-10-2024 | false +TX1 | SGM4932K | John | 10-10-2024 | false +TX1 | SGM4932K | John | 30 | false +TX1 | SGM4932K | John | 30 | 10-10-2024 +idk1 | SGM4932K | John | 30 | 10-10-2024 | false +TX1 | SGM4932K | 1487914 | 1957197591 | 10-10-2024 | false \ No newline at end of file diff --git a/src/test/java/parser/CarParserTest.java b/src/test/java/parser/CarParserTest.java new file mode 100644 index 0000000000..e12d08768c --- /dev/null +++ b/src/test/java/parser/CarParserTest.java @@ -0,0 +1,137 @@ +package parser; + +import car.Car; +import exceptions.CarException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class CarParserTest { + + @Test + public void isValidFormat_validUserInput_expectTrue() { + String userInput = "add-car /n civic /c 12345 /p 150"; + String userInput1 = "add-car /n Abc /c DEF123X /p 57.30"; + + // Valid format refers to correct order/sequence of parameters + assertTrue(CarParser.isValidFormat(userInput)); + assertTrue(CarParser.isValidFormat(userInput1)); + } + + @Test + public void isValidFormat_invalidUserInput_expectFalse() throws CarException, NumberFormatException { + String userInput = "add-car civic ABC123 150"; + String userInput1 = "add-car /c BCE123 /n civic /p 150"; + String userInput2 = "add-car /p 130 /c XYZ888 /n civic"; + + // Valid format refers to correct order/sequence of parameters + assertFalse(CarParser.isValidFormat(userInput)); + assertFalse(CarParser.isValidFormat(userInput1)); + assertFalse(CarParser.isValidFormat(userInput2)); + } + + @Test + public void isValidFormat_missingRequiredFields_expectFalse() { + String userInput = "add-car /n civic /p 150"; // Missing license plate number + String userInput1 = "add-car /c 12345 /p 150"; // Missing car name + + assertFalse(CarParser.isValidFormat(userInput)); + assertFalse(CarParser.isValidFormat(userInput1)); + } + + @Test + public void isValidFormat_boundaryPriceValue_expectTrue() { + String userInput = "add-car /n civic /c 12345 /p 0"; // Boundary price value (0) + String userInput1 = "add-car /n civic /c 12345 /p 99999"; // Upper boundary value for price + + assertTrue(CarParser.isValidFormat(userInput)); + assertTrue(CarParser.isValidFormat(userInput1)); + } + + @Test + public void parseIntoCar_validUserInput_carObjectCreated() { + String userInput = "add-car /n civic /c SGT1234X /p 150"; + + Car car = CarParser.parseIntoCar(userInput); + assertEquals("civic", car.getModel()); + assertEquals("SGT1234X", car.getLicensePlateNumber()); + assertEquals(150, car.getPrice()); + } + + @Test + public void isValidPrice_nonNegativePrice_expectTrue() { + assertTrue(CarParser.isValidPrice(100)); + assertTrue(CarParser.isValidPrice(100.50)); + assertTrue(CarParser.isValidPrice(100.3)); + assertTrue(CarParser.isValidPrice(100.1234560)); + assertTrue(CarParser.isValidPrice(0)); + } + + @Test + public void isValidPrice_negativePrice_expectFalse() { + assertFalse(CarParser.isValidPrice(-10)); + assertFalse(CarParser.isValidPrice(-10.5)); + assertFalse(CarParser.isValidPrice(-10.56)); + } + + @Test + public void isValidLicensePlateNumber_validLicensePlateNumberFormat_expectTrue() { + assertTrue(CarParser.isValidLicensePlateNumber("SGE1234X")); + assertTrue(CarParser.isValidLicensePlateNumber("STD123Y")); + assertTrue(CarParser.isValidLicensePlateNumber("SRC12Z")); + assertTrue(CarParser.isValidLicensePlateNumber("SLB1A")); + } + + @Test + public void isValidLicensePlateNumber_invalidLicensePlateNumberFormat_expectFalse() { + // License plate number doesn't start with S + assertFalse(CarParser.isValidLicensePlateNumber("1234")); + assertFalse(CarParser.isValidLicensePlateNumber("ABC1234")); + + // License plate number length not in the valid range (>= 5 && <= 8) + assertFalse(CarParser.isValidLicensePlateNumber("SG1T")); + assertFalse(CarParser.isValidLicensePlateNumber("SGET12345B")); + + // Numeric part of license plate number contains letters + assertFalse(CarParser.isValidLicensePlateNumber("SGF1A35T")); + assertFalse(CarParser.isValidLicensePlateNumber("SABCDET")); + + // Numeric part of license plate number starts with 0 + assertFalse(CarParser.isValidLicensePlateNumber("SDT0017B")); + } + + @Test + public void parseIntoCar_invalidUserInput_carExceptionThrown() throws CarException { + String userInput = "add-car /n civic /c JKL12345 /p -138"; + String userInput1 = "add-car /n civic ABC123 /p 150"; + + assertThrows(CarException.class, () -> CarParser.parseIntoCar(userInput)); + assertThrows(CarException.class, () -> CarParser.parseIntoCar(userInput1)); + } + + @Test + public void parseIntoCar_boundaryPriceValue_carObjectCreated() { + String userInput = "add-car /n civic /c SCT6677K /p 0"; // Minimum price + String userInput1 = "add-car /n civic /c SPL9773R /p 10000"; // Maximum valid price + + Car car = CarParser.parseIntoCar(userInput); + assertEquals(0, car.getPrice()); + + Car car1 = CarParser.parseIntoCar(userInput1); + assertEquals(10000, car1.getPrice()); + } + + @Test + public void parseIntoCar_invalidPrice_expectCarException() { + String userInput = "add-car /n civic /c SCT1234N /p -100"; // Negative price + String userInput1 = "add-car /n civic /c SBE678L /p abc"; // Non-numeric price + String userInput2 = "add-car /n civic /c SBE678L /p 99999999999999999999"; // Price exceeding limit + + assertThrows(CarException.class, () -> CarParser.parseIntoCar(userInput)); + assertThrows(NumberFormatException.class, () -> CarParser.parseIntoCar(userInput1)); + assertThrows(CarException.class, () -> CarParser.parseIntoCar(userInput2)); + } +} diff --git a/src/test/java/parser/CustomerParserTest.java b/src/test/java/parser/CustomerParserTest.java new file mode 100644 index 0000000000..de9eb47bde --- /dev/null +++ b/src/test/java/parser/CustomerParserTest.java @@ -0,0 +1,114 @@ +package parser; + +import customer.Customer; +import exceptions.CustomerException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CustomerParserTest { + + @Test + void parseParameterContents() { + String userInput = "add-user /u test /a 18 /c 82750174"; + String[] parameters = { "/u", "/a", "/c" }; + String[] contents = CustomerParser.parseParameterContents(parameters, userInput); + assert contents[0].equals("test"); + assert contents[1].equals("18"); + assert contents[2].equals("82750174"); + } + + @Test + void parseIntoCustomer() { + String userInput = "add-user /u test /a 18 /c 82750174"; + Customer customer = CustomerParser.parseIntoCustomer(userInput); + assertEquals("test", customer.getCustomerName()); + assertEquals(18, customer.getAge()); + assertEquals("82750174", customer.getContactNumber()); + } + + @Test + void parseWithValidPhoneNumberStartingWith8() { + String userInput = "add-user /u validUser /a 25 /c 81234567"; + Customer customer = CustomerParser.parseIntoCustomer(userInput); + assertEquals("81234567", customer.getContactNumber()); + } + + @Test + void parseWithValidPhoneNumberStartingWith9() { + String userInput = "add-user /u validUser /a 30 /c 92345678"; + Customer customer = CustomerParser.parseIntoCustomer(userInput); + assertEquals("92345678", customer.getContactNumber()); + } + + @Test + void parseWithInvalidPhoneNumberTooShort() { + String userInput = "add-user /u invalidUser /a 22 /c 8123456"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithInvalidPhoneNumberTooLong() { + String userInput = "add-user /u invalidUser /a 22 /c 8123456789"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithInvalidPhoneNumberStartingWithOtherThan8Or9() { + String userInput = "add-user /u invalidUser /a 22 /c 71234567"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithPhoneNumberStartingWith6() { + String userInput = "add-user /u invalidUser /a 22 /c 61234567"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithPhoneNumberStartingWith0() { + String userInput = "add-user /u invalidUser /a 22 /c 01234567"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithPhoneNumberContainingLetters() { + String userInput = "add-user /u invalidUser /a 22 /c 9ABC5678"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithPhoneNumberContainingSpecialCharacters() { + String userInput = "add-user /u invalidUser /a 22 /c 9234@678"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithPhoneNumberContainingSpace() { + String userInput = "add-user /u invalidUser /a 22 /c 9234 5678"; + assertThrows(CustomerException.class, () -> { + CustomerParser.parseIntoCustomer(userInput); + }); + } + + @Test + void parseWithValidPhoneNumberEightDigitsOnly() { + String userInput = "add-user /u validUser /a 28 /c 98765432"; + Customer customer = CustomerParser.parseIntoCustomer(userInput); + assertEquals("98765432", customer.getContactNumber()); + } +} diff --git a/src/test/java/parser/ParserTest.java b/src/test/java/parser/ParserTest.java new file mode 100644 index 0000000000..f1a0808bf4 --- /dev/null +++ b/src/test/java/parser/ParserTest.java @@ -0,0 +1,89 @@ +package parser; + +import car.CarList; +import customer.CustomerList; +import exceptions.CliRentalException; +import exceptions.CustomerException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class ParserTest { + + @BeforeEach + public void setUp() { + CustomerList.getCustomers().clear(); + CarList.getCarsList().clear(); + } + + @Test + public void parse_validUserInput_commandActionExecuted() throws CliRentalException { + String userInput = "add-user /u John Doe /a 18 /c 82750174"; + + assertFalse(Parser.parse(userInput)); + assertEquals(1, CustomerList.getCustomers().size()); + assertEquals("John Doe", CustomerList.getCustomers().get(0).getCustomerName()); + + String userInput1 = "add-car /n Honda Civic /c SGE4966P /p 123"; + assertFalse(Parser.parse(userInput1)); + assertEquals(1, CarList.getCarsList().size()); + assertEquals(123.0, CarList.getCarsList().get(0).getPrice()); + + String userInput2 = "exit"; + assertTrue(Parser.parse(userInput2)); + } + + @Test + public void parse_invalidUserInput_invalidCharactersInName() { + String userInput = "add-user /u John@Doe /a 25 /c 82750174"; + assertThrows(CustomerException.class, () -> Parser.parse(userInput)); + + String userInput1 = "add-user /u Jane_Doe /a 30 /c 92345678"; + assertThrows(CustomerException.class, () -> Parser.parse(userInput1)); + + String userInput2 = "add-user /u Mike123 /a 22 /c 83456789"; + assertThrows(CustomerException.class, () -> Parser.parse(userInput2)); + + String userInput3 = "add-user /u Sarah! /a 28 /c 94567890"; + assertThrows(CustomerException.class, () -> Parser.parse(userInput3)); + } + + @Test + public void parse_invalidUserInput_duplicateNames() throws CliRentalException { + String userInput = "add-user /u John Doe /a 18 /c 82750174"; + Parser.parse(userInput); + + // Try to add the same name in different cases + String duplicateUserInput1 = "add-user /u john doe /a 21 /c 92345678"; + assertThrows(CustomerException.class, () -> Parser.parse(duplicateUserInput1)); + + String duplicateUserInput2 = "add-user /u JOHN DOE /a 22 /c 83456789"; + assertThrows(CustomerException.class, () -> Parser.parse(duplicateUserInput2)); + + // Exact duplicate + String duplicateUserInput3 = "add-user /u John Doe /a 25 /c 91234567"; + assertThrows(CustomerException.class, () -> Parser.parse(duplicateUserInput3)); + } + + @Test + public void parse_invalidUserInput_tooYoung() { + String userInput = "add-user /u John Doe /a 16 /c 82750174"; // Age below minimum + assertThrows(CustomerException.class, () -> Parser.parse(userInput)); + } + + @Test + public void parse_invalidUserInput_invalidContactNumber() { + String userInput = "add-user /u John Doe /a 25 /c 123456"; // Too short + assertThrows(CustomerException.class, () -> Parser.parse(userInput)); + + String userInput1 = "add-user /u Jane Doe /a 30 /c 9123456789"; // Too long + assertThrows(CustomerException.class, () -> Parser.parse(userInput1)); + + String userInput2 = "add-user /u Mike Doe /a 22 /c 71234567"; // Starts with invalid digit + assertThrows(CustomerException.class, () -> Parser.parse(userInput2)); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/clirental/CliRental.java similarity index 78% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/seedu/clirental/CliRental.java index 2dda5fd651..dab8cfaac4 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/clirental/CliRental.java @@ -1,10 +1,9 @@ -package seedu.duke; +package seedu.clirental; import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; -class DukeTest { +class CliRentalTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/transaction/TransactionListTest.java b/src/test/java/transaction/TransactionListTest.java new file mode 100644 index 0000000000..3c0e96efaa --- /dev/null +++ b/src/test/java/transaction/TransactionListTest.java @@ -0,0 +1,589 @@ +package transaction; + +import car.Car; +import car.CarList; +import customer.Customer; +import customer.CustomerList; +import exceptions.CarException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import parser.CarParser; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class TransactionListTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + void setUp() { + // Redirect System.out to capture print statements + System.setOut(new PrintStream(outContent)); + + // Clear the transaction list before each test + TransactionList.getTransactionList().clear(); + CarList.clearCarList(); + // Reset the transaction counter using reflection to ensure predictable transaction IDs + try { + java.lang.reflect.Field counterField = TransactionList.class.getDeclaredField("txCounter"); + counterField.setAccessible(true); + counterField.setInt(null, 1); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + fail("Failed to reset transactionCounter"); + } + } + + @AfterEach + void tearDown() { + // Restore the original System.out + System.setOut(originalOut); + } + + @Test + @DisplayName("Test adding a valid transaction with correct license plate format") + void testAddTxValid() { + try (MockedStatic carParserMock = Mockito.mockStatic(CarParser.class); + MockedStatic carListMock = Mockito.mockStatic(CarList.class)) { + + // Define a valid license plate adhering to SXX####X format + String validLicensePlate = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Mock the static methods + carParserMock.when(() -> CarParser.isValidLicensePlateNumber(validLicensePlate)).thenReturn(true); + carListMock.when(() -> CarList.isExistingLicensePlateNumber(validLicensePlate)).thenReturn(true); + + // Create a transaction + Transaction transaction = new Transaction(validLicensePlate, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + + // Add the transaction + TransactionList.addTx(transaction); + + // Verify that the transaction was added + assertEquals(1, TransactionList.getTransactionList().size(), + "Transaction should be added to the list"); + assertEquals(transaction, TransactionList.getTransactionList().get(0), + "Added transaction should match"); + + // Verify that CarList.markCarAsRented was called + carListMock.verify(() -> CarList.markCarAsRented(validLicensePlate), + Mockito.times(1)); + + // Verify the printed output + String expectedOutput = "Transaction added:" + System.lineSeparator() + transaction + + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Printed output should match expected"); + } + } + + @Test + @DisplayName("Test adding a transaction with invalid license plate format") + void testAddTxInvalidLicensePlateFormat() { + try (MockedStatic carParserMock = Mockito.mockStatic(CarParser.class)) { + + // Define an invalid license plate not adhering to SXX####X format + String invalidLicensePlate = "TAB12345"; // Does not start with 'S' and has too many characters + + // Mock the static method to return false + carParserMock.when(() -> CarParser.isValidLicensePlateNumber(invalidLicensePlate)).thenReturn(false); + + // Create a transaction with invalid license plate + Transaction transaction = new Transaction(invalidLicensePlate, "Jane Doe", 3, + LocalDate.of(2024, 10, 2)); + + // Attempt to add the transaction and expect an exception + CarException exception = assertThrows(CarException.class, () -> TransactionList.addTx(transaction), + "Adding a transaction with invalid license plate should throw CarException"); + + assertEquals(""" + Oops!! License Plate number is invalid... + + License plate number format: SXX####X + X -> Letters [A - Z], #### -> Numbers [1 - 9999]""", exception.getMessage(), + "Exception message should match"); + + // Verify that no transaction was added + assertTrue(TransactionList.getTransactionList().isEmpty(), "Transaction list should remain empty"); + } + } + + @Test + @DisplayName("Test adding a transaction with non-existing license plate number") + void testAddTxLicensePlateNotFound() { + try (MockedStatic carParserMock = Mockito.mockStatic(CarParser.class); + MockedStatic carListMock = Mockito.mockStatic(CarList.class)) { + + // Define a valid license plate format but not existing in CarList + String validButNonExistingLicensePlate = "SCD5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SCD5678Z" , 0.01)); + // Mock the static methods + carParserMock.when(() -> CarParser.isValidLicensePlateNumber(validButNonExistingLicensePlate)). + thenReturn(true); + carListMock.when(() -> CarList.isExistingLicensePlateNumber(validButNonExistingLicensePlate)). + thenReturn(false); + + // Create a transaction with a valid but non-existing license plate + Transaction transaction = new Transaction(validButNonExistingLicensePlate, "Alice Smith", + 2, LocalDate.of(2024, 10, 3)); + + // Attempt to add the transaction and expect an exception + CarException exception = assertThrows(CarException.class, () -> TransactionList.addTx(transaction), + "Adding a transaction with non-existing license plate should throw CarException"); + + assertEquals("Car license plate number not found!!" + System.lineSeparator() + + "Use command to view list of available cars.", + exception.getMessage(), "Exception message should match"); + + // Verify that no transaction was added + assertTrue(TransactionList.getTransactionList().isEmpty(), "Transaction list should remain empty"); + } + } + + @Test + @DisplayName("Test adding a transaction without printing info") + void testAddTxWithoutPrintingInfo() { + // Define a valid license plate adhering to SXX####X format + String validLicensePlate = "SXY9012A"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY9012A" , 0.01)); + + // Create a transaction + Transaction transaction = new Transaction(validLicensePlate, "Bob Brown", 4, + LocalDate.of(2024, 10, 4)); + + // Add the transaction without printing info + TransactionList.addTxWithoutPrintingInfo(transaction); + + // Verify that the transaction was added + assertEquals(1, TransactionList.getTransactionList().size(), + "Transaction should be added to the list"); + assertEquals(transaction, TransactionList.getTransactionList().get(0), + "Added transaction should match"); + + // Verify that nothing was printed + assertTrue(outContent.toString().isEmpty(), "No output should be printed"); + } + + @Test + @DisplayName("Test printing all transactions when list is empty") + void testPrintAllTransactionsEmpty() { + TransactionList.printAllTransactions(); + + String expectedOutput = "No transaction available." + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), + "Should indicate that no transactions are available"); + } + + @Test + @DisplayName("Test printing all transactions") + void testPrintAllTransactions() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + TransactionList.printAllTransactions(); + + String expectedOutput = "Here are all the transactions:" + System.lineSeparator() + + "1) " + tx1 + System.lineSeparator() + + "2) " + tx2 + System.lineSeparator(); + + assertEquals(expectedOutput, outContent.toString(), "Printed transactions should match the list"); + } + + @Test + @DisplayName("Test printing completed transactions") + void testPrintCompletedTransactions() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + tx1.setCompleted(true); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + TransactionList.printCompletedTransactions(); + + String expectedOutput = "Here are all the completed transactions:" + System.lineSeparator() + + "1) " + tx1 + System.lineSeparator(); + + assertEquals(expectedOutput, outContent.toString(), "Printed completed transactions should match"); + } + + @Test + @DisplayName("Test printing uncompleted transactions") + void testPrintUncompletedTransactions() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + tx2.setCompleted(true); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + TransactionList.printUncompletedTransactions(); + + String expectedOutput = "Here are all the uncompleted transactions:" + System.lineSeparator() + + "1) " + tx1 + System.lineSeparator(); + + assertEquals(expectedOutput, outContent.toString(), "Printed uncompleted transactions should match"); + } + + @Test + @DisplayName("Test removing a transaction by transaction ID") + void testRemoveTxByTxId() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + // Remove tx1 + TransactionList.removeTxByTxId("tx1"); // Assuming case-insensitive + + // Verify that tx1 is removed + assertEquals(1, TransactionList.getTransactionList().size(), + "Transaction list should have one transaction after removal"); + assertEquals(tx2, TransactionList.getTransactionList().get(0), "Remaining transaction should be tx2"); + + // Verify the printed output + String expectedOutput = "Transaction deleted: " + tx1 + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Printed output should confirm deletion"); + } + + @Test + @DisplayName("Test removing a transaction with non-existing transaction ID") + void testRemoveTxByTxIdNotFound() { + // Define a valid license plate adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + // Attempt to remove a non-existing transaction + TransactionList.removeTxByTxId("tx999"); + + // Verify that the transaction list remains unchanged + assertEquals(1, TransactionList.getTransactionList().size(), + "Transaction list should remain unchanged"); + + // Verify the printed output + String expectedOutput = "Transaction not found" + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Should indicate that transaction was not found"); + } + + @Test + @DisplayName("Test finding transactions by customer") + void testFindTxsByCustomer() { + // Add users to CustomerList + CustomerList.addCustomer(new Customer("John Doe", 25, "92345678")); + CustomerList.addCustomer(new Customer("Jane Smith", 30, "93456789")); + CustomerList.addCustomer(new Customer("Mike Johnson", 28, "91234567")); + + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + String licensePlate3 = "SCD9012A"; + String licensePlate4 = "SGD4091D"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SCD9012A" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SGD4091D" , 0.01)); + + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + TransactionList.addTxWithoutPrintingInfo(tx2); + + Transaction tx3 = new Transaction(licensePlate3, "Mike Johnson", 2, + LocalDate.of(2024, 10, 3)); + TransactionList.addTxWithoutPrintingInfo(tx3); + + // Mark tx1 as completed + TransactionList.markCompletedByTxId(tx1.getTransactionId()); + + Transaction tx4 = new Transaction(licensePlate4, "John Doe", 2, + LocalDate.of(2024, 10, 3)); + TransactionList.addTxWithoutPrintingInfo(tx4); + + // Find transactions by "John Doe" + TransactionList.findTxsByCustomer("john doe"); + + String expectedOutput = + "Customer added" + System.lineSeparator() + + "Customer name: John Doe\n" + + "Age: 25\n" + + "Contact Number: 92345678" + System.lineSeparator() + + "Customer added" + System.lineSeparator() + + "Customer name: Jane Smith\n" + + "Age: 30\n" + + "Contact Number: 93456789" + System.lineSeparator() + + "Customer added" + System.lineSeparator() + + "Customer name: Mike Johnson\n" + + "Age: 28\n" + + "Contact Number: 91234567" + System.lineSeparator() + + "Transaction completed: [X] TX1 | SAB1234C | John Doe | 5 days" + System.lineSeparator() + + "Start Date: 01-10-2024 | End Date: 06-10-2024" + System.lineSeparator() + + "Transaction(s) by John Doe found:" + System.lineSeparator() + + "[X] " + tx1.getTransactionId() + " | " + licensePlate1 + + " | John Doe | 5 days" + System.lineSeparator() + + "Start Date: 01-10-2024 | End Date: 06-10-2024" + System.lineSeparator() + + "[ ] " + tx4.getTransactionId() + " | " + licensePlate4 + + " | John Doe | 2 days" + System.lineSeparator() + + "Start Date: 03-10-2024 | End Date: 05-10-2024"; + + String actualOutput = outContent.toString(); + + // Check that the found transactions are printed + assertTrue(actualOutput.contains("Transaction(s) by John Doe found:"), + "Should indicate transactions found by customer"); + assertTrue(actualOutput.contains(tx1.toString()), "Should contain tx1 details"); + assertTrue(actualOutput.contains(tx4.toString()), "Should contain tx4 details"); + assertEquals(expectedOutput, actualOutput.trim(), "Printed output should confirm deletion"); + } + + + @Test + @DisplayName("Test finding transactions by customer with no matches") + void testFindTxsByCustomerNoMatches() { + // Define a valid license plate adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + // Attempt to find transactions by a non-existing customer + TransactionList.findTxsByCustomer("Alice Johnson"); + + String expectedOutput = "User Alice Johnson was not found" + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Should indicate that no transactions was found"); + } + + @Test + @DisplayName("Test marking a transaction as completed by transaction ID") + void testMarkCompletedByTxId() { + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + // Mark the transaction as completed + TransactionList.markCompletedByTxId(tx1.getTransactionId().toLowerCase()); + + // Verify that the transaction is marked as completed + assertTrue(tx1.isCompleted(), "Transaction should be marked as completed"); + + // Verify the printed output + String expectedOutput = "Transaction completed: " + tx1 + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Printed output should confirm completion"); + } + + @Test + @DisplayName("Test marking a transaction as completed with non-existing transaction ID") + void testMarkCompletedByTxIdNotFound() { + // Define a valid license plate adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + // Attempt to mark a non-existing transaction as completed + TransactionList.markCompletedByTxId("TX999"); + + // Verify that the transaction is not marked as completed + assertFalse(tx1.isCompleted(), "Transaction should remain uncompleted"); + + // Verify the printed output + String expectedOutput = "Transaction not found" + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Should indicate that transaction was not found"); + } + + @Test + @DisplayName("Test unmarking a transaction as completed by transaction ID") + void testUnmarkCompletedByTxId() { + // Define a valid license plate adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction and mark it as completed + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + tx1.setCompleted(true); + + // Unmark the transaction as completed + TransactionList.unmarkCompletedByTxId(tx1.getTransactionId().toLowerCase()); // Assuming case-insensitive + + // Verify that the transaction is unmarked + assertFalse(tx1.isCompleted(), "Transaction should be marked as uncompleted"); + + // Verify the printed output + String expectedOutput = "Transaction set uncompleted: " + tx1 + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Printed output should confirm un-completion"); + } + + @Test + @DisplayName("Test unmarking a transaction as completed with non-existing transaction ID") + void testUnmarkCompletedByTxIdNotFound() { + // Define a valid license plate adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + // Add a transaction + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + TransactionList.addTxWithoutPrintingInfo(tx1); + + // Attempt to unmark a non-existing transaction + TransactionList.unmarkCompletedByTxId("TX999"); + + // Verify that the transaction remains unchanged + assertFalse(tx1.isCompleted(), "Transaction should remain uncompleted"); + + // Verify the printed output + String expectedOutput = "Transaction not found" + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString(), "Should indicate that transaction was not found"); + } + + @Test + @DisplayName("Test converting transaction list to file string") + void testTransactionListToFileString() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + String expectedFileString = tx1.toFileString() + System.lineSeparator() + tx2.toFileString() + + System.lineSeparator(); + String actualFileString = TransactionList.transactionListToFileString(); + + assertEquals(expectedFileString, actualFileString, + "File string representation should match expected format"); + } + + @Test + @DisplayName("Test retrieving the transaction list") + void testGetTransactionList() { + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + // Add transactions without printing info + Transaction tx1 = new Transaction(licensePlate1, "John Doe", 5, + LocalDate.of(2024, 10, 1)); + Transaction tx2 = new Transaction(licensePlate2, "Jane Smith", 3, + LocalDate.of(2024, 10, 2)); + TransactionList.addTxWithoutPrintingInfo(tx1); + TransactionList.addTxWithoutPrintingInfo(tx2); + + ArrayList transactions = TransactionList.getTransactionList(); + + assertEquals(2, transactions.size(), "Transaction list should contain two transactions"); + assertTrue(transactions.contains(tx1), "Transaction list should contain tx1"); + assertTrue(transactions.contains(tx2), "Transaction list should contain tx2"); + } + + @Test + @DisplayName("Test adding multiple transactions and verifying transaction IDs") + void testMultipleTransactionIds() { + try (MockedStatic carParserMock = Mockito.mockStatic(CarParser.class); + MockedStatic carListMock = Mockito.mockStatic(CarList.class)) { + + // Define valid license plates adhering to SXX####X format + String licensePlate1 = "SAB1234C"; + String licensePlate2 = "SXY5678Z"; + String licensePlate3 = "SCD9012A"; + + CarList.addCarWithoutPrintingInfo(new Car("1" , "SAB1234C" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SXY5678Z" , 0.01)); + CarList.addCarWithoutPrintingInfo(new Car("1" , "SCD9012A" , 0.01)); + // Mock the static methods to always return valid + carParserMock.when(() -> CarParser.isValidLicensePlateNumber(Mockito.anyString())). + thenAnswer(invocation -> { + String plate = invocation.getArgument(0); + return plate.matches("S[A-Z]{2}\\d{4}[A-Z]"); + }); + carListMock.when(() -> CarList.isExistingLicensePlateNumber(Mockito.anyString())).thenReturn(true); + + // Add multiple transactions + Transaction tx1 = new Transaction(licensePlate1, "Alice", 2, + LocalDate.of(2024, 10, 5)); + Transaction tx2 = new Transaction(licensePlate2, "Bob", 3, + LocalDate.of(2024, 10, 6)); + Transaction tx3 = new Transaction(licensePlate3, "Charlie", 1, + LocalDate.of(2024, 10, 7)); + + TransactionList.addTx(tx1); + TransactionList.addTx(tx2); + TransactionList.addTx(tx3); + + // Verify transaction IDs + assertEquals("TX1", tx1.getTransactionId(), "First transaction ID should be TX1"); + assertEquals("TX2", tx2.getTransactionId(), "Second transaction ID should be TX2"); + assertEquals("TX3", tx3.getTransactionId(), "Third transaction ID should be TX3"); + + // Verify that all transactions are added + assertEquals(3, TransactionList.getTransactionList().size(), + "All transactions should be added"); + } + } +} diff --git a/src/test/java/transaction/TransactionTest.java b/src/test/java/transaction/TransactionTest.java new file mode 100644 index 0000000000..f5a53506d6 --- /dev/null +++ b/src/test/java/transaction/TransactionTest.java @@ -0,0 +1,114 @@ +package transaction; + +import customer.Customer; +import customer.CustomerList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +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 parser.TransactionParser.dateTimeFormatter; + +class TransactionTest { + private Transaction transaction1; + private Transaction transaction2; + private int transactionCounter = 1; + + private String generateTransactionId() { + return "TX" + transactionCounter++; + } + + @BeforeEach + void setUp() { + CustomerList.removeAllCustomers(); // Clear customers list before each test + CustomerList.addCustomer(new Customer("John Doe", 25, "92345678")); + CustomerList.addCustomer(new Customer("Jane Smith", 30, "93456789")); + LocalDate startDate1 = LocalDate.parse("01-10-2024", dateTimeFormatter); + LocalDate startDate2 = LocalDate.parse("02-10-2024", dateTimeFormatter); + transaction1 = new Transaction("SGA1234A", "John Doe", 5, startDate1); + transaction1.setTransactionId(generateTransactionId()); + transaction2 = new Transaction("SGZ5678B", "Jane Smith", 3, startDate2); + transaction2.setTransactionId(generateTransactionId()); + } + + @Test + void testTransactionIdGeneration() { + assertEquals("TX1", transaction1.getTransactionId()); + assertEquals("TX2", transaction2.getTransactionId()); + } + + @Test + void testGetCarLicensePlate() { + assertEquals("SGA1234A", transaction1.getCarLicensePlate(), "Car license plate should match"); + assertEquals("SGZ5678B", transaction2.getCarLicensePlate(), "Car license plate should match"); + } + + @Test + void testGetCustomer() { + assertEquals("John Doe", transaction1.getCustomer(), "Customer name should match"); + assertEquals("Jane Smith", transaction2.getCustomer(), "Customer name should match"); + } + + @Test + void testIsCompletedInitiallyFalse() { + assertFalse(transaction1.isCompleted(), "New transaction should not be completed"); + assertFalse(transaction2.isCompleted(), "New transaction should not be completed"); + } + + @Test + void testSetCompleted() { + transaction1.setCompleted(true); + assertTrue(transaction1.isCompleted(), "Transaction should be marked as completed"); + + transaction2.setCompleted(true); + assertTrue(transaction2.isCompleted(), "Transaction should be marked as completed"); + + transaction2.setCompleted(false); + assertFalse(transaction2.isCompleted(), "Transaction should be marked as not completed"); + } + + @Test + void testToStringWhenNotCompleted() { + String expected1 = "[ ] " + transaction1.getTransactionId() + + " | SGA1234A | John Doe | 5 days" + System.lineSeparator() + "Start Date: 01-10-2024" + + " | End Date: 06-10-2024"; + String expected2 = "[ ] " + transaction2.getTransactionId() + + " | SGZ5678B | Jane Smith | 3 days" + System.lineSeparator() + "Start Date: 02-10-2024" + + " | End Date: 05-10-2024"; + + assertEquals(expected1, transaction1.toString(), "toString should match expected format when not completed"); + assertEquals(expected2, transaction2.toString(), "toString should match expected format when not completed"); + } + + @Test + void testToStringWhenCompleted() { + transaction1.setCompleted(true); + transaction2.setCompleted(true); + + String expected1 = "[X] " + transaction1.getTransactionId() + + " | SGA1234A | John Doe | 5 days" + System.lineSeparator() + "Start Date: 01-10-2024" + + " | End Date: 06-10-2024"; + String expected2 = "[X] " + transaction2.getTransactionId() + + " | SGZ5678B | Jane Smith | 3 days" + System.lineSeparator() + "Start Date: 02-10-2024" + + " | End Date: 05-10-2024"; + + assertEquals(expected1, transaction1.toString(), "toString should match expected format when completed"); + assertEquals(expected2, transaction2.toString(), "toString should match expected format when completed"); + } + + @Test + void testMultipleTransactionIds() { + LocalDate startDate3 = LocalDate.parse("03-10-2024", dateTimeFormatter); + LocalDate startDate4 = LocalDate.parse("04-10-2024", dateTimeFormatter); + Transaction transaction3 = new Transaction("SGB4321C", "Alice Johnson", 2, startDate3); + transaction3.setTransactionId(generateTransactionId()); + Transaction transaction4 = new Transaction("SGD8765D", "Bob Brown", 4, startDate4); + transaction4.setTransactionId(generateTransactionId()); + + assertEquals("TX3", transaction3.getTransactionId(), "Third transaction ID should be TX3"); + assertEquals("TX4", transaction4.getTransactionId(), "Fourth transaction ID should be TX4"); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..bc2663af4c 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,153 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ______ __ _ ____ __ __ + / ____/ / / (_) / __ \ ___ ____ / /_ ____ _ / / + / / / / / / / /_/ / / _ \ / __ \ / __/ / __ `/ / / + / /___ / / / / / _, _/ / __/ / / / // /_ / /_/ / / / + \____/ /_/ /_/ /_/ |_| \___/ /_/ /_/ \__/ \__,_/ /_/ -What is your name? -Hello James Gosling +Hello, thank you for choosing our car rental management program CliRental +____________________________________________________________ +Available Commands: +help - Provides a list of all commands and their descriptions. +add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER] - Adds a new customer to the system. +remove-user /u [CUSTOMER_NAME] - Removes a customer from the system. +remove-all-users - Remove all customers. +list-users - Lists all customers. +add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE] - Adds a new car to the fleet. +remove-car /i [LICENSE_PLATE_NUMBER] - Removes a car from the fleet. +list-cars - Lists all cars. +list-rented - Lists all rented-out cars. +list-available - Lists all available cars. +remove-all-cars - Remove all existing cars in the cars list. +add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: ] - Adds a new rental transaction. +mark-tx /t [TRANSACTION_ID] - Marks a rental transaction completed. +unmark-tx /t [TRANSACTION_ID] - Unmark a rental transaction. +remove-tx /t [TRANSACTION_ID] - Removes an existing rental transaction. +remove-all-txs - Removes transactions history +list-txs - Lists all transactions. +list-txs-completed - Lists all completed transactions. +list-txs-uncompleted - Lists all uncompleted transactions. +find-txs-by-customer /u [CUSTOMER_NAME] - Finds transactions by a customer's name. +exit - Exits the program. +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Customer added +Customer name: Obama +Age: 42 +Contact Number: 80009000 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Customer added +Customer name: Trump +Age: 78 +Contact Number: 89965443 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Car added to list +Car details: +Audi A4 | SZX789C | $248.00 | Available | Affordable | Median price: 248.0 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Car added to list +Car details: +Ford F150 | STV968Z | $348.00 | Available | Expensive | Median price: 248.0 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Here are the current cars in the company: +1) Audi A4 | SZX789C | $248.00 | Available | Affordable | Median price: 248.0 +2) Ford F150 | STV968Z | $348.00 | Available | Expensive | Median price: 248.0 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Here are all the customers: +1) Obama | 42 | 80009000 +2) Trump | 78 | 89965443 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Available Commands: +help - Provides a list of all commands and their descriptions. +add-user /u [CUSTOMER_NAME] /a [AGE] /c [CONTACT_NUMBER] - Adds a new customer to the system. +remove-user /u [CUSTOMER_NAME] - Removes a customer from the system. +remove-all-users - Remove all customers. +list-users - Lists all customers. +add-car /n [CAR_MODEL] /c [LICENSE_PLATE_NUMBER] /p [PRICE] - Adds a new car to the fleet. +remove-car /i [LICENSE_PLATE_NUMBER] - Removes a car from the fleet. +list-cars - Lists all cars. +list-rented - Lists all rented-out cars. +list-available - Lists all available cars. +remove-all-cars - Remove all existing cars in the cars list. +add-tx /c [LICENSE_PLATE_NUMBER] /u [CUSTOMER_NAME] /d [DURATION] /s [START_DATE: ] - Adds a new rental transaction. +mark-tx /t [TRANSACTION_ID] - Marks a rental transaction completed. +unmark-tx /t [TRANSACTION_ID] - Unmark a rental transaction. +remove-tx /t [TRANSACTION_ID] - Removes an existing rental transaction. +remove-all-txs - Removes transactions history +list-txs - Lists all transactions. +list-txs-completed - Lists all completed transactions. +list-txs-uncompleted - Lists all uncompleted transactions. +find-txs-by-customer /u [CUSTOMER_NAME] - Finds transactions by a customer's name. +exit - Exits the program. +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Transaction added: +[ ] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +This car has been rented! +What would you like to do? +____________________________________________________________ +Transaction added: +[ ] TX2 | STV968Z | Obama | 20 days +Start Date: 13-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Here are all the transactions: +1) [ ] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +2) [ ] TX2 | STV968Z | Obama | 20 days +Start Date: 13-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Transaction completed: [X] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Here are all the transactions: +1) [X] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +2) [ ] TX2 | STV968Z | Obama | 20 days +Start Date: 13-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Transaction set uncompleted: [ ] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Here are all the transactions: +1) [ ] TX1 | SZX789C | Trump | 21 days +Start Date: 12-11-2024 | End Date: 03-12-2024 +2) [ ] TX2 | STV968Z | Obama | 20 days +Start Date: 13-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +Transaction(s) by Obama found: +[ ] TX2 | STV968Z | Obama | 20 days +Start Date: 13-11-2024 | End Date: 03-12-2024 +____________________________________________________________ +What would you like to do? +____________________________________________________________ +____________________________________________________________ +Goodbye! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..8ae7d97d0e 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,17 @@ -James Gosling \ No newline at end of file +add-user /u Obama /a 42 /c 80009000 +add-user /u Trump /a 78 /c 89965443 +add-car /n Audi A4 /c SZX789C /p 248 +add-car /n Ford F150 /c STV968Z /p 348 +list-cars +list-users +help +add-tx /c SZX789C /u trump /d 21 /s 12-11-2024 +add-tx /c SZX789C /u obama /d 20 /s 13-11-2024 +add-tx /c STV968Z /u obama /d 20 /s 13-11-2024 +list-txs +mark-tx /t tx1 +list-txs +unmark-tx /t tx1 +list-txs +find-txs-by-customer /u obama +exit \ No newline at end of file diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 25ac7a2989..51c610ef79 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -1,19 +1,50 @@ @echo off -setlocal enableextensions -pushd %~dp0 + +REM Ensure we're in the script's directory +cd /d "%~dp0" + +REM Check if data directory and required files exist, create if necessary +if not exist "data" ( + echo data does not exist. Creating it now....... + mkdir data + echo. > data\carData.txt + echo. > data\customerData.txt + echo. > data\transactionData.txt + echo data directory and files created successfully. +) else ( + echo. > data\carData.txt + echo. > data\customerData.txt + echo. > data\transactionData.txt +) cd .. call gradlew clean shadowJar -cd build\libs -for /f "tokens=*" %%a in ( - 'dir /b *.jar' -) do ( - set jarloc=%%a +cd text-ui-test + +REM Find the JAR file in the build/libs directory +for %%f in ("..\build\libs\*.jar") do set JAR_FILE=%%f + +REM Check if the JAR file was found +if not defined JAR_FILE ( + echo Error: No jar file found in ..\build\libs + echo Test failed! + exit /b 1 ) -java -jar %jarloc% < ..\..\text-ui-test\input.txt > ..\..\text-ui-test\ACTUAL.TXT +REM Run the Java application and redirect input/output +java -jar "%JAR_FILE%" < input.txt > ACTUAL.TXT -cd ..\..\text-ui-test +REM Use PowerShell to convert line endings +powershell -Command "(Get-Content -Path 'EXPECTED.TXT') | Set-Content -NoNewline -Path 'EXPECTED-UNIX.TXT'" +powershell -Command "(Get-Content -Path 'ACTUAL.TXT') | Set-Content -NoNewline -Path 'ACTUAL.TXT'" -FC ACTUAL.TXT EXPECTED.TXT >NUL && ECHO Test passed! || Echo Test failed! +REM Compare the output files +fc /w EXPECTED-UNIX.TXT ACTUAL.TXT >nul +if %errorlevel% equ 0 ( + echo Test passed! + exit /b 0 +) else ( + echo Test failed! + exit /b 1 +) \ No newline at end of file diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index 1dcbd12021..ae465f714e 100755 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -1,23 +1,33 @@ -#!/usr/bin/env bash - -# change to script directory +# Ensure we're in the script's directory cd "${0%/*}" +# Check if data directory and required files exist, create if necessary +if [ ! -d "data" ]; then + echo "data does not exist. Creating it now......." + mkdir -p data + touch data/carData.txt + touch data/customerData.txt + touch data/transactionData.txt + echo "data directory and files created successfully." +fi +> data/carData.txt +> data/customerData.txt +> data/transactionData.txt + cd .. ./gradlew clean shadowJar cd text-ui-test -java -jar $(find ../build/libs/ -mindepth 1 -print -quit) < input.txt > ACTUAL.TXT +java -jar $(find ../build/libs/ -name '*.jar' -print -quit) < input.txt > ACTUAL.TXT cp EXPECTED.TXT EXPECTED-UNIX.TXT dos2unix EXPECTED-UNIX.TXT ACTUAL.TXT diff EXPECTED-UNIX.TXT ACTUAL.TXT -if [ $? -eq 0 ] -then +if [ $? -eq 0 ]; then echo "Test passed!" exit 0 else echo "Test failed!" exit 1 -fi +fi \ No newline at end of file diff --git a/text-ui-test/testingCommands b/text-ui-test/testingCommands new file mode 100644 index 0000000000..de537ff800 --- /dev/null +++ b/text-ui-test/testingCommands @@ -0,0 +1,13 @@ +help +add-user /u John /a 22 /c 90907638 +list-users +remove-user /u John +add-car /n Toyota Corolla /c SGM4932K /p 120 +list-cars +remove-car /i SGM4932K +add-tx /p SBS123B /u John /d 30 /s 2024-10-17 +list-tx +remove-tx /t 1 +remove-all-cars +remove-all-txs +exit \ No newline at end of file