getMarket(final String searchTerm) {
*/
public void updateMarket() {
for (Stock stock : exchange.getAllStocks()) {
- stock.updatePrice();
- stock.updateStockGraph();
+ StockController controller = stockControllers.get(stock.getSymbol());
+ if (controller != null) {
+ stock.addNewSalesPrice(controller.updatePrice());
+ }
}
}
+ @Override
+ public void nextTick() {
+ updateMarket();
+ }
+
/**
* Initializes the market by generating stock history and graphs.
*/
public void startMarket() {
for (Stock stock : exchange.getAllStocks()) {
- stock.fakeHistory(stockResolution);
- stock.updateStockGraph();
-
+ StockController controller = stockControllers.get(stock.getSymbol());
+ if (controller != null) {
+ for (int i = 0; i < STOCK_RESOLUTION; i++) {
+ stock.addNewSalesPrice(controller.updatePrice());
+ }
+ }
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java
index e06705f..a101e46 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java
@@ -12,10 +12,16 @@
*/
public class StockController {
+ /** Threshold for absolute trend value to be considered "steep". */
private static final int STEEP_THRESHOLD = 46;
+ /** Magnitude multiplier for price shocks during steep trends. */
private static final int EVENT_MAGNITUDE = 90;
+ /** Maximum range for random price noise added each tick. */
private static final double NOISE_RANGE = 8.0;
+ /** Minimum number of frames a generated trend must last. */
private static final int MIN_TREND_FRAMES = 10;
+ /** Number of ticks to delay before applying a pending price shock during a steep trend. */
+ private static final int SHOCK_DELAY_TICKS = 20;
private final Stock stock;
@@ -95,7 +101,7 @@ private void nextTrend() {
if (isTrajectorySteep()) {
prepareEvent();
- delay = 20;
+ delay = SHOCK_DELAY_TICKS;
} else {
delay = 0;
pendingShock = null;
@@ -131,6 +137,11 @@ private boolean isDelay() {
return delay > 0;
}
+ /**
+ * Returns the current stock price.
+ *
+ * @return the current price
+ */
public BigDecimal getPrice() {
return price;
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/TimeAndWeatherController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/TimeAndWeatherController.java
index f05df71..103fbe2 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/TimeAndWeatherController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/TimeAndWeatherController.java
@@ -25,6 +25,12 @@ public class TimeAndWeatherController implements AppController {
private final GameController gameController;
private final AudioManager audioManager = AudioManager.get();
+ private static final int HOURS_PER_DAY = 24;
+ private static final int SATURDAY_INDEX = 6;
+ private static final int SUNDAY_INDEX = 0;
+ private static final int MIN_TEMPERATURE = -10;
+ private static final int MAX_TEMPERATURE = 30;
+
private static final Day[] DAYS = {Day.SUN, Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI, Day.SAT};
/**
@@ -43,25 +49,27 @@ public TimeAndWeatherController(GameController gameController) {
*/
@Override
public void nextTick() {
- int nextHour = hour.get() + 1;
- if (nextHour >= 24) {
- dayIndex.set((dayIndex.get() + 1) % 7);
- updateGameState();
- updateWeather();
- hour.set(0);
- Platform.runLater(() -> audioManager.playSfx(Audio.CLOCK));
- } else {
- hour.set(nextHour);
- }
+ Platform.runLater(() -> {
+ int nextHour = hour.get() + 1;
+ if (nextHour >= HOURS_PER_DAY) {
+ dayIndex.set((dayIndex.get() + 1) % 7);
+ updateGameState();
+ updateWeather();
+ hour.set(0);
+ audioManager.playSfx(Audio.CLOCK);
+ } else {
+ hour.set(nextHour);
+ }
+ });
}
/**
* Updates the weather and temperature values.
*/
private void updateWeather() {
- int change = random.nextInt(11) - 5; // -5 to +5
+ int change = random.nextInt(11) - 5;
int newTemp = temperature.get() + change;
- newTemp = Math.min(Math.max(newTemp, -10), 30);
+ newTemp = Math.min(Math.max(newTemp, MIN_TEMPERATURE), MAX_TEMPERATURE);
temperature.set(newTemp);
@@ -157,13 +165,17 @@ public String getWeekString() {
}
/**
- * Updates the current game state based on the day.
+ * Updates the current game state based on the day index.
+ *
+ * Saturday (index 6) → {@link GameState#RENTDAY};
+ * Sunday (index 0) → {@link GameState#FREEDAY};
+ * all other days → {@link GameState#WORKDAY}.
*/
public void updateGameState() {
- if (dayIndex.get() == 6) {
+ if (dayIndex.get() == SATURDAY_INDEX) {
gameController.setGameState(GameState.RENTDAY);
return;
- } else if (dayIndex.get() == 0) {
+ } else if (dayIndex.get() == SUNDAY_INDEX) {
gameController.setGameState(GameState.FREEDAY);
return;
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/AppStoreController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/AppStoreController.java
index b63f3d5..43be481 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/AppStoreController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/AppStoreController.java
@@ -13,10 +13,8 @@ public class AppStoreController implements AppController {
/**
* Constructs the App Store controller.
- *
- * @param appStoreApp the App Store view component
*/
- public AppStoreController(final AppStoreApp appStoreApp) {
+ public AppStoreController() {
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/StockAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/StockAppController.java
index 80a8b12..10add7b 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/StockAppController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/StockAppController.java
@@ -23,6 +23,9 @@
/** Controller for StockApp. */
public final class StockAppController implements AppController {
+ private static final System.Logger LOGGER =
+ System.getLogger(StockAppController.class.getName());
+
private final MarketController marketController;
private final StockApp stockApp;
private final Player player;
@@ -97,9 +100,9 @@ public StockAppController(
stockApp.getQuantitySpinner().valueProperty().addListener(
(obs, old, newValue) -> {
- if (isWeekend) {
+ if (isWeekend && onMarketClosed != null) {
onMarketClosed.run();
- }
+ }
if (currentStock != null) {
updateReceipt();
}
@@ -113,17 +116,15 @@ public StockAppController(
/** Navigates to stock detail page. */
public void navigateToStock(final Stock stock) {
currentStock = stock;
- stock.getStockGraph().setVisibility(true);
- Platform.runLater(stock::updateStockGraph);
+ stockApp.getStockGraph().setVisibility(true);
stockApp.showStockPage(stock);
+ stockApp.getStockGraph().update(stock);
updateReceipt();
}
/** Navigates to search page. */
private void navigateToSearch() {
- if (currentStock != null) {
- currentStock.getStockGraph().setVisibility(false);
- }
+ stockApp.getStockGraph().setVisibility(false);
currentStock = null;
stockApp.showSearchPage();
}
@@ -262,20 +263,23 @@ private void handleTransaction() {
if (value > 0) {
try {
handleBuy(currentStock, quantity);
+ } catch (edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.InsufficientFunds e) {
+ if (onInsufficientFunds != null) {
+ onInsufficientFunds.run();
+ }
} catch (Exception e) {
- onInsufficientFunds.run();
+ LOGGER.log(System.Logger.Level.WARNING, "Unexpected error during buy transaction", e);
}
} else {
handleSell(currentStock, quantity);
}
}
- private void handleBuy(final Stock stock, final BigDecimal quantity) throws Exception {
+ private void handleBuy(final Stock stock, final BigDecimal quantity) {
Transaction transaction = marketController.getExchange()
.buy(stock.getSymbol(), quantity);
if (transaction != null) {
transaction.commit(player);
- player.getPortfolio().addShare(transaction.getShare());
player.updateStatus();
}
}
@@ -294,11 +298,10 @@ private void handleSell(final Stock stock, final BigDecimal quantity) {
if (transaction != null) {
try {
transaction.commit(player);
+ player.updateStatus();
} catch (Exception e) {
- System.out.println("Transaction failed");
+ LOGGER.log(System.Logger.Level.WARNING, "Unexpected error during sell transaction", e);
}
- player.getPortfolio().removeShare(transaction.getShare());
- player.updateStatus();
}
});
}
@@ -334,7 +337,6 @@ private void updateStockList() {
/** Processes next tick. */
@Override
public void nextTick() {
- marketController.updateMarket();
Platform.runLater(() -> stockApp.getStockList().refresh());
if (currentStock != null) {
stockApp.updatePriceLabel(currentStock);
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java
index 6781e17..0c1eac8 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java
@@ -150,7 +150,6 @@ public void showMarketClosedWarning() {
*/
public void showInsufficientFundsWarning() {
warningApp.configure(
- "NO MORE MONEY!",
"Insufficient Funds",
"You don't have enough cash to complete this purchase.",
"I'll be back"
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopViewController.java
index 9feae79..4b8e991 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopViewController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopViewController.java
@@ -89,7 +89,13 @@ public DesktopViewController(
this.timeAndWeatherController = new TimeAndWeatherController(gameController);
gameController.addAppController(timeAndWeatherController);
- this.marketController = new MarketController(stocksStream);
+ try {
+ this.marketController = new MarketController(stocksStream);
+ } catch (Exception e) {
+ // In a real app we'd show an error dialog, but here we'll at least not swallow it silently
+ throw new RuntimeException("Failed to load market", e);
+ }
+
this.mailController = new MailController(timeAndWeatherController);
gameController.addAppController(mailController);
@@ -126,14 +132,15 @@ public DesktopViewController(
}
private List initializeApps(GameController gameController) {
- AppStoreApp appStoreApp = new AppStoreApp();
- gameController.addAppController(new AppStoreController(appStoreApp));
+ final AppStoreApp appStoreApp = new AppStoreApp();
+ gameController.addAppController(new AppStoreController());
final HustlersApp hustlersApp = new HustlersApp();
StockApp stockApp = new StockApp(player);
this.stockAppController = new StockAppController(stockApp, marketController, player);
gameController.addAppController(stockAppController);
+ gameController.addAppController(marketController);
final MailApp mailApp = new MailApp(mailController);
final NewsApp newsApp = new NewsApp();
@@ -232,12 +239,12 @@ private void setupListeners() {
timeAndWeatherController.temperatureProperty().addListener((obs, ov, nv) ->
desktopView.updateTemperature(String.valueOf(nv)));
- player.getNameProperty().addListener((obs, ov, nv) ->
- desktopView.updatePlayerName(nv));
+ player.addNameListener(name ->
+ Platform.runLater(() -> desktopView.updatePlayerName(name)));
player.updateStatus();
- player.getStatusProperty().addListener((obs, ov, nv) ->
- desktopView.updatePlayerStatus(nv.name()));
+ player.addStatusListener(s ->
+ Platform.runLater(() -> desktopView.updatePlayerStatus(s.name())));
desktopView.updateClock(timeAndWeatherController.getTimeString());
String startDay = timeAndWeatherController.getDayOfWeekString();
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/StartViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/StartViewController.java
index eccac6a..3743dcf 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/StartViewController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/StartViewController.java
@@ -17,6 +17,9 @@
public class StartViewController {
+ private static final System.Logger LOGGER =
+ System.getLogger(StartViewController.class.getName());
+
private String username = "";
private final StartView startView;
@@ -27,18 +30,18 @@ public class StartViewController {
private final WarningApp warningApp;
private static final List TATE_NAMES = List.of(
- "Top G",
- "Cobra Tate",
- "Tristan's Brother",
- "Emory's Son",
- "Matrix Escaper",
- "Bugatti Owner",
- "Hustler G",
- "Chin Checker",
- "Sparkling Water Enthusiast",
- "The Talisman",
- "Baby Oil Hoarder",
- "Roofie Supplier"
+ "Investor G",
+ "Stock Master",
+ "Market Pro",
+ "Financial Wizard",
+ "Profit Seeker",
+ "Wealth Builder",
+ "Capital King",
+ "Trade Expert",
+ "Dividend Dreamer",
+ "Bullish Trader",
+ "Savvy Investor",
+ "Money Manager"
);
public StartViewController(final Millions application, final StartView startView) {
@@ -124,6 +127,14 @@ private Difficulty resolveDifficulty() {
return Difficulty.EASY;
}
+ if (Objects.isNull(userSelectedPath)) {
+ try {
+ userSelectedPath = Path.of(Objects.requireNonNull(
+ getClass().getClassLoader().getResource("stocks.csv")).toURI());
+ } catch (Exception e) {
+ LOGGER.log(System.Logger.Level.WARNING, "Could not resolve default stocks.csv resource", e);
+ }
+
if (selectedMode.startsWith("Medium")) {
return Difficulty.MEDIUM;
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBus.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBus.java
index 13d1d48..7e428e0 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBus.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBus.java
@@ -1,9 +1,9 @@
package edu.ntnu.idi.idatt2003.gruppe42.model;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
/**
@@ -16,6 +16,14 @@ public class EventBus {
private static final EventBus INSTANCE = new EventBus();
private final Map, List>> listeners = new HashMap<>();
+ /**
+ * Resets the EventBus by clearing all listeners.
+ * Useful for testing isolation.
+ */
+ public synchronized void reset() {
+ listeners.clear();
+ }
+
/**
* Returns the singleton instance of EventBus.
*
@@ -33,18 +41,35 @@ public static EventBus get() {
* @param listener the consumer to invoke when an event of the given type is published.
*/
@SuppressWarnings("unchecked")
- public void subscribe(Class type, Consumer listener) {
- listeners.computeIfAbsent(type, k -> new ArrayList<>())
+ public synchronized void subscribe(Class type, Consumer listener) {
+ listeners.computeIfAbsent(type, k -> new CopyOnWriteArrayList<>())
.add((Consumer) listener);
}
+ /**
+ * Unsubscribes a listener from events of the given type.
+ *
+ * @param the event type.
+ * @param type the class of the event.
+ * @param listener the listener to remove.
+ */
+ public synchronized void unsubscribe(Class type, Consumer listener) {
+ List> handlers = listeners.get(type);
+ if (handlers != null) {
+ handlers.remove(listener);
+ }
+ }
+
/**
* Publishes an event to all subscribers of its type.
*
* @param event the event to publish.
*/
public void publish(Object event) {
- List> handlers = listeners.get(event.getClass());
+ List> handlers;
+ synchronized (this) {
+ handlers = listeners.get(event.getClass());
+ }
if (handlers != null) {
handlers.forEach(h -> h.accept(event));
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Exchange.java
index a1f745f..4257a34 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Exchange.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Exchange.java
@@ -25,10 +25,15 @@ public class Exchange {
* @param stocks the stocks to list on this exchange.
*/
public Exchange(final List stocks) {
+ if (stocks == null) {
+ throw new IllegalArgumentException("Stocks list cannot be null");
+ }
this.week = 0;
this.stockMap = new HashMap<>();
for (Stock stock : stocks) {
- stockMap.put(stock.getSymbol(), stock);
+ if (stock != null) {
+ stockMap.put(stock.getSymbol(), stock);
+ }
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java
index b92e457..a5abc74 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java
@@ -11,17 +11,29 @@
* company has a positive or negative trend.
*/
public class News {
+ private static final Random RANDOM = new Random();
+
+ private static final List AUTHORS = List.of(
+ "market@news.com",
+ "financial@journal.com",
+ "insider@trading.com",
+ "economy@update.com"
+ );
+
private final String company;
private final int trend;
- private final Random random = new Random();
/**
* Constructs a News object for a given company and trend.
*
- * @param company the company the news is related to
+ * @param company the company the news is related to; must not be {@code null}
* @param trend a numeric indicator of market trend (positive = good news, negative = bad news)
+ * @throws IllegalArgumentException if {@code company} is {@code null} or blank
*/
public News(String company, int trend) {
+ if (company == null || company.isBlank()) {
+ throw new IllegalArgumentException("Company name cannot be null or blank");
+ }
this.company = company;
this.trend = trend;
}
@@ -32,10 +44,7 @@ public News(String company, int trend) {
* @return a randomly selected fictional author email
*/
public String generateAuthor() {
- List authors = new ArrayList<>();
- // Saved as List in-case new authors are added
- authors.add("market@news.com");
- return authors.get(random.nextInt(authors.size()));
+ return AUTHORS.get(RANDOM.nextInt(AUTHORS.size()));
}
/**
@@ -48,53 +57,52 @@ public String generateAuthor() {
*/
public String generateNews() {
if (trend > 0) {
- List goodNews = getStringsGood();
-
- return goodNews.get(random.nextInt(goodNews.size()));
+ List goodNews = getPositiveHeadlines();
+ return goodNews.get(RANDOM.nextInt(goodNews.size()));
}
- List badNews = getStringsBad();
- return badNews.get(random.nextInt(badNews.size()));
+ List badNews = getNegativeHeadlines();
+ return badNews.get(RANDOM.nextInt(badNews.size()));
}
- private List getStringsGood() {
+ private List getPositiveHeadlines() {
List goodNews = new ArrayList<>();
goodNews.add("Turns out the product of " + company + " is good for the environment.");
+
goodNews.add("Beaver population increased due to the efforts of " + company);
goodNews.add("Eminem dropped new hit single, namedropping " + company);
+
goodNews.add("Founder of " + company + " seen feeding the homeless.");
goodNews.add(company + " got frontpage in new forbes magazine, "
+ "on top companies to invest in.");
- goodNews.add("IShowSpeed seen using " + company
- + " products in the background of viral livestream.");
+ goodNews.add("Viral social media influencer seen using " + company
+ + " products in the background of a livestream.");
goodNews.add("New studies show that " + company
- + " took part in inventing the rainbow back in 4,542,997,974 BC");
+ + " has been a market leader for decades.");
goodNews.add(company + " has made breakthroughs in the field of artificial intelligence.");
goodNews.add("After the release of the brand new logo of " + company
+ ", the logo has been called the \"a glimpse of the future\".");
goodNews.add("EX-EMPLOYEES of " + company + " REVEAL that the work environment "
- + "and safety regulations are not up to basic standards but far surpassing them.");
+ + "and safety regulations far surpass basic standards.");
goodNews.add("Founder of " + company
- + " seen rudely pushing a homeless man out of the road, saving his life.");
+ + " seen helping a local community project, garnering massive praise.");
return goodNews;
}
- private List getStringsBad() {
+ private List getNegativeHeadlines() {
List badNews = new ArrayList<>();
badNews.add("Turns out the product of " + company + " is bad for the environment.");
- badNews.add("Founder of " + company + " seen rudely pushing a homeless man out of the way.");
- badNews.add(company + " mentioned in epstein files.");
- badNews.add(company + " has created many dams for beavers, causing them to lay"
- + " around in the sun all day and die of laziness.");
- badNews.add(company + " initiated a campaign to feed the homeless with burgers, using dog meat."
- + "This has decreased the number of starvation deaths among the homeless.");
- badNews.add("Turns out " + company + " created the first covid-19 vaccines, "
- + "which are not safe for humans.");
+ badNews.add("Founder of " + company + " seen rudely pushing a bystander out of the way.");
+ badNews.add(company + " mentioned in controversial regulatory filings.");
+ badNews.add(company + " failed to meet environmental standards in several regions.");
+ badNews.add(company + " initiated a campaign that backfired among its core demographic.");
+ badNews.add("Product recall at " + company + " due to safety concerns.");
badNews.add("Extensive research of " + company + " shows financial holes in the company.");
- badNews.add("Founder of " + company + " heiling at press conference discussing great ideas.");
+ badNews.add("Founder of " + company
+ + " under fire for controversial remarks at press conference.");
badNews.add("EX-EMPLOYEES of " + company + " REVEAL that the work environment"
+ " and safety regulations are not up to basic standards.");
- badNews.add("US-Army found using " + company
- + " issued weapons that malfunctioned causing mass civilian casualties.");
+ badNews.add("Supply chain issues at " + company
+ + " causing significant delays and loss of revenue.");
badNews.add(company + " got frontpage in new forbes magazine, "
+ "on top companies not to invest in.");
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Player.java
index c2517fa..cbedf32 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Player.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Player.java
@@ -3,12 +3,9 @@
import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.InsufficientFunds;
import edu.ntnu.idi.idatt2003.gruppe42.model.transaction.TransactionArchive;
import java.math.BigDecimal;
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
/**
* Represents a player in the game, holding their financial state, portfolio,
@@ -21,14 +18,22 @@
public class Player {
private static final int MAX_LIVES = 3;
+ private static final int INVESTOR_WEEKS = 10;
+ private static final int SPECULATOR_WEEKS = 20;
+ private static final double INVESTOR_NET_WORTH_MULTIPLIER = 1.2;
+ private static final double SPECULATOR_NET_WORTH_MULTIPLIER = 5.0;
- private final StringProperty name = new SimpleStringProperty();
+ private String name;
private final BigDecimal startingMoney;
private BigDecimal money;
private final Portfolio portfolio;
private final TransactionArchive transactionArchive;
- private final IntegerProperty lives = new SimpleIntegerProperty(MAX_LIVES);
- private final ObjectProperty status = new SimpleObjectProperty<>(Status.NOVICE);
+ private int lives = MAX_LIVES;
+ private Status status = Status.NOVICE;
+
+ private final List> nameListeners = new ArrayList<>();
+ private final List> livesListeners = new ArrayList<>();
+ private final List> statusListeners = new ArrayList<>();
/**
* Constructs a new {@code Player} with the given name and starting balance.
@@ -40,7 +45,13 @@ public class Player {
* @param startingMoney the initial cash balance; must not be {@code null} or negative
*/
public Player(final String name, final BigDecimal startingMoney) {
- this.name.set(name);
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Name cannot be null or blank");
+ }
+ if (startingMoney == null || startingMoney.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Starting money cannot be null or negative");
+ }
+ this.name = name;
this.startingMoney = startingMoney;
this.money = startingMoney;
this.portfolio = new Portfolio();
@@ -48,48 +59,76 @@ public Player(final String name, final BigDecimal startingMoney) {
}
/**
- * Returns the observable {@link StringProperty} backing the player's name.
- *
- * Useful for binding UI components directly to the player's name.
+ * Registers a listener that is notified whenever the player's name changes.
*
- * @return the name property; never {@code null}
+ * @param listener the consumer to call with the new name; must not be {@code null}
*/
- public StringProperty getNameProperty() {
- return name;
+ public void addNameListener(final Consumer listener) {
+ nameListeners.add(listener);
}
+ /**
+ * Returns the player's display name.
+ *
+ * @return the name; never {@code null}
+ */
public String getName() {
- return name.get();
+ return name;
}
/**
- * Sets the player's display name.
+ * Sets the player's display name and notifies registered name listeners.
*
* @param name the new name; must not be {@code null}
*/
public void setName(final String name) {
- this.name.set(name);
+ this.name = name;
+ nameListeners.forEach(l -> l.accept(name));
}
+ /**
+ * Returns the initial cash balance the player started with.
+ *
+ * @return the starting money; never {@code null}
+ */
public BigDecimal getStartingMoney() {
return startingMoney;
}
+ /**
+ * Returns the player's current cash balance.
+ *
+ * @return the current money; never {@code null}
+ */
public BigDecimal getMoney() {
return money;
}
+ /**
+ * Returns the player's current status as a string.
+ *
+ * @return the status name (e.g., {@code "NOVICE"}, {@code "INVESTOR"}, {@code "SPECULATOR"})
+ */
public String getStatus() {
- return status.get().name();
+ return status.name();
}
- /** Adds money. */
+ /**
+ * Adds the given amount to the player's cash balance and refreshes their status.
+ *
+ * @param amount the amount to add; must not be {@code null}
+ */
public void addMoney(final BigDecimal amount) {
money = money.add(amount);
updateStatus();
}
- /** Withdraws money. */
+ /**
+ * Withdraws the given amount from the player's cash balance and refreshes their status.
+ *
+ * @param amount the amount to withdraw; must not be {@code null}
+ * @throws InsufficientFunds if the player's current balance is less than {@code amount}
+ */
public void withdrawMoney(final BigDecimal amount) throws InsufficientFunds {
if (money.compareTo(amount) < 0) {
throw new InsufficientFunds("Not enough money to withdraw " + amount);
@@ -98,37 +137,78 @@ public void withdrawMoney(final BigDecimal amount) throws InsufficientFunds {
updateStatus();
}
- /** Deducts rent. */
+ /**
+ * Deducts rent from the player's cash balance and refreshes their status.
+ *
+ * Unlike {@link #withdrawMoney(BigDecimal)}, this may push the balance negative,
+ * which triggers the in-debt game condition.
+ *
+ * @param amount the rent amount to deduct; must not be {@code null}
+ */
public void deductRent(final BigDecimal amount) {
money = money.subtract(amount);
updateStatus();
}
+ /**
+ * Returns whether the player's cash balance is negative.
+ *
+ * @return {@code true} if the player is in debt
+ */
public boolean isInDebt() {
return money.compareTo(BigDecimal.ZERO) < 0;
}
+ /**
+ * Returns the player's total net worth (cash plus portfolio value).
+ *
+ * @return the net worth; never {@code null}
+ */
public BigDecimal getNetWorth() {
return money.add(portfolio.getNetWorth());
}
- public IntegerProperty getLivesProperty() {
- return lives;
+ /**
+ * Registers a listener that is notified whenever the player's life count changes.
+ *
+ * @param listener the consumer to call with the new life count; must not be {@code null}
+ */
+ public void addLivesListener(final Consumer listener) {
+ livesListeners.add(listener);
}
+ /**
+ * Returns the player's current number of lives.
+ *
+ * @return the number of lives remaining
+ */
public int getLives() {
- return lives.get();
+ return lives;
}
- /** Decrements life. */
+ /**
+ * Decrements the player's life count by one, stopping at zero,
+ * and notifies registered lives listeners.
+ */
public void loseLife() {
- lives.set(Math.max(0, lives.get() - 1));
+ lives = Math.max(0, lives - 1);
+ livesListeners.forEach(l -> l.accept(lives));
}
+ /**
+ * Returns the player's stock portfolio.
+ *
+ * @return the portfolio; never {@code null}
+ */
public Portfolio getPortfolio() {
return portfolio;
}
+ /**
+ * Returns the player's transaction archive.
+ *
+ * @return the transaction archive; never {@code null}
+ */
public TransactionArchive getTransactionArchive() {
return transactionArchive;
}
@@ -151,32 +231,40 @@ public TransactionArchive getTransactionArchive() {
* {@link #withdrawMoney(BigDecimal)}, and {@link #deductRent(BigDecimal)}.
*/
public void updateStatus() {
- final int investorWeeks = 10;
- final int speculatorWeeks = 20;
- final double investorMult = 1.2;
- final double speculatorMult = 5.0;
-
int weeksActive = transactionArchive.countDistinctWeeks();
BigDecimal netWorth = getNetWorth();
- BigDecimal investorTarget = startingMoney.multiply(BigDecimal.valueOf(investorMult));
- BigDecimal speculatorTarget = startingMoney.multiply(BigDecimal.valueOf(speculatorMult));
+ BigDecimal investorTarget = startingMoney.multiply(
+ BigDecimal.valueOf(INVESTOR_NET_WORTH_MULTIPLIER));
+ BigDecimal speculatorTarget = startingMoney.multiply(
+ BigDecimal.valueOf(SPECULATOR_NET_WORTH_MULTIPLIER));
- boolean isSpeculator = weeksActive >= speculatorWeeks
+ boolean isSpeculator = weeksActive >= SPECULATOR_WEEKS
&& netWorth.compareTo(speculatorTarget) >= 0;
- boolean isInvestor = weeksActive >= investorWeeks
+ boolean isInvestor = weeksActive >= INVESTOR_WEEKS
&& netWorth.compareTo(investorTarget) >= 0;
+ Status next;
if (isSpeculator) {
- status.set(Status.SPECULATOR);
+ next = Status.SPECULATOR;
} else if (isInvestor) {
- status.set(Status.INVESTOR);
+ next = Status.INVESTOR;
} else {
- status.set(Status.NOVICE);
+ next = Status.NOVICE;
+ }
+
+ if (next != status) {
+ status = next;
+ statusListeners.forEach(l -> l.accept(status));
}
}
- public ObjectProperty getStatusProperty() {
- return status;
+ /**
+ * Registers a listener that is notified whenever the player's status changes.
+ *
+ * @param listener the consumer to call with the new status; must not be {@code null}
+ */
+ public void addStatusListener(final Consumer listener) {
+ statusListeners.add(listener);
}
}
\ No newline at end of file
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Portfolio.java
index dd4700c..f9aa0fa 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Portfolio.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Portfolio.java
@@ -39,12 +39,10 @@ public boolean addShare(final Share share) {
final int scale = 10;
- // Check if we already own a share with the same stock symbol
for (Share existing : shares) {
if (existing.getStock().getSymbol()
.equals(share.getStock().getSymbol())) {
- // Weighted average purchase price: (q1*p1 + q2*p2) / (q1+q2)
BigDecimal totalQuantity = existing.getQuantity()
.add(share.getQuantity());
@@ -53,7 +51,6 @@ public boolean addShare(final Share share) {
.add(share.getQuantity().multiply(share.getPurchasePrice()))
.divide(totalQuantity, scale, RoundingMode.HALF_UP);
- // Replace the existing share with the merged one
shares.remove(existing);
return shares.add(
new Share(existing.getStock(), totalQuantity, weightedPrice)
@@ -107,8 +104,13 @@ public boolean removeShare(final Share newShare) {
}
}
+ /**
+ * Returns an unmodifiable snapshot of all shares currently held in the portfolio.
+ *
+ * @return an unmodifiable list of shares; never {@code null}
+ */
public List getShares() {
- return shares;
+ return List.copyOf(shares);
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
index d3c782b..18b2f9c 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Share.java
@@ -23,11 +23,25 @@ public Share(
final BigDecimal quantity,
final BigDecimal purchasePrice
) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock cannot be null");
+ }
+ if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Quantity must be positive");
+ }
+ if (purchasePrice == null || purchasePrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Purchase price cannot be negative");
+ }
this.stock = stock;
this.quantity = quantity;
this.purchasePrice = purchasePrice;
}
+ /**
+ * Returns the stock this share belongs to.
+ *
+ * @return the stock; never {@code null}
+ */
public Stock getStock() {
return stock;
}
@@ -41,10 +55,20 @@ public void setQuantity(final BigDecimal quantity) {
this.quantity = quantity;
}
+ /**
+ * Returns the number of units held for this position.
+ *
+ * @return the quantity; always positive
+ */
public BigDecimal getQuantity() {
return quantity;
}
+ /**
+ * Returns the price per unit paid at the time of purchase.
+ *
+ * @return the purchase price; never negative
+ */
public BigDecimal getPurchasePrice() {
return purchasePrice;
}
@@ -67,10 +91,8 @@ public boolean equals(Object object) {
}
Share share = (Share) object;
return Objects.equals(stock.getSymbol(), share.stock.getSymbol())
- && (quantity == null ? share.quantity == null :
- quantity.compareTo(share.quantity) == 0)
- && (purchasePrice == null ? share.purchasePrice == null :
- purchasePrice.compareTo(share.purchasePrice) == 0);
+ && quantity.compareTo(share.quantity) == 0
+ && purchasePrice.compareTo(share.purchasePrice) == 0;
}
/**
@@ -80,6 +102,10 @@ public boolean equals(Object object) {
*/
@Override
public int hashCode() {
- return Objects.hash(stock.getSymbol(), quantity, purchasePrice);
+ return Objects.hash(
+ stock.getSymbol(),
+ quantity.stripTrailingZeros(),
+ purchasePrice.stripTrailingZeros()
+ );
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
index 7c7d9c5..f36fe1a 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Status.java
@@ -11,7 +11,7 @@
* {@link #INVESTOR}: Achieved by trading for at least 10 distinct weeks
* and increasing net worth by at least 20%.
* {@link #SPECULATOR}: Achieved by trading for at least 20 distinct weeks
- * and doubling net worth.
+ * and increasing net worth by at least 5 times.
*
*/
public enum Status {
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
index 430c5e1..0bea46a 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/Stock.java
@@ -1,7 +1,5 @@
package edu.ntnu.idi.idatt2003.gruppe42.model;
-import edu.ntnu.idi.idatt2003.gruppe42.controller.StockController;
-import edu.ntnu.idi.idatt2003.gruppe42.view.views.components.StockGraph;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -9,21 +7,17 @@
/**
* Represents a publicly traded stock belonging to a company.
*
- * A stock maintains a full chronological price history, exposes summary
- * statistics (highest, lowest, the latest change), and delegates price-update
- * logic to a {@link StockController}. An optional {@link StockGraph} can be
- * lazily attached for visual rendering.
+ *
A stock maintains a full chronological price history and exposes summary
+ * statistics (highest, lowest, the latest change).
*
*
Price history is append-only: new prices are recorded via
- * {@link #addNewSalesPrice(BigDecimal)} or {@link #updatePrice()}, and the
+ * {@link #addNewSalesPrice(BigDecimal)} and the
* most recent entry is always considered the current market price.
*/
public class Stock {
private final String symbol;
private final String company;
- private StockGraph stockGraph;
private final List prices = new ArrayList<>();
- private final StockController stockController;
/** Constructs a new stock. */
public Stock(
@@ -31,10 +25,18 @@ public Stock(
final String company,
final BigDecimal salesPrice
) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
+ }
+ if (company == null || company.isBlank()) {
+ throw new IllegalArgumentException("Company cannot be null or blank");
+ }
+ if (salesPrice == null || salesPrice.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Sales price cannot be negative");
+ }
this.symbol = symbol;
this.company = company;
prices.add(salesPrice);
- stockController = new StockController(this);
}
public String getSymbol() {
@@ -75,38 +77,4 @@ public BigDecimal getLatestPriceChange() {
int index120Ago = Math.max(0, prices.size() - 121);
return getSalesPrice().subtract(prices.get(index120Ago));
}
-
- /** Updates the price. */
- public void updatePrice() {
- BigDecimal newPrice = stockController.updatePrice();
- prices.add(newPrice);
- }
-
- /**
- * Returns the {@link StockGraph} component associated with this stock,
- * creating it lazily on the first call.
- *
- * @return the stock graph; never {@code null}
- */
- public StockGraph getStockGraph() {
- if (stockGraph == null) {
- stockGraph = new StockGraph();
- }
- return stockGraph;
- }
-
- /** Generates fake history. */
- public void fakeHistory(int stockResolution) {
- for (int i = 0; i < stockResolution; i++) {
- updatePrice();
- }
- java.util.Collections.reverse(prices);
- }
-
- /** Updates the stock graph. */
- public void updateStockGraph() {
- if (stockGraph != null && stockGraph.getVisibility()) {
- stockGraph.update(this);
- }
- }
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
index 1f497a5..31fb04f 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java
@@ -89,14 +89,9 @@ public static List readFromFile(final InputStream inputStream)
}
if (!SYMBOL_PATTERN.matcher(symbol).matches()) {
- throw new StockFileParseException(
- lineNumber,
- trimmed,
- String.format(
- "Ticker symbol \"%s\" is invalid",
- symbol
- )
- );
+ throw new StockFileParseException(lineNumber, trimmed,
+ String.format("Ticker symbol \"%s\" is invalid — "
+ + "must be 1-5 characters (A-Z or .)", symbol));
}
String company = parts[1].trim();
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/PurchaseCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/PurchaseCalculator.java
index b1b8cd3..e59dbba 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/PurchaseCalculator.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/PurchaseCalculator.java
@@ -7,6 +7,9 @@
* Calculates the cost of a stock purchase transaction, including commission.
*/
public final class PurchaseCalculator implements TransactionCalculator {
+
+ private static final BigDecimal COMMISSION_RATE = new BigDecimal("0.01");
+
private final BigDecimal purchasePrice;
private final BigDecimal quantity;
@@ -37,8 +40,7 @@ public BigDecimal calculateGross() {
*/
@Override
public BigDecimal calculateCommission() {
- final BigDecimal commissionRate = new BigDecimal("0.01");
- return calculateGross().multiply(commissionRate);
+ return calculateGross().multiply(COMMISSION_RATE);
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
index 332b1f9..444b327 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculator.java
@@ -7,6 +7,10 @@
* Calculates the proceeds of a stock sale transaction, including commission and capital gains tax.
*/
public final class SaleCalculator implements TransactionCalculator {
+
+ private static final BigDecimal COMMISSION_RATE = new BigDecimal("0.01");
+ private static final BigDecimal TAX_RATE = new BigDecimal("0.22");
+
private final BigDecimal purchasePrice;
private final BigDecimal salesPrice;
private final BigDecimal quantity;
@@ -39,8 +43,7 @@ public BigDecimal calculateGross() {
*/
@Override
public BigDecimal calculateCommission() {
- final BigDecimal commissionRate = new BigDecimal("0.01");
- return calculateGross().multiply(commissionRate);
+ return calculateGross().multiply(COMMISSION_RATE);
}
/**
@@ -51,10 +54,12 @@ public BigDecimal calculateCommission() {
*/
@Override
public BigDecimal calculateTax() {
- final BigDecimal taxRate = new BigDecimal("0.22");
BigDecimal differencePrice = salesPrice.subtract(purchasePrice);
- BigDecimal difference = differencePrice.multiply(quantity);
- return difference.multiply(taxRate);
+ BigDecimal profit = differencePrice.multiply(quantity);
+ if (profit.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO;
+ }
+ return profit.multiply(TAX_RATE);
}
/**
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
index 8f22e11..841f236 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Purchase.java
@@ -33,6 +33,23 @@ public Purchase(final Share share, final int week) {
@Override
public void commit(final Player player) {
player.withdrawMoney(getCalculator().calculateTotal());
+ try {
+ player.getPortfolio().addShare(getShare());
+ } catch (Exception e) {
+ // Rollback: return money if portfolio update fails
+ player.addMoney(getCalculator().calculateTotal());
+ throw e;
+ }
player.getTransactionArchive().add(this);
}
+
+ /**
+ * Returns the total cost of this purchase as a negative contribution to weekly income.
+ *
+ * @return the purchase total negated
+ */
+ @Override
+ public java.math.BigDecimal netIncomeContribution() {
+ return getCalculator().calculateTotal().negate();
+ }
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
index 1ccb8aa..e5ba743 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Sale.java
@@ -33,6 +33,29 @@ public Sale(final Share share, final int week) {
@Override
public void commit(final Player player) {
player.addMoney(getCalculator().calculateTotal());
+ try {
+ if (!player.getPortfolio().removeShare(getShare())) {
+ throw new IllegalStateException("Failed to remove share from portfolio");
+ }
+ } catch (Exception e) {
+ // Best-effort rollback: if portfolio removal fails, reverse the money credit.
+ try {
+ player.withdrawMoney(getCalculator().calculateTotal());
+ } catch (Exception rollbackFailure) {
+ e.addSuppressed(rollbackFailure);
+ }
+ throw e;
+ }
player.getTransactionArchive().add(this);
}
+
+ /**
+ * Returns the net proceeds of this sale as a positive contribution to weekly income.
+ *
+ * @return the sale total after commission and tax
+ */
+ @Override
+ public java.math.BigDecimal netIncomeContribution() {
+ return getCalculator().calculateTotal();
+ }
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
index 60d72be..3918095 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/Transaction.java
@@ -3,6 +3,7 @@
import edu.ntnu.idi.idatt2003.gruppe42.model.Player;
import edu.ntnu.idi.idatt2003.gruppe42.model.Share;
import edu.ntnu.idi.idatt2003.gruppe42.model.calculator.TransactionCalculator;
+import java.math.BigDecimal;
/**
* Base class for all stock-related transactions.
@@ -33,14 +34,29 @@ protected Transaction(
this.calculator = calculator;
}
+ /**
+ * Returns the share involved in this transaction.
+ *
+ * @return the share; never {@code null}
+ */
public Share getShare() {
return share;
}
+ /**
+ * Returns the game week in which this transaction occurred.
+ *
+ * @return the week number
+ */
public int getWeek() {
return week;
}
+ /**
+ * Returns the calculator used to compute this transaction's financial values.
+ *
+ * @return the calculator; never {@code null}
+ */
public TransactionCalculator getCalculator() {
return calculator;
}
@@ -51,7 +67,16 @@ public TransactionCalculator getCalculator() {
* Subclasses define the specific behavior (e.g., buying or selling shares).
*
* @param player the player executing the transaction
- * @throws Exception if the transaction cannot be completed
*/
- public abstract void commit(Player player) throws Exception;
+ public abstract void commit(Player player);
+
+ /**
+ * Returns this transaction's signed contribution to weekly net income.
+ *
+ *
Sales return a positive value (money received); purchases return a negative
+ * value (money spent). Used by {@link TransactionArchive#calculateNetIncome(int)}.
+ *
+ * @return the signed net-income contribution of this transaction
+ */
+ public abstract BigDecimal netIncomeContribution();
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchive.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchive.java
index 647d881..9093438 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchive.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchive.java
@@ -126,11 +126,7 @@ public int countDistinctWeeks() {
public BigDecimal calculateNetIncome(final int week) {
BigDecimal net = BigDecimal.ZERO;
for (Transaction t : getTransactions(week)) {
- if (t instanceof Sale) {
- net = net.add(t.getCalculator().calculateTotal());
- } else if (t instanceof Purchase) {
- net = net.subtract(t.getCalculator().calculateTotal());
- }
+ net = net.add(t.netIncomeContribution());
}
return net;
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java
index fa9e4bd..b2c3b50 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java
@@ -44,7 +44,7 @@ public class BankApp extends Popup {
*
Initializes the portfolio list view and builds the UI layout.
*/
public BankApp() {
- super(550, 500, 220, 100, App.BANK);
+ super(550, 600, 220, 100, App.BANK);
portfolioList = new ListView<>();
portfolioList.setSelectionModel(null);
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
index 527cc78..94339f5 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/StockApp.java
@@ -46,6 +46,7 @@ public class StockApp extends Popup {
private final Button confirmButton;
private final Button backButton;
private final Receipt receipt;
+ private final StockGraph stockGraph;
/**
* Constructs the Stock trading popup.
@@ -55,6 +56,8 @@ public class StockApp extends Popup {
public StockApp(final Player player) {
super(475, 450, 140, 140, App.STOCK);
+ this.stockGraph = new StockGraph();
+
this.searchField = new TextField();
this.searchField.setPromptText("Search stocks...");
this.searchField.getStyleClass().add("search-field");
@@ -137,15 +140,15 @@ public void showStockPage(final Stock stock) {
priceLabel = new Label("$" + String.format("%,.2f", stock.getSalesPrice()));
priceLabel.getStyleClass().add("stock-price-large");
- StockGraph graph = stock.getStockGraph();
- VBox.setVgrow(graph, Priority.ALWAYS);
+ VBox.setVgrow(stockGraph, Priority.ALWAYS);
+ stockGraph.update(stock);
receipt.update(stock, new BigDecimal(quantitySpinner.getValue()));
HBox receiptCentered = new HBox(receipt);
receiptCentered.setAlignment(Pos.CENTER);
content.setSpacing(20);
- content.getChildren().setAll(topControls, headerBox, priceLabel, graph, receiptCentered);
+ content.getChildren().setAll(topControls, headerBox, priceLabel, stockGraph, receiptCentered);
}
/**
@@ -155,12 +158,19 @@ public void showStockPage(final Stock stock) {
*/
public void updatePriceLabel(final Stock stock) {
if (priceLabel != null) {
- Platform.runLater(() ->
- priceLabel.setText("$" + String.format("%,.2f", stock.getSalesPrice()))
- );
+ Platform.runLater(() -> {
+ priceLabel.setText("$" + String.format("%,.2f", stock.getSalesPrice()));
+ if (stockGraph.getVisibility()) {
+ stockGraph.update(stock);
+ }
+ });
}
}
+ public StockGraph getStockGraph() {
+ return stockGraph;
+ }
+
public TextField getSearchField() {
return searchField;
}
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/DesktopBottomBar.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/DesktopBottomBar.java
index 96585c7..b427a7e 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/DesktopBottomBar.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/DesktopBottomBar.java
@@ -128,7 +128,7 @@ private HBox buildHeartsBox(final Player player) {
hearts[i].getStyleClass().add(active ? "heart-active" : "heart-empty");
}
};
- player.getLivesProperty().addListener((obs, ov, nv) -> Platform.runLater(refresh));
+ player.addLivesListener(nv -> Platform.runLater(refresh));
refresh.run();
return box;
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/MarketControllerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/MarketControllerTest.java
new file mode 100644
index 0000000..4bcb4f7
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/MarketControllerTest.java
@@ -0,0 +1,52 @@
+package edu.ntnu.idi.idatt2003.gruppe42.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link MarketController}.
+ *
+ *
Verifies market initialization, stock price updates, and exchange state.
+ */
+public class MarketControllerTest {
+ private Path tempFile;
+ private MarketController controller;
+
+ @BeforeEach
+ void setUp() throws IOException, StockFileParseException {
+ tempFile = Files.createTempFile("market_test", ".csv");
+ Files.writeString(tempFile, "AAPL,Apple Inc.,100.00\nMSFT,Microsoft,200.00");
+ controller = new MarketController(tempFile);
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ Files.deleteIfExists(tempFile);
+ }
+
+ @Test
+ void testMarketInitialization() {
+ assertNotNull(controller.getExchange());
+ assertTrue(controller.getExchange().hasStock("AAPL"));
+ assertTrue(controller.getExchange().hasStock("MSFT"));
+ }
+
+ @Test
+ void testUpdateMarket() {
+ BigDecimal initialPrice = controller.getExchange().getStock("AAPL").getSalesPrice();
+ controller.updateMarket();
+ BigDecimal newPrice = controller.getExchange().getStock("AAPL").getSalesPrice();
+ // Price history should have grown by one tick
+ assertEquals(122, controller.getExchange().getStock("AAPL").getHistoricalPrices().size());
+ }
+}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockControllerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockControllerTest.java
new file mode 100644
index 0000000..62ec64c
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockControllerTest.java
@@ -0,0 +1,48 @@
+package edu.ntnu.idi.idatt2003.gruppe42.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.ntnu.idi.idatt2003.gruppe42.model.Stock;
+import java.math.BigDecimal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link StockController}.
+ *
+ *
Verifies that price updates behave correctly over many ticks.
+ */
+public class StockControllerTest {
+ private Stock stock;
+ private StockController controller;
+
+ @BeforeEach
+ void setUp() {
+ stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100.00"));
+ controller = new StockController(stock);
+ }
+
+ @Test
+ void testInitialPrice() {
+ assertEquals(0, new BigDecimal("100.00").compareTo(controller.getPrice()));
+ }
+
+ @Test
+ void testUpdatePrice() {
+ BigDecimal newPrice = controller.updatePrice();
+ assertNotNull(newPrice);
+ // Price should have changed or at least be non-negative
+ assertTrue(newPrice.compareTo(BigDecimal.ZERO) >= 0);
+ }
+
+ @Test
+ void testPriceNotNegative() {
+ // Force many updates to see if it ever goes negative
+ for (int i = 0; i < 1000; i++) {
+ BigDecimal price = controller.updatePrice();
+ assertTrue(price.compareTo(BigDecimal.ZERO) >= 0, "Price should never be negative");
+ }
+ }
+}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBusTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBusTest.java
new file mode 100644
index 0000000..33962a6
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/EventBusTest.java
@@ -0,0 +1,76 @@
+package edu.ntnu.idi.idatt2003.gruppe42.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the {@link EventBus} class.
+ *
+ *
Verifies publish/subscribe semantics, type filtering, unsubscription,
+ * and null-safety.
+ */
+public class EventBusTest {
+
+ @BeforeEach
+ void setUp() {
+ EventBus.get().reset();
+ }
+
+ @Test
+ void testSubscribeAndPublish() {
+ EventBus bus = EventBus.get();
+ AtomicInteger count = new AtomicInteger(0);
+ bus.subscribe(String.class, s -> count.incrementAndGet());
+
+ bus.publish("Hello");
+ assertEquals(1, count.get());
+
+ bus.publish(123); // Integer event must not trigger String listener
+ assertEquals(1, count.get());
+ }
+
+ @Test
+ void testUnsubscribe() {
+ EventBus bus = EventBus.get();
+ AtomicInteger count = new AtomicInteger(0);
+ Consumer listener = s -> count.incrementAndGet();
+
+ bus.subscribe(String.class, listener);
+ bus.publish("Test");
+ assertEquals(1, count.get());
+
+ bus.unsubscribe(String.class, listener);
+ bus.publish("Test Again");
+ assertEquals(1, count.get());
+ }
+
+ @Test
+ void testPublishWithNoSubscribers() {
+ // Publishing to a type with no listeners must not throw.
+ EventBus.get().publish("NoSubscriberEvent");
+ }
+
+ @Test
+ void testPublishNullThrows() {
+ assertThrows(NullPointerException.class, () -> EventBus.get().publish(null));
+ }
+
+ @Test
+ void testMultipleSubscribersSameType() {
+ EventBus bus = EventBus.get();
+ AtomicInteger countA = new AtomicInteger(0);
+ AtomicInteger countB = new AtomicInteger(0);
+
+ bus.subscribe(String.class, s -> countA.incrementAndGet());
+ bus.subscribe(String.class, s -> countB.incrementAndGet());
+
+ bus.publish("Broadcast");
+ assertEquals(1, countA.get());
+ assertEquals(1, countB.get());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/MessageTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/MessageTest.java
new file mode 100644
index 0000000..a1aea1a
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/MessageTest.java
@@ -0,0 +1,37 @@
+package edu.ntnu.idi.idatt2003.gruppe42.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the {@link Message} class.
+ *
+ * Verifies correct formatted-time output for various week, day, and hour values.
+ */
+public class MessageTest {
+
+ @Test
+ void testGetFormattedTimeAfternoon() {
+ Message message = new Message("author", "title", "content", 1, "MON", 13);
+ assertEquals("Week 1, Mon, 13:00", message.getFormattedTime());
+ }
+
+ @Test
+ void testGetFormattedTimeMorningWithLeadingZero() {
+ Message message = new Message("author", "title", "content", 5, "wednesday", 9);
+ assertEquals("Week 5, Wednesday, 09:00", message.getFormattedTime());
+ }
+
+ @Test
+ void testGetFormattedTimeMidnight() {
+ Message message = new Message("author", "title", "content", 3, "FRI", 0);
+ assertEquals("Week 3, Fri, 00:00", message.getFormattedTime());
+ }
+
+ @Test
+ void testGetFormattedTimeEndOfDay() {
+ Message message = new Message("author", "title", "content", 10, "SAT", 23);
+ assertEquals("Week 10, Sat, 23:00", message.getFormattedTime());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
index fb02274..c64d2d7 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PlayerTest.java
@@ -89,4 +89,32 @@ void getNetworthTest() {
new BigDecimal("5000")));
assertEquals(new BigDecimal("14950.00"), player.getNetWorth());
}
+
+ @Test
+ void testUpdateStatusToInvestor() {
+ // Requires 10 distinct weeks and net worth >= 1.2 * starting money (12000)
+ for (int i = 0; i < 10; i++) {
+ player.getTransactionArchive().add(new edu.ntnu.idi.idatt2003.gruppe42.model.transaction.Purchase(
+ new Share(new Stock("AAPL", "Apple Inc.", new BigDecimal("100")), new BigDecimal("1"), new BigDecimal("100")),
+ i
+ ));
+ }
+ player.addMoney(new BigDecimal("2000")); // Net worth becomes 12000
+ player.updateStatus();
+ assertEquals("INVESTOR", player.getStatus());
+ }
+
+ @Test
+ void testUpdateStatusToSpeculator() {
+ // Requires 20 distinct weeks and net worth >= 5 * starting money (50000)
+ for (int i = 0; i < 20; i++) {
+ player.getTransactionArchive().add(new edu.ntnu.idi.idatt2003.gruppe42.model.transaction.Purchase(
+ new Share(new Stock("AAPL", "Apple Inc.", new BigDecimal("100")), new BigDecimal("1"), new BigDecimal("100")),
+ i
+ ));
+ }
+ player.addMoney(new BigDecimal("40000")); // Net worth becomes 50000
+ player.updateStatus();
+ assertEquals("SPECULATOR", player.getStatus());
+ }
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
index a71b58d..2d19d60 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/PortfolioTest.java
@@ -9,9 +9,9 @@
import org.junit.jupiter.api.Test;
/**
- * Unit tests for the {@link Exchange} class.
+ * Unit tests for the {@link Portfolio} class.
*
- *
Verifies stock management, search functionality, and week progression behavior.
+ *
Verifies share management, merging logic, and quantity tracking.
*/
public class PortfolioTest {
private Portfolio portfolio;
@@ -52,8 +52,25 @@ void removeShareTest() {
}
@Test
- void containsTest() {
- Portfolio portfolio = new Portfolio();
+ void containsNullReturnsFalse() {
assertFalse(portfolio.contains(null));
}
+
+ @Test
+ void removeFromEmptyPortfolioReturnsFalse() {
+ assertFalse(portfolio.removeShare(share));
+ }
+
+ @Test
+ void netWorthEmptyPortfolioIsZero() {
+ assertEquals(0, BigDecimal.ZERO.compareTo(portfolio.getNetWorth()));
+ }
+
+ @Test
+ void netWorthAfterAddReflectsCurrentPrice() {
+ portfolio.addShare(share);
+ // share: qty=10, salesPrice=10000, purchasePrice=15000 (loss).
+ // gross=100000, commission=1000, tax=0 (no profit), total=99000.
+ assertEquals(0, new BigDecimal("99000").compareTo(portfolio.getNetWorth()));
+ }
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/ShareTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/ShareTest.java
index 7655ba4..80af3ed 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/ShareTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/ShareTest.java
@@ -1,6 +1,8 @@
package edu.ntnu.idi.idatt2003.gruppe42.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
@@ -9,9 +11,10 @@
* Unit tests for the {@link Share} class.
*
*
Verifies that share properties (stock, quantity, and purchase price)
- * are correctly stored and retrieved.
+ * are correctly stored and retrieved, and that the constructor rejects invalid input.
*/
public class ShareTest {
+
@Test
void sharePropertiesTest() {
Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
@@ -23,4 +26,48 @@ void sharePropertiesTest() {
assertEquals(0, quantity.compareTo(share.getQuantity()));
assertEquals(0, purchasePrice.compareTo(share.getPurchasePrice()));
}
+
+ @Test
+ void nullStockThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(null, new BigDecimal("1"), new BigDecimal("100")));
+ }
+
+ @Test
+ void zeroQuantityThrows() {
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, BigDecimal.ZERO, new BigDecimal("100")));
+ }
+
+ @Test
+ void negativeQuantityThrows() {
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, new BigDecimal("-1"), new BigDecimal("100")));
+ }
+
+ @Test
+ void negativePurchasePriceThrows() {
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ assertThrows(IllegalArgumentException.class, () ->
+ new Share(stock, new BigDecimal("1"), new BigDecimal("-0.01")));
+ }
+
+ @Test
+ void equalSharesAreEqual() {
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ Share a = new Share(stock, new BigDecimal("5"), new BigDecimal("90"));
+ Share b = new Share(stock, new BigDecimal("5"), new BigDecimal("90"));
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ }
+
+ @Test
+ void differentQuantityNotEqual() {
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ Share a = new Share(stock, new BigDecimal("5"), new BigDecimal("90"));
+ Share b = new Share(stock, new BigDecimal("6"), new BigDecimal("90"));
+ assertNotEquals(a, b);
+ }
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
index ff9c44a..f98aceb 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandlerTest.java
@@ -2,10 +2,13 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException;
+import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
@@ -15,6 +18,14 @@
* including handling of valid entries, comments, and empty lines.
*/
public class StockFileHandlerTest {
+ private Path testFile;
+
+ @AfterEach
+ void tearDown() throws IOException {
+ if (testFile != null) {
+ Files.deleteIfExists(testFile);
+ }
+ }
/**
* Tests that a valid CSV file is correctly parsed into Stock objects.
@@ -41,15 +52,34 @@ void readFromFileTest() throws Exception {
* @throws Exception if file creation or reading fails
*/
@Test
- void readFromFileWithCommentsAndEmptyLines() throws Exception {
- Path testFile = Files.createTempFile("stocks", ".csv");
- Files.writeString(testFile,
- "#Comment\n\nAAPL,Apple Inc.,276.43");
+ void readFromFileWithCommentsAndEmptyLines() throws IOException, StockFileParseException {
+ testFile = Files.createTempFile("stocks", "csv");
+ Files.writeString(testFile, "#Comment\n\nAAPL,Apple Inc.,276.43");
- try (InputStream in = Files.newInputStream(testFile)) {
- List stocks = StockFileHandler.readFromFile(in);
+ List stocks = StockFileHandler.readFromFile(testFile);
+ assertEquals(1, stocks.size());
+ }
- assertEquals(1, stocks.size());
- }
+ @Test
+ void testReadInvalidSymbol() throws IOException {
+ testFile = Files.createTempFile("invalid_symbol", "csv");
+ Files.writeString(testFile, "INVALIDTOOLONG,Apple,100.00");
+ org.junit.jupiter.api.Assertions.assertThrows(StockFileParseException.class, () ->
+ StockFileHandler.readFromFile(testFile));
}
+
+ @Test
+ void testReadInvalidPrice() throws IOException {
+ testFile = Files.createTempFile("invalid_price", "csv");
+ Files.writeString(testFile, "AAPL,Apple,100");
+ org.junit.jupiter.api.Assertions.assertThrows(StockFileParseException.class, () ->
+ StockFileHandler.readFromFile(testFile));
+ }
+
+ @Test
+ void testReadMissingFields() throws IOException {
+ testFile = Files.createTempFile("missing_fields", "csv");
+ Files.writeString(testFile, "AAPL,Apple");
+ org.junit.jupiter.api.Assertions.assertThrows(StockFileParseException.class, () ->
+ StockFileHandler.readFromFile(testFile));
}
\ No newline at end of file
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
index 1231d97..6c76b27 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/calculator/SaleCalculatorTest.java
@@ -69,4 +69,21 @@ void testCalculateTotal() {
assertNotEquals(0, new BigDecimal("1375.01").compareTo(calculator.calculateTotal()));
assertNotEquals(0, new BigDecimal("1374.99").compareTo(calculator.calculateTotal()));
}
+
+ @Test
+ void testCalculateTaxLoss() {
+ BigDecimal purchasePrice = new BigDecimal("150");
+ BigDecimal salesPrice = new BigDecimal("100");
+ BigDecimal quantity = new BigDecimal("10");
+ Stock stock = new Stock("AAPL", "Apple Inc.", salesPrice);
+ Share share = new Share(stock, quantity, purchasePrice);
+ SaleCalculator lossCalculator = new SaleCalculator(share);
+
+ // Tax should be 0 on a loss, not negative
+ assertEquals(0, BigDecimal.ZERO.compareTo(lossCalculator.calculateTax()), "Tax should be zero on loss");
+
+ // Total should be: gross(1000) - commission(10) - tax(0) = 990
+ BigDecimal expectedTotal = new BigDecimal("990");
+ assertEquals(0, expectedTotal.compareTo(lossCalculator.calculateTotal()), "Total payout should not include negative tax");
+ }
}
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchiveTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchiveTest.java
index 9502a4d..227ff53 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchiveTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/model/transaction/TransactionArchiveTest.java
@@ -27,7 +27,7 @@ void setUp() {
Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("90"));
purchase = new Purchase(share, 1);
- sale = new Sale(share, 2);
+ sale = new Sale(share, 2);
}
@Test
@@ -67,4 +67,37 @@ void countDistinctWeeksTest() {
assertEquals(2, archive.countDistinctWeeks());
}
+
+ @Test
+ void testCalculateNetIncome() {
+ // Purchase: 10 shares @ 90 → gross 900, commission 9, total cost 909.
+ // Sale: 10 shares @ 100 → gross 1000, commission 10, tax 22% of (100-90)*10=22, total 968.
+ // Net = 968 - 909 = 59.
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
+ Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("90"));
+ Purchase p = new Purchase(share, 1);
+ Sale s = new Sale(share, 1);
+
+ archive.add(p);
+ archive.add(s);
+
+ BigDecimal net = archive.calculateNetIncome(1);
+ assertEquals(0, new BigDecimal("59.00").compareTo(net));
+ }
+
+ @Test
+ void testCalculateNetIncomeEmptyWeek() {
+ assertEquals(0, BigDecimal.ZERO.compareTo(archive.calculateNetIncome(99)));
+ }
+
+ @Test
+ void testCountDistinctWeeksEmptyArchive() {
+ assertEquals(0, archive.countDistinctWeeks());
+ }
+
+ @Test
+ void testGetTransactionsUnknownWeekReturnsEmpty() {
+ archive.add(purchase);
+ assertTrue(archive.getTransactions(999).isEmpty());
+ }
}