getShares() {
- return shares;
+ return List.copyOf(shares);
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
index d3c782b..c527b20 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
@@ -23,6 +23,15 @@ public Share(
final BigDecimal quantity,
final BigDecimal purchasePrice
) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock cannot be null");
+ }
+ if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Quantity must be positive");
+ }
+ if (purchasePrice == null || purchasePrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Purchase price cannot be negative");
+ }
this.stock = stock;
this.quantity = quantity;
this.purchasePrice = purchasePrice;
@@ -80,6 +89,10 @@ public boolean equals(Object object) {
*/
@Override
public int hashCode() {
- return Objects.hash(stock.getSymbol(), quantity, purchasePrice);
+ return Objects.hash(
+ stock.getSymbol(),
+ quantity == null ? null : quantity.stripTrailingZeros(),
+ purchasePrice == null ? null : purchasePrice.stripTrailingZeros()
+ );
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
index 7c7d9c5..f36fe1a 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
@@ -11,7 +11,7 @@
* {@link #INVESTOR}: Achieved by trading for at least 10 distinct weeks
* and increasing net worth by at least 20%.
* {@link #SPECULATOR}: Achieved by trading for at least 20 distinct weeks
- * and doubling net worth.
+ * and increasing net worth by at least 5 times.
*
*/
public enum Status {
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
index 430c5e1..0bea46a 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
@@ -1,7 +1,5 @@
package edu.ntnu.idi.idatt2003.gruppe42.model;
-import edu.ntnu.idi.idatt2003.gruppe42.controller.StockController;
-import edu.ntnu.idi.idatt2003.gruppe42.view.views.components.StockGraph;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -9,21 +7,17 @@
/**
* Represents a publicly traded stock belonging to a company.
*
- * A stock maintains a full chronological price history, exposes summary
- * statistics (highest, lowest, the latest change), and delegates price-update
- * logic to a {@link StockController}. An optional {@link StockGraph} can be
- * lazily attached for visual rendering.
+ *
A stock maintains a full chronological price history and exposes summary
+ * statistics (highest, lowest, the latest change).
*
*
Price history is append-only: new prices are recorded via
- * {@link #addNewSalesPrice(BigDecimal)} or {@link #updatePrice()}, and the
+ * {@link #addNewSalesPrice(BigDecimal)} and the
* most recent entry is always considered the current market price.
*/
public class Stock {
private final String symbol;
private final String company;
- private StockGraph stockGraph;
private final List prices = new ArrayList<>();
- private final StockController stockController;
/** Constructs a new stock. */
public Stock(
@@ -31,10 +25,18 @@ public Stock(
final String company,
final BigDecimal salesPrice
) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
+ }
+ if (company == null || company.isBlank()) {
+ throw new IllegalArgumentException("Company cannot be null or blank");
+ }
+ if (salesPrice == null || salesPrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Sales price cannot be negative");
+ }
this.symbol = symbol;
this.company = company;
prices.add(salesPrice);
- stockController = new StockController(this);
}
public String getSymbol() {
@@ -75,38 +77,4 @@ public BigDecimal getLatestPriceChange() {
int index120Ago = Math.max(0, prices.size() - 121);
return getSalesPrice().subtract(prices.get(index120Ago));
}
-
- /** Updates the price. */
- public void updatePrice() {
- BigDecimal newPrice = stockController.updatePrice();
- prices.add(newPrice);
- }
-
- /**
- * Returns the {@link StockGraph} component associated with this stock,
- * creating it lazily on the first call.
- *
- * @return the stock graph; never {@code null}
- */
- public StockGraph getStockGraph() {
- if (stockGraph == null) {
- stockGraph = new StockGraph();
- }
- return stockGraph;
- }
-
- /** Generates fake history. */
- public void fakeHistory(int stockResolution) {
- for (int i = 0; i < stockResolution; i++) {
- updatePrice();
- }
- java.util.Collections.reverse(prices);
- }
-
- /** Updates the stock graph. */
- public void updateStockGraph() {
- if (stockGraph != null && stockGraph.getVisibility()) {
- stockGraph.update(this);
- }
- }
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
index 17859fd..838731c 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
@@ -82,7 +82,7 @@ public static List readFromFile(final Path path)
if (!SYMBOL_PATTERN.matcher(symbol).matches()) {
throw new StockFileParseException(lineNumber, trimmed,
String.format("Ticker symbol \"%s\" is invalid — "
- + "must be exactly 3 uppercase letters (A-Z)", symbol));
+ + "must be 1-5 uppercase letters (A-Z) or dots (.)", symbol));
}
String company = parts[1].trim();
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
index 332b1f9..c9610f2 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
@@ -53,8 +53,11 @@ public BigDecimal calculateCommission() {
public BigDecimal calculateTax() {
final BigDecimal taxRate = new BigDecimal("0.22");
BigDecimal differencePrice = salesPrice.subtract(purchasePrice);
- BigDecimal difference = differencePrice.multiply(quantity);
- return difference.multiply(taxRate);
+ BigDecimal profit = differencePrice.multiply(quantity);
+ if (profit.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO;
+ }
+ return profit.multiply(taxRate);
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
index 8f22e11..051fa7d 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
@@ -33,6 +33,7 @@ public Purchase(final Share share, final int week) {
@Override
public void commit(final Player player) {
player.withdrawMoney(getCalculator().calculateTotal());
+ player.getPortfolio().addShare(getShare());
player.getTransactionArchive().add(this);
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
index 1ccb8aa..5ddd2a0 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
@@ -33,6 +33,7 @@ public Sale(final Share share, final int week) {
@Override
public void commit(final Player player) {
player.addMoney(getCalculator().calculateTotal());
+ player.getPortfolio().removeShare(getShare());
player.getTransactionArchive().add(this);
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
index 60d72be..9733c0c 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
@@ -51,7 +51,6 @@ public TransactionCalculator getCalculator() {
* Subclasses define the specific behavior (e.g., buying or selling shares).
*
* @param player the player executing the transaction
- * @throws Exception if the transaction cannot be completed
*/
- public abstract void commit(Player player) throws Exception;
+ public abstract void commit(Player player);
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
index 03d20e8..da7ac61 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
@@ -46,6 +46,7 @@ public class StockApp extends Popup {
private final Button confirmButton;
private final Button backButton;
private final Receipt receipt;
+ private final StockGraph stockGraph;
/**
* Constructs the Stock trading popup.
@@ -55,6 +56,8 @@ public class StockApp extends Popup {
public StockApp(final Player player) {
super(475, 450, 140, 140, App.STOCK);
+ this.stockGraph = new StockGraph();
+
this.searchField = new TextField();
this.searchField.setPromptText("Search stocks...");
this.searchField.getStyleClass().add("search-field");
@@ -137,15 +140,15 @@ public void showStockPage(final Stock stock) {
priceLabel = new Label("$" + String.format("%,.2f", stock.getSalesPrice()));
priceLabel.getStyleClass().add("stock-price-large");
- StockGraph graph = stock.getStockGraph();
- VBox.setVgrow(graph, Priority.ALWAYS);
+ VBox.setVgrow(stockGraph, Priority.ALWAYS);
+ stockGraph.update(stock);
receipt.update(stock, new BigDecimal(quantitySpinner.getValue()));
HBox receiptCentered = new HBox(receipt);
receiptCentered.setAlignment(Pos.CENTER);
content.setSpacing(20);
- content.getChildren().setAll(topControls, headerBox, priceLabel, graph, receiptCentered);
+ content.getChildren().setAll(topControls, headerBox, priceLabel, stockGraph, receiptCentered);
}
/**
@@ -155,12 +158,19 @@ public void showStockPage(final Stock stock) {
*/
public void updatePriceLabel(final Stock stock) {
if (priceLabel != null) {
- Platform.runLater(() ->
- priceLabel.setText("$" + String.format("%,.2f", stock.getSalesPrice()))
- );
+ Platform.runLater(() -> {
+ priceLabel.setText("$" + String.format("%,.2f", stock.getSalesPrice()));
+ if (stockGraph.getVisibility()) {
+ stockGraph.update(stock);
+ }
+ });
}
}
+ public StockGraph getStockGraph() {
+ return stockGraph;
+ }
+
public TextField getSearchField() {
return searchField;
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
index fb02274..c64d2d7 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
@@ -89,4 +89,32 @@ void getNetworthTest() {
new BigDecimal("5000")));
assertEquals(new BigDecimal("14950.00"), player.getNetWorth());
}
+
+ @Test
+ void testUpdateStatusToInvestor() {
+ // Requires 10 distinct weeks and net worth >= 1.2 * starting money (12000)
+ for (int i = 0; i < 10; i++) {
+ player.getTransactionArchive().add(new edu.ntnu.idi.idatt2003.gruppe42.model.transaction.Purchase(
+ new Share(new Stock("AAPL", "Apple Inc.", new BigDecimal("100")), new BigDecimal("1"), new BigDecimal("100")),
+ i
+ ));
+ }
+ player.addMoney(new BigDecimal("2000")); // Net worth becomes 12000
+ player.updateStatus();
+ assertEquals("INVESTOR", player.getStatus());
+ }
+
+ @Test
+ void testUpdateStatusToSpeculator() {
+ // Requires 20 distinct weeks and net worth >= 5 * starting money (50000)
+ for (int i = 0; i < 20; i++) {
+ player.getTransactionArchive().add(new edu.ntnu.idi.idatt2003.gruppe42.model.transaction.Purchase(
+ new Share(new Stock("AAPL", "Apple Inc.", new BigDecimal("100")), new BigDecimal("1"), new BigDecimal("100")),
+ i
+ ));
+ }
+ player.addMoney(new BigDecimal("40000")); // Net worth becomes 50000
+ player.updateStatus();
+ assertEquals("SPECULATOR", player.getStatus());
+ }
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
index a71b58d..78c79fd 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
@@ -9,9 +9,9 @@
import org.junit.jupiter.api.Test;
/**
- * Unit tests for the {@link Exchange} class.
+ * Unit tests for the {@link Portfolio} class.
*
- *
Verifies stock management, search functionality, and week progression behavior.
+ *
Verifies share management, merging logic, and quantity tracking.
*/
public class PortfolioTest {
private Portfolio portfolio;
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
index d61bcb4..22c4d92 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
@@ -17,28 +17,20 @@
public class StockFileHandlerTest {
@Test
- void readFromFileTest() throws IOException {
+ void readFromFileTest() throws Exception {
Path testFile = Files.createTempFile("stocks", "csv");
Files.writeString(testFile, "AAPL,Apple Inc.,276.43\nMSFT,Microsoft,404.68");
- try {
- List stocks = StockFileHandler.readFromFile(testFile);
- assertEquals(2, stocks.size());
- assertEquals("AAPL", stocks.get(0).getSymbol());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ List stocks = StockFileHandler.readFromFile(testFile);
+ assertEquals(2, stocks.size());
+ assertEquals("AAPL", stocks.get(0).getSymbol());
}
@Test
- void readFromFileWithCommentsAndEmptyLines() throws IOException {
+ void readFromFileWithCommentsAndEmptyLines() throws Exception {
Path testFile = Files.createTempFile("stocks", "csv");
Files.writeString(testFile, "#Comment\n\nAAPL,Apple Inc.,276.43");
- try {
- List stocks = StockFileHandler.readFromFile(testFile);
- assertEquals(1, stocks.size());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ List stocks = StockFileHandler.readFromFile(testFile);
+ assertEquals(1, stocks.size());
}
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
index 1231d97..6c76b27 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
@@ -69,4 +69,21 @@ void testCalculateTotal() {
assertNotEquals(0, new BigDecimal("1375.01").compareTo(calculator.calculateTotal()));
assertNotEquals(0, new BigDecimal("1374.99").compareTo(calculator.calculateTotal()));
}
+
+ @Test
+ void testCalculateTaxLoss() {
+ BigDecimal purchasePrice = new BigDecimal("150");
+ BigDecimal salesPrice = new BigDecimal("100");
+ BigDecimal quantity = new BigDecimal("10");
+ Stock stock = new Stock("AAPL", "Apple Inc.", salesPrice);
+ Share share = new Share(stock, quantity, purchasePrice);
+ SaleCalculator lossCalculator = new SaleCalculator(share);
+
+ // Tax should be 0 on a loss, not negative
+ assertEquals(0, BigDecimal.ZERO.compareTo(lossCalculator.calculateTax()), "Tax should be zero on loss");
+
+ // Total should be: gross(1000) - commission(10) - tax(0) = 990
+ BigDecimal expectedTotal = new BigDecimal("990");
+ assertEquals(0, expectedTotal.compareTo(lossCalculator.calculateTotal()), "Total payout should not include negative tax");
+ }
}