This project is based on the AddressBook-Level3 project created by the SE-EDU initiative.
Libraries used:
Tools used:
Documentation:
Refer to the guide Setting up and getting started.
The Architecture Diagram given above explains the high-level design of the App.
Given below is a quick overview of main components and how they interact with each other.
Main components of the architecture
Main (consisting of classes Main and MainApp) is in charge of the app launch and shut down.
The bulk of the app's work is done by the following four components:
UI: The UI of the App.Logic: The command executor.Model: Holds the data of the App in memory.Storage: Reads data from, and writes data to, the hard disk.Commons represents a collection of classes used by multiple other components.
How the architecture components interact with each other
The Sequence Diagram below shows how the components interact with each other for the scenario where the user issues the command delete 1.
Each of the four main components (also shown in the diagram above),
interface with the same name as the Component.{Component Name}Manager class (which follows the corresponding API interface mentioned in the previous point).For example, the Logic component defines its API in the Logic.java interface and implements its functionality using the LogicManager.java class which follows the Logic interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below.
The sections below give more details of each component.
The API of this component is specified in Ui.java
The UI consists of a MainWindow that is made up of parts e.g. CommandBox, ResultDisplay, PersonListPanel, StatusBarFooter etc. All these, including the MainWindow, inherit from the abstract UiPart class which captures the commonalities between classes that represent parts of the visible GUI.
The UI component uses the JavaFx UI framework. The layout of these UI parts are defined in matching .fxml files that are in the src/main/resources/view folder. For example, the layout of the MainWindow is specified in MainWindow.fxml
The UI component,
Logic component.Model data so that the UI can be updated with the modified data.Logic component, because the UI relies on the Logic to execute commands.Model component, as it displays Person object residing in the Model.API : Logic.java
Here's a (partial) class diagram of the Logic component:
The sequence diagram below illustrates the interactions within the Logic component, taking execute("delete 1") API call as an example.
Note: The lifeline for DeleteCommandParser should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
How the Logic component works:
Logic is called upon to execute a command, it is passed to an AddressBookParser object which in turn creates a parser that matches the command (e.g. DeleteCommandParser) and uses it to parse the command.Command object (more precisely, an object of one of its subclasses e.g. DeleteCommand) which is executed by the LogicManager.Model when it is executed (e.g. to delete a person).Model) to achieve.CommandResult object which is returned back from Logic.Here are the other classes in Logic (omitted from the class diagram above) that are used for parsing a user command:
How the parsing works:
AddressBookParser class creates an XYZCommandParser (XYZ is a placeholder for the specific command name e.g.AddCommandParser) which uses the other classes shown above to parse the user command and create a XYZCommand object (e.g. AddCommand) which the AddressBookParser returns back as a Command object.XYZCommandParser classes (e.g. AddCommandParser, DeleteCommandParser, ...) inherit from the Parser interface so that they can be treated similarly where possible e.g, during testing.ListCommand, StatsCommand, ClearCommand, ExitCommand, and HelpCommand do not require parsers as they take no parameters.API : Model.java
The Model component,
Person objects (which are contained in a UniquePersonList object).Person objects (e.g. results of a search query) as a separate filtered list which is exposed to outsiders as an unmodifiable ObservableList<Person> that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change.UserPref object that represents the user’s preferences. This is exposed to the outside as a ReadOnlyUserPref objects.Model represents data entities of the domain, they should make sense on their own without depending on other components)Note: An alternative (arguably, a more OOP) model is given below. It has:
AddressBook, which Person referencesAddressBook, which Person referencesAddressBook, which Tag optionally referencesThis allows AddressBook to:
Tag object per unique tag, instead of each Person needing their own Tag objectsRole object per unique role, instead of each Person needing their own Role objectsTagGroup objects for organizing tags into categories (e.g. "PropertyType", "Location")Tags can optionally belong to a TagGroup, enabling structured organization and group-based filtering of contacts.

API : Storage.java
The Storage component,
AddressBookStorage and UserPrefStorage, which means it can be treated as either one (if only the functionality of only one is needed).Model component (because the Storage component's job is to save/retrieve objects that belong to the Model)Address book data is persisted as a JSON file through JsonSerializableAddressBook, which contains:
A list of JsonAdaptedPerson objects (each containing JsonAdaptedTag and JsonAdaptedRole objects)
A list of JsonAdaptedTagGroup objects representing tag categories
This structure enables centralized storage of Tag Groups alongside person data, maintaining consistency with the Model component's Tag Group management.
Classes used by multiple components are in the seedu.address.commons package.
This section describes some noteworthy details on how certain features are implemented.
The duplicate handling mechanism ensures that no two contacts in the address book can have the same phone number or email. This is important for property agents who need to maintain unique contact information for each client, as these fields serve as critical identifiers for communication and identification.
The feature is implemented through checks in the Model component, specifically:
Model#hasSamePhoneNumber(Person person) — Checks if any existing person in the address book has the same phone number as the given person.Model#hasSameEmail(Person person) — Checks if any existing person in the address book has the same email as the given person.These operations are exposed in the Model interface and implemented in ModelManager, which delegates the checks to AddressBook and ultimately to UniquePersonList.
Given below is an example usage scenario and how the duplicate handling mechanism behaves. For illustration purposes, we will use phone number as the example, though the same logic applies to email checks.
Step 1. The user attempts to add a new contact John with phone number 12345678 by executing the command add n/John p/12345678 e/john@example.com a/123 Street r/Buyer s/Pending. The AddCommand is created and executed.
Step 2. During execution, AddCommand performs duplicate validation checks for phone number and email.
Model#hasSamePhoneNumber(toAdd) (illustrated in the sequence diagram below)Model#hasSameEmail(toAdd)Each check cascades through the components:
ModelManager calls the corresponding AddressBook method (e.g. AddressBook#hasSamePhoneNumber(person))AddressBook calls the corresponding UniquePersonList method (e.g. UniquePersonList#containsPhoneNumber(person))UniquePersonList iterates through all persons to check if any existing person has the same field valueStep 3. If any of the checks return true (e.g. the phone number 12345678 already exists belonging to another contact Alice), AddCommand throws a CommandException with an appropriate error message such as "This phone number already exists in the address book".
Step 4. If all checks pass (return false), the new contact is successfully added to the address book.
The following sequence diagrams show how the duplicate phone number check works during the execution of an add command (the same pattern applies to email checks):
Note: The lifeline for AddCommandParser should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
Note: Name duplication is not checked because property agents may legitimately have multiple contacts with the same name (e.g. common names like "John Tan"). Phone numbers and emails are more reliable unique identifiers for contact management.
Aspect: When to perform the duplicate field checks:
Alternative 1 (current choice): Check during command execution in AddCommand and EditCommand.
Alternative 2: Enforce uniqueness constraints at the UniquePersonList level.
Aspect: Scope of the uniqueness checks:
Alternative 1 (current choice): Phone numbers and emails must be globally unique across all contacts.
Alternative 2: Allow duplicate fields but warn the user.
The Tag Group Management feature allows users to organize tags into logical categories (Tag Groups). This helps property agents categorize and manage their contacts more effectively by grouping related tags together (e.g. propertyType, location, priceRange).
Tag Groups provide the following benefits:
Regex Patterns:
Standalone tags: ^[a-zA-Z0-9]+$
VIP, priorityGrouped tags: ^([a-zA-Z0-9]+)\.([a-zA-Z0-9][a-zA-Z0-9.\-_]*)$
[a-zA-Z0-9]+ (alphanumeric only).)[a-zA-Z0-9][a-zA-Z0-9.\-_]* (alphanumeric, dots, hyphens, underscores)priceRange.1.5M-2MThis relaxed validation for VALUES allows property agents to use more descriptive tag values like 500k-1M, 1.5M-2M, or Bishan-North.
The Tag Group Management feature is implemented through multiple layers of the application:
Core Classes:
TagGroup: Represents a Tag Group with an alphanumeric name. Ensures immutability and validates names using regex patterns.Tag: Modified to optionally reference a TagGroup. Tags can exist independently or be associated with a group.AddressBook: Maintains a Set<TagGroup> to store all created Tag Groups. Provides methods to add, remove, and check Tag Groups.Model interface: Exposes operations for Tag Group management (addTagGroup, deleteTagGroup, hasTagGroup, isTagGroupInUse).Key Methods:
Model#addTagGroup(TagGroup): Adds a new Tag Group to the address book if it doesn't already exist.Model#deleteTagGroup(TagGroup): Removes a Tag Group from the address book after validation.Model#hasTagGroup(TagGroup): Checks if a Tag Group exists in the address book.Model#isTagGroupInUse(TagGroup): Checks if any person's tags reference the specified Tag Group. Uses Java Streams with flatMap() and anyMatch() for efficient checking.Command Classes:
TagGroupCommand: Creates a new Tag Group or lists all existing Tag Groups. Handles both parameterized (create) and parameterless (list) executions.DeleteTagGroupCommand: Deletes a Tag Group after checking that it's not in use by any person's tags.Parser Classes:
TagGroupCommandParser: Parses user input for the tg command. Distinguishes between listing (no arguments) and creating (with GROUP_NAME argument).DeleteTagGroupCommandParser: Parses user input for the dtg command and validates the Tag Group name.Command Execution Flow:
tg propertyType)AddressBookParser routes to the appropriate parserModelStorage Classes:
JsonAdaptedTagGroup: Jackson-friendly adapter class for TagGroup serialization/deserialization.JsonSerializableAddressBook: Extended to include a List<JsonAdaptedTagGroup> field for persisting Tag Groups.Persistence Flow:
addressbook.jsonAddressBookUser Goal: Create a Tag Group called propertyType
Steps:
tg propertyTypeAddressBookParser creates a TagGroupCommandParserTagGroupCommandParser parses the input and creates a TagGroupCommand with the group nameTagGroupCommand#execute() checks if the Tag Group already exists using Model#hasTagGroup()Model#addTagGroup() is called to add the Tag GroupSequence Diagram:
The sequence diagram above illustrates the interaction between Logic and Model components when creating a Tag Group.
Note: The lifeline for TagGroupCommandParser should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
User Goal: View all created Tag Groups
Steps:
tg (without arguments)AddressBookParser creates a TagGroupCommandParserTagGroupCommandParser detects no arguments and creates a TagGroupCommand for listingTagGroupCommand#execute() retrieves all Tag Groups using Model#getTagGroupList()User Goal: Delete an unused Tag Group called propertyType
Steps:
dtg propertyTypeAddressBookParser creates a DeleteTagGroupCommandParserDeleteTagGroupCommandParser parses the input and creates a DeleteTagGroupCommandDeleteTagGroupCommand#execute() performs validation:
Model#hasTagGroup()Model#isTagGroupInUse()Model#deleteTagGroup() is calledActivity Diagram:
The activity diagram above shows the decision flow when deleting a Tag Group, including validation steps.
Aspect: Tag Group Deletion Policy
Alternative 1 (current choice): Prevent deletion if Tag Group is in use
Alternative 2: Cascade delete all associated tags
Alternative 3: Convert to untagged status
Aspect: Tag Group storage structure
Alternative 1 (current choice): Store as a Set<TagGroup> in AddressBook
Alternative 2: Store as a Map<String, TagGroup> keyed by Tag Group name
TagGroup already contains its nameAlternative 3: Store as a sorted list for ordered display
Justification: Alternative 1 was chosen for its simplicity and efficiency. The unordered nature of the Set is not a significant drawback since Tag Groups can be sorted when displayed if needed.
Aspect: Tag and TagGroup relationship design
Alternative 1 (current choice): Tags optionally reference TagGroup; TagGroups don't maintain lists of Tags
Alternative 2: Bidirectional relationship where TagGroups maintain lists of associated Tags
Justification: Alternative 1 was chosen to minimize complexity and avoid synchronization issues. The use case of "finding all tags in a group" is rare compared to checking if a tag belongs to a group.
The following validation rules are enforced for Tag Group operations:
Tag Group names:
^[a-zA-Z0-9]+$TagGroup constructorCreating Tag Groups:
Model#hasTagGroup() before addingPropertyType ≠ propertytype)Deleting Tag Groups:
Model#isTagGroupInUse() which:
flatMap()anyMatch() to check if any tag references the groupTags with groups:
t/GROUP.VALUE (e.g. t/propertyType.HDB).), hyphens (-), and underscores (_)Tag.isValidTagFormat()The feature implements comprehensive error handling for various edge cases:
| Error Scenario | Command | Error Message |
|---|---|---|
| Invalid Tag Group name (contains spaces) | tg property type | Tag Group names should be alphanumeric |
| Invalid Tag Group name (special chars) | tg property-type! | Tag Group names should be alphanumeric |
| Duplicate Tag Group | tg propertyType (when it exists) | This Tag Group already exists in the address book. |
| Delete non-existent Tag Group | dtg location (doesn't exist) | The Tag Group location does not exist. |
| Delete Tag Group in use | dtg propertyType (when in use) | This Tag Group is currently in use and cannot be deleted. Please remove all tags associated with this group first. |
The proposed undo/redo mechanism is facilitated by VersionedAddressBook. It extends AddressBook with an undo/redo history, stored internally as an addressBookStateList and currentStatePointer. Additionally, it implements the following operations:
VersionedAddressBook#commit() — Saves the current address book state in its history.VersionedAddressBook#undo() — Restores the previous address book state from its history.VersionedAddressBook#redo() — Restores a previously undone address book state from its history.These operations are exposed in the Model interface as Model#commitAddressBook(), Model#undoAddressBook() and Model#redoAddressBook() respectively.
Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.
Step 1. The user launches the application for the first time. The VersionedAddressBook will be initialized with the initial address book state, and the currentStatePointer pointing to that single address book state.
Step 2. The user executes delete 5 command to delete the 5th person in the address book. The delete command calls Model#commitAddressBook(), causing the modified state of the address book after the delete 5 command executes to be saved in the addressBookStateList, and the currentStatePointer is shifted to the newly inserted address book state.
Step 3. The user executes add n/David … to add a new person. The add command also calls Model#commitAddressBook(), causing another modified address book state to be saved into the addressBookStateList.
Note: If a command fails its execution, it will not call Model#commitAddressBook(), so the address book state will not be saved into the addressBookStateList.
Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the undo command. The undo command will call Model#undoAddressBook(), which will shift the currentStatePointer once to the left, pointing it to the previous address book state, and restores the address book to that state.
Note: If the currentStatePointer is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The undo command uses Model#canUndoAddressBook() to check if this is the case. If so, it will return an error to the user rather
than attempting to perform the undo.
The following sequence diagram shows how an undo operation goes through the Logic component:
Note: The lifeline for UndoCommand should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
Similarly, how an undo operation goes through the Model component is shown below:
The redo command does the opposite — it calls Model#redoAddressBook(), which shifts the currentStatePointer once to the right, pointing to the previously undone state, and restores the address book to that state.
Note: If the currentStatePointer is at index addressBookStateList.size() - 1, pointing to the latest address book state, then there are no undone AddressBook states to restore. The redo command uses Model#canRedoAddressBook() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
Step 5. The user then decides to execute the command list. Commands that do not modify the address book, such as list, will usually not call Model#commitAddressBook(), Model#undoAddressBook() or Model#redoAddressBook(). Thus, the addressBookStateList remains unchanged.
Step 6. The user executes clear, which calls Model#commitAddressBook(). Since the currentStatePointer is not pointing at the end of the addressBookStateList, all address book states after the currentStatePointer will be purged. Reason: It no longer makes sense to redo the add n/David … command. This is the behavior that most modern desktop applications follow.
The following activity diagram summarizes what happens when a user executes a new command:
Aspect: How undo & redo executes:
Alternative 1 (current choice): Saves the entire address book.
Alternative 2: Individual command knows how to undo/redo by itself.
delete, just save the person being deleted).{more aspects and alternatives to be added}
Target user profile:
Value proposition: manage and organise different kinds of contacts and their information faster than a typical mouse/GUI driven app
Priorities: High (must have) - * * *, Medium (nice to have) - * *, Low (unlikely to have) - *
| Priority | As a … | I can … | So that … |
|---|---|---|---|
* * | potential user | see sample contacts | I can expect how the contacts will look like before using the App |
* * * | new user | access help instructions easily | I can refer to instructions when I forget how to use the App |
* * | new user | delete all sample contacts at once | I can start using the app proper quickly |
* * * | new user | view all existing contacts in a list | I can see all my current contacts easily |
* * * | new user | add a new contact with contact information | I can keep track of my growing contacts |
* * * | new user | delete a contact and all its contact information | I can remove contacts I no longer need |
* * * | new user | view a contact's full contact information | I can easily reference all information about a contact |
* * | new user | edit a contact's generic contact information | I can make amends to changes easily |
* * * | new user | assign one or more roles to a contact during creation | I can easily recall my professional relationship with each contact |
* * | new user | delete a role from an existing contact | I can remove roles that no longer apply to the contact |
* * | new user | add a role to an existing contact | I can easily add roles to contacts without having to recreate the whole contact |
* * * | new user | assign a status to a contact during creation | I can track each client's transaction status |
* * | new user | delete a status from an existing contact | I can remove its status if it no longer applies to the contact |
* * | new user | add a status to an existing contact | I can freely decide to track status of contacts at a later time |
* * * | new user | create Tag Groups such as property price or location | I can easily group contacts under business-related categories |
* * * | new user | delete an existing Tag Group | I can clean up Tag Groupings that I no longer want to track about my contacts |
* * | new user | view all existing Tag Groups in a list | I can easily view my current Tag Groups so I do not create duplicates |
* * * | new user | create a contact with grouped tags | I can easily assign a specific tag values within a Tag Group when creating a contact |
* * | new user | create a contact with standalone tags unrelated to any Tag Group | I can still track unique metrics about a contact that do not belong to any Tag Group yet |
* * | expert user | list all client roles | I can understand what types of roles I have already created so far |
* * | expert user | be warned when creating a role that already exists | I can reduce the number of duplicated roles created |
* * | expert user | filter contacts quickly across some roles, status or Tag Groups | I can easily look for specific contacts that match some unique contact information |
* * | expert user | easily record the date and history of each status change | I can easily track when each deal is closed or monitor past transactions |
* | expert user | undo commands quickly | I can quickly rectify mistakes |
* | expert user | redo commands quickly | I can quickly make similar transactions |
* | expert user | apply advanced filters on contacts using boolean-style filtering | I can have finer control over filtering |
* | expert user | filter contacts with substring matching | I can filter more generically if I am unsure of the exact details |
* | expert user | export my contact book | I can migrate my data to other devices |
* | expert user | import existing client data from a csv file | I can start from an existing database |
* * | user with many contacts | find a contact by exact name | I can find if a contact exist without having to go through the entire list |
* * | user with many contacts | find a contact with a substring match of the name | I can find a contact if I forgot how to spell his exact name |
* * | user with many contacts | view statistics of clients grouped by their transaction status | I can quickly see how many clients are pending, completed, or have no status |
(For all use cases below, the System is the 'TrackerGuru' and the Actor is the 'Property Agent', unless specified otherwise. The term 'User' will be synonymous with 'Property Agent')
Use case: UC1 - Add a contact
Guarantees
MSS
User requests to add a contact.
TrackerGuru saves the contact and its details.
TrackerGuru displays a success message to the user.
Use case ends.
Extensions
Use case: UC2 - Delete a contact
MSS
User requests to delete a contact.
TrackerGuru deletes the contact.
TrackerGuru displays a success message to the user.
Use case ends.
Extensions
Use case: UC3 - Filter contacts by roles, statuses, and Tag Groups
Guarantees
MSS
User requests to filter contacts by specifying one or more criteria such as role, status, and/or Tag Group.
TrackerGuru processes the filter criteria and retrieves matching contacts.
TrackerGuru displays the filtered list of contacts to the user.
Use case ends.
Extensions
Use case: UC4 - Search contact by name
Guarantees
MSS
User requests for contacts whose name matches the given keywords.
TrackerGuru displays all matching contacts.
TrackerGuru displays a success message to the user.
Use case ends.
Extensions
Use case: UC5 Edit a contact
Guarantees
MSS
User requests to edit contact with updated details.
TrackerGuru edits and saves the updated contact information.
TrackerGuru displays success message to the user.
TrackerGuru's contact list reflects the updated contact information.
Use case ends.
Extensions
Use case: UC6 - Create a Tag Group
Guarantees
MSS
User requests to create a Tag Group.
TrackerGuru saves the Tag Group name.
TrackerGuru displays a success message to the user.
Use case ends.
Extensions
The following are sample instructions for manually testing TrackerGuru. They serve as a starting point; you are encouraged to perform exploratory testing beyond these examples to uncover edge cases.
Note: Each test case includes the command to execute and the expected outcome. Testers should verify that error and success messages match the described behavior.
Initial launch
Download the .jar file and place it into an empty folder.
Double-click the file to launch.
Expected: The GUI opens with sample contacts loaded. The window size may not be optimal initially.
Saving window preferences
Resize the window and move it to a different location. Close the window.
.jar file Deleting While All Persons Are Shown
Prerequisites: Use list command to show all persons. Ensure there are multiple persons listed.
Test case: delete 1
Expected: The first contact is deleted. The status message shows details of the deleted person.
Test case: delete 0
Expected: Error message shown. No person is deleted.
Other invalid commands to try: delete, delete x (where x > list size)
Expected: Error message displayed. No changes to list.
Creating a New Tag Group
Test case: tg PropertyType
Expected: Tag Group "PropertyType" created successfully. Confirmation message shown.
Test case: tg PropertyType
Expected: Error message shown indicating the Tag Group already exists.
Test case: tg Property Type (with space)
Expected: Error message shown indicating Tag Group names must be alphanumeric with no blanks.
Test case: tg
Expected: A list of all your created Tag Groups.
Other invalid commands to try: tg tag-Group, tg tagGroup 123!
Expected: Error message about invalid format.
Deleting a Tag Group
Prerequisites: Create a Tag Group Location using tg Location that is not referenced by any contact's tags.
Use tg Property Type and add a tag of this Tag Group to the first contact using edit 1 t/PropertyType.HDB
Test case: dtg Location
Expected: Tag Group "Location" is deleted. Success message shown.
Test case: dtg PropertyType
Expected: Error message indicating the Tag Group is in use and cannot be deleted.
Test case: dtg NonExistent
Expected: Error message indicating Tag Group does not exist.
Other invalid commands to try: dtg, dtg 123!
Expected: Error message about invalid format.
Using tags with Tag Groups
Prerequisites: Create a Tag Group "PropertyType" using tg PropertyType.
Test case: Add contact with grouped tag: add n/John Doe p/98765432 e/johnd@example.com a/123 Street r/Buyer t/PropertyType.HDB
Expected: Contact added with tag "PropertyType.HDB". Success message shown.
Test case: Add contact with tag referencing non-existent group: add n/Jane Doe p/98765433 e/janed@example.com a/456 Street r/Seller t/Nonexistent.Condo
Expected: Error message indicating Tag Group "Nonexistent" does not exist.
Prerequisites: List all persons using the list command. There should be multiple persons in the list.
Test case: edit 1 r/Buyer
Expected: The first contact’s role list is replaced with a single role “Buyer”. Success message shown.
Test case: edit 1 r/Buyer r/Investor
Expected: The first contact’s roles are replaced with “Buyer” and “Investor”. Success message shown.
Test case: edit 1 r/
Expected: All roles are removed from the first contact. Success message shown.
Test case: edit 1 r/_Admin or edit 1 r/-Leader
Expected: Error message indicating that roles cannot start with hyphen or underscore.
Other incorrect commands to try: edit 1 r/Inval!d
Expected: Error message about invalid role format.
Basic filtering
Prerequisites:
PropertyType and Location.t/PropertyType.HDB, t/Location.East to some contacts.Buyer, Seller, Investor to some contacts.Pending and Completed to some contacts.Test case: filter tg/PropertyType
Expected: Lists all contacts with the “PropertyType.HDB” tag.
Test case: filter r/Buyer
Expected: Lists all contacts with the “Buyer” role (case-insensitive).
Test case: filter s/Pending
Expected: Lists all contacts with status “Pending” (case-insensitive).
Multiple filter fields
Test case: filter tg/PropertyType tg/Location
Expected: Shows contacts that have tags from either Tag Groups.
Test case: filter r/Buyer r/Investor
Expected: Shows contacts that have either roles.
Test case: filter s/Pending s/Completed
Expected: Shows contacts with either status.
Test case: filter tg/PropertyType r/Buyer s/Pending
Expected: Lists only contacts that satisfy either criteria: Tag Group PropertyType, role Buyer, and status Pending.
Invalid and edge cases
Test case: filter
Expected: Error message shown.
Test case: filter tg/ or filter r/ or filter s/
Expected: Error message shown, filter values must be specified.
Test case: filter tg/UnknownGroup r/UnknownRole
Expected: No contacts shown, success message with empty list.
Dealing with missing/corrupted data files
Simulating a corrupted data file:
[JAR file location]/data/addressbook.json} or bracket ]Expected: TrackerGuru starts with an empty address book and shows a warning message about corrupted data. The corrupted file is not overwritten, unless the user executes any commands that modify the empty address book.
Simulating a missing data file:
addressbook.json file from the data folderExpected: TrackerGuru starts with sample contacts (default data).
Simulating invalid Tag Group references:
addressbook.json"PropertyType.HDB"tagGroups array, delete the {"tagGroupName": "PropertyType"} entryExpected: For data resilience, TrackerGuru commands still work normally with Tag Group PropertyType and does not affect normal operations.
Only when listing using tg, PropertyType will not appear.
Team size: 5
Allow Optional Contact Fields in Add Command: Currently, the add command requires all four fields (name, phone, email, address) to be provided: add n/John Doe p/98765432 e/johnd@example.com a/123 Street. We plan to make phone, email and address optional, allowing commands like
add n/John Doe p/98765432 where missing fields display as "N/A".
Support Incremental Editing for Roles, Statuses, and Tags: Currently, edit 1 r/Investor replaces all existing roles, causing data loss if the contact previously had roles like [Buyer] [Seller]. We plan to introduce new prefixes: r+/Investor to add roles, r-/Buyer to remove specific roles,
and keep r/Agent for replacing all roles. The same syntax will apply to tags (t+/, t-/) and status (s+/, s-/).
Improve Handling of Corrupted or Invalid Data Entries: Currently, when corrupted or invalid data is detected in the storage file, the application throws a warning and loads an empty storage file. We plan to enhance this by detecting, isolating and reporting only the corrupted entries during data loading. The application will retain and process all valid data while displaying clear warnings that specify which entries are corrupted and why. This improvement increases data resilience, usability and debugging efficiency.
Separate Command for Listing Tag Groups: Currently, the tg command has dual behavior - tg TAGGROUP_NAME creates a Tag Group while tg (without arguments) lists all existing Tag Groups. This dual behavior can be confusing as the same command does different things based on whether arguments are provided. We plan to introduce a separate ltg command specifically for listing Tag Groups.
tg PropertyType → Creates Tag Group "PropertyType"tg → Lists all Tag Groupstg PropertyType → Creates Tag Group "PropertyType" (unchanged)ltg → Lists all Tag Groupstg → Shows error message directing users to use ltg for listingTrackerGuru required approximately 50% of effort spent on creating AB3 due to:
Implementing a system supporting both grouped tags (PropertyType.HDB) and standalone tags (Urgent) was complex and represented a significant architectural departure from AB3's simple tag structure.
Key issues:
Tag and TagGroup should connect to each other while keeping data consistent across all parts of the appprice.1.5M-2MSolution: Made Tag reference TagGroup in one direction only, implemented graceful fallback when Tag Groups are missing, and created JsonAdaptedTagGroup for file storage. Updated AddressBook and ModelManager with methods to manage Tag Groups (addTagGroup(), deleteTagGroup(), hasTagGroup(), listTagGroups()) and added validation in AddCommand/EditCommand to check Tag Groups exist before saving contacts.
Implementing duplicate phone/email detection across add and edit operations required careful design.
Key issues:
Solution: Implemented hasSamePhoneNumber() and hasSameEmail() at each Model layer using Java Streams for efficiency and changed the duplicate handling logic to check for duplicate phone number and email.
The filter command needed to support OR logic across Tag Groups, roles, and statuses.
Key issues:
ObservableList reactivelySolution: Created specialised predicate classes and combined them with more lenient "or" logic
Preventing deletion of in-use Tag Groups required scanning all contacts efficiently.
Key issues:
Solution: Used Model#isTagGroupInUse() to check all contacts in one pass for better performance.
Domain-Specific Features: Transformed AB3 into a specialized property agent tool with organised Tag Groups that work like folders for categorizing tags (e.g. propertyType.HDB), roles, statuses, filtering, and statistics.
Data Integrity: Implemented duplicate detection for phone/email, comprehensive Tag Group validation that accepts flexible real-world formats, and graceful handling of corrupted data including handling missing Tag Group references.
Code Quality: Maintained clean architecture with proper separation between different parts of the app, comprehensive test coverage, and extensive documentation.
Model (Tag Groups, duplicate detection), Logic (new parsers/commands), UniquePersonList (duplicate checking)