From 0041d99120374b71ac7aeb47bb2bd258e9f35640 Mon Sep 17 00:00:00 2001 From: nicolengk Date: Wed, 18 Oct 2023 23:03:58 +0800 Subject: [PATCH 01/34] Add delprop feature --- .../java/seedu/address/logic/Messages.java | 2 + .../logic/commands/DeletePropertyCommand.java | 70 ++++++++++ .../logic/parser/AddressBookParser.java | 15 +-- .../parser/DeletePropertyCommandParser.java | 30 +++++ .../commands/DeletePropertyCommandTest.java | 120 ++++++++++++++++++ .../address/testutil/TypicalProperties.java | 2 + 6 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java create mode 100644 src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java create mode 100644 src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java create mode 100644 src/test/java/seedu/address/testutil/TypicalProperties.java diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index a3f8eb496b2..2ef635b1dfb 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -17,6 +17,8 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX = "The customer index provided is invalid"; + + public static final String MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX = "The property index provided is invalid"; public static final String MESSAGE_CUSTOMERS_LISTED_OVERVIEW = "%1$d customers listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; diff --git a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java new file mode 100644 index 00000000000..5ffee08faa7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.customer.Customer; +import seedu.address.model.property.Property; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Deletes a property identified using it's displayed index from the address book. + */ +public class DeletePropertyCommand extends Command { + + public static final String COMMAND_WORD = "delprop"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the property identified by the index number used in the displayed property list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_PROPERTY_SUCCESS = "Deleted Property: %1$s"; + + private final Index targetIndex; + + public DeletePropertyCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPropertyList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); + } + + Property propertyToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteProperty(propertyToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_PROPERTY_SUCCESS, Messages.format(propertyToDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeletePropertyCommand)) { + return false; + } + + DeletePropertyCommand otherDeleteCommand = (DeletePropertyCommand) other; + return targetIndex.equals(otherDeleteCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 049b6c3526f..929b41234a9 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -8,16 +8,7 @@ import java.util.regex.Pattern; import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.AddCustomerCommand; -import seedu.address.logic.commands.AddPropertyCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.*; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -63,8 +54,8 @@ public Command parseCommand(String userInput) throws ParseException { case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case DeletePropertyCommand.COMMAND_WORD: + return new DeletePropertyCommandParser().parse(arguments); case ClearCommand.COMMAND_WORD: return new ClearCommand(); diff --git a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java new file mode 100644 index 00000000000..020d8102367 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeletePropertyCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeletePropertyCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeletePropertyCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeletePropertyCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeletePropertyCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java new file mode 100644 index 00000000000..f54d402ee46 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -0,0 +1,120 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.customer.Customer; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code DeleteCommand}. + */ +public class DeleteCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Customer customerToDelete = model.getFilteredCustomerList().get(INDEX_FIRST_CUSTOMER.getZeroBased()); + DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); + + String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, + Messages.format(customerToDelete)); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteCustomer(customerToDelete); + + assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredCustomerList().size() + 1); + DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); + + assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + + Customer customerToDelete = model.getFilteredCustomerList().get(INDEX_FIRST_CUSTOMER.getZeroBased()); + DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); + + String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, + Messages.format(customerToDelete)); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteCustomer(customerToDelete); + showNoCustomer(expectedModel); + + assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + + Index outOfBoundIndex = INDEX_SECOND_CUSTOMER; + // ensures that outOfBoundIndex is still in bounds of budget book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getCustomerList().size()); + + DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); + + assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX); + } + + @Test + public void equals() { + DeleteCommand deleteFirstCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); + DeleteCommand deleteSecondCommand = new DeleteCommand(INDEX_SECOND_CUSTOMER); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + DeleteCommand deleteFirstCommandCopy = new DeleteCommand(INDEX_FIRST_CUSTOMER); + assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different customer -> returns false + assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); + } + + @Test + public void toStringMethod() { + Index targetIndex = Index.fromOneBased(1); + DeleteCommand deleteCommand = new DeleteCommand(targetIndex); + String expected = DeleteCommand.class.getCanonicalName() + "{targetIndex=" + targetIndex + "}"; + assertEquals(expected, deleteCommand.toString()); + } + + /** + * Updates {@code model}'s filtered list to show no one. + */ + private void showNoCustomer(Model model) { + model.updateFilteredCustomerList(p -> false); + + assertTrue(model.getFilteredCustomerList().isEmpty()); + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalProperties.java b/src/test/java/seedu/address/testutil/TypicalProperties.java new file mode 100644 index 00000000000..40061fa3ca8 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalProperties.java @@ -0,0 +1,2 @@ +package seedu.address.testutil;public class TypicalProperties { +} From 998f49f41b14d961a759d1eff6bc7b20328ce6a1 Mon Sep 17 00:00:00 2001 From: Nicole Ng Date: Thu, 19 Oct 2023 13:20:54 +0800 Subject: [PATCH 02/34] fix imports and headers --- .../address/logic/commands/DeletePropertyCommand.java | 1 - .../seedu/address/logic/parser/AddressBookParser.java | 11 ++++++++++- .../logic/parser/DeletePropertyCommandParser.java | 5 ++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java index 5ffee08faa7..9b43347aea0 100644 --- a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java @@ -5,7 +5,6 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.customer.Customer; import seedu.address.model.property.Property; import java.util.List; diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 929b41234a9..4c27b154c75 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -8,7 +8,16 @@ import java.util.regex.Pattern; import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.*; +import seedu.address.logic.commands.AddCustomerCommand; +import seedu.address.logic.commands.AddPropertyCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeletePropertyCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ListCommand; import seedu.address.logic.parser.exceptions.ParseException; /** diff --git a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java index 020d8102367..8177049d0a1 100644 --- a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java @@ -1,20 +1,19 @@ package seedu.address.logic.parser; import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.parser.exceptions.ParseException; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; /** - * Parses input arguments and creates a new DeleteCommand object + * Parses input arguments and creates a new DeletePropertyCommand object */ public class DeletePropertyCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * and returns a DeletePropertyCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public DeletePropertyCommand parse(String args) throws ParseException { From 77edde895436944ec67d22624934f16bba5528d5 Mon Sep 17 00:00:00 2001 From: Nicole Ng Date: Thu, 19 Oct 2023 13:38:59 +0800 Subject: [PATCH 03/34] Add listcust feature --- .../commands/{ListCommand.java => ListCustomerCommand.java} | 4 ++-- .../java/seedu/address/logic/parser/AddressBookParser.java | 6 +++--- src/test/java/seedu/address/logic/LogicManagerTest.java | 6 +++--- .../java/seedu/address/logic/commands/ListCommandTest.java | 4 ++-- .../seedu/address/logic/parser/AddressBookParserTest.java | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/main/java/seedu/address/logic/commands/{ListCommand.java => ListCustomerCommand.java} (83%) diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCustomerCommand.java similarity index 83% rename from src/main/java/seedu/address/logic/commands/ListCommand.java rename to src/main/java/seedu/address/logic/commands/ListCustomerCommand.java index d3a5dc97804..121cd2911ed 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCustomerCommand.java @@ -8,9 +8,9 @@ /** * Lists all customers in the address book to the user. */ -public class ListCommand extends Command { +public class ListCustomerCommand extends Command { - public static final String COMMAND_WORD = "list"; + public static final String COMMAND_WORD = "listcust"; public static final String MESSAGE_SUCCESS = "Listed all customers"; diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index fcd2a8f482f..3f198744916 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -17,7 +17,7 @@ import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListCustomerCommand; import seedu.address.logic.commands.ListPropertyCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -73,8 +73,8 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: - return new ListCommand(); + case ListCustomerCommand.COMMAND_WORD: + return new ListCustomerCommand(); case ListPropertyCommand.COMMAND_WORD: return new ListPropertyCommand(); diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index 4950e564d63..d507d71a422 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -20,7 +20,7 @@ import seedu.address.logic.commands.AddCustomerCommand; import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListCustomerCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; @@ -70,8 +70,8 @@ public void execute_commandExecutionError_throwsCommandException() { @Test public void execute_validCommand_success() throws Exception { - String listCommand = ListCommand.COMMAND_WORD; - assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model); + String listCommand = ListCustomerCommand.COMMAND_WORD; + assertCommandSuccess(listCommand, ListCustomerCommand.MESSAGE_SUCCESS, model); } @Test diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index 46d620abb97..51a0d0be400 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -29,12 +29,12 @@ public void setUp() { @Test public void execute_listIsNotFiltered_showsSameList() { - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); + assertCommandSuccess(new ListCustomerCommand(), model, ListCustomerCommand.MESSAGE_SUCCESS, expectedModel); } @Test public void execute_listIsFiltered_showsEverything() { showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); + assertCommandSuccess(new ListCustomerCommand(), model, ListCustomerCommand.MESSAGE_SUCCESS, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index e8a2f4dcc4a..8751e6fbf5b 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -20,7 +20,7 @@ import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListCustomerCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.customer.Customer; import seedu.address.model.customer.NameContainsKeywordsPredicate; @@ -83,8 +83,8 @@ public void parseCommand_help() throws Exception { @Test public void parseCommand_list() throws Exception { - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); + assertTrue(parser.parseCommand(ListCustomerCommand.COMMAND_WORD) instanceof ListCustomerCommand); + assertTrue(parser.parseCommand(ListCustomerCommand.COMMAND_WORD + " 3") instanceof ListCustomerCommand); } @Test From 76122b4c136f490e02c0977269eb2cd6fa38d961 Mon Sep 17 00:00:00 2001 From: saraozn Date: Thu, 19 Oct 2023 15:02:11 +0800 Subject: [PATCH 04/34] fix comments --- .../address/logic/parser/DeleteCustomerCommandParser.java | 6 +++--- .../address/logic/commands/DeleteCustomerCommandTest.java | 2 +- .../logic/parser/DeleteCustomerCommandParserTest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java index 93a6792f4cc..d6fd0c8538b 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java @@ -9,13 +9,13 @@ /** - * Parses input arguments and creates a new DeleteCommand object + * Parses input arguments and creates a new DeleteCustomerCommand object */ public class DeleteCustomerCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the DeleteCustomerCommand + * and returns a DeleteCustomerCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public DeleteCustomerCommand parse(String args) throws ParseException { diff --git a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java index 08a63d2aacd..167ebdc76ee 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java @@ -17,7 +17,7 @@ /** * Contains integration tests (interaction with the Model) and unit tests for - * {@code DeleteCommand}. + * {@code DeleteCustomerCommand}. */ public class DeleteCustomerCommandTest { diff --git a/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java index 4455e79c22f..ec545901692 100644 --- a/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java @@ -10,8 +10,8 @@ /** * As we are only doing white-box testing, our test cases do not cover path variations - * outside of the DeleteCommand code. For example, inputs "1" and "1 abc" take the - * same path through the DeleteCommand, and therefore we test only one of them. + * outside of the DeleteCustomerCommand code. For example, inputs "1" and "1 abc" take the + * same path through the DeleteCustomerCommand, and therefore we test only one of them. * The path variation for those two cases occur inside the ParserUtil, and * therefore should be covered by the ParserUtilTest. */ From 9ec8a0d9a21ff23034a99b32bea10bc18a36aa47 Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Fri, 20 Oct 2023 09:46:31 +0800 Subject: [PATCH 05/34] remove debug statements --- src/main/java/seedu/address/model/ModelManager.java | 1 - src/main/java/seedu/address/ui/CustomerCard.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index ccac1ab619f..585b4a41c84 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -191,7 +191,6 @@ public void updateFilteredCustomerList(Predicate predicate) { */ @Override public ObservableList getFilteredPropertyList() { - System.out.println(filteredProperties); return filteredProperties; } diff --git a/src/main/java/seedu/address/ui/CustomerCard.java b/src/main/java/seedu/address/ui/CustomerCard.java index 8ef5529cd87..cbdc7ed97fb 100644 --- a/src/main/java/seedu/address/ui/CustomerCard.java +++ b/src/main/java/seedu/address/ui/CustomerCard.java @@ -48,7 +48,6 @@ public CustomerCard(Customer customer, int displayedIndex) { super(FXML); this.customer = customer; id.setText(displayedIndex + ". "); - System.out.println(customer.getName().fullName); name.setText(customer.getName().fullName); phone.setText(customer.getPhone().value); budget.setText(customer.getBudget().toString()); From 5209a54c7eec6add75c0d65377a2c2274c7e601a Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Tue, 24 Oct 2023 20:33:14 +0800 Subject: [PATCH 06/34] Add tests for listprop feature --- .../commands/ListPropertyCommandTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java diff --git a/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java new file mode 100644 index 00000000000..02c5b41de04 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java @@ -0,0 +1,40 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.PropertyBook; +import seedu.address.model.UserPrefs; + +/** + * Contains integration tests (interaction with the Model) and unit tests for ListPropertyCommand. + */ +public class ListPropertyCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + assertCommandSuccess(new ListPropertyCommand(), model, ListPropertyCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsFiltered_showsEverything() { + showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + assertCommandSuccess(new ListPropertyCommand(), model, ListPropertyCommand.MESSAGE_SUCCESS, expectedModel); + } +} From a22c16d8a707eddacb80c7d0d51e097511b410a7 Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Tue, 24 Oct 2023 20:53:50 +0800 Subject: [PATCH 07/34] Fix checkstyle errors --- .../address/logic/commands/DeletePropertyCommand.java | 8 ++++---- .../seedu/address/logic/parser/AddressBookParser.java | 2 +- .../address/logic/parser/DeletePropertyCommandParser.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java index 9b43347aea0..852e03475ff 100644 --- a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java @@ -1,5 +1,9 @@ package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; + +import java.util.List; + import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -7,10 +11,6 @@ import seedu.address.model.Model; import seedu.address.model.property.Property; -import java.util.List; - -import static java.util.Objects.requireNonNull; - /** * Deletes a property identified using it's displayed index from the address book. */ diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index d7ea7e9162c..768306ab7fd 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -12,8 +12,8 @@ import seedu.address.logic.commands.AddPropertyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.DeleteCustomerCommand; +import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; diff --git a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java index 8177049d0a1..5418af72274 100644 --- a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java @@ -1,11 +1,11 @@ package seedu.address.logic.parser; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.parser.exceptions.ParseException; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - /** * Parses input arguments and creates a new DeletePropertyCommand object */ From c91e6cce21e59ec15d52e82c65ba62b061456605 Mon Sep 17 00:00:00 2001 From: nicolengk Date: Tue, 24 Oct 2023 21:21:15 +0800 Subject: [PATCH 08/34] Add tests for listcust feature --- .../{ListCommandTest.java => ListCustomerCommandTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/java/seedu/address/logic/commands/{ListCommandTest.java => ListCustomerCommandTest.java} (97%) diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java similarity index 97% rename from src/test/java/seedu/address/logic/commands/ListCommandTest.java rename to src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java index 51a0d0be400..775bef2fd97 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java @@ -16,7 +16,7 @@ /** * Contains integration tests (interaction with the Model) and unit tests for ListCommand. */ -public class ListCommandTest { +public class ListCustomerCommandTest { private Model model; private Model expectedModel; From 051cac5c79cad9c36963776dd1238b1f5521a9db Mon Sep 17 00:00:00 2001 From: saraozn Date: Tue, 24 Oct 2023 23:19:32 +0800 Subject: [PATCH 09/34] add test case for property --- .../commands/AddCustomerCommandTest.java | 1 - .../AddPropertyCommandIntegrationTest.java | 49 ++++ .../commands/AddPropertyCommandTest.java | 256 ++++++++++++++++++ .../commands/CommandPropertyTestUtil.java | 128 +++++++++ .../parser/AddPropertyCommandParserTest.java | 196 ++++++++++++++ .../logic/parser/AddressBookParserTest.java | 16 +- .../address/model/property/PriceTest.java | 69 +++++ .../model/property/PropAddressTest.java | 56 ++++ ...PropNameContainsKeywordsPredicateTest.java | 85 ++++++ .../address/model/property/PropNameTest.java | 60 ++++ .../address/model/property/PropPhoneTest.java | 60 ++++ .../address/model/property/PropertyTest.java | 100 +++++++ .../property/UniquePropertyListTest.java | 176 ++++++++++++ .../address/testutil/PropertyBuilder.java | 96 +++++++ .../seedu/address/testutil/PropertyUtil.java | 63 +++++ .../address/testutil/TypicalProperties.java | 76 +++++- 16 files changed, 1483 insertions(+), 4 deletions(-) create mode 100644 src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java create mode 100644 src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java create mode 100644 src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java create mode 100644 src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java create mode 100644 src/test/java/seedu/address/model/property/PriceTest.java create mode 100644 src/test/java/seedu/address/model/property/PropAddressTest.java create mode 100644 src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java create mode 100644 src/test/java/seedu/address/model/property/PropNameTest.java create mode 100644 src/test/java/seedu/address/model/property/PropPhoneTest.java create mode 100644 src/test/java/seedu/address/model/property/PropertyTest.java create mode 100644 src/test/java/seedu/address/model/property/UniquePropertyListTest.java create mode 100644 src/test/java/seedu/address/testutil/PropertyBuilder.java create mode 100644 src/test/java/seedu/address/testutil/PropertyUtil.java diff --git a/src/test/java/seedu/address/logic/commands/AddCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCustomerCommandTest.java index b6eb17fe526..90798fcfcae 100644 --- a/src/test/java/seedu/address/logic/commands/AddCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCustomerCommandTest.java @@ -189,7 +189,6 @@ public void setProperty(Property target, Property editedProperty) { throw new AssertionError("This method should not be called."); } - @Override public ObservableList getFilteredCustomerList() { throw new AssertionError("This method should not be called."); diff --git a/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java new file mode 100644 index 00000000000..aeca9057e8d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.PropertyBook; +import seedu.address.model.UserPrefs; +import seedu.address.model.property.Property; +import seedu.address.testutil.PropertyBuilder; + +/** + * Contains integration tests (interaction with the Model) for {@code AddPropertyCommand}. + */ +public class AddPropertyCommandIntegrationTest { + + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + } + + @Test + public void execute_newProperty_success() { + Property validProperty = new PropertyBuilder().build(); + + Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + expectedModel.addProperty(validProperty); + + assertCommandSuccess(new AddPropertyCommand(validProperty), model, + String.format(AddPropertyCommand.MESSAGE_SUCCESS, Messages.format(validProperty)), + expectedModel); + } + + @Test + public void execute_duplicateProperty_throwsCommandException() { + Property propertyInList = model.getPropertyBook().getPropertyList().get(0); + assertCommandFailure(new AddPropertyCommand(propertyInList), model, + AddPropertyCommand.MESSAGE_DUPLICATE_PROPERTY); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java new file mode 100644 index 00000000000..533f61d826f --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java @@ -0,0 +1,256 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalProperties.AQUAVISTA; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.PropertyBook; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyPropertyBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.customer.Customer; +import seedu.address.model.property.Property; +import seedu.address.testutil.PropertyBuilder; + +public class AddPropertyCommandTest { + + @Test + public void constructor_nullProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddPropertyCommand(null)); + } + + @Test + public void execute_propertyAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingPropertyAdded modelStub = new ModelStubAcceptingPropertyAdded(); + Property validProperty = new PropertyBuilder().build(); + + CommandResult commandResult = new AddPropertyCommand(validProperty).execute(modelStub); + + assertEquals(String.format(AddPropertyCommand.MESSAGE_SUCCESS, Messages.format(validProperty)), + commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validProperty), modelStub.propertiesAdded); + } + + @Test + public void execute_duplicateProperty_throwsCommandException() { + Property validProperty = new PropertyBuilder().build(); + AddPropertyCommand addPropertyCommand = new AddPropertyCommand(validProperty); + ModelStub modelStub = new ModelStubWithProperty(validProperty); + + assertThrows(CommandException.class, AddPropertyCommand.MESSAGE_DUPLICATE_PROPERTY, () + -> addPropertyCommand.execute(modelStub)); + } + + @Test + public void equals() { + Property aquavista= new PropertyBuilder().withName("Aquavista").build(); + Property skyvista = new PropertyBuilder().withName("Skyvista").build(); + AddPropertyCommand addAquavistaCommand = new AddPropertyCommand(aquavista); + AddPropertyCommand addSkyvistaCommand = new AddPropertyCommand(skyvista); + + // same object -> returns true + assertTrue(addAquavistaCommand.equals(addAquavistaCommand)); + + // same values -> returns true + AddPropertyCommand addAquavistaCommandCopy = new AddPropertyCommand(aquavista); + assertTrue(addAquavistaCommand.equals(addAquavistaCommandCopy)); + + // different types -> returns false + assertFalse(addAquavistaCommand.equals(1)); + + // null -> returns false + assertFalse(addAquavistaCommand.equals(null)); + + // different property -> returns false + assertFalse(addAquavistaCommand.equals(addSkyvistaCommand)); + } + + @Test + public void toStringMethod() { + AddPropertyCommand addPropertyCommand = new AddPropertyCommand(AQUAVISTA); + String expected = AddPropertyCommand.class.getCanonicalName() + "{toAdd=" + AQUAVISTA+ "}"; + assertEquals(expected, addPropertyCommand.toString()); + } + + /** + * A default model stub that have all of the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + public Path getPropertyBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + public void setPropertyBookFilePath(Path propertyBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addCustomer(Customer customer) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addProperty(Property property) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPropertyBook(ReadOnlyPropertyBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyPropertyBook getPropertyBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasCustomer(Customer customer) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasProperty(Property property) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteCustomer(Customer target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteProperty(Property target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCustomer(Customer target, Customer editedCustomer) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setProperty(Property target, Property editedProperty) { + throw new AssertionError("This method should not be called."); + } + + + @Override + public ObservableList getFilteredCustomerList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPropertyList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredCustomerList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPropertyList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single Property. + */ + private class ModelStubWithProperty extends ModelStub { + private final Property property; + + ModelStubWithProperty(Property property) { + requireNonNull(property); + this.property = property; + } + + @Override + public boolean hasProperty(Property Property) { + requireNonNull(property); + return this.property.isSameProperty(property); + } + } + + /** + * A Model stub that always accept the Property being added. + */ + private class ModelStubAcceptingPropertyAdded extends ModelStub { + final ArrayList propertiesAdded = new ArrayList<>(); + + @Override + public boolean hasProperty(Property Property) { + requireNonNull(Property); + return propertiesAdded.stream().anyMatch(Property::isSameProperty); + } + + @Override + public void addProperty(Property Property) { + requireNonNull(Property); + propertiesAdded.add(Property); + } + + @Override + public ReadOnlyPropertyBook getPropertyBook() { + return new PropertyBook(); + } + } + +} diff --git a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java new file mode 100644 index 00000000000..2de50e5f61b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java @@ -0,0 +1,128 @@ +package seedu.address.logic.commands; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.PropertyBook; +import seedu.address.model.customer.Customer; +import seedu.address.model.customer.NameContainsKeywordsPredicate; +import seedu.address.model.property.PropNameContainsKeywordsPredicate; +import seedu.address.model.property.Property; +import seedu.address.testutil.EditCustomerDescriptorBuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.*; +import static seedu.address.testutil.Assert.assertThrows; + +/** + * Contains helper methods for testing commands. + */ +public class CommandPropertyTestUtil { + + public static final String VALID_NAME_AQUAVIEW = "AquaVIEW"; + public static final String VALID_NAME_SKYVIEW = "Skyview"; + public static final String VALID_PHONE_AQUAVIEW = "11111111"; + public static final String VALID_PHONE_SKYVIEW = "22222222"; + public static final String VALID_ADDRESS_AQUAVIEW = "42 Merlion Avenue, Garden City, Singapore 54321"; + public static final String VALID_ADDRESS_SKYVIEW = "17 Rainbow Crescent, Serene Heights, Singapore 67890"; + public static final String VALID_PRICE_AQUAVIEW = "1000000"; + public static final String VALID_PRICE_SKYVIEW = "5000000"; + public static final String VALID_TAG_BIG = "big"; + public static final String VALID_TAG_SQUARE = "square"; + + public static final String NAME_DESC_AQUAVIEW = " " + PREFIX_NAME + VALID_NAME_AQUAVIEW; + public static final String NAME_DESC_SKYVIEW = " " + PREFIX_NAME + VALID_NAME_SKYVIEW; + public static final String PHONE_DESC_AQUAVIEW = " " + PREFIX_PHONE + VALID_PHONE_AQUAVIEW; + public static final String PHONE_DESC_SKYVIEW = " " + PREFIX_PHONE + VALID_PHONE_SKYVIEW; + public static final String ADDRESS_DESC_AQUAVIEW = " " + PREFIX_ADDRESS + VALID_ADDRESS_AQUAVIEW; + public static final String ADDRESS_DESC_SKYVIEW = " " + PREFIX_ADDRESS + VALID_ADDRESS_SKYVIEW; + public static final String PRICE_DESC_AQUAVIEW = " " + PREFIX_PRICE + VALID_PRICE_AQUAVIEW; + public static final String PRICE_DESC_SKYVIEW = " " + PREFIX_PRICE + VALID_PRICE_SKYVIEW; + public static final String TAG_DESC_SQUARE = " " + PREFIX_TAG + VALID_TAG_SQUARE; + public static final String TAG_DESC_BIG = " " + PREFIX_TAG + VALID_TAG_BIG; + + public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names + public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones + public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + "SKYVIEW!yahoo"; // missing '@' symbol + public static final String INVALID_PRICE_DESC = " " + PREFIX_PRICE; // empty string not allowed for PRICE + public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags + + public static final String PREAMBLE_WHITESPACE = "\t \r \n"; + public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; + +//uncomment after edit command is done +// public static final EditCommand.EditPropertyDescriptor DESC_AQUAVIEW; +// public static final EditCommand.EditPropertyDescriptor DESC_SKYVIEW; +// +// static { +// DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) +// .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) +// .withTags(VALID_TAG_SQUARE).build(); +// DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) +// .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) +// .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); +// } + + /** + * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel} + */ + public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, + Model expectedModel) { + try { + CommandResult result = command.execute(actualModel); + assertEquals(expectedCommandResult, result); + assertEquals(expectedModel, actualModel); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandResult, Model)} + * that takes a string {@code expectedMessage}. + */ + public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, + Model expectedModel) { + CommandResult expectedCommandResult = new CommandResult(expectedMessage); + assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); + } + + /** + * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the price book, filtered customer list and selected customer in {@code actualModel} remain unchanged + */ + public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { + // we are unable to defensively copy the model for comparison later, so we can + // only do so by copying its components. + PropertyBook expectedPropertyBook = new PropertyBook(actualModel.getPropertyBook()); + List expectedFilteredList = new ArrayList<>(actualModel.getFilteredPropertyList()); + + assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); + assertEquals(expectedPropertyBook, actualModel.getPropertyBook()); + assertEquals(expectedFilteredList, actualModel.getFilteredPropertyList()); + } + /** + * Updates {@code model}'s filtered list to show only the customer at the given {@code targetIndex} in the + * {@code model}'s price book. + */ + public static void showPropertyAtIndex(Model model, Index targetIndex) { + assertTrue(targetIndex.getZeroBased() < model.getFilteredPropertyList().size()); + + Property property = model.getFilteredPropertyList().get(targetIndex.getZeroBased()); + final String[] splitName = property.getName().fullName.split("\\s+"); + model.updateFilteredPropertyList(new PropNameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); + + assertEquals(1, model.getFilteredPropertyList().size()); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java new file mode 100644 index 00000000000..195b7df46f0 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java @@ -0,0 +1,196 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_NON_EMPTY; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_SQUARE; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalProperties.AQUAVISTA; +import static seedu.address.testutil.TypicalProperties.SKYVIEW; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddPropertyCommand; +import seedu.address.model.property.Price; +import seedu.address.model.property.Property; +import seedu.address.model.property.PropAddress; +import seedu.address.model.property.PropName; +import seedu.address.model.property.PropPhone; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.PropertyBuilder; + +public class AddPropertyCommandParserTest { + private AddPropertyCommandParser parser = new AddPropertyCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Property expectedPROPERTY = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE).build(); + + // whitespace only preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE, new AddPropertyCommand(expectedPROPERTY)); + + + // multiple tags - all accepted + Property expectedPROPERTYMultipleTags = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE, VALID_TAG_BIG) + .build(); + assertParseSuccess(parser, + NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + TAG_DESC_BIG + TAG_DESC_SQUARE, + new AddPropertyCommand(expectedPROPERTYMultipleTags)); + } + + @Test + public void parse_repeatedNonTagValue_failure() { + String validExpectedPROPERTYString = NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE; + + // multiple names + assertParseFailure(parser, NAME_DESC_AQUAVISTA + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + + // multiple phones + assertParseFailure(parser, PHONE_DESC_AQUAVISTA + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + + // multiple ADDRESSs + assertParseFailure(parser, ADDRESS_DESC_AQUAVISTA + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + + // multiple PRICEs + assertParseFailure(parser, PRICE_DESC_AQUAVISTA + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); + + // multiple fields repeated + assertParseFailure(parser, + validExpectedPROPERTYString + PHONE_DESC_AQUAVISTA + ADDRESS_DESC_AQUAVISTA + NAME_DESC_AQUAVISTA + PRICE_DESC_AQUAVISTA + + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_PRICE, PREFIX_ADDRESS, PREFIX_PHONE)); + + // invalid value followed by valid value + + // invalid name + assertParseFailure(parser, INVALID_NAME_DESC + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + + // invalid ADDRESS + assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + + // invalid phone + assertParseFailure(parser, INVALID_PHONE_DESC + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + + // invalid PRICE + assertParseFailure(parser, INVALID_PRICE_DESC + validExpectedPROPERTYString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); + + // valid value followed by invalid value + + // invalid name + assertParseFailure(parser, validExpectedPROPERTYString + INVALID_NAME_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + + // invalid ADDRESS + assertParseFailure(parser, validExpectedPROPERTYString + INVALID_ADDRESS_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + + // invalid phone + assertParseFailure(parser, validExpectedPROPERTYString + INVALID_PHONE_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + + // invalid PRICE + assertParseFailure(parser, validExpectedPROPERTYString + INVALID_PRICE_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); + } + + @Test + public void parse_optionalFieldsMissing_success() { + // zero tags + Property expectedPROPERTY = new PropertyBuilder(AQUAVISTA).withTags().build(); + assertParseSuccess(parser, NAME_DESC_AQUAVISTA + PHONE_DESC_AQUAVISTA + ADDRESS_DESC_AQUAVISTA + PRICE_DESC_AQUAVISTA, + new AddPropertyCommand(expectedPROPERTY)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPropertyCommand.MESSAGE_USAGE); + + // missing name prefix + assertParseFailure(parser, VALID_NAME_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW, + expectedMessage); + + // missing phone prefix + assertParseFailure(parser, NAME_DESC_SKYVIEW + VALID_PHONE_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW, + expectedMessage); + + // missing ADDRESS prefix + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + VALID_ADDRESS_SKYVIEW + PRICE_DESC_SKYVIEW, + expectedMessage); + + // missing PRICE prefix + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + VALID_PRICE_SKYVIEW, + expectedMessage); + + // all prefixes missing + assertParseFailure(parser, VALID_NAME_SKYVIEW + VALID_PHONE_SKYVIEW + VALID_ADDRESS_SKYVIEW + VALID_PRICE_SKYVIEW, + expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + // invalid name + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + + TAG_DESC_BIG + TAG_DESC_SQUARE, PropName.MESSAGE_CONSTRAINTS); + + // invalid phone + assertParseFailure(parser, NAME_DESC_SKYVIEW + INVALID_PHONE_DESC + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + + TAG_DESC_BIG + TAG_DESC_SQUARE, PropPhone.MESSAGE_CONSTRAINTS); + + // invalid ADDRESS + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + INVALID_ADDRESS_DESC + PRICE_DESC_SKYVIEW + + TAG_DESC_BIG + TAG_DESC_SQUARE, PropAddress.MESSAGE_CONSTRAINTS); + + // invalid PRICE + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + INVALID_PRICE_DESC + + TAG_DESC_BIG + TAG_DESC_SQUARE, Price.MESSAGE_CONSTRAINTS); + + // invalid tag + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + + INVALID_TAG_DESC + VALID_TAG_BIG, Tag.MESSAGE_CONSTRAINTS); + + // two invalid values, only first invalid value reported + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + INVALID_PRICE_DESC, + PropName.MESSAGE_CONSTRAINTS); + + // non-empty preamble + assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + + PRICE_DESC_SKYVIEW + TAG_DESC_BIG + TAG_DESC_SQUARE, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPropertyCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 25e77ac9044..59699038857 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -14,8 +14,9 @@ import org.junit.jupiter.api.Test; import seedu.address.logic.commands.AddCustomerCommand; +import seedu.address.logic.commands.AddPropertyCommand; import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteCustomerCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; @@ -24,21 +25,32 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.customer.Customer; import seedu.address.model.customer.NameContainsKeywordsPredicate; +import seedu.address.model.property.Property; import seedu.address.testutil.CustomerBuilder; import seedu.address.testutil.CustomerUtil; import seedu.address.testutil.EditCustomerDescriptorBuilder; +import seedu.address.testutil.PropertyBuilder; +import seedu.address.testutil.PropertyUtil; public class AddressBookParserTest { private final AddressBookParser parser = new AddressBookParser(); @Test - public void parseCommand_add() throws Exception { + public void parseCommand_addcust() throws Exception { Customer customer = new CustomerBuilder().build(); AddCustomerCommand command = (AddCustomerCommand) parser.parseCommand(CustomerUtil.getAddCommand(customer)); assertEquals(new AddCustomerCommand(customer), command); } + @Test + public void parseCommand_addprop() throws Exception { + Property property = new PropertyBuilder().build(); + AddPropertyCommand command = (AddPropertyCommand) parser.parseCommand(PropertyUtil.getAddCommand(property)); + assertEquals(new AddPropertyCommand(property), command); + } + + @Test public void parseCommand_clear() throws Exception { assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); diff --git a/src/test/java/seedu/address/model/property/PriceTest.java b/src/test/java/seedu/address/model/property/PriceTest.java new file mode 100644 index 00000000000..f864ed210e0 --- /dev/null +++ b/src/test/java/seedu/address/model/property/PriceTest.java @@ -0,0 +1,69 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + + +public class PriceTest { + private static final float OTHER_TYPE_INVALID_PRICE = 5.0f; + private static final String NOT_INTEGER_INVALID_PRICE = "one million"; + private static final String ONE_DIGIT_INVALID_PRICE = "1"; + private static final String TWO_DIGIT_INVALID_PRICE = "11"; + private static final String THREE_DIGIT_INVALID_PRICE = "111"; + private static final String FOUR_DIGIT_INVALID_PRICE = "1111"; + private static final String VALID_PRICE = "100000"; + private static final String LONG_VALID_PRICE = Integer.toString(Integer.MAX_VALUE); + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Price(null)); + } + + @Test + public void constructor_invalidPrice_throwsIllegalArgumentException() { + String invalidPrice = ""; + assertThrows(IllegalArgumentException.class, () -> new Price(invalidPrice)); + } + + @Test + public void isValidPrice() { + // null price + assertThrows(NullPointerException.class, () -> Price.isValidPrice(null)); + + // invalid prices + assertFalse(Price.isValidPrice("")); // empty string + assertFalse(Price.isValidPrice(" ")); // spaces only + assertFalse(Price.isValidPrice(NOT_INTEGER_INVALID_PRICE)); // not an integer + assertFalse(Price.isValidPrice(ONE_DIGIT_INVALID_PRICE)); // one digit only + assertFalse(Price.isValidPrice(TWO_DIGIT_INVALID_PRICE)); // two digit only + assertFalse(Price.isValidPrice(THREE_DIGIT_INVALID_PRICE)); // three digit only + assertFalse(Price.isValidPrice(FOUR_DIGIT_INVALID_PRICE)); // four digit only + + + // valid prices + assertTrue(Price.isValidPrice(VALID_PRICE)); + assertTrue(Price.isValidPrice(LONG_VALID_PRICE)); // long budget + } + + @Test + public void equals() { + Price price = new Price(VALID_PRICE); + + // same values -> returns true + assertTrue(price.equals(new Price(VALID_PRICE))); + + // same object -> returns true + assertTrue(price.equals(price)); + + // null -> returns false + assertFalse(price.equals(null)); + + // different types -> returns false + assertFalse(price.equals(OTHER_TYPE_INVALID_PRICE)); + + // different values -> returns false + assertFalse(price.equals(new Price(LONG_VALID_PRICE))); + } +} diff --git a/src/test/java/seedu/address/model/property/PropAddressTest.java b/src/test/java/seedu/address/model/property/PropAddressTest.java new file mode 100644 index 00000000000..87491cf0b96 --- /dev/null +++ b/src/test/java/seedu/address/model/property/PropAddressTest.java @@ -0,0 +1,56 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PropAddressTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new PropAddress(null)); + } + + @Test + public void constructor_invalidPropAddress_throwsIllegalArgumentException() { + String invalidAddress = ""; + assertThrows(IllegalArgumentException.class, () -> new PropAddress(invalidAddress)); + } + + @Test + public void isValidPropAddress() { + // null budget + assertThrows(NullPointerException.class, () -> PropAddress.isValidAddress(null)); + + // invalid property addresses + assertFalse(PropAddress.isValidAddress("")); // empty string + assertFalse(PropAddress.isValidAddress(" ")); // spaces only + + // valid property addresses + assertTrue(PropAddress.isValidAddress("Blk 456, Den Road, #01-355")); + assertTrue(PropAddress.isValidAddress("-")); // one character + assertTrue(PropAddress.isValidAddress("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA")); // long budget + } + + @Test + public void equals() { + PropAddress propAddress = new PropAddress("Valid Address"); + + // same values -> returns true + assertTrue(propAddress.equals(new PropAddress("Valid Address"))); + + // same object -> returns true + assertTrue(propAddress.equals(propAddress)); + + // null -> returns false + assertFalse(propAddress.equals(null)); + + // different types -> returns false + assertFalse(propAddress.equals(5.0f)); + + // different values -> returns false + assertFalse(propAddress.equals(new PropAddress("Other Valid Address"))); + } +} diff --git a/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..b3bdd8b35f2 --- /dev/null +++ b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java @@ -0,0 +1,85 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PropertyBuilder; + +public class PropNameContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + PropNameContainsKeywordsPredicate firstPredicate = new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); + PropNameContainsKeywordsPredicate secondPredicate = new PropNameContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + PropNameContainsKeywordsPredicate firstPredicateCopy = new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different property -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_nameContainsKeywords_returnsTrue() { + // One keyword + PropNameContainsKeywordsPredicate predicate = new PropNameContainsKeywordsPredicate(Collections.singletonList("Aquavista")); + assertTrue(predicate.test(new PropertyBuilder().withName(" Skyview").build())); + + // Multiple keywords + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("Aquavista", "Skyview")); + assertTrue(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); + + // Only one matching keyword + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("Skyview", "Horizonview")); + assertTrue(predicate.test(new PropertyBuilder().withName("Aquavista Horizonview").build())); + + // Mixed-case keywords + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("AqUaVisTa", "SkyViEw")); + assertTrue(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + // Zero keywords + PropNameContainsKeywordsPredicate predicate = new PropNameContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista").build())); + + // Non-matching keyword + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("Horizonview")); + assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); + + // Keywords match phone, address and price, but does not match name + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("12345", "123 Orchid Lane, Singapore 456789", "Main", "Street")); + assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista").withPhone("12345") + .withAddress("123 Orchid Lane, Singapore 456789").withPrice("123456").build())); + } + + @Test + public void toStringMethod() { + List keywords = List.of("keyword1", "keyword2"); + PropNameContainsKeywordsPredicate predicate = new PropNameContainsKeywordsPredicate(keywords); + + String expected = PropNameContainsKeywordsPredicate.class.getCanonicalName() + "{keywords=" + keywords + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/property/PropNameTest.java b/src/test/java/seedu/address/model/property/PropNameTest.java new file mode 100644 index 00000000000..f32ff61f936 --- /dev/null +++ b/src/test/java/seedu/address/model/property/PropNameTest.java @@ -0,0 +1,60 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PropNameTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new PropName(null)); + } + + @Test + public void constructor_invalidPropName_throwsIllegalArgumentException() { + String invalidName = ""; + assertThrows(IllegalArgumentException.class, () -> new PropName(invalidName)); + } + + @Test + public void isValidPropName() { + // null name + assertThrows(NullPointerException.class, () -> PropName.isValidName(null)); + + // invalid name + assertFalse(PropName.isValidName("")); // empty string + assertFalse(PropName.isValidName(" ")); // spaces only + assertFalse(PropName.isValidName("^")); // only non-alphanumeric characters + assertFalse(PropName.isValidName("skyview*")); // contains non-alphanumeric characters + + // valid name + assertTrue(PropName.isValidName("skyview")); // alphabets only + assertTrue(PropName.isValidName("12345")); // numbers only + assertTrue(PropName.isValidName("skyview 2")); // alphanumeric characters + assertTrue(PropName.isValidName("Skyview")); // with capital letters + assertTrue(PropName.isValidName("Skyview Horizonview")); // long names + } + + @Test + public void equals() { + PropName name = new PropName("Valid Name"); + + // same values -> returns true + assertTrue(name.equals(new PropName("Valid Name"))); + + // same object -> returns true + assertTrue(name.equals(name)); + + // null -> returns false + assertFalse(name.equals(null)); + + // different types -> returns false + assertFalse(name.equals(5.0f)); + + // different values -> returns false + assertFalse(name.equals(new PropName("Other Valid Name"))); + } +} diff --git a/src/test/java/seedu/address/model/property/PropPhoneTest.java b/src/test/java/seedu/address/model/property/PropPhoneTest.java new file mode 100644 index 00000000000..7eafbbc8ce0 --- /dev/null +++ b/src/test/java/seedu/address/model/property/PropPhoneTest.java @@ -0,0 +1,60 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PropPhoneTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new PropPhone(null)); + } + + @Test + public void constructor_invalidPropPhone_throwsIllegalArgumentException() { + String invalidPhone = ""; + assertThrows(IllegalArgumentException.class, () -> new PropPhone(invalidPhone)); + } + + @Test + public void isValidPropPhone() { + // null phone number + assertThrows(NullPointerException.class, () -> PropPhone.isValidPhone(null)); + + // invalid phone numbers + assertFalse(PropPhone.isValidPhone("")); // empty string + assertFalse(PropPhone.isValidPhone(" ")); // spaces only + assertFalse(PropPhone.isValidPhone("91")); // less than 3 numbers + assertFalse(PropPhone.isValidPhone("phone")); // non-numeric + assertFalse(PropPhone.isValidPhone("9011p041")); // alphabets within digits + assertFalse(PropPhone.isValidPhone("9312 1534")); // spaces within digits + + // valid phone numbers + assertTrue(PropPhone.isValidPhone("911")); // exactly 3 numbers + assertTrue(PropPhone.isValidPhone("93121534")); + assertTrue(PropPhone.isValidPhone("124293842033123")); // long phone numbers + } + + @Test + public void equals() { + PropPhone phone = new PropPhone("999"); + + // same values -> returns true + assertTrue(phone.equals(new PropPhone("999"))); + + // same object -> returns true + assertTrue(phone.equals(phone)); + + // null -> returns false + assertFalse(phone.equals(null)); + + // different types -> returns false + assertFalse(phone.equals(5.0f)); + + // different values -> returns false + assertFalse(phone.equals(new PropPhone("995"))); + } +} diff --git a/src/test/java/seedu/address/model/property/PropertyTest.java b/src/test/java/seedu/address/model/property/PropertyTest.java new file mode 100644 index 00000000000..866e8fd640a --- /dev/null +++ b/src/test/java/seedu/address/model/property/PropertyTest.java @@ -0,0 +1,100 @@ +package seedu.address.model.property; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalProperties.AQUAVISTA; +import static seedu.address.testutil.TypicalProperties.SKYVIEW; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PropertyBuilder; + +public class PropertyTest { + + @Test + public void asObservableList_modifyList_throwsUnsupportedOperationException() { + Property property = new PropertyBuilder().build(); + assertThrows(UnsupportedOperationException.class, () -> property.getTags().remove(0)); + } + + @Test + public void isSameProperty() { + // same object -> returns true + assertTrue(AQUAVISTA.isSameProperty(AQUAVISTA)); + + // null -> returns false + assertFalse(AQUAVISTA.isSameProperty(null)); + + // same name, all other attributes different -> returns true + Property editedAlice = new PropertyBuilder(AQUAVISTA).withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW) + .withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG).build(); + assertTrue(AQUAVISTA.isSameProperty(editedAlice)); + + // different name, all other attributes same -> returns false + editedAlice = new PropertyBuilder(AQUAVISTA).withName(VALID_NAME_SKYVIEW).build(); + assertFalse(AQUAVISTA.isSameProperty(editedAlice)); + + // name differs in case, all other attributes same -> returns false + Property editedSkyview = new PropertyBuilder(SKYVIEW).withName(VALID_NAME_SKYVIEW.toLowerCase()).build(); + assertFalse(SKYVIEW.isSameProperty(editedSkyview)); + + // name has trailing spaces, all other attributes same -> returns false + String nameWithTrailingSpaces = VALID_NAME_SKYVIEW + " "; + editedSkyview = new PropertyBuilder(SKYVIEW).withName(nameWithTrailingSpaces).build(); + assertFalse(SKYVIEW.isSameProperty(editedSkyview)); + } + + @Test + public void equals() { + // same values -> returns true + Property aquavistaCopy = new PropertyBuilder(AQUAVISTA).build(); + assertTrue(AQUAVISTA.equals(aquavistaCopy)); + + // same object -> returns true + assertTrue(AQUAVISTA.equals(AQUAVISTA)); + + // null -> returns false + assertFalse(AQUAVISTA.equals(null)); + + // different type -> returns false + assertFalse(AQUAVISTA.equals(5)); + + // different property -> returns false + assertFalse(AQUAVISTA.equals(SKYVIEW)); + + // different name -> returns false + Property editedAquavista = new PropertyBuilder(AQUAVISTA).withName(VALID_NAME_SKYVIEW).build(); + assertFalse(AQUAVISTA.equals(editedAquavista)); + + // different phone -> returns false + editedAquavista = new PropertyBuilder(AQUAVISTA).withPhone(VALID_PHONE_SKYVIEW).build(); + assertFalse(AQUAVISTA.equals(editedAquavista)); + + // different email -> returns false + editedAquavista = new PropertyBuilder(AQUAVISTA).withAddress(VALID_ADDRESS_SKYVIEW).build(); + assertFalse(AQUAVISTA.equals(editedAquavista)); + + // different budget -> returns false + editedAquavista = new PropertyBuilder(AQUAVISTA).withPrice(VALID_PRICE_SKYVIEW).build(); + assertFalse(AQUAVISTA.equals(editedAquavista)); + + // different tags -> returns false + editedAquavista = new PropertyBuilder(AQUAVISTA).withTags(VALID_TAG_BIG).build(); + assertFalse(AQUAVISTA.equals(editedAquavista)); + } + + @Test + public void toStringMethod() { + String expected = Property.class.getCanonicalName() + "{name=" + AQUAVISTA.getName() + ", phone=" + AQUAVISTA.getPhone() + + ", address=" + AQUAVISTA.getAddress() + ", price=" + AQUAVISTA.getPrice() + ", tags=" + AQUAVISTA.getTags() + "}"; + assertEquals(expected, AQUAVISTA.toString()); + } +} diff --git a/src/test/java/seedu/address/model/property/UniquePropertyListTest.java b/src/test/java/seedu/address/model/property/UniquePropertyListTest.java new file mode 100644 index 00000000000..b1c25b83336 --- /dev/null +++ b/src/test/java/seedu/address/model/property/UniquePropertyListTest.java @@ -0,0 +1,176 @@ +package seedu.address.model.property; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalProperties.AQUAVISTA; +import static seedu.address.testutil.TypicalProperties.SKYVIEW; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.property.exceptions.PropertyNotFoundException; +import seedu.address.model.property.exceptions.DuplicatePropertyException; +import seedu.address.testutil.PropertyBuilder; + +public class UniquePropertyListTest { + + private final UniquePropertyList uniquePropertyList = new UniquePropertyList(); + + @Test + public void contains_nullProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.contains(null)); + } + + @Test + public void contains_propertyNotInList_returnsFalse() { + assertFalse(uniquePropertyList.contains(AQUAVISTA)); + } + + @Test + public void contains_propertyInList_returnsTrue() { + uniquePropertyList.add(AQUAVISTA); + assertTrue(uniquePropertyList.contains(AQUAVISTA)); + } + + @Test + public void contains_propertyWithSameIdentityFieldsInList_returnsTrue() { + uniquePropertyList.add(AQUAVISTA); + Property editedAlice = new PropertyBuilder(AQUAVISTA).withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG) + .build(); + assertTrue(uniquePropertyList.contains(editedAlice)); + } + + @Test + public void add_nullProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.add(null)); + } + + @Test + public void add_duplicateProperty_throwsDuplicatePropertyException() { + uniquePropertyList.add(AQUAVISTA); + assertThrows(DuplicatePropertyException.class, () -> uniquePropertyList.add(AQUAVISTA)); + } + + @Test + public void setProperty_nullTargetProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.setProperty(null, AQUAVISTA)); + } + + @Test + public void setProperty_nullEditedProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.setProperty(AQUAVISTA, null)); + } + + @Test + public void setProperty_targetPropertyNotInList_throwsPropertyNotFoundException() { + assertThrows(PropertyNotFoundException.class, () -> uniquePropertyList.setProperty(AQUAVISTA, AQUAVISTA)); + } + + @Test + public void setProperty_editedPropertyIsSameProperty_success() { + uniquePropertyList.add(AQUAVISTA); + uniquePropertyList.setProperty(AQUAVISTA, AQUAVISTA); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + expectedUniquePropertyList.add(AQUAVISTA); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperty_editedPropertyHasSameIdentity_success() { + uniquePropertyList.add(AQUAVISTA); + Property editedAlice = new PropertyBuilder(AQUAVISTA).withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG) + .build(); + uniquePropertyList.setProperty(AQUAVISTA, editedAlice); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + expectedUniquePropertyList.add(editedAlice); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperty_editedPropertyHasDifferentIdentity_success() { + uniquePropertyList.add(AQUAVISTA); + uniquePropertyList.setProperty(AQUAVISTA, SKYVIEW); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + expectedUniquePropertyList.add(SKYVIEW); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperty_editedPropertyHasNonUniqueIdentity_throwsDuplicatePropertyException() { + uniquePropertyList.add(AQUAVISTA); + uniquePropertyList.add(SKYVIEW); + assertThrows(DuplicatePropertyException.class, () -> uniquePropertyList.setProperty(AQUAVISTA, SKYVIEW)); + } + + @Test + public void remove_nullProperty_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.remove(null)); + } + + @Test + public void remove_propertyDoesNotExist_throwsPropertyNotFoundException() { + assertThrows(PropertyNotFoundException.class, () -> uniquePropertyList.remove(AQUAVISTA)); + } + + @Test + public void remove_existingProperty_removesProperty() { + uniquePropertyList.add(AQUAVISTA); + uniquePropertyList.remove(AQUAVISTA); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperty_nullUniquePropertyList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.setProperties((UniquePropertyList) null)); + } + + @Test + public void setProperty_uniquePropertyList_replacesOwnListWithProvidedUniquePropertyList() { + uniquePropertyList.add(AQUAVISTA); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + expectedUniquePropertyList.add(SKYVIEW); + uniquePropertyList.setProperties(expectedUniquePropertyList); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperty_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniquePropertyList.setProperties((List) null)); + } + + @Test + public void setProperties_list_replacesOwnListWithProvidedList() { + uniquePropertyList.add(AQUAVISTA); + List propertyList = Collections.singletonList(SKYVIEW); + uniquePropertyList.setProperties(propertyList); + UniquePropertyList expectedUniquePropertyList = new UniquePropertyList(); + expectedUniquePropertyList.add(SKYVIEW); + assertEquals(expectedUniquePropertyList, uniquePropertyList); + } + + @Test + public void setProperties_listWithDuplicateProperties_throwsDuplicatePropertyException() { + List listWithDuplicateProperties = Arrays.asList(AQUAVISTA, AQUAVISTA); + assertThrows(DuplicatePropertyException.class, () + -> uniquePropertyList.setProperties(listWithDuplicateProperties)); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> uniquePropertyList.asUnmodifiableObservableList().remove(0)); + } + + @Test + public void toStringMethod() { + assertEquals(uniquePropertyList.asUnmodifiableObservableList().toString(), uniquePropertyList.toString()); + } +} diff --git a/src/test/java/seedu/address/testutil/PropertyBuilder.java b/src/test/java/seedu/address/testutil/PropertyBuilder.java new file mode 100644 index 00000000000..8f1eef53029 --- /dev/null +++ b/src/test/java/seedu/address/testutil/PropertyBuilder.java @@ -0,0 +1,96 @@ +package seedu.address.testutil; + +import java.util.HashSet; +import java.util.Set; + +import seedu.address.model.property.Price; +import seedu.address.model.property.PropAddress; +import seedu.address.model.property.Property; +import seedu.address.model.property.PropName; +import seedu.address.model.property.PropPhone; +import seedu.address.model.tag.Tag; +import seedu.address.model.util.SampleDataUtil; + +/** + * A utility class to help with building Property objects. + */ +public class PropertyBuilder { + + public static final String DEFAULT_NAME = "Light House"; + public static final String DEFAULT_ADDRESS = "25 Prince George's Park, Singapore 118424"; + public static final String DEFAULT_PHONE = "85462384"; + public static final String DEFAULT_PRICE = "1230000"; + + private PropName name; + private PropAddress address; + private PropPhone phone; + private Price price; + private Set tags; + + /** + * Creates a {@code PropertyrBuilder} with the default details. + */ + public PropertyBuilder() { + name = new PropName(DEFAULT_NAME); + address = new PropAddress(DEFAULT_ADDRESS); + phone = new PropPhone(DEFAULT_PHONE); + price = new Price(DEFAULT_PRICE); + tags = new HashSet<>(); + } + + /** + * Initializes the PropertyBuilder with the data of {@code propertyToCopy}. + */ + public PropertyBuilder(Property propertyToCopy) { + name = propertyToCopy.getName(); + address = propertyToCopy.getAddress(); + phone = propertyToCopy.getPhone(); + price = propertyToCopy.getPrice(); + tags = new HashSet<>(propertyToCopy.getTags()); + } + + /** + * Sets the {@code Name} of the {@code Property} that we are building. + */ + public PropertyBuilder withName(String name) { + this.name = new PropName(name); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code Property} that we are building. + */ + public PropertyBuilder withTags(String ... tags) { + this.tags = SampleDataUtil.getTagSet(tags); + return this; + } + + /** + * Sets the {@code Price} of the {@code Property} that we are building. + */ + public PropertyBuilder withPrice(String price) { + this.price = new Price(price.trim()); + return this; + } + + /** + * Sets the {@code Phone} of the {@code Property} that we are building. + */ + public PropertyBuilder withPhone(String phone) { + this.phone = new PropPhone(phone); + return this; + } + + /** + * Sets the {@code PropAddress} of the {@code Property} that we are building. + */ + public PropertyBuilder withAddress(String address) { + this.address = new PropAddress(address); + return this; + } + + public Property build() { + return new Property(name, address, phone, price, tags); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/address/testutil/PropertyUtil.java b/src/test/java/seedu/address/testutil/PropertyUtil.java new file mode 100644 index 00000000000..7a023b6e684 --- /dev/null +++ b/src/test/java/seedu/address/testutil/PropertyUtil.java @@ -0,0 +1,63 @@ +package seedu.address.testutil; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; + +import seedu.address.logic.commands.AddPropertyCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.model.property.Property; +import seedu.address.model.tag.Tag; + +/** + * A utility class for Property. + */ +public class PropertyUtil { + + /** + * Returns an add command string for adding the {@code Property}. + */ + public static String getAddCommand(Property property) { + return AddPropertyCommand.COMMAND_WORD + " " + getPropertyDetails(property); + } + + /** + * Returns the part of command string for the given {@code Property}'s details. + */ + public static String getPropertyDetails(Property property) { + StringBuilder sb = new StringBuilder(); + sb.append(PREFIX_NAME + property.getName().fullName + " "); + sb.append(PREFIX_PHONE + property.getPhone().value + " "); + sb.append(PREFIX_EMAIL + property.getAddress().value + " "); + sb.append(PREFIX_BUDGET + property.getPrice().value + " "); + property.getTags().stream().forEach( + s -> sb.append(PREFIX_TAG + s.tagName + " ") + ); + return sb.toString(); + } + + /** + * Returns the part of command string for the given {@code EditPropertyDescriptor}'s details. + */ +//editprop to be implemented first +// public static String getEditPropertyDescriptorDetails(EditPropertyCommand.EditPropertyDescriptor descriptor) { +// StringBuilder sb = new StringBuilder(); +// descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); +// descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); +// descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); +// descriptor.getBudget().ifPresent(budget -> sb.append(PREFIX_BUDGET).append(budget.value).append(" ")); +// if (descriptor.getTags().isPresent()) { +// Set tags = descriptor.getTags().get(); +// if (tags.isEmpty()) { +// sb.append(PREFIX_TAG); +// } else { +// tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); +// } +// } +// return sb.toString(); +// } +} diff --git a/src/test/java/seedu/address/testutil/TypicalProperties.java b/src/test/java/seedu/address/testutil/TypicalProperties.java index 40061fa3ca8..8e84b11e768 100644 --- a/src/test/java/seedu/address/testutil/TypicalProperties.java +++ b/src/test/java/seedu/address/testutil/TypicalProperties.java @@ -1,2 +1,76 @@ -package seedu.address.testutil;public class TypicalProperties { +package seedu.address.testutil; + +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.PropertyBook; +import seedu.address.model.property.Property; + +/** + * A utility class containing a list of {@code Customer} objects to be used in tests. + */ +public class TypicalProperties { + + public static final Property AQUAVISTA = new PropertyBuilder().withName("Aquavista") + .withPrice("123456").withAddress("123 Orchid Lane, Singapore 456789") + .withPhone("94351253") + .withTags("pink").build(); + public static final Property SKYVISTA = new PropertyBuilder().withName("Skyvista") + .withPrice("8880000") + .withAddress("456 Sapphire Avenue, Singapore 987654").withPhone("98765432") + .withTags("square", "garden").build(); + public static final Property HORIZONVIEW = new PropertyBuilder().withName("Horizonview").withPhone("95352563") + .withAddress("789 Palm Grove Road, Singapore 321012").withPrice("40000").build(); + public static final Property LUXELOFT = new PropertyBuilder().withName("Luxeloft").withPhone("87652533") + .withAddress("234 Amber Crescent, Singapore 567890").withPrice("10000").withTags("garage").build(); + public static final Property RIVERIA = new PropertyBuilder().withName("Riveria").withPhone("9482224") + .withAddress(" 567 Maple Lane, Singapore 109876").withPrice("4000000").build(); + public static final Property AZURE = new PropertyBuilder().withName("Azure").withPhone("9482427") + .withAddress("101 Radiant Lane, Singapore 123456").withPrice("874000").build(); + public static final Property TRANQUILIS = new PropertyBuilder().withName("Tranquilis").withPhone("9482442") + .withAddress(" 202 Shoreline Street, Singapore 654321").withPrice("321950").build(); + + // Manually added + public static final Property ASCEND = new PropertyBuilder().withName("Ascend").withPhone("8482424") + .withAddress("909 Skyline Boulevard, Singapore 789123").withPrice("4321000").build(); + public static final Property SPECTRA = new PropertyBuilder().withName("Spectra").withPhone("8482131") + .withAddress(" 808 Colorful Street, Singapore 456012").withPrice("500000").build(); + + // Manually added - Customer's details found in {@code CommandTestUtil} + public static final Property AQUAVIEW = new PropertyBuilder().withName(VALID_NAME_AQUAVIEW).withPhone(VALID_PHONE_AQUAVIEW) + .withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW).withTags(VALID_TAG_SQUARE).build(); + public static final Property SKYVIEW = new PropertyBuilder().withName(VALID_NAME_SKYVIEW).withPhone(VALID_PHONE_SKYVIEW) + .withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG, VALID_TAG_SQUARE) + .build(); + + public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER + + private void TypicalProperty() {} // prevents instantiation + + /** + * Returns an {@code AddressBook} with all the typical customers. + */ + public static PropertyBook getTypicalPropertyBook() { + PropertyBook pb = new PropertyBook(); + for (Property property : getTypicalProperties()) { + pb.addProperty(property); + } + return pb; + } + + public static List getTypicalProperties() { + return new ArrayList<>(Arrays.asList(AQUAVIEW, SKYVISTA, HORIZONVIEW, LUXELOFT, RIVERIA, AZURE, TRANQUILIS)); + } } From f4b8c078e1395dd720fb3adb18faef7a9e1bb89d Mon Sep 17 00:00:00 2001 From: saraozn Date: Tue, 24 Oct 2023 23:27:13 +0800 Subject: [PATCH 10/34] fix checks --- docs/DeveloperGuide.md | 8 ++++---- docs/team/saraozn.md | 8 ++++---- src/test/java/seedu/address/testutil/PropertyBuilder.java | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 2a5818c75c7..9b7eb29ab7e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -262,17 +262,17 @@ Customer-property management tool for property agents new to the real estate ind **Value proposition**: 2 entities: - + 1. Customer: * Create : * Can add the types of property customer is looking for * Read : * Can check the types of property customer is looking for - * Update : + * Update : * Can update the types of property customer is looking for - * Delete : + * Delete : * Delete client profile - * Find : + * Find : * Match customer to property 2. Property * Create : diff --git a/docs/team/saraozn.md b/docs/team/saraozn.md index df91ab31486..8e0b1bd043d 100644 --- a/docs/team/saraozn.md +++ b/docs/team/saraozn.md @@ -12,10 +12,10 @@ The user interacts with it using a CLI, and it has a GUI created with JavaFX. It Given below are my contributions to the project. * **New Feature**: TO BE ADDED - * What it does: - * Justification: - * Highlights: - * Credits: + * What it does: + * Justification: + * Highlights: + * Credits: * **Code contributed**: [RepoSense link]() diff --git a/src/test/java/seedu/address/testutil/PropertyBuilder.java b/src/test/java/seedu/address/testutil/PropertyBuilder.java index 8f1eef53029..8e6c8b3055f 100644 --- a/src/test/java/seedu/address/testutil/PropertyBuilder.java +++ b/src/test/java/seedu/address/testutil/PropertyBuilder.java @@ -93,4 +93,4 @@ public Property build() { return new Property(name, address, phone, price, tags); } -} \ No newline at end of file +} From 2b89c4323b234de4688ff9a3920ead1c15c58a71 Mon Sep 17 00:00:00 2001 From: saraozn Date: Tue, 24 Oct 2023 23:36:23 +0800 Subject: [PATCH 11/34] fix checkstyle --- .../address/logic/commands/DeletePropertyCommand.java | 8 ++++---- .../seedu/address/logic/parser/AddressBookParser.java | 2 +- .../address/logic/parser/DeleteCustomerCommandParser.java | 2 -- .../address/logic/parser/DeletePropertyCommandParser.java | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java index 9b43347aea0..f895561839d 100644 --- a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java @@ -1,5 +1,9 @@ package seedu.address.logic.commands; +import java.util.List; + +import static java.util.Objects.requireNonNull; + import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -7,10 +11,6 @@ import seedu.address.model.Model; import seedu.address.model.property.Property; -import java.util.List; - -import static java.util.Objects.requireNonNull; - /** * Deletes a property identified using it's displayed index from the address book. */ diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index d7ea7e9162c..768306ab7fd 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -12,8 +12,8 @@ import seedu.address.logic.commands.AddPropertyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.DeleteCustomerCommand; +import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; diff --git a/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java index d6fd0c8538b..584a3c127b0 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCustomerCommandParser.java @@ -6,8 +6,6 @@ import seedu.address.logic.commands.DeleteCustomerCommand; import seedu.address.logic.parser.exceptions.ParseException; - - /** * Parses input arguments and creates a new DeleteCustomerCommand object */ diff --git a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java index 8177049d0a1..5418af72274 100644 --- a/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeletePropertyCommandParser.java @@ -1,11 +1,11 @@ package seedu.address.logic.parser; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.parser.exceptions.ParseException; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - /** * Parses input arguments and creates a new DeletePropertyCommand object */ From bbf73fcf9da820c88ac55b88151ed0dd578e694a Mon Sep 17 00:00:00 2001 From: saraozn Date: Tue, 24 Oct 2023 23:42:38 +0800 Subject: [PATCH 12/34] fix style --- .../seedu/address/logic/commands/DeletePropertyCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java index f895561839d..852e03475ff 100644 --- a/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeletePropertyCommand.java @@ -1,9 +1,9 @@ package seedu.address.logic.commands; -import java.util.List; - import static java.util.Objects.requireNonNull; +import java.util.List; + import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; From 19b4bcaa0359ce828ee764065a17dd52d4b6c021 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 00:09:03 +0800 Subject: [PATCH 13/34] fix AddPropertyCommandParserTest bugs --- .../parser/AddPropertyCommandParserTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java index 195b7df46f0..b9a458291f8 100644 --- a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java @@ -1,18 +1,18 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; -import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PHONE_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_TAG_DESC; -import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_SKYVIEW; -import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_AQUAVISTA; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_NON_EMPTY; import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_WHITESPACE; @@ -30,7 +30,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalProperties.AQUAVISTA; +import static seedu.address.testutil.TypicalProperties.AQUAVIEW; import static seedu.address.testutil.TypicalProperties.SKYVIEW; import org.junit.jupiter.api.Test; @@ -71,24 +71,24 @@ public void parse_repeatedNonTagValue_failure() { + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE; // multiple names - assertParseFailure(parser, NAME_DESC_AQUAVISTA + validExpectedPROPERTYString, + assertParseFailure(parser, NAME_DESC_AQUAVIEW + validExpectedPROPERTYString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); // multiple phones - assertParseFailure(parser, PHONE_DESC_AQUAVISTA + validExpectedPROPERTYString, + assertParseFailure(parser, PHONE_DESC_AQUAVIEW + validExpectedPROPERTYString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); // multiple ADDRESSs - assertParseFailure(parser, ADDRESS_DESC_AQUAVISTA + validExpectedPROPERTYString, + assertParseFailure(parser, ADDRESS_DESC_AQUAVIEW + validExpectedPROPERTYString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); // multiple PRICEs - assertParseFailure(parser, PRICE_DESC_AQUAVISTA + validExpectedPROPERTYString, + assertParseFailure(parser, PRICE_DESC_AQUAVIEW + validExpectedPROPERTYString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); // multiple fields repeated assertParseFailure(parser, - validExpectedPROPERTYString + PHONE_DESC_AQUAVISTA + ADDRESS_DESC_AQUAVISTA + NAME_DESC_AQUAVISTA + PRICE_DESC_AQUAVISTA + validExpectedPROPERTYString + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + NAME_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW + validExpectedPROPERTYString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_PRICE, PREFIX_ADDRESS, PREFIX_PHONE)); @@ -132,8 +132,8 @@ public void parse_repeatedNonTagValue_failure() { @Test public void parse_optionalFieldsMissing_success() { // zero tags - Property expectedPROPERTY = new PropertyBuilder(AQUAVISTA).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AQUAVISTA + PHONE_DESC_AQUAVISTA + ADDRESS_DESC_AQUAVISTA + PRICE_DESC_AQUAVISTA, + Property expectedPROPERTY = new PropertyBuilder(AQUAVIEW).withTags().build(); + assertParseSuccess(parser, NAME_DESC_AQUAVIEW + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW, new AddPropertyCommand(expectedPROPERTY)); } From 87103c8b3c0f9162aefd28e0ebc848ae631ede42 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 00:16:27 +0800 Subject: [PATCH 14/34] fix delprop command test --- .../commands/DeletePropertyCommandTest.java | 77 ++++++++++--------- .../address/testutil/TypicalIndexes.java | 4 + 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java index f54d402ee46..02e337d2050 100644 --- a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -3,12 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandPropertyTestUtil.showPropertyAtIndex; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PROPERTY; import org.junit.jupiter.api.Test; @@ -16,8 +16,9 @@ import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; +import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; -import seedu.address.model.customer.Customer; +import seedu.address.model.property.Property; /** * Contains integration tests (interaction with the Model) and unit tests for @@ -25,70 +26,70 @@ */ public class DeleteCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { - Customer customerToDelete = model.getFilteredCustomerList().get(INDEX_FIRST_CUSTOMER.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); + Property propertyToDelete = model.getFilteredPropertyList().get(INDEX_FIRST_PROPERTY.getZeroBased()); + DeletePropertyCommand deleteCommand = new DeletePropertyCommand(INDEX_FIRST_PROPERTY); - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, - Messages.format(customerToDelete)); + String expectedMessage = String.format(DeletePropertyCommand.MESSAGE_DELETE_PROPERTY_SUCCESS, + Messages.format(propertyToDelete)); - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deleteCustomer(customerToDelete); + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + expectedModel.deleteProperty(propertyToDelete); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); } @Test public void execute_invalidIndexUnfilteredList_throwsCommandException() { - Index outOfBoundIndex = Index.fromOneBased(model.getFilteredCustomerList().size() + 1); - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPropertyList().size() + 1); + DeletePropertyCommand deleteCommand = new DeletePropertyCommand(outOfBoundIndex); - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX); + assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); } @Test public void execute_validIndexFilteredList_success() { - showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); - Customer customerToDelete = model.getFilteredCustomerList().get(INDEX_FIRST_CUSTOMER.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); + Property propertyToDelete = model.getFilteredPropertyList().get(INDEX_FIRST_PROPERTY.getZeroBased()); + DeletePropertyCommand deleteCommand = new DeletePropertyCommand(INDEX_FIRST_PROPERTY); - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, - Messages.format(customerToDelete)); + String expectedMessage = String.format(DeletePropertyCommand.MESSAGE_DELETE_PROPERTY_SUCCESS, + Messages.format(propertyToDelete)); - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deleteCustomer(customerToDelete); - showNoCustomer(expectedModel); + Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + expectedModel.deleteProperty(propertyToDelete); + showNoPROPERTY(expectedModel); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); } @Test public void execute_invalidIndexFilteredList_throwsCommandException() { - showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); - Index outOfBoundIndex = INDEX_SECOND_CUSTOMER; + Index outOfBoundIndex = INDEX_SECOND_PROPERTY; // ensures that outOfBoundIndex is still in bounds of budget book list - assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getCustomerList().size()); + assertTrue(outOfBoundIndex.getZeroBased() < model.getPropertyBook().getPropertyList().size()); - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); + DeletePropertyCommand deleteCommand = new DeletePropertyCommand(outOfBoundIndex); - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX); + assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); } @Test public void equals() { - DeleteCommand deleteFirstCommand = new DeleteCommand(INDEX_FIRST_CUSTOMER); - DeleteCommand deleteSecondCommand = new DeleteCommand(INDEX_SECOND_CUSTOMER); + DeletePropertyCommand deleteFirstCommand = new DeletePropertyCommand(INDEX_FIRST_PROPERTY); + DeletePropertyCommand deleteSecondCommand = new DeletePropertyCommand(INDEX_SECOND_PROPERTY); // same object -> returns true assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); // same values -> returns true - DeleteCommand deleteFirstCommandCopy = new DeleteCommand(INDEX_FIRST_CUSTOMER); + DeletePropertyCommand deleteFirstCommandCopy = new DeletePropertyCommand(INDEX_FIRST_PROPERTY); assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); // different types -> returns false @@ -97,24 +98,24 @@ public void equals() { // null -> returns false assertFalse(deleteFirstCommand.equals(null)); - // different customer -> returns false + // different property -> returns false assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); } @Test public void toStringMethod() { Index targetIndex = Index.fromOneBased(1); - DeleteCommand deleteCommand = new DeleteCommand(targetIndex); - String expected = DeleteCommand.class.getCanonicalName() + "{targetIndex=" + targetIndex + "}"; + DeletePropertyCommand deleteCommand = new DeletePropertyCommand(targetIndex); + String expected = DeletePropertyCommand.class.getCanonicalName() + "{targetIndex=" + targetIndex + "}"; assertEquals(expected, deleteCommand.toString()); } /** * Updates {@code model}'s filtered list to show no one. */ - private void showNoCustomer(Model model) { - model.updateFilteredCustomerList(p -> false); + private void showNoPROPERTY(Model model) { + model.updateFilteredPropertyList(p -> false); - assertTrue(model.getFilteredCustomerList().isEmpty()); + assertTrue(model.getFilteredPropertyList().isEmpty()); } } diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 9e43edccc6b..cc2f9ccd07c 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -9,4 +9,8 @@ public class TypicalIndexes { public static final Index INDEX_FIRST_CUSTOMER = Index.fromOneBased(1); public static final Index INDEX_SECOND_CUSTOMER = Index.fromOneBased(2); public static final Index INDEX_THIRD_CUSTOMER = Index.fromOneBased(3); + + public static final Index INDEX_FIRST_PROPERTY = Index.fromOneBased(1); + public static final Index INDEX_SECOND_PROPERTY = Index.fromOneBased(2); + public static final Index INDEX_THIRD_PROPERTY = Index.fromOneBased(3); } From cc6b17d51568ba71ff5cb6aadbd25c02d3d493b5 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 00:35:21 +0800 Subject: [PATCH 15/34] fix small bug for delprop test --- .../seedu/address/logic/commands/DeletePropertyCommandTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java index 02e337d2050..e2c18e52c8f 100644 --- a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -24,7 +24,7 @@ * Contains integration tests (interaction with the Model) and unit tests for * {@code DeleteCommand}. */ -public class DeleteCommandTest { +public class DeletePropertyCommandTest { private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); From 1462b02fc433c32179a5c5aae7dae9b2f27b5cf7 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 01:19:33 +0800 Subject: [PATCH 16/34] fix checkstyle --- .../commands/AddPropertyCommandTest.java | 20 ++--- .../commands/CommandPropertyTestUtil.java | 54 ++++++------- .../commands/DeleteCustomerCommandTest.java | 18 +++-- .../commands/DeletePropertyCommandTest.java | 4 +- .../parser/AddPropertyCommandParserTest.java | 76 ++++++++++--------- .../DeleteCustomerCommandParserTest.java | 9 ++- ...PropNameContainsKeywordsPredicateTest.java | 15 ++-- .../address/model/property/PropertyTest.java | 11 +-- .../property/UniquePropertyListTest.java | 2 +- .../address/testutil/PropertyBuilder.java | 2 +- .../seedu/address/testutil/PropertyUtil.java | 25 ------ .../address/testutil/TypicalProperties.java | 17 +++-- 12 files changed, 121 insertions(+), 132 deletions(-) diff --git a/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java index 533f61d826f..8ea35c47e19 100644 --- a/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddPropertyCommandTest.java @@ -18,8 +18,8 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.PropertyBook; import seedu.address.model.Model; +import seedu.address.model.PropertyBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyPropertyBook; import seedu.address.model.ReadOnlyUserPrefs; @@ -58,7 +58,7 @@ public void execute_duplicateProperty_throwsCommandException() { @Test public void equals() { - Property aquavista= new PropertyBuilder().withName("Aquavista").build(); + Property aquavista = new PropertyBuilder().withName("Aquavista").build(); Property skyvista = new PropertyBuilder().withName("Skyvista").build(); AddPropertyCommand addAquavistaCommand = new AddPropertyCommand(aquavista); AddPropertyCommand addSkyvistaCommand = new AddPropertyCommand(skyvista); @@ -83,7 +83,7 @@ public void equals() { @Test public void toStringMethod() { AddPropertyCommand addPropertyCommand = new AddPropertyCommand(AQUAVISTA); - String expected = AddPropertyCommand.class.getCanonicalName() + "{toAdd=" + AQUAVISTA+ "}"; + String expected = AddPropertyCommand.class.getCanonicalName() + "{toAdd=" + AQUAVISTA + "}"; assertEquals(expected, addPropertyCommand.toString()); } @@ -223,7 +223,7 @@ private class ModelStubWithProperty extends ModelStub { } @Override - public boolean hasProperty(Property Property) { + public boolean hasProperty(Property property) { requireNonNull(property); return this.property.isSameProperty(property); } @@ -236,15 +236,15 @@ private class ModelStubAcceptingPropertyAdded extends ModelStub { final ArrayList propertiesAdded = new ArrayList<>(); @Override - public boolean hasProperty(Property Property) { - requireNonNull(Property); - return propertiesAdded.stream().anyMatch(Property::isSameProperty); + public boolean hasProperty(Property property) { + requireNonNull(property); + return propertiesAdded.stream().anyMatch(property::isSameProperty); } @Override - public void addProperty(Property Property) { - requireNonNull(Property); - propertiesAdded.add(Property); + public void addProperty(Property property) { + requireNonNull(property); + propertiesAdded.add(property); } @Override diff --git a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java index 2de50e5f61b..c521119fd22 100644 --- a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java @@ -1,31 +1,31 @@ package seedu.address.logic.commands; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.PropertyBook; -import seedu.address.model.customer.Customer; -import seedu.address.model.customer.NameContainsKeywordsPredicate; import seedu.address.model.property.PropNameContainsKeywordsPredicate; import seedu.address.model.property.Property; -import seedu.address.testutil.EditCustomerDescriptorBuilder; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.CliSyntax.*; -import static seedu.address.testutil.Assert.assertThrows; /** * Contains helper methods for testing commands. */ public class CommandPropertyTestUtil { - public static final String VALID_NAME_AQUAVIEW = "AquaVIEW"; + public static final String VALID_NAME_AQUAVIEW = "Aquaview"; public static final String VALID_NAME_SKYVIEW = "Skyview"; public static final String VALID_PHONE_AQUAVIEW = "11111111"; public static final String VALID_PHONE_SKYVIEW = "22222222"; @@ -49,25 +49,25 @@ public class CommandPropertyTestUtil { public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + "SKYVIEW!yahoo"; // missing '@' symbol + public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + "SKYVIEW!"; // ! not allowed in address public static final String INVALID_PRICE_DESC = " " + PREFIX_PRICE; // empty string not allowed for PRICE public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; -//uncomment after edit command is done -// public static final EditCommand.EditPropertyDescriptor DESC_AQUAVIEW; -// public static final EditCommand.EditPropertyDescriptor DESC_SKYVIEW; -// -// static { -// DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) -// .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) -// .withTags(VALID_TAG_SQUARE).build(); -// DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) -// .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) -// .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); -// } + //uncomment after editprop command is done + // public static final EditCommand.EditPropertyDescriptor DESC_AQUAVIEW; + // public static final EditCommand.EditPropertyDescriptor DESC_SKYVIEW; + // + // static { + // DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) + // .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) + // .withTags(VALID_TAG_SQUARE).build(); + // DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) + // .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) + // .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); + // } /** * Executes the given {@code command}, confirms that
diff --git a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java index 167ebdc76ee..1229a867f51 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java @@ -1,6 +1,17 @@ package seedu.address.logic.commands; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; + import org.junit.jupiter.api.Test; + import seedu.address.commons.core.index.Index; import seedu.address.logic.Messages; import seedu.address.model.Model; @@ -8,13 +19,6 @@ import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.customer.Customer; - -import static org.junit.jupiter.api.Assertions.*; -import static seedu.address.logic.commands.CommandTestUtil.*; -import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; - /** * Contains integration tests (interaction with the Model) and unit tests for * {@code DeleteCustomerCommand}. diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java index e2c18e52c8f..34390d0149e 100644 --- a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -62,7 +62,7 @@ public void execute_validIndexFilteredList_success() { Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); expectedModel.deleteProperty(propertyToDelete); - showNoPROPERTY(expectedModel); + showNoProperty(expectedModel); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); } @@ -113,7 +113,7 @@ public void toStringMethod() { /** * Updates {@code model}'s filtered list to show no one. */ - private void showNoPROPERTY(Model model) { + private void showNoProperty(Model model) { model.updateFilteredPropertyList(p -> false); assertTrue(model.getFilteredPropertyList().isEmpty()); diff --git a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java index b9a458291f8..9d75cd6cc0e 100644 --- a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java @@ -1,8 +1,6 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVIEW; -import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; @@ -16,6 +14,8 @@ import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_NON_EMPTY; import static seedu.address.logic.commands.CommandPropertyTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_BIG; import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_SQUARE; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; @@ -24,10 +24,10 @@ import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import static seedu.address.testutil.TypicalProperties.AQUAVIEW; @@ -38,10 +38,10 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.AddPropertyCommand; import seedu.address.model.property.Price; -import seedu.address.model.property.Property; import seedu.address.model.property.PropAddress; import seedu.address.model.property.PropName; import seedu.address.model.property.PropPhone; +import seedu.address.model.property.Property; import seedu.address.model.tag.Tag; import seedu.address.testutil.PropertyBuilder; @@ -50,91 +50,93 @@ public class AddPropertyCommandParserTest { @Test public void parse_allFieldsPresent_success() { - Property expectedPROPERTY = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE).build(); + Property expectedProperty = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE).build(); // whitespace only preamble assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW - + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE, new AddPropertyCommand(expectedPROPERTY)); + + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE, new AddPropertyCommand(expectedProperty)); // multiple tags - all accepted - Property expectedPROPERTYMultipleTags = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE, VALID_TAG_BIG) + Property expectedPropertyMultipleTags = new PropertyBuilder(SKYVIEW).withTags(VALID_TAG_SQUARE, VALID_TAG_BIG) .build(); assertParseSuccess(parser, - NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + TAG_DESC_BIG + TAG_DESC_SQUARE, - new AddPropertyCommand(expectedPROPERTYMultipleTags)); + NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + + TAG_DESC_BIG + TAG_DESC_SQUARE, + new AddPropertyCommand(expectedPropertyMultipleTags)); } @Test public void parse_repeatedNonTagValue_failure() { - String validExpectedPROPERTYString = NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + String validExpectedPropertyString = NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + TAG_DESC_SQUARE; // multiple names - assertParseFailure(parser, NAME_DESC_AQUAVIEW + validExpectedPROPERTYString, + assertParseFailure(parser, NAME_DESC_AQUAVIEW + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); // multiple phones - assertParseFailure(parser, PHONE_DESC_AQUAVIEW + validExpectedPROPERTYString, + assertParseFailure(parser, PHONE_DESC_AQUAVIEW + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); // multiple ADDRESSs - assertParseFailure(parser, ADDRESS_DESC_AQUAVIEW + validExpectedPROPERTYString, + assertParseFailure(parser, ADDRESS_DESC_AQUAVIEW + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); // multiple PRICEs - assertParseFailure(parser, PRICE_DESC_AQUAVIEW + validExpectedPROPERTYString, + assertParseFailure(parser, PRICE_DESC_AQUAVIEW + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); // multiple fields repeated assertParseFailure(parser, - validExpectedPROPERTYString + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + NAME_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW - + validExpectedPROPERTYString, + validExpectedPropertyString + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + NAME_DESC_AQUAVIEW + + PRICE_DESC_AQUAVIEW + + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_PRICE, PREFIX_ADDRESS, PREFIX_PHONE)); // invalid value followed by valid value // invalid name - assertParseFailure(parser, INVALID_NAME_DESC + validExpectedPROPERTYString, + assertParseFailure(parser, INVALID_NAME_DESC + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); // invalid ADDRESS - assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPROPERTYString, + assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); // invalid phone - assertParseFailure(parser, INVALID_PHONE_DESC + validExpectedPROPERTYString, + assertParseFailure(parser, INVALID_PHONE_DESC + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); // invalid PRICE - assertParseFailure(parser, INVALID_PRICE_DESC + validExpectedPROPERTYString, + assertParseFailure(parser, INVALID_PRICE_DESC + validExpectedPropertyString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); // valid value followed by invalid value // invalid name - assertParseFailure(parser, validExpectedPROPERTYString + INVALID_NAME_DESC, + assertParseFailure(parser, validExpectedPropertyString + INVALID_NAME_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); // invalid ADDRESS - assertParseFailure(parser, validExpectedPROPERTYString + INVALID_ADDRESS_DESC, + assertParseFailure(parser, validExpectedPropertyString + INVALID_ADDRESS_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); // invalid phone - assertParseFailure(parser, validExpectedPROPERTYString + INVALID_PHONE_DESC, + assertParseFailure(parser, validExpectedPropertyString + INVALID_PHONE_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); // invalid PRICE - assertParseFailure(parser, validExpectedPROPERTYString + INVALID_PRICE_DESC, + assertParseFailure(parser, validExpectedPropertyString + INVALID_PRICE_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PRICE)); } @Test public void parse_optionalFieldsMissing_success() { // zero tags - Property expectedPROPERTY = new PropertyBuilder(AQUAVIEW).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AQUAVIEW + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW, - new AddPropertyCommand(expectedPROPERTY)); + Property expectedProperty = new PropertyBuilder(AQUAVIEW).withTags().build(); + assertParseSuccess(parser, NAME_DESC_AQUAVIEW + PHONE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + + PRICE_DESC_AQUAVIEW, new AddPropertyCommand(expectedProperty)); } @Test @@ -142,24 +144,24 @@ public void parse_compulsoryFieldMissing_failure() { String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPropertyCommand.MESSAGE_USAGE); // missing name prefix - assertParseFailure(parser, VALID_NAME_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW, - expectedMessage); + assertParseFailure(parser, VALID_NAME_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + + PRICE_DESC_SKYVIEW, expectedMessage); // missing phone prefix - assertParseFailure(parser, NAME_DESC_SKYVIEW + VALID_PHONE_SKYVIEW + ADDRESS_DESC_SKYVIEW + PRICE_DESC_SKYVIEW, - expectedMessage); + assertParseFailure(parser, NAME_DESC_SKYVIEW + VALID_PHONE_SKYVIEW + ADDRESS_DESC_SKYVIEW + + PRICE_DESC_SKYVIEW, expectedMessage); // missing ADDRESS prefix - assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + VALID_ADDRESS_SKYVIEW + PRICE_DESC_SKYVIEW, - expectedMessage); + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + VALID_ADDRESS_SKYVIEW + + PRICE_DESC_SKYVIEW, expectedMessage); // missing PRICE prefix - assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + VALID_PRICE_SKYVIEW, - expectedMessage); + assertParseFailure(parser, NAME_DESC_SKYVIEW + PHONE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + + VALID_PRICE_SKYVIEW, expectedMessage); // all prefixes missing - assertParseFailure(parser, VALID_NAME_SKYVIEW + VALID_PHONE_SKYVIEW + VALID_ADDRESS_SKYVIEW + VALID_PRICE_SKYVIEW, - expectedMessage); + assertParseFailure(parser, VALID_NAME_SKYVIEW + VALID_PHONE_SKYVIEW + VALID_ADDRESS_SKYVIEW + + VALID_PRICE_SKYVIEW, expectedMessage); } @Test diff --git a/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java index ec545901692..344442822be 100644 --- a/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/DeleteCustomerCommandParserTest.java @@ -1,13 +1,13 @@ package seedu.address.logic.parser; -import org.junit.jupiter.api.Test; -import seedu.address.logic.commands.DeleteCustomerCommand; - import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteCustomerCommand; /** * As we are only doing white-box testing, our test cases do not cover path variations * outside of the DeleteCustomerCommand code. For example, inputs "1" and "1 abc" take the @@ -26,6 +26,7 @@ public void parse_validArgs_returnsDeleteCustomerCommand() { @Test public void parse_invalidArgs_throwsParseException() { - assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCustomerCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteCustomerCommand.MESSAGE_USAGE)); } } diff --git a/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java index b3bdd8b35f2..9ba6a09de0b 100644 --- a/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java @@ -19,14 +19,17 @@ public void equals() { List firstPredicateKeywordList = Collections.singletonList("first"); List secondPredicateKeywordList = Arrays.asList("first", "second"); - PropNameContainsKeywordsPredicate firstPredicate = new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); - PropNameContainsKeywordsPredicate secondPredicate = new PropNameContainsKeywordsPredicate(secondPredicateKeywordList); + PropNameContainsKeywordsPredicate firstPredicate = + new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); + PropNameContainsKeywordsPredicate secondPredicate = + new PropNameContainsKeywordsPredicate(secondPredicateKeywordList); // same object -> returns true assertTrue(firstPredicate.equals(firstPredicate)); // same values -> returns true - PropNameContainsKeywordsPredicate firstPredicateCopy = new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); + PropNameContainsKeywordsPredicate firstPredicateCopy = + new PropNameContainsKeywordsPredicate(firstPredicateKeywordList); assertTrue(firstPredicate.equals(firstPredicateCopy)); // different types -> returns false @@ -42,7 +45,8 @@ public void equals() { @Test public void test_nameContainsKeywords_returnsTrue() { // One keyword - PropNameContainsKeywordsPredicate predicate = new PropNameContainsKeywordsPredicate(Collections.singletonList("Aquavista")); + PropNameContainsKeywordsPredicate predicate = + new PropNameContainsKeywordsPredicate(Collections.singletonList("Aquavista")); assertTrue(predicate.test(new PropertyBuilder().withName(" Skyview").build())); // Multiple keywords @@ -69,7 +73,8 @@ public void test_nameDoesNotContainKeywords_returnsFalse() { assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); // Keywords match phone, address and price, but does not match name - predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("12345", "123 Orchid Lane, Singapore 456789", "Main", "Street")); + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("12345", "123 Orchid Lane, Singapore 456789", + "Main", "Street")); assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista").withPhone("12345") .withAddress("123 Orchid Lane, Singapore 456789").withPrice("123456").build())); } diff --git a/src/test/java/seedu/address/model/property/PropertyTest.java b/src/test/java/seedu/address/model/property/PropertyTest.java index 866e8fd640a..6cf093bfe75 100644 --- a/src/test/java/seedu/address/model/property/PropertyTest.java +++ b/src/test/java/seedu/address/model/property/PropertyTest.java @@ -1,13 +1,12 @@ package seedu.address.model.property; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalProperties.AQUAVISTA; @@ -34,7 +33,8 @@ public void isSameProperty() { assertFalse(AQUAVISTA.isSameProperty(null)); // same name, all other attributes different -> returns true - Property editedAlice = new PropertyBuilder(AQUAVISTA).withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW) + Property editedAlice = new PropertyBuilder(AQUAVISTA).withPhone(VALID_PHONE_SKYVIEW) + .withAddress(VALID_ADDRESS_SKYVIEW) .withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG).build(); assertTrue(AQUAVISTA.isSameProperty(editedAlice)); @@ -93,8 +93,9 @@ public void equals() { @Test public void toStringMethod() { - String expected = Property.class.getCanonicalName() + "{name=" + AQUAVISTA.getName() + ", phone=" + AQUAVISTA.getPhone() - + ", address=" + AQUAVISTA.getAddress() + ", price=" + AQUAVISTA.getPrice() + ", tags=" + AQUAVISTA.getTags() + "}"; + String expected = Property.class.getCanonicalName() + "{name=" + AQUAVISTA.getName() + + ", phone=" + AQUAVISTA.getPhone() + ", address=" + AQUAVISTA.getAddress() + + ", price=" + AQUAVISTA.getPrice() + ", tags=" + AQUAVISTA.getTags() + "}"; assertEquals(expected, AQUAVISTA.toString()); } } diff --git a/src/test/java/seedu/address/model/property/UniquePropertyListTest.java b/src/test/java/seedu/address/model/property/UniquePropertyListTest.java index b1c25b83336..fa18a081988 100644 --- a/src/test/java/seedu/address/model/property/UniquePropertyListTest.java +++ b/src/test/java/seedu/address/model/property/UniquePropertyListTest.java @@ -15,8 +15,8 @@ import org.junit.jupiter.api.Test; -import seedu.address.model.property.exceptions.PropertyNotFoundException; import seedu.address.model.property.exceptions.DuplicatePropertyException; +import seedu.address.model.property.exceptions.PropertyNotFoundException; import seedu.address.testutil.PropertyBuilder; public class UniquePropertyListTest { diff --git a/src/test/java/seedu/address/testutil/PropertyBuilder.java b/src/test/java/seedu/address/testutil/PropertyBuilder.java index 8e6c8b3055f..feda97f3e03 100644 --- a/src/test/java/seedu/address/testutil/PropertyBuilder.java +++ b/src/test/java/seedu/address/testutil/PropertyBuilder.java @@ -5,9 +5,9 @@ import seedu.address.model.property.Price; import seedu.address.model.property.PropAddress; -import seedu.address.model.property.Property; import seedu.address.model.property.PropName; import seedu.address.model.property.PropPhone; +import seedu.address.model.property.Property; import seedu.address.model.tag.Tag; import seedu.address.model.util.SampleDataUtil; diff --git a/src/test/java/seedu/address/testutil/PropertyUtil.java b/src/test/java/seedu/address/testutil/PropertyUtil.java index 7a023b6e684..ab9dc08e09c 100644 --- a/src/test/java/seedu/address/testutil/PropertyUtil.java +++ b/src/test/java/seedu/address/testutil/PropertyUtil.java @@ -6,12 +6,8 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Set; - import seedu.address.logic.commands.AddPropertyCommand; -import seedu.address.logic.commands.EditCommand; import seedu.address.model.property.Property; -import seedu.address.model.tag.Tag; /** * A utility class for Property. @@ -39,25 +35,4 @@ public static String getPropertyDetails(Property property) { ); return sb.toString(); } - - /** - * Returns the part of command string for the given {@code EditPropertyDescriptor}'s details. - */ -//editprop to be implemented first -// public static String getEditPropertyDescriptorDetails(EditPropertyCommand.EditPropertyDescriptor descriptor) { -// StringBuilder sb = new StringBuilder(); -// descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); -// descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); -// descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); -// descriptor.getBudget().ifPresent(budget -> sb.append(PREFIX_BUDGET).append(budget.value).append(" ")); -// if (descriptor.getTags().isPresent()) { -// Set tags = descriptor.getTags().get(); -// if (tags.isEmpty()) { -// sb.append(PREFIX_TAG); -// } else { -// tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); -// } -// } -// return sb.toString(); -// } } diff --git a/src/test/java/seedu/address/testutil/TypicalProperties.java b/src/test/java/seedu/address/testutil/TypicalProperties.java index 8e84b11e768..a24d7e50366 100644 --- a/src/test/java/seedu/address/testutil/TypicalProperties.java +++ b/src/test/java/seedu/address/testutil/TypicalProperties.java @@ -1,13 +1,13 @@ package seedu.address.testutil; -import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_AQUAVIEW; -import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; @@ -49,15 +49,16 @@ public class TypicalProperties { .withAddress(" 808 Colorful Street, Singapore 456012").withPrice("500000").build(); // Manually added - Customer's details found in {@code CommandTestUtil} - public static final Property AQUAVIEW = new PropertyBuilder().withName(VALID_NAME_AQUAVIEW).withPhone(VALID_PHONE_AQUAVIEW) - .withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW).withTags(VALID_TAG_SQUARE).build(); - public static final Property SKYVIEW = new PropertyBuilder().withName(VALID_NAME_SKYVIEW).withPhone(VALID_PHONE_SKYVIEW) - .withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW).withTags(VALID_TAG_BIG, VALID_TAG_SQUARE) - .build(); + public static final Property AQUAVIEW = new PropertyBuilder().withName(VALID_NAME_AQUAVIEW) + .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) + .withTags(VALID_TAG_SQUARE).build(); + public static final Property SKYVIEW = new PropertyBuilder().withName(VALID_NAME_SKYVIEW) + .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) + .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER - private void TypicalProperty() {} // prevents instantiation + private TypicalProperties() {} // prevents instantiation /** * Returns an {@code AddressBook} with all the typical customers. From 25d0fc069ba2261bf620845536741c4054ba0c75 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 01:29:10 +0800 Subject: [PATCH 17/34] fix checkstyle --- .../address/logic/commands/CommandPropertyTestUtil.java | 5 +++-- .../address/logic/parser/AddPropertyCommandParserTest.java | 4 ++-- .../java/seedu/address/model/property/PropAddressTest.java | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java index c521119fd22..5ac2ea7ebb0 100644 --- a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java @@ -2,10 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.testutil.Assert.assertThrows; @@ -62,7 +62,8 @@ public class CommandPropertyTestUtil { // // static { // DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) - // .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) + // .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW) + // .withPrice(VALID_PRICE_AQUAVIEW) // .withTags(VALID_TAG_SQUARE).build(); // DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) // .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) diff --git a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java index 9d75cd6cc0e..fd5999eb28a 100644 --- a/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddPropertyCommandParserTest.java @@ -3,10 +3,10 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_SKYVIEW; -import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_TAG_DESC; import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_AQUAVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_SKYVIEW; @@ -18,10 +18,10 @@ import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_BIG; import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_SQUARE; -import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_SKYVIEW; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; diff --git a/src/test/java/seedu/address/model/property/PropAddressTest.java b/src/test/java/seedu/address/model/property/PropAddressTest.java index 87491cf0b96..4f8d078f93c 100644 --- a/src/test/java/seedu/address/model/property/PropAddressTest.java +++ b/src/test/java/seedu/address/model/property/PropAddressTest.java @@ -31,7 +31,8 @@ public void isValidPropAddress() { // valid property addresses assertTrue(PropAddress.isValidAddress("Blk 456, Den Road, #01-355")); assertTrue(PropAddress.isValidAddress("-")); // one character - assertTrue(PropAddress.isValidAddress("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA")); // long budget + assertTrue(PropAddress.isValidAddress("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA")); + // long budget } @Test From c7216317afa640b0ecada01d1317b4c59347c6e5 Mon Sep 17 00:00:00 2001 From: Fredy Lawrence Date: Wed, 25 Oct 2023 16:18:34 +0800 Subject: [PATCH 18/34] Fix PropName Regex --- src/main/java/seedu/address/model/property/PropName.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/address/model/property/PropName.java b/src/main/java/seedu/address/model/property/PropName.java index b5d8867d5be..8feef23c84f 100644 --- a/src/main/java/seedu/address/model/property/PropName.java +++ b/src/main/java/seedu/address/model/property/PropName.java @@ -10,14 +10,13 @@ public class PropName { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should should not be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; - + public static final String VALIDATION_REGEX = "[^\\s-](.*)"; public final String fullName; /** From d2863c2a062614e91b9f68ddd2e08cfeb8594570 Mon Sep 17 00:00:00 2001 From: Nicole Ng Date: Wed, 25 Oct 2023 17:12:16 +0800 Subject: [PATCH 19/34] Update DG for delete and find command and UG for find command --- docs/DeveloperGuide.md | 60 ++++++++++++++++++++++++++++++++++++++++++ docs/UserGuide.md | 43 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 9b7eb29ab7e..bb55939222c 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -154,6 +154,66 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Deleting of customers and properties +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to delete the profile of any customer or property that has been previously added into the app. For example, the property agent might want to remove a particular property after it has been sold. +Or, a particular client is no longer interested in buying a house anymore. + +#### Implementation +The `DeleteCustomerCommand` and `DeletePropertyComannd` classes extends from the `Command` class. They are used to delete the details of a Customer or Property respectively. The command expects exactly one `INDEX` of the Customer or Property to be deleted, otherwise and error message will be displayed. +When the delete command is inputted, the `DeleteCustomerCommandParser` and `DeletePropertyCommandParser` classes are used to parse the user input and create the `DeleteCustomerCommand` and `DeletePropertyCommand` objects respectively. +When these created command objects are executed by the `LogicManager`, the `DeleteCustomerCommand#execute(Model model)` or `DeletePropertyCommand#execute(Model model)` methods are called. These methods will delete the customer or property in the model, and return a `CommandResult` object. + +The following sequence diagram shows how the `DeleteCustomerCommand` is executed. +**INSERT SEQUENCE DIAGRAM HERE** + +#### Design Considerations +**Aspect: How the delete commands should relate to each other:** + +* **Alternative 1 (current choice):** `DeleteCustomerCommand` and `DeletePropertyCommand` are separate, and both inherit from the `Command` class. + * Pros: + * Both the `Customer` and `Property` classes have different fields that are exclusive to each other. + * This reduces complexity of the system, and unexpected behaviours. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase. +* **Alternative 2:** A single `DeleteCommand` class is used to edit both customer and property. + * Cons: + * Unnecessary complexity is introduced into the system. + + +### Finding of Customers and Properties +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to find and access the details of a particular Customer or Property that has been previously added into the app. For example, the property agent may want to refresh their memory on a particular customer's budget. Or the property agent may want to check the details of a particular property. + +#### Implementation +The `FindCustomerCommand` and `FindPropertyCommand` classes extends the `Command` class. They are used to find the profiles of a customer or property, respectively. +Both commands allow the user to find any customer or property. The commands expect at least one substring to base the search on, otherwise an error message will be displayed. + +When the find command is inputted, the `FindCustomerCommandParser` and `FindPropertyCommandParser` classes are used to parse the user input and create the respective `FindCustomerCommand` and `FindPropertyCommand` objects. +When these created command objects are executed by the `LogicManager`, the `FindCustomerCommand#execute(Model model)` or `FindPropertyCommand#execute(Model model)` methods are called. These methods will find the customer or property in the model, and return a `CommandResult` object. + +The following sequence diagram shows how the `FindCustomerCommand` is executed. +**INSERT SEQUENCE DIAGRAM HERE** + +#### Design Considerations +**Aspect: How the find commands should relate to each other:** + +* **Alternative 1 (current choice):** `FindCustomerCommand` and `FindPropertyCommand` are separate, and both inherit from the `Command` class. + * Pros: + * Both the `Customer` and `Property` classes have different fields that are exclusive to each other. + * This reduces complexity of the system, and unexpected behaviours. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase. +* **Alternative 2:** A single `FindCommand` class is used to find both customer and property. + * Cons: + * Unnecessary complexity is introduced into the system. + ### \[Proposed\] Undo/redo feature #### Proposed Implementation diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 15c7920322d..b4ac8260333 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -183,6 +183,49 @@ When command fails: * `No such property index` for wrong parameter or index beyond list size * `Invalid command` for misspelling of command +### Finding a customer : `findcust` + +Finds and returns a customer or a list of customers whose name contains the substring inputted. + +Format: `findcust String` + +* Finds and returns the customer(s) whose name contains the `String` substring. +* The String must be in the same language as the name, i.e English. +* The String should only contain the relevant alphabets + +Examples: +* `list` followed by `findcust F` finds and returns the customer(s) with names that begin with "F" in the customer list. +* `list` followed by `findcust F J` finds and returns the customer(s) with names that begin with "F" and/or "J" in the customer list. + + +When command succeeds: +* `1 customer listed` + +When command fails: +* `Invalid command format` for missing parameter +* `Unknown command` for misspelling of command + +### Finding a property : `findprop` + +Finds and returns a property or a list of properties whose name contains the substring inputted. + +Format: `findprop String` + +* Finds and returns the property or properties whose name contains the `String` substring. +* The String must be in the same language as the name, i.e English. +* The String should only contain the relevant alphabets + +Examples: +* `list` followed by `findprop F` finds and returns the property or properties with names that begin with "F" in the property list. +* `list` followed by `findprop F J` finds and returns the property or properties with names that begin with "F" and/or "J" in the property list. + + +When command succeeds: +* `1 property listed` + +When command fails: +* `Invalid command format` for missing parameter +* `Unknown command` for misspelling of command ### Exiting the program : `exit` From 786b65aea25a5a2d8fc4f86ea83b6595fe124c78 Mon Sep 17 00:00:00 2001 From: saraozn Date: Wed, 25 Oct 2023 18:45:40 +0800 Subject: [PATCH 20/34] add editprop command --- .../logic/commands/EditPropertyCommand.java | 243 ++++++++++++++++++ .../logic/parser/AddressBookParser.java | 4 + .../parser/EditPropertyCommandParser.java | 86 +++++++ .../address/model/ReadOnlyAddressBook.java | 1 - .../AddCustomerCommandIntegrationTest.java | 3 +- .../AddPropertyCommandIntegrationTest.java | 3 +- .../logic/commands/ClearCommandTest.java | 6 +- .../commands/CommandPropertyTestUtil.java | 26 +- .../commands/DeleteCustomerCommandTest.java | 3 +- .../commands/DeletePropertyCommandTest.java | 3 +- .../logic/commands/EditCommandTest.java | 3 +- .../commands/EditPropertyCommandTest.java | 195 ++++++++++++++ .../logic/commands/FindCommandTest.java | 6 +- .../logic/commands/ListCommandTest.java | 3 +- .../logic/parser/AddressBookParserTest.java | 24 +- .../parser/EditPropertyCommandParserTest.java | 208 +++++++++++++++ .../seedu/address/model/AddressBookTest.java | 1 - .../EditPropertyDescriptorBuilder.java | 87 +++++++ .../seedu/address/testutil/PropertyUtil.java | 33 ++- 19 files changed, 904 insertions(+), 34 deletions(-) create mode 100644 src/main/java/seedu/address/logic/commands/EditPropertyCommand.java create mode 100644 src/main/java/seedu/address/logic/parser/EditPropertyCommandParser.java create mode 100644 src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java create mode 100644 src/test/java/seedu/address/logic/parser/EditPropertyCommandParserTest.java create mode 100644 src/test/java/seedu/address/testutil/EditPropertyDescriptorBuilder.java diff --git a/src/main/java/seedu/address/logic/commands/EditPropertyCommand.java b/src/main/java/seedu/address/logic/commands/EditPropertyCommand.java new file mode 100644 index 00000000000..41ea573c8e0 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditPropertyCommand.java @@ -0,0 +1,243 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROPERTIES; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.property.Price; +import seedu.address.model.property.PropAddress; +import seedu.address.model.property.PropName; +import seedu.address.model.property.PropPhone; +import seedu.address.model.property.Property; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing property in the Price book. + */ +public class EditPropertyCommand extends Command { + + public static final String COMMAND_WORD = "editprop"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the property identified " + + "by the index number used in the displayed property list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_PRICE + "PRICE] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25"; + + public static final String MESSAGE_EDIT_PROPERTY_SUCCESS = "Edited property: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PROPERTY = "This property already exists in PropertyMatch."; + + private final Index index; + private final EditPropertyDescriptor editPropertyDescriptor; + + /** + * @param index of the property in the filtered property list to edit + * @param editPropertyDescriptor details to edit the property with + */ + public EditPropertyCommand(Index index, EditPropertyDescriptor editPropertyDescriptor) { + requireNonNull(index); + requireNonNull(editPropertyDescriptor); + + this.index = index; + this.editPropertyDescriptor = new EditPropertyDescriptor(editPropertyDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPropertyList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); + } + + Property propertyToEdit = lastShownList.get(index.getZeroBased()); + Property editedproperty = createEditedproperty(propertyToEdit, editPropertyDescriptor); + + if (!propertyToEdit.isSameProperty(editedproperty) && model.hasProperty(editedproperty)) { + throw new CommandException(MESSAGE_DUPLICATE_PROPERTY); + } + + model.setProperty(propertyToEdit, editedproperty); + model.updateFilteredPropertyList(PREDICATE_SHOW_ALL_PROPERTIES); + return new CommandResult(String.format(MESSAGE_EDIT_PROPERTY_SUCCESS, Messages.format(editedproperty))); + } + + /** + * Creates and returns a {@code property} with the details of {@code propertyToEdit} + * edited with {@code editpropertyDescriptor}. + */ + private static Property createEditedproperty(Property propertyToEdit, + EditPropertyDescriptor editpropertyDescriptor) { + assert propertyToEdit != null; + + PropName updatedName = editpropertyDescriptor.getName().orElse(propertyToEdit.getName()); + PropPhone updatedPhone = editpropertyDescriptor.getPhone().orElse(propertyToEdit.getPhone()); + PropAddress updatedAddress = editpropertyDescriptor.getAddress().orElse(propertyToEdit.getAddress()); + Price updatedPrice = editpropertyDescriptor.getPrice().orElse(propertyToEdit.getPrice()); + Set updatedTags = editpropertyDescriptor.getTags().orElse(propertyToEdit.getTags()); + + return new Property(updatedName, updatedAddress, updatedPhone, updatedPrice, updatedTags); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPropertyCommand)) { + return false; + } + + EditPropertyCommand otherEditCommand = (EditPropertyCommand) other; + return index.equals(otherEditCommand.index) + && editPropertyDescriptor.equals(otherEditCommand.editPropertyDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("editPropertyDescriptor", editPropertyDescriptor) + .toString(); + } + + /** + * Stores the details to edit the property with. Each non-empty field value will replace the + * corresponding field value of the property. + */ + public static class EditPropertyDescriptor { + private PropName name; + private PropPhone phone; + private PropAddress address; + private Price price; + private Set tags; + + public EditPropertyDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditPropertyDescriptor(EditPropertyDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setAddress(toCopy.address); + setPrice(toCopy.price); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, address, price, tags); + } + + public void setName(PropName name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(PropPhone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setAddress(PropAddress address) { + this.address = address; + } + + public Optional getAddress() { + return Optional.ofNullable(address); + } + + public void setPrice(Price price) { + this.price = price; + } + + public Optional getPrice() { + return Optional.ofNullable(price); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPropertyDescriptor)) { + return false; + } + + EditPropertyDescriptor otherEditpropertyDescriptor = (EditPropertyDescriptor) other; + return Objects.equals(name, otherEditpropertyDescriptor.name) + && Objects.equals(phone, otherEditpropertyDescriptor.phone) + && Objects.equals(address, otherEditpropertyDescriptor.address) + && Objects.equals(price, otherEditpropertyDescriptor.price) + && Objects.equals(tags, otherEditpropertyDescriptor.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("address", address) + .add("price", price) + .add("tags", tags) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 768306ab7fd..507ecaf16f1 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -15,6 +15,7 @@ import seedu.address.logic.commands.DeleteCustomerCommand; import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditPropertyCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; @@ -65,6 +66,9 @@ public Command parseCommand(String userInput) throws ParseException { case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); + case EditPropertyCommand.COMMAND_WORD: + return new EditPropertyCommandParser().parse(arguments); + case DeletePropertyCommand.COMMAND_WORD: return new DeletePropertyCommandParser().parse(arguments); diff --git a/src/main/java/seedu/address/logic/parser/EditPropertyCommandParser.java b/src/main/java/seedu/address/logic/parser/EditPropertyCommandParser.java new file mode 100644 index 00000000000..f43bf1a6e4a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditPropertyCommandParser.java @@ -0,0 +1,86 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditPropertyCommand; +import seedu.address.logic.commands.EditPropertyCommand.EditPropertyDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditPropertyCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditPropertyCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_ADDRESS, PREFIX_PRICE, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditPropertyCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_ADDRESS, PREFIX_PRICE); + + EditPropertyDescriptor editPropertyDescriptor = new EditPropertyDescriptor(); + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editPropertyDescriptor.setName(ParserUtil.parsePropName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + editPropertyDescriptor.setPhone(ParserUtil.parsePropPhone(argMultimap.getValue(PREFIX_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editPropertyDescriptor.setAddress(ParserUtil.parsePropAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + } + if (argMultimap.getValue(PREFIX_PRICE).isPresent()) { + editPropertyDescriptor.setPrice(ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPropertyDescriptor::setTags); + + if (!editPropertyDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditPropertyCommand.MESSAGE_NOT_EDITED); + } + + return new EditPropertyCommand(index, editPropertyDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 7f0b9992b80..bc627f9e86e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -13,5 +13,4 @@ public interface ReadOnlyAddressBook { * This list will not contain any duplicate customers. */ ObservableList getCustomerList(); - } diff --git a/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java index 59d4b139622..447b4532eb1 100644 --- a/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java @@ -3,6 +3,7 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,7 +25,7 @@ public class AddCustomerCommandIntegrationTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); } @Test diff --git a/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java index aeca9057e8d..6f6e58ed3de 100644 --- a/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java @@ -3,6 +3,7 @@ import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,7 +25,7 @@ public class AddPropertyCommandIntegrationTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); } @Test diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java index 036a49fccea..8f184a95b2e 100644 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java @@ -2,13 +2,13 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.Test; import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; public class ClearCommandTest { @@ -23,8 +23,8 @@ public void execute_emptyAddressBook_success() { @Test public void execute_nonEmptyAddressBook_success() { - Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); - Model expectedModel = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); expectedModel.setAddressBook(new AddressBook()); assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java index 5ac2ea7ebb0..c877a970dbf 100644 --- a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java @@ -19,6 +19,7 @@ import seedu.address.model.PropertyBook; import seedu.address.model.property.PropNameContainsKeywordsPredicate; import seedu.address.model.property.Property; +import seedu.address.testutil.EditPropertyDescriptorBuilder; /** * Contains helper methods for testing commands. @@ -56,19 +57,18 @@ public class CommandPropertyTestUtil { public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; - //uncomment after editprop command is done - // public static final EditCommand.EditPropertyDescriptor DESC_AQUAVIEW; - // public static final EditCommand.EditPropertyDescriptor DESC_SKYVIEW; - // - // static { - // DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) - // .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW) - // .withPrice(VALID_PRICE_AQUAVIEW) - // .withTags(VALID_TAG_SQUARE).build(); - // DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) - // .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) - // .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); - // } + public static final EditPropertyCommand.EditPropertyDescriptor DESC_AQUAVIEW; + public static final EditPropertyCommand.EditPropertyDescriptor DESC_SKYVIEW; + + static { + DESC_AQUAVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) + .withPhone(VALID_PHONE_AQUAVIEW).withAddress(VALID_ADDRESS_AQUAVIEW) + .withPrice(VALID_PRICE_AQUAVIEW) + .withTags(VALID_TAG_SQUARE).build(); + DESC_SKYVIEW = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) + .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_SKYVIEW).withPrice(VALID_PRICE_SKYVIEW) + .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); + } /** * Executes the given {@code command}, confirms that
diff --git a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java index 1229a867f51..10b1306b559 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java @@ -9,6 +9,7 @@ import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.Test; @@ -25,7 +26,7 @@ */ public class DeleteCustomerCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java index 34390d0149e..9ce1d500494 100644 --- a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -9,6 +9,7 @@ import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PROPERTY; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.Test; @@ -26,7 +27,7 @@ */ public class DeletePropertyCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 27b42713546..339637ad2e8 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -14,6 +14,7 @@ import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_CUSTOMER; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.Test; @@ -34,7 +35,7 @@ */ public class EditCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); @Test public void execute_allFieldsSpecifiedUnfilteredList_success() { diff --git a/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java new file mode 100644 index 00000000000..ddf5ac12628 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java @@ -0,0 +1,195 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandPropertyTestUtil.DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandPropertyTestUtil.showPropertyAtIndex; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PROPERTY; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditPropertyCommand.EditPropertyDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.PropertyBook; +import seedu.address.model.UserPrefs; +import seedu.address.model.property.Property; +import seedu.address.testutil.EditPropertyDescriptorBuilder; +import seedu.address.testutil.PropertyBuilder; + + +/** + * Contains integration tests (interaction with the Model) and unit tests for EditCommand. + */ +public class EditPropertyCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + Property editedProperty = new PropertyBuilder().build(); + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder(editedProperty).build(); + EditPropertyCommand editCommand = new EditPropertyCommand(INDEX_FIRST_PROPERTY, descriptor); + + String expectedMessage = String.format(EditPropertyCommand.MESSAGE_EDIT_PROPERTY_SUCCESS, + Messages.format(editedProperty)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new PropertyBook(), new UserPrefs()); + expectedModel.setProperty(model.getFilteredPropertyList().get(0), editedProperty); + + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() { + Index indexLastProperty = Index.fromOneBased(model.getFilteredPropertyList().size()); + Property lastProperty = model.getFilteredPropertyList().get(indexLastProperty.getZeroBased()); + + PropertyBuilder propertyInList = new PropertyBuilder(lastProperty); + Property editedProperty = propertyInList.withName(VALID_NAME_SKYVIEW).withPhone(VALID_PHONE_SKYVIEW) + .withTags(VALID_TAG_BIG).build(); + + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW) + .withPhone(VALID_PHONE_SKYVIEW).withTags(VALID_TAG_BIG).build(); + EditPropertyCommand editCommand = new EditPropertyCommand(indexLastProperty, descriptor); + + String expectedMessage = String.format(EditPropertyCommand.MESSAGE_EDIT_PROPERTY_SUCCESS, + Messages.format(editedProperty)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new PropertyBook(), new UserPrefs()); + expectedModel.setProperty(lastProperty, editedProperty); + + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_noFieldSpecifiedUnfilteredList_success() { + EditPropertyCommand editCommand = new EditPropertyCommand(INDEX_FIRST_PROPERTY, new EditPropertyDescriptor()); + Property editedProperty = model.getFilteredPropertyList().get(INDEX_FIRST_PROPERTY.getZeroBased()); + + String expectedMessage = String.format(EditPropertyCommand.MESSAGE_EDIT_PROPERTY_SUCCESS, + Messages.format(editedProperty)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new PropertyBook(), new UserPrefs()); + + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_filteredList_success() { + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); + + Property propertyInFilteredList = model.getFilteredPropertyList().get(INDEX_FIRST_PROPERTY.getZeroBased()); + Property editedProperty = new PropertyBuilder(propertyInFilteredList).withName(VALID_NAME_SKYVIEW).build(); + EditPropertyCommand editCommand = new EditPropertyCommand(INDEX_FIRST_PROPERTY, + new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW).build()); + + String expectedMessage = String.format(EditPropertyCommand.MESSAGE_EDIT_PROPERTY_SUCCESS, + Messages.format(editedProperty)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new PropertyBook(), new UserPrefs()); + expectedModel.setProperty(model.getFilteredPropertyList().get(0), editedProperty); + + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicatePropertyUnfilteredList_failure() { + Property firstProperty = model.getFilteredPropertyList().get(INDEX_FIRST_PROPERTY.getZeroBased()); + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder(firstProperty).build(); + EditPropertyCommand editCommand = new EditPropertyCommand(INDEX_SECOND_PROPERTY, descriptor); + + assertCommandFailure(editCommand, model, EditPropertyCommand.MESSAGE_DUPLICATE_PROPERTY); + } + + @Test + public void execute_duplicatePropertyFilteredList_failure() { + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); + + // edit property in filtered list into a duplicate in budget book + Property propertyInList = model.getPropertyBook().getPropertyList().get(INDEX_SECOND_PROPERTY.getZeroBased()); + EditPropertyCommand editCommand = new EditPropertyCommand(INDEX_FIRST_PROPERTY, + new EditPropertyDescriptorBuilder(propertyInList).build()); + + assertCommandFailure(editCommand, model, EditPropertyCommand.MESSAGE_DUPLICATE_PROPERTY); + } + + @Test + public void execute_invalidPropertyIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPropertyList().size() + 1); + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW).build(); + EditPropertyCommand editCommand = new EditPropertyCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of budget book + */ + @Test + public void execute_invalidPropertyIndexFilteredList_failure() { + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); + Index outOfBoundIndex = INDEX_SECOND_PROPERTY; + // ensures that outOfBoundIndex is still in bounds of budget book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getPropertyBook().getPropertyList().size()); + + EditPropertyCommand editCommand = new EditPropertyCommand(outOfBoundIndex, + new EditPropertyDescriptorBuilder().withName(VALID_NAME_SKYVIEW).build()); + + assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PROPERTY_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final EditPropertyCommand standardCommand = new EditPropertyCommand(INDEX_FIRST_PROPERTY, DESC_AQUAVIEW); + + // same values -> returns true + EditPropertyDescriptor copyDescriptor = new EditPropertyDescriptor(DESC_AQUAVIEW); + EditPropertyCommand commandWithSameValues = new EditPropertyCommand(INDEX_FIRST_PROPERTY, copyDescriptor); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new EditPropertyCommand(INDEX_SECOND_PROPERTY, DESC_AQUAVIEW))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new EditPropertyCommand(INDEX_FIRST_PROPERTY, DESC_SKYVIEW))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + EditPropertyDescriptor editPropertyDescriptor = new EditPropertyDescriptor(); + EditPropertyCommand editCommand = new EditPropertyCommand(index, editPropertyDescriptor); + String expected = EditPropertyCommand.class.getCanonicalName() + "{index=" + index + ", editPropertyDescriptor=" + + editPropertyDescriptor + "}"; + assertEquals(expected, editCommand.toString()); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index a1b30ab0b6a..25f4b538a96 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -9,6 +9,7 @@ import static seedu.address.testutil.TypicalCustomers.ELLE; import static seedu.address.testutil.TypicalCustomers.FIONA; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import java.util.Arrays; import java.util.Collections; @@ -17,7 +18,6 @@ import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.customer.NameContainsKeywordsPredicate; @@ -25,8 +25,8 @@ * Contains integration tests (interaction with the Model) for {@code FindCommand}. */ public class FindCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); - private Model expectedModel = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); @Test public void equals() { diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index 51a0d0be400..2faa743340c 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -4,6 +4,7 @@ import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,7 +24,7 @@ public class ListCommandTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 59699038857..1db740aad61 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -6,6 +6,7 @@ import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; import java.util.Arrays; import java.util.List; @@ -17,7 +18,9 @@ import seedu.address.logic.commands.AddPropertyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.DeleteCustomerCommand; +import seedu.address.logic.commands.DeletePropertyCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditPropertyCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; @@ -29,6 +32,7 @@ import seedu.address.testutil.CustomerBuilder; import seedu.address.testutil.CustomerUtil; import seedu.address.testutil.EditCustomerDescriptorBuilder; +import seedu.address.testutil.EditPropertyDescriptorBuilder; import seedu.address.testutil.PropertyBuilder; import seedu.address.testutil.PropertyUtil; @@ -59,11 +63,17 @@ public void parseCommand_clear() throws Exception { @Test public void parseCommand_delcust() throws Exception { - DeleteCustomerCommand command = (DeleteCustomerCommand) parser.parseCommand( - DeleteCustomerCommand.COMMAND_WORD + " " + INDEX_FIRST_CUSTOMER.getOneBased()); - assertEquals(new DeleteCustomerCommand(INDEX_FIRST_CUSTOMER), command); + DeletePropertyCommand command = (DeletePropertyCommand) parser.parseCommand( + DeletePropertyCommand.COMMAND_WORD + " " + INDEX_FIRST_PROPERTY.getOneBased()); + assertEquals(new DeletePropertyCommand(INDEX_FIRST_PROPERTY), command); } + @Test + public void parseCommand_delprop() throws Exception { + DeleteCustomerCommand command = (DeleteCustomerCommand) parser.parseCommand( + DeleteCustomerCommand.COMMAND_WORD + " " + INDEX_FIRST_PROPERTY.getOneBased()); + assertEquals(new DeleteCustomerCommand(INDEX_FIRST_PROPERTY), command); + } @Test public void parseCommand_edit() throws Exception { Customer customer = new CustomerBuilder().build(); @@ -72,6 +82,14 @@ public void parseCommand_edit() throws Exception { + INDEX_FIRST_CUSTOMER.getOneBased() + " " + CustomerUtil.getEditCustomerDescriptorDetails(descriptor)); assertEquals(new EditCommand(INDEX_FIRST_CUSTOMER, descriptor), command); } + @Test + public void parseCommand_editprop() throws Exception { + Property property = new PropertyBuilder().build(); + EditPropertyCommand.EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder(property).build(); + EditPropertyCommand command = (EditPropertyCommand) parser.parseCommand(EditPropertyCommand.COMMAND_WORD + " " + + INDEX_FIRST_PROPERTY.getOneBased() + " " + PropertyUtil.getEditPropertyDescriptorDetails(descriptor)); + assertEquals(new EditPropertyCommand(INDEX_FIRST_PROPERTY, descriptor), command); + } @Test public void parseCommand_exit() throws Exception { diff --git a/src/test/java/seedu/address/logic/parser/EditPropertyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditPropertyCommandParserTest.java new file mode 100644 index 00000000000..edb4c51ee13 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditPropertyCommandParserTest.java @@ -0,0 +1,208 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.ADDRESS_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PHONE_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_PRICE_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandPropertyTestUtil.NAME_DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PHONE_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.PRICE_DESC_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.TAG_DESC_SQUARE; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_ADDRESS_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_NAME_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PHONE_SKYVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_PRICE_AQUAVIEW; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_BIG; +import static seedu.address.logic.commands.CommandPropertyTestUtil.VALID_TAG_SQUARE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PROPERTY; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PROPERTY; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditPropertyCommand; +import seedu.address.logic.commands.EditPropertyCommand.EditPropertyDescriptor; +import seedu.address.model.property.Price; +import seedu.address.model.property.PropAddress; +import seedu.address.model.property.PropName; +import seedu.address.model.property.PropPhone; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditPropertyDescriptorBuilder; + +public class EditPropertyCommandParserTest { + + private static final String TAG_EMPTY = " " + PREFIX_TAG; + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditPropertyCommand.MESSAGE_USAGE); + + private EditPropertyCommandParser parser = new EditPropertyCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_NAME_AQUAVIEW, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", EditPropertyCommand.MESSAGE_NOT_EDITED); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + NAME_DESC_AQUAVIEW, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + NAME_DESC_AQUAVIEW, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_NAME_DESC, PropName.MESSAGE_CONSTRAINTS); // invalid name + assertParseFailure(parser, "1" + INVALID_PHONE_DESC, PropPhone.MESSAGE_CONSTRAINTS); // invalid phone + assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, PropAddress.MESSAGE_CONSTRAINTS); // invalid address + assertParseFailure(parser, "1" + INVALID_PRICE_DESC, Price.MESSAGE_CONSTRAINTS); // invalid price + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag + + // invalid phone followed by valid address + assertParseFailure(parser, "1" + INVALID_PHONE_DESC + ADDRESS_DESC_AQUAVIEW, PropPhone.MESSAGE_CONSTRAINTS); + + // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Property} being edited, + // parsing it together with a valid tag results in error + assertParseFailure(parser, "1" + TAG_DESC_SQUARE + TAG_DESC_BIG + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_SQUARE + TAG_EMPTY + TAG_DESC_BIG, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_SQUARE + TAG_DESC_BIG, Tag.MESSAGE_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_ADDRESS_DESC + + VALID_PRICE_AQUAVIEW + VALID_PHONE_AQUAVIEW, PropName.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_allFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND_PROPERTY; + String userInput = targetIndex.getOneBased() + PHONE_DESC_SKYVIEW + TAG_DESC_BIG + + ADDRESS_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW + NAME_DESC_AQUAVIEW + TAG_DESC_SQUARE; + + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW) + .withPhone(VALID_PHONE_SKYVIEW).withAddress(VALID_ADDRESS_AQUAVIEW).withPrice(VALID_PRICE_AQUAVIEW) + .withTags(VALID_TAG_BIG, VALID_TAG_SQUARE).build(); + EditPropertyCommand expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_someFieldsSpecified_success() { + Index targetIndex = INDEX_FIRST_PROPERTY; + String userInput = targetIndex.getOneBased() + PHONE_DESC_SKYVIEW + ADDRESS_DESC_AQUAVIEW; + + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withPhone(VALID_PHONE_SKYVIEW) + .withAddress(VALID_ADDRESS_AQUAVIEW).build(); + EditPropertyCommand expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_oneFieldSpecified_success() { + // name + Index targetIndex = INDEX_THIRD_PROPERTY; + String userInput = targetIndex.getOneBased() + NAME_DESC_AQUAVIEW; + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withName(VALID_NAME_AQUAVIEW).build(); + EditPropertyCommand expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // phone + userInput = targetIndex.getOneBased() + PHONE_DESC_AQUAVIEW; + descriptor = new EditPropertyDescriptorBuilder().withPhone(VALID_PHONE_AQUAVIEW).build(); + expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // ADDRESS + userInput = targetIndex.getOneBased() + ADDRESS_DESC_AQUAVIEW; + descriptor = new EditPropertyDescriptorBuilder().withAddress(VALID_ADDRESS_AQUAVIEW).build(); + expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // PRICE + userInput = targetIndex.getOneBased() + PRICE_DESC_AQUAVIEW; + descriptor = new EditPropertyDescriptorBuilder().withPrice(VALID_PRICE_AQUAVIEW).build(); + expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + // tags + userInput = targetIndex.getOneBased() + TAG_DESC_SQUARE; + descriptor = new EditPropertyDescriptorBuilder().withTags(VALID_TAG_SQUARE).build(); + expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleRepeatedFields_failure() { + // More extensive testing of duplicate parameter detections is done in + // AddPropertyCommandParserTest#parse_repeatedNonTagValue_failure() + + // valid followed by invalid + Index targetIndex = INDEX_FIRST_PROPERTY; + String userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + PHONE_DESC_SKYVIEW; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + + // invalid followed by valid + userInput = targetIndex.getOneBased() + PHONE_DESC_SKYVIEW + INVALID_PHONE_DESC; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + + // mulltiple valid fields repeated + userInput = targetIndex.getOneBased() + PHONE_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + + TAG_DESC_SQUARE + PHONE_DESC_AQUAVIEW + PRICE_DESC_AQUAVIEW + ADDRESS_DESC_AQUAVIEW + TAG_DESC_SQUARE + + PHONE_DESC_SKYVIEW + PRICE_DESC_SKYVIEW + ADDRESS_DESC_SKYVIEW + TAG_DESC_BIG; + + assertParseFailure(parser, userInput, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE, PREFIX_ADDRESS, PREFIX_PRICE)); + + // multiple invalid values + userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + INVALID_PRICE_DESC + INVALID_ADDRESS_DESC + + INVALID_PHONE_DESC + INVALID_PRICE_DESC + INVALID_ADDRESS_DESC; + + assertParseFailure(parser, userInput, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE, PREFIX_ADDRESS, PREFIX_PRICE)); + } + + @Test + public void parse_resetTags_success() { + Index targetIndex = INDEX_THIRD_PROPERTY; + String userInput = targetIndex.getOneBased() + TAG_EMPTY; + + EditPropertyDescriptor descriptor = new EditPropertyDescriptorBuilder().withTags().build(); + EditPropertyCommand expectedCommand = new EditPropertyCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 809f8147b8e..3de974bea95 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -94,7 +94,6 @@ public void toStringMethod() { */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList customers = FXCollections.observableArrayList(); - AddressBookStub(Collection customers) { this.customers.setAll(customers); } diff --git a/src/test/java/seedu/address/testutil/EditPropertyDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPropertyDescriptorBuilder.java new file mode 100644 index 00000000000..6bc36bc4f09 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditPropertyDescriptorBuilder.java @@ -0,0 +1,87 @@ +package seedu.address.testutil; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.EditPropertyCommand.EditPropertyDescriptor; +import seedu.address.model.property.Price; +import seedu.address.model.property.PropAddress; +import seedu.address.model.property.PropName; +import seedu.address.model.property.PropPhone; +import seedu.address.model.property.Property; +import seedu.address.model.tag.Tag; + +/** + * A utility class to help with building EditpropertyDescriptor objects. + */ +public class EditPropertyDescriptorBuilder { + + private EditPropertyDescriptor descriptor; + + public EditPropertyDescriptorBuilder() { + descriptor = new EditPropertyDescriptor(); + } + + public EditPropertyDescriptorBuilder(EditPropertyDescriptor descriptor) { + this.descriptor = new EditPropertyDescriptor(descriptor); + } + + /** + * Returns an {@code EditpropertyDescriptor} with fields containing {@code property}'s details + */ + public EditPropertyDescriptorBuilder(Property property) { + descriptor = new EditPropertyDescriptor(); + descriptor.setName(property.getName()); + descriptor.setPhone(property.getPhone()); + descriptor.setAddress(property.getAddress()); + descriptor.setPrice(property.getPrice()); + descriptor.setTags(property.getTags()); + } + + /** + * Sets the {@code Name} of the {@code EditpropertyDescriptor} that we are building. + */ + public EditPropertyDescriptorBuilder withName(String name) { + descriptor.setName(new PropName(name)); + return this; + } + + /** + * Sets the {@code Phone} of the {@code EditpropertyDescriptor} that we are building. + */ + public EditPropertyDescriptorBuilder withPhone(String phone) { + descriptor.setPhone(new PropPhone(phone)); + return this; + } + + /** + * Sets the {@code Address} of the {@code EditpropertyDescriptor} that we are building. + */ + public EditPropertyDescriptorBuilder withAddress(String address) { + descriptor.setAddress(new PropAddress(address)); + return this; + } + + /** + * Sets the {@code Price} of the {@code EditpropertyDescriptor} that we are building. + */ + public EditPropertyDescriptorBuilder withPrice(String price) { + descriptor.setPrice(new Price(price)); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditpropertyDescriptor} + * that we are building. + */ + public EditPropertyDescriptorBuilder withTags(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTags(tagSet); + return this; + } + + public EditPropertyDescriptor build() { + return descriptor; + } +} diff --git a/src/test/java/seedu/address/testutil/PropertyUtil.java b/src/test/java/seedu/address/testutil/PropertyUtil.java index ab9dc08e09c..d21cafa654f 100644 --- a/src/test/java/seedu/address/testutil/PropertyUtil.java +++ b/src/test/java/seedu/address/testutil/PropertyUtil.java @@ -1,13 +1,19 @@ package seedu.address.testutil; -import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Set; + import seedu.address.logic.commands.AddPropertyCommand; +import seedu.address.logic.commands.EditPropertyCommand; import seedu.address.model.property.Property; +import seedu.address.model.tag.Tag; + + /** * A utility class for Property. @@ -28,11 +34,30 @@ public static String getPropertyDetails(Property property) { StringBuilder sb = new StringBuilder(); sb.append(PREFIX_NAME + property.getName().fullName + " "); sb.append(PREFIX_PHONE + property.getPhone().value + " "); - sb.append(PREFIX_EMAIL + property.getAddress().value + " "); - sb.append(PREFIX_BUDGET + property.getPrice().value + " "); + sb.append(PREFIX_ADDRESS + property.getAddress().value + " "); + sb.append(PREFIX_PRICE + property.getPrice().value + " "); property.getTags().stream().forEach( s -> sb.append(PREFIX_TAG + s.tagName + " ") ); return sb.toString(); } + /** + * Returns the part of command string for the given {@code EditPropertyDescriptor}'s details. + */ + public static String getEditPropertyDescriptorDetails(EditPropertyCommand.EditPropertyDescriptor descriptor) { + StringBuilder sb = new StringBuilder(); + descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); + descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); + descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getPrice().ifPresent(price -> sb.append(PREFIX_PRICE).append(price.value).append(" ")); + if (descriptor.getTags().isPresent()) { + Set tags = descriptor.getTags().get(); + if (tags.isEmpty()) { + sb.append(PREFIX_TAG); + } else { + tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); + } + } + return sb.toString(); + } } From fe815ccf8cd37fdcc6ec7966cc9e53f2baddcbcc Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Wed, 25 Oct 2023 22:37:53 +0800 Subject: [PATCH 21/34] Fix cicd pipeline errors --- build.gradle | 4 ++++ .../java/seedu/address/logic/LogicManagerTest.java | 2 +- .../commands/AddCustomerCommandIntegrationTest.java | 6 +++--- .../commands/AddPropertyCommandIntegrationTest.java | 6 +++--- .../logic/commands/CommandPropertyTestUtil.java | 4 ++-- .../logic/commands/DeletePropertyCommandTest.java | 8 ++++---- .../logic/commands/ListCustomerCommandTest.java | 6 +++--- .../logic/commands/ListPropertyCommandTest.java | 12 ++++++------ .../address/logic/parser/AddressBookParserTest.java | 1 + .../PropNameContainsKeywordsPredicateTest.java | 5 ++--- .../seedu/address/model/property/PropNameTest.java | 4 ++-- .../seedu/address/model/property/PropertyTest.java | 2 +- .../java/seedu/address/testutil/PropertyUtil.java | 8 ++++---- .../seedu/address/testutil/TypicalProperties.java | 6 +++--- 14 files changed, 39 insertions(+), 35 deletions(-) diff --git a/build.gradle b/build.gradle index aa0fc4552f1..b8bb251c22e 100644 --- a/build.gradle +++ b/build.gradle @@ -69,4 +69,8 @@ shadowJar { archiveFileName = 'addressbook.jar' } +run { + enableAssertions = true +} + defaultTasks 'clean', 'test' diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index d507d71a422..690c7fed2b0 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -64,7 +64,7 @@ public void execute_invalidCommandFormat_throwsParseException() { @Test public void execute_commandExecutionError_throwsCommandException() { - String deleteCommand = "delete 9"; + String deleteCommand = "delcust 9"; assertCommandException(deleteCommand, MESSAGE_INVALID_CUSTOMER_DISPLAYED_INDEX); } diff --git a/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java index 59d4b139622..980dab6d815 100644 --- a/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCustomerCommandIntegrationTest.java @@ -3,6 +3,7 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,7 +11,6 @@ import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.customer.Customer; import seedu.address.testutil.CustomerBuilder; @@ -24,14 +24,14 @@ public class AddCustomerCommandIntegrationTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); } @Test public void execute_newCustomer_success() { Customer validCustomer = new CustomerBuilder().build(); - Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.addCustomer(validCustomer); assertCommandSuccess(new AddCustomerCommand(validCustomer), model, diff --git a/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java index aeca9057e8d..9ac32828953 100644 --- a/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddPropertyCommandIntegrationTest.java @@ -3,6 +3,7 @@ import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandPropertyTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,7 +11,6 @@ import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.property.Property; import seedu.address.testutil.PropertyBuilder; @@ -24,14 +24,14 @@ public class AddPropertyCommandIntegrationTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); } @Test public void execute_newProperty_success() { Property validProperty = new PropertyBuilder().build(); - Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.addProperty(validProperty); assertCommandSuccess(new AddPropertyCommand(validProperty), model, diff --git a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java index 5ac2ea7ebb0..304a185aef0 100644 --- a/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandPropertyTestUtil.java @@ -47,9 +47,9 @@ public class CommandPropertyTestUtil { public static final String TAG_DESC_SQUARE = " " + PREFIX_TAG + VALID_TAG_SQUARE; public static final String TAG_DESC_BIG = " " + PREFIX_TAG + VALID_TAG_BIG; - public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names + public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + " "; // '&' not allowed in names public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + "SKYVIEW!"; // ! not allowed in address + public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + " "; // ! not allowed in address public static final String INVALID_PRICE_DESC = " " + PREFIX_PRICE; // empty string not allowed for PRICE public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags diff --git a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java index 34390d0149e..645f7719e4c 100644 --- a/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeletePropertyCommandTest.java @@ -9,6 +9,7 @@ import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PROPERTY; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.Test; @@ -16,7 +17,6 @@ import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.property.Property; @@ -26,7 +26,7 @@ */ public class DeletePropertyCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { @@ -36,7 +36,7 @@ public void execute_validIndexUnfilteredList_success() { String expectedMessage = String.format(DeletePropertyCommand.MESSAGE_DELETE_PROPERTY_SUCCESS, Messages.format(propertyToDelete)); - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.deleteProperty(propertyToDelete); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); @@ -60,7 +60,7 @@ public void execute_validIndexFilteredList_success() { String expectedMessage = String.format(DeletePropertyCommand.MESSAGE_DELETE_PROPERTY_SUCCESS, Messages.format(propertyToDelete)); - Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.deleteProperty(propertyToDelete); showNoProperty(expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java index 775bef2fd97..02a9915722b 100644 --- a/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCustomerCommandTest.java @@ -4,13 +4,13 @@ import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; /** @@ -23,8 +23,8 @@ public class ListCustomerCommandTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); } @Test diff --git a/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java index 02c5b41de04..1df2c8a1ec2 100644 --- a/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListPropertyCommandTest.java @@ -1,16 +1,16 @@ package seedu.address.logic.commands; +import static seedu.address.logic.commands.CommandPropertyTestUtil.showPropertyAtIndex; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showCustomerAtIndex; import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_CUSTOMER; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PROPERTY; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; /** @@ -23,8 +23,8 @@ public class ListPropertyCommandTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new PropertyBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); } @Test @@ -34,7 +34,7 @@ public void execute_listIsNotFiltered_showsSameList() { @Test public void execute_listIsFiltered_showsEverything() { - showCustomerAtIndex(model, INDEX_FIRST_CUSTOMER); + showPropertyAtIndex(model, INDEX_FIRST_PROPERTY); assertCommandSuccess(new ListPropertyCommand(), model, ListPropertyCommand.MESSAGE_SUCCESS, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 59699038857..d5f21d22d22 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -47,6 +47,7 @@ public void parseCommand_addcust() throws Exception { public void parseCommand_addprop() throws Exception { Property property = new PropertyBuilder().build(); AddPropertyCommand command = (AddPropertyCommand) parser.parseCommand(PropertyUtil.getAddCommand(property)); + System.out.println(command); assertEquals(new AddPropertyCommand(property), command); } diff --git a/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java index 9ba6a09de0b..896903d55d7 100644 --- a/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/property/PropNameContainsKeywordsPredicateTest.java @@ -47,7 +47,7 @@ public void test_nameContainsKeywords_returnsTrue() { // One keyword PropNameContainsKeywordsPredicate predicate = new PropNameContainsKeywordsPredicate(Collections.singletonList("Aquavista")); - assertTrue(predicate.test(new PropertyBuilder().withName(" Skyview").build())); + assertTrue(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); // Multiple keywords predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("Aquavista", "Skyview")); @@ -73,8 +73,7 @@ public void test_nameDoesNotContainKeywords_returnsFalse() { assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista Skyview").build())); // Keywords match phone, address and price, but does not match name - predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("12345", "123 Orchid Lane, Singapore 456789", - "Main", "Street")); + predicate = new PropNameContainsKeywordsPredicate(Arrays.asList("Horizonview", "Main", "Street")); assertFalse(predicate.test(new PropertyBuilder().withName("Aquavista").withPhone("12345") .withAddress("123 Orchid Lane, Singapore 456789").withPrice("123456").build())); } diff --git a/src/test/java/seedu/address/model/property/PropNameTest.java b/src/test/java/seedu/address/model/property/PropNameTest.java index f32ff61f936..c7aee379194 100644 --- a/src/test/java/seedu/address/model/property/PropNameTest.java +++ b/src/test/java/seedu/address/model/property/PropNameTest.java @@ -27,10 +27,10 @@ public void isValidPropName() { // invalid name assertFalse(PropName.isValidName("")); // empty string assertFalse(PropName.isValidName(" ")); // spaces only - assertFalse(PropName.isValidName("^")); // only non-alphanumeric characters - assertFalse(PropName.isValidName("skyview*")); // contains non-alphanumeric characters // valid name + assertTrue(PropName.isValidName("^")); // only non-alphanumeric characters + assertTrue(PropName.isValidName("skyview*")); // contains non-alphanumeric characters assertTrue(PropName.isValidName("skyview")); // alphabets only assertTrue(PropName.isValidName("12345")); // numbers only assertTrue(PropName.isValidName("skyview 2")); // alphanumeric characters diff --git a/src/test/java/seedu/address/model/property/PropertyTest.java b/src/test/java/seedu/address/model/property/PropertyTest.java index 6cf093bfe75..817669e16b9 100644 --- a/src/test/java/seedu/address/model/property/PropertyTest.java +++ b/src/test/java/seedu/address/model/property/PropertyTest.java @@ -94,7 +94,7 @@ public void equals() { @Test public void toStringMethod() { String expected = Property.class.getCanonicalName() + "{name=" + AQUAVISTA.getName() - + ", phone=" + AQUAVISTA.getPhone() + ", address=" + AQUAVISTA.getAddress() + + ", address=" + AQUAVISTA.getAddress() + ", phone=" + AQUAVISTA.getPhone() + ", price=" + AQUAVISTA.getPrice() + ", tags=" + AQUAVISTA.getTags() + "}"; assertEquals(expected, AQUAVISTA.toString()); } diff --git a/src/test/java/seedu/address/testutil/PropertyUtil.java b/src/test/java/seedu/address/testutil/PropertyUtil.java index ab9dc08e09c..48fb1c1beb8 100644 --- a/src/test/java/seedu/address/testutil/PropertyUtil.java +++ b/src/test/java/seedu/address/testutil/PropertyUtil.java @@ -1,9 +1,9 @@ package seedu.address.testutil; -import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import seedu.address.logic.commands.AddPropertyCommand; @@ -28,8 +28,8 @@ public static String getPropertyDetails(Property property) { StringBuilder sb = new StringBuilder(); sb.append(PREFIX_NAME + property.getName().fullName + " "); sb.append(PREFIX_PHONE + property.getPhone().value + " "); - sb.append(PREFIX_EMAIL + property.getAddress().value + " "); - sb.append(PREFIX_BUDGET + property.getPrice().value + " "); + sb.append(PREFIX_ADDRESS + property.getAddress().value + " "); + sb.append(PREFIX_PRICE + property.getPrice().value + " "); property.getTags().stream().forEach( s -> sb.append(PREFIX_TAG + s.tagName + " ") ); diff --git a/src/test/java/seedu/address/testutil/TypicalProperties.java b/src/test/java/seedu/address/testutil/TypicalProperties.java index a24d7e50366..a975b2e3357 100644 --- a/src/test/java/seedu/address/testutil/TypicalProperties.java +++ b/src/test/java/seedu/address/testutil/TypicalProperties.java @@ -36,17 +36,17 @@ public class TypicalProperties { public static final Property LUXELOFT = new PropertyBuilder().withName("Luxeloft").withPhone("87652533") .withAddress("234 Amber Crescent, Singapore 567890").withPrice("10000").withTags("garage").build(); public static final Property RIVERIA = new PropertyBuilder().withName("Riveria").withPhone("9482224") - .withAddress(" 567 Maple Lane, Singapore 109876").withPrice("4000000").build(); + .withAddress("567 Maple Lane, Singapore 109876").withPrice("4000000").build(); public static final Property AZURE = new PropertyBuilder().withName("Azure").withPhone("9482427") .withAddress("101 Radiant Lane, Singapore 123456").withPrice("874000").build(); public static final Property TRANQUILIS = new PropertyBuilder().withName("Tranquilis").withPhone("9482442") - .withAddress(" 202 Shoreline Street, Singapore 654321").withPrice("321950").build(); + .withAddress("202 Shoreline Street, Singapore 654321").withPrice("321950").build(); // Manually added public static final Property ASCEND = new PropertyBuilder().withName("Ascend").withPhone("8482424") .withAddress("909 Skyline Boulevard, Singapore 789123").withPrice("4321000").build(); public static final Property SPECTRA = new PropertyBuilder().withName("Spectra").withPhone("8482131") - .withAddress(" 808 Colorful Street, Singapore 456012").withPrice("500000").build(); + .withAddress("808 Colorful Street, Singapore 456012").withPrice("500000").build(); // Manually added - Customer's details found in {@code CommandTestUtil} public static final Property AQUAVIEW = new PropertyBuilder().withName(VALID_NAME_AQUAVIEW) From 4f6d3c6cfc0e5e582e82dc0d2a0ee9fadf4f602e Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 10:04:01 +0800 Subject: [PATCH 22/34] Fix editprop tests errors --- .../address/logic/commands/DeleteCustomerCommandTest.java | 5 ++--- .../seedu/address/logic/commands/EditCommandTest.java | 8 ++++---- .../address/logic/commands/EditPropertyCommandTest.java | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java index 10b1306b559..c0c04eb6aca 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCustomerCommandTest.java @@ -17,7 +17,6 @@ import seedu.address.logic.Messages; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.PropertyBook; import seedu.address.model.UserPrefs; import seedu.address.model.customer.Customer; /** @@ -36,7 +35,7 @@ public void execute_validIndexUnfilteredList_success() { String expectedMessage = String.format(DeleteCustomerCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, Messages.format(customerToDelete)); - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.deleteCustomer(customerToDelete); assertCommandSuccess(delcustCommand, model, expectedMessage, expectedModel); @@ -60,7 +59,7 @@ public void execute_validIndexFilteredList_success() { String expectedMessage = String.format(DeleteCustomerCommand.MESSAGE_DELETE_CUSTOMER_SUCCESS, Messages.format(customerToDelete)); - Model expectedModel = new ModelManager(model.getAddressBook(), new PropertyBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getPropertyBook(), new UserPrefs()); expectedModel.deleteCustomer(customerToDelete); showNoCustomer(expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 339637ad2e8..db984943783 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -47,7 +47,7 @@ public void execute_allFieldsSpecifiedUnfilteredList_success() { Messages.format(editedCustomer)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + model.getPropertyBook(), new UserPrefs()); expectedModel.setCustomer(model.getFilteredCustomerList().get(0), editedCustomer); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -70,7 +70,7 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() { Messages.format(editedCustomer)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); expectedModel.setCustomer(lastCustomer, editedCustomer); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -85,7 +85,7 @@ public void execute_noFieldSpecifiedUnfilteredList_success() { Messages.format(editedCustomer)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -103,7 +103,7 @@ public void execute_filteredList_success() { Messages.format(editedCustomer)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); expectedModel.setCustomer(model.getFilteredCustomerList().get(0), editedCustomer); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java b/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java index ddf5ac12628..3597fa3330f 100644 --- a/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditPropertyCommandTest.java @@ -48,7 +48,7 @@ public void execute_allFieldsSpecifiedUnfilteredList_success() { Messages.format(editedProperty)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); expectedModel.setProperty(model.getFilteredPropertyList().get(0), editedProperty); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -71,7 +71,7 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() { Messages.format(editedProperty)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); expectedModel.setProperty(lastProperty, editedProperty); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -86,7 +86,7 @@ public void execute_noFieldSpecifiedUnfilteredList_success() { Messages.format(editedProperty)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -104,7 +104,7 @@ public void execute_filteredList_success() { Messages.format(editedProperty)); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), - new PropertyBook(), new UserPrefs()); + new PropertyBook(model.getPropertyBook()), new UserPrefs()); expectedModel.setProperty(model.getFilteredPropertyList().get(0), editedProperty); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); From 3674d79c701cdd978c013745add33ffaf50916b6 Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 10:53:47 +0800 Subject: [PATCH 23/34] Fix bug where app will not start if no data files are provided --- src/main/java/seedu/address/MainApp.java | 4 +- .../address/model/util/SampleDataUtil.java | 45 ++++++++++--------- .../address/testutil/TypicalProperties.java | 4 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 2ec64cbb047..2ab78ad1adc 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -40,7 +40,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 2, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -88,7 +88,7 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() - + " populated with a sample PropertyMatch."); + + " populated with a sample AddressBook."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataLoadingException e) { diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index fc63f57a3c7..cb5f843bc84 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -43,30 +43,30 @@ public static Customer[] getSampleCustomers() { getTagSet("small")), new Customer(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), new Budget("200000"), - getTagSet("city-center")) + getTagSet("city", "center")) }; } public static Property[] getSampleProperties() { return new Property[] { - new Property(new PropName("Alex Yeoh"), new PropAddress("Blk 30 Geylang Street 29, #06-40"), - new PropPhone("87438807"), new Price("10101010"), - getTagSet("friends")), - new Property(new PropName("Bernice Yu"), new PropAddress("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - new PropPhone("87438808"), new Price("2504830"), - getTagSet("friends")), - new Property(new PropName("Charlotte Oliveiro"), new PropAddress("Blk 11 Ang Mo Kio Street 74, #11-04"), - new PropPhone("87438809"), new Price("36817468"), - getTagSet("friends")), - new Property(new PropName("David Li"), new PropAddress("Blk 436 Serangoon Gardens Street 26, #16-43"), - new PropPhone("87438810"), new Price("81648137"), - getTagSet("friends")), - new Property(new PropName("Irfan Ibrahim"), new PropAddress("Blk 47 Tampines Street 20, #17-35"), - new PropPhone("87438811"), new Price("8276464"), - getTagSet("friends")), - new Property(new PropName("Roy Balakrishnan"), new PropAddress("Blk 45 Aljunied Street 85, #11-31"), - new PropPhone("87438812"), new Price("42846276"), - getTagSet("friends")) + new Property(new PropName("Aquavista"), new PropAddress("123 Orchid Lane, Singapore 456789"), + new PropPhone("94351253"), new Price("123456"), + getTagSet("pink")), + new Property(new PropName("Skyvista"), new PropAddress("456 Sapphire Avenue, Singapore 987654"), + new PropPhone("98765432"), new Price("8880000"), + getTagSet("square", "garden")), + new Property(new PropName("Horizonview"), new PropAddress("789 Palm Grove Road, Singapore 321012"), + new PropPhone("95352563"), new Price("400000"), + getTagSet()), + new Property(new PropName("Luxeloft"), new PropAddress("234 Amber Crescent, Singapore 567890"), + new PropPhone("87652533"), new Price("100000"), + getTagSet("garage")), + new Property(new PropName("Riveria"), new PropAddress("567 Maple Lane, Singapore 109876"), + new PropPhone("9482224"), new Price("4000000"), + getTagSet()), + new Property(new PropName("Azure"), new PropAddress("202 Shoreline Street, Singapore 654321"), + new PropPhone("9482442"), new Price("321950"), + getTagSet()) }; } @@ -79,11 +79,12 @@ public static ReadOnlyAddressBook getSampleAddressBook() { } public static ReadOnlyPropertyBook getSamplePropertyBook() { - PropertyBook sampleAb = new PropertyBook(); + PropertyBook samplePb = new PropertyBook(); for (Property sampleProperty : getSampleProperties()) { - sampleAb.addProperty(sampleProperty); + System.out.println(sampleProperty); + samplePb.addProperty(sampleProperty); } - return sampleAb; + return samplePb; } /** * Returns a tag set containing the list of strings given. diff --git a/src/test/java/seedu/address/testutil/TypicalProperties.java b/src/test/java/seedu/address/testutil/TypicalProperties.java index a975b2e3357..bd626812577 100644 --- a/src/test/java/seedu/address/testutil/TypicalProperties.java +++ b/src/test/java/seedu/address/testutil/TypicalProperties.java @@ -32,9 +32,9 @@ public class TypicalProperties { .withAddress("456 Sapphire Avenue, Singapore 987654").withPhone("98765432") .withTags("square", "garden").build(); public static final Property HORIZONVIEW = new PropertyBuilder().withName("Horizonview").withPhone("95352563") - .withAddress("789 Palm Grove Road, Singapore 321012").withPrice("40000").build(); + .withAddress("789 Palm Grove Road, Singapore 321012").withPrice("400000").build(); public static final Property LUXELOFT = new PropertyBuilder().withName("Luxeloft").withPhone("87652533") - .withAddress("234 Amber Crescent, Singapore 567890").withPrice("10000").withTags("garage").build(); + .withAddress("234 Amber Crescent, Singapore 567890").withPrice("100000").withTags("garage").build(); public static final Property RIVERIA = new PropertyBuilder().withName("Riveria").withPhone("9482224") .withAddress("567 Maple Lane, Singapore 109876").withPrice("4000000").build(); public static final Property AZURE = new PropertyBuilder().withName("Azure").withPhone("9482427") From 548ece3037aadb0551657952e6e7fd85cedfc63e Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 10:56:15 +0800 Subject: [PATCH 24/34] Change build file name to propertymatch --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8bb251c22e..ca67cc09ac0 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'propertymatch.jar' } run { From 0d81d569e7de03a9f216b2d2d371acf81f05f320 Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 16:07:27 +0800 Subject: [PATCH 25/34] Add dg for exit and editcust --- docs/DeveloperGuide.md | 107 ++++++++++++++++-- .../diagrams/EditCustomerSequenceDiagram.puml | 72 ++++++++++++ docs/images/EditCustomerSequenceDiagram.png | Bin 0 -> 66198 bytes 3 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 docs/diagrams/EditCustomerSequenceDiagram.puml create mode 100644 docs/images/EditCustomerSequenceDiagram.png diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 9b7eb29ab7e..e3cee141981 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -154,6 +154,90 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Editing of buyers and properties +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to edit the details of a customer or property after adding it to the application. For example, the property agent may want to change the budget range of a customer after adding it to PropertyMatch. +Or, the property agent may want to change the price of a property after adding it to PropertyMatch. + +#### Implementation +The `EditCustomerCommand` and `EditPropertyCommand` classes extends the `Command` class. They are used to edit the details of a customer or property, respectively. +Both commands allow the user to change any of the fields of a customer or property. The commands expect at least one flag to be edited, otherwise an error message will be displayed. +When the edit command is inputted, the `EditCustomerCommandParser` and `EditPropertyCommandParser` classes are used to parse the user input and create the respective `EditCustomerCommand` and `EditPropertyCommand` objects. +When these created command objects are executed by the `LogicManager`, the `EditCustomerCommand#execute(Model model)` or `EditPropertyCommand#execute(Model model)` methods are called. These methods will edit the customer or property in the model, and return a `CommandResult` object. + +
:exclamation: **Note:** +To be more concise, we will be referring to both customers and properties as entities in this section from here onwards. +
+ +During this execution process, the existing entity is first retrieved from the model. The fields of the entities are then edited according to what flags were passed in by the user during the edit commands. +A new customer or property is then created with the edited fields, and any fields that have not been edited will be copied over from the original entity. The new entity is then added to the model, and the original entity is removed from the model. +The new customer or property is then added into the model, replacing the old one. The new entity will then be displayed to the user, and a success message is displayed. + +The following sequence diagram shows how the `EditCustomerCommand` is executed. +![EditCustomerSequenceDiagram](images/EditCustomerSequenceDiagram.png) + +#### Design Considerations +**Aspect: How the edit commands should relate to each other:** + +* **Alternative 1 (current choice):** `EditCustomerCommand` and `EditPropertyCommand` are separate, and both inherit from the `Command` class. + * Pros: + * Both the `Customer` and `Property` classes have different fields that are exclusive to each other. + * This reduces complexity of the system, and unexpected behaviours. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase. +* **Alternative 2:** A single `EditCommand` class is used to edit both customer and property. + * Cons: + * Unnecessary complexity is introduced into the system. + +**Aspect: How the edited entities should interact with the model:** +* We also decided for the edit commands to create a new entity, instead of editing the existing one. This allows us to not include any setters in the `Customer` and `Property` classes, which make the objects immutable, so there is less likelihood of unexpected changes to the object. This enables us to maintain the defensiveness of our code. + By creating a new entity every time the property agent edits, we can easily add the new customer or property into the model, and remove the old one. This also allows us to easily undo the edit command in the future, by simply adding the old entity back into the model. + + +### Reset the application with `Clear` +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to clear the list of customers or properties to start from a clean slate. + +#### Implementation +The `ClearCommand` extends the `Command` class. It is used to clear the list of customers or properties. + +#### Design Considerations + +* **Alternative 1:** `ClearPropertyCommand` and `ClearCustomerCommand` are separate, and both inherit from the `Command` class. + * Pros: + * Allows the property agent to clear only customers or properties. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase + * More commands for the property agent to remember +* **Alternative 2 (current choice):** A single `ClearCommand` class is used to clear both customers and properties. + * Pros: + * Less overhead to deal with + * Lesser commands for the property agent to remember + * Cons: + * Unable to clear only customers or properties + + +### Exit with a delay +[Back to top](#table-of-contents) + +#### Motivation +The property agent should exit the application with a peace of mind. A delay is required for the property agent to read the exit message before the application closes. + +#### Implementation +The `ExitCommand` extends the `Command` class. It is used to close the application and display the goodbye message. +The following code is used to implement the delay. There are many ways to implement a timeout, but since we are using JavaFX. This is probably the simplest way to accomplish it. +``` +PauseTransition delay = new PauseTransition(Duration.seconds(3)); +delay.setOnFinished(e -> primaryStage.hide()); +delay.play(); +``` + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -426,6 +510,8 @@ Actor: Property Agent ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X +* **Customer**: A customer interested in purchasing or investing in properties. +* **Property**: A property that is listed or accessible in the market. -------------------------------------------------------------------------------------------------------------------- @@ -453,7 +539,16 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +### Listing all properties + +**Prerequisites**: Property list should be filtered to show a subset of the original list. + +1. Test case: `listprop` + Expected: Property list should return to its original state containing all properties. "Listed all properties" message should be displayed + on the screen. + +2. Test case: `listprop 1`, `listprop x` + Expected: Same behaviour as above. ### Deleting a customer @@ -470,12 +565,4 @@ testers are expected to do more *exploratory* testing. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ - -### Saving data - -1. Dealing with missing/corrupted data files - - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ - -1. _{ more test cases …​ }_ +1. _{ more test cases …​ }_ \ No newline at end of file diff --git a/docs/diagrams/EditCustomerSequenceDiagram.puml b/docs/diagrams/EditCustomerSequenceDiagram.puml new file mode 100644 index 00000000000..1b27cee0f07 --- /dev/null +++ b/docs/diagrams/EditCustomerSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditCustomerCommandParser" as EditCustomerCommandParser LOGIC_COLOR +participant "command:EditCustomerCommand" as EditCustomerCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("editcust 1 n/Janet") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edistcust 1 n/Janet") +activate AddressBookParser + +create EditCustomerCommandParser +AddressBookParser -> EditCustomerCommandParser : new EditCustomerCommandParser("1 n/Janet") +activate EditCustomerCommandParser + +EditCustomerCommandParser --> AddressBookParser +deactivate EditCustomerCommandParser + +AddressBookParser -> EditCustomerCommandParser : parse("1 n/Janet") +activate EditCustomerCommandParser + +create EditCustomerCommand +EditCustomerCommandParser -> EditCustomerCommand : new EditCustomerCommand(1, editCustomerDescriptor) +activate EditCustomerCommand + +EditCustomerCommand --> EditCustomerCommandParser : command +deactivate EditCustomerCommand + +EditCustomerCommandParser --> AddressBookParser : command +deactivate EditCustomerCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCustomerCommandParser -[hidden]-> AddressBookParser +destroy EditCustomerCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> EditCustomerCommand : execute() +activate EditCustomerCommand + +EditCustomerCommand -> EditCustomerCommand : createEditedCustomer(customerToEdit, editCustomerDescriptor) +activate EditCustomerCommand +EditCustomerCommand --> EditCustomerCommand : editedCustomer +deactivate EditCustomerCommand + +EditCustomerCommand -> Model : setCustomer(CustomerToEdit, editedCustomer) + +EditCustomerCommand -> Model : updateFilteredCustomerList(PREDICATE_SHOW_ALL_CUSTOMERS) + +create CommandResult +EditCustomerCommand -> CommandResult : new CommandResult("Edited customer: " + editedCustomer) +activate CommandResult + +CommandResult --> EditCustomerCommand +deactivate CommandResult + +EditCustomerCommand --> LogicManager : commandResult +deactivate EditCustomerCommand + +[<--LogicManager : commandResult +deactivate LogicManager +@enduml \ No newline at end of file diff --git a/docs/images/EditCustomerSequenceDiagram.png b/docs/images/EditCustomerSequenceDiagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c037380dd467beaa5e0b3134b7e81cd0fad42d19 GIT binary patch literal 66198 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 UUVXz0;t-47ki4FF_2J|H2MqdstN;K2 literal 0 HcmV?d00001 From b26635abb4058d8db0cdc7e00e32b85e20e4f61a Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 16:09:25 +0800 Subject: [PATCH 26/34] Add eol --- docs/DeveloperGuide.md | 4 +--- docs/diagrams/EditCustomerSequenceDiagram.puml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index e3cee141981..3917c66db92 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -563,6 +563,4 @@ testers are expected to do more *exploratory* testing. Expected: No customer is deleted. Error details shown in the status message. Status bar remains the same. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. - -1. _{ more test cases …​ }_ \ No newline at end of file + Expected: Similar to previous. \ No newline at end of file diff --git a/docs/diagrams/EditCustomerSequenceDiagram.puml b/docs/diagrams/EditCustomerSequenceDiagram.puml index 1b27cee0f07..3d440df10d3 100644 --- a/docs/diagrams/EditCustomerSequenceDiagram.puml +++ b/docs/diagrams/EditCustomerSequenceDiagram.puml @@ -69,4 +69,4 @@ deactivate EditCustomerCommand [<--LogicManager : commandResult deactivate LogicManager -@enduml \ No newline at end of file +@enduml From 3082f1d358906ff1e90345dcfd99812947c09d78 Mon Sep 17 00:00:00 2001 From: jianrong7 Date: Thu, 26 Oct 2023 16:11:06 +0800 Subject: [PATCH 27/34] Add eol --- docs/DeveloperGuide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 3917c66db92..20b3ed5d4b9 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -563,4 +563,4 @@ testers are expected to do more *exploratory* testing. Expected: No customer is deleted. Error details shown in the status message. Status bar remains the same. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. \ No newline at end of file + Expected: Similar to previous. From f42f3285c2bbaa427ffb30a446870e4ad6e950f9 Mon Sep 17 00:00:00 2001 From: FerdiHS Date: Thu, 26 Oct 2023 19:40:59 +0800 Subject: [PATCH 28/34] Adds feature to filter customers based on budget and tags --- .../logic/commands/AddCustomerCommand.java | 2 +- .../logic/commands/FilterCustomerCommand.java | 66 +++++++++++++++++++ .../logic/parser/AddressBookParser.java | 4 ++ .../parser/FilterCustomerCommandParser.java | 52 +++++++++++++++ .../seedu/address/model/customer/Budget.java | 13 +++- .../BudgetAndTagsInRangePredicate.java | 54 +++++++++++++++ 6 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/main/java/seedu/address/logic/commands/FilterCustomerCommand.java create mode 100644 src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java create mode 100644 src/main/java/seedu/address/model/customer/BudgetAndTagsInRangePredicate.java diff --git a/src/main/java/seedu/address/logic/commands/AddCustomerCommand.java b/src/main/java/seedu/address/logic/commands/AddCustomerCommand.java index a213a6934a2..c98634f8b27 100644 --- a/src/main/java/seedu/address/logic/commands/AddCustomerCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCustomerCommand.java @@ -25,7 +25,7 @@ public class AddCustomerCommand extends Command { + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + "[" + PREFIX_BUDGET + "BUDGET] " + + PREFIX_BUDGET + "BUDGET " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " diff --git a/src/main/java/seedu/address/logic/commands/FilterCustomerCommand.java b/src/main/java/seedu/address/logic/commands/FilterCustomerCommand.java new file mode 100644 index 00000000000..09717ebebfd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCustomerCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.customer.BudgetAndTagsInRangePredicate; + +/** + * Filter all customers in the address book to the user based on specific tags and/or budget. + */ +public class FilterCustomerCommand extends Command { + public static final String COMMAND_WORD = "filtercust"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters customers from the address book. " + + "Parameters: " + + "[" + PREFIX_BUDGET + "BUDGET] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_BUDGET + "100000000 " + + PREFIX_TAG + "bright " + + PREFIX_TAG + "sunny"; + + private BudgetAndTagsInRangePredicate predicate; + + /** + * Creates an FilteredCustomerCommand to get all the specified {@code Customer} + */ + public FilterCustomerCommand(BudgetAndTagsInRangePredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateFilteredCustomerList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_CUSTOMERS_LISTED_OVERVIEW, model.getFilteredCustomerList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCustomerCommand)) { + return false; + } + + FilterCustomerCommand otherFilterCustomerCommand = (FilterCustomerCommand) other; + return predicate.equals(otherFilterCustomerCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 507ecaf16f1..e8591b1ca97 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -17,6 +17,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditPropertyCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCustomerCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCustomerCommand; @@ -81,6 +82,9 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FilterCustomerCommand.COMMAND_WORD: + return new FilterCustomerCommandParser().parse(arguments); + case ListCustomerCommand.COMMAND_WORD: return new ListCustomerCommand(); diff --git a/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java new file mode 100644 index 00000000000..8df82528a14 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java @@ -0,0 +1,52 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; + +import seedu.address.logic.commands.FilterCustomerCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.customer.Budget; +import seedu.address.model.customer.BudgetAndTagsInRangePredicate; +import seedu.address.model.tag.Tag; + +/** + * Filters and lists all customers in address book whose budget and/or tags are selected. + */ +public class FilterCustomerCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * and returns a FilterCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCustomerCommand parse(String args) throws ParseException { + requireNonNull(args); + Budget budget = null; + Set tags; + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_BUDGET, PREFIX_TAG); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_BUDGET); + if (argMultimap.getValue(PREFIX_BUDGET).isPresent()) { + budget = ParserUtil.parseBudget(argMultimap.getValue(PREFIX_BUDGET).get()); + } + + tags = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + if (isNull(budget) && tags.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterCustomerCommand.MESSAGE_USAGE)); + } + + return new FilterCustomerCommand(new BudgetAndTagsInRangePredicate(budget, tags)); + } + +} diff --git a/src/main/java/seedu/address/model/customer/Budget.java b/src/main/java/seedu/address/model/customer/Budget.java index 254643c104c..c756d0d5df2 100644 --- a/src/main/java/seedu/address/model/customer/Budget.java +++ b/src/main/java/seedu/address/model/customer/Budget.java @@ -1,5 +1,6 @@ package seedu.address.model.customer; +import static java.util.Objects.isNull; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; @@ -17,7 +18,7 @@ public class Budget { public final String value; /** - * Constructs a {@code Phone}. + * Constructs a {@code Budget}. * * @param budget A valid budget number. */ @@ -35,6 +36,16 @@ public static boolean isValidBudget(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns true if the other budget is greater or equal this budget + * + * @param other the other budget being compared + * @return whether the other budget is greater or equal to this budget + */ + public boolean isInRangeBudget(Budget other) { + return isNull(other) || amount >= other.amount; + } + @Override public String toString() { return "$" + value; diff --git a/src/main/java/seedu/address/model/customer/BudgetAndTagsInRangePredicate.java b/src/main/java/seedu/address/model/customer/BudgetAndTagsInRangePredicate.java new file mode 100644 index 00000000000..3b46455767e --- /dev/null +++ b/src/main/java/seedu/address/model/customer/BudgetAndTagsInRangePredicate.java @@ -0,0 +1,54 @@ +package seedu.address.model.customer; + +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Customer}'s {@code Budget} and/or {@code Tags} are in range of the specified budget and/or tags. + */ +public class BudgetAndTagsInRangePredicate implements Predicate { + private final Budget budget; + private final Set tags; + + /** + * Constructs a {@code BudgetAndTagsInRangePredicate}. + * + * @param budget the specified budget if any + * @param tags the specified tags if any + */ + public BudgetAndTagsInRangePredicate(Budget budget, Set tags) { + this.budget = budget; + this.tags = tags; + } + + @Override + public boolean test(Customer customer) { + return customer.getTags().containsAll(tags) && customer.getBudget().isInRangeBudget(budget); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof BudgetAndTagsInRangePredicate)) { + return false; + } + + BudgetAndTagsInRangePredicate otherBudgetAndTagsInRangePredicate = (BudgetAndTagsInRangePredicate) other; + return budget.equals(otherBudgetAndTagsInRangePredicate.budget) + && tags.equals(otherBudgetAndTagsInRangePredicate.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("budget", budget) + .add("tags", tags).toString(); + } +} From ef0c39b703fce6621600ddbd7f10848128e2c599 Mon Sep 17 00:00:00 2001 From: nicolengk Date: Fri, 27 Oct 2023 00:09:12 +0800 Subject: [PATCH 29/34] Update DG --- docs/DeveloperGuide.md | 4 +- .../diagrams/FindCustomerSequenceDiagram.puml | 65 ++++++++++++++++++ docs/images/FindCustomerSequenceDiagram.png | Bin 0 -> 42448 bytes docs/images/img.png | Bin 0 -> 41382 bytes 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 docs/diagrams/FindCustomerSequenceDiagram.puml create mode 100644 docs/images/FindCustomerSequenceDiagram.png create mode 100644 docs/images/img.png diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index bb55939222c..6fc70c2cc0e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -166,8 +166,6 @@ The `DeleteCustomerCommand` and `DeletePropertyComannd` classes extends from the When the delete command is inputted, the `DeleteCustomerCommandParser` and `DeletePropertyCommandParser` classes are used to parse the user input and create the `DeleteCustomerCommand` and `DeletePropertyCommand` objects respectively. When these created command objects are executed by the `LogicManager`, the `DeleteCustomerCommand#execute(Model model)` or `DeletePropertyCommand#execute(Model model)` methods are called. These methods will delete the customer or property in the model, and return a `CommandResult` object. -The following sequence diagram shows how the `DeleteCustomerCommand` is executed. -**INSERT SEQUENCE DIAGRAM HERE** #### Design Considerations **Aspect: How the delete commands should relate to each other:** @@ -198,7 +196,7 @@ When the find command is inputted, the `FindCustomerCommandParser` and `FindProp When these created command objects are executed by the `LogicManager`, the `FindCustomerCommand#execute(Model model)` or `FindPropertyCommand#execute(Model model)` methods are called. These methods will find the customer or property in the model, and return a `CommandResult` object. The following sequence diagram shows how the `FindCustomerCommand` is executed. -**INSERT SEQUENCE DIAGRAM HERE** +![FindCustomerSequenceDiagram](images/FindCustomerSequenceDiagram.png) #### Design Considerations **Aspect: How the find commands should relate to each other:** diff --git a/docs/diagrams/FindCustomerSequenceDiagram.puml b/docs/diagrams/FindCustomerSequenceDiagram.puml new file mode 100644 index 00000000000..d5f195239bf --- /dev/null +++ b/docs/diagrams/FindCustomerSequenceDiagram.puml @@ -0,0 +1,65 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCustomerCommandParser" as FindCustomerCommandParser LOGIC_COLOR +participant "command:FindCustomerCommand" as FindCustomerCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("findcust Jack") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("findcust Jack") +activate AddressBookParser + +create FindCustomerCommandParser +AddressBookParser -> FindCustomerCommandParser : new FindCustomerCommandParser() +activate FindCustomerCommandParser + +FindCustomerCommandParser --> AddressBookParser +deactivate FindCustomerCommandParser + +AddressBookParser -> FindCustomerCommandParser : parse("Jack") +activate FindCustomerCommandParser + +create FindCustomerCommand +FindCustomerCommandParser -> FindCustomerCommand : new FindCustomerCommand(predicate) +activate FindCustomerCommand + +FindCustomerCommand --> FindCustomerCommandParser : command +deactivate FindCustomerCommand + +FindCustomerCommandParser --> AddressBookParser : command +deactivate FindCustomerCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindCustomerCommandParser -[hidden]-> AddressBookParser +destroy FindCustomerCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> FindCustomerCommand : execute() +activate FindCustomerCommand + +FindCustomerCommand -> Model : updateFilteredCustomerList(predicate) + +create CommandResult +FindCustomerCommand -> CommandResult : new CommandResult(num + "customers listed!") +activate CommandResult + +CommandResult --> FindCustomerCommand +deactivate CommandResult + +FindCustomerCommand --> LogicManager +deactivate FindCustomerCommand + +[<--LogicManager : commandResult +deactivate LogicManager +@enduml diff --git a/docs/images/FindCustomerSequenceDiagram.png b/docs/images/FindCustomerSequenceDiagram.png new file mode 100644 index 0000000000000000000000000000000000000000..96f06d1949504fbb7c9d688290bf988959e3d8ba GIT binary patch literal 42448 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≶z)iTIIISewztvoiNj$Og!c$ET)1&W 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 literal 0 HcmV?d00001 diff --git a/docs/images/img.png b/docs/images/img.png new file mode 100644 index 0000000000000000000000000000000000000000..a26b8902a441aa767d570b7949566cf00c13f4aa GIT binary patch literal 41382 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 Date: Fri, 27 Oct 2023 00:10:13 +0800 Subject: [PATCH 30/34] Update DG --- docs/images/img.png | Bin 41382 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/img.png diff --git a/docs/images/img.png b/docs/images/img.png deleted file mode 100644 index a26b8902a441aa767d570b7949566cf00c13f4aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41382 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 Date: Fri, 27 Oct 2023 02:05:17 +0800 Subject: [PATCH 31/34] Adds unit tests to filter customers --- .../parser/FilterCustomerCommandParser.java | 5 +- .../commands/FilterCustomerCommandTest.java | 271 ++++++++++++++++++ .../FilterCustomerCommandParserTest.java | 45 +++ .../BudgetAndTagsInRangePredicateTest.java | 195 +++++++++++++ .../address/model/customer/BudgetTest.java | 15 + 5 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 src/test/java/seedu/address/logic/commands/FilterCustomerCommandTest.java create mode 100644 src/test/java/seedu/address/logic/parser/FilterCustomerCommandParserTest.java create mode 100644 src/test/java/seedu/address/model/customer/BudgetAndTagsInRangePredicateTest.java diff --git a/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java index 8df82528a14..d79d5ae843a 100644 --- a/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FilterCustomerCommandParser.java @@ -4,9 +4,6 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Set; @@ -34,7 +31,7 @@ public FilterCustomerCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_BUDGET, PREFIX_TAG); - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_BUDGET); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_BUDGET); if (argMultimap.getValue(PREFIX_BUDGET).isPresent()) { budget = ParserUtil.parseBudget(argMultimap.getValue(PREFIX_BUDGET).get()); } diff --git a/src/test/java/seedu/address/logic/commands/FilterCustomerCommandTest.java b/src/test/java/seedu/address/logic/commands/FilterCustomerCommandTest.java new file mode 100644 index 00000000000..ed0ff0ed011 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FilterCustomerCommandTest.java @@ -0,0 +1,271 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_CUSTOMERS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.testutil.TypicalCustomers.ALICE; +import static seedu.address.testutil.TypicalCustomers.BENSON; +import static seedu.address.testutil.TypicalCustomers.getTypicalAddressBook; +import static seedu.address.testutil.TypicalProperties.getTypicalPropertyBook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.parser.FilterCustomerCommandParser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.customer.Budget; +import seedu.address.model.customer.BudgetAndTagsInRangePredicate; +import seedu.address.model.tag.Tag; + + +public class FilterCustomerCommandTest { + private Model model; + private Model expectedModel; + + @BeforeEach + public void setup() { + model = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalPropertyBook(), new UserPrefs()); + } + + @Test + public void equals() { + String smallBudgetString = "100000"; + String bigBudgetString = "1000000000"; + + String firstTagString = "sunny"; + String secondTagString = "bright"; + + String filterFirstCustomerCommandString = FilterCustomerCommand.COMMAND_WORD + " " + + PREFIX_BUDGET + smallBudgetString + " " + PREFIX_TAG + firstTagString; + String filterSecondCustomerCommandString = FilterCustomerCommand.COMMAND_WORD + " " + + PREFIX_BUDGET + bigBudgetString + " " + PREFIX_TAG + secondTagString; + + FilterCustomerCommand filterFirstCustomerCommand = preparePredicate(filterFirstCustomerCommandString); + FilterCustomerCommand filterSecondCustomerCommand = preparePredicate(filterSecondCustomerCommandString); + + // same object -> returns true + assertTrue(filterFirstCustomerCommand.equals(filterFirstCustomerCommand)); + + // same values -> returns true + FilterCustomerCommand filterFirstCustomerCommandCopy = preparePredicate(filterFirstCustomerCommandString); + assertTrue(filterFirstCustomerCommand.equals(filterFirstCustomerCommandCopy)); + + // different types -> returns false + assertFalse(filterFirstCustomerCommand.equals(1)); + + // null -> returns false + assertFalse(filterFirstCustomerCommand.equals(null)); + + // different customer -> returns false + assertFalse(filterFirstCustomerCommand.equals(filterSecondCustomerCommand)); + } + + @Test + public void execute_noBudgetSingleTag_noCustomerFound() { + // no customer found + String notFoundTagString = "notfound"; + Tag notFoundTag = new Tag(notFoundTagString); + + Set notFoundTags = new HashSet<>(); + notFoundTags.add(notFoundTag); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(null, notFoundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_TAG + notFoundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 0); + expectedModel.updateFilteredCustomerList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredCustomerList()); + } + + @Test + public void execute_noBudgetSingleTag_customerFound() { + // customers found + String foundTagString = "pink"; + Tag foundTag = new Tag(foundTagString); + + Set foundTags = new HashSet<>(); + foundTags.add(foundTag); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(null, foundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_TAG + foundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + expectedModel.updateFilteredCustomerList(predicate); + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 1); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE), model.getFilteredCustomerList()); + } + + @Test + public void execute_noBudgetSomeTags_noCustomerFound() { + // no customer found + String notFoundTagString = "notfound"; + String otherNotFoundString = "othernotfound"; + + Tag notFoundTag = new Tag(notFoundTagString); + Tag otherNotFoundTag = new Tag(otherNotFoundString); + + Set notFoundTags = new HashSet<>(); + notFoundTags.add(notFoundTag); + notFoundTags.add(otherNotFoundTag); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(null, notFoundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_TAG + notFoundTagString + " " + + PREFIX_TAG + otherNotFoundString; + FilterCustomerCommand command = preparePredicate(commandString); + + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 0); + expectedModel.updateFilteredCustomerList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredCustomerList()); + } + + @Test + public void execute_noBudgetSomeTags_customerFound() { + // customers found + String foundTagString = "square"; + String otherFoundTagString = "garden"; + + Tag foundTag = new Tag(foundTagString); + Tag otherFoundTag = new Tag(otherFoundTagString); + + Set foundTags = new HashSet<>(); + foundTags.add(foundTag); + foundTags.add(otherFoundTag); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(null, foundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_TAG + foundTagString + " " + + PREFIX_TAG + otherFoundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + expectedModel.updateFilteredCustomerList(predicate); + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 1); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON), model.getFilteredCustomerList()); + } + + @Test + public void execute_withBudgetSingleTag_noCustomerFound() { + // no customer found + String notFoundTagString = "notfound"; + Tag notFoundTag = new Tag(notFoundTagString); + + + Set notFoundTags = new HashSet<>(); + notFoundTags.add(notFoundTag); + + String budgetString = "100000"; + Budget budget = new Budget(budgetString); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(budget, notFoundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_BUDGET + budgetString + " " + + PREFIX_TAG + notFoundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 0); + expectedModel.updateFilteredCustomerList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredCustomerList()); + } + + @Test + public void execute_withBudgetSingleTag_customerFound() { + // customers found + String foundTagString = "pink"; + Tag foundTag = new Tag(foundTagString); + + Set foundTags = new HashSet<>(); + foundTags.add(foundTag); + + String budgetString = "10000"; + Budget budget = new Budget(budgetString); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(budget, foundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_BUDGET + budgetString + " " + + PREFIX_TAG + foundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + expectedModel.updateFilteredCustomerList(predicate); + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 1); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE), model.getFilteredCustomerList()); + } + + @Test + public void execute_withBudgetSomeTags_noCustomerFound() { + // no customer found + String notFoundTagString = "notfound"; + String otherNotFoundString = "othernotfound"; + + Tag notFoundTag = new Tag(notFoundTagString); + Tag otherNotFoundTag = new Tag(otherNotFoundString); + + Set notFoundTags = new HashSet<>(); + notFoundTags.add(notFoundTag); + notFoundTags.add(otherNotFoundTag); + + String budgetString = "10000"; + Budget budget = new Budget(budgetString); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(budget, notFoundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_BUDGET + budgetString + " " + + PREFIX_TAG + notFoundTagString + " " + PREFIX_TAG + otherNotFoundString; + FilterCustomerCommand command = preparePredicate(commandString); + + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 0); + expectedModel.updateFilteredCustomerList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredCustomerList()); + } + + @Test + public void execute_withBudgetSomeTags_customerFound() { + // customers found + String foundTagString = "square"; + String otherFoundTagString = "garden"; + + Tag foundTag = new Tag(foundTagString); + Tag otherFoundTag = new Tag(otherFoundTagString); + + Set foundTags = new HashSet<>(); + foundTags.add(foundTag); + foundTags.add(otherFoundTag); + + String budgetString = "10000"; + Budget budget = new Budget(budgetString); + + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(budget, foundTags); + String commandString = FilterCustomerCommand.COMMAND_WORD + " " + PREFIX_TAG + foundTagString + " " + + PREFIX_TAG + otherFoundTagString; + FilterCustomerCommand command = preparePredicate(commandString); + + expectedModel.updateFilteredCustomerList(predicate); + String expectedMessage = String.format(MESSAGE_CUSTOMERS_LISTED_OVERVIEW, 1); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON), model.getFilteredCustomerList()); + } + + private FilterCustomerCommand preparePredicate(String message) { + try { + return (new FilterCustomerCommandParser()).parse(message); + } catch (ParseException e) { + assert false; + return null; + } + } +} diff --git a/src/test/java/seedu/address/logic/parser/FilterCustomerCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FilterCustomerCommandParserTest.java new file mode 100644 index 00000000000..1227f35c754 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FilterCustomerCommandParserTest.java @@ -0,0 +1,45 @@ +package seedu.address.logic.parser; + + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FilterCustomerCommand; +import seedu.address.model.customer.Budget; +import seedu.address.model.customer.BudgetAndTagsInRangePredicate; +import seedu.address.model.tag.Tag; + +public class FilterCustomerCommandParserTest { + private FilterCustomerCommandParser parser = new FilterCustomerCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCustomerCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFilterCustomerCommand() { + String budgetString = "10000"; + Budget budget = new Budget(budgetString); + + String tagString = "pink"; + Tag tag = new Tag(tagString); + Set tags = new HashSet<>(); + tags.add(tag); + + String userInput = " " + PREFIX_BUDGET + budgetString + " " + PREFIX_TAG + tagString; + + FilterCustomerCommand expectedFilterCustomerCommand = + new FilterCustomerCommand(new BudgetAndTagsInRangePredicate(budget, tags)); + assertParseSuccess(parser, userInput, expectedFilterCustomerCommand); + } +} diff --git a/src/test/java/seedu/address/model/customer/BudgetAndTagsInRangePredicateTest.java b/src/test/java/seedu/address/model/customer/BudgetAndTagsInRangePredicateTest.java new file mode 100644 index 00000000000..f324dcff3c6 --- /dev/null +++ b/src/test/java/seedu/address/model/customer/BudgetAndTagsInRangePredicateTest.java @@ -0,0 +1,195 @@ +package seedu.address.model.customer; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.Tag; +import seedu.address.testutil.CustomerBuilder; + +public class BudgetAndTagsInRangePredicateTest { + @Test + public void equals() { + Budget firstBudget = new Budget("1000000"); + Budget secondBudget = new Budget("2000000"); + + Tag firstTag = new Tag("sunny"); + Tag secondTag = new Tag("bright"); + + Set firstTags = new HashSet<>(); + Set secondTags = new HashSet<>(); + + firstTags.add(firstTag); + secondTags.add(secondTag); + + BudgetAndTagsInRangePredicate firstPredicate = new BudgetAndTagsInRangePredicate(firstBudget, firstTags); + BudgetAndTagsInRangePredicate secondPredicate = new BudgetAndTagsInRangePredicate(secondBudget, firstTags); + BudgetAndTagsInRangePredicate thirdPredicate = new BudgetAndTagsInRangePredicate(firstBudget, secondTags); + BudgetAndTagsInRangePredicate fourthPredicate = new BudgetAndTagsInRangePredicate(secondBudget, secondTags); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + BudgetAndTagsInRangePredicate firstPredicateCopy = new BudgetAndTagsInRangePredicate(firstBudget, firstTags); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different budget -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + + // different tags -> return false + assertFalse(firstPredicate.equals(thirdPredicate)); + + // different tags and budget -> return false + assertFalse(firstPredicate.equals(fourthPredicate)); + } + + @Test + public void test_budgetAndTagsInRangeReturnTrue() { + String smallBudgetString = "100000"; + String bigBudgetString = "1000000000"; + + Budget smallBudget = new Budget(smallBudgetString); + + String firstTagString = "sunny"; + String secondTagString = "bright"; + + Tag firstTag = new Tag(firstTagString); + Tag secondTag = new Tag(secondTagString); + + Set emptyTags = new HashSet<>(); + Set singleTags = new HashSet<>(); + Set someTags = new HashSet<>(); + + singleTags.add(firstTag); + someTags.add(firstTag); + someTags.add(secondTag); + + // Same budget with empty tag + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(smallBudget, emptyTags); + assertTrue(predicate.test(new CustomerBuilder().withBudget(smallBudgetString).build())); + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString).build())); + + // Lower budget with empty tag + assertTrue(predicate.test(new CustomerBuilder().withBudget(bigBudgetString).build())); + assertTrue(predicate.test(new CustomerBuilder().withBudget(bigBudgetString).withTags(firstTagString).build())); + + + // No budget with single tag + predicate = new BudgetAndTagsInRangePredicate(null, singleTags); + assertTrue(predicate.test(new CustomerBuilder().withTags(firstTagString).build())); + assertTrue(predicate.test(new CustomerBuilder().withTags(firstTagString, secondTagString).build())); + + // Same budget with single tag + predicate = new BudgetAndTagsInRangePredicate(smallBudget, singleTags); + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString).build())); + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString, secondTagString).build())); + + // Lower budget with single tag + assertTrue(predicate.test(new CustomerBuilder().withBudget(bigBudgetString).withTags(firstTagString).build())); + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(firstTagString, secondTagString).build())); + + // No budget with multiple tags + predicate = new BudgetAndTagsInRangePredicate(null, someTags); + assertTrue(predicate.test(new CustomerBuilder().withTags(firstTagString, secondTagString).build())); + + // Same budget with multiple tags + predicate = new BudgetAndTagsInRangePredicate(smallBudget, someTags); + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString, secondTagString).build())); + + // Lower budget with multiple tags + assertTrue(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(firstTagString, secondTagString).build())); + } + + @Test + public void test_budgetAndTagsInRangeReturnFalse() { + String smallBudgetString = "100000"; + String bigBudgetString = "1000000000"; + + Budget bigBudget = new Budget(bigBudgetString); + + String firstTagString = "sunny"; + String secondTagString = "bright"; + String thirdTagString = "square"; + + Tag firstTag = new Tag(firstTagString); + Tag secondTag = new Tag(secondTagString); + + Set emptyTags = new HashSet<>(); + Set singleTags = new HashSet<>(); + Set someTags = new HashSet<>(); + + singleTags.add(firstTag); + someTags.add(firstTag); + someTags.add(secondTag); + + // Bigger budget with empty tag + BudgetAndTagsInRangePredicate predicate = new BudgetAndTagsInRangePredicate(bigBudget, emptyTags); + assertFalse(predicate.test(new CustomerBuilder().withBudget(smallBudgetString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString).build())); + + // No budget with single tag + predicate = new BudgetAndTagsInRangePredicate(null, singleTags); + assertFalse(predicate.test(new CustomerBuilder().withTags(secondTagString).build())); + + // Same budget with single tag + predicate = new BudgetAndTagsInRangePredicate(bigBudget, singleTags); + assertFalse(predicate.test(new CustomerBuilder().withBudget(bigBudgetString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(secondTagString).build())); + + // Bigger budget with single tag + assertFalse(predicate.test(new CustomerBuilder().withBudget(smallBudgetString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(secondTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString, secondTagString).build())); + + // No budget with multiple tags + predicate = new BudgetAndTagsInRangePredicate(null, someTags); + assertFalse(predicate.test(new CustomerBuilder().build())); + assertFalse(predicate.test(new CustomerBuilder().withTags(firstTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withTags(firstTagString, thirdTagString).build())); + + // Same budget with multiple tags + predicate = new BudgetAndTagsInRangePredicate(bigBudget, someTags); + assertFalse(predicate.test(new CustomerBuilder().withBudget(bigBudgetString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(firstTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(thirdTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(bigBudgetString).withTags(firstTagString, thirdTagString).build())); + + // Bigger budget with multiple tags + predicate = new BudgetAndTagsInRangePredicate(bigBudget, someTags); + assertFalse(predicate.test(new CustomerBuilder().withBudget(smallBudgetString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString, secondTagString).build())); + assertFalse(predicate.test(new CustomerBuilder() + .withBudget(smallBudgetString).withTags(firstTagString, thirdTagString).build())); + + } +} diff --git a/src/test/java/seedu/address/model/customer/BudgetTest.java b/src/test/java/seedu/address/model/customer/BudgetTest.java index 38ab9bd1948..45e892e1e2b 100644 --- a/src/test/java/seedu/address/model/customer/BudgetTest.java +++ b/src/test/java/seedu/address/model/customer/BudgetTest.java @@ -47,6 +47,21 @@ public void isValidBudget() { assertTrue(Budget.isValidBudget(LONG_VALID_BUDGET)); // long budget } + @Test + public void isInRangeBudget() { + // null budget + assertTrue(new Budget(VALID_BUDGET).isInRangeBudget(null)); + + // smaller budget + assertTrue(new Budget(LONG_VALID_BUDGET).isInRangeBudget(new Budget(VALID_BUDGET))); + + // same budget + assertTrue(new Budget(VALID_BUDGET).isInRangeBudget(new Budget(VALID_BUDGET))); + + // bigger budget + assertFalse(new Budget(VALID_BUDGET).isInRangeBudget(new Budget(LONG_VALID_BUDGET))); + } + @Test public void equals() { Budget budget = new Budget(VALID_BUDGET); From 102ffdfd561c0a5cf82f527172710c1ff8e200c5 Mon Sep 17 00:00:00 2001 From: FerdiHS Date: Fri, 27 Oct 2023 04:40:28 +0800 Subject: [PATCH 32/34] Add filtercust and filterprop to the UG and DG --- docs/DeveloperGuide.md | 73 ++++++++++++++ docs/UserGuide.md | 47 ++++++++- .../FilterCustomerSequenceDiagram.puml | 89 ++++++++++++++++++ docs/images/FilterCustomerSequenceDiagram.png | Bin 0 -> 73670 bytes 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 docs/diagrams/FilterCustomerSequenceDiagram.puml create mode 100644 docs/images/FilterCustomerSequenceDiagram.png diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 9b7eb29ab7e..dd4ad998bb1 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -154,6 +154,79 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Filtering of customers +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to see a list of customers based on their budget. For example, the property agent may want to filter customers with budget more than $100000. +Or, the property agent may want to see a list of customers based on the characteristics of the property they desired. For example, the property agent may want to filter customers who love pink properties. +Or, the property agent may want to see a list of customers based on both budget and characteristics to enhance productivity. + +#### Implementation +The `FilterCustomerCommand` class extends the `Command` class. They are used to filter customers. +The command allows the user to filter customers based on their budget and/or properties' characteristics they love. The commands expect at least one flag, either budget or characteristics, to be used as a filter. +When the filter command is inputted, the `FilterCustomerCommandParser` class is used to parse the user input and create the respective `FilterCustomerCommand` objects. +When these created command objects are executed by the `LogicManager`, the `FilterCustomerCommand#execute(Model model)` methods are called. These methods will update the filtered customer list in the `model` which will eventually update the customers shown in the UI, and return a `CommandResult` object. + +During this execution process, a new `BudgetAndTagsInRangePredicate` object which is used as a predicate to check whether a customer's budget is higher and if all the characteristics are desired by the customer. +All customers will be tested using this `BudgetAndTagsInRangePredicate`. Customers which satisfy this condition will be included into the `FilteredCustomerList` in the model. + +The following sequence diagram shows how the `FilterCustomerCommand` is executed. +![FilterCustomerSequenceDiagram](images/FilterCustomerSequenceDiagram.png) + +#### Design Considerations +**Aspect: How the filter customer commands should relate to filter property commands:** + +* **Alternative 1 (current choice):** `FilterCustomerCommand` inherit from the `Command` class and separated with the command used to filter properties (`FilterPropertyCommand`). + * Pros: + * Both the `Customer` and `Property` classes have different fields that are exclusive to each other. + * This reduces complexity of the system, and unexpected behaviours. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase. +* **Alternative 2:** A single `FilterCommand` class is used to edit both customer and property. + * Cons: + * Unnecessary complexity is introduced into the system. + +**Aspect: How the filtered customers should interact with the model:** +* We also decided for the filter commands to put the filtered customers in a different list (`FilteredCustomerList`), instead of removing the 'unused' customers from the model. + +### Filtering of properties +[Back to top](#table-of-contents) + +#### Motivation +The property agent may want to see a list of properties based on their budget. For example, the property agent may want to filter properties with price less than $1000000. +Or, the property agent may want to see a list of properties based on the characteristics. For example, the property agent may want to filter pink properties. +Or, the property agent may want to see a list of properties based on both price and characteristics to enhance productivity. + +#### Implementation +The `FilterPropertyCommand` class extends the `Command` class. They are used to filter properties. +The command allows the user to filter properties based on their price and/or characteristics. The commands expect at least one flag, either price or characteristics, to be used as a filter. +When the filter command is inputted, the `FilterPropertyCommandParser` class is used to parse the user input and create the respective `FilterPropertyCommand` objects. +When these created command objects are executed by the `LogicManager`, the `FilterPropertyCommand#execute(Model model)` methods are called. These methods will update the filtered property list in the `model` which will eventually update the properties shown in the UI, and return a `CommandResult` object. + +During this execution process, a new `PriceAndTagsInRangePredicate` object which is used as a predicate to check whether a property's price is lower and if the property has all the characteristics. +All properties will be tested using this `PriceAndTagsInRangePredicate`. Properties which satisfy this condition will be included into the `FilteredPropertyList` in the model. + +#### Design Considerations +**Aspect: How the filter property commands should relate to filter property commands:** + +* **Alternative 1 (current choice):** `FilterPropertyCommand` inherit from the `Command` class and separated with the command used to filter customers (`FilterCustomerCommand`). + * Pros: + * Both the `Customer` and `Property` classes have different fields that are exclusive to each other. + * This reduces complexity of the system, and unexpected behaviours. + * The inheritance of the `Command` class allows us to keep to the Command design pattern, to easily add more types of edit commands in the future, without having to change the existing code. + * Cons: + * More boilerplate code for each of the classes, which increases the size of the codebase. +* **Alternative 2:** A single `FilterCommand` class is used to edit both customer and property. + * Cons: + * Unnecessary complexity is introduced into the system. + +**Aspect: How the filtered properties should interact with the model:** +* We also decided for the filter commands to put the filtered properties in a different list (`FilteredPropertyList`), instead of removing the 'unused' properties from the model. + + + ### \[Proposed\] Undo/redo feature #### Proposed Implementation diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 15c7920322d..3209852d390 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -116,7 +116,7 @@ When command fails: * `Missing name parameter for add customers command` for missing name parameter * `Missing phone parameter for add customers command` for missing phone parameter * `Missing email parameter for add customers command` for missing email parameter -* `Invalid Command` for mispelling of command +* `Invalid Command` for misspelling of command ### Listing all customers : `listcust` @@ -183,6 +183,51 @@ When command fails: * `No such property index` for wrong parameter or index beyond list size * `Invalid command` for misspelling of command +### Filter customers : `filtercust` + +Format: `filtercust [b/BUDGET] [c/CHARACTERISTIC]…​` + +Parameter: +* `b/BUDGET` (optional) : The budget of the customer (Integer) +* `c/CHARACTERISTIC` (optional) : The characteristics of the property the customer is looking for (String) + +Notes: +* Even though both `BUDGET` and `CHARACTERISTIC` are optional, at least one of them should exist. + +Examples: +* `filtercust b/100000` +* `filtercust b/250000 c/white` +* `filtercust c/white` + +When command succeeds: +* `4 customers listed!` when there are 4 customers fulfilling the filter. + +When command fails: +* `Invalid command format!` for missing both `BUDGET` and `CHARACTERISTIC` parameters. +* `Unknown command` for misspelling of command. + +### Filter properties : `filterprop` + +Format: `filtercust [pr/PRICE] [c/CHARACTERISTIC]…​` + +Parameter: +* `pr/PRICE` (optional) : The price of the property (Integer) +* `c/CHARACTERISTIC` (optional) : The characteristics of the property (String) + +Notes: +* Even though both `PROPERTY` and `CHARACTERISTIC` are optional, at least one of them should exist. + +Examples: +* `filterprop pr/100000` +* `filterprop pr/250000 c/white` +* `filterprop c/white` + +When command succeeds: +* `4 properties listed!` when there are 4 properties fulfilling the filter. + +When command fails: +* `Invalid command format!` for missing both `PRICE` and `CHARACTERISTIC` parameters. +* `Unknown command` for misspelling of command. ### Exiting the program : `exit` diff --git a/docs/diagrams/FilterCustomerSequenceDiagram.puml b/docs/diagrams/FilterCustomerSequenceDiagram.puml new file mode 100644 index 00000000000..108c659ef8c --- /dev/null +++ b/docs/diagrams/FilterCustomerSequenceDiagram.puml @@ -0,0 +1,89 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FilterCustomerCommandParser" as FilterCustomerCommandParser LOGIC_COLOR +participant ":Budget" as Budget LOGIC_COLOR +participant ":Tag" as Tag LOGIC_COLOR +participant ":BudgetAndTagsInRangePredicate" as BudgetAndTagsInRangePredicate LOGIC_COLOR +participant "command:FilterCustomerCommand" as FilterCustomerCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("filtercust b/100000 c/pink") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("filtercust b/100000 c/pink") +activate AddressBookParser + +create FilterCustomerCommandParser +AddressBookParser -> FilterCustomerCommandParser : new FilterCustomerCommandParser("b/100000 c/pink") +activate FilterCustomerCommandParser + +FilterCustomerCommandParser --> AddressBookParser +deactivate FilterCustomerCommandParser + +AddressBookParser -> FilterCustomerCommandParser : parse("b/100000 c/pink") +activate FilterCustomerCommandParser + +create Budget +FilterCustomerCommandParser -> Budget: new Budget(100000) +activate Budget + +Budget --> FilterCustomerCommandParser: budget +deactivate Budget + +create Tag +FilterCustomerCommandParser -> Tag: new Tag("pink") +activate Tag + +Tag --> FilterCustomerCommandParser: tag +deactivate Tag + +create BudgetAndTagsInRangePredicate +FilterCustomerCommandParser -> BudgetAndTagsInRangePredicate: new BudgetAndTagsInRangePredicate(budget, tags) +activate BudgetAndTagsInRangePredicate + +BudgetAndTagsInRangePredicate --> FilterCustomerCommandParser: budgetAndTagsInRangePredicate +deactivate BudgetAndTagsInRangePredicate + +create FilterCustomerCommand +FilterCustomerCommandParser -> FilterCustomerCommand : new FilterCustomerCommand(budgetAndTagsInRangePredicate) +activate FilterCustomerCommand + +FilterCustomerCommand --> FilterCustomerCommandParser : command +deactivate FilterCustomerCommand + +FilterCustomerCommandParser --> AddressBookParser : command +deactivate FilterCustomerCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FilterCustomerCommandParser -[hidden]-> AddressBookParser +destroy FilterCustomerCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> FilterCustomerCommand : execute() +activate FilterCustomerCommand + +FilterCustomerCommand -> Model : updateFilteredCustomerList(budgetAndTagsInRangePredicate) + +create CommandResult +FilterCustomerCommand -> CommandResult : new CommandResult(numberOfFilteredCustomer + " customers listed!") +activate CommandResult + +CommandResult --> FilterCustomerCommand +deactivate CommandResult + +FilterCustomerCommand --> LogicManager : commandResult +deactivate FilterCustomerCommand + +[<--LogicManager : commandResult +deactivate LogicManager +@enduml \ No newline at end of file diff --git a/docs/images/FilterCustomerSequenceDiagram.png b/docs/images/FilterCustomerSequenceDiagram.png new file mode 100644 index 0000000000000000000000000000000000000000..cb3c28dce91020ac29f194f5fd48f0fb97f7159a GIT binary patch literal 73670 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 T`}h?w-7$E&`njxgN@xNA?^hXs literal 0 HcmV?d00001 From c22ae9434e1dfdcf93dc1401fc0f9e335734814e Mon Sep 17 00:00:00 2001 From: FerdiHS Date: Fri, 27 Oct 2023 04:42:47 +0800 Subject: [PATCH 33/34] Add a new line to the end of FilterCustomerSequenceDiagram.puml --- docs/diagrams/FilterCustomerSequenceDiagram.puml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/diagrams/FilterCustomerSequenceDiagram.puml b/docs/diagrams/FilterCustomerSequenceDiagram.puml index 108c659ef8c..b0dd2df0c8e 100644 --- a/docs/diagrams/FilterCustomerSequenceDiagram.puml +++ b/docs/diagrams/FilterCustomerSequenceDiagram.puml @@ -86,4 +86,4 @@ deactivate FilterCustomerCommand [<--LogicManager : commandResult deactivate LogicManager -@enduml \ No newline at end of file +@enduml From 3e765614ea2964f24514f5bbed7cec2fb97aa7aa Mon Sep 17 00:00:00 2001 From: FerdiHS Date: Fri, 27 Oct 2023 10:48:00 +0800 Subject: [PATCH 34/34] Add a logger --- src/main/java/seedu/address/MainApp.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 2ab78ad1adc..b90306fb0c4 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -87,6 +87,7 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { + logger.info("Data file " + storage.getAddressBookFilePath() + " not found."); logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); }