diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..319ff73
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.checkstyle.configuration": "/google_checks.xml"
+}
\ No newline at end of file
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..9f86503
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index a74b899..1b52053 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,27 @@
maven-javadoc-plugin
3.12.0
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.1
+
+ checkstyle.xml
+ UTF-8
+ true
+ false
+ false
+
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.12.5
+
+
+
\ No newline at end of file
diff --git a/src/main/java/Controller/StockFileHandler.java b/src/main/java/Controller/StockFileHandler.java
index b64f9c0..bf808a7 100644
--- a/src/main/java/Controller/StockFileHandler.java
+++ b/src/main/java/Controller/StockFileHandler.java
@@ -1,37 +1,58 @@
package Controller;
-import java.nio.file.*;
+
+import Model.Stock;
import java.io.IOException;
import java.math.BigDecimal;
+import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
-import Model.Stock;
+/**
+ * Handles loading and saving Stock data from/to CSV files.
+ * The CSV format is: ticker,company_name,price
+ * Lines starting with '#' are treated as comments and ignored.
+ */
public class StockFileHandler {
- // lesing
- public List loadStocksFromFile(String filename) throws IOException {
- return Files.readAllLines(Paths.get(filename)).stream()
- .map(String::trim)
- .filter(line -> !line.isEmpty() && !line.startsWith("#"))
- .map(line -> line.split(","))
- .filter(parts -> parts.length == 3)
- .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2])))
- .toList();
- }
+ /**
+ * Loads stocks from a CSV file.
+ * Expected format: ticker,company,price
+ * Empty lines and lines starting with '#' are ignored.
+ *
+ * @param filename the path to the CSV file
+ * @return a list of Stock objects loaded from the file
+ * @throws IOException if the file cannot be read
+ */
+ public List loadStocksFromFile(String filename) throws IOException {
+ return Files.readAllLines(Paths.get(filename)).stream()
+ .map(String::trim)
+ .filter(line -> !line.isEmpty() && !line.startsWith("#"))
+ .map(line -> line.split(","))
+ .filter(parts -> parts.length == 3)
+ .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2])))
+ .toList();
+ }
- // lagring
- public void saveStocksToFile(String filename, List stocks) throws IOException {
- List lines = new ArrayList<>();
- lines.add("# Ticker,Name,Price");
+ /**
+ * Saves stocks to a CSV file.
+ * The file includes a header comment line followed by one stock per line.
+ *
+ * @param filename the path to the CSV file to write
+ * @param stocks the list of stocks to save
+ * @throws IOException if the file cannot be written
+ */
+ public void saveStocksToFile(String filename, List stocks) throws IOException {
+ List lines = new ArrayList<>();
+ lines.add("# Ticker,Name,Price");
- lines.addAll(stocks.stream()
- .map(stock -> String.format("%s,%s,%s",
- stock.getSymbol(),
- stock.getCompany(),
- stock.getSalesPrice().toString()))
- .toList());
+ lines.addAll(stocks.stream()
+ .map(stock -> String.format("%s,%s,%s",
+ stock.getSymbol(),
+ stock.getCompany(),
+ stock.getSalesPrice().toString()))
+ .toList());
- Files.write(Paths.get(filename), lines);
- }
+ Files.write(Paths.get(filename), lines);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/Model/Exchange.java b/src/main/java/Model/Exchange.java
index 6db979c..7feb4f2 100644
--- a/src/main/java/Model/Exchange.java
+++ b/src/main/java/Model/Exchange.java
@@ -1,4 +1,5 @@
package Model;
+
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
@@ -6,216 +7,323 @@
import java.util.Map;
import java.util.Random;
+/**
+ * Represents a stock exchange where players can buy and sell stocks.
+ * The Exchange maintains a collection of stocks, advances through weeks,
+ * and executes trades. It implements the Observer pattern to notify listeners
+ * (typically the UI) whenever its state changes.
+ */
public class Exchange {
- private final String name;
- private int week;
- private final Map stockMap;
- private final Random random;
-
- // Registered observers
- private final List observers = new ArrayList<>();
-
- public Exchange(String name, List stocks) {
- if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Exchange name cannot be null or blank");
- }
- if (stocks == null) {
- throw new IllegalArgumentException("Stock list cannot be null");
- }
-
- this.name = name;
- this.week = 1;
- this.stockMap = new HashMap<>();
- this.random = new Random();
-
- for (Stock stock : stocks) {
- if (stock == null) {
- throw new IllegalArgumentException("Stock list must not contain null entries");
- }
- stockMap.put(stock.getSymbol(), stock);
- }
+ private final String name;
+ private int week;
+ private final Map stockMap;
+ private final Random random;
+
+ // Registered observers
+ private final List observers = new ArrayList<>();
+
+ /**
+ * Constructs a new Exchange with the given name and initial stocks.
+ *
+ * @param name the exchange name. Cannot be null or blank
+ * @param stocks the initial list of stocks. Cannot be null or contain null entries
+ * @throws IllegalArgumentException if any parameter is invalid
+ */
+ public Exchange(String name, List stocks) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Exchange name cannot be null or blank");
}
-
- // ---- Observer ----
-
- public void addObserver(ExchangeObserver observer) {
- if (observer == null) {
- throw new IllegalArgumentException("Observer cannot be null");
- }
- if (!observers.contains(observer)) {
- observers.add(observer);
- }
+ if (stocks == null) {
+ throw new IllegalArgumentException("Stock list cannot be null");
}
-
- public void removeObserver(ExchangeObserver observer) {
- observers.remove(observer);
+
+ this.name = name;
+ this.week = 1;
+ this.stockMap = new HashMap<>();
+ this.random = new Random();
+
+ for (Stock stock : stocks) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock list must not contain null entries");
+ }
+ stockMap.put(stock.getSymbol(), stock);
}
-
- private void notifyObservers() {
- for (ExchangeObserver observer : observers) {
- observer.onExchangeUpdated(this);
- }
+ }
+
+ // ---- Observer ----
+
+ /**
+ * Registers an observer to receive notifications of state changes.
+ *
+ * @param observer the observer to register. Cannot be null
+ * @throws IllegalArgumentException if observer is null
+ */
+ public void addObserver(ExchangeObserver observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("Observer cannot be null");
}
-
- public String getName() {
- return name;
+ if (!observers.contains(observer)) {
+ observers.add(observer);
}
-
- public int getWeek() {
- return week;
+ }
+
+ /**
+ * Unregisters an observer from receiving notifications.
+ *
+ * @param observer the observer to remove
+ */
+ public void removeObserver(ExchangeObserver observer) {
+ observers.remove(observer);
+ }
+
+ /**
+ * Notifies all registered observers of a state change.
+ */
+ private void notifyObservers() {
+ for (ExchangeObserver observer : observers) {
+ observer.onExchangeUpdated(this);
}
-
- public boolean hasStock(String symbol) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or blank");
- }
- return stockMap.containsKey(symbol);
+ }
+
+ /**
+ * Returns the exchange name.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the current week number.
+ *
+ * @return the week
+ */
+ public int getWeek() {
+ return week;
+ }
+
+ /**
+ * Checks if a stock with the given symbol is available on this exchange.
+ *
+ * @param symbol the stock symbol. Cannot be null or blank
+ * @return true if the stock exists; false otherwise
+ * @throws IllegalArgumentException if symbol is invalid
+ */
+ public boolean hasStock(String symbol) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
}
-
- public Stock getStock(String symbol) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or blank");
- }
- return stockMap.get(symbol);
+ return stockMap.containsKey(symbol);
+ }
+
+ /**
+ * Returns the stock with the given symbol.
+ *
+ * @param symbol the stock symbol. Cannot be null or blank
+ * @return the Stock, or null if not found
+ * @throws IllegalArgumentException if symbol is invalid
+ */
+ public Stock getStock(String symbol) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
+ }
+ return stockMap.get(symbol);
+ }
+
+ /**
+ * Searches for stocks matching the search term by symbol or company name.
+ *
+ * @param searchTerm the search term. Cannot be null
+ * @return a list of matching stocks (case-insensitive)
+ * @throws IllegalArgumentException if searchTerm is null
+ */
+ public List findStocks(String searchTerm) {
+ if (searchTerm == null) {
+ throw new IllegalArgumentException("Search term cannot be null");
}
-
- public List findStocks(String searchTerm) {
- if (searchTerm == null) {
- throw new IllegalArgumentException("Search term cannot be null");
- }
- List result = new ArrayList<>();
- String lowerSearch = searchTerm.toLowerCase();
-
- for (Stock stock : stockMap.values()) {
- if (stock.getSymbol().toLowerCase().contains(lowerSearch)
- || stock.getCompany().toLowerCase().contains(lowerSearch)) {
- result.add(stock);
- }
- }
-
- return result;
+ List result = new ArrayList<>();
+ String lowerSearch = searchTerm.toLowerCase();
+
+ for (Stock stock : stockMap.values()) {
+ if (stock.getSymbol().toLowerCase().contains(lowerSearch)
+ || stock.getCompany().toLowerCase().contains(lowerSearch)) {
+ result.add(stock);
+ }
}
- public Transaction buy(String symbol, BigDecimal quantity, Player player) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or blank");
- }
- if (quantity == null) {
- throw new IllegalArgumentException("Quantity cannot be null");
- }
- if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
- throw new IllegalArgumentException("Quantity must be greater than zero");
- }
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
+ return result;
+ }
+
+ /**
+ * Executes a buy transaction for the specified stock and quantity.
+ *
+ * @param symbol the stock symbol. Cannot be null or blank
+ * @param quantity the number of shares to buy. Must be greater than zero
+ * @param player the player executing the purchase. Cannot be null
+ * @return the completed Purchase transaction
+ * @throws IllegalArgumentException if any parameter is invalid or stock not found
+ * @throws IllegalStateException if player has insufficient funds
+ */
+ public Transaction buy(String symbol, BigDecimal quantity, Player player) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
+ }
+ if (quantity == null) {
+ throw new IllegalArgumentException("Quantity cannot be null");
+ }
+ if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Quantity must be greater than zero");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
- Stock stock = getStock(symbol);
- if (stock == null) {
- throw new IllegalArgumentException("No stock found with symbol: " + symbol);
- }
-
- // lager en ny "andel" basert på nåværende salgspris
- Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice());
- Purchase purchase = new Purchase(shareToBuy, this.week);
- purchase.commit(player);
-
- notifyObservers();
-
- return purchase;
+ Stock stock = getStock(symbol);
+ if (stock == null) {
+ throw new IllegalArgumentException("No stock found with symbol: " + symbol);
}
- public Transaction sell(Share share, Player player) {
-
- return sell(share, share.getQuantity(), player);
+ // Creates a new share based on current selling price
+ Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice());
+ Purchase purchase = new Purchase(shareToBuy, this.week);
+ purchase.commit(player);
+
+ notifyObservers();
+
+ return purchase;
+ }
+
+ /**
+ * Executes a full sale transaction for all shares in the given holding.
+ *
+ * @param share the share to sell completely. Cannot be null
+ * @param player the player executing the sale. Cannot be null
+ * @return the completed Sale transaction
+ * @throws IllegalArgumentException if any parameter is invalid
+ * @throws IllegalStateException if player doesn't own the share
+ */
+ public Transaction sell(Share share, Player player) {
+ return sell(share, share.getQuantity(), player);
+ }
+
+ /**
+ * Executes a partial or complete sale transaction.
+ * If the sell quantity is less than the share quantity, creates a partial sale
+ * and leaves the remainder in the portfolio.
+ *
+ * @param originalShare the share to sell from. Cannot be null
+ * @param sellQuantity the quantity to sell. Cannot exceed the original quantity
+ * @param player the player executing the sale. Cannot be null
+ * @return the completed Sale transaction
+ * @throws IllegalArgumentException if any parameter is invalid
+ * @throws IllegalStateException if sell quantity exceeds holding
+ */
+ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player player) {
+ if (originalShare == null || sellQuantity == null) {
+ throw new IllegalArgumentException("Share and quantity cannot be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
}
- public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player player) {
- if (originalShare == null || sellQuantity == null) {
- throw new IllegalArgumentException("Share and quantity cannot be null");
- }
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- // Kan ikke selge mer enn man eier
- if (sellQuantity.compareTo(originalShare.getQuantity()) > 0) {
- throw new IllegalArgumentException("Cannot sell more shares than owned");
- }
-
+ // Cannot sell more shares than you have
+ if (sellQuantity.compareTo(originalShare.getQuantity()) > 0) {
+ throw new IllegalArgumentException("Cannot sell more shares than owned");
+ }
- Share shareToSell;
- if (sellQuantity.compareTo(originalShare.getQuantity()) < 0) {
+ Share shareToSell;
- /**
- * Delsalg:
- * Original mengde - mengde som selges = gjenværende mengde
- */
+ if (sellQuantity.compareTo(originalShare.getQuantity()) < 0) {
- player.getPortfolio().removeShare(originalShare);
- BigDecimal remainderShare = originalShare.getQuantity().subtract(sellQuantity);
+ // Partial sale:
+ // Original quantity - quantity sold = remaining quantity
- player.getPortfolio().addShare(new Share(originalShare.getStock(), remainderShare, originalShare.getPurchasePrice()));
+ player.getPortfolio().removeShare(originalShare);
- /**
- * Legger delmengden midlertidig til i portfolio slik at Sale.commit() finner den
- */
+ BigDecimal remainderShare = originalShare.getQuantity().subtract(sellQuantity);
- shareToSell = new Share(originalShare.getStock(), sellQuantity, originalShare.getPurchasePrice());
+ player.getPortfolio().addShare(new Share(originalShare
+ .getStock(), remainderShare, originalShare.getPurchasePrice()));
- player.getPortfolio().addShare(shareToSell);
+
+ // Temporarily adds the subset to the portfolio so that Sale.commit() can find it
- }
+ shareToSell = new Share(originalShare
+ .getStock(), sellQuantity, originalShare.getPurchasePrice());
- else {
- // Fullstendig salg: hele andelen selges som normalt
- shareToSell = originalShare;
- }
+ player.getPortfolio().addShare(shareToSell);
- // Salgstransaksjon
- Transaction sale = TransactionFactory.createSale(shareToSell, this.week);
+ }
- sale.commit(player);
- notifyObservers();
- return sale;
+ else {
+ // Complete sale: the entire share is sold as normal
+ shareToSell = originalShare;
}
- public void advance() {
-
- week++;
+ // Salestransaction
+ Transaction sale = TransactionFactory.createSale(shareToSell, this.week);
- for (Stock stock : stockMap.values()) { // henter stock-objektene
+ sale.commit(player);
+ notifyObservers();
+ return sale;
+ }
- BigDecimal currentPrice = stock.getSalesPrice(); // henter siste pris fra Stock
+ /**
+ * Advances the game to the next week and updates all stock prices.
+ * Each stock's price is randomly adjusted by -10% to +10% per week.
+ * Notifies all observers of the change.
+ */
+ public void advance() {
+
+ week++;
- double changePercent = (random.nextDouble() - 0.5) * 0.1;
+ for (Stock stock : stockMap.values()) { // Retrieves the stock-objects
- BigDecimal change = currentPrice.multiply(BigDecimal.valueOf(changePercent));
+ BigDecimal currentPrice = stock.getSalesPrice(); // Retrieves last price from Stock
- BigDecimal newPrice = currentPrice.add(change);
+ double changePercent = (random.nextDouble() - 0.5) * 0.1;
- if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // unngå negativ pris
- stock.addNewSalesPrice(newPrice);
- }
- }
+ BigDecimal change = currentPrice.multiply(BigDecimal.valueOf(changePercent));
- notifyObservers();
- }
+ BigDecimal newPrice = currentPrice.add(change);
- public List getGainers(int limit) { // viser "vinnerne"
- return stockMap.values().stream()
- .sorted((s1, s2) -> s2.getLatestPriceChange().compareTo(s1.getLatestPriceChange()))
- .limit(limit)
- .toList();
+ if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // avoid negative price
+ stock.addNewSalesPrice(newPrice);
+ }
}
- public List getLosers(int limit) { // viser "taperne"
- return stockMap.values().stream()
- .sorted((s1, s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange()))
- .limit(limit)
- .toList();
- }
+ notifyObservers();
+ }
+
+ /**
+ * Returns the top-performing stocks (gainers) for the current week.
+ * Gainers are stocks with the highest positive price change.
+ *
+ * @param limit the maximum number of gainers to return
+ * @return a list of the top gainers, sorted by price change (highest first)
+ */
+ public List getGainers(int limit) { // shows the "winners"
+ return stockMap.values().stream()
+ .sorted((s1, s2) -> s2.getLatestPriceChange().compareTo(s1.getLatestPriceChange()))
+ .limit(limit)
+ .toList();
+ }
+
+ /**
+ * Returns the lowest-performing stocks (losers) for the current week.
+ * Losers are stocks with the highest negative price change.
+ *
+ * @param limit the maximum number of losers to return
+ * @return a list of the top losers, sorted by price change (lowest first)
+ */
+ public List getLosers(int limit) { // shows the "losers"
+ return stockMap.values().stream()
+ .sorted((s1, s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange()))
+ .limit(limit)
+ .toList();
+ }
}
diff --git a/src/main/java/Model/ExchangeObserver.java b/src/main/java/Model/ExchangeObserver.java
index ad3f6a5..7cef281 100644
--- a/src/main/java/Model/ExchangeObserver.java
+++ b/src/main/java/Model/ExchangeObserver.java
@@ -1,16 +1,16 @@
package Model;
/**
- * Observer interface for the Exchange subject.
- * Implement this interface to receive notifications whenever the exchange
- * state changes (week advances, a trade is committed).
+ * Observer interface for receiving notifications from the Exchange.
+ * Implementations are notified whenever the Exchange state changes,
+ * allowing the View and other components to stay synchronized.
*/
public interface ExchangeObserver {
- /**
- * Called by the Exchange after its state has changed.
- *
- * @param exchange the Exchange that triggered the notification
- */
- void onExchangeUpdated(Exchange exchange);
+ /**
+ * Called by the Exchange when its state changes.
+ *
+ * @param exchange the Exchange that triggered the notification
+ */
+ void onExchangeUpdated(Exchange exchange);
}
diff --git a/src/main/java/Model/Player.java b/src/main/java/Model/Player.java
index ef078a5..dd9d81e 100644
--- a/src/main/java/Model/Player.java
+++ b/src/main/java/Model/Player.java
@@ -1,96 +1,144 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Represents a player in the stock trading game.
+ * A Player manages their personal portfolio of shares, cash balance, and transaction
+ * history. Players can buy and sell stocks, and their performance is tracked through
+ * status levels based on trading activity and net worth growth.
+ */
public class Player {
- private String name;
- private BigDecimal startingMoney;
- private BigDecimal money;
- private Portfolio portfolio;
- private TransactionArchive transactionArchive;
-
- public Player(String name, BigDecimal startingMoney) {
- if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Player name cannot be null or blank");
- }
- if (startingMoney == null) {
- throw new IllegalArgumentException("Starting money cannot be null");
- }
- if (startingMoney.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Starting money cannot be negative");
- }
+ private String name;
+ private BigDecimal startingMoney;
+ private BigDecimal money;
+ private Portfolio portfolio;
+ private TransactionArchive transactionArchive;
- this.name = name;
- this.startingMoney = startingMoney;
- this.money = startingMoney;
- this.portfolio = new Portfolio();
- this.transactionArchive = new TransactionArchive();
+ /**
+ * Constructs a new Player with the given name and starting capital.
+ *
+ * @param name the player's name. Cannot be null or blank
+ * @param startingMoney the initial cash balance. Cannot be null or negative
+ * @throws IllegalArgumentException if any parameter is invalid
+ */
+ public Player(String name, BigDecimal startingMoney) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Player name cannot be null or blank");
}
-
- public String getName() {
- return this.name;
+ if (startingMoney == null) {
+ throw new IllegalArgumentException("Starting money cannot be null");
}
-
- public BigDecimal getMoney() {
- return this.money;
+ if (startingMoney.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Starting money cannot be negative");
}
- public void addMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount cannot be null");
- }
- if (amount.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Amount to add cannot be negative");
- }
- this.money = this.money.add(amount);
- }
+ this.name = name;
+ this.startingMoney = startingMoney;
+ this.money = startingMoney;
+ this.portfolio = new Portfolio();
+ this.transactionArchive = new TransactionArchive();
+ }
- public void withdrawMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount cannot be null");
- }
- if (amount.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Amount to withdraw cannot be negative");
- }
- this.money = this.money.subtract(amount);
- }
+ /**
+ * Returns the player's name.
+ *
+ * @return the player's name
+ */
+ public String getName() {
+ return this.name;
+ }
- public Portfolio getPortfolio() {
- return this.portfolio;
+ /**
+ * Returns the player's current cash balance.
+ *
+ * @return the current amount of money available
+ */
+ public BigDecimal getMoney() {
+ return this.money;
+ }
+
+ /**
+ * Adds the specified amount to the player's cash balance.
+ *
+ * @param amount the amount to add. Cannot be null or negative
+ * @throws IllegalArgumentException if amount is invalid
+ */
+ public void addMoney(BigDecimal amount) {
+ if (amount == null) {
+ throw new IllegalArgumentException("Amount cannot be null");
+ }
+ if (amount.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Amount to add cannot be negative");
}
+ this.money = this.money.add(amount);
+ }
- public TransactionArchive getTransactionArchive() {
- return this.transactionArchive;
+ /**
+ * Removes the specified amount from the player's cash balance.
+ *
+ * @param amount the amount to withdraw. Cannot be null or negative
+ * @throws IllegalArgumentException if amount is invalid
+ */
+ public void withdrawMoney(BigDecimal amount) {
+ if (amount == null) {
+ throw new IllegalArgumentException("Amount cannot be null");
}
+ if (amount.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Amount to withdraw cannot be negative");
+ }
+ this.money = this.money.subtract(amount);
+ }
- /**
- * Returns the player's current status based on trading activity and net worth performance.
- *
- * Status levels:
- * - NOVICE: Starting level, no requirements
- * - INVESTOR: At least 10 weeks of trading AND net worth increased by at least 20%
- * - SPECULATOR: At least 20 weeks of trading AND net worth at least doubled
- *
- * @return the player's current PlayerStatus
- */
- public PlayerStatus getStatus() {
- // Calculate total net worth: current cash + portfolio value
- BigDecimal totalNetWorth = this.money.add(this.portfolio.getNetWorth());
-
- // Get number of weeks with trading activity
- int weeksActive = this.transactionArchive.countDistinctWeeks();
+ /**
+ * Returns the player's portfolio.
+ *
+ * @return the portfolio containing held shares
+ */
+ public Portfolio getPortfolio() {
+ return this.portfolio;
+ }
+
+ /**
+ * Returns the player's transaction archive.
+ *
+ * @return the archive of all transactions
+ */
+ public TransactionArchive getTransactionArchive() {
+ return this.transactionArchive;
+ }
+
+
+ /**
+ * Returns the player's current status based on trading activity and net worth performance.
+ * Status levels:
+ * - NOVICE: Starting level, no requirements
+ * - INVESTOR: At least 10 weeks of trading AND net worth increased by at least 20%
+ * - SPECULATOR: At least 20 weeks of trading AND net worth at least doubled
+ *
+ * @return the player's current PlayerStatus
+ */
+ public PlayerStatus getStatus() {
+ // Calculate total net worth: current cash + portfolio value
+ BigDecimal totalNetWorth = this.money.add(this.portfolio.getNetWorth());
- // Check for SPECULATOR status: 20+ weeks AND net worth doubled
- if (weeksActive >= 20 && totalNetWorth.compareTo(this.startingMoney.multiply(new BigDecimal("2"))) >= 0) {
- return PlayerStatus.SPECULATOR;
- }
+ // Get number of weeks with trading activity
+ int weeksActive = this.transactionArchive.countDistinctWeeks();
- // Check for INVESTOR status: 10+ weeks AND net worth increased by 20%
- if (weeksActive >= 10 && totalNetWorth.compareTo(this.startingMoney.multiply(new BigDecimal("1.2"))) >= 0) {
- return PlayerStatus.INVESTOR;
- }
+ // Check for SPECULATOR status: 20+ weeks AND net worth doubled
+ if (weeksActive >= 20 && totalNetWorth.compareTo(this
+ .startingMoney.multiply(new BigDecimal("2"))) >= 0) {
+ return PlayerStatus.SPECULATOR;
+ }
- // Default: NOVICE
- return PlayerStatus.NOVICE;
+ // Check for INVESTOR status: 10+ weeks AND net worth increased by 20%
+ if (weeksActive >= 10 && totalNetWorth.compareTo(this
+ .startingMoney.multiply(new BigDecimal("1.2"))) >= 0) {
+ return PlayerStatus.INVESTOR;
}
+
+ // Default: NOVICE
+ return PlayerStatus.NOVICE;
+ }
}
diff --git a/src/main/java/Model/PlayerStatus.java b/src/main/java/Model/PlayerStatus.java
index cf14ea8..312e38f 100644
--- a/src/main/java/Model/PlayerStatus.java
+++ b/src/main/java/Model/PlayerStatus.java
@@ -1,25 +1,36 @@
package Model;
/**
- * Enum representing the player's status level based on trading activity and performance.
- *
- * Status levels:
- * - NOVICE: Starting level, no requirements
- * - INVESTOR: Traded for at least 10 weeks AND increased net worth by at least 20%
- * - SPECULATOR: Traded for at least 20 weeks AND doubled the net worth (100% increase)
+ * Enum representing the player's trading status level.
+ * Status levels are determined by trading activity (weeks with transactions)
+ * and net worth performance compared to starting capital.
+ * Levels:
+ * - NOVICE: Initial status (default)
+ * - INVESTOR: 10+ weeks of trading AND net worth increased by 20%
+ * - SPECULATOR: 20+ weeks of trading AND net worth doubled
*/
public enum PlayerStatus {
NOVICE("Novice"),
INVESTOR("Investor"),
SPECULATOR("Speculator");
- private final String displayName;
+ private final String displayName;
- PlayerStatus(String displayName) {
- this.displayName = displayName;
- }
+ /**
+ * Constructs a PlayerStatus with the given display name.
+ *
+ * @param displayName the name for UI display
+ */
+ PlayerStatus(String displayName) {
+ this.displayName = displayName;
+ }
- public String getDisplayName() {
- return displayName;
- }
+ /**
+ * Returns the display name for this status.
+ *
+ * @return the display name
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
}
diff --git a/src/main/java/Model/Portfolio.java b/src/main/java/Model/Portfolio.java
index ba050e7..7c9e38d 100644
--- a/src/main/java/Model/Portfolio.java
+++ b/src/main/java/Model/Portfolio.java
@@ -1,54 +1,104 @@
package Model;
+
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
+/**
+ * Represents a portfolio of shares held by a player.
+ * A Portfolio manages a collection of Share objects and provides methods to add,
+ * remove, and query holdings. It can calculate the total net worth based on current
+ * market prices.
+ */
public class Portfolio {
- private final List shares;
+ private final List shares;
- public Portfolio() {
- this.shares = new ArrayList<>();
- }
+ /**
+ * Constructs a new empty Portfolio.
+ */
+ public Portfolio() {
+ this.shares = new ArrayList<>();
+ }
- public boolean addShare(Share share) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
- return shares.add(share);
+ /**
+ * Adds a share to the portfolio.
+ *
+ * @param share the share to add. Cannot be null
+ * @return true if the share was added successfully
+ * @throws IllegalArgumentException if share is null
+ */
+ public boolean addShare(Share share) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
}
+ return shares.add(share);
+ }
- public boolean removeShare(Share share) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
- return shares.remove(share);
+ /**
+ * Removes a share from the portfolio.
+ *
+ * @param share the share to remove. Cannot be null
+ * @return true if the share was removed; false if it was not in the portfolio
+ * @throws IllegalArgumentException if share is null
+ */
+ public boolean removeShare(Share share) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
}
+ return shares.remove(share);
+ }
- public List getShares() {
- return new ArrayList<>(shares);
- }
+ /**
+ * Returns a defensive copy of all shares in the portfolio.
+ *
+ * @return a copy of the shares list
+ */
+ public List getShares() {
+ return new ArrayList<>(shares);
+ }
- public List getShares(String symbol) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or blank");
- }
- return shares.stream()
- .filter(share -> share.getStock().getSymbol().equals(symbol))
- .toList();
+ /**
+ * Returns all shares in the portfolio for a specific stock symbol.
+ *
+ * @param symbol the stock symbol to filter by. Cannot be null or blank
+ * @return a list of shares matching the symbol
+ * @throws IllegalArgumentException if symbol is null or blank
+ */
+ public List getShares(String symbol) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
}
+ return shares.stream()
+ .filter(share -> share.getStock().getSymbol().equals(symbol))
+ .toList();
+ }
- public boolean contains(Share share) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
- return shares.contains(share);
+ /**
+ * Checks if the portfolio contains a specific share.
+ *
+ * @param share the share to check for. Cannot be null
+ * @return true if the share is in the portfolio; false otherwise
+ * @throws IllegalArgumentException if share is null
+ */
+ public boolean contains(Share share) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
}
+ return shares.contains(share);
+ }
- public BigDecimal getNetWorth() {
- return shares.stream()
- .map(share -> new SaleCalculator(share).calculateTotal())
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- }
+ /**
+ * Calculates the total net worth of the portfolio based on current market prices.
+ * The net worth is the sum of all shares valued at their current sales prices,
+ * minus any transaction costs (commissions and taxes).
+ *
+ * @return the total net worth of all holdings
+ */
+ public BigDecimal getNetWorth() {
+ return shares.stream()
+ .map(share -> new SaleCalculator(share).calculateTotal())
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
}
diff --git a/src/main/java/Model/Purchase.java b/src/main/java/Model/Purchase.java
index 33c41e5..b81f655 100644
--- a/src/main/java/Model/Purchase.java
+++ b/src/main/java/Model/Purchase.java
@@ -1,32 +1,53 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Represents a stock purchase transaction.
+ * A Purchase transaction represents a player buying shares. When committed,
+ * it deducts money from the player's account, adds the shares to their portfolio,
+ * and records the transaction in the archive.
+ */
public class Purchase extends Transaction {
- public Purchase(Share share, int week) {
- super(share, week, new PurchaseCalculator(share));
- }
+ /**
+ * Constructs a new Purchase transaction.
+ *
+ * @param share the share to purchase. Cannot be null
+ * @param week the week number when the purchase occurs. Must be at least 1
+ */
+ public Purchase(Share share, int week) {
+ super(share, week, new PurchaseCalculator(share));
+ }
- @Override
- public void commit(Player player) {
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
- if (isCommitted()) {
- throw new IllegalStateException("Purchase has already been committed");
- }
+ /**
+ * Commits this purchase transaction, deducting funds and adding shares to the portfolio.
+ *
+ * @param player the player executing the purchase. Cannot be null
+ * @throws IllegalArgumentException if player is null
+ * @throws IllegalStateException if purchase has already been committed or if
+ * the player has insufficient funds
+ */
+ @Override
+ public void commit(Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
+ if (isCommitted()) {
+ throw new IllegalStateException("Purchase has already been committed");
+ }
- BigDecimal price = this.getCalculator().calculateTotal();
+ BigDecimal price = this.getCalculator().calculateTotal();
- if (player.getMoney().compareTo(price) < 0) {
- throw new IllegalStateException(
- "Insufficient funds: required " + price + ", available " + player.getMoney()
- );
- }
+ if (player.getMoney().compareTo(price) < 0) {
+ throw new IllegalStateException(
+ "Insufficient funds: required " + price + ", available " + player.getMoney()
+ );
+ }
- player.withdrawMoney(price);
- player.getPortfolio().addShare(this.getShare());
- player.getTransactionArchive().add(this);
+ player.withdrawMoney(price);
+ player.getPortfolio().addShare(this.getShare());
+ player.getTransactionArchive().add(this);
- this.committed = true;
- }
+ this.committed = true;
+ }
}
diff --git a/src/main/java/Model/PurchaseCalculator.java b/src/main/java/Model/PurchaseCalculator.java
index 6a29413..e2a7482 100644
--- a/src/main/java/Model/PurchaseCalculator.java
+++ b/src/main/java/Model/PurchaseCalculator.java
@@ -1,30 +1,64 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Calculates costs and totals for purchase transactions.
+ * A PurchaseCalculator computes the gross cost (quantity × price), applies
+ * a commission fee, and produces the total transaction cost.
+ */
public class PurchaseCalculator implements TransactionCalculator {
- private BigDecimal purchasePrice;
- private BigDecimal quantity;
-
- public PurchaseCalculator(Share share) {
- this.purchasePrice = share.getPurchasePrice();
- this.quantity = share.getQuantity();
- }
+ private final BigDecimal purchasePrice;
+ private final BigDecimal quantity;
+
+ /**
+ * Constructs a PurchaseCalculator for the given share.
+ *
+ * @param share the share being purchased. Cannot be null
+ */
+ public PurchaseCalculator(Share share) {
+ this.purchasePrice = share.getPurchasePrice();
+ this.quantity = share.getQuantity();
+ }
- public BigDecimal calculateGross() {
- return this.purchasePrice.multiply(this.quantity);
- }
+ /**
+ * Calculates the gross cost without fees (quantity × purchase price).
+ *
+ * @return the gross cost
+ */
+ @Override
+ public BigDecimal calculateGross() {
+ return this.purchasePrice.multiply(this.quantity);
+ }
- public BigDecimal calculateCommission() {
- BigDecimal rate = new BigDecimal("0.005");
- return calculateGross().multiply(rate);
- }
+ /**
+ * Calculates the purchase commission (0.5% of gross).
+ *
+ * @return the commission fee
+ */
+ @Override
+ public BigDecimal calculateCommission() {
+ BigDecimal rate = new BigDecimal("0.005");
+ return calculateGross().multiply(rate);
+ }
- public BigDecimal calculateTax() {
- BigDecimal tax = new BigDecimal("0");
- return tax;
- }
+ /**
+ * Calculates the tax on purchase (always 0 for purchases).
+ *
+ * @return zero
+ */
+ @Override
+ public BigDecimal calculateTax() {
+ return BigDecimal.ZERO;
+ }
- public BigDecimal calculateTotal() {
- return calculateGross().add(calculateCommission()).add(calculateTax());
- }
+ /**
+ * Calculates the total purchase cost (gross + commission + tax).
+ *
+ * @return the total transaction cost
+ */
+ @Override
+ public BigDecimal calculateTotal() {
+ return calculateGross().add(calculateCommission()).add(calculateTax());
+ }
}
diff --git a/src/main/java/Model/Sale.java b/src/main/java/Model/Sale.java
index 7653df4..8b2e05e 100644
--- a/src/main/java/Model/Sale.java
+++ b/src/main/java/Model/Sale.java
@@ -1,31 +1,52 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Represents a stock sale transaction.
+ * A Sale transaction represents a player selling shares from their portfolio.
+ * When committed, it adds money to the player's account, removes the shares from
+ * their portfolio, and records the transaction in the archive.
+ */
public class Sale extends Transaction {
- public Sale(Share share, int week) {
- super(share, week, new SaleCalculator(share));
- }
+ /**
+ * Constructs a new Sale transaction.
+ *
+ * @param share the share to sell. Cannot be null
+ * @param week the week number when the sale occurs. Must be at least 1
+ */
+ public Sale(Share share, int week) {
+ super(share, week, new SaleCalculator(share));
+ }
- @Override
- public void commit(Player player) {
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
- if (isCommitted()) {
- throw new IllegalStateException("Sale has already been committed");
- }
- if (!player.getPortfolio().contains(this.getShare())) {
- throw new IllegalStateException(
- "Share not found in player's portfolio: " + this.getShare().getStock().getSymbol()
- );
- }
+ /**
+ * Commits this sale transaction, crediting funds and removing shares from the portfolio.
+ *
+ * @param player the player executing the sale. Cannot be null
+ * @throws IllegalArgumentException if player is null
+ * @throws IllegalStateException if sale has already been committed or if the
+ * player does not own the share being sold
+ */
+ @Override
+ public void commit(Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
+ if (isCommitted()) {
+ throw new IllegalStateException("Sale has already been committed");
+ }
+ if (!player.getPortfolio().contains(this.getShare())) {
+ throw new IllegalStateException(
+ "Share not found in player's portfolio: " + this.getShare().getStock().getSymbol()
+ );
+ }
- BigDecimal price = getCalculator().calculateTotal();
+ BigDecimal price = getCalculator().calculateTotal();
- player.addMoney(price);
- player.getPortfolio().removeShare(this.getShare());
- player.getTransactionArchive().add(this);
+ player.addMoney(price);
+ player.getPortfolio().removeShare(this.getShare());
+ player.getTransactionArchive().add(this);
- this.committed = true;
- }
+ this.committed = true;
+ }
}
diff --git a/src/main/java/Model/SaleCalculator.java b/src/main/java/Model/SaleCalculator.java
index 27fd394..271225a 100644
--- a/src/main/java/Model/SaleCalculator.java
+++ b/src/main/java/Model/SaleCalculator.java
@@ -1,36 +1,72 @@
package Model;
+
import java.math.BigDecimal;
-public class SaleCalculator implements TransactionCalculator{
- private BigDecimal purchasePrice;
- private BigDecimal salesPrice;
- private BigDecimal quantity;
-
- public SaleCalculator(Share share) {
- this.purchasePrice = share.getPurchasePrice();
- this.salesPrice = share.getStock().getSalesPrice();
- this.quantity = share.getQuantity();
- }
-
- public BigDecimal calculateGross() {
- return this.salesPrice.multiply(this.quantity);
- }
-
- public BigDecimal calculateCommission() {
- BigDecimal rate = new BigDecimal("0.01");
- return calculateGross().multiply(rate);
- }
-
- public BigDecimal calculateTax() {
- BigDecimal sellingCost = this.purchasePrice.multiply(this.quantity);
- BigDecimal profit = calculateGross().subtract(calculateCommission()).subtract(sellingCost);
- BigDecimal rate = new BigDecimal("0.3");
- return profit.multiply(rate);
- }
-
- public BigDecimal calculateTotal() {
- return calculateGross().subtract(calculateCommission()).subtract(calculateTax());
- }
+/**
+ * Calculates costs, taxes, and proceeds for sale transactions.
+ * A SaleCalculator computes the gross revenue (quantity × sales price), applies
+ * a commission fee, calculates capital gains tax, and produces the net proceeds.
+ */
+public class SaleCalculator implements TransactionCalculator {
+ private final BigDecimal purchasePrice;
+ private final BigDecimal salesPrice;
+ private final BigDecimal quantity;
+
+ /**
+ * Constructs a SaleCalculator for the given share.
+ *
+ * @param share the share being sold. Cannot be null
+ */
+ public SaleCalculator(Share share) {
+ this.purchasePrice = share.getPurchasePrice();
+ this.salesPrice = share.getStock().getSalesPrice();
+ this.quantity = share.getQuantity();
+ }
+
+ /**
+ * Calculates the gross revenue (quantity × sales price).
+ *
+ * @return the gross revenue
+ */
+ @Override
+ public BigDecimal calculateGross() {
+ return this.salesPrice.multiply(this.quantity);
+ }
+
+ /**
+ * Calculates the sale commission (1% of gross).
+ *
+ * @return the commission fee
+ */
+ @Override
+ public BigDecimal calculateCommission() {
+ BigDecimal rate = new BigDecimal("0.01");
+ return calculateGross().multiply(rate);
+ }
+
+ /**
+ * Calculates the capital gains tax (30% of profit).
+ * Profit is calculated as: revenue - commission - original purchase cost.
+ *
+ * @return the capital gains tax
+ */
+ @Override
+ public BigDecimal calculateTax() {
+ BigDecimal sellingCost = this.purchasePrice.multiply(this.quantity);
+ BigDecimal profit = calculateGross().subtract(calculateCommission()).subtract(sellingCost);
+ BigDecimal rate = new BigDecimal("0.3");
+ return profit.multiply(rate);
+ }
+
+ /**
+ * Calculates the net proceeds (gross - commission - tax).
+ *
+ * @return the total amount credited to the player
+ */
+ @Override
+ public BigDecimal calculateTotal() {
+ return calculateGross().subtract(calculateCommission()).subtract(calculateTax());
+ }
diff --git a/src/main/java/Model/Share.java b/src/main/java/Model/Share.java
index 92d6101..05a4c08 100644
--- a/src/main/java/Model/Share.java
+++ b/src/main/java/Model/Share.java
@@ -1,44 +1,73 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Represents a share holding of a stock.
+ * A Share encapsulates a quantity of a stock purchased at a specific price.
+ * It provides information about the stock and the purchase terms.
+ */
public class Share {
- private final Stock stock;
- private final BigDecimal quantity;
- private final BigDecimal purchasePrice;
-
- public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) {
- if (stock == null) {
- throw new IllegalArgumentException("Stock cannot be null");
- }
- if (quantity == null) {
- throw new IllegalArgumentException("Quantity cannot be null");
- }
- if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
- throw new IllegalArgumentException("Quantity must be greater than zero");
- }
- if (purchasePrice == null) {
- throw new IllegalArgumentException("Purchase price cannot be null");
- }
- if (purchasePrice.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Purchase price cannot be negative");
- }
-
- this.stock = stock;
- this.quantity = quantity;
- this.purchasePrice = purchasePrice;
- }
+ private final Stock stock;
+ private final BigDecimal quantity;
+ private final BigDecimal purchasePrice;
- public Stock getStock() {
- return stock;
+ /**
+ * Constructs a new Share with the specified stock, quantity, and purchase price.
+ *
+ * @param stock the Stock being held. Cannot be null
+ * @param quantity the number of shares held. Must be greater than zero
+ * @param purchasePrice the price per share at purchase. Cannot be null or negative
+ * @throws IllegalArgumentException if any parameter is invalid
+ */
+ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock cannot be null");
+ }
+ if (quantity == null) {
+ throw new IllegalArgumentException("Quantity cannot be null");
+ }
+ if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Quantity must be greater than zero");
}
-
- public BigDecimal getQuantity() {
- return quantity;
+ if (purchasePrice == null) {
+ throw new IllegalArgumentException("Purchase price cannot be null");
}
-
- public BigDecimal getPurchasePrice() {
- return purchasePrice;
+ if (purchasePrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Purchase price cannot be negative");
}
+
+ this.stock = stock;
+ this.quantity = quantity;
+ this.purchasePrice = purchasePrice;
+ }
+
+ /**
+ * Returns the Stock associated with this share.
+ *
+ * @return the stock being held
+ */
+ public Stock getStock() {
+ return stock;
+ }
+
+ /**
+ * Returns the quantity of shares held.
+ *
+ * @return the number of shares
+ */
+ public BigDecimal getQuantity() {
+ return quantity;
+ }
+
+ /**
+ * Returns the price per share at the time of purchase.
+ *
+ * @return the purchase price
+ */
+ public BigDecimal getPurchasePrice() {
+ return purchasePrice;
+ }
}
diff --git a/src/main/java/Model/Stock.java b/src/main/java/Model/Stock.java
index 52c1fd2..3c6866e 100644
--- a/src/main/java/Model/Stock.java
+++ b/src/main/java/Model/Stock.java
@@ -1,77 +1,118 @@
package Model;
+
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
+/**
+ * Represents a stock traded on the exchange.
+ * A Stock tracks the company's trading symbol, company name, and historical price
+ * data. Prices are maintained in chronological order, with the latest price being
+ * the current sales price.
+ */
public class Stock {
- private final String symbol;
- private final String company;
- private final List prices;
-
- public Stock(String symbol, String company, 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 name cannot be null or blank");
- }
- if (salesPrice == null) {
- throw new IllegalArgumentException("Initial sales price cannot be null");
- }
- if (salesPrice.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Initial sales price cannot be negative");
- }
+ private final String symbol;
+ private final String company;
+ private final List prices;
- this.symbol = symbol;
- this.company = company;
- this.prices = new ArrayList<>();
- this.prices.add(salesPrice);
+ /**
+ * Constructs a new Stock with the given symbol, company name, and initial sales price.
+ *
+ * @param symbol the unique ticker symbol (e.g., "AAPL"). Cannot be null or blank
+ * @param company the company name. Cannot be null or blank
+ * @param salesPrice the initial sales price. Cannot be null or negative
+ * @throws IllegalArgumentException if any parameter is invalid
+ */
+ public Stock(String symbol, String company, BigDecimal salesPrice) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
}
-
- public String getSymbol() {
- return symbol;
+ if (company == null || company.isBlank()) {
+ throw new IllegalArgumentException("Company name cannot be null or blank");
}
-
- public String getCompany() {
- return company;
+ if (salesPrice == null) {
+ throw new IllegalArgumentException("Initial sales price cannot be null");
}
-
- public BigDecimal getSalesPrice() {
- return prices.get(prices.size() - 1);
- }
-
- public void addNewSalesPrice(BigDecimal price) {
- if (price == null) {
- throw new IllegalArgumentException("Price cannot be null");
- }
- if (price.compareTo(BigDecimal.ZERO) < 0) {
- throw new IllegalArgumentException("Price cannot be negative");
- }
- prices.add(price);
+ if (salesPrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Initial sales price cannot be negative");
}
- public List getHistoricalPrices() {
- return new ArrayList<>(prices); // returnerer en kopi for å beskytte selve listen
- }
+ this.symbol = symbol;
+ this.company = company;
+ this.prices = new ArrayList<>();
+ this.prices.add(salesPrice);
+ }
- public BigDecimal getHighestPrice() {
- return prices.stream()
- .reduce(prices.get(0), BigDecimal::max);
- }
+ /**
+ * Returns the ticker symbol of this stock.
+ *
+ * @return the stock symbol
+ */
+ public String getSymbol() {
+ return symbol;
+ }
+
+ /**
+ * Returns the company name associated with this stock.
+ *
+ * @return the company name
+ */
+ public String getCompany() {
+ return company;
+ }
- public BigDecimal getLowestPrice() {
- return prices.stream()
- .reduce(prices.get(0), BigDecimal::min);
+ /**
+ * Returns the current (most recent) sales price of this stock.
+ *
+ * @return the current sales price
+ */
+ public BigDecimal getSalesPrice() {
+ return prices.get(prices.size() - 1);
+ }
+
+ /**
+ * Records a new sales price for this stock.
+ *
+ * @param price the new sales price. Cannot be null or negative
+ * @throws IllegalArgumentException if price is invalid
+ */
+ public void addNewSalesPrice(BigDecimal price) {
+ if (price == null) {
+ throw new IllegalArgumentException("Price cannot be null");
+ }
+ if (price.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Price cannot be negative");
}
+ prices.add(price);
+ }
- public BigDecimal getLatestPriceChange() {
- if (prices.size() < 2) {
- return BigDecimal.ZERO;
- }
+ public List getHistoricalPrices() {
+ return new ArrayList<>(prices); // returns a copy to protect the list itself
+ }
- BigDecimal latest = prices.get(prices.size() - 1);
- BigDecimal previous = prices.get(prices.size() - 2);
- return latest.subtract(previous);
+ public BigDecimal getHighestPrice() {
+ return prices.stream()
+ .reduce(prices.get(0), BigDecimal::max);
+ }
+
+ public BigDecimal getLowestPrice() {
+ return prices.stream()
+ .reduce(prices.get(0), BigDecimal::min);
+ }
+
+ /**
+ * Method that gets the latest price change.
+ *
+ * @return returns the price change
+ */
+ public BigDecimal getLatestPriceChange() {
+ if (prices.size() < 2) {
+ return BigDecimal.ZERO;
}
+
+ BigDecimal latest = prices.get(prices.size() - 1);
+ BigDecimal previous = prices.get(prices.size() - 2);
+ return latest.subtract(previous);
+ }
}
diff --git a/src/main/java/Model/Transaction.java b/src/main/java/Model/Transaction.java
index de68854..a8eb4f1 100644
--- a/src/main/java/Model/Transaction.java
+++ b/src/main/java/Model/Transaction.java
@@ -1,42 +1,84 @@
package Model;
+
+/**
+ * Abstract base class for all transactions (purchases and sales).
+ * A Transaction represents a trading action taken by a player on a specific week.
+ * Transactions use the Strategy pattern via TransactionCalculator to compute costs
+ * and can be committed to apply their effects to the player's portfolio.
+ */
public abstract class Transaction {
- private Share share;
- private int week;
- private TransactionCalculator calculator;
- protected boolean committed;
-
- protected Transaction(Share share, int week, TransactionCalculator calculator) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
- if (week < 1) {
- throw new IllegalArgumentException("Week must be at least 1");
- }
- if (calculator == null) {
- throw new IllegalArgumentException("Calculator cannot be null");
- }
-
- this.share = share;
- this.week = week;
- this.calculator = calculator;
- }
+ private Share share;
+ private int week;
+ private TransactionCalculator calculator;
+ protected boolean committed;
- public Share getShare() {
- return this.share;
+ /**
+ * Constructs a Transaction for the specified share in the given week.
+ *
+ * @param share the share being traded. Cannot be null
+ * @param week the week number when the transaction occurs. Must be at least 1
+ * @param calculator the calculator for transaction costs. Cannot be null
+ * @throws IllegalArgumentException if any parameter is invalid
+ */
+ protected Transaction(Share share, int week, TransactionCalculator calculator) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
}
-
- public int getWeek() {
- return this.week;
+ if (week < 1) {
+ throw new IllegalArgumentException("Week must be at least 1");
}
-
- public TransactionCalculator getCalculator() {
- return this.calculator;
+ if (calculator == null) {
+ throw new IllegalArgumentException("Calculator cannot be null");
}
- public boolean isCommitted() {
- return this.committed;
- }
+ this.share = share;
+ this.week = week;
+ this.calculator = calculator;
+ }
+
+ /**
+ * Returns the share being traded in this transaction.
+ *
+ * @return the share
+ */
+ public Share getShare() {
+ return this.share;
+ }
+
+ /**
+ * Returns the week number when this transaction occurs.
+ *
+ * @return the week number
+ */
+ public int getWeek() {
+ return this.week;
+ }
+
+ /**
+ * Returns the calculator used to compute transaction costs.
+ *
+ * @return the transaction calculator
+ */
+ public TransactionCalculator getCalculator() {
+ return this.calculator;
+ }
+
+ /**
+ * Returns whether this transaction has been committed.
+ *
+ * @return true if committed; false otherwise
+ */
+ public boolean isCommitted() {
+ return this.committed;
+ }
- public abstract void commit(Player player);
+ /**
+ * Commits this transaction, applying its effects to the player's portfolio.
+ *
+ * @param player the player executing the transaction. Cannot be null
+ * @throws IllegalArgumentException if player is null
+ * @throws IllegalStateException if the transaction has already been committed
+ */
+ public abstract void commit(Player player);
}
diff --git a/src/main/java/Model/TransactionArchive.java b/src/main/java/Model/TransactionArchive.java
index 149e2be..4af5bff 100644
--- a/src/main/java/Model/TransactionArchive.java
+++ b/src/main/java/Model/TransactionArchive.java
@@ -1,45 +1,100 @@
package Model;
+
import java.util.ArrayList;
-import java.util.stream.Collectors;
import java.util.List;
+import java.util.stream.Collectors;
+/**
+ * Maintains a chronological archive of all transactions executed by a player.
+ * The TransactionArchive allows retrieval of transactions by week and type
+ * (purchase or sale), and provides statistics about trading activity.
+ */
public class TransactionArchive {
- private List transactions;
+ private final List transactions;
- public TransactionArchive() {
- this.transactions = new ArrayList<>();
- }
+ /**
+ * Constructs a new empty TransactionArchive.
+ */
+ public TransactionArchive() {
+ this.transactions = new ArrayList<>();
+ }
- public boolean add(Transaction transaction) {
- if (transaction == null) {
- throw new IllegalArgumentException("Should not be null"); // Eller NullPointerExeption?
- }
+ /**
+ * Adds a transaction to the archive.
+ *
+ * @param transaction the transaction to add. Cannot be null
+ * @return true if the transaction was added successfully
+ * @throws IllegalArgumentException if transaction is null
+ */
+ public boolean add(Transaction transaction) {
+ if (transaction == null) {
+ throw new IllegalArgumentException("Should not be null");
+ }
- return transactions.add(transaction);
+ return transactions.add(transaction);
+ }
- }
+ /**
+ * Checks if the archive is empty.
+ *
+ * @return true if no transactions have been recorded
+ */
+ public boolean isEmpty() {
+ return transactions.isEmpty();
+ }
- public boolean isEmpty() {
- return transactions.isEmpty();
- }
+ /**
+ * Returns all transactions that occurred in a specific week.
+ *
+ * @param week the week number
+ * @return a list of transactions in that week
+ */
+ public List getTransactions(int week) {
+ return transactions.stream()
+ .filter(transaction -> transaction.getWeek() == week).collect(Collectors.toList());
+ }
- public List getTransactions(int week) {
- return transactions.stream().filter(transaction -> transaction.getWeek() == week).collect(Collectors.toList());
- }
-
- public List getPurchase(int week) {
- return transactions.stream().filter(transaction -> transaction.getWeek() == week).filter(purchase -> purchase instanceof Purchase).map(transaction -> (Purchase) transaction).collect(Collectors.toList());
- }
+ /**
+ * Returns all purchases that occurred in a specific week.
+ *
+ * @param week the week number
+ * @return a list of purchases in that week
+ */
+ public List getPurchase(int week) {
+ return transactions.stream()
+ .filter(transaction -> transaction.getWeek() == week)
+ .filter(purchase -> purchase instanceof Purchase)
+ .map(transaction -> (Purchase) transaction).collect(Collectors.toList());
+ }
- public List getSale(int week) {
- return transactions.stream().filter(transaction -> transaction.getWeek() == week).filter(sale -> sale instanceof Sale).map(transaction -> (Sale) transaction).collect(Collectors.toList());
- }
+ /**
+ * Returns all sales that occurred in a specific week.
+ *
+ * @param week the week number
+ * @return a list of sales in that week
+ */
+ public List getSale(int week) {
+ return transactions.stream()
+ .filter(transaction -> transaction.getWeek() == week)
+ .filter(sale -> sale instanceof Sale)
+ .map(transaction -> (Sale) transaction).collect(Collectors.toList());
+ }
- public List getAllTransactions() {
- return new ArrayList<>(transactions);
- }
+ /**
+ * Returns a defensive copy of all transactions in the archive.
+ *
+ * @return a copy of the transaction list
+ */
+ public List getAllTransactions() {
+ return new ArrayList<>(transactions);
+ }
- public int countDistinctWeeks() {
- return (int) transactions.stream().map(Transaction::getWeek).distinct().count();
- }
+ /**
+ * Counts the number of distinct weeks with trading activity.
+ *
+ * @return the number of unique weeks represented in the archive
+ */
+ public int countDistinctWeeks() {
+ return (int) transactions.stream().map(Transaction::getWeek).distinct().count();
+ }
}
diff --git a/src/main/java/Model/TransactionCalculator.java b/src/main/java/Model/TransactionCalculator.java
index 560992d..2691e02 100644
--- a/src/main/java/Model/TransactionCalculator.java
+++ b/src/main/java/Model/TransactionCalculator.java
@@ -1,11 +1,40 @@
package Model;
+
import java.math.BigDecimal;
+/**
+ * Strategy interface for calculating transaction costs and proceeds.
+ * Implementations compute fees and totals for different transaction types
+ * (purchases and sales), following the Strategy design pattern.
+ */
public interface TransactionCalculator {
- // Methods in interface is automatically public abstract.
- BigDecimal calculateGross();
- BigDecimal calculateCommission();
- BigDecimal calculateTax();
- BigDecimal calculateTotal();
+ /**
+ * Calculates the base cost/revenue without fees.
+ *
+ * @return the gross amount
+ */
+ BigDecimal calculateGross();
+
+ /**
+ * Calculates the commission fee on the transaction.
+ *
+ * @return the commission amount
+ */
+ BigDecimal calculateCommission();
+
+ /**
+ * Calculates any applicable taxes on the transaction.
+ *
+ * @return the tax amount
+ */
+ BigDecimal calculateTax();
+
+ /**
+ * Calculates the final total after all fees and taxes.
+ *
+ * @return the total transaction amount
+ */
+ BigDecimal calculateTotal();
+
}
diff --git a/src/main/java/Model/TransactionFactory.java b/src/main/java/Model/TransactionFactory.java
index a3b8c9a..1f799d1 100644
--- a/src/main/java/Model/TransactionFactory.java
+++ b/src/main/java/Model/TransactionFactory.java
@@ -1,22 +1,33 @@
package Model;
/**
- * Factory for creating transaction objects
+ * Factory for creating transaction objects.
+ * Uses the Factory pattern to encapsulate transaction creation logic,
+ * allowing clients to create Purchase and Sale transactions without
+ * knowing implementation details.
*/
public class TransactionFactory {
- /**
- * Create purchase transaction
- */
- public static Transaction createPurchase(Share share, int week) {
- return new Purchase(share, week);
- }
+ /**
+ * Creates a new Purchase transaction.
+ *
+ * @param share the share to purchase. Cannot be null
+ * @param week the week when the purchase occurs. Must be at least 1
+ * @return a new Purchase transaction
+ */
+ public static Transaction createPurchase(Share share, int week) {
+ return new Purchase(share, week);
+ }
- /**
- * Create sale transaction
- */
- public static Transaction createSale(Share share, int week) {
- return new Sale(share, week);
- }
+ /**
+ * Creates a new Sale transaction.
+ *
+ * @param share the share to sell. Cannot be null
+ * @param week the week when the sale occurs. Must be at least 1
+ * @return a new Sale transaction
+ */
+ public static Transaction createSale(Share share, int week) {
+ return new Sale(share, week);
+ }
}
diff --git a/src/main/java/View/GameSetupScene.java b/src/main/java/View/GameSetupScene.java
index 0ed8ded..44052ca 100644
--- a/src/main/java/View/GameSetupScene.java
+++ b/src/main/java/View/GameSetupScene.java
@@ -1,6 +1,12 @@
package View;
-import javafx.geometry.Insets;
+import Controller.StockFileHandler;
+import Model.Exchange;
+import Model.Stock;
+import java.io.File;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.function.Consumer;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
@@ -8,220 +14,259 @@
import javafx.stage.FileChooser;
import javafx.stage.Stage;
-import Controller.StockFileHandler;
-import Model.Exchange;
-import Model.Stock;
-import java.io.File;
-import java.math.BigDecimal;
-import java.util.List;
-import java.util.function.Consumer;
+/**
+ * Displays the game setup scene where players configure their game parameters.
+ * Allows players to enter their name, starting capital, and select a stock data file
+ * before starting the main game. Notifies listeners when the game starts.
+ */
public class GameSetupScene {
- private Scene scene;
- private Consumer onGameStart;
- private File selectedFile;
- private Label fileLabel;
-
- public GameSetupScene(Consumer onGameStart) {
- this.onGameStart = onGameStart;
- this.scene = createScene();
+ private final Scene scene;
+ private final Consumer onGameStart;
+ private File selectedFile;
+ private Label fileLabel;
+
+ /**
+ * Constructs a GameSetupScene with a callback for when the game starts.
+ *
+ * @param onGameStart callback invoked when the player starts the game. Cannot be null
+ */
+ public GameSetupScene(Consumer onGameStart) {
+ this.onGameStart = onGameStart;
+ this.scene = createScene();
+ }
+
+ /**
+ * Creates the UI layout for the setup scene.
+ *
+ * @return the constructed Scene
+ */
+ private Scene createScene() {
+
+ // Card (centered cream box)
+ VBox card = new VBox(20);
+ card.getStyleClass().add("card");
+ card.setMaxWidth(560);
+ card.setMinWidth(400);
+
+ // Title
+ Label titleLabel = new Label("Stock Trading Game");
+ titleLabel.getStyleClass().add("title-label");
+
+ // Spacer below title
+ Region titleSpacer = new Region();
+ titleSpacer.setMinHeight(6);
+
+ // Player name row
+ HBox nameBox = new HBox(16);
+ nameBox.setAlignment(Pos.CENTER_LEFT);
+ nameBox.setMaxWidth(Double.MAX_VALUE);
+
+ Label nameLabel = new Label("Player:");
+ nameLabel.setMinWidth(130);
+ nameLabel.getStyleClass().add("form-label");
+
+ TextField nameField = new TextField();
+ nameField.setPromptText("Enter your name");
+ HBox.setHgrow(nameField, Priority.ALWAYS);
+
+ nameBox.getChildren().addAll(nameLabel, nameField);
+
+ // Starting capital row
+ HBox capitalBox = new HBox(16);
+ capitalBox.setAlignment(Pos.CENTER_LEFT);
+ capitalBox.setMaxWidth(Double.MAX_VALUE);
+
+ Label capitalLabel = new Label("Start Capital:");
+ capitalLabel.setMinWidth(130);
+ capitalLabel.getStyleClass().add("form-label");
+
+ TextField capitalField = new TextField();
+ capitalField.setPromptText("e.g. 10000");
+ HBox.setHgrow(capitalField, Priority.ALWAYS);
+
+ capitalBox.getChildren().addAll(capitalLabel, capitalField);
+
+ // File selection row
+ HBox fileBox = new HBox(16);
+ fileBox.setAlignment(Pos.CENTER_LEFT);
+ fileBox.setMaxWidth(Double.MAX_VALUE);
+
+ Label fileSelectLabel = new Label("Stock Data File:");
+ fileSelectLabel.setMinWidth(130);
+ fileSelectLabel.getStyleClass().add("form-label");
+
+ fileLabel = new Label("No file selected");
+ fileLabel.getStyleClass().add("file-label");
+ HBox.setHgrow(fileLabel, Priority.ALWAYS);
+
+ Button browseButton = new Button("Browse");
+ browseButton.getStyleClass().add("browse-button");
+ browseButton.setOnAction(e -> selectFile());
+
+ fileBox.getChildren().addAll(fileSelectLabel, fileLabel, browseButton);
+
+ // Spacer before buttons
+ Region buttonSpacer = new Region();
+ buttonSpacer.setMinHeight(10);
+
+ // Button row
+ HBox buttonBox = new HBox(12);
+ buttonBox.setAlignment(Pos.CENTER_LEFT);
+ buttonBox.setMaxWidth(Double.MAX_VALUE);
+
+ Button startButton = new Button("Start Game");
+ startButton.getStyleClass().add("start-button");
+ startButton.setMaxWidth(Double.MAX_VALUE);
+ startButton.setOnAction(e -> handleStart(nameField, capitalField));
+ HBox.setHgrow(startButton, Priority.ALWAYS);
+
+ Button exitButton = new Button("Exit");
+ exitButton.getStyleClass().add("exit-button");
+ exitButton.setMaxWidth(Double.MAX_VALUE);
+ exitButton.setOnAction(e -> System.exit(0));
+ HBox.setHgrow(exitButton, Priority.ALWAYS);
+
+ buttonBox.getChildren().addAll(startButton, exitButton);
+
+ card.getChildren().addAll(
+ titleLabel,
+ titleSpacer,
+ nameBox,
+ capitalBox,
+ fileBox,
+ buttonSpacer,
+ buttonBox
+ );
+
+ // Centers the root
+ VBox root = new VBox(card);
+ root.setAlignment(Pos.CENTER);
+ root.setFillWidth(false);
+
+ Scene scene = new Scene(root, 800, 550);
+ scene.getStylesheets().add(
+ getClass().getResource("/Style/Global.css").toExternalForm()
+ );
+
+ return scene;
+ }
+
+ /**
+ * Validates inputs and starts the game when the Start button is clicked.
+ *
+ * @param nameField the text field containing the player name
+ * @param capitalField the text field containing the starting capital
+ */
+ private void handleStart(TextField nameField, TextField capitalField) {
+ String name = nameField.getText().trim();
+ String capitalStr = capitalField.getText().trim();
+
+ if (name.isEmpty()) {
+ showAlert("Validation Error", "Please enter a player name");
+ return;
}
- private Scene createScene() {
-
- // Card (centered cream box)
- VBox card = new VBox(20);
- card.getStyleClass().add("card");
- card.setMaxWidth(560);
- card.setMinWidth(400);
-
- // Title
- Label titleLabel = new Label("Stock Trading Game");
- titleLabel.getStyleClass().add("title-label");
-
- // Spacer below title
- Region titleSpacer = new Region();
- titleSpacer.setMinHeight(6);
-
- // Player name row
- HBox nameBox = new HBox(16);
- nameBox.setAlignment(Pos.CENTER_LEFT);
- nameBox.setMaxWidth(Double.MAX_VALUE);
-
- Label nameLabel = new Label("Player:");
- nameLabel.setMinWidth(130);
- nameLabel.getStyleClass().add("form-label");
-
- TextField nameField = new TextField();
- nameField.setPromptText("Enter your name");
- HBox.setHgrow(nameField, Priority.ALWAYS);
-
- nameBox.getChildren().addAll(nameLabel, nameField);
-
- // Starting capital row
- HBox capitalBox = new HBox(16);
- capitalBox.setAlignment(Pos.CENTER_LEFT);
- capitalBox.setMaxWidth(Double.MAX_VALUE);
-
- Label capitalLabel = new Label("Start Capital:");
- capitalLabel.setMinWidth(130);
- capitalLabel.getStyleClass().add("form-label");
-
- TextField capitalField = new TextField();
- capitalField.setPromptText("e.g. 10000");
- HBox.setHgrow(capitalField, Priority.ALWAYS);
-
- capitalBox.getChildren().addAll(capitalLabel, capitalField);
-
- // File selection row
- HBox fileBox = new HBox(16);
- fileBox.setAlignment(Pos.CENTER_LEFT);
- fileBox.setMaxWidth(Double.MAX_VALUE);
-
- Label fileSelectLabel = new Label("Stock Data File:");
- fileSelectLabel.setMinWidth(130);
- fileSelectLabel.getStyleClass().add("form-label");
-
- fileLabel = new Label("No file selected");
- fileLabel.getStyleClass().add("file-label");
- HBox.setHgrow(fileLabel, Priority.ALWAYS);
-
- Button browseButton = new Button("Browse");
- browseButton.getStyleClass().add("browse-button");
- browseButton.setOnAction(e -> selectFile());
-
- fileBox.getChildren().addAll(fileSelectLabel, fileLabel, browseButton);
-
- // Spacer before buttons
- Region buttonSpacer = new Region();
- buttonSpacer.setMinHeight(10);
-
- // Button row — buttons fill full card width equally
- HBox buttonBox = new HBox(12);
- buttonBox.setAlignment(Pos.CENTER_LEFT);
- buttonBox.setMaxWidth(Double.MAX_VALUE);
-
- Button startButton = new Button("Start Game");
- startButton.getStyleClass().add("start-button");
- startButton.setMaxWidth(Double.MAX_VALUE);
- startButton.setOnAction(e -> handleStart(nameField, capitalField));
- HBox.setHgrow(startButton, Priority.ALWAYS);
-
- Button exitButton = new Button("Exit");
- exitButton.getStyleClass().add("exit-button");
- exitButton.setMaxWidth(Double.MAX_VALUE);
- exitButton.setOnAction(e -> System.exit(0));
- HBox.setHgrow(exitButton, Priority.ALWAYS);
-
- buttonBox.getChildren().addAll(startButton, exitButton);
-
- card.getChildren().addAll(
- titleLabel,
- titleSpacer,
- nameBox,
- capitalBox,
- fileBox,
- buttonSpacer,
- buttonBox
- );
-
- // Center the card without stretching it vertically
- VBox root = new VBox(card);
- root.setAlignment(Pos.CENTER);
- root.setFillWidth(false);
-
- Scene scene = new Scene(root, 800, 550);
- scene.getStylesheets().add(
- getClass().getResource("/Style/Global.css").toExternalForm()
- );
-
- return scene;
+ if (capitalStr.isEmpty()) {
+ showAlert("Validation Error", "Please enter starting capital");
+ return;
}
- private void handleStart(TextField nameField, TextField capitalField) {
- String name = nameField.getText().trim();
- String capitalStr = capitalField.getText().trim();
-
- if (name.isEmpty()) {
- showAlert("Validation Error", "Please enter a player name");
- return;
- }
-
- if (capitalStr.isEmpty()) {
- showAlert("Validation Error", "Please enter starting capital");
- return;
- }
-
- if (selectedFile == null) {
- showAlert("Validation Error", "Please select a stock data file");
- return;
- }
-
- try {
- BigDecimal capital = new BigDecimal(capitalStr);
-
- if (capital.compareTo(BigDecimal.ZERO) <= 0) {
- showAlert("Validation Error", "Starting capital must be greater than 0");
- return;
- }
-
- StockFileHandler fileHandler = new StockFileHandler();
- List stocks = fileHandler.loadStocksFromFile(selectedFile.getAbsolutePath());
-
- if (stocks.isEmpty()) {
- showAlert("Error", "No stocks found in the selected file");
- return;
- }
-
- Exchange exchange = new Exchange("Main Exchange", stocks);
- onGameStart.accept(new StartGameData(name, capital, exchange));
-
- } catch (NumberFormatException ex) {
- showAlert("Validation Error", "Starting capital must be a valid number");
- } catch (Exception ex) {
- showAlert("Error", "Failed to load file: " + ex.getMessage());
- }
+ if (selectedFile == null) {
+ showAlert("Validation Error", "Please select a stock data file");
+ return;
}
- private void selectFile() {
- FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle("Select Stock Data File");
- fileChooser.getExtensionFilters().addAll(
- new FileChooser.ExtensionFilter("CSV Files", "*.csv"),
- new FileChooser.ExtensionFilter("All Files", "*.*")
- );
+ try {
+ BigDecimal capital = new BigDecimal(capitalStr);
- File file = fileChooser.showOpenDialog(new Stage());
+ if (capital.compareTo(BigDecimal.ZERO) <= 0) {
+ showAlert("Validation Error", "Starting capital must be greater than 0");
+ return;
+ }
- if (file != null) {
- selectedFile = file;
- fileLabel.setText(file.getName());
- }
- }
+ StockFileHandler fileHandler = new StockFileHandler();
+ List stocks = fileHandler.loadStocksFromFile(selectedFile.getAbsolutePath());
- private void showAlert(String title, String message) {
- Alert alert = new Alert(Alert.AlertType.WARNING);
- alert.setTitle(title);
- alert.setHeaderText(null);
- alert.setContentText(message);
- alert.showAndWait();
- }
+ if (stocks.isEmpty()) {
+ showAlert("Error", "No stocks found in the selected file");
+ return;
+ }
- public Scene getScene() {
- return scene;
- }
+ Exchange exchange = new Exchange("Main Exchange", stocks);
+ onGameStart.accept(new StartGameData(name, capital, exchange));
- public static class StartGameData {
- public final String playerName;
- public final BigDecimal startingCapital;
- public final Exchange exchange;
-
- public StartGameData(String playerName, BigDecimal startingCapital, Exchange exchange) {
- this.playerName = playerName;
- this.startingCapital = startingCapital;
- this.exchange = exchange;
- }
+ } catch (NumberFormatException ex) {
+ showAlert("Validation Error", "Starting capital must be a valid number");
+ } catch (Exception ex) {
+ showAlert("Error", "Failed to load file: " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Opens a file chooser dialog to select a stock data file.
+ */
+ private void selectFile() {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Select Stock Data File");
+ fileChooser.getExtensionFilters().addAll(
+ new FileChooser.ExtensionFilter("CSV Files", "*.csv"),
+ new FileChooser.ExtensionFilter("All Files", "*.*")
+ );
+
+ File file = fileChooser.showOpenDialog(new Stage());
+
+ if (file != null) {
+ selectedFile = file;
+ fileLabel.setText(file.getName());
+ }
+ }
+
+ /**
+ * Displays an alert dialog with the given title and message.
+ *
+ * @param title the dialog title
+ * @param message the alert message
+ */
+ private void showAlert(String title, String message) {
+ Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setTitle(title);
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
+ /**
+ * Returns the Scene for display in the application window.
+ *
+ * @return the setup scene
+ */
+ public Scene getScene() {
+ return scene;
+ }
+
+ /**
+ * Data class encapsulating game setup parameters.
+ * Contains the exchange configured with stocks, player name, and starting capital.
+ */
+ public static class StartGameData {
+ public final String playerName;
+ public final BigDecimal startingCapital;
+ public final Exchange exchange;
+
+ /**
+ * StartGameData method that includes playername, startingcapital and exchange.
+ *
+ * @param playerName the player's name
+ * @param startingCapital startingcapital of the player
+ * @param exchange exchange class
+ */
+ public StartGameData(String playerName, BigDecimal startingCapital, Exchange exchange) {
+ this.playerName = playerName;
+ this.startingCapital = startingCapital;
+ this.exchange = exchange;
}
+ }
}
diff --git a/src/main/java/View/Launcher.java b/src/main/java/View/Launcher.java
index 0dd5db2..fb7bc49 100644
--- a/src/main/java/View/Launcher.java
+++ b/src/main/java/View/Launcher.java
@@ -1,10 +1,17 @@
package View;
/**
- * Launcher class for the Stock Trading Game
+ * Entry point for launching the Stock Trading Game application.
+ * This launcher delegates to the main StockTradingGameApp class to start the JavaFX
+ * application. It provides a conventional main method entry point.
*/
public class Launcher {
- public static void main(String[] args) {
- StockTradingGameApp.main(args);
- }
+ /**
+ * Main method to launch the application.
+ *
+ * @param args command-line arguments (not used)
+ */
+ public static void main(String[] args) {
+ StockTradingGameApp.main(args);
+ }
}
diff --git a/src/main/java/View/MainGameScene.java b/src/main/java/View/MainGameScene.java
index 749d2da..6f4cd27 100644
--- a/src/main/java/View/MainGameScene.java
+++ b/src/main/java/View/MainGameScene.java
@@ -1,656 +1,693 @@
package View;
-import javafx.geometry.Insets;
+import Model.*;
+import java.math.BigDecimal;
+import java.util.List;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import Model.*;
-import java.math.BigDecimal;
-import java.util.List;
/**
- * Main game UI. Implements ExchangeObserver so the view automatically refreshes
- * whenever the Exchange notifies it of a state change (week advance or trade).
+ * The main game user interface scene.
+ * Displays the game board with stocks, portfolio, trading interface, and transaction
+ * history. Implements ExchangeObserver to automatically refresh UI when the game state changes.
*/
public class MainGameScene implements ExchangeObserver {
- private Scene scene;
- private Exchange exchange;
- private Player player;
- private Runnable onExit;
- private Label statusLabel;
-
- // References to UI components that need refreshing
- private TableView portfolioTable;
- private ListView holdingsList;
- private TableView historyTable;
- private ComboBox weekFilterCombo;
-
- public MainGameScene(Exchange exchange, Player player, Runnable onExit) {
- this.exchange = exchange;
- this.player = player;
- this.onExit = onExit;
-
- // Register this view as an observer of the exchange
- this.exchange.addObserver(this);
-
- this.scene = createScene();
- }
-
- /**
- * Called automatically by Exchange whenever its state changes.
- * Refreshes all UI elements to reflect the latest data.
- */
- @Override
- public void onExchangeUpdated(Exchange exchange) {
- updateStatus();
- refreshAllUI();
- }
-
- private Scene createScene() {
- VBox root = new VBox(0);
-
- // TOP STATUS BAR
- HBox topBar = new HBox(12);
- topBar.getStyleClass().add("top-bar");
- topBar.setAlignment(Pos.CENTER_LEFT);
-
- statusLabel = new Label();
- statusLabel.getStyleClass().add("status-label");
- updateStatus();
-
- Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL);
- sep.setPrefHeight(20);
-
- Button nextWeekBtn = new Button("Next week");
- nextWeekBtn.getStyleClass().addAll("action-button");
- nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update
-
- Button exitBtn = new Button("Exit");
- exitBtn.getStyleClass().add("exit-button");
- exitBtn.setOnAction(e -> {
- if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) {
- exchange.removeObserver(this); // clean up before closing
- onExit.run();
- }
- });
-
- topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn);
-
- // MAIN CONTENT TABS
- TabPane tabs = new TabPane();
- tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
- tabs.getTabs().addAll(
- new Tab("Stocks", createStocksPanel()),
- new Tab("Portfolio", createPortfolioPanel()),
- new Tab("Trade", createTradePanel()),
- new Tab("History", createHistoryPanel())
- );
-
- VBox.setVgrow(tabs, Priority.ALWAYS);
- root.getChildren().addAll(topBar, tabs);
-
- Scene scene = new Scene(root, 1000, 700);
- scene.getStylesheets().add(
- getClass().getResource("/Style/Global.css").toExternalForm()
- );
- return scene;
- }
-
- private VBox createStocksPanel() {
- VBox panel = new VBox(10);
- panel.getStyleClass().add("content-area");
-
- HBox searchBox = new HBox(8);
- searchBox.setAlignment(Pos.CENTER_LEFT);
-
- TextField search = new TextField();
- search.setPromptText("Search symbol or company...");
- HBox.setHgrow(search, Priority.ALWAYS);
-
- Button searchBtn = new Button("Search");
- searchBtn.getStyleClass().add("action-button");
-
- ComboBox filter = new ComboBox<>();
- filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers"));
- filter.setValue("All");
-
- TableView table = new TableView<>();
- addStockColumns(table);
-
- Runnable loadStocks = () -> {
- List stocks;
- String filterVal = filter.getValue();
- String searchVal = search.getText().trim();
-
- if ("Gainers".equals(filterVal)) {
- stocks = exchange.getGainers(20);
- } else if ("Losers".equals(filterVal)) {
- stocks = exchange.getLosers(20);
- } else {
- stocks = exchange.findStocks(searchVal);
- }
-
- ObservableList data = FXCollections.observableArrayList();
- for (Stock s : stocks) {
- data.add(new StockRow(s));
- }
- table.setItems(data);
+ private final Scene scene;
+ private final Exchange exchange;
+ private final Player player;
+ private final Runnable onExit;
+ private Label statusLabel;
+
+ // References to UI components that need refreshing
+ private TableView portfolioTable;
+ private ListView holdingsList;
+ private TableView historyTable;
+ private ComboBox weekFilterCombo;
+
+ /**
+ * Constructs the main game scene.
+ *
+ * @param exchange the Exchange managing the game state. Cannot be null
+ * @param player the Player being displayed. Cannot be null
+ * @param onExit callback to run when exiting the game. Cannot be null
+ */
+ public MainGameScene(Exchange exchange, Player player, Runnable onExit) {
+ this.exchange = exchange;
+ this.player = player;
+ this.onExit = onExit;
+
+ // Register this view as an observer of the exchange
+ this.exchange.addObserver(this);
+
+ this.scene = createScene();
+ }
+
+ /**
+ * Callback invoked when the Exchange notifies observers of state changes.
+ *
+ * @param exchange the Exchange that changed
+ */
+ @Override
+ public void onExchangeUpdated(Exchange exchange) {
+ updateStatus();
+ refreshAllUI();
+ }
+
+ /**
+ * Creates the main game scene layout.
+ *
+ * @return the constructed Scene
+ */
+ private Scene createScene() {
+ VBox root = new VBox(0);
+
+ // TOP STATUS BAR
+ HBox topBar = new HBox(12);
+ topBar.getStyleClass().add("top-bar");
+ topBar.setAlignment(Pos.CENTER_LEFT);
+
+ statusLabel = new Label();
+ statusLabel.getStyleClass().add("status-label");
+ updateStatus();
+
+ Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL);
+ sep.setPrefHeight(20);
+
+ Button nextWeekBtn = new Button("Next week");
+ nextWeekBtn.getStyleClass().addAll("action-button");
+ nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update
+
+ Button exitBtn = new Button("Exit");
+ exitBtn.getStyleClass().add("exit-button");
+ exitBtn.setOnAction(e -> {
+ if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) {
+ exchange.removeObserver(this); // clean up before closing
+ onExit.run();
+ }
+ });
+
+ topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn);
+
+ // MAIN CONTENT TABS
+ TabPane tabs = new TabPane();
+ tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+ tabs.getTabs().addAll(
+ new Tab("Stocks", createStocksPanel()),
+ new Tab("Portfolio", createPortfolioPanel()),
+ new Tab("Trade", createTradePanel()),
+ new Tab("History", createHistoryPanel())
+ );
+
+ VBox.setVgrow(tabs, Priority.ALWAYS);
+ root.getChildren().addAll(topBar, tabs);
+
+ Scene scene = new Scene(root, 1000, 700);
+ scene.getStylesheets().add(
+ getClass().getResource("/Style/Global.css").toExternalForm()
+ );
+ return scene;
+ }
+
+ /**
+ * Creates the stocks panel displaying available stocks.
+ *
+ * @return the stocks panel
+ */
+ private VBox createStocksPanel() {
+ VBox panel = new VBox(10);
+ panel.getStyleClass().add("content-area");
+
+ HBox searchBox = new HBox(8);
+ searchBox.setAlignment(Pos.CENTER_LEFT);
+
+ TextField search = new TextField();
+ search.setPromptText("Search symbol or company...");
+ HBox.setHgrow(search, Priority.ALWAYS);
+
+ Button searchBtn = new Button("Search");
+ searchBtn.getStyleClass().add("action-button");
+
+ ComboBox filter = new ComboBox<>();
+ filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers"));
+ filter.setValue("All");
+
+ TableView table = new TableView<>();
+ addStockColumns(table);
+
+ Runnable loadStocks = () -> {
+ List stocks;
+ String filterVal = filter.getValue();
+ String searchVal = search.getText().trim();
+
+ if (null == filterVal) {
+ stocks = exchange.findStocks(searchVal);
+ } else {
+ stocks = switch (filterVal) {
+ case "Gainers" -> exchange.getGainers(20);
+ case "Losers" -> exchange.getLosers(20);
+ default -> exchange.findStocks(searchVal);
};
-
- searchBtn.setOnAction(e -> loadStocks.run());
- filter.setOnAction(e -> loadStocks.run());
- search.setOnAction(e -> loadStocks.run());
-
- searchBox.getChildren().addAll(search, searchBtn, filter);
- loadStocks.run();
-
- // Double-click a row to view price statistics
- table.setOnMouseClicked(event -> {
- if (event.getClickCount() == 2) {
- StockRow selected = table.getSelectionModel().getSelectedItem();
- if (selected != null) {
- showStockStats(selected.s);
- }
- }
- });
-
- VBox.setVgrow(table, Priority.ALWAYS);
-
- Label hint = new Label("Double-click a row to view price history");
- hint.getStyleClass().add("file-label");
-
- panel.getChildren().addAll(searchBox, table, hint);
- return panel;
- }
-
- private VBox createPortfolioPanel() {
- VBox panel = new VBox(10);
- panel.getStyleClass().add("content-area");
-
- Label heading = new Label("Holdings:");
-
- portfolioTable = new TableView<>();
- addPortfolioColumns(portfolioTable);
- updatePortfolio(portfolioTable);
-
- Button refresh = new Button("Refresh");
- refresh.getStyleClass().add("action-button");
- refresh.setOnAction(e -> updatePortfolio(portfolioTable));
-
- Button sellSelected = new Button("Sell Selected");
- sellSelected.getStyleClass().add("action-button");
- sellSelected.setOnAction(e -> {
- PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem();
- if (selected == null) {
- alert("Error", "Select a holding to sell.");
- return;
- }
- Transaction trans = exchange.sell(selected.s, player); // observer fires refresh
- if (trans != null && trans.isCommitted()) {
- showConfirmation("Sale successful", trans);
- } else {
- alert("Failed", "Could not complete the sale.");
- }
- });
-
- HBox buttons = new HBox(10, refresh, sellSelected);
- buttons.setAlignment(Pos.CENTER_LEFT);
-
- VBox.setVgrow(portfolioTable, Priority.ALWAYS);
- panel.getChildren().addAll(heading, portfolioTable, buttons);
- return panel;
- }
-
- private VBox createTradePanel() {
- VBox panel = new VBox(0);
- panel.getStyleClass().add("content-area");
-
- TabPane tradeTabs = new TabPane();
- tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
- tradeTabs.getTabs().addAll(
- new Tab("Buy", createBuyTab()),
- new Tab("Sell", createSellTab())
- );
-
- VBox.setVgrow(tradeTabs, Priority.ALWAYS);
- panel.getChildren().add(tradeTabs);
- return panel;
- }
-
- private VBox createBuyTab() {
- VBox box = new VBox(10);
- box.getStyleClass().add("content-area");
-
- Label heading = new Label("Buy Stocks:");
-
- GridPane form = new GridPane();
- form.setHgap(10);
- form.setVgap(10);
-
- TextField stockField = new TextField();
- stockField.setPromptText("Stock symbol (e.g. AAPL)");
-
- TextField qtyField = new TextField();
- qtyField.setPromptText("Quantity");
-
- form.add(new Label("Symbol:"), 0, 0);
- form.add(stockField, 1, 0);
- form.add(new Label("Quantity:"), 0, 1);
- form.add(qtyField, 1, 1);
-
- Label infoLabel = new Label();
-
- stockField.textProperty().addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel));
- qtyField.textProperty().addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel));
-
- Button buyBtn = new Button("Buy");
- buyBtn.getStyleClass().add("action-button");
- buyBtn.setOnAction(e -> {
- try {
- Stock s = exchange.getStock(stockField.getText().toUpperCase());
- if (s == null) {
- alert("Error", "Stock not found");
- return;
- }
- BigDecimal qty = new BigDecimal(qtyField.getText());
- Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh
- if (trans != null && trans.isCommitted()) {
- showConfirmation("Purchase successful", trans);
- stockField.clear();
- qtyField.clear();
- infoLabel.setText("");
- } else {
- alert("Failed", "Insufficient funds or error");
- }
- } catch (Exception ex) {
- alert("Error", ex.getMessage());
- }
- });
-
- box.getChildren().addAll(heading, form, infoLabel, buyBtn);
- return box;
- }
-
- private VBox createSellTab() {
- VBox box = new VBox(10);
- box.getStyleClass().add("content-area");
-
- Label heading = new Label("Your Holdings:");
-
- holdingsList = new ListView<>();
- holdingsList.setPrefHeight(400);
-
- holdingsList.setCellFactory(lv -> new ListCell() {
- @Override
- protected void updateItem(Share s, boolean empty) {
- super.updateItem(s, empty);
- if (empty || s == null) {
- setText(null);
- } else {
- setText(
- s.getStock().getSymbol() + " - " +
- s.getQuantity() + " @ $" +
- formatMoney(s.getStock().getSalesPrice())
- );
- }
- }
- });
-
- updateHoldingsList(holdingsList);
-
- // Quantity row
- HBox quantityRow = new HBox(8);
- quantityRow.setAlignment(Pos.CENTER_LEFT);
- TextField quantityField = new TextField();
- quantityField.setPromptText("Antall");
- quantityField.setPrefWidth(120);
- quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField);
-
- // Automatically fills in the maximum quantity when a holding is selected
- holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> {
- if (selected != null) {
- quantityField.setText(selected.getQuantity().toPlainString());
- }
- });
-
- Button sellBtn = new Button("Sell Selected");
- sellBtn.setOnAction(e -> {
- Share selected = holdingsList.getSelectionModel().getSelectedItem();
- if (selected == null) {
- alert("Error", "Select a holding to sell");
- return;
+ }
+
+ ObservableList data = FXCollections.observableArrayList();
+ for (Stock s : stocks) {
+ data.add(new StockRow(s));
+ }
+ table.setItems(data);
+ };
+
+ searchBtn.setOnAction(e -> loadStocks.run());
+ filter.setOnAction(e -> loadStocks.run());
+ search.setOnAction(e -> loadStocks.run());
+
+ searchBox.getChildren().addAll(search, searchBtn, filter);
+ loadStocks.run();
+
+ VBox.setVgrow(table, Priority.ALWAYS);
+ panel.getChildren().addAll(searchBox, table);
+ return panel;
+ }
+
+ /**
+ * Creates the portfolio panel showing held shares and net worth.
+ *
+ * @return the portfolio panel
+ */
+ private VBox createPortfolioPanel() {
+ VBox panel = new VBox(10);
+ panel.getStyleClass().add("content-area");
+
+ Label heading = new Label("Holdings:");
+
+ portfolioTable = new TableView<>();
+ addPortfolioColumns(portfolioTable);
+ updatePortfolio(portfolioTable);
+
+ Button refresh = new Button("Refresh");
+ refresh.getStyleClass().add("action-button");
+ refresh.setOnAction(e -> updatePortfolio(portfolioTable));
+
+ Button sellSelected = new Button("Sell Selected");
+ sellSelected.getStyleClass().add("action-button");
+ sellSelected.setOnAction(e -> {
+ PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem();
+ if (selected == null) {
+ alert("Error", "Select a holding to sell.");
+ return;
+ }
+ Transaction trans = exchange.sell(selected.s, player); // observer fires refresh
+ if (trans != null && trans.isCommitted()) {
+ showConfirmation("Sale successful", trans);
+ } else {
+ alert("Failed", "Could not complete the sale.");
+ }
+ });
+
+ HBox buttons = new HBox(10, refresh, sellSelected);
+ buttons.setAlignment(Pos.CENTER_LEFT);
+
+ VBox.setVgrow(portfolioTable, Priority.ALWAYS);
+ panel.getChildren().addAll(heading, portfolioTable, buttons);
+ return panel;
+ }
+
+ /**
+ * Creates the trading panel with buy and sell tabs.
+ *
+ * @return the trading panel
+ */
+ private VBox createTradePanel() {
+ VBox panel = new VBox(0);
+ panel.getStyleClass().add("content-area");
+
+ TabPane tradeTabs = new TabPane();
+ tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+ tradeTabs.getTabs().addAll(
+ new Tab("Buy", createBuyTab()),
+ new Tab("Sell", createSellTab())
+ );
+
+ VBox.setVgrow(tradeTabs, Priority.ALWAYS);
+ panel.getChildren().add(tradeTabs);
+ return panel;
+ }
+
+ /**
+ * Creates the buy transaction tab.
+ *
+ * @return the buy tab content
+ */
+ private VBox createBuyTab() {
+ VBox box = new VBox(10);
+ box.getStyleClass().add("content-area");
+
+ Label heading = new Label("Buy Stocks:");
+
+ GridPane form = new GridPane();
+ form.setHgap(10);
+ form.setVgap(10);
+
+ TextField stockField = new TextField();
+ stockField.setPromptText("Stock symbol (e.g. AAPL)");
+
+ TextField qtyField = new TextField();
+ qtyField.setPromptText("Quantity");
+
+ form.add(new Label("Symbol:"), 0, 0);
+ form.add(stockField, 1, 0);
+ form.add(new Label("Quantity:"), 0, 1);
+ form.add(qtyField, 1, 1);
+
+ Label infoLabel = new Label();
+
+ stockField.textProperty()
+ .addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel));
+ qtyField.textProperty()
+ .addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel));
+
+ Button buyBtn = new Button("Buy");
+ buyBtn.getStyleClass().add("action-button");
+ buyBtn.setOnAction(e -> {
+ try {
+ Stock s = exchange.getStock(stockField.getText().toUpperCase());
+ if (s == null) {
+ alert("Error", "Stock not found");
+ return;
+ }
+ BigDecimal qty = new BigDecimal(qtyField.getText());
+ Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh
+ if (trans != null && trans.isCommitted()) {
+ showConfirmation("Purchase successful", trans);
+ stockField.clear();
+ qtyField.clear();
+ infoLabel.setText("");
+ } else {
+ alert("Failed", "Insufficient funds or error");
+ }
+ } catch (Exception ex) {
+ alert("Error", ex.getMessage());
+ }
+ });
+
+ box.getChildren().addAll(heading, form, infoLabel, buyBtn);
+ return box;
+ }
+
+ /**
+ * Creates the sell transaction tab.
+ *
+ * @return the sell tab content
+ */
+ private VBox createSellTab() {
+ VBox box = new VBox(10);
+ box.getStyleClass().add("content-area");
+
+ Label heading = new Label("Your Holdings:");
+
+ holdingsList = new ListView<>();
+ holdingsList.setPrefHeight(400);
+
+ holdingsList.setCellFactory(lv -> new ListCell() {
+ @Override
+ protected void updateItem(Share s, boolean empty) {
+ super.updateItem(s, empty);
+ if (empty || s == null) {
+ setText(null);
+ } else {
+ setText(
+ s.getStock().getSymbol() + " - "
+ + s.getQuantity() + " @ $"
+ + formatMoney(s.getStock().getSalesPrice())
+ );
}
+ }
+ });
+
+ updateHoldingsList(holdingsList);
+
+ // Numer of rows
+ HBox quantityRow = new HBox(8);
+ quantityRow.setAlignment(Pos.CENTER_LEFT);
+ TextField quantityField = new TextField();
+ quantityField.setPromptText("Antall");
+ quantityField.setPrefWidth(120);
+ quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField);
+
+ // Fill in the maximum number automatically when selecting a holding
+ holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> {
+ if (selected != null) {
+ quantityField.setText(selected.getQuantity().toPlainString());
+ }
+ });
+
+ Button sellBtn = new Button("Sell Selected");
+ sellBtn.setOnAction(e -> {
+ Share selected = holdingsList.getSelectionModel().getSelectedItem();
+ if (selected == null) {
+ alert("Error", "Select a holding to sell");
+ return;
+ }
- try {
- BigDecimal quantity = new BigDecimal(quantityField.getText().trim());
-
- if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
- alert("Error", "Quantity must be greater than zero");
- return;
- }
+ try {
+ BigDecimal quantity = new BigDecimal(quantityField.getText().trim());
- Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh
+ if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ alert("Error", "Quantity must be greater than zero");
+ return;
+ }
- if (trans != null && trans.isCommitted()) {
- showConfirmation("Sale successful", trans);
- }
+ Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh
- else {
- alert("Failed", "Could not complete sale. Check quantity.");
- }
- }
-
- catch (NumberFormatException ex) {
- alert("Error", "Enter a valid number for quantity");
- }
- });
-
- box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn);
-
- return box;
+ if (trans != null && trans.isCommitted()) {
+ showConfirmation("Sale successful", trans);
+ } else {
+ alert("Failed", "Could not complete sale. Check quantity.");
+ }
+ } catch (NumberFormatException ex) {
+ alert("Error", "Enter a valid number for quantity");
+ }
+ });
+
+ box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn);
+
+ return box;
+ }
+
+ /**
+ * Creates the transaction history panel.
+ *
+ * @return the history panel
+ */
+ private VBox createHistoryPanel() {
+ VBox panel = new VBox(10);
+ panel.getStyleClass().add("content-area");
+
+ weekFilterCombo = new ComboBox<>();
+ updateWeekCombo(weekFilterCombo);
+
+ historyTable = new TableView<>();
+ addHistoryColumns(historyTable);
+ updateHistory(historyTable, null);
+
+ weekFilterCombo.setOnAction(e -> updateHistory(
+ historyTable,
+ weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0
+ ? null : weekFilterCombo.getValue()
+ ));
+
+ HBox filterRow = new HBox(8);
+ filterRow.setAlignment(Pos.CENTER_LEFT);
+ filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo);
+
+ VBox.setVgrow(historyTable, Priority.ALWAYS);
+ panel.getChildren().addAll(filterRow, historyTable);
+ return panel;
+ }
+
+ // ---- Column helpers ----
+
+ @SuppressWarnings("unchecked")
+private void addStockColumns(TableView table) {
+ table.getColumns().addAll(
+ createCol("Symbol", 90, "symbol"),
+ createCol("Company", 180, "company"),
+ createCol("Price", 90, "price"),
+ createCol("Change", 90, "change"),
+ createCol("High", 90, "high"),
+ createCol("Low", 90, "low")
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addPortfolioColumns(TableView table) {
+ table.getColumns().addAll(
+ createCol("Symbol", 90, "symbol"),
+ createCol("Qty", 70, "qty"),
+ createCol("Buy Price", 100, "buyPrice"),
+ createCol("Current Price", 120, "currentPrice"),
+ createCol("Gain/Loss", 100, "gainLoss")
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addHistoryColumns(TableView table) {
+ table.getColumns().addAll(
+ createCol("Week", 70, "week"),
+ createCol("Type", 70, "type"),
+ createCol("Symbol", 90, "symbol"),
+ createCol("Qty", 70, "qty"),
+ createCol("Total", 110, "total")
+ );
+ }
+
+ private TableColumn createCol(String header, int width, String property) {
+ TableColumn col = new TableColumn<>(header);
+ col.setPrefWidth(width);
+ col.setCellValueFactory(cellData -> {
+ try {
+ return new javafx.beans.property.SimpleStringProperty(
+ cellData.getValue().getClass()
+ .getMethod("get" + capitalize(property))
+ .invoke(cellData.getValue()).toString()
+ );
+ } catch (Exception e) {
+ return new javafx.beans.property.SimpleStringProperty("");
+ }
+ });
+ return col;
+ }
+
+ private String capitalize(String s) {
+ return s.substring(0, 1).toUpperCase() + s.substring(1);
+ }
+
+ // ---- Data updaters ----
+
+ private void updatePortfolio(TableView table) {
+ ObservableList data = FXCollections.observableArrayList();
+ for (Share s : player.getPortfolio().getShares()) {
+ data.add(new PortfolioRow(s));
+ }
+ table.setItems(data);
+ }
+
+ private void updateHoldingsList(ListView list) {
+ ObservableList items = FXCollections.observableArrayList();
+ items.addAll(player.getPortfolio().getShares());
+ list.setItems(items);
+ }
+
+ private void updateHistory(TableView table, Integer week) {
+ ObservableList data = FXCollections.observableArrayList();
+ List trans = week == null
+ ? player.getTransactionArchive().getAllTransactions()
+ : player.getTransactionArchive().getTransactions(week);
+ for (Transaction t : trans) {
+ data.add(new HistoryRow(t));
+ }
+ table.setItems(data);
+ }
+
+ private void updateWeekCombo(ComboBox combo) {
+ ObservableList weeks = FXCollections.observableArrayList();
+ weeks.add(0);
+ int total = player.getTransactionArchive().countDistinctWeeks();
+ for (int i = 1; i <= total; i++) {
+ weeks.add(i);
+ }
+ combo.setItems(weeks);
+ combo.setValue(0);
+ }
+
+ private void updateBuyInfo(String symbol, TextField qtyField, Label label) {
+ try {
+ if (symbol == null || symbol.isEmpty()) {
+ return;
+ }
+ Stock s = exchange.getStock(symbol.toUpperCase());
+ if (s == null) {
+ return;
+ }
+ BigDecimal qty = new BigDecimal(qtyField.getText());
+ BigDecimal gross = s.getSalesPrice().multiply(qty);
+ BigDecimal comm = gross.multiply(new BigDecimal("0.005"));
+ label.setText("Gross: $" + formatMoney(gross) +
+ " | Commission: $" + formatMoney(comm) +
+ " | Total: $" + formatMoney(gross.add(comm)));
+ } catch (Exception e) {
+ // ignore partial input
+ }
+ }
+
+ private void updateStatus() {
+ PlayerStatus playerStatus = player.getStatus();
+ statusLabel.setText(
+ "Player: " + player.getName()
+ + " | Status: " + playerStatus.getDisplayName()
+ + " | Week: " + exchange.getWeek()
+ + " | Cash: $" + formatMoney(player.getMoney())
+ + " | Net Worth: $" + formatMoney(getNetWorth())
+ );
+ }
+
+ private void refreshAllUI() {
+ if (portfolioTable != null) {
+ updatePortfolio(portfolioTable);
+ }
+ if (holdingsList != null) {
+ updateHoldingsList(holdingsList);
+ }
+ if (historyTable != null) {
+ updateHistory(historyTable,
+ weekFilterCombo != null && weekFilterCombo.getValue() != null
+ ? weekFilterCombo.getValue() : null);
+ if (weekFilterCombo != null) {
+ updateWeekCombo(weekFilterCombo);
+ }
}
+ }
- private VBox createHistoryPanel() {
- VBox panel = new VBox(10);
- panel.getStyleClass().add("content-area");
+ private BigDecimal getNetWorth() {
+ return player.getMoney().add(player.getPortfolio().getNetWorth());
+ }
- weekFilterCombo = new ComboBox<>();
- updateWeekCombo(weekFilterCombo);
+ private void showConfirmation(String title, Transaction t) {
+ StringBuilder msg = new StringBuilder();
+ if (t instanceof Purchase) {
+ Purchase p = (Purchase) t;
+ msg.append("Bought ").append(formatMoney(p.getShare().getQuantity()))
+ .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice()))
+ .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal()));
+ } else {
+ Sale s = (Sale) t;
+ msg.append("Sold ").append(formatMoney(s.getShare().getQuantity()))
+ .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice()))
+ .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal()));
+ }
+ alert(title, msg.toString());
+ }
- historyTable = new TableView<>();
- addHistoryColumns(historyTable);
- updateHistory(historyTable, null);
+ private void alert(String title, String msg) {
+ Alert a = new Alert(Alert.AlertType.INFORMATION);
+ a.setTitle(title);
+ a.setHeaderText(null);
+ a.setContentText(msg);
+ a.showAndWait();
+ }
- weekFilterCombo.setOnAction(e -> updateHistory(
- historyTable,
- weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0
- ? null : weekFilterCombo.getValue()
- ));
+ private boolean confirm(String title, String msg) {
+ Alert a = new Alert(Alert.AlertType.CONFIRMATION);
+ a.setTitle(title);
+ a.setHeaderText(null);
+ a.setContentText(msg);
+ return a.showAndWait().get() == ButtonType.OK;
+ }
- HBox filterRow = new HBox(8);
- filterRow.setAlignment(Pos.CENTER_LEFT);
- filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo);
+ private String formatMoney(BigDecimal n) {
+ return n.setScale(2, java.math.RoundingMode.HALF_UP).toString();
+ }
- VBox.setVgrow(historyTable, Priority.ALWAYS);
- panel.getChildren().addAll(filterRow, historyTable);
- return panel;
- }
+ public Scene getScene() {
+ return scene;
+ }
- // ---- Column helpers ----
+ // ---- Data row classes ----
- private void addStockColumns(TableView table) {
- table.getColumns().addAll(
- createCol("Symbol", 90, "symbol"),
- createCol("Company", 180, "company"),
- createCol("Price", 90, "price"),
- createCol("Change", 90, "change"),
- createCol("High", 90, "high"),
- createCol("Low", 90, "low")
- );
- }
+ static class StockRow {
+ Stock s;
- private void addPortfolioColumns(TableView table) {
- table.getColumns().addAll(
- createCol("Symbol", 90, "symbol"),
- createCol("Qty", 70, "qty"),
- createCol("Buy Price", 100, "buyPrice"),
- createCol("Current Price", 120, "currentPrice"),
- createCol("Gain/Loss", 100, "gainLoss")
- );
+ StockRow(Stock s) {
+ this.s = s;
}
- private void addHistoryColumns(TableView table) {
- table.getColumns().addAll(
- createCol("Week", 70, "week"),
- createCol("Type", 70, "type"),
- createCol("Symbol", 90, "symbol"),
- createCol("Qty", 70, "qty"),
- createCol("Total", 110, "total")
- );
+ public String getSymbol() {
+ return s.getSymbol();
}
- private TableColumn createCol(String header, int width, String property) {
- TableColumn col = new TableColumn<>(header);
- col.setPrefWidth(width);
- col.setCellValueFactory(cellData -> {
- try {
- return new javafx.beans.property.SimpleStringProperty(
- cellData.getValue().getClass()
- .getMethod("get" + capitalize(property))
- .invoke(cellData.getValue()).toString()
- );
- } catch (Exception e) {
- return new javafx.beans.property.SimpleStringProperty("");
- }
- });
- return col;
+ public String getCompany() {
+ return s.getCompany();
}
- private String capitalize(String s) {
- return s.substring(0, 1).toUpperCase() + s.substring(1);
+ public String getPrice() {
+ return "$" + s.getSalesPrice();
}
- // ---- Data updaters ----
-
- private void updatePortfolio(TableView table) {
- ObservableList data = FXCollections.observableArrayList();
- for (Share s : player.getPortfolio().getShares()) {
- data.add(new PortfolioRow(s));
- }
- table.setItems(data);
+ public String getChange() {
+ return "$" + s.getLatestPriceChange();
}
-
- private void updateHoldingsList(ListView list) {
- ObservableList items = FXCollections.observableArrayList();
- items.addAll(player.getPortfolio().getShares());
- list.setItems(items);
+
+ public String getHigh() {
+ return "$" + s.getHighestPrice();
}
- private void updateHistory(TableView table, Integer week) {
- ObservableList data = FXCollections.observableArrayList();
- List trans = week == null
- ? player.getTransactionArchive().getAllTransactions()
- : player.getTransactionArchive().getTransactions(week);
- for (Transaction t : trans) {
- data.add(new HistoryRow(t));
- }
- table.setItems(data);
+ public String getLow() {
+ return "$" + s.getLowestPrice();
}
- private void updateWeekCombo(ComboBox combo) {
- ObservableList weeks = FXCollections.observableArrayList();
- weeks.add(0);
- int total = player.getTransactionArchive().countDistinctWeeks();
- for (int i = 1; i <= total; i++) weeks.add(i);
- combo.setItems(weeks);
- combo.setValue(0);
- }
+ }
- private void updateBuyInfo(String symbol, TextField qtyField, Label label) {
- try {
- if (symbol == null || symbol.isEmpty()) return;
- Stock s = exchange.getStock(symbol.toUpperCase());
- if (s == null) return;
- BigDecimal qty = new BigDecimal(qtyField.getText());
- BigDecimal gross = s.getSalesPrice().multiply(qty);
- BigDecimal comm = gross.multiply(new BigDecimal("0.005"));
- label.setText("Gross: $" + formatMoney(gross) +
- " | Commission: $" + formatMoney(comm) +
- " | Total: $" + formatMoney(gross.add(comm)));
- } catch (Exception e) {
- // ignore partial input
- }
- }
+ static class PortfolioRow {
+ Share s;
- private void updateStatus() {
- PlayerStatus playerStatus = player.getStatus();
- statusLabel.setText(
- "Player: " + player.getName() +
- " | Status: " + playerStatus.getDisplayName() +
- " | Week: " + exchange.getWeek() +
- " | Cash: $" + formatMoney(player.getMoney()) +
- " | Net Worth: $" + formatMoney(getNetWorth())
- );
+ PortfolioRow(Share s) {
+ this.s = s;
}
-
- private void refreshAllUI() {
- if (portfolioTable != null) {
- updatePortfolio(portfolioTable);
- }
- if (holdingsList != null) {
- updateHoldingsList(holdingsList);
- }
- if (historyTable != null) {
- updateHistory(historyTable,
- weekFilterCombo != null && weekFilterCombo.getValue() != null
- ? weekFilterCombo.getValue() : null);
- if (weekFilterCombo != null) {
- updateWeekCombo(weekFilterCombo);
- }
- }
+
+ public String getSymbol() {
+ return s.getStock().getSymbol();
}
- private BigDecimal getNetWorth() {
- return player.getMoney().add(player.getPortfolio().getNetWorth());
+ public String getQty() {
+ return s.getQuantity().toString();
}
- private void showStockStats(Stock stock) {
- Dialog dialog = new Dialog<>();
- dialog.setTitle(stock.getSymbol() + " — " + stock.getCompany());
- dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
-
- VBox content = new VBox(12);
- content.setPrefWidth(340);
-
- // --- Number Sumary ---
- BigDecimal change = stock.getLatestPriceChange();
- String changeSign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
-
- GridPane summary = new GridPane();
- summary.setHgap(16);
- summary.setVgap(6);
- addStatRow(summary, 0, "Current price:", "$" + formatMoney(stock.getSalesPrice()));
- addStatRow(summary, 1, "Week change:", changeSign + formatMoney(change));
- addStatRow(summary, 2, "All-time high:", "$" + formatMoney(stock.getHighestPrice()));
- addStatRow(summary, 3, "All-time low:", "$" + formatMoney(stock.getLowestPrice()));
- addStatRow(summary, 4, "Weeks tracked:", String.valueOf(stock.getHistoricalPrices().size()));
-
- // --- Price history list ---
- Label histHeading = new Label("Price history:");
- histHeading.setStyle("-fx-font-weight: bold;");
-
- ListView histList = new ListView<>();
- histList.setPrefHeight(180);
- List prices = stock.getHistoricalPrices();
- ObservableList rows = FXCollections.observableArrayList();
- for (int i = 0; i < prices.size(); i++) {
- String change_i = "";
- if (i > 0) {
- BigDecimal diff = prices.get(i).subtract(prices.get(i - 1));
- change_i = " (" + (diff.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "") +
- formatMoney(diff) + ")";
- }
- rows.add("Week " + (i + 1) + ": $" + formatMoney(prices.get(i)) + change_i);
- }
- histList.setItems(rows);
-
- // Scroll to the most recent week
- if (!rows.isEmpty()) histList.scrollTo(rows.size() - 1);
-
- content.getChildren().addAll(summary, new Separator(), histHeading, histList);
- dialog.getDialogPane().setContent(content);
- dialog.showAndWait();
+ public String getBuyPrice() {
+ return "$" + s.getPurchasePrice();
}
- /** Adds a label and value to the given row. */
- private void addStatRow(GridPane grid, int row, String labelText, String valueText) {
- Label lbl = new Label(labelText);
- lbl.setStyle("-fx-font-weight: bold;");
- Label val = new Label(valueText);
- grid.add(lbl, 0, row);
- grid.add(val, 1, row);
+ public String getCurrentPrice() {
+ return "$" + s.getStock().getSalesPrice();
}
- private void showConfirmation(String title, Transaction t) {
- StringBuilder msg = new StringBuilder();
- if (t instanceof Purchase) {
- Purchase p = (Purchase) t;
- msg.append("Bought ").append(formatMoney(p.getShare().getQuantity()))
- .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice()))
- .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal()));
- } else {
- Sale s = (Sale) t;
- msg.append("Sold ").append(formatMoney(s.getShare().getQuantity()))
- .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice()))
- .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal()));
- }
- alert(title, msg.toString());
+ public String getGainLoss() {
+ SaleCalculator calc = new SaleCalculator(s);
+ BigDecimal val = calc.calculateTotal();
+ BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity());
+ return "$" + val.subtract(cost);
}
+ }
- private void alert(String title, String msg) {
- Alert a = new Alert(Alert.AlertType.INFORMATION);
- a.setTitle(title);
- a.setHeaderText(null);
- a.setContentText(msg);
- a.showAndWait();
- }
+ static class HistoryRow {
+ Transaction t;
- private boolean confirm(String title, String msg) {
- Alert a = new Alert(Alert.AlertType.CONFIRMATION);
- a.setTitle(title);
- a.setHeaderText(null);
- a.setContentText(msg);
- return a.showAndWait().get() == ButtonType.OK;
+ HistoryRow(Transaction t) {
+ this.t = t;
}
- private String formatMoney(BigDecimal n) {
- return n.setScale(2, java.math.RoundingMode.HALF_UP).toString();
+ public String getWeek() {
+ return String.valueOf(t.getWeek());
}
- public Scene getScene() {
- return scene;
+ public String getType() {
+ return t instanceof Purchase ? "BUY" : "SELL";
}
- // ---- Data row classes ----
-
- static class StockRow {
- Stock s;
- StockRow(Stock s) { this.s = s; }
- public String getSymbol() { return s.getSymbol(); }
- public String getCompany() { return s.getCompany(); }
- public String getPrice() { return "$" + s.getSalesPrice(); }
- public String getChange() { return "$" + s.getLatestPriceChange(); }
- public String getHigh() { return "$" + s.getHighestPrice(); }
- public String getLow() { return "$" + s.getLowestPrice(); }
+ public String getSymbol() {
+ return t.getShare().getStock().getSymbol();
}
- static class PortfolioRow {
- Share s;
- PortfolioRow(Share s) { this.s = s; }
- public String getSymbol() { return s.getStock().getSymbol(); }
- public String getQty() { return s.getQuantity().toString(); }
- public String getBuyPrice() { return "$" + s.getPurchasePrice(); }
- public String getCurrentPrice() { return "$" + s.getStock().getSalesPrice(); }
- public String getGainLoss() {
- SaleCalculator calc = new SaleCalculator(s);
- BigDecimal val = calc.calculateTotal();
- BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity());
- return "$" + val.subtract(cost);
- }
+ public String getQty() {
+ return t.getShare().getQuantity().toString();
}
- static class HistoryRow {
- Transaction t;
- HistoryRow(Transaction t) { this.t = t; }
- public String getWeek() { return String.valueOf(t.getWeek()); }
- public String getType() { return t instanceof Purchase ? "BUY" : "SELL"; }
- public String getSymbol() { return t.getShare().getStock().getSymbol(); }
- public String getQty() { return t.getShare().getQuantity().toString(); }
- public String getTotal() { return "$" + t.getCalculator().calculateTotal(); }
+ public String getTotal() {
+ return "$" + t.getCalculator().calculateTotal();
}
+ }
}
diff --git a/src/main/java/View/StockTradingGameApp.java b/src/main/java/View/StockTradingGameApp.java
index a2d6543..b1ab987 100644
--- a/src/main/java/View/StockTradingGameApp.java
+++ b/src/main/java/View/StockTradingGameApp.java
@@ -1,49 +1,73 @@
package View;
-import javafx.application.Application;
-import javafx.stage.Stage;
import Controller.StockFileHandler;
import Model.*;
+import javafx.application.Application;
+import javafx.stage.Stage;
-import java.math.BigDecimal;
-
+/**
+ * Main JavaFX application class for the Stock Trading Game.
+ * Manages the application lifecycle, window setup, scene transitions,
+ * and exchanges between the game setup and main game views.
+ */
public class StockTradingGameApp extends Application {
- private Stage primaryStage;
- private Exchange exchange;
- private Player player;
- private StockFileHandler fileHandler;
+ private Stage primaryStage;
+ private Exchange exchange;
+ private Player player;
+ private StockFileHandler fileHandler;
- @Override
- public void start(Stage primaryStage) {
- this.primaryStage = primaryStage;
- this.fileHandler = new StockFileHandler();
+ /**
+ * Initializes and displays the primary stage.
+ *
+ * @param primaryStage the primary window stage provided by JavaFX
+ */
+ @Override
+ public void start(Stage primaryStage) {
+ this.primaryStage = primaryStage;
+ this.fileHandler = new StockFileHandler();
- primaryStage.setTitle("Stock Trading Game");
- primaryStage.setWidth(1200);
- primaryStage.setHeight(800);
+ primaryStage.setTitle("Stock Trading Game");
+ primaryStage.setWidth(1200);
+ primaryStage.setHeight(800);
- showGameSetup();
- primaryStage.show();
- }
+ showGameSetup();
+ primaryStage.show();
+ }
- private void showGameSetup() {
- GameSetupScene setupScene = new GameSetupScene(this::startGame);
- primaryStage.setScene(setupScene.getScene());
- }
+ /**
+ * Displays the game setup scene where players configure their game.
+ */
+ private void showGameSetup() {
+ GameSetupScene setupScene = new GameSetupScene(this::startGame);
+ primaryStage.setScene(setupScene.getScene());
+ }
- private void startGame(GameSetupScene.StartGameData gameData) {
- this.exchange = gameData.exchange;
- this.player = new Player(gameData.playerName, gameData.startingCapital);
+ /**
+ * Starts the main game scene after setup is complete.
+ *
+ * @param gameData the game configuration data from the setup scene
+ */
+ private void startGame(GameSetupScene.StartGameData gameData) {
+ this.exchange = gameData.exchange;
+ this.player = new Player(gameData.playerName, gameData.startingCapital);
- MainGameScene gameScene = new MainGameScene(exchange, player, this::endGame);
- primaryStage.setScene(gameScene.getScene());
- }
+ MainGameScene gameScene = new MainGameScene(exchange, player, this::endGame);
+ primaryStage.setScene(gameScene.getScene());
+ }
- private void endGame() {
- primaryStage.close();
- }
+ /**
+ * Ends the game and closes the application.
+ */
+ private void endGame() {
+ primaryStage.close();
+ }
- public static void main(String[] args) {
- launch(args);
- }
+ /**
+ * Main method to launch the JavaFX application.
+ *
+ * @param args command-line arguments (not used)
+ */
+ public static void main(String[] args) {
+ launch(args);
+ }
}
diff --git a/src/test/java/ExchangeTest.java b/src/test/java/ExchangeTest.java
index 9261bd6..cc55f5d 100644
--- a/src/test/java/ExchangeTest.java
+++ b/src/test/java/ExchangeTest.java
@@ -1,265 +1,365 @@
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import Model.Exchange;
import Model.Player;
+import Model.Purchase;
+import Model.Sale;
import Model.Share;
import Model.Stock;
import Model.Transaction;
-import Model.Purchase;
-import Model.Sale;
-
-import static org.junit.jupiter.api.Assertions.*;
-
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+/**
+ * Unit tests for the Exchange class.
+ * Tests core exchange functionality including stock management, buying/selling,
+ * market data retrieval, and error handling.
+ */
public class ExchangeTest {
- private Exchange exchange;
- private Stock apple;
- private Stock google;
- private Player player;
-
- @BeforeEach
- void setUp() {
- apple = new Stock("AAPL", "Apple", new BigDecimal("100"));
- google = new Stock("GOOGL", "Google", new BigDecimal("200"));
-
- List stocks = new ArrayList<>();
- stocks.add(apple);
- stocks.add(google);
-
- exchange = new Exchange("TestExchange", stocks);
- player = new Player("Jane", new BigDecimal("500000"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testGetName() {
- assertEquals("TestExchange", exchange.getName());
- }
-
- @Test
- void testInitialWeekIsOne() {
- assertEquals(1, exchange.getWeek());
- }
-
- @Test
- void testAdvanceIncrementsWeek() {
- exchange.advance();
- assertEquals(2, exchange.getWeek());
- }
-
- @Test
- void testHasStockReturnsTrue() {
- assertTrue(exchange.hasStock("AAPL"));
- }
-
- @Test
- void testHasStockReturnsFalse() {
- assertFalse(exchange.hasStock("MSFT"));
- }
-
- @Test
- void testGetStockReturnsCorrectStock() {
- assertEquals(apple, exchange.getStock("AAPL"));
- }
-
- @Test
- void testGetStockReturnsNullForUnknown() {
- assertNull(exchange.getStock("MSFT"));
- }
-
- @Test
- void testFindStocksBySymbol() {
- List result = exchange.findStocks("AAPL");
- assertEquals(1, result.size());
- assertEquals("AAPL", result.get(0).getSymbol());
- }
-
- @Test
- void testFindStocksByCompanyName() {
- List result = exchange.findStocks("e");
- assertEquals(2, result.size());
- }
-
- @Test
- void testFindStocksNoMatch() {
- List result = exchange.findStocks("Samsung");
- assertEquals(0, result.size());
- }
-
- @Test
- void testFindStocksEmptyTermReturnsAll() {
- List result = exchange.findStocks("");
- assertEquals(2, result.size());
- }
-
- @Test
- void testBuyReturnsCommittedPurchase() {
- Transaction t = exchange.buy("AAPL", new BigDecimal("5"), player);
- assertNotNull(t);
- assertTrue(t.isCommitted());
- assertInstanceOf(Purchase.class, t);
- }
-
- @Test
- void testBuyDeductsMoneyFromPlayer() {
- BigDecimal before = player.getMoney();
- exchange.buy("AAPL", new BigDecimal("5"), player);
- assertTrue(player.getMoney().compareTo(before) < 0);
- }
-
- @Test
- void testBuyAddsShareToPortfolio() {
- exchange.buy("AAPL", new BigDecimal("5"), player);
- assertFalse(player.getPortfolio().getShares("AAPL").isEmpty());
- }
-
- @Test
- void testSellReturnsCommittedSale() {
- exchange.buy("AAPL", new BigDecimal("5"), player);
- Share share = player.getPortfolio().getShares("AAPL").get(0);
-
- Transaction t = exchange.sell(share, player);
- assertNotNull(t);
- assertTrue(t.isCommitted());
- assertInstanceOf(Sale.class, t);
- }
-
- @Test
- void testSellAddsMoney() {
- exchange.buy("AAPL", new BigDecimal("5"), player);
- Share share = player.getPortfolio().getShares("AAPL").get(0);
- BigDecimal before = player.getMoney();
-
- exchange.sell(share, player);
- assertTrue(player.getMoney().compareTo(before) > 0);
- }
-
- @Test
- void testSellRemovesShareFromPortfolio() {
- exchange.buy("AAPL", new BigDecimal("5"), player);
- Share share = player.getPortfolio().getShares("AAPL").get(0);
-
- exchange.sell(share, player);
- assertTrue(player.getPortfolio().getShares("AAPL").isEmpty());
- }
-
- @Test
- void testGetGainersReturnsRequestedLimit() {
- exchange.advance(); // prices must have changed at least once for a meaningful sort
- List gainers = exchange.getGainers(1);
- assertEquals(1, gainers.size());
- }
-
- @Test
- void testGetLosersReturnsRequestedLimit() {
- exchange.advance();
- List losers = exchange.getLosers(1);
- assertEquals(1, losers.size());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testConstructorNullNameThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Exchange(null, List.of(apple))
- );
- }
-
- @Test
- void testConstructorBlankNameThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Exchange(" ", List.of(apple))
- );
- }
-
- @Test
- void testConstructorNullStockListThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Exchange("X", null)
- );
- }
-
- @Test
- void testConstructorNullStockEntryThrows() {
- List withNull = new ArrayList<>();
- withNull.add(apple);
- withNull.add(null);
- assertThrows(IllegalArgumentException.class, () ->
- new Exchange("X", withNull)
- );
- }
-
- @Test
- void testBuyNullSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.buy(null, new BigDecimal("5"), player)
- );
- }
-
- @Test
- void testBuyUnknownSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.buy("MSFT", new BigDecimal("5"), player)
- );
- }
-
- @Test
- void testBuyZeroQuantityThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.buy("AAPL", BigDecimal.ZERO, player)
- );
- }
-
- @Test
- void testBuyNegativeQuantityThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.buy("AAPL", new BigDecimal("-1"), player)
- );
- }
-
- @Test
- void testBuyNullPlayerThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.buy("AAPL", new BigDecimal("5"), null)
- );
- }
-
- @Test
- void testBuyInsufficientFundsThrows() {
- Player poorPlayer = new Player("Broke", new BigDecimal("1"));
- assertThrows(IllegalStateException.class, () ->
- exchange.buy("AAPL", new BigDecimal("5"), poorPlayer)
- );
- }
-
- @Test
- void testSellNullPlayerThrows() {
- exchange.buy("AAPL", new BigDecimal("5"), player);
- Share share = player.getPortfolio().getShares("AAPL").get(0);
- assertThrows(IllegalArgumentException.class, () ->
- exchange.sell(share, null)
- );
- }
-
- @Test
- void testSellShareNotInPortfolioThrows() {
- Share unowned = new Share(apple, new BigDecimal("1"), new BigDecimal("100"));
- assertThrows(IllegalStateException.class, () ->
- exchange.sell(unowned, player)
- );
- }
-
- @Test
- void testAddNullObserverThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- exchange.addObserver(null)
- );
- }
+ private Exchange exchange;
+ private Stock apple;
+ private Stock google;
+ private Player player;
+
+ @BeforeEach
+ void setUp() {
+ apple = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ google = new Stock("GOOGL", "Google", new BigDecimal("200"));
+
+ List stocks = new ArrayList<>();
+ stocks.add(apple);
+ stocks.add(google);
+
+ exchange = new Exchange("TestExchange", stocks);
+ player = new Player("Jane", new BigDecimal("500000"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that exchange returns its correct name.
+ */
+ @Test
+ void testGetName() {
+ assertEquals("TestExchange", exchange.getName());
+ }
+
+ /**
+ * Tests that exchange starts at week 1.
+ */
+ @Test
+ void testInitialWeekIsOne() {
+ assertEquals(1, exchange.getWeek());
+ }
+
+ /**
+ * Tests that calling advance increments the week counter.
+ */
+ @Test
+ void testAdvanceIncrementsWeek() {
+ exchange.advance();
+ assertEquals(2, exchange.getWeek());
+ }
+
+ /**
+ * Tests that hasStock returns true for a known stock symbol.
+ */
+ @Test
+ void testHasStockReturnsTrue() {
+ assertTrue(exchange.hasStock("AAPL"));
+ }
+
+ /**
+ * Tests that hasStock returns false for an unknown stock symbol.
+ */
+ @Test
+ void testHasStockReturnsFalse() {
+ assertFalse(exchange.hasStock("MSFT"));
+ }
+
+ /**
+ * Tests that getStock returns the correct Stock object for a given symbol.
+ */
+ @Test
+ void testGetStockReturnsCorrectStock() {
+ assertEquals(apple, exchange.getStock("AAPL"));
+ }
+
+ /**
+ * Tests that getStock returns null for an unknown symbol.
+ */
+ @Test
+ void testGetStockReturnsNullForUnknown() {
+ assertNull(exchange.getStock("MSFT"));
+ }
+
+ /**
+ * Tests that findStocks correctly locates stocks by symbol.
+ */
+ @Test
+ void testFindStocksBySymbol() {
+ List result = exchange.findStocks("AAPL");
+ assertEquals(1, result.size());
+ assertEquals("AAPL", result.get(0).getSymbol());
+ }
+
+ /**
+ * Tests that findStocks correctly locates stocks by partial company name match.
+ */
+ @Test
+ void testFindStocksByCompanyName() {
+ List result = exchange.findStocks("e");
+ assertEquals(2, result.size());
+ }
+
+ /**
+ * Tests that findStocks returns empty list when no stocks match search term.
+ */
+ @Test
+ void testFindStocksNoMatch() {
+ List result = exchange.findStocks("Samsung");
+ assertEquals(0, result.size());
+ }
+
+ /**
+ * Tests that findStocks with empty search term returns all stocks.
+ */
+ @Test
+ void testFindStocksEmptyTermReturnsAll() {
+ List result = exchange.findStocks("");
+ assertEquals(2, result.size());
+ }
+
+ /**
+ * Tests that buying a stock returns a committed Purchase transaction.
+ */
+ @Test
+ void testBuyReturnsCommittedPurchase() {
+ Transaction t = exchange.buy("AAPL", new BigDecimal("5"), player);
+ assertNotNull(t);
+ assertTrue(t.isCommitted());
+ assertInstanceOf(Purchase.class, t);
+ }
+
+ /**
+ * Tests that buying stock deducts the correct amount from player's balance.
+ */
+ @Test
+ void testBuyDeductsMoneyFromPlayer() {
+ BigDecimal before = player.getMoney();
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ assertTrue(player.getMoney().compareTo(before) < 0);
+ }
+
+ /**
+ * Tests that buying stock adds the share to player's portfolio.
+ */
+ @Test
+ void testBuyAddsShareToPortfolio() {
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ assertFalse(player.getPortfolio().getShares("AAPL").isEmpty());
+ }
+
+ /**
+ * Tests that selling a share returns a committed Sale transaction.
+ */
+ @Test
+ void testSellReturnsCommittedSale() {
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ Share share = player.getPortfolio().getShares("AAPL").get(0);
+
+ Transaction t = exchange.sell(share, player);
+ assertNotNull(t);
+ assertTrue(t.isCommitted());
+ assertInstanceOf(Sale.class, t);
+ }
+
+ /**
+ * Tests that selling stock adds the correct amount to player's balance.
+ */
+ @Test
+ void testSellAddsMoney() {
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ Share share = player.getPortfolio().getShares("AAPL").get(0);
+ BigDecimal before = player.getMoney();
+
+ exchange.sell(share, player);
+ assertTrue(player.getMoney().compareTo(before) > 0);
+ }
+
+ /**
+ * Tests that selling stock removes the share from player's portfolio.
+ */
+ @Test
+ void testSellRemovesShareFromPortfolio() {
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ Share share = player.getPortfolio().getShares("AAPL").get(0);
+
+ exchange.sell(share, player);
+ assertTrue(player.getPortfolio().getShares("AAPL").isEmpty());
+ }
+
+ /**
+ * Tests that getGainers returns the requested number of gaining stocks.
+ */
+ @Test
+ void testGetGainersReturnsRequestedLimit() {
+ exchange.advance(); // prices must have changed at least once for a meaningful sort
+ List gainers = exchange.getGainers(1);
+ assertEquals(1, gainers.size());
+ }
+
+ /**
+ * Tests that getLosers returns the requested number of losing stocks.
+ */
+ @Test
+ void testGetLosersReturnsRequestedLimit() {
+ exchange.advance();
+ List losers = exchange.getLosers(1);
+ assertEquals(1, losers.size());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that Exchange constructor throws when given a null name.
+ */
+ @Test
+ void testConstructorNullNameThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Exchange(null, List.of(apple))
+ );
+ }
+
+ /**
+ * Tests that Exchange constructor throws when given a blank name.
+ */
+ @Test
+ void testConstructorBlankNameThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Exchange(" ", List.of(apple))
+ );
+ }
+
+ /**
+ * Tests that Exchange constructor throws when given a null stock list.
+ */
+ @Test
+ void testConstructorNullStockListThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Exchange("X", null)
+ );
+ }
+
+ /**
+ * Tests that Exchange constructor throws when stock list contains null entry.
+ */
+ @Test
+ void testConstructorNullStockEntryThrows() {
+ List withNull = new ArrayList<>();
+ withNull.add(apple);
+ withNull.add(null);
+ assertThrows(IllegalArgumentException.class, () ->
+ new Exchange("X", withNull)
+ );
+ }
+
+ /**
+ * Tests that buy throws when given a null symbol.
+ */
+ @Test
+ void testBuyNullSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.buy(null, new BigDecimal("5"), player)
+ );
+ }
+
+ /**
+ * Tests that buy throws when given an unknown stock symbol.
+ */
+ @Test
+ void testBuyUnknownSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.buy("MSFT", new BigDecimal("5"), player)
+ );
+ }
+
+ /**
+ * Tests that buy throws when given zero quantity.
+ */
+ @Test
+ void testBuyZeroQuantityThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.buy("AAPL", BigDecimal.ZERO, player)
+ );
+ }
+
+ /**
+ * Tests that buy throws when given negative quantity.
+ */
+ @Test
+ void testBuyNegativeQuantityThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.buy("AAPL", new BigDecimal("-1"), player)
+ );
+ }
+
+ /**
+ * Tests that buy throws when given a null player.
+ */
+ @Test
+ void testBuyNullPlayerThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.buy("AAPL", new BigDecimal("5"), null)
+ );
+ }
+
+ /**
+ * Tests that buy throws when player has insufficient funds.
+ */
+ @Test
+ void testBuyInsufficientFundsThrows() {
+ Player poorPlayer = new Player("Broke", new BigDecimal("1"));
+ assertThrows(IllegalStateException.class, () ->
+ exchange.buy("AAPL", new BigDecimal("5"), poorPlayer)
+ );
+ }
+
+ /**
+ * Tests that sell throws when given a null player.
+ */
+ @Test
+ void testSellNullPlayerThrows() {
+ exchange.buy("AAPL", new BigDecimal("5"), player);
+ Share share = player.getPortfolio().getShares("AAPL").get(0);
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.sell(share, null)
+ );
+ }
+
+ /**
+ * Tests that sell throws when share is not in player's portfolio.
+ */
+ @Test
+ void testSellShareNotInPortfolioThrows() {
+ Share unowned = new Share(apple, new BigDecimal("1"), new BigDecimal("100"));
+ assertThrows(IllegalStateException.class, () ->
+ exchange.sell(unowned, player)
+ );
+ }
+
+ /**
+ * Tests that addObserver throws when given a null observer.
+ */
+ @Test
+ void testAddNullObserverThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ exchange.addObserver(null)
+ );
+ }
}
+
diff --git a/src/test/java/PlayerTest.java b/src/test/java/PlayerTest.java
index ac1f95a..cedf182 100644
--- a/src/test/java/PlayerTest.java
+++ b/src/test/java/PlayerTest.java
@@ -1,128 +1,183 @@
import static org.junit.jupiter.api.Assertions.*;
+import Model.Player;
+import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import Model.Player;
-
-import java.math.BigDecimal;
+/**
+ * Unit tests for the Player class.
+ * Tests player creation, money management, portfolio, and error handling.
+ */
public class PlayerTest {
- private Player player;
-
- @BeforeEach
- void setUp() {
- player = new Player("Jane", new BigDecimal("1000"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testGetName() {
- assertEquals("Jane", player.getName());
- }
-
- @Test
- void testGetMoneyMatchesStartingCapital() {
- assertEquals(new BigDecimal("1000"), player.getMoney());
- }
-
- @Test
- void testAddMoney() {
- player.addMoney(new BigDecimal("500"));
- assertEquals(new BigDecimal("1500"), player.getMoney());
- }
-
- @Test
- void testAddMoneyZero() {
- player.addMoney(BigDecimal.ZERO);
- assertEquals(new BigDecimal("1000"), player.getMoney());
- }
-
- @Test
- void testWithdrawMoney() {
- player.withdrawMoney(new BigDecimal("400"));
- assertEquals(new BigDecimal("600"), player.getMoney());
- }
-
- @Test
- void testWithdrawMoneyZero() {
- player.withdrawMoney(BigDecimal.ZERO);
- assertEquals(new BigDecimal("1000"), player.getMoney());
- }
-
- @Test
- void testPortfolioInitiallyEmpty() {
- assertTrue(player.getPortfolio().getShares().isEmpty());
- }
-
- @Test
- void testTransactionArchiveInitiallyEmpty() {
- assertTrue(player.getTransactionArchive().isEmpty());
- }
-
- @Test
- void testStartingMoneyZeroAllowed() {
- Player broke = new Player("Broke", BigDecimal.ZERO);
- assertEquals(BigDecimal.ZERO, broke.getMoney());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testNullNameThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Player(null, new BigDecimal("1000"))
- );
- }
-
- @Test
- void testBlankNameThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Player(" ", new BigDecimal("1000"))
- );
- }
-
- @Test
- void testNullStartingMoneyThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Player("Jane", null)
- );
- }
-
- @Test
- void testNegativeStartingMoneyThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Player("Jane", new BigDecimal("-1"))
- );
- }
-
- @Test
- void testAddNullAmountThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- player.addMoney(null)
- );
- }
-
- @Test
- void testAddNegativeAmountThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- player.addMoney(new BigDecimal("-100"))
- );
- }
-
- @Test
- void testWithdrawNullAmountThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- player.withdrawMoney(null)
- );
- }
-
- @Test
- void testWithdrawNegativeAmountThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- player.withdrawMoney(new BigDecimal("-100"))
- );
- }
+ private Player player;
+
+ @BeforeEach
+ void setUp() {
+ player = new Player("Jane", new BigDecimal("1000"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that player name is stored and retrieved correctly.
+ */
+ @Test
+ void testGetName() {
+ assertEquals("Jane", player.getName());
+ }
+
+ /**
+ * Tests that player's initial money equals starting capital.
+ */
+ @Test
+ void testGetMoneyMatchesStartingCapital() {
+ assertEquals(new BigDecimal("1000"), player.getMoney());
+ }
+
+ /**
+ * Tests that adding money increases player's balance.
+ */
+ @Test
+ void testAddMoney() {
+ player.addMoney(new BigDecimal("500"));
+ assertEquals(new BigDecimal("1500"), player.getMoney());
+ }
+
+ /**
+ * Tests that adding zero money does not change balance.
+ */
+ @Test
+ void testAddMoneyZero() {
+ player.addMoney(BigDecimal.ZERO);
+ assertEquals(new BigDecimal("1000"), player.getMoney());
+ }
+
+ /**
+ * Tests that withdrawing money decreases player's balance.
+ */
+ @Test
+ void testWithdrawMoney() {
+ player.withdrawMoney(new BigDecimal("400"));
+ assertEquals(new BigDecimal("600"), player.getMoney());
+ }
+
+ /**
+ * Tests that withdrawing zero money does not change balance.
+ */
+ @Test
+ void testWithdrawMoneyZero() {
+ player.withdrawMoney(BigDecimal.ZERO);
+ assertEquals(new BigDecimal("1000"), player.getMoney());
+ }
+
+ /**
+ * Tests that player's portfolio is initially empty.
+ */
+ @Test
+ void testPortfolioInitiallyEmpty() {
+ assertTrue(player.getPortfolio().getShares().isEmpty());
+ }
+
+ /**
+ * Tests that player's transaction archive is initially empty.
+ */
+ @Test
+ void testTransactionArchiveInitiallyEmpty() {
+ assertTrue(player.getTransactionArchive().isEmpty());
+ }
+
+ /**
+ * Tests that player can be created with zero starting money.
+ */
+ @Test
+ void testStartingMoneyZeroAllowed() {
+ Player broke = new Player("Broke", BigDecimal.ZERO);
+ assertEquals(BigDecimal.ZERO, broke.getMoney());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that Player constructor throws when given a null name.
+ */
+ @Test
+ void testNullNameThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Player(null, new BigDecimal("1000"))
+ );
+ }
+
+ /**
+ * Tests that Player constructor throws when given a blank name.
+ */
+ @Test
+ void testBlankNameThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Player(" ", new BigDecimal("1000"))
+ );
+ }
+
+ /**
+ * Tests that Player constructor throws when given null starting money.
+ */
+ @Test
+ void testNullStartingMoneyThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Player("Jane", null)
+ );
+ }
+
+ /**
+ * Tests that Player constructor throws when given negative starting money.
+ */
+ @Test
+ void testNegativeStartingMoneyThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Player("Jane", new BigDecimal("-1"))
+ );
+ }
+
+ /**
+ * Tests that addMoney throws when given null amount.
+ */
+ @Test
+ void testAddNullAmountThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ player.addMoney(null)
+ );
+ }
+
+ /**
+ * Tests that addMoney throws when given negative amount.
+ */
+ @Test
+ void testAddNegativeAmountThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ player.addMoney(new BigDecimal("-100"))
+ );
+ }
+
+ /**
+ * Tests that withdrawMoney throws when given null amount.
+ */
+ @Test
+ void testWithdrawNullAmountThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ player.withdrawMoney(null)
+ );
+ }
+
+ /**
+ * Tests that withdrawMoney throws when given negative amount.
+ */
+ @Test
+ void testWithdrawNegativeAmountThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ player.withdrawMoney(new BigDecimal("-100"))
+ );
+ }
}
+
diff --git a/src/test/java/PortfolioTest.java b/src/test/java/PortfolioTest.java
index 34e46aa..359f3c5 100644
--- a/src/test/java/PortfolioTest.java
+++ b/src/test/java/PortfolioTest.java
@@ -1,155 +1,210 @@
import static org.junit.jupiter.api.Assertions.*;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
import Model.Portfolio;
import Model.Share;
import Model.Stock;
-
import java.math.BigDecimal;
import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+/**
+ * Unit tests for the Portfolio class.
+ * Tests share management, portfolio queries, net worth calculation, and error
+ * handling.
+ */
public class PortfolioTest {
- private Portfolio portfolio;
- private Stock stock;
- private Share share;
-
- @BeforeEach
- void setUp() {
- portfolio = new Portfolio();
- stock = new Stock("AAPL", "Apple", new BigDecimal("150"));
- share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testAddShare() {
- boolean result = portfolio.addShare(share);
- assertTrue(result);
- assertTrue(portfolio.contains(share));
- }
-
- @Test
- void testAddMultipleShares() {
- Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200"));
- Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190"));
-
- portfolio.addShare(share);
- portfolio.addShare(share2);
-
- assertEquals(2, portfolio.getShares().size());
- }
-
- @Test
- void testRemoveShare() {
- portfolio.addShare(share);
- boolean result = portfolio.removeShare(share);
- assertTrue(result);
- assertFalse(portfolio.contains(share));
- }
-
- @Test
- void testRemoveShareNotInPortfolioReturnsFalse() {
- boolean result = portfolio.removeShare(share);
- assertFalse(result);
- }
-
- @Test
- void testGetSharesReturnsAll() {
- portfolio.addShare(share);
- List shares = portfolio.getShares();
- assertEquals(1, shares.size());
- assertTrue(shares.contains(share));
- }
-
- @Test
- void testGetSharesIsDefensiveCopy() {
- portfolio.addShare(share);
- List copy = portfolio.getShares();
- copy.clear();
- assertEquals(1, portfolio.getShares().size());
- }
-
- @Test
- void testGetSharesBySymbol() {
- Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200"));
- Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190"));
-
- portfolio.addShare(share);
- portfolio.addShare(share2);
-
- List result = portfolio.getShares("AAPL");
- assertEquals(1, result.size());
- assertTrue(result.contains(share));
- }
-
- @Test
- void testGetSharesBySymbolNoMatch() {
- portfolio.addShare(share);
- List result = portfolio.getShares("MSFT");
- assertTrue(result.isEmpty());
- }
-
- @Test
- void testContainsReturnsTrueWhenPresent() {
- portfolio.addShare(share);
- assertTrue(portfolio.contains(share));
- }
-
- @Test
- void testContainsReturnsFalseWhenAbsent() {
- assertFalse(portfolio.contains(share));
- }
-
- @Test
- void testGetNetWorthEmptyPortfolio() {
- assertEquals(BigDecimal.ZERO, portfolio.getNetWorth());
- }
-
- @Test
- void testGetNetWorthWithShares() {
- portfolio.addShare(share);
- // net worth should be positive when portfolio has holdings
- assertTrue(portfolio.getNetWorth().compareTo(BigDecimal.ZERO) > 0);
- }
-
- // ---- Negative tests ----
-
- @Test
- void testAddNullShareThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- portfolio.addShare(null)
- );
- }
-
- @Test
- void testRemoveNullShareThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- portfolio.removeShare(null)
- );
- }
-
- @Test
- void testContainsNullShareThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- portfolio.contains(null)
- );
- }
-
- @Test
- void testGetSharesByNullSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- portfolio.getShares((String) null)
- );
- }
-
- @Test
- void testGetSharesByBlankSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- portfolio.getShares(" ")
- );
- }
+ private Portfolio portfolio;
+ private Stock stock;
+ private Share share;
+
+ @BeforeEach
+ void setUp() {
+ portfolio = new Portfolio();
+ stock = new Stock("AAPL", "Apple", new BigDecimal("150"));
+ share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that adding a share to portfolio returns true and succeeds.
+ */
+ @Test
+ void testAddShare() {
+ boolean result = portfolio.addShare(share);
+ assertTrue(result);
+ assertTrue(portfolio.contains(share));
+ }
+
+ /**
+ * Tests that portfolio can store multiple shares from different stocks.
+ */
+ @Test
+ void testAddMultipleShares() {
+ Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200"));
+ Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190"));
+
+ portfolio.addShare(share);
+ portfolio.addShare(share2);
+
+ assertEquals(2, portfolio.getShares().size());
+ }
+
+ /**
+ * Tests that removing a share from portfolio returns true and succeeds.
+ */
+ @Test
+ void testRemoveShare() {
+ portfolio.addShare(share);
+ boolean result = portfolio.removeShare(share);
+ assertTrue(result);
+ assertFalse(portfolio.contains(share));
+ }
+
+ /**
+ * Tests that removing a share not in portfolio returns false.
+ */
+ @Test
+ void testRemoveShareNotInPortfolioReturnsFalse() {
+ boolean result = portfolio.removeShare(share);
+ assertFalse(result);
+ }
+
+ /**
+ * Tests that getShares returns all shares in portfolio.
+ */
+ @Test
+ void testGetSharesReturnsAll() {
+ portfolio.addShare(share);
+ List shares = portfolio.getShares();
+ assertEquals(1, shares.size());
+ assertTrue(shares.contains(share));
+ }
+
+ /**
+ * Tests that getShares returns a defensive copy that cannot modify internal
+ * state.
+ */
+ @Test
+ void testGetSharesIsDefensiveCopy() {
+ portfolio.addShare(share);
+ List copy = portfolio.getShares();
+ copy.clear();
+ assertEquals(1, portfolio.getShares().size());
+ }
+
+ /**
+ * Tests that getShares by symbol returns only shares of that stock.
+ */
+ @Test
+ void testGetSharesBySymbol() {
+ Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200"));
+ Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190"));
+
+ portfolio.addShare(share);
+ portfolio.addShare(share2);
+
+ List result = portfolio.getShares("AAPL");
+ assertEquals(1, result.size());
+ assertTrue(result.contains(share));
+ }
+
+ /**
+ * Tests that getShares by symbol returns empty list when no matches.
+ */
+ @Test
+ void testGetSharesBySymbolNoMatch() {
+ portfolio.addShare(share);
+ List result = portfolio.getShares("MSFT");
+ assertTrue(result.isEmpty());
+ }
+
+ /**
+ * Tests that contains returns true for shares in portfolio.
+ */
+ @Test
+ void testContainsReturnsTrueWhenPresent() {
+ portfolio.addShare(share);
+ assertTrue(portfolio.contains(share));
+ }
+
+ /**
+ * Tests that contains returns false for shares not in portfolio.
+ */
+ @Test
+ void testContainsReturnsFalseWhenAbsent() {
+ assertFalse(portfolio.contains(share));
+ }
+
+ /**
+ * Tests that empty portfolio has zero net worth.
+ */
+ @Test
+ void testGetNetWorthEmptyPortfolio() {
+ assertEquals(BigDecimal.ZERO, portfolio.getNetWorth());
+ }
+
+ /**
+ * Tests that portfolio with shares has positive net worth.
+ */
+ @Test
+ void testGetNetWorthWithShares() {
+ portfolio.addShare(share);
+ // net worth should be positive when portfolio has holdings
+ assertTrue(portfolio.getNetWorth().compareTo(BigDecimal.ZERO) > 0);
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that addShare throws when given null share.
+ */
+ @Test
+ void testAddNullShareThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ portfolio.addShare(null)
+ );
+ }
+
+ /**
+ * Tests that removeShare throws when given null share.
+ */
+ @Test
+ void testRemoveNullShareThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ portfolio.removeShare(null)
+ );
+ }
+
+ /**
+ * Tests that contains throws when given null share.
+ */
+ @Test
+ void testContainsNullShareThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ portfolio.contains(null)
+ );
+ }
+
+ /**
+ * Tests that getShares throws when given null symbol.
+ */
+ @Test
+ void testGetSharesByNullSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ portfolio.getShares((String) null)
+ );
+ }
+
+ /**
+ * Tests that getShares throws when given blank symbol.
+ */
+ @Test
+ void testGetSharesByBlankSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ portfolio.getShares(" ")
+ );
+ }
}
diff --git a/src/test/java/PurchaseTest.java b/src/test/java/PurchaseTest.java
index d41e71a..de1c2ca 100644
--- a/src/test/java/PurchaseTest.java
+++ b/src/test/java/PurchaseTest.java
@@ -1,103 +1,139 @@
import static org.junit.jupiter.api.Assertions.*;
-import java.math.BigDecimal;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
import Model.Player;
import Model.Purchase;
import Model.Share;
import Model.Stock;
+import java.math.BigDecimal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+/**
+ * Unit tests for the Purchase class.
+ * Tests purchase transaction handling, commitment, and error handling.
+ */
public class PurchaseTest {
- private Stock stock;
- private Share share;
- private Purchase purchase;
- private Player player;
-
- @BeforeEach
- void setUp() {
- stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
- share = new Share(stock, new BigDecimal("20"), new BigDecimal("50"));
- purchase = new Purchase(share, 1);
- player = new Player("Jane", new BigDecimal("500000"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testCommitDeductsMoneyFromPlayer() {
- BigDecimal before = player.getMoney();
- purchase.commit(player);
- assertTrue(player.getMoney().compareTo(before) < 0);
- }
-
- @Test
- void testCommitAddsShareToPortfolio() {
- purchase.commit(player);
- assertTrue(player.getPortfolio().contains(share));
- }
-
- @Test
- void testCommitRecordedInArchive() {
- purchase.commit(player);
- assertTrue(player.getTransactionArchive().getAllTransactions().contains(purchase));
- }
-
- @Test
- void testCommitSetsCommittedFlag() {
- purchase.commit(player);
- assertTrue(purchase.isCommitted());
- }
-
- @Test
- void testGetShareReturnsCorrectShare() {
- assertEquals(share, purchase.getShare());
- }
-
- @Test
- void testGetWeekReturnsCorrectWeek() {
- assertEquals(1, purchase.getWeek());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testCommitNullPlayerThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- purchase.commit(null)
- );
- }
-
- @Test
- void testCommitInsufficientFundsThrows() {
- Player poorPlayer = new Player("Broke", new BigDecimal("1"));
- assertThrows(IllegalStateException.class, () ->
- purchase.commit(poorPlayer)
- );
- }
-
- @Test
- void testCommitAlreadyCommittedThrows() {
- purchase.commit(player);
- assertThrows(IllegalStateException.class, () ->
- purchase.commit(player)
- );
- }
-
- @Test
- void testConstructorWeekZeroThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Purchase(share, 0)
- );
- }
-
- @Test
- void testConstructorNegativeWeekThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Purchase(share, -5)
- );
- }
+ private Stock stock;
+ private Share share;
+ private Purchase purchase;
+ private Player player;
+
+ @BeforeEach
+ void setUp() {
+ stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ share = new Share(stock, new BigDecimal("20"), new BigDecimal("50"));
+ purchase = new Purchase(share, 1);
+ player = new Player("Jane", new BigDecimal("500000"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that committing a purchase deducts money from player's balance.
+ */
+ @Test
+ void testCommitDeductsMoneyFromPlayer() {
+ BigDecimal before = player.getMoney();
+ purchase.commit(player);
+ assertTrue(player.getMoney().compareTo(before) < 0);
+ }
+
+ /**
+ * Tests that committing a purchase adds the share to player's portfolio.
+ */
+ @Test
+ void testCommitAddsShareToPortfolio() {
+ purchase.commit(player);
+ assertTrue(player.getPortfolio().contains(share));
+ }
+
+ /**
+ * Tests that committed purchase is recorded in transaction archive.
+ */
+ @Test
+ void testCommitRecordedInArchive() {
+ purchase.commit(player);
+ assertTrue(player.getTransactionArchive().getAllTransactions().contains(purchase));
+ }
+
+ /**
+ * Tests that committing a purchase sets the committed flag.
+ */
+ @Test
+ void testCommitSetsCommittedFlag() {
+ purchase.commit(player);
+ assertTrue(purchase.isCommitted());
+ }
+
+ /**
+ * Tests that getShare returns the correct share object.
+ */
+ @Test
+ void testGetShareReturnsCorrectShare() {
+ assertEquals(share, purchase.getShare());
+ }
+
+ /**
+ * Tests that getWeek returns the purchase week.
+ */
+ @Test
+ void testGetWeekReturnsCorrectWeek() {
+ assertEquals(1, purchase.getWeek());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that commit throws when given null player.
+ */
+ @Test
+ void testCommitNullPlayerThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ purchase.commit(null)
+ );
+ }
+
+ /**
+ * Tests that commit throws when player has insufficient funds.
+ */
+ @Test
+ void testCommitInsufficientFundsThrows() {
+ Player poorPlayer = new Player("Broke", new BigDecimal("1"));
+ assertThrows(IllegalStateException.class, () ->
+ purchase.commit(poorPlayer)
+ );
+ }
+
+ /**
+ * Tests that commit throws when purchase was already committed.
+ */
+ @Test
+ void testCommitAlreadyCommittedThrows() {
+ purchase.commit(player);
+ assertThrows(IllegalStateException.class, () ->
+ purchase.commit(player)
+ );
+ }
+
+ /**
+ * Tests that Purchase constructor throws when given zero week.
+ */
+ @Test
+ void testConstructorWeekZeroThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Purchase(share, 0)
+ );
+ }
+
+ /**
+ * Tests that Purchase constructor throws when given negative week.
+ */
+ @Test
+ void testConstructorNegativeWeekThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Purchase(share, -5)
+ );
+ }
}
+
diff --git a/src/test/java/SaleTest.java b/src/test/java/SaleTest.java
index 622f262..31efe4d 100644
--- a/src/test/java/SaleTest.java
+++ b/src/test/java/SaleTest.java
@@ -1,104 +1,141 @@
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import Model.Player;
import Model.Sale;
import Model.Share;
import Model.Stock;
-
-import static org.junit.jupiter.api.Assertions.*;
-
import java.math.BigDecimal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the Sale class.
+ * Tests sale transaction handling, commitment, and error handling.
+ */
public class SaleTest {
- private Stock stock;
- private Share share;
- private Sale sale;
- private Player player;
-
- @BeforeEach
- void setUp() {
- stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
- share = new Share(stock, new BigDecimal("20"), new BigDecimal("80"));
- sale = new Sale(share, 1);
- player = new Player("Jane", new BigDecimal("500"));
- player.getPortfolio().addShare(share);
- }
-
- // ---- Positive tests ----
-
- @Test
- void testCommitAddsMoney() {
- BigDecimal before = player.getMoney();
- sale.commit(player);
- assertTrue(player.getMoney().compareTo(before) > 0);
- }
-
- @Test
- void testCommitRemovesShareFromPortfolio() {
- sale.commit(player);
- assertFalse(player.getPortfolio().contains(share));
- }
-
- @Test
- void testCommitRecordedInArchive() {
- sale.commit(player);
- assertTrue(player.getTransactionArchive().getAllTransactions().contains(sale));
- }
-
- @Test
- void testCommitSetsCommittedFlag() {
- sale.commit(player);
- assertTrue(sale.isCommitted());
- }
-
- @Test
- void testGetShareReturnsCorrectShare() {
- assertEquals(share, sale.getShare());
- }
-
- @Test
- void testGetWeekReturnsCorrectWeek() {
- assertEquals(1, sale.getWeek());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testCommitNullPlayerThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- sale.commit(null)
- );
- }
-
- @Test
- void testCommitAlreadyCommittedThrows() {
- sale.commit(player);
- assertThrows(IllegalStateException.class, () ->
- sale.commit(player)
- );
- }
-
- @Test
- void testCommitShareNotInPortfolioThrows() {
- Player otherPlayer = new Player("Bob", new BigDecimal("500"));
- assertThrows(IllegalStateException.class, () ->
- sale.commit(otherPlayer)
- );
- }
-
- @Test
- void testConstructorWeekZeroThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Sale(share, 0)
- );
- }
-
- @Test
- void testConstructorNegativeWeekThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Sale(share, -3)
- );
- }
+ private Stock stock;
+ private Share share;
+ private Sale sale;
+ private Player player;
+
+ @BeforeEach
+ void setUp() {
+ stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ share = new Share(stock, new BigDecimal("20"), new BigDecimal("80"));
+ sale = new Sale(share, 1);
+ player = new Player("Jane", new BigDecimal("500"));
+ player.getPortfolio().addShare(share);
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that committing a sale adds money to player's balance.
+ */
+ @Test
+ void testCommitAddsMoney() {
+ BigDecimal before = player.getMoney();
+ sale.commit(player);
+ assertTrue(player.getMoney().compareTo(before) > 0);
+ }
+
+ /**
+ * Tests that committing a sale removes the share from player's portfolio.
+ */
+ @Test
+ void testCommitRemovesShareFromPortfolio() {
+ sale.commit(player);
+ assertFalse(player.getPortfolio().contains(share));
+ }
+
+ /**
+ * Tests that committed sale is recorded in transaction archive.
+ */
+ @Test
+ void testCommitRecordedInArchive() {
+ sale.commit(player);
+ assertTrue(player.getTransactionArchive().getAllTransactions().contains(sale));
+ }
+
+ /**
+ * Tests that committing a sale sets the committed flag.
+ */
+ @Test
+ void testCommitSetsCommittedFlag() {
+ sale.commit(player);
+ assertTrue(sale.isCommitted());
+ }
+
+ /**
+ * Tests that getShare returns the correct share object.
+ */
+ @Test
+ void testGetShareReturnsCorrectShare() {
+ assertEquals(share, sale.getShare());
+ }
+
+ /**
+ * Tests that getWeek returns the sale week.
+ */
+ @Test
+ void testGetWeekReturnsCorrectWeek() {
+ assertEquals(1, sale.getWeek());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that commit throws when given null player.
+ */
+ @Test
+ void testCommitNullPlayerThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ sale.commit(null)
+ );
+ }
+
+ /**
+ * Tests that commit throws when sale was already committed.
+ */
+ @Test
+ void testCommitAlreadyCommittedThrows() {
+ sale.commit(player);
+ assertThrows(IllegalStateException.class, () ->
+ sale.commit(player)
+ );
+ }
+
+ /**
+ * Tests that commit throws when share is not in player's portfolio.
+ */
+ @Test
+ void testCommitShareNotInPortfolioThrows() {
+ Player otherPlayer = new Player("Bob", new BigDecimal("500"));
+ assertThrows(IllegalStateException.class, () ->
+ sale.commit(otherPlayer)
+ );
+ }
+
+ /**
+ * Tests that Sale constructor throws when given zero week.
+ */
+ @Test
+ void testConstructorWeekZeroThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Sale(share, 0)
+ );
+ }
+
+ /**
+ * Tests that Sale constructor throws when given negative week.
+ */
+ @Test
+ void testConstructorNegativeWeekThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Sale(share, -3)
+ );
+ }
}
+
diff --git a/src/test/java/ShareTest.java b/src/test/java/ShareTest.java
index fbc018d..37b5422 100644
--- a/src/test/java/ShareTest.java
+++ b/src/test/java/ShareTest.java
@@ -1,96 +1,134 @@
import static org.junit.jupiter.api.Assertions.*;
+import Model.Share;
+import Model.Stock;
+import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import Model.Share;
-import Model.Stock;
-import java.math.BigDecimal;
+/**
+ * Unit tests for the Share class.
+ * Tests share creation, property access, and error handling.
+ */
public class ShareTest {
- private Stock stock;
-
- @BeforeEach
- void setUp() {
- stock = new Stock("AAPL", "Apple", new BigDecimal("150"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testShareConstructorNotNull() {
- Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
- assertNotNull(share);
- }
-
- @Test
- void testGetStock() {
- Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
- assertEquals(stock, share.getStock());
- }
-
- @Test
- void testGetQuantity() {
- Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
- assertEquals(new BigDecimal("10"), share.getQuantity());
- }
-
- @Test
- void testGetPurchasePrice() {
- Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
- assertEquals(new BigDecimal("140"), share.getPurchasePrice());
- }
-
- @Test
- void testPurchasePriceZeroAllowed() {
- // buying at price 0 is an edge case but not invalid
- Share share = new Share(stock, new BigDecimal("5"), BigDecimal.ZERO);
- assertEquals(BigDecimal.ZERO, share.getPurchasePrice());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testNullStockThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(null, new BigDecimal("10"), new BigDecimal("100"))
- );
- }
-
- @Test
- void testNullQuantityThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(stock, null, new BigDecimal("100"))
- );
- }
-
- @Test
- void testZeroQuantityThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(stock, BigDecimal.ZERO, new BigDecimal("100"))
- );
- }
-
- @Test
- void testNegativeQuantityThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(stock, new BigDecimal("-5"), new BigDecimal("100"))
- );
- }
-
- @Test
- void testNullPurchasePriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(stock, new BigDecimal("10"), null)
- );
- }
-
- @Test
- void testNegativePurchasePriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Share(stock, new BigDecimal("10"), new BigDecimal("-50"))
- );
- }
+ private Stock stock;
+
+ @BeforeEach
+ void setUp() {
+ stock = new Stock("AAPL", "Apple", new BigDecimal("150"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that Share constructor creates a non-null object.
+ */
+ @Test
+ void testShareConstructorNotNull() {
+ Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
+ assertNotNull(share);
+ }
+
+ /**
+ * Tests that getStock returns the correct stock object.
+ */
+ @Test
+ void testGetStock() {
+ Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
+ assertEquals(stock, share.getStock());
+ }
+
+ /**
+ * Tests that getQuantity returns the correct quantity.
+ */
+ @Test
+ void testGetQuantity() {
+ Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
+ assertEquals(new BigDecimal("10"), share.getQuantity());
+ }
+
+ /**
+ * Tests that getPurchasePrice returns the correct purchase price.
+ */
+ @Test
+ void testGetPurchasePrice() {
+ Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140"));
+ assertEquals(new BigDecimal("140"), share.getPurchasePrice());
+ }
+
+ /**
+ * Tests that share can be created with zero purchase price.
+ */
+ @Test
+ void testPurchasePriceZeroAllowed() {
+ // buying at price 0 is an edge case but not invalid
+ Share share = new Share(stock, new BigDecimal("5"), BigDecimal.ZERO);
+ assertEquals(BigDecimal.ZERO, share.getPurchasePrice());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that Share constructor throws when given null stock.
+ */
+ @Test
+ void testNullStockThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(null, new BigDecimal("10"), new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Share constructor throws when given null quantity.
+ */
+ @Test
+ void testNullQuantityThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, null, new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Share constructor throws when given zero quantity.
+ */
+ @Test
+ void testZeroQuantityThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, BigDecimal.ZERO, new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Share constructor throws when given negative quantity.
+ */
+ @Test
+ void testNegativeQuantityThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, new BigDecimal("-5"), new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Share constructor throws when given null purchase price.
+ */
+ @Test
+ void testNullPurchasePriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, new BigDecimal("10"), null)
+ );
+ }
+
+ /**
+ * Tests that Share constructor throws when given negative purchase price.
+ */
+ @Test
+ void testNegativePurchasePriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, new BigDecimal("10"), new BigDecimal("-50"))
+ );
+ }
}
+
diff --git a/src/test/java/StockTest.java b/src/test/java/StockTest.java
index 6a6a477..c07c75b 100644
--- a/src/test/java/StockTest.java
+++ b/src/test/java/StockTest.java
@@ -4,161 +4,231 @@
import Model.Stock;
import static org.junit.jupiter.api.Assertions.*;
-
import java.math.BigDecimal;
import java.util.List;
+/**
+ * Unit tests for the Stock class.
+ * Tests stock creation, price tracking, and error handling.
+ */
public class StockTest {
- private Stock stock;
-
- @BeforeEach
- void setUp() {
- stock = new Stock("AAPL", "Apple", new BigDecimal("1000"));
- }
-
- // ---- Positive tests ----
-
- @Test
- void testGetSymbol() {
- assertEquals("AAPL", stock.getSymbol());
- }
-
- @Test
- void testGetCompany() {
- assertEquals("Apple", stock.getCompany());
- }
-
- @Test
- void testGetSalesPrice() {
- assertEquals(new BigDecimal("1000"), stock.getSalesPrice());
- }
-
- @Test
- void testGetSalesPriceZero() {
- Stock zeroStock = new Stock("ZERO", "ZeroCorp", new BigDecimal("0"));
- assertEquals(new BigDecimal("0"), zeroStock.getSalesPrice());
- }
-
- @Test
- void testAddNewSalesPrice() {
- stock.addNewSalesPrice(new BigDecimal("1200"));
- assertEquals(new BigDecimal("1200"), stock.getSalesPrice());
- }
-
- @Test
- void testAddNewSalesPriceZero() {
- stock.addNewSalesPrice(new BigDecimal("0"));
- assertEquals(new BigDecimal("0"), stock.getSalesPrice());
- }
-
- @Test
- void testGetHistoricalPricesInitial() {
- List history = stock.getHistoricalPrices();
- assertEquals(1, history.size());
- assertEquals(new BigDecimal("1000"), history.get(0));
- }
-
- @Test
- void testGetHistoricalPricesAfterUpdates() {
- stock.addNewSalesPrice(new BigDecimal("1100"));
- stock.addNewSalesPrice(new BigDecimal("1200"));
- assertEquals(3, stock.getHistoricalPrices().size());
- }
-
- @Test
- void testGetHighestPrice() {
- stock.addNewSalesPrice(new BigDecimal("1500"));
- stock.addNewSalesPrice(new BigDecimal("800"));
- assertEquals(new BigDecimal("1500"), stock.getHighestPrice());
- }
-
- @Test
- void testGetLowestPrice() {
- stock.addNewSalesPrice(new BigDecimal("1500"));
- stock.addNewSalesPrice(new BigDecimal("800"));
- assertEquals(new BigDecimal("800"), stock.getLowestPrice());
- }
-
- @Test
- void testGetLatestPriceChangePositive() {
- stock.addNewSalesPrice(new BigDecimal("1100"));
- assertEquals(new BigDecimal("100"), stock.getLatestPriceChange());
- }
-
- @Test
- void testGetLatestPriceChangeNegative() {
- stock.addNewSalesPrice(new BigDecimal("900"));
- assertEquals(new BigDecimal("-100"), stock.getLatestPriceChange());
- }
-
- @Test
- void testGetLatestPriceChangeOnlyOnePrice() {
- assertEquals(BigDecimal.ZERO, stock.getLatestPriceChange());
- }
-
- @Test
- void testHistoricalPricesIsDefensiveCopy() {
- List history = stock.getHistoricalPrices();
- history.add(new BigDecimal("9999"));
- assertEquals(1, stock.getHistoricalPrices().size());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testNullSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock(null, "Apple", new BigDecimal("100"))
- );
- }
-
- @Test
- void testBlankSymbolThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock(" ", "Apple", new BigDecimal("100"))
- );
- }
-
- @Test
- void testNullCompanyThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock("AAPL", null, new BigDecimal("100"))
- );
- }
-
- @Test
- void testBlankCompanyThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock("AAPL", " ", new BigDecimal("100"))
- );
- }
-
- @Test
- void testNullInitialPriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock("AAPL", "Apple", null)
- );
- }
-
- @Test
- void testNegativeInitialPriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- new Stock("AAPL", "Apple", new BigDecimal("-1"))
- );
- }
-
- @Test
- void testAddNullPriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- stock.addNewSalesPrice(null)
- );
- }
-
- @Test
- void testAddNegativePriceThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- stock.addNewSalesPrice(new BigDecimal("-10"))
- );
- }
+ private Stock stock;
+
+ @BeforeEach
+ void setUp() {
+ stock = new Stock("AAPL", "Apple", new BigDecimal("1000"));
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that getSymbol returns the correct stock symbol.
+ */
+ @Test
+ void testGetSymbol() {
+ assertEquals("AAPL", stock.getSymbol());
+ }
+
+ /**
+ * Tests that getCompany returns the correct company name.
+ */
+ @Test
+ void testGetCompany() {
+ assertEquals("Apple", stock.getCompany());
+ }
+
+ /**
+ * Tests that getSalesPrice returns the current stock price.
+ */
+ @Test
+ void testGetSalesPrice() {
+ assertEquals(new BigDecimal("1000"), stock.getSalesPrice());
+ }
+
+ /**
+ * Tests that stock can have zero sales price.
+ */
+ @Test
+ void testGetSalesPriceZero() {
+ Stock zeroStock = new Stock("ZERO", "ZeroCorp", new BigDecimal("0"));
+ assertEquals(new BigDecimal("0"), zeroStock.getSalesPrice());
+ }
+
+ /**
+ * Tests that adding a new sales price updates the current price.
+ */
+ @Test
+ void testAddNewSalesPrice() {
+ stock.addNewSalesPrice(new BigDecimal("1200"));
+ assertEquals(new BigDecimal("1200"), stock.getSalesPrice());
+ }
+
+ /**
+ * Tests that zero price can be added to price history.
+ */
+ @Test
+ void testAddNewSalesPriceZero() {
+ stock.addNewSalesPrice(new BigDecimal("0"));
+ assertEquals(new BigDecimal("0"), stock.getSalesPrice());
+ }
+
+ /**
+ * Tests that historical prices initially contain only the initial price.
+ */
+ @Test
+ void testGetHistoricalPricesInitial() {
+ List history = stock.getHistoricalPrices();
+ assertEquals(1, history.size());
+ assertEquals(new BigDecimal("1000"), history.get(0));
+ }
+
+ /**
+ * Tests that historical prices accumulates all price changes.
+ */
+ @Test
+ void testGetHistoricalPricesAfterUpdates() {
+ stock.addNewSalesPrice(new BigDecimal("1100"));
+ stock.addNewSalesPrice(new BigDecimal("1200"));
+ assertEquals(3, stock.getHistoricalPrices().size());
+ }
+
+ /**
+ * Tests that getHighestPrice returns the maximum price from history.
+ */
+ @Test
+ void testGetHighestPrice() {
+ stock.addNewSalesPrice(new BigDecimal("1500"));
+ stock.addNewSalesPrice(new BigDecimal("800"));
+ assertEquals(new BigDecimal("1500"), stock.getHighestPrice());
+ }
+
+ /**
+ * Tests that getLowestPrice returns the minimum price from history.
+ */
+ @Test
+ void testGetLowestPrice() {
+ stock.addNewSalesPrice(new BigDecimal("1500"));
+ stock.addNewSalesPrice(new BigDecimal("800"));
+ assertEquals(new BigDecimal("800"), stock.getLowestPrice());
+ }
+
+ /**
+ * Tests that getLatestPriceChange returns positive change for price increase.
+ */
+ @Test
+ void testGetLatestPriceChangePositive() {
+ stock.addNewSalesPrice(new BigDecimal("1100"));
+ assertEquals(new BigDecimal("100"), stock.getLatestPriceChange());
+ }
+
+ /**
+ * Tests that getLatestPriceChange returns negative change for price decrease.
+ */
+ @Test
+ void testGetLatestPriceChangeNegative() {
+ stock.addNewSalesPrice(new BigDecimal("900"));
+ assertEquals(new BigDecimal("-100"), stock.getLatestPriceChange());
+ }
+
+ /**
+ * Tests that price change is zero when only one price exists.
+ */
+ @Test
+ void testGetLatestPriceChangeOnlyOnePrice() {
+ assertEquals(BigDecimal.ZERO, stock.getLatestPriceChange());
+ }
+
+ /**
+ * Tests that getHistoricalPrices returns a defensive copy.
+ */
+ @Test
+ void testHistoricalPricesIsDefensiveCopy() {
+ List history = stock.getHistoricalPrices();
+ history.add(new BigDecimal("9999"));
+ assertEquals(1, stock.getHistoricalPrices().size());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that Stock constructor throws when given null symbol.
+ */
+ @Test
+ void testNullSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock(null, "Apple", new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Stock constructor throws when given blank symbol.
+ */
+ @Test
+ void testBlankSymbolThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock(" ", "Apple", new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Stock constructor throws when given null company name.
+ */
+ @Test
+ void testNullCompanyThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock("AAPL", null, new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Stock constructor throws when given blank company name.
+ */
+ @Test
+ void testBlankCompanyThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock("AAPL", " ", new BigDecimal("100"))
+ );
+ }
+
+ /**
+ * Tests that Stock constructor throws when given null initial price.
+ */
+ @Test
+ void testNullInitialPriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock("AAPL", "Apple", null)
+ );
+ }
+
+ /**
+ * Tests that Stock constructor throws when given negative initial price.
+ */
+ @Test
+ void testNegativeInitialPriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Stock("AAPL", "Apple", new BigDecimal("-1"))
+ );
+ }
+
+ /**
+ * Tests that addNewSalesPrice throws when given null price.
+ */
+ @Test
+ void testAddNullPriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ stock.addNewSalesPrice(null)
+ );
+ }
+
+ /**
+ * Tests that addNewSalesPrice throws when given negative price.
+ */
+ @Test
+ void testAddNegativePriceThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ stock.addNewSalesPrice(new BigDecimal("-10"))
+ );
+ }
}
+
diff --git a/src/test/java/TransactionArchiveTest.java b/src/test/java/TransactionArchiveTest.java
index 14ff009..df7ed0c 100644
--- a/src/test/java/TransactionArchiveTest.java
+++ b/src/test/java/TransactionArchiveTest.java
@@ -1,148 +1,196 @@
import static org.junit.jupiter.api.Assertions.*;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
import Model.Player;
import Model.Purchase;
import Model.Sale;
import Model.Share;
import Model.Stock;
import Model.TransactionArchive;
-
import java.math.BigDecimal;
import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+/**
+ * Unit tests for the TransactionArchive class.
+ * Tests transaction recording, retrieval, filtering, and error handling.
+ */
public class TransactionArchiveTest {
- private TransactionArchive archive;
- private Stock stock;
- private Share share;
- private Purchase purchase;
- private Sale sale;
-
- @BeforeEach
- void setUp() {
- archive = new TransactionArchive();
- stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
- share = new Share(stock, new BigDecimal("10"), new BigDecimal("90"));
- purchase = new Purchase(share, 1);
- sale = new Sale(share, 2);
- }
-
- // ---- Positive tests ----
-
- @Test
- void testIsEmptyOnCreation() {
- assertTrue(archive.isEmpty());
- }
-
- @Test
- void testIsEmptyAfterAdd() {
- archive.add(purchase);
- assertFalse(archive.isEmpty());
- }
-
- @Test
- void testAddReturnsTrue() {
- assertTrue(archive.add(purchase));
- }
-
- @Test
- void testGetAllTransactions() {
- archive.add(purchase);
- archive.add(sale);
- assertEquals(2, archive.getAllTransactions().size());
- }
-
- @Test
- void testGetAllTransactionsIsDefensiveCopy() {
- archive.add(purchase);
- List> copy = archive.getAllTransactions();
- copy.clear();
- assertEquals(1, archive.getAllTransactions().size());
- }
-
- @Test
- void testGetTransactionsByWeek() {
- archive.add(purchase); // week 1
- archive.add(sale); // week 2
-
- List> week1 = archive.getTransactions(1);
- assertEquals(1, week1.size());
- assertTrue(week1.contains(purchase));
- }
-
- @Test
- void testGetTransactionsByWeekNoMatch() {
- archive.add(purchase); // week 1
- assertTrue(archive.getTransactions(99).isEmpty());
- }
-
- @Test
- void testGetPurchaseByWeek() {
- archive.add(purchase); // week 1
- archive.add(sale); // week 2
-
- List purchases = archive.getPurchase(1);
- assertEquals(1, purchases.size());
- assertTrue(purchases.contains(purchase));
- }
-
- @Test
- void testGetPurchaseByWeekExcludesSales() {
- // Add a sale at week 1 — should not appear in getPurchase(1)
- Sale saleWeek1 = new Sale(share, 1);
- archive.add(saleWeek1);
-
- assertTrue(archive.getPurchase(1).isEmpty());
- }
-
- @Test
- void testGetSaleByWeek() {
- archive.add(purchase); // week 1
- archive.add(sale); // week 2
-
- List sales = archive.getSale(2);
- assertEquals(1, sales.size());
- assertTrue(sales.contains(sale));
- }
-
- @Test
- void testGetSaleByWeekExcludesPurchases() {
- // Add a purchase at week 2 — should not appear in getSale(2)
- Purchase purchaseWeek2 = new Purchase(share, 2);
- archive.add(purchaseWeek2);
-
- assertTrue(archive.getSale(2).isEmpty());
- }
-
- @Test
- void testCountDistinctWeeksEmpty() {
- assertEquals(0, archive.countDistinctWeeks());
- }
-
- @Test
- void testCountDistinctWeeksMultipleTransactionsSameWeek() {
- Purchase purchase2 = new Purchase(share, 1);
- archive.add(purchase);
- archive.add(purchase2);
- assertEquals(1, archive.countDistinctWeeks());
- }
-
- @Test
- void testCountDistinctWeeksAcrossWeeks() {
- archive.add(purchase); // week 1
- archive.add(sale); // week 2
- assertEquals(2, archive.countDistinctWeeks());
- }
-
- // ---- Negative tests ----
-
- @Test
- void testAddNullThrows() {
- assertThrows(IllegalArgumentException.class, () ->
- archive.add(null)
- );
- }
+ private TransactionArchive archive;
+ private Stock stock;
+ private Share share;
+ private Purchase purchase;
+ private Sale sale;
+
+ @BeforeEach
+ void setUp() {
+ archive = new TransactionArchive();
+ stock = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ share = new Share(stock, new BigDecimal("10"), new BigDecimal("90"));
+ purchase = new Purchase(share, 1);
+ sale = new Sale(share, 2);
+ }
+
+ // ---- Positive tests ----
+
+ /**
+ * Tests that archive is empty immediately after creation.
+ */
+ @Test
+ void testIsEmptyOnCreation() {
+ assertTrue(archive.isEmpty());
+ }
+
+ /**
+ * Tests that isEmpty returns false after adding a transaction.
+ */
+ @Test
+ void testIsEmptyAfterAdd() {
+ archive.add(purchase);
+ assertFalse(archive.isEmpty());
+ }
+
+ /**
+ * Tests that add method returns true for successful addition.
+ */
+ @Test
+ void testAddReturnsTrue() {
+ assertTrue(archive.add(purchase));
+ }
+
+ /**
+ * Tests that getAllTransactions returns all stored transactions.
+ */
+ @Test
+ void testGetAllTransactions() {
+ archive.add(purchase);
+ archive.add(sale);
+ assertEquals(2, archive.getAllTransactions().size());
+ }
+
+ /**
+ * Tests that getAllTransactions returns a defensive copy.
+ */
+ @Test
+ void testGetAllTransactionsIsDefensiveCopy() {
+ archive.add(purchase);
+ List> copy = archive.getAllTransactions();
+ copy.clear();
+ assertEquals(1, archive.getAllTransactions().size());
+ }
+
+ /**
+ * Tests that getTransactions filters transactions by week number.
+ */
+ @Test
+ void testGetTransactionsByWeek() {
+ archive.add(purchase); // week 1
+ archive.add(sale); // week 2
+
+ List> week1 = archive.getTransactions(1);
+ assertEquals(1, week1.size());
+ assertTrue(week1.contains(purchase));
+ }
+
+ /**
+ * Tests that getTransactions returns empty list for non-existent week.
+ */
+ @Test
+ void testGetTransactionsByWeekNoMatch() {
+ archive.add(purchase); // week 1
+ assertTrue(archive.getTransactions(99).isEmpty());
+ }
+
+ /**
+ * Tests that getPurchase returns only purchase transactions for week.
+ */
+ @Test
+ void testGetPurchaseByWeek() {
+ archive.add(purchase); // week 1
+ archive.add(sale); // week 2
+
+ List purchases = archive.getPurchase(1);
+ assertEquals(1, purchases.size());
+ assertTrue(purchases.contains(purchase));
+ }
+
+ /**
+ * Tests that getPurchase does not return sale transactions.
+ */
+ @Test
+ void testGetPurchaseByWeekExcludesSales() {
+ // Add a sale at week 1 — should not appear in getPurchase(1)
+ Sale saleWeek1 = new Sale(share, 1);
+ archive.add(saleWeek1);
+
+ assertTrue(archive.getPurchase(1).isEmpty());
+ }
+
+ /**
+ * Tests that getSale returns only sale transactions for week.
+ */
+ @Test
+ void testGetSaleByWeek() {
+ archive.add(purchase); // week 1
+ archive.add(sale); // week 2
+
+ List sales = archive.getSale(2);
+ assertEquals(1, sales.size());
+ assertTrue(sales.contains(sale));
+ }
+
+ /**
+ * Tests that getSale does not return purchase transactions.
+ */
+ @Test
+ void testGetSaleByWeekExcludesPurchases() {
+ // Add a purchase at week 2 — should not appear in getSale(2)
+ Purchase purchaseWeek2 = new Purchase(share, 2);
+ archive.add(purchaseWeek2);
+
+ assertTrue(archive.getSale(2).isEmpty());
+ }
+
+ /**
+ * Tests that countDistinctWeeks returns zero for empty archive.
+ */
+ @Test
+ void testCountDistinctWeeksEmpty() {
+ assertEquals(0, archive.countDistinctWeeks());
+ }
+
+ /**
+ * Tests that countDistinctWeeks counts only distinct weeks.
+ */
+ @Test
+ void testCountDistinctWeeksMultipleTransactionsSameWeek() {
+ Purchase purchase2 = new Purchase(share, 1);
+ archive.add(purchase);
+ archive.add(purchase2);
+ assertEquals(1, archive.countDistinctWeeks());
+ }
+
+ /**
+ * Tests that countDistinctWeeks correctly counts across multiple weeks.
+ */
+ @Test
+ void testCountDistinctWeeksAcrossWeeks() {
+ archive.add(purchase); // week 1
+ archive.add(sale); // week 2
+ assertEquals(2, archive.countDistinctWeeks());
+ }
+
+ // ---- Negative tests ----
+
+ /**
+ * Tests that add throws when given null transaction.
+ */
+ @Test
+ void testAddNullThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ archive.add(null)
+ );
+ }
}
+
diff --git a/target/test-classes/ExchangeTest.class b/target/test-classes/ExchangeTest.class
index 238ca9d..c301cc2 100644
Binary files a/target/test-classes/ExchangeTest.class and b/target/test-classes/ExchangeTest.class differ
diff --git a/target/test-classes/PortfolioTest.class b/target/test-classes/PortfolioTest.class
index 679e0c4..6929863 100644
Binary files a/target/test-classes/PortfolioTest.class and b/target/test-classes/PortfolioTest.class differ
diff --git a/target/test-classes/ShareTest.class b/target/test-classes/ShareTest.class
index abf202d..bdbf4a6 100644
Binary files a/target/test-classes/ShareTest.class and b/target/test-classes/ShareTest.class differ
diff --git a/target/test-classes/StockTest.class b/target/test-classes/StockTest.class
index 0008700..c71a54d 100644
Binary files a/target/test-classes/StockTest.class and b/target/test-classes/StockTest.class differ