diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java new file mode 100644 index 0000000..8d19749 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java @@ -0,0 +1,18 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.exceptions; + +/** + * Exception primarily thrown when the active + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Player} object + * does not have enough money for a transaction to complete. + * */ +public class NotEnoughMoneyException extends RuntimeException { + + /** + * Constructor. + * + * @param message the exception message. + * */ + public NotEnoughMoneyException(final String message) { + super(message); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java index 9cac71e..61809ce 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java @@ -2,10 +2,11 @@ import edu.ntnu.idi.idatt2003.g40.mappe.controller.PlayerStatusController; import edu.ntnu.idi.idatt2003.g40.mappe.engine.TransactionArchive; +import edu.ntnu.idi.idatt2003.g40.mappe.exceptions.NotEnoughMoneyException; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.math.BigDecimal; -import javafx.beans.property.FloatProperty; -import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.ReadOnlyFloatProperty; +import javafx.beans.property.ReadOnlyFloatWrapper; /** * Represents a player in the system. @@ -37,14 +38,18 @@ public final class Player { private BigDecimal money; /** - * Current net-worth of player as a listenable {@link FloatProperty} object. + * Current net-worth of player as a listenable, + * read-only, {@link ReadOnlyFloatWrapper} object. * */ - private final FloatProperty networthAsFloatProp = new SimpleFloatProperty(0); + private final ReadOnlyFloatWrapper networthAsFloatProp = + new ReadOnlyFloatWrapper(0f); /** - * Current money of player as a listenable {@link FloatProperty} object. + * Current money of player as a read-only + * {@link ReadOnlyFloatWrapper} object. * */ - private final FloatProperty moneyAsFloatProp = new SimpleFloatProperty(0); + private final ReadOnlyFloatWrapper moneyAsFloatProp + = new ReadOnlyFloatWrapper(0f); /** * The players' portfolio, holding their shares. @@ -63,19 +68,28 @@ public final class Player { * @param name the name of the player * @param startingMoney the starting amount of money * - * @throws IllegalArgumentException if name is null. + * @throws IllegalArgumentException if name is empty, + * or starting money is null, + * zero or negative. */ - public Player(final String name, final BigDecimal startingMoney) throws IllegalArgumentException { + public Player(final String name, + final BigDecimal startingMoney) + throws IllegalArgumentException { if (!Validator.NOT_EMPTY.isValid(name)) { - throw new IllegalArgumentException("Invalid name!"); + throw new IllegalArgumentException("Player name cannot be empty!"); + } + if (startingMoney == null + || startingMoney.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Starting money cannot be null, zero, or negative!" + ); } this.name = name; this.startingMoney = startingMoney; this.money = this.startingMoney; - this.networthAsFloatProp.setValue(this.startingMoney); - this.moneyAsFloatProp.setValue(this.startingMoney); this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); + updateObservableProperties(); } /** @@ -109,18 +123,42 @@ public BigDecimal getMoney() { * Adds money to the players balance. * * @param amount the amount to add + * + * @throws IllegalArgumentException if money to add is negative or zero. */ - public void addMoney(final BigDecimal amount) { + public void addMoney(final BigDecimal amount) + throws IllegalArgumentException { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Can only add positive values to player!" + ); + } money = money.add(amount); + updateObservableProperties(); } /** * Withdraws money from the players balance. * * @param amount the amount to withdraw + * + * @throws IllegalArgumentException if money to withdraw is negative or zero, + * or if amount is more than current money. */ - public void withdrawMoney(final BigDecimal amount) { + public void withdrawMoney(final BigDecimal amount) + throws IllegalArgumentException { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Amount to withdraw must be positive!" + ); + } + if (money.compareTo(amount) < 0) { + throw new IllegalArgumentException( + "Cannot withdraw more money than available balance!" + ); + } money = money.subtract(amount); + updateObservableProperties(); } /** @@ -152,21 +190,23 @@ public BigDecimal getNetWorth() { } /** - * Get net-worth as a {@link FloatProperty} object, allowing listening for changes. + * Get net-worth as a {@link ReadOnlyFloatProperty} object, + * allowing listening for changes. * - * @return FloatProperty. + * @return networth as an immutable value. * */ - public FloatProperty getNetWorthAsFloatProperty() { - return networthAsFloatProp; + public ReadOnlyFloatProperty getNetWorthAsFloatProperty() { + return networthAsFloatProp.getReadOnlyProperty(); } /** - * Get money as a {@link FloatProperty} object, allowing listening for changes. + * Get money as a {@link ReadOnlyFloatProperty} object, + * allowing listening for changes. * - * @return FloatProperty. + * @return money as an immutable value. * */ - public FloatProperty getMoneyAsFloatProperty() { - return moneyAsFloatProp; + public ReadOnlyFloatProperty getMoneyAsFloatProperty() { + return moneyAsFloatProp.getReadOnlyProperty(); } /** @@ -184,22 +224,46 @@ public PlayerStatus getStatus() { * Method for handling a transaction for the player. * * @param transaction the transaction to handle. + * + * @throws IllegalArgumentException if transaction is null. + * @throws NotEnoughMoneyException if player does not have enough + * money for the transaction. * */ - public void handleTransaction(final Transaction transaction) { + public void handleTransaction(final Transaction transaction) + throws IllegalArgumentException, NotEnoughMoneyException { + if (transaction == null) { + throw new IllegalArgumentException("Cannot handle null transaction!"); + } + if (transaction instanceof Purchase purchase) { - if (money.floatValue() > transaction.getCalculator().calculateTotal().floatValue()) { + BigDecimal totalCost = purchase.getCalculator().calculateTotal(); + if (this.money.compareTo(totalCost) < 0) { + throw new NotEnoughMoneyException("Not enough money for transaction!"); + } + } + + switch (transaction) { + case Purchase purchase -> { withdrawMoney(purchase.getCalculator().calculateTotal()); portfolio.addShare(purchase.getShare()); - transactionArchive.add(transaction); - transaction.commit(this); } - } else if (transaction instanceof Sale sale) { - addMoney(sale.getCalculator().calculateTotal()); - portfolio.removeShare(sale.getShare()); - transactionArchive.add(transaction); - transaction.commit(this); + case Sale sale -> { + addMoney(sale.getCalculator().calculateTotal()); + portfolio.removeShare(sale.getShare()); + } + default -> throw new IllegalStateException("Unexpected value: " + transaction); } - networthAsFloatProp.setValue(getNetWorth().floatValue()); - moneyAsFloatProp.setValue(money); + transactionArchive.add(transaction); + transaction.commit(this); + + updateObservableProperties(); + } + + /** + * Helper method to synchronize the listener values. + */ + private void updateObservableProperties() { + this.moneyAsFloatProp.setValue(this.money.floatValue()); + this.networthAsFloatProp.setValue(this.getNetWorth().floatValue()); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index ff00e77..d790f97 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -14,7 +14,7 @@ * expected to be loaded with the default bundled stock data file. *

*/ -public class SaveGame { +public final class SaveGame { /** Display name of the save. */ private final String name; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java index 733b9c5..490a93c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java @@ -30,19 +30,17 @@ private TransactionFactory() { public static Transaction createTransaction(final TransactionType transactionType, final Share share, - final int week, - final TransactionCalculator - calculator) + final int week) throws IllegalArgumentException { if (transactionType == null || share == null - || !Validator.VALID_POSITIVE_INT.isValid(Integer.toString(week)) - || calculator == null) { + || !Validator.VALID_WEEK.isValid(Integer.toString(week)) + ) { throw new IllegalArgumentException("Null or empty parameters for factory!"); } else { return switch (transactionType) { - case SALE -> new Sale(share, week, calculator); - case PURCHASE -> new Purchase(share, week, calculator); + case SALE -> new Sale(share, week, new SaleCalculator(share)); + case PURCHASE -> new Purchase(share, week, new PurchaseCalculator(share)); default -> throw new IllegalArgumentException("Invalid transaction type!"); }; diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java index 1765d46..d5952dc 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java @@ -31,7 +31,7 @@ void getStatusAfterDoubleIncreaseReturnsBetterStatus() { @Test void gettingStatusWhenNegativeDifferenceReturnsWorstStatus() { - testPlayer.addMoney(new BigDecimal(-1000)); + testPlayer.withdrawMoney(new BigDecimal(1000)); assertEquals(PlayerStatus.NOOB, testPlayer.getStatus()); } @@ -40,10 +40,10 @@ void multipleChangesInValueCauseCorrectStatus() { testPlayer.addMoney(new BigDecimal(2000)); assertEquals(PlayerStatus.PRO, testPlayer.getStatus()); - testPlayer.addMoney(new BigDecimal(-1000)); + testPlayer.withdrawMoney(new BigDecimal(1000)); assertEquals(PlayerStatus.TRYHARD, testPlayer.getStatus()); - testPlayer.addMoney(new BigDecimal(-500)); + testPlayer.withdrawMoney(new BigDecimal(500)); assertEquals(PlayerStatus.GOOD, testPlayer.getStatus()); } -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java index 70dd13b..9ac2d64 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java @@ -1,65 +1,197 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import edu.ntnu.idi.idatt2003.g40.mappe.exceptions.NotEnoughMoneyException; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import java.math.BigDecimal; - -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class PlayerTest { + /** + * Test player to use. + * */ + private Player testPlayer; + + @BeforeEach + void setUp() { + testPlayer = new Player("Alice", new BigDecimal("1000")); + } + @Test void constructorSetsNameMoneyPortfolioAndArchive() { - Player player = new Player("Alice", new BigDecimal("1000")); + assertEquals("Alice", testPlayer.getName()); + assertEquals(new BigDecimal("1000"), testPlayer.getMoney()); + assertNotNull(testPlayer.getPortfolio()); + assertNotNull(testPlayer.getTransactionArchive()); + assertEquals(1000f, + testPlayer.getMoneyAsFloatProperty().floatValue()); + assertEquals(1000f, + testPlayer.getNetWorthAsFloatProperty().floatValue()); + } - assertEquals("Alice", player.getName()); - assertEquals(new BigDecimal("1000"), player.getMoney()); - assertNotNull(player.getPortfolio()); - assertNotNull(player.getTransactionArchive()); + @Test + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Player("Bob", new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("", new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player(null, new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", null) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", new BigDecimal("0")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", new BigDecimal("-100")) + ); } @Test void addMoneyIncreasesBalance() { - Player player = new Player("Bob", new BigDecimal("500")); - - player.addMoney(new BigDecimal("200")); + testPlayer.addMoney(new BigDecimal("200")); + assertEquals(new BigDecimal("1200"), testPlayer.getMoney()); + } - assertEquals(new BigDecimal("700"), player.getMoney()); + @Test + void addMoneyThrowsExceptionWhenIllegalArguments() { + assertDoesNotThrow( + () -> testPlayer.addMoney(new BigDecimal("200")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(null) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(new BigDecimal("-10")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(new BigDecimal("0")) + ); } @Test void withdrawMoneyDecreasesBalance() { - Player player = new Player("Charlie", new BigDecimal("500")); - - player.withdrawMoney(new BigDecimal("150")); + testPlayer.withdrawMoney(new BigDecimal("150")); + assertEquals(new BigDecimal("850"), testPlayer.getMoney()); + } - assertEquals(new BigDecimal("350"), player.getMoney()); + @Test + void withdrawMoneyThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> testPlayer.withdrawMoney(new BigDecimal("200")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(null) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("-10")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("0")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("99999")) + ); } @Test void addAndWithdrawMoneyUpdateBalanceCorrectly() { - Player player = new Player("Dana", new BigDecimal("1000")); + testPlayer.addMoney(new BigDecimal("250")); + testPlayer.withdrawMoney(new BigDecimal("300")); - player.addMoney(new BigDecimal("250")); - player.withdrawMoney(new BigDecimal("300")); - - assertEquals(new BigDecimal("950"), player.getMoney()); + assertEquals(new BigDecimal("950"), testPlayer.getMoney()); } @Test - void getNetWorthCalculatesCorrectly() { + void getNetWorthCalculatesCorrectlyForSales() { Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); - Player player = new Player("Bob", new BigDecimal("900")); Share share = new Share(stock, new BigDecimal("1"), new BigDecimal("100.00")); - player.getPortfolio().addShare(share); - SaleCalculator saleCalculator = new SaleCalculator(share); + BigDecimal expectedNetWorth = testPlayer.getNetWorth(); + Transaction transaction = TransactionFactory.createTransaction(TransactionType.SALE, share, 1); + + testPlayer.handleTransaction(transaction); + + BigDecimal actualNetWorth = testPlayer.getNetWorth(); + expectedNetWorth = expectedNetWorth.add(transaction.getCalculator().calculateTotal()); + + assertEquals(expectedNetWorth, actualNetWorth); + assertEquals(actualNetWorth.floatValue(), + testPlayer.getNetWorthAsFloatProperty().floatValue()); + assertEquals(testPlayer.getMoney().floatValue(), + testPlayer.getMoneyAsFloatProperty().floatValue()); + } + + @Test + void getNetWorthCalculatesCorrectlyForPurchases() { + Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); + Share share = new Share(stock, new BigDecimal("1"), stock.getSalesPrice()); + + BigDecimal expectedNetWorth = testPlayer.getNetWorth(); + Transaction transaction = TransactionFactory.createTransaction(TransactionType.PURCHASE, share, 1); + + testPlayer.handleTransaction(transaction); + + BigDecimal actualNetWorth = testPlayer.getNetWorth(); + expectedNetWorth = expectedNetWorth.subtract( + transaction.getCalculator().calculateCommission() + ); + + assertEquals(expectedNetWorth, actualNetWorth); + assertEquals(actualNetWorth.floatValue(), + testPlayer.getNetWorthAsFloatProperty().floatValue()); + assertEquals(testPlayer.getMoney().floatValue(), + testPlayer.getMoneyAsFloatProperty().floatValue()); + } + + @Test + void handleTransactionThrowsExceptionsOnIllegalArgumentsAndNotEnoughMoney() { + Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); + Share share = new Share(stock, new BigDecimal("1"), stock.getSalesPrice()); + + Share expensiveShare = new Share(stock, new BigDecimal("999.99"), new BigDecimal("999.99")); + + Transaction validTransaction = TransactionFactory.createTransaction( + TransactionType.PURCHASE, share, 1 + ); + + Transaction tooExpensiveTransaction = TransactionFactory.createTransaction( + TransactionType.PURCHASE, expensiveShare, 1 + ); + + assertDoesNotThrow( + () -> testPlayer.handleTransaction(validTransaction) + ); - BigDecimal calculatedNetWorth = player.getMoney().add(saleCalculator.calculateTotal()); - BigDecimal actualNetWorth = player.getNetWorth(); + assertThrows(IllegalArgumentException.class, + () -> testPlayer.handleTransaction(null) + ); - assertEquals(calculatedNetWorth, actualNetWorth); + assertThrows(NotEnoughMoneyException.class, + () -> testPlayer.handleTransaction(tooExpensiveTransaction) + ); } }