diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index a3a9af3..bc1ec00 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -4,7 +4,11 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import edu.ntnu.idi.idatt2003.g40.mappe.service.*; +import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.math.BigDecimal; import java.util.ArrayList; @@ -13,7 +17,7 @@ import java.util.Map; import java.util.Random; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; /** * Represents a stock exchange where stocks can be traded. @@ -40,7 +44,7 @@ public final class Exchange { /** * Current week (set to 1 in constructor). * */ - private final IntegerProperty week = new SimpleIntegerProperty(1); + private final ReadOnlyIntegerWrapper week = new ReadOnlyIntegerWrapper(1); /** * Map of {@link Stock} objects. Key is stock symbol. Value is stock. @@ -120,10 +124,12 @@ public boolean hasStock(final String symbol) { * * @return {@link Stock} element gotten. * - * @throws IllegalArgumentException if symbol is invalid. + * @throws IllegalArgumentException if symbol is invalid or not in exchange. * */ - public Stock getStock(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { + public Stock getStock(final String symbol) + throws IllegalArgumentException { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) + || !stockMap.containsKey(symbol)) { throw new IllegalArgumentException( Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } @@ -164,10 +170,12 @@ public List findStocks(final String searchTerm) { public Transaction buy(final String symbol, final BigDecimal quantity, final Player player) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) || player == null) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) + || quantity == null + || player == null) { throw new IllegalArgumentException("Invalid purchase!"); } - Stock stock = stockMap.get(symbol); + Stock stock = getStock(symbol); Share share = new Share(stock, quantity, stock.getSalesPrice()); TransactionCalculator calculator = new PurchaseCalculator(share); Transaction purchase = TransactionFactory.createTransaction( @@ -212,10 +220,12 @@ TransactionType.SALE, share, getWeek(), calculator * @param stockSymbol the stock to sell shares in. * @param player the player buying stock. * - * @return List of transactions commited to sell the shares. + * @return List of transactions commited to sell the shares. * * @throws IllegalArgumentException if any parameter is null, - * or if player does not have enough shares. + * or if player does not have enough shares, + * or if player does not own any shares + * of the given stock. * */ public List sell(BigDecimal amount, final String stockSymbol, @@ -225,41 +235,50 @@ public List sell(BigDecimal amount, || player == null || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { throw new IllegalArgumentException("Invalid sell!"); - } else { + } - List sharesOfStock = player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) - .toList(); + // Get all shares the player owns of the stock given in the parameter. + List sharesOfStock = player.getPortfolio().getShares().stream() + .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) + .toList(); - BigDecimal totalOwned = player.getPortfolio() - .getTotalSharesBySymbol(stockSymbol); + // Throws error if player does not own any shares of the given stock. + if (sharesOfStock.isEmpty()) { + throw new IllegalArgumentException("Player does not own" + + " any shares of this stock!"); + } - if (amount.compareTo(totalOwned) > 0) { - amount = totalOwned; - } - ArrayList transactions = new ArrayList<>(); - BigDecimal remainingToSell = amount; + // Gets the total quantity amount of shares owned of given stock. + BigDecimal totalOwned = player.getPortfolio() + .getTotalSharesBySymbol(stockSymbol); + + // If amount wanted to sell is greater than total owned, + // sells entire collection. + if (amount.compareTo(totalOwned) > 0) { + amount = totalOwned; + } + ArrayList transactions = new ArrayList<>(); + BigDecimal remainingToSell = amount; - for (Share share : sharesOfStock) { - if (remainingToSell.compareTo(BigDecimal.ZERO) <= 0) { - break; - } + // For every share owned, sells if the quantity is less than + // or equal to the remaining amount of shares to sell. + // Splits share if the remaining amount to sell is less than share quantity. + for (Share share : sharesOfStock) { - BigDecimal shareQty = share.getQuantity(); + BigDecimal shareQty = share.getQuantity(); - if (shareQty.compareTo(remainingToSell) <= 0) { - remainingToSell = remainingToSell.subtract(shareQty); - transactions.add(sell(share, player)); - } else { - Share newShare = player.getPortfolio().splitShare( - share, remainingToSell - ); - remainingToSell = BigDecimal.ZERO; - transactions.add(sell(newShare, player)); - } + if (shareQty.compareTo(remainingToSell) <= 0) { + remainingToSell = remainingToSell.subtract(shareQty); + transactions.add(sell(share, player)); + } else { + Share newShare = player.getPortfolio().splitShare( + share, remainingToSell + ); + transactions.add(sell(newShare, player)); + break; } - return transactions; } + return transactions; } /** @@ -275,12 +294,13 @@ public void advance() { for (Stock stock : stockMap.values()) { BigDecimal currentPrice = stock.getSalesPrice(); - double change = ( - (random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); + BigDecimal change = BigDecimal.valueOf(random.nextDouble() * 0.10 - 0.05) + .add(BigDecimal.valueOf(stock.getFortune())); stock.setFortune(0); - BigDecimal factor = BigDecimal.valueOf(1 + change); + BigDecimal factor = BigDecimal.ONE.add(change); - BigDecimal newPrice = currentPrice.multiply(factor); + BigDecimal newPrice = currentPrice.multiply(factor) + .setScale(2, java.math.RoundingMode.HALF_UP); stock.addNewSalesPrice(newPrice); } week.set(week.get() + 1); diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java index e4dd5ee..147810b 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java @@ -1,6 +1,8 @@ package edu.ntnu.idi.idatt2003.g40.mappe.engine; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; @@ -13,12 +15,12 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import java.math.BigDecimal; -import java.util.List; - import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +56,8 @@ void setUp() { appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200")); pearStock = new Stock("Pear", "Pear Inc.", new BigDecimal("97")); - testExchange = new Exchange("NASDAQ", List.of(appleStock, teslaStock)); + testExchange = new Exchange("NASDAQ", + List.of(appleStock, teslaStock, pearStock)); testPlayer = new Player("Alice", new BigDecimal("1000")); } @@ -62,10 +65,27 @@ void setUp() { void constructorSetsNameWeekAndStocksCorrectly() { assertEquals("NASDAQ", testExchange.getName()); assertEquals(1, testExchange.getWeek()); + assertEquals(1, testExchange.weekProperty().intValue()); assertTrue(testExchange.hasStock("AAPL")); assertTrue(testExchange.hasStock("TSLA")); } + @Test + void constructorThrowsErrorOnInvalidParameters() { + assertDoesNotThrow(() -> + new Exchange("Valid name", + List.of(appleStock, teslaStock, pearStock))); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange(null, List.of(appleStock, teslaStock, pearStock))); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange("Valid name", null)); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange("Valid name", new ArrayList<>())); + } + @Test void getStockReturnsCorrectStock() { Stock result = testExchange.getStock("AAPL"); @@ -73,6 +93,17 @@ void getStockReturnsCorrectStock() { assertSame(appleStock, result); } + @Test + void getStockThrowsExceptionOnInvalidStock() { + assertDoesNotThrow( + () -> testExchange.getStock("AAPL") + ); + assertThrows(IllegalArgumentException.class, + () -> testExchange.getStock("INVALIDSYMBOL")); + assertThrows(IllegalArgumentException.class, + () -> testExchange.getStock("NVID")); + } + @Test void findStocksReturnsMatchingStocksBySymbolOrCompany() { List resultBySymbol = testExchange.findStocks("AAP"); @@ -104,10 +135,30 @@ void buyReturnsPurchaseAndWithdrawsMoneyFromPlayer() { assertEquals(expectedPlayerMoney, testPlayer.getMoney()); } + @Test + void buyThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow(() -> + testExchange.buy("AAPL", new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("NVID", new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy(null, new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("AAPL", null, testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("AAPL", new BigDecimal("2"), null)); + + } + @Test void sellReturnsSaleAndAddsMoneyToPlayer() { Share share = - new Share(appleStock, new BigDecimal("2"), appleStock.getSalesPrice()); + new Share(appleStock, new BigDecimal("2"), + appleStock.getSalesPrice()); Transaction transaction = testExchange.sell(share, testPlayer); @@ -122,31 +173,33 @@ void sellReturnsSaleAndAddsMoneyToPlayer() { } @Test - void sellingAmountOfSharesRequiringSplitFunctions() { - Transaction purchase = testExchange.buy( - appleStock.getSymbol(), new BigDecimal("3"), testPlayer - ); + void sellShareObjectThrowsExceptionOnIllegalArguments() { + Transaction transaction = testExchange.buy("AAPL", new BigDecimal("2"), testPlayer); + Share shareObject = transaction.getShare(); + assertDoesNotThrow(() -> + testExchange.sell(shareObject, testPlayer)); - List sales = testExchange.sell( - new BigDecimal("1.5"), - appleStock.getSymbol(), - testPlayer - ); + assertThrows(IllegalArgumentException.class, () -> + testExchange.sell(null, testPlayer)); - SaleCalculator saleCalculator = new SaleCalculator(sales.getFirst().getShare()); - PurchaseCalculator purchaseCalculator = new PurchaseCalculator(purchase.getShare()); - BigDecimal expectedPlayerMoney = testPlayer.getStartingMoney() - .subtract(purchaseCalculator.calculateTotal()) - .add(saleCalculator.calculateTotal()); + assertThrows(IllegalArgumentException.class, () -> + testExchange.sell(shareObject, null)); + } + + @Test + void sellingPartialSharesUpdatesPlayerMoneyAndPortfolioCorrectly() { + testExchange.buy(appleStock.getSymbol(), new BigDecimal("3"), testPlayer); + + List sales = + testExchange.sell(new BigDecimal("1.5"), appleStock.getSymbol(), testPlayer); assertEquals(1, sales.size()); + BigDecimal expectedPlayerMoney = new BigDecimal("847.000"); assertEquals(expectedPlayerMoney, testPlayer.getMoney()); - BigDecimal expectedShareAmountOwned = purchase.getShare().getQuantity() - .subtract(sales.getFirst().getShare().getQuantity()); - - assertEquals(expectedShareAmountOwned, + BigDecimal expectedRemainingShares = new BigDecimal("1.5"); + assertEquals(expectedRemainingShares, testPlayer .getPortfolio() .getShares(appleStock.getSymbol()) @@ -156,22 +209,64 @@ void sellingAmountOfSharesRequiringSplitFunctions() { } @Test - void sellingAmountOfSharesRequiringSplitFunctions2() { - // Arrange: Clear setup with explicit numbers - Transaction purchase1 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("3.12"), testPlayer); - Transaction purchase2 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer); + void sellingPartialAmountOfSharesRequiringSplitSplitsSharesCorrectly() { + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("3.12"), testPlayer); + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("4.56"), testPlayer); - // Act: Sell 6 shares (requires taking 3.12 from purchase1 and 2.88 from purchase2) - List sales = testExchange.sell(new BigDecimal("6.00"), appleStock.getSymbol(), testPlayer); + List sales = + testExchange.sell(new BigDecimal("6.00"), + appleStock.getSymbol(), testPlayer); - // Assert 1: Verify the split logic actually happened - assertEquals(2, sales.size(), "Should split the sale across two purchase records"); + assertEquals(2, sales.size()); assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity()); - // Assert 2: Verify player money matches a known, pre-calculated constant - BigDecimal expectedFinalMoney = new BigDecimal("822.16000"); // Replace with the actual hardcoded math result - assertEquals(expectedFinalMoney, testPlayer.getMoney()); + BigDecimal expectedPlayerMoney = new BigDecimal("822.16000"); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void attemptingToSellMoreSharesThanOwnedSellsAllSharesOwned() { + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("3.12"), testPlayer); + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("4.56"), testPlayer); + + List sales = + testExchange.sell(new BigDecimal("100.00"), + appleStock.getSymbol(), testPlayer); + assertEquals(2, sales.size()); + + assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); + assertEquals(new BigDecimal("4.56"), sales.get(1).getShare().getQuantity()); + + BigDecimal expectedPlayerMoney = new BigDecimal("988.48000"); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void attemptingToSellPartialAmountOfSharesWithStockNotOwnedByPlayerThrowsError() { + BigDecimal testAmount = new BigDecimal("6.00"); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, + appleStock.getSymbol(), testPlayer)); + } + + @Test + void attemptingToSellPartialAmountOfSharesWithInvalidNumberAndPlayerThrowsError() { + BigDecimal testAmount = new BigDecimal("6.00"); + testExchange.buy(appleStock.getSymbol(), testAmount, testPlayer); + + assertDoesNotThrow(() -> testExchange.sell(testAmount, + appleStock.getSymbol(), testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(null, appleStock.getSymbol(), testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, null, testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, appleStock.getSymbol(), null)); } @Test @@ -186,16 +281,11 @@ void advanceIncreasesWeekAndUpdatesStockPrice() { @Test void getGainersActuallyReturnsProperGainers() { - Stock teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200.00")); - Stock pearStock = new Stock("PEAR", "Pear inc.", new BigDecimal("97.00")); - appleStock.addNewSalesPrice(new BigDecimal("150.00")); teslaStock.addNewSalesPrice(new BigDecimal("230.00")); pearStock.addNewSalesPrice(new BigDecimal("112.00")); - Exchange exchange = new Exchange("Exchange", List.of(appleStock, teslaStock, pearStock)); - - List actualGainers = exchange.getGainers(2); + List actualGainers = testExchange.getGainers(2); boolean actualGainersContainLimitedGainers = actualGainers.contains(teslaStock) && actualGainers.contains(appleStock); @@ -209,17 +299,23 @@ void getGainersActuallyReturnsProperGainers() { } @Test - void getLosersActuallyReturnsProperLosers() { - Stock teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200.00")); - Stock pearStock = new Stock("PEAR", "Pear inc.", new BigDecimal("97.00")); + void getGainersDoesNotReturnLoser() { + appleStock.addNewSalesPrice(new BigDecimal("150.00")); + teslaStock.addNewSalesPrice(new BigDecimal("230.00")); + pearStock.addNewSalesPrice(new BigDecimal("20")); + List actualGainers = testExchange.getGainers(3); + + assertFalse(actualGainers.contains(pearStock)); + } + + @Test + void getLosersActuallyReturnsProperLosers() { appleStock.addNewSalesPrice(new BigDecimal("50.00")); teslaStock.addNewSalesPrice(new BigDecimal("170.00")); - pearStock.addNewSalesPrice(new BigDecimal("82.00")); - - Exchange exchange = new Exchange("Exchange", List.of(appleStock, teslaStock, pearStock)); + pearStock.addNewSalesPrice(new BigDecimal("87")); - List actualLosers = exchange.getLosers(2); + List actualLosers = testExchange.getLosers(2); boolean actualLosersContainsValidLosers = actualLosers.contains(teslaStock) && actualLosers.contains(appleStock); @@ -233,18 +329,27 @@ void getLosersActuallyReturnsProperLosers() { } @Test - void invalidLimitForGettingGainersThrowError() { - Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + void getLosersDoesNotReturnWinners() { + + appleStock.addNewSalesPrice(new BigDecimal("50.00")); + teslaStock.addNewSalesPrice(new BigDecimal("170.00")); + pearStock.addNewSalesPrice(new BigDecimal("200")); - assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(0)); - assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-1)); + List actualLosers = testExchange.getLosers(3); + + boolean actualLosersNotContainsWinner = !actualLosers.contains(pearStock); + assertTrue(actualLosersNotContainsWinner); } @Test - void invalidLimitForGettingLosersThrowError() { - Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + void invalidLimitForGettingGainersThrowError() { + assertThrows(IllegalArgumentException.class, () -> testExchange.getGainers(0)); + assertThrows(IllegalArgumentException.class, () -> testExchange.getGainers(-1)); + } - assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(0)); - assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1)); + @Test + void invalidLimitForGettingLosersThrowError() { + assertThrows(IllegalArgumentException.class, () -> testExchange.getLosers(0)); + assertThrows(IllegalArgumentException.class, () -> testExchange.getLosers(-1)); } }