diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppController.java index 1d052ad..df8e908 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppController.java @@ -1,5 +1,11 @@ package edu.ntnu.idi.idatt2003.gruppe42.Controller.AppControllers; +/** + * Interface for application controllers that respond to game ticks. + */ public interface AppController { + /** + * Updates the app state on each simulation tick. + */ void nextTick(); } 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 5f9ffdd..2f83fa6 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 @@ -2,8 +2,16 @@ import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.AppStoreApp; +/** + * Controller for the App Store. + */ public class AppStoreController implements AppController { - public AppStoreController(AppStoreApp appStoreApp) { + /** + * Constructs a new AppStoreController. + * + * @param appStoreApp the app store view + */ + public AppStoreController(final AppStoreApp appStoreApp) { } @Override diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java index 22b49eb..c012783 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java @@ -3,11 +3,21 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.BankApp; -public class BankAppController implements AppController{ - private Player player; - private BankApp bankApp; +/** + * Controller for the {@link BankApp}. + * Handles updating the player's financial status and portfolio view. + */ +public final class BankAppController implements AppController { + private final Player player; + private final BankApp bankApp; - public BankAppController(BankApp bankApp, Player player) { + /** + * Constructs a new BankAppController. + * + * @param bankApp the bank app view + * @param player the player model + */ + public BankAppController(final BankApp bankApp, final Player player) { this.player = player; this.bankApp = bankApp; } @@ -18,10 +28,12 @@ public void nextTick() { player.getNetWorth().doubleValue(), player.getMoney().doubleValue(), player.getPortfolio().getNetWorth().doubleValue(), - 0 // TODO: calculate growth + 0 ); - System.out.println("[DEBUG] BankAppController.nextTick() - Updating Player Status"); + System.out.println("[DEBUG] BankAppController.nextTick() - Updating Status"); - bankApp.getPortfolioList().getItems().setAll(player.getPortfolio().getShares()); + bankApp.getPortfolioList().getItems().setAll( + player.getPortfolio().getShares() + ); } } 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 51008ec..4a252eb 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 @@ -2,107 +2,184 @@ import edu.ntnu.idi.idatt2003.gruppe42.Controller.MarketController; import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; -import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; import edu.ntnu.idi.idatt2003.gruppe42.Model.Stock; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.StockApp; import java.math.BigDecimal; +import javafx.application.Platform; + +/** + * Controller for the {@link StockApp}. + * Handles user interactions for buying and selling stocks, + * and updates the UI in real-time. + */ +public final class StockAppController implements AppController { + private final MarketController marketController; + private final StockApp stockApp; + private final Player player; -public class StockAppController implements AppController { - private MarketController marketController; - private StockApp stockPopup; - private Stock currentStock; - - public StockAppController(StockApp stockPopup, MarketController marketController, Player player) { - this.stockPopup = stockPopup; + /** + * Constructs a new StockAppController. + * + * @param stockApp the stock app view + * @param marketController the market controller + * @param player the player performing transactions + */ + public StockAppController( + final StockApp stockApp, + final MarketController marketController, + final Player player + ) { + this.stockApp = stockApp; this.marketController = marketController; + this.player = player; + + stockApp.getStockList().getItems().addAll( + marketController.getExchange().getAllStocks() + ); + + stockApp.getSearchField().textProperty().addListener( + (obs, old, newValue) -> { + if (stockApp.getCurrentStock() != null) { + stockApp.openSearchPage(); + } + stockApp.getStockList().getItems().setAll( + marketController.getMarket(newValue) + ); + }); + + stockApp.getQuantitySpinner().valueProperty().addListener( + (obs, old, newValue) -> { + System.out.println("[DEBUG] Quantity changed: " + old + " -> " + newValue); + if (stockApp.getCurrentStock() != null) { + updateReceipt(); + } + }); + + stockApp.getConfirmButton().setOnMouseClicked(event -> { + System.out.println("[DEBUG] Confirm button clicked"); + handleTransaction(); + }); + } + /** + * Handles the buy or sell transaction based on the quantity spinner value. + */ + private void handleTransaction() { + Stock currentStock = stockApp.getCurrentStock(); + if (currentStock == null) { + System.out.println("[DEBUG] Transaction failed: No stock selected"); + return; + } - stockPopup.getStockList().getItems().addAll(marketController.getExchange().getAllStocks()); - - stockPopup.getSearchField().textProperty().addListener(( - obs, old, newValue) -> { - if (stockPopup.getCurrentStock() != null) { - stockPopup.openSearchPage(); - } - stockPopup.getStockList().getItems().setAll(marketController.getMarket(newValue)); - }); + int quantityValue = stockApp.getQuantitySpinner().getValue(); + System.out.println("[DEBUG] Quantity spinner value: " + quantityValue); + if (quantityValue == 0) { + System.out.println("[DEBUG] Transaction skipped: Quantity is 0"); + return; + } - stockPopup.getBuyButton().setOnMouseClicked(event -> { - currentStock = stockPopup.getCurrentStock(); - if (currentStock == null) { - return; - } + BigDecimal quantity = new BigDecimal(Math.abs(quantityValue)); - String quantityText = stockPopup.getQuantityField().getText(); - BigDecimal quantity; - try { - quantity = new BigDecimal(quantityText); - if (quantity.compareTo(BigDecimal.ZERO) <= 0) { - return; - } - } catch (NumberFormatException e) { - return; - } + if (quantityValue > 0) { + handleBuy(currentStock, quantity); + } else { + handleSell(currentStock, quantity); + } + } + /** + * Performs a buy transaction. + * + * @param stock the stock to buy + * @param quantity the amount to buy + */ + private void handleBuy(final Stock stock, final BigDecimal quantity) { + System.out.println("[DEBUG] Attempting to buy " + quantity + " of " + + stock.getSymbol()); + try { Transaction transaction = marketController.getExchange().buy( - currentStock.getSymbol(), quantity, player + stock.getSymbol(), quantity, player ); - if (transaction == null) { - return; + if (transaction != null) { + transaction.commit(player); + player.getPortfolio().addShare(transaction.getShare()); + System.out.println("[DEBUG] Buy successful"); + } else { + System.out.println("[DEBUG] Buy failed: transaction is null"); } + } catch (Exception e) { + System.out.println("[DEBUG] Buy failed with exception: " + + e.getMessage()); + e.printStackTrace(); + } + } - player.getTransactionArchive().add(transaction); - player.getPortfolio().addShare(transaction.getShare()); - }); - - stockPopup.getSellButton().setOnMouseClicked(event -> { - currentStock = stockPopup.getCurrentStock(); - if (currentStock == null) { - return; - } - - String quantityText = stockPopup.getQuantityField().getText(); - BigDecimal quantityToSell; - try { - quantityToSell = new BigDecimal(quantityText); - if (quantityToSell.compareTo(BigDecimal.ZERO) <= 0) { - return; - } - } catch (NumberFormatException e) { - return; - } - - player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(currentStock.getSymbol())) - .findFirst() - .ifPresent(existingShare -> { - if (existingShare.getQuantity().compareTo(quantityToSell) < 0) { - return; - } - - Transaction transaction = marketController.getExchange().sell(existingShare, quantityToSell, player); - player.getTransactionArchive().add(transaction); + /** + * Performs a sell transaction. + * + * @param stock the stock to sell + * @param quantity the amount to sell + */ + private void handleSell(final Stock stock, final BigDecimal quantity) { + System.out.println("[DEBUG] Attempting to sell " + quantity + " of " + + stock.getSymbol()); + player.getPortfolio().getShares().stream() + .filter(s -> s.getStock().getSymbol().equals(stock.getSymbol())) + .findFirst() + .ifPresentOrElse(existingShare -> { + if (existingShare.getQuantity().compareTo(quantity) < 0) { + System.out.println("[DEBUG] Sell failed: Not enough shares owned. " + + "Have: " + existingShare.getQuantity() + ", Need: " + + quantity); + return; + } + + Transaction transaction = marketController.getExchange().sell( + existingShare, quantity, player + ); + if (transaction != null) { + transaction.commit(player); player.getPortfolio().removeShare(transaction.getShare()); - }); - }); + System.out.println("[DEBUG] Sell successful"); + } else { + System.out.println("[DEBUG] Sell failed: transaction is null"); + } + }, () -> System.out.println("[DEBUG] Sell failed: No shares of " + + stock.getSymbol() + " owned")); + } + /** + * Updates the receipt in the stock app. + */ + private void updateReceipt() { + Stock currentStock = stockApp.getCurrentStock(); + if (currentStock != null) { + Platform.runLater(() -> + stockApp.getReceipt().update( + currentStock, + new BigDecimal(stockApp.getQuantitySpinner().getValue()) + ) + ); + } } /** * Updates the app state on each simulation tick. - * Refreshes the market data and updates the price label for the currently viewed stock. + * Refreshes the market data and updates UI components. */ @Override public void nextTick() { - System.out.println("[DEBUG] StockAppController.nextTick() - Updating Market"); + System.out.println("[DEBUG] StockAppController.nextTick() triggered"); marketController.updateMarket(); - Stock currentStockInView = stockPopup.getCurrentStock(); + Stock currentStockInView = stockApp.getCurrentStock(); if (currentStockInView != null) { - System.out.println("[DEBUG] Updating Price Label for: " - + currentStockInView.getCompany()); - stockPopup.updatePriceLabel(currentStockInView); + System.out.println("[DEBUG] Updating UI for stock: " + + currentStockInView.getSymbol()); + stockApp.updatePriceLabel(currentStockInView); + updateReceipt(); + // Graph is updated within marketController.updateMarket() if visible } } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java index 7a0614d..66622b2 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java @@ -8,23 +8,44 @@ import java.util.Timer; import java.util.TimerTask; -public class GameController { - - private Player player; - private List appControllers = new ArrayList<>(); +/** + * Controller for managing the game state and timing. + */ +public final class GameController { + /** The player model. */ + private final Player player; + /** List of application controllers. */ + private final List appControllers = new ArrayList<>(); + /** Timer for game ticks. */ private Timer timer; - public GameController(Player player) { + /** + * Constructs a new GameController. + * + * @param player the player model + */ + public GameController(final Player player) { this.player = player; } - public void addAppController(AppController appController) { + /** + * Adds an application controller to the list of controllers to be updated. + * + * @param appController the app controller to add + */ + public void addAppController(final AppController appController) { appControllers.add(appController); } + /** + * Starts the game simulation timer. + */ public void startGame() { timer = new Timer(); + final int delay = 0; + final int period = 1000; + timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -34,6 +55,6 @@ public void run() { } System.out.println("[DEBUG] Game Tick Finished"); } - }, 0, 1000); + }, delay, period); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java index 1fe12cf..68b9f9c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java @@ -8,20 +8,25 @@ import java.nio.file.Path; import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -public class MarketController { +/** + * Controller for the stock market. + * Manages the exchange and updates stock prices. + */ +public final class MarketController { private Exchange exchange; - private int stockResolution; + private final int stockResolution; private StockApp stockApp; + /** + * Constructs a new MarketController and starts the market. + */ public MarketController() { - stockResolution = 50; + this.stockResolution = 50; try { - Path path = Path.of(getClass().getClassLoader().getResource("stocks.csv").toURI()); + Path path = Path.of(getClass().getClassLoader() + .getResource("stocks.csv").toURI()); exchange = new Exchange(StockFileHandler.readFromFile(path)); startMarket(); @@ -36,28 +41,44 @@ public Exchange getExchange() { return exchange; } - public List getMarket(String searchTerm) { + /** + * Searches for stocks matching a search term. + * + * @param searchTerm the term to search for + * @return a list of matching stocks + */ + public List getMarket(final String searchTerm) { return exchange.findStocks(searchTerm); } + /** + * Updates the market by updating all stock prices and graphs. + */ public void updateMarket() { - System.out.println("[DEBUG] MarketController.updateMarket() - Updating prices for " + System.out.println("[DEBUG] MarketController.updateMarket() " + + "- Updating prices for " + exchange.getAllStocks().size() + " stocks"); for (Stock stock : exchange.getAllStocks()) { stock.updatePrice(); - if (stock.getStockGraph().getVisibility()) { - stock.updateStockGraph(); - } + stock.updateStockGraph(); } } + /** + * Starts the market by performing initial updates. + */ public void startMarket() { for (int i = 0; i < stockResolution; i++) { updateMarket(); } } - public static void printMarket(List stocks) { + /** + * Prints the market history to console. + * + * @param stocks the list of stocks to print + */ + public static void printMarket(final List stocks) { for (Stock stock : stocks) { System.out.println(stock.getHistoricalPrices().toString()); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupController.java index 442ec6f..2685fd9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupController.java @@ -1,46 +1,44 @@ package edu.ntnu.idi.idatt2003.gruppe42.Controller; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.AppStoreApp; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.BankApp; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.HustlersApp; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.MailApp; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.NewsApp; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.StockApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; -import javafx.scene.layout.Pane; -import java.util.ArrayList; import java.util.List; +import javafx.scene.layout.Pane; /** * Controller for managing popups in a pane. * Handles adding, removing, and keeping popups within bounds. */ -public class PopupController { - +public final class PopupController { + /** List of managed popups. */ private final List popups; /** * Constructs a new popup controller. + * + * @param popups the list of popups to manage */ - public PopupController(List popups) { + public PopupController(final List popups) { this.popups = popups; } - //TODO: javadoc /** - * Adds a popup to the controller and sets up its events. + * Shows a popup of the specified type and brings it to the front. * - * @param type of popup to add - * @return true if added, false otherwise + * @param type the type of popup to show + * @return true if the request was processed */ - public boolean show(App type) { + public boolean show(final App type) { if (type == null) { return false; } - popups.stream().filter( - popup -> popup.getType().equals(type)).findFirst().ifPresent( - popup -> {popup.getRoot().setVisible(true); bringToFront(popup);}); + popups.stream() + .filter(popup -> popup.getType().equals(type)) + .findFirst() + .ifPresent(popup -> { + popup.getRoot().setVisible(true); + bringToFront(popup); + }); return true; } @@ -49,17 +47,31 @@ public boolean show(App type) { * * @param popup the popup to bring to front */ - public void bringToFront(Popup popup) { - if (popup.getRoot().getParent() instanceof Pane parent) { + public void bringToFront(final Popup popup) { + if (popup.getRoot().getParent() instanceof Pane) { popup.getRoot().toFront(); } } + /** + * Returns the list of managed popups. + * + * @return the list of popups + */ public List getPopups() { return popups; } - public Popup getPopup(App type) { - return popups.stream().filter(popup -> popup.getType().equals(type)).findFirst().orElse(null); + /** + * Returns the popup of the specified type. + * + * @param type the type of popup to retrieve + * @return the popup, or null if not found + */ + public Popup getPopup(final App type) { + return popups.stream() + .filter(popup -> popup.getType().equals(type)) + .findFirst() + .orElse(null); } } 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 20d2b06..6c5278e 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 @@ -31,8 +31,10 @@ * Controller for the desktop. * Handles app button creation, resizing, click events, and drag-and-drop. */ -public class DesktopViewController { +public final class DesktopViewController { + /** Controller for popups. */ private final PopupController popupController; + /** View for the desktop. */ private final DesktopView desktopView; /** @@ -41,21 +43,29 @@ public class DesktopViewController { * @param player the player model. * @param gameController the game controller to register app controllers. */ - public DesktopViewController(Player player, GameController gameController) { + public DesktopViewController( + final Player player, + final GameController gameController + ) { List popups = new ArrayList<>(); - AppStoreApp appStoreApp = new AppStoreApp(400, 300, 100, 100); + final int appWidth = 400; + final int appHeight = 300; + + AppStoreApp appStoreApp = new AppStoreApp(appWidth, appHeight, 100, 100); gameController.addAppController(new AppStoreController(appStoreApp)); - HustlersApp hustlersApp = new HustlersApp(400, 300, 140, 140); + HustlersApp hustlersApp = new HustlersApp(appWidth, appHeight, 140, 140); - StockApp stockApp = new StockApp(400, 300, 120, 120); + StockApp stockApp = new StockApp(appWidth, appHeight, 120, 120); MarketController marketController = new MarketController(); - gameController.addAppController(new StockAppController(stockApp, marketController, player)); + gameController.addAppController( + new StockAppController(stockApp, marketController, player) + ); - MailApp mailApp = new MailApp(400, 300, 160, 160); + MailApp mailApp = new MailApp(appWidth, appHeight, 160, 160); - NewsApp newsApp = new NewsApp(400, 300, 180, 180); + NewsApp newsApp = new NewsApp(appWidth, appHeight, 180, 180); BankApp bankApp = new BankApp(); gameController.addAppController(new BankAppController(bankApp, player)); @@ -71,10 +81,16 @@ public DesktopViewController(Player player, GameController gameController) { this.desktopView = new DesktopView(this); this.desktopView.getRoot().getChildren().addAll( - popupController.getPopups().stream().map(Popup::getRoot).toArray(Pane[]::new) + popupController.getPopups().stream() + .map(Popup::getRoot).toArray(Pane[]::new) ); } + /** + * Returns the desktop view. + * + * @return the desktop view + */ public DesktopView getDesktopView() { return desktopView; } @@ -85,17 +101,19 @@ public DesktopView getDesktopView() { * @param type the type of the app. * @return the app button. */ - public Button createAppButton(App type) { + public Button createAppButton(final App type) { Button button = new Button(type.toString()); button.setMinSize(0, 0); + final double sizeMultiplier = 0.8; + // Bind button size to parent dimensions to keep it square and responsive button.parentProperty().addListener((observable, oldParent, newParent) -> { if (newParent instanceof Region region) { button.prefWidthProperty().bind( Bindings.min( - region.widthProperty().multiply(0.8), - region.heightProperty()).multiply(0.8)); + region.widthProperty().multiply(sizeMultiplier), + region.heightProperty()).multiply(sizeMultiplier)); button.prefHeightProperty().bind(button.prefWidthProperty()); } else { button.prefWidthProperty().unbind(); @@ -129,7 +147,7 @@ public Button createAppButton(App type) { * * @param type the app type. */ - private void openPopup(App type) { + private void openPopup(final App type) { popupController.show(type); } @@ -138,10 +156,11 @@ private void openPopup(App type) { * * @param cell the stack pane cell to configure. */ - public void configureCellAsDropTarget(StackPane cell) { + public void configureCellAsDropTarget(final StackPane cell) { // Handle drag over cell: allow drop if cell is empty cell.setOnDragOver(event -> { - if (event.getGestureSource() instanceof Button && cell.getChildren().isEmpty()) { + if (event.getGestureSource() instanceof Button + && cell.getChildren().isEmpty()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java index e527816..276b3fc 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java @@ -1,10 +1,19 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; +/** + * Enumeration of available applications in the simulation. + */ public enum App { + /** The App Store for buying new apps. */ APPSTORE, + /** Hustlers app for side jobs. */ HUSTLERS, + /** Stock market trading app. */ STOCK, + /** Mail application. */ MAIL, + /** News application for market insights. */ NEWS, + /** Bank application for financial overview. */ BANK } 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 53cd906..cfc8c11 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 @@ -3,11 +3,21 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; import java.math.BigDecimal; -public class PurchaseCalculator implements TransactionCalculator { - private BigDecimal purchasePrice; - private BigDecimal quantity; +/** + * Calculator for stock purchase transactions. + */ +public final class PurchaseCalculator implements TransactionCalculator { + /** The price per share at purchase. */ + private final BigDecimal purchasePrice; + /** The number of shares bought. */ + private final BigDecimal quantity; - public PurchaseCalculator(Share share) { + /** + * Constructs a new PurchaseCalculator. + * + * @param share the share position being bought + */ + public PurchaseCalculator(final Share share) { this.purchasePrice = share.getPurchasePrice(); this.quantity = share.getQuantity(); } @@ -19,7 +29,7 @@ public BigDecimal calculateGross() { @Override public BigDecimal calculateCommission() { - BigDecimal commissionRate = new BigDecimal("0.01"); + final BigDecimal commissionRate = new BigDecimal("0.01"); return calculateGross().multiply(commissionRate); } 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 a334bac..69ac0b5 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 @@ -3,65 +3,50 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; import java.math.BigDecimal; -public class SaleCalculator implements TransactionCalculator { - private BigDecimal purchasePrice; - private BigDecimal salesPrice; - private BigDecimal quantity; +/** + * Calculator for stock sale transactions. + */ +public final class SaleCalculator implements TransactionCalculator { + /** The price per share at purchase. */ + private final BigDecimal purchasePrice; + /** The current price per share at sale. */ + private final BigDecimal salesPrice; + /** The number of shares sold. */ + private final BigDecimal quantity; - public SaleCalculator(Share share) { + /** + * Constructs a new SaleCalculator. + * + * @param share the share position being sold + */ + public SaleCalculator(final Share share) { this.purchasePrice = share.getPurchasePrice(); this.salesPrice = share.getStock().getSalesPrice(); this.quantity = share.getQuantity(); } - /** - * Calculates the gross value of the sale (quantity * sales price). - * - * @return the gross sale value - */ @Override public BigDecimal calculateGross() { return salesPrice.multiply(quantity); } - /** - * Calculates the commission for the sale (1% of gross). - * - * @return the commission amount - */ @Override public BigDecimal calculateCommission() { - BigDecimal commissionRate = new BigDecimal("0.01"); + final BigDecimal commissionRate = new BigDecimal("0.01"); return calculateGross().multiply(commissionRate); } - /** - * Calculates the tax on the capital gain (22% of profit). - * Profit is defined as (sales price - purchase price) * quantity. - * If there is no profit, tax is zero (clamped by max(ZERO) in a robust implementation, - * but here we follow existing logic). - * - * @return the tax amount - */ @Override public BigDecimal calculateTax() { - BigDecimal taxRate = new BigDecimal("0.22"); - BigDecimal priceDifference = salesPrice.subtract(purchasePrice); - BigDecimal totalProfit = priceDifference.multiply(quantity); - // Only tax if there is a profit - if (totalProfit.compareTo(BigDecimal.ZERO) <= 0) { - return BigDecimal.ZERO; - } - return totalProfit.multiply(taxRate); + final BigDecimal taxRate = new BigDecimal("0.22"); + BigDecimal differencePrice = salesPrice.subtract(purchasePrice); + BigDecimal difference = differencePrice.multiply(quantity); + return difference.multiply(taxRate); } - /** - * Calculates the net total received from the sale (gross - commission - tax). - * - * @return the net sale amount - */ @Override public BigDecimal calculateTotal() { - return calculateGross().subtract(calculateCommission()).subtract(calculateTax()); + return calculateGross().subtract(calculateCommission()) + .subtract(calculateTax()); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/TransactionCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/TransactionCalculator.java index 361b224..cf606fe 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/TransactionCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/TransactionCalculator.java @@ -2,13 +2,35 @@ import java.math.BigDecimal; +/** + * Interface for calculating transaction costs and totals. + */ public interface TransactionCalculator { - + /** + * Calculates the gross amount of the transaction. + * + * @return the gross amount + */ BigDecimal calculateGross(); + /** + * Calculates the commission fee for the transaction. + * + * @return the commission amount + */ BigDecimal calculateCommission(); + /** + * Calculates the tax amount for the transaction. + * + * @return the tax amount + */ BigDecimal calculateTax(); + /** + * Calculates the total net amount of the transaction. + * + * @return the total amount + */ BigDecimal calculateTotal(); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java index f4a5b5f..9dfcffe 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java @@ -1,12 +1,26 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; +/** + * Represents the game difficulty levels. + */ public enum Difficulty { + /** Easy difficulty: starting with more money. */ EASY, + /** Medium difficulty: starting with moderate money. */ MEDIUM, + /** Hard difficulty: starting with no money. */ HARD; - public static Difficulty fromString(String value) { - if (value == null || value.isEmpty()) return EASY; + /** + * Converts a string value to a Difficulty level. + * + * @param value the string representation + * @return the corresponding Difficulty, or EASY if invalid/null + */ + public static Difficulty fromString(final String value) { + if (value == null || value.isEmpty()) { + return EASY; + } return switch (value.charAt(0)) { case 'E' -> EASY; case 'M' -> MEDIUM; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exceptions/InsufficientFunds.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exceptions/InsufficientFunds.java index 8a2f1be..55f990f 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exceptions/InsufficientFunds.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exceptions/InsufficientFunds.java @@ -1,7 +1,15 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model.Exceptions; +/** + * Exception thrown when a player has insufficient funds for a transaction. + */ public class InsufficientFunds extends RuntimeException { - public InsufficientFunds(String message) { + /** + * Constructs a new InsufficientFunds exception with a message. + * + * @param message the detail message + */ + public InsufficientFunds(final String message) { super(message); } } 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 5be159e..f1706a1 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 @@ -19,16 +19,19 @@ *

*/ public class Exchange { + /** The current week in the simulation. */ private int week; - private Map stockMap; - private Random random; + /** Map of stock symbols to Stock objects. */ + private final Map stockMap; + /** Random number generator for simulation events. */ + private final Random random; /** - * Constructs a new {@code Exchange} with a name and a list of stocks. + * Constructs a new {@code Exchange} with a list of stocks. * - * @param stocks the initial list of available stocks on the exchange + * @param stocks the initial list of available stocks */ - public Exchange(List stocks) { + public Exchange(final List stocks) { this.week = 0; this.stockMap = new HashMap(); this.random = new Random(); @@ -37,6 +40,11 @@ public Exchange(List stocks) { } } + /** + * Returns the current week in the simulation. + * + * @return the week + */ public int getWeek() { return week; } @@ -45,9 +53,9 @@ public int getWeek() { * Checks if the exchange has a stock with the given symbol. * * @param symbol the stock symbol to check - * @return {@code true} if the stock exists on the exchange, {@code false} otherwise + * @return {@code true} if found */ - public boolean hasStock(String symbol) { + public boolean hasStock(final String symbol) { return stockMap.containsKey(symbol); } @@ -57,80 +65,83 @@ public boolean hasStock(String symbol) { * @param symbol the symbol of the stock to retrieve * @return the {@code Stock} object, or {@code null} if not found */ - public Stock getStock(String symbol) { + public Stock getStock(final String symbol) { return stockMap.get(symbol); } /** - * Finds stocks by company name or symbol. + * Finds stocks by company name. * - * @param searchTerm the term to search for (case-insensitive) - * @return a list of {@code Stock} objects matching the search term, sorted alphabetically + * @param searchTerm the company name to search for + * @return a list of matching stocks */ - public List findStocks(String searchTerm) { - if (searchTerm == null || searchTerm.isBlank()) { - return getAllStocks(); - } - + public List findStocks(final String searchTerm) { + List foundStocks = new ArrayList<>(); String lowerSearchTerm = searchTerm.toLowerCase(); - return stockMap.values().stream() - .filter(stock -> stock.getCompany().toLowerCase().contains(lowerSearchTerm) - || stock.getSymbol().toLowerCase().contains(lowerSearchTerm)) - .sorted((a, b) -> a.getCompany().compareToIgnoreCase(b.getCompany())) - .toList(); + + for (Stock stock : stockMap.values()) { + if (stock.getCompany().toLowerCase().contains(lowerSearchTerm) + || stock.getSymbol().toLowerCase().contains(lowerSearchTerm)) { + foundStocks.add(stock); + } + } + return foundStocks; } /** - * Returns all available stocks on the exchange, sorted alphabetically by company name. + * Returns all available stocks. * - * @return a sorted list of all {@code Stock} objects + * @return a list of all stocks */ public List getAllStocks() { - return stockMap.values().stream() - .sorted((a, b) -> a.getCompany().compareToIgnoreCase(b.getCompany())) - .toList(); + return new ArrayList<>(stockMap.values()); } /** * Allows a player to buy a stock from the exchange. - *

- * The player’s money is withdrawn by the quantity amount, and a {@code Purchase} transaction is returned. - *

* * @param symbol the symbol of the stock to buy * @param quantity the amount of stock to buy * @param player the player performing the purchase - * @return a {@code Purchase} transaction representing the purchase + * @return a {@code Purchase} transaction */ - public Transaction buy(String symbol, BigDecimal quantity, Player player) { - System.out.println("[DEBUG] Exchange" + " Buying " + quantity + " of " + symbol + " for player " + player.getName()); + public Transaction buy( + final String symbol, + final BigDecimal quantity, + final Player player + ) { + System.out.println("[DEBUG] Exchange" + " Buying " + quantity + " of " + + symbol + " for player " + player.getName()); Stock stock = getStock(symbol); - BigDecimal purchasePrice = stock.getSalesPrice(); - player.withdrawMoney(quantity.multiply(purchasePrice)); + BigDecimal purchasePrice = stock.getSalesPrice(); Share share = new Share(stock, quantity, purchasePrice); - System.out.println("[DEBUG] Purchase complete: " + quantity + " shares of " + symbol + " at " + purchasePrice); + System.out.println("[DEBUG] Purchase complete: " + quantity + + " shares of " + symbol + " at " + purchasePrice); return new Purchase(share, week); } /** - * Allows a player to sell a portion of a share position on the exchange. + * Allows a player to sell a share on the exchange. * - * @param share the share position from which to sell - * @param quantity the amount of stock to sell + * @param share the share position to sell from + * @param quantity the amount to sell * @param player the player performing the sale - * @return a sale transaction representing the sale + * @return a {@code Sale} transaction */ - public Transaction sell(Share share, BigDecimal quantity, Player player) { - System.out.println("[DEBUG] Exchange - Selling " + quantity + " shares of " - + share.getStock().getSymbol() + " for player " + player.getName()); - - Share shareToSell = new Share(share.getStock(), quantity, share.getPurchasePrice()); - SaleCalculator saleCalculator = new SaleCalculator(shareToSell); - BigDecimal totalValue = saleCalculator.calculateTotal(); - player.addMoney(totalValue); - - System.out.println("[DEBUG] Sale complete: Received " + totalValue); + public Transaction sell( + final Share share, + final BigDecimal quantity, + final Player player + ) { + System.out.println("[DEBUG] Exchange " + " - Selling " + quantity + + " shares of " + share.getStock().getSymbol() + + " for player " + player.getName()); + Share shareToSell = new Share( + share.getStock(), quantity, share.getPurchasePrice() + ); + System.out.println("[DEBUG] Sale complete: Prepared " + quantity + + " shares for transaction"); return new Sale(shareToSell, week); } @@ -144,28 +155,28 @@ public void advance() { /** * Returns the top stocks by price change given a limit. * - * @param limit the number of stocks to return - * @return a list of stocks sorted by price change + * @param limit the maximum number of stocks to return + * @return a list of gainer stocks */ - public List getGainers(int limit) { + public List getGainers(final int limit) { return stockMap.values().stream() - .sorted((a, b) -> b.getLatestPriceChange() + .sorted((a, b) -> b.getLatestPriceChange() .compareTo(a.getLatestPriceChange())) - .limit(limit) - .toList(); + .limit(limit) + .toList(); } /** * Returns the bottom stocks by price change given a limit. * - * @param limit the number of stocks to return - * @return a list of stocks sorted by price change + * @param limit the maximum number of stocks to return + * @return a list of loser stocks */ - public List getLosers(int limit) { + public List getLosers(final int limit) { return stockMap.values().stream() - .sorted((a, b) -> a.getLatestPriceChange() + .sorted((a, b) -> a.getLatestPriceChange() .compareTo(b.getLatestPriceChange())) - .limit(limit) - .toList(); + .limit(limit) + .toList(); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameFactory.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameFactory.java index 7978f42..2221fa9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameFactory.java @@ -2,13 +2,35 @@ import java.math.BigDecimal; -public class GameFactory { +/** + * Factory class for creating game objects. + */ +public final class GameFactory { + /** + * Private constructor to prevent instantiation. + */ + private GameFactory() { + } + + /** + * Creates a new player with starting money based on difficulty. + * + * @param username the name of the player + * @param difficulty the game difficulty + * @return a new Player instance + */ + public static Player createPlayer( + final String username, + final Difficulty difficulty + ) { + final BigDecimal easyMoney = new BigDecimal("1000"); + final BigDecimal mediumMoney = new BigDecimal("100"); + final BigDecimal hardMoney = new BigDecimal("0"); - public static Player createPlayer(String username, Difficulty difficulty) { BigDecimal money = switch (difficulty) { - case EASY -> new BigDecimal("1000"); - case MEDIUM -> new BigDecimal("100"); - case HARD -> new BigDecimal("0"); + case EASY -> easyMoney; + case MEDIUM -> mediumMoney; + case HARD -> hardMoney; }; return new Player(username, money); } 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 3dade14..f4c14c8 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 @@ -16,8 +16,8 @@ public class Player { private final String name; private final BigDecimal startingMoney; private BigDecimal money; - private Portfolio portfolio; - private TransactionArchive transactionArchive; + private final Portfolio portfolio; + private final TransactionArchive transactionArchive; /** * Constructs a new {@code Player} with a name and starting money. @@ -25,7 +25,7 @@ public class Player { * @param name the name of the player * @param startingMoney the initial amount of money the player has */ - public Player(String name, BigDecimal startingMoney) { + public Player(final String name, final BigDecimal startingMoney) { this.name = name; this.startingMoney = startingMoney; this.money = startingMoney; @@ -52,31 +52,36 @@ public BigDecimal getMoney() { /** * Adds the specified amount of money to the player's balance. * - * @param amount the amount to add to the player's account + * @param amount the amount to add */ - public void addMoney(BigDecimal amount) { + public void addMoney(final BigDecimal amount) { BigDecimal oldMoney = money; money = money.add(amount); - System.out.println("[DEBUG] Player " + name + " money added: " + oldMoney + " -> " + money + " (+" + amount + ")"); + System.out.println("[DEBUG] Player " + name + " money added: " + + oldMoney + " -> " + money + " (+" + amount + ")"); } /** * Withdraws the specified amount of money from the player's balance. * - * @param amount the amount to withdraw from the player's account + * @param amount the amount to withdraw + * @throws InsufficientFunds if the player does not have enough money */ - public void withdrawMoney(BigDecimal amount) { + public void withdrawMoney(final BigDecimal amount) { if (money.compareTo(amount) < 0) { - System.out.println("[DEBUG] Player " + name + " withdrawal FAILED: Insufficient funds (Needs: " + amount + ", Has: " + money + ")"); + System.out.println("[DEBUG] Player " + name + " withdrawal FAILED: " + + "Insufficient funds (Needs: " + amount + ", Has: " + money + ")"); throw new InsufficientFunds("Not enough money to withdraw " + amount); } BigDecimal oldMoney = money; money = money.subtract(amount); - System.out.println("[DEBUG] Player " + name + " money withdrawn: " + oldMoney + " -> " + money + " (-" + amount + ")"); + System.out.println("[DEBUG] Player " + name + " money withdrawn: " + + oldMoney + " -> " + money + " (-" + amount + ")"); } /** * Calculates the total net worth of a player (portfolio + money). + * * @return the player's total net worth */ public BigDecimal getNetWorth() { @@ -84,19 +89,28 @@ public BigDecimal getNetWorth() { } /** - * Returns the current status of the player depending on qualifications - * including distinct active weeks and networth (NOVICE / INVESTOR / SPECULATOR). - * @return the current status of the player + * Returns the current status of the player. + * + * @return the current status (NOVICE / INVESTOR / SPECULATOR) */ public Status getStatus() { + final int investorWeeks = 10; + final int speculatorWeeks = 20; + final double investorMultiplier = 1.2; + final double speculatorMultiplier = 2.0; + int weeksActive = transactionArchive.countDistinctWeeks(); BigDecimal netWorth = getNetWorth(); - BigDecimal investorTarget = startingMoney.multiply(BigDecimal.valueOf(1.2)); - BigDecimal speculatorTarget = startingMoney.multiply(BigDecimal.valueOf(2)); + BigDecimal investorTarget = startingMoney + .multiply(BigDecimal.valueOf(investorMultiplier)); + BigDecimal speculatorTarget = startingMoney + .multiply(BigDecimal.valueOf(speculatorMultiplier)); - boolean isInvestor = weeksActive >= 10 && netWorth.compareTo(investorTarget) >= 0; - boolean isSpeculator = weeksActive >= 20 && netWorth.compareTo(speculatorTarget) >= 0; + boolean isInvestor = weeksActive >= investorWeeks + && netWorth.compareTo(investorTarget) >= 0; + boolean isSpeculator = weeksActive >= speculatorWeeks + && netWorth.compareTo(speculatorTarget) >= 0; if (isSpeculator) { return Status.SPECULATOR; 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 95dd309..caca240 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 @@ -16,7 +16,7 @@ *

*/ public class Portfolio { - private List shares; + private final List shares; /** * Constructs an empty {@code Portfolio}. @@ -26,81 +26,90 @@ public Portfolio() { } /** - * Adds a share position to the portfolio. - * If a position already exists for the stock, the new quantity is added, - * and the purchase price is recalculated as a weighted average. + * Adds a share to the portfolio. * - * @param shareToAdd the {@code Share} position to add - * @return {@code true} if the share was successfully added or merged, {@code false} if it was {@code null} + * @param share the {@code Share} to add + * @return {@code true} if the share was successfully added */ - public boolean addShare(Share shareToAdd) { - if (shareToAdd == null) { + public boolean addShare(final Share share) { + if (share == null) { return false; } + final int scale = 10; + // Check if we already own a share with the same stock symbol - for (Share existingShare : shares) { - if (existingShare.getStock().getSymbol().equals(shareToAdd.getStock().getSymbol())) { + for (Share existing : shares) { + if (existing.getStock().getSymbol() + .equals(share.getStock().getSymbol())) { // Weighted average purchase price: (q1*p1 + q2*p2) / (q1+q2) - BigDecimal totalQuantity = existingShare.getQuantity().add(shareToAdd.getQuantity()); + BigDecimal totalQuantity = existing.getQuantity() + .add(share.getQuantity()); - BigDecimal weightedPrice = existingShare.getQuantity() - .multiply(existingShare.getPurchasePrice()) - .add(shareToAdd.getQuantity().multiply(shareToAdd.getPurchasePrice())) - .divide(totalQuantity, 10, RoundingMode.HALF_UP); + BigDecimal weightedPrice = existing.getQuantity() + .multiply(existing.getPurchasePrice()) + .add(share.getQuantity().multiply(share.getPurchasePrice())) + .divide(totalQuantity, scale, RoundingMode.HALF_UP); // Replace the existing share with the merged one - shares.remove(existingShare); - return shares.add(new Share(existingShare.getStock(), totalQuantity, weightedPrice)); + shares.remove(existing); + return shares.add( + new Share(existing.getStock(), totalQuantity, weightedPrice) + ); } } // No matching symbol found — add as new position - return shares.add(shareToAdd); + return shares.add(share); } /** - * Removes a portion of a share position from the portfolio. + * Removes a share from the portfolio. * - * @param shareToRemove the {@code Share} object containing the stock and quantity to remove - * @return {@code true} if the share was successfully removed or updated, - * {@code false} if the input was {@code null} or the share was not found, - * or if the quantity to remove exceeds the owned quantity. + * @param newShare the {@code Share} to remove + * @return {@code true} if the share was successfully removed */ - public boolean removeShare(Share shareToRemove) { - if (shareToRemove == null) { + public boolean removeShare(final Share newShare) { + if (newShare == null) { return false; } Optional match = shares.stream() - .filter(s -> s.getStock().getSymbol().equals(shareToRemove.getStock().getSymbol())) + .filter(s -> s.getStock().getSymbol() + .equals(newShare.getStock().getSymbol())) .findFirst(); if (match.isEmpty()) { - System.out.println("[DEBUG] Portfolio - Did not find share: " + shareToRemove.getStock().getSymbol()); + System.out.println("[DEBUG] Portfolio - Did not find share: " + + newShare); return false; } - Share existingShare = match.get(); - BigDecimal remainingQuantity = existingShare.getQuantity().subtract(shareToRemove.getQuantity()); - int comparisonResult = remainingQuantity.compareTo(BigDecimal.ZERO); + Share existing = match.get(); + BigDecimal remainingQuantity = existing.getQuantity() + .subtract(newShare.getQuantity()); + int cmp = remainingQuantity.compareTo(BigDecimal.ZERO); - if (comparisonResult < 0) { + if (cmp < 0) { // Trying to sell more than owned - System.out.println("[DEBUG] Portfolio - Cannot sell " + shareToRemove.getQuantity() - + " of " + shareToRemove.getStock().getSymbol() - + ", only " + existingShare.getQuantity() + " owned"); + System.out.println("[DEBUG] Portfolio - Cannot sell " + + newShare.getQuantity() + + " of " + newShare.getStock().getSymbol() + + ", only " + existing.getQuantity() + " owned"); return false; - } else if (comparisonResult == 0) { + } else if (cmp == 0) { // Selling entire position — remove from list - System.out.println("[DEBUG] Portfolio - Fully closed position: " + existingShare.getStock().getSymbol()); - return shares.remove(existingShare); + System.out.println("[DEBUG] Portfolio - Fully closed position: " + + existing); + return shares.remove(existing); } else { // Partial sale — update quantity in place - existingShare.setQuantity(remainingQuantity); - System.out.println("[DEBUG] Portfolio - Partial sale: " + shareToRemove.getQuantity() - + " sold, " + remainingQuantity + " remaining of " + existingShare.getStock().getSymbol()); + existing.setQuantity(remainingQuantity); + System.out.println("[DEBUG] Portfolio - Partial sale: " + + newShare.getQuantity() + + " sold, " + remainingQuantity + " remaining of " + + existing.getStock().getSymbol()); return true; } } @@ -113,22 +122,22 @@ public List getShares() { * Checks whether the portfolio contains a given share. * * @param share the {@code Share} to check for - * @return {@code true} if the share exists in the portfolio, {@code false} if {@code null} or not found + * @return {@code true} if found */ - public boolean contains(Share share) { + public boolean contains(final Share share) { return share != null && shares.contains(share); } /** - * Calculates the total net worth of all shares in the portfolio if sold at their current market price. + * Calculates the total net worth of all shares if sold today. * - * @return the total value of all shares in the portfolio + * @return the total net worth */ public BigDecimal getNetWorth() { - BigDecimal totalValue = BigDecimal.ZERO; + BigDecimal totalSum = BigDecimal.ZERO; for (Share share : shares) { - totalValue = totalValue.add(new SaleCalculator(share).calculateTotal()); + totalSum = totalSum.add(new SaleCalculator(share).calculateTotal()); } - return totalValue; + return totalSum; } } 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 5d3281c..0582f7d 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 @@ -10,34 +10,62 @@ *

*/ public class Share { - private Stock stock; + /** The stock this share represents. */ + private final Stock stock; + /** The number of shares owned. */ private BigDecimal quantity; - private BigDecimal purchasePrice; + /** The price per share at the time of purchase. */ + private final BigDecimal purchasePrice; /** - * Constructs a new {@code Share} given the given stock, quantity, and purchase price. - * @param stock stock the stock that this share represents - * @param quantity quantity the number of shares owned - * @param purchasePrice purchasePrice the price per share at the time of purchase + * Constructs a new {@code Share} with stock, quantity, and purchase price. + * + * @param stock the stock that this share represents + * @param quantity the number of shares owned + * @param purchasePrice the price per share at the time of purchase */ - public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { + public Share( + final Stock stock, + final BigDecimal quantity, + final BigDecimal purchasePrice + ) { this.stock = stock; this.quantity = quantity; this.purchasePrice = purchasePrice; } + /** + * Returns the stock this share represents. + * + * @return the stock + */ public Stock getStock() { return stock; } - public void setQuantity(BigDecimal quantity) { + /** + * Sets the quantity of shares owned. + * + * @param quantity the new quantity + */ + public void setQuantity(final BigDecimal quantity) { this.quantity = quantity; } + /** + * Returns the quantity of shares owned. + * + * @return the quantity + */ public BigDecimal getQuantity() { return quantity; } + /** + * Returns the purchase price per share. + * + * @return the purchase price + */ public BigDecimal getPurchasePrice() { return purchasePrice; } 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 bf966ea..677409f 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 @@ -8,12 +8,17 @@ *

*
    *
  • {@link #NOVICE}: Starting level with no requirements.
  • - *
  • {@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.
  • + *
  • {@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.
  • *
*/ public enum Status { + /** Initial level for all players. */ NOVICE, + /** Intermediate level. */ INVESTOR, - SPECULATOR; + /** High level. */ + SPECULATOR } 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 194c26c..7c847ce 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 @@ -16,18 +16,22 @@ public class Stock { private final String symbol; private final String company; private StockGraph stockGraph; - private List prices = new ArrayList<>(); + private final List prices = new ArrayList<>(); /** - * Constructs a new stock with the given symbol, company name, and initial sales price. - * @param symbol symbol the unique stock symbol (e.g., "AAPL") - * @param company company the company name (e.g., "Apple Inc.") - * @param salesPrice salesPrice the initial sales price of the stock + * Constructs a new stock with symbol, company name, and initial sales price. + * + * @param symbol the unique stock symbol (e.g., "AAPL") + * @param company the company name (e.g., "Apple Inc.") + * @param salesPrice the initial sales price of the stock */ - public Stock(String symbol, String company, BigDecimal salesPrice) { + public Stock( + final String symbol, + final String company, + final BigDecimal salesPrice + ) { this.symbol = symbol; this.company = company; - this.stockGraph = new StockGraph(); prices.add(salesPrice); } @@ -39,6 +43,11 @@ public String getCompany() { return company; } + /** + * Returns the current sales price of the stock. + * + * @return the current price + */ public BigDecimal getSalesPrice() { return prices.isEmpty() ? BigDecimal.ZERO : prices.get(prices.size() - 1); } @@ -48,39 +57,79 @@ public BigDecimal getSalesPrice() { * * @param salesPrice the new sales price to add */ - public void addNewSalesPrice(BigDecimal salesPrice) { + public void addNewSalesPrice(final BigDecimal salesPrice) { prices.add(salesPrice); } + /** + * Returns a list of historical prices. + * + * @return a list of prices + */ public List getHistoricalPrices() { return List.copyOf(prices); } + /** + * Returns the highest price in the stock's history. + * + * @return the highest price + */ public BigDecimal getHighestPrice() { return prices.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); } + + /** + * Returns the lowest price in the stock's history. + * + * @return the lowest price + */ public BigDecimal getLowestPrice() { return prices.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); } + /** + * Returns the difference between the current and previous price. + * + * @return the price change + */ public BigDecimal getLatestPriceChange() { - if (prices.size() < 2) { + final int minSize = 2; + if (prices.size() < minSize) { return BigDecimal.ZERO; } return prices.get(prices.size() - 1) .subtract(prices.get(prices.size() - 2)); } + /** + * Simulates a price update. + */ public void updatePrice() { BigDecimal oldPrice = getSalesPrice(); BigDecimal newPrice = oldPrice.add(new BigDecimal("1")); newPrice = newPrice.max(BigDecimal.ZERO); prices.add(newPrice); } + + /** + * Returns the stock graph component, initializing it if necessary. + * + * @return the stock graph + */ public StockGraph getStockGraph() { + if (stockGraph == null) { + stockGraph = new StockGraph(); + } return stockGraph; } + + /** + * Updates the stock graph if it is visible. + */ public void updateStockGraph() { - stockGraph.update(this); + 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 a2d7d88..19dd290 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 @@ -12,23 +12,28 @@ /** * Utility class for handling file data. - * Includes methods for reading and writing from file, as well as parsing stock data into objects. - * Using BufferedReader/BufferedWriter for optimized handling. + * Includes methods for reading and writing from file. */ -public class StockFileHandler { +public final class StockFileHandler { + /** + * Private constructor to prevent instantiation. + */ + private StockFileHandler() { + } /** * Method for parsing stock data into stock objects. * - * @param path the relative path of the file being parsed (from source root) - * @return list of stock objects parsed from data file - * @throws IOException if file not found + * @param path the path of the file being parsed + * @return list of stock objects + * @throws IOException if file not found or error occurs */ - public static List readFromFile(Path path) throws IOException { + public static List readFromFile(final Path path) throws IOException { List stocks = new ArrayList<>(); - try (BufferedReader bufferedReader = new BufferedReader(new FileReader(path.toFile()))) { + try (BufferedReader bufferedReader = new BufferedReader( + new FileReader(path.toFile()))) { String line; while ((line = bufferedReader.readLine()) != null) { line = line.trim(); @@ -39,9 +44,13 @@ public static List readFromFile(Path path) throws IOException { String[] parts = line.split(","); - String symbol = parts[0]; - String company = parts[1]; - BigDecimal salePrice = new BigDecimal(parts[2].trim()); + final int symbolIndex = 0; + final int companyIndex = 1; + final int priceIndex = 2; + + String symbol = parts[symbolIndex]; + String company = parts[companyIndex]; + BigDecimal salePrice = new BigDecimal(parts[priceIndex].trim()); stocks.add(new Stock(symbol, company, salePrice)); } @@ -52,11 +61,14 @@ public static List readFromFile(Path path) throws IOException { /** * Method for writing stock information to file. * - * @param path the relative path of the file being parsed (from source root) - * @param stocks list of stock objects to be transcribed to data file - * @throws IOException if file not found + * @param path the path of the file to write to + * @param stocks list of stock objects to be transcribed + * @throws IOException if error occurs */ - public static void writeToFile(Path path, List stocks) throws IOException { + public static void writeToFile( + final Path path, + final List stocks + ) throws IOException { try (BufferedWriter writer = Files.newBufferedWriter(path)) { writer.write("# S&P 500 Companies by Market Cap"); @@ -65,9 +77,9 @@ public static void writeToFile(Path path, List stocks) throws IOException for (Stock stock : stocks) { String line = String.format("%s,%s,%s", - stock.getSymbol(), - stock.getCompany(), - stock.getSalesPrice()); + stock.getSymbol(), + stock.getCompany(), + stock.getSalesPrice()); writer.write(line); writer.newLine(); 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 c442fec..5aaffcb 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 @@ -7,17 +7,24 @@ import java.math.BigDecimal; -public class Purchase extends Transaction { - - public Purchase(Share share, int week) { +/** + * Represents a stock purchase transaction. + */ +public final class Purchase extends Transaction { + /** + * Constructs a new Purchase transaction. + * + * @param share the share position being bought + * @param week the simulation week + */ + public Purchase(final Share share, final int week) { super(share, week, new PurchaseCalculator(share)); } @Override - public void commit(Player player) { + public void commit(final Player player) { setCommitted(true); player.withdrawMoney(getCalculator().calculateTotal()); player.getTransactionArchive().add(this); - } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java index fcfe51b..426c5f7 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 @@ -4,14 +4,22 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; -public class Sale extends Transaction { - - public Sale(Share share, int week) { +/** + * Represents a stock sale transaction. + */ +public final class Sale extends Transaction { + /** + * Constructs a new Sale transaction. + * + * @param share the share position being sold + * @param week the simulation week + */ + public Sale(final Share share, final int week) { super(share, week, new SaleCalculator(share)); } @Override - public void commit(Player player) { + public void commit(final Player player) { setCommitted(true); player.addMoney(getCalculator().calculateTotal()); player.getTransactionArchive().add(this); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Transaction.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Transaction.java index 0052939..f3d8c09 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 @@ -4,38 +4,86 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; +/** + * Abstract base class for financial transactions. + */ public abstract class Transaction { - private Share share; - private int week; - private TransactionCalculator calculator; + /** The share position involved in the transaction. */ + private final Share share; + /** The week the transaction occurred. */ + private final int week; + /** The calculator used for this transaction. */ + private final TransactionCalculator calculator; + /** Whether the transaction has been committed. */ private boolean committed = false; - protected Transaction(Share share, int week, TransactionCalculator calculator) { + /** + * Constructs a new Transaction. + * + * @param share the share position + * @param week the simulation week + * @param calculator the cost calculator + */ + protected Transaction( + final Share share, + final int week, + final TransactionCalculator calculator + ) { this.share = share; this.week = week; this.calculator = calculator; this.committed = false; } + /** + * Returns the share position. + * + * @return the share + */ public Share getShare() { return share; } + /** + * Returns the week of the transaction. + * + * @return the week + */ public int getWeek() { return week; } + /** + * Returns the calculator used. + * + * @return the calculator + */ public TransactionCalculator getCalculator() { return calculator; } + /** + * Returns whether the transaction is committed. + * + * @return true if committed + */ public boolean isCommitted() { return committed; } - public void commit(Player player) {} + /** + * Commits the transaction for a player. + * + * @param player the player performing the transaction + */ + public abstract void commit(Player player); - public void setCommitted(boolean committed) { + /** + * Sets the committed status of the transaction. + * + * @param committed the new status + */ + public void setCommitted(final boolean committed) { this.committed = committed; } } 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 455c7ba..250ef1f 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 @@ -5,14 +5,27 @@ import java.util.List; import java.util.Set; -public class TransactionArchive { - private List transactions; - +/** + * Archive for storing and querying past transactions. + */ +public final class TransactionArchive { + /** The list of stored transactions. */ + private final List transactions; + + /** + * Constructs an empty TransactionArchive. + */ public TransactionArchive() { this.transactions = new ArrayList<>(); } - public boolean add(Transaction transaction) { + /** + * Adds a transaction to the archive. + * + * @param transaction the transaction to add + * @return true if added + */ + public boolean add(final Transaction transaction) { if (transaction == null) { return false; } else { @@ -20,11 +33,22 @@ public boolean add(Transaction transaction) { } } + /** + * Checks if the archive is empty. + * + * @return true if empty + */ public boolean isEmpty() { return transactions.isEmpty(); } - public List getTransactions(int week) { + /** + * Returns all transactions that occurred in a specific week. + * + * @param week the simulation week + * @return a list of transactions + */ + public List getTransactions(final int week) { List transactionsThisWeek = new ArrayList<>(); for (Transaction transaction : transactions) { @@ -36,7 +60,13 @@ public List getTransactions(int week) { return transactionsThisWeek; } - public List getPurchases(int week) { + /** + * Returns all purchase transactions that occurred in a specific week. + * + * @param week the simulation week + * @return a list of purchases + */ + public List getPurchases(final int week) { List purchasesThisWeek = new ArrayList<>(); for (Transaction transaction : transactions) { @@ -48,7 +78,13 @@ public List getPurchases(int week) { return purchasesThisWeek; } - public List getSales(int week) { + /** + * Returns all sale transactions that occurred in a specific week. + * + * @param week the simulation week + * @return a list of sales + */ + public List getSales(final int week) { List salesThisWeek = new ArrayList<>(); for (Transaction transaction : transactions) { @@ -60,6 +96,11 @@ public List getSales(int week) { return salesThisWeek; } + /** + * Counts the number of distinct weeks in which transactions occurred. + * + * @return the number of distinct weeks + */ public int countDistinctWeeks() { Set distinctWeeks = new HashSet<>(); 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 9035e21..4ffea33 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 @@ -16,57 +16,87 @@ * A popup for the Bank app. */ public class BankApp extends Popup { - private ListView portfolioList; - private Label netWorthLabel = new Label("Net Worth: "); - private Label unInvestedMoneyLabel = new Label("Cash: "); - private Label investedMoneyLabel = new Label("Stock: "); - private Label growthPercentageLabel = new Label("Growth: "); + private final ListView portfolioList; + private final Label netWorthLabel = new Label("Net Worth: "); + private final Label unInvestedMoneyLabel = new Label("Cash: "); + private final Label investedMoneyLabel = new Label("Stock: "); + private final Label growthPercentageLabel = new Label("Growth: "); /** * Constructs a new Bank popup. */ - public BankApp(){ + public BankApp() { super(400, 300, 120, 120, App.BANK); - portfolioList = new ListView<>(); + this.portfolioList = new ListView<>(); updateContent(); } - private void updateContent(){ + private void updateContent() { GridPane statusPane = new GridPane(); - statusPane.add(netWorthLabel, 0, 0); - statusPane.add(unInvestedMoneyLabel, 0, 1); - statusPane.add(investedMoneyLabel, 1, 1); - statusPane.add(growthPercentageLabel, 1, 0); + final int col0 = 0; + final int col1 = 1; + final int row0 = 0; + final int row1 = 1; + + statusPane.add(netWorthLabel, col0, row0); + statusPane.add(unInvestedMoneyLabel, col0, row1); + statusPane.add(investedMoneyLabel, col1, row1); + statusPane.add(growthPercentageLabel, col1, row0); portfolioList.setCellFactory( - lv -> - new ListCell() { - @Override - protected void updateItem(Share share, boolean empty) { - super.updateItem(share, empty); - if (empty || share == null) { - setGraphic(null); - } else { - GridPane sharePane = new GridPane(); - sharePane.add(new Label(share.getStock().getCompany()), 0, 0); - sharePane.add(new Label("Purchase"), 1, 0); - sharePane.add(new Label(share.getStock().getSymbol()), 0, 1); - sharePane.add(new Label(share.getPurchasePrice().toString()), 1, 1); - sharePane.add(new Label("Quantity"), 0, 2); - sharePane.add(new Label("Sale"), 1, 2); - sharePane.add(new Label(share.getQuantity().toString()), 0, 3); - sharePane.add(new Label(share.getStock().getSalesPrice().toString()), 1, 3); - sharePane.setAlignment(Pos.CENTER_LEFT); - setGraphic(sharePane); - } - } - }); + lv -> + new ListCell() { + @Override + protected void updateItem(final Share share, final boolean empty) { + super.updateItem(share, empty); + if (empty || share == null) { + setGraphic(null); + } else { + GridPane sharePane = new GridPane(); + final int col0 = 0; + final int col1 = 1; + final int row0 = 0; + final int row1 = 1; + final int row2 = 2; + final int row3 = 3; + + sharePane.add(new Label(share.getStock().getCompany()), + col0, row0); + sharePane.add(new Label("Purchase"), col1, row0); + sharePane.add(new Label(share.getStock().getSymbol()), + col0, row1); + sharePane.add(new Label(share.getPurchasePrice().toString()), + col1, row1); + sharePane.add(new Label("Quantity"), col0, row2); + sharePane.add(new Label("Sale"), col1, row2); + sharePane.add(new Label(share.getQuantity().toString()), + col0, row3); + sharePane.add(new Label(share.getStock().getSalesPrice() + .toString()), col1, row3); + sharePane.setAlignment(Pos.CENTER_LEFT); + setGraphic(sharePane); + } + } + }); content.getChildren().setAll(statusPane, portfolioList); } - public void updateStatus(double netWorth, double unInvestedMoney, double investedMoney, double growthPercentage) { + /** + * Updates the financial status displayed in the bank app. + * + * @param netWorth total net worth + * @param unInvestedMoney available cash + * @param investedMoney value of shares + * @param growthPercentage growth since start + */ + public void updateStatus( + final double netWorth, + final double unInvestedMoney, + final double investedMoney, + final double growthPercentage + ) { Platform.runLater(() -> { netWorthLabel.setText("Net Worth: " + netWorth); unInvestedMoneyLabel.setText("Cash: " + unInvestedMoney); 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 37aad5a..680c0f7 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 @@ -3,7 +3,9 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.Model.Stock; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components.Receipt; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components.StockGraph; +import java.math.BigDecimal; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.control.*; @@ -14,13 +16,13 @@ * A popup for the Stock app. */ public class StockApp extends Popup { - private ListView stockList; - private TextField searchField; + private final ListView stockList; + private final TextField searchField; private Label priceLabel; private Stock currentStock; - private Button buyButton; - private Button sellButton; - private TextField quantityField; + private final Spinner quantitySpinner; + private final Button confirmButton; + private final Receipt receipt; /** * Constructs a new Stock popup. @@ -30,32 +32,32 @@ public class StockApp extends Popup { * @param x x-position of the popup * @param y y-position of the popup */ - public StockApp(int width, int height, int x, int y) { + public StockApp(final int width, final int height, final int x, final int y) { super(width, height, x, y, App.STOCK); - searchField = new TextField(); - stockList = new ListView<>(); - buyButton = new Button("Buy"); - sellButton = new Button("Sell"); - quantityField = new TextField("1"); - quantityField.setPromptText("Quantity"); - - // Only allow numeric input - quantityField.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - quantityField.setText(newValue.replaceAll("[^\\d]", "")); - } - }); + this.searchField = new TextField(); + this.stockList = new ListView<>(); + + final int minQuantity = -1000; + final int maxQuantity = 1000; + final int initialQuantity = 0; + this.quantitySpinner = new Spinner<>( + minQuantity, maxQuantity, initialQuantity + ); + + this.confirmButton = new Button("Confirm"); + this.receipt = new Receipt(null, BigDecimal.ZERO); stockList.setCellFactory( lv -> new ListCell() { @Override - protected void updateItem(Stock stock, boolean empty) { + protected void updateItem(final Stock stock, final boolean empty) { super.updateItem(stock, empty); if (empty || stock == null) { setGraphic(null); } else { - HBox row = new HBox(10, new Label(stock.getCompany())); + final int spacing = 10; + HBox row = new HBox(spacing, new Label(stock.getCompany())); row.setOnMouseClicked(event -> { System.out.println(stock.getCompany() + " is pressed!"); openStockPage(stock); @@ -69,7 +71,12 @@ protected void updateItem(Stock stock, boolean empty) { content.getChildren().addAll(searchField, stockList); } - public void openStockPage(Stock stock) { + /** + * Opens the page for a specific stock. + * + * @param stock the stock to view + */ + public void openStockPage(final Stock stock) { currentStock = stock; content.setAlignment(Pos.TOP_LEFT); @@ -81,25 +88,47 @@ public void openStockPage(Stock stock) { priceLabel = new Label(stock.getSalesPrice().toString()); - header.getChildren().addAll(companyLabel, priceLabel, quantityField, buyButton, sellButton); + header.getChildren().addAll(companyLabel, priceLabel, quantitySpinner); stock.getStockGraph().setVisibility(true); - Platform.runLater(() -> stock.updateStockGraph()); + Platform.runLater(stock::updateStockGraph); StockGraph graph = stock.getStockGraph(); - content.getChildren().setAll(searchField, closeButton, header, graph); + + content.getChildren().setAll( + searchField, closeButton, header, graph, confirmButton, receipt + ); } - public void updatePriceLabel(Stock stock) { + /** + * Updates the price label if the specified stock is currently in view. + * + * @param stock the stock whose price updated + */ + public void updatePriceLabel(final Stock stock) { if (priceLabel != null && stock == currentStock) { - Platform.runLater(() -> priceLabel.setText(stock.getSalesPrice().toString())); + Platform.runLater(() -> priceLabel.setText( + stock.getSalesPrice().toString()) + ); } } - public void setCloseButtonAction(Button closeButton, Stock stock) { + /** + * Configures the close button to return to the search page. + * + * @param closeButton the button to configure + * @param stock the stock currently viewed + */ + public void setCloseButtonAction( + final Button closeButton, + final Stock stock + ) { stock.getStockGraph().setVisibility(false); closeButton.setOnAction(e -> openSearchPage()); } + /** + * Returns to the stock search page. + */ public void openSearchPage() { currentStock = null; content.getChildren().setAll(searchField, stockList); @@ -112,19 +141,20 @@ public Stock getCurrentStock() { public TextField getSearchField() { return searchField; } - public Button getBuyButton() { - return buyButton; - } - public Button getSellButton() { - return sellButton; + + public ListView getStockList() { + return stockList; } - public TextField getQuantityField() { - return quantityField; + public Spinner getQuantitySpinner() { + return quantitySpinner; } - public ListView getStockList() { - return stockList; + public Button getConfirmButton() { + return confirmButton; } + public Receipt getReceipt() { + return receipt; + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java index 657e831..3553490 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java @@ -15,18 +15,28 @@ * Handles dragging, layout, and common components. */ public abstract class Popup { - - protected int width; - protected int height; - protected double dx; - protected double dy; - protected App type; - - protected BorderPane root; - protected ScrollPane scrollPane; - protected HBox header; - protected VBox content; - protected Button closeButton; + /** Width of the popup. */ + private final int width; + /** Height of the popup. */ + private final int height; + /** X-offset for dragging. */ + private double dx; + /** Y-offset for dragging. */ + private double dy; + /** The app type of the popup. */ + private final App type; + + /** Root layout node. */ + private final BorderPane root; + /** Scrollable container for content. */ + private final ScrollPane scrollPane; + /** Header container. */ + private final HBox header; + /** Close button. */ + private final Button closeButton; + + /** Content container. */ + protected final VBox content; /** * Constructs a new popup. @@ -35,16 +45,23 @@ public abstract class Popup { * @param height height of the popup * @param x x-position of the popup * @param y y-position of the popup + * @param type the app type */ - protected Popup(int width, int height, int x, int y, App type) { + protected Popup( + final int width, + final int height, + final int x, + final int y, + final App type + ) { this.width = width; this.height = height; this.type = type; - root = new BorderPane(); scrollPane = new ScrollPane(); - scrollPane.setStyle("-fx-hbar-policy: never; -fx-vbar-policy: AS_NEEDED; -fx-background-insets: 0; -fx-padding: 0;"); + scrollPane.setStyle("-fx-hbar-policy: never; -fx-vbar-policy: AS_NEEDED; " + + "-fx-background-insets: 0; -fx-padding: 0;"); header = new HBox(); content = new VBox(); @@ -93,6 +110,8 @@ private void makeDraggable() { /** * Returns the width of the popup. + * + * @return the width */ public int getWidth() { return width; @@ -100,6 +119,8 @@ public int getWidth() { /** * Returns the height of the popup. + * + * @return the height */ public int getHeight() { return height; @@ -107,6 +128,8 @@ public int getHeight() { /** * Returns the x-position of the popup. + * + * @return the x-position */ public double getX() { return root.getLayoutX(); @@ -114,6 +137,8 @@ public double getX() { /** * Returns the y-position of the popup. + * + * @return the y-position */ public double getY() { return root.getLayoutY(); @@ -121,6 +146,8 @@ public double getY() { /** * Returns the root node of the popup. + * + * @return the root */ public BorderPane getRoot() { return root; @@ -128,13 +155,15 @@ public BorderPane getRoot() { /** * Returns the type of the popup. + * + * @return the type */ public App getType() { return type; } /** - * Checks if a popup is out of bounds and moves it back if necessary + * Checks if a popup is out of bounds and moves it back if necessary. */ public void keepInBounds() { if (getRoot().getParent() instanceof Pane parent) { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java new file mode 100644 index 0000000..08578ee --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java @@ -0,0 +1,61 @@ +package edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.Stock; +import java.math.BigDecimal; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +/** + * Component for displaying a transaction receipt. + */ +public final class Receipt extends VBox { + private Stock stock; + private BigDecimal quantity; + + /** + * Constructs a new Receipt. + * + * @param stock the stock + * @param quantity the quantity + */ + public Receipt(final Stock stock, final BigDecimal quantity) { + update(stock, quantity); + } + + /** + * Updates the receipt with new transaction data. + * + * @param stock the stock + * @param quantity the quantity + */ + public void update(final Stock stock, final BigDecimal quantity) { + this.stock = stock; + this.quantity = quantity; + if (stock == null) { + this.getChildren().clear(); + return; + } + GridPane receiptPane = new GridPane(); + + final int col0 = 0; + final int col1 = 1; + final int row0 = 0; + final int row1 = 1; + final int row2 = 2; + final int row3 = 3; + + receiptPane.add(new Label(stock.getCompany()), col0, row0); + receiptPane.add(new Label("Quantity"), col1, row0); + receiptPane.add(new Label(stock.getSymbol()), col0, row1); + receiptPane.add(new Label(quantity.toString()), col1, row1); + receiptPane.add(new Label("Price"), col0, row2); + receiptPane.add(new Label("Amount"), col1, row2); + receiptPane.add(new Label(stock.getSalesPrice().toString()), col0, row3); + receiptPane.add(new Label((stock.getSalesPrice().multiply(quantity)) + .toString()), col1, row3); + receiptPane.setAlignment(Pos.CENTER); + this.getChildren().setAll(new Label("Receipt"), receiptPane); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java index 31f18cb..40b88a8 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java @@ -10,28 +10,31 @@ import java.math.BigDecimal; import java.util.List; -public class StockGraph extends VBox { - +/** + * Component for displaying a stock price history graph. + */ +public final class StockGraph extends VBox { private final LineChart lineChart; - private final NumberAxis xAxis; - private final NumberAxis yAxis; private boolean isVisible = false; private final int stockResolution = 50; - + /** + * Constructs a new StockGraph. + */ public StockGraph() { - xAxis = new NumberAxis(); + NumberAxis xAxis = new NumberAxis(); xAxis.setLabel("Time"); xAxis.setAutoRanging(true); xAxis.setTickLabelsVisible(false); xAxis.setTickMarkVisible(false); - yAxis = new NumberAxis(); + NumberAxis yAxis = new NumberAxis(); yAxis.setLabel("Price"); yAxis.setAutoRanging(true); lineChart = new LineChart<>(xAxis, yAxis); - lineChart.setPrefSize(100, 100); + final int prefSize = 100; + lineChart.setPrefSize(prefSize, prefSize); lineChart.setCreateSymbols(false); lineChart.setLegendVisible(false); lineChart.setAnimated(false); @@ -39,18 +42,27 @@ public StockGraph() { getChildren().add(lineChart); } - public boolean update(Stock stock) { + /** + * Updates the graph with data from the specified stock. + * + * @param stock the stock to display + * @return true if update was scheduled + */ + public boolean update(final Stock stock) { if (stock == null) { return false; } List history = stock.getHistoricalPrices(); - if (history.size() < stockResolution) { + if (history.isEmpty()) { return false; } - List latestHistory = history.subList(Math.max(history.size() - stockResolution, 0), history.size()); + List latestHistory = history.subList( + Math.max(history.size() - stockResolution, 0), + history.size() + ); Platform.runLater(() -> { lineChart.getData().clear(); @@ -58,7 +70,7 @@ public boolean update(Stock stock) { XYChart.Series series = new XYChart.Series<>(); series.setName(stock.getCompany()); - for (int i = 0; i < stockResolution; i++) { + for (int i = 0; i < latestHistory.size(); i++) { series.getData().add(new XYChart.Data<>(i, latestHistory.get(i))); } @@ -71,7 +83,7 @@ public boolean getVisibility() { return isVisible; } - public void setVisibility(boolean visible) { + public void setVisibility(final boolean visible) { isVisible = visible; } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java index 010c120..7a01c33 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java @@ -14,13 +14,22 @@ * View for the desktop. * Displays a 6x4 grid of apps. */ -public class DesktopView { +public final class DesktopView { + /** Controller for the desktop. */ private final DesktopViewController desktopViewController; + /** Root layout node. */ private final BorderPane root; + /** Number of columns in the grid. */ private static final int COLS = 6; + /** Number of rows in the grid. */ private static final int ROWS = 4; - public DesktopView(DesktopViewController desktopViewController) { + /** + * Constructs a new DesktopView. + * + * @param desktopViewController the controller + */ + public DesktopView(final DesktopViewController desktopViewController) { this.root = new BorderPane(); this.desktopViewController = desktopViewController; } @@ -47,16 +56,17 @@ private GridPane createGrid() { grid.setHgap(0); grid.setVgap(0); - // Set constraints to make the grid fit the width and height of the screen + // Set constraints to make the grid fit the width and height + final double maxPercent = 100.0; for (int col = 0; col < COLS; col++) { ColumnConstraints colConstraints = new ColumnConstraints(); - colConstraints.setPercentWidth(100.0 / COLS); + colConstraints.setPercentWidth(maxPercent / COLS); colConstraints.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().add(colConstraints); } for (int row = 0; row < ROWS; row++) { RowConstraints rowConstraints = new RowConstraints(); - rowConstraints.setPercentHeight(100.0 / ROWS); + rowConstraints.setPercentHeight(maxPercent / ROWS); rowConstraints.setVgrow(Priority.ALWAYS); grid.getRowConstraints().add(rowConstraints); } @@ -68,7 +78,9 @@ private GridPane createGrid() { // Add initial buttons to the first row based on the Apps enum if (row == 0 && col < App.values().length) { - cell.getChildren().add(desktopViewController.createAppButton(App.values()[col])); + cell.getChildren().add( + desktopViewController.createAppButton(App.values()[col]) + ); } } } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/Model/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/Model/ExchangeTest.java index 3c3099a..ec9ee0d 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/Model/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/Model/ExchangeTest.java @@ -29,13 +29,28 @@ void stockManagementTest() { @Test void findStocksTest() { List result = exchange.findStocks("Apple Inc."); + assertEquals(1, result.size()); + assertEquals("Apple Inc.", result.get(0).getCompany()); + + // Partial match + result = exchange.findStocks("App"); + assertEquals(1, result.size()); + assertEquals("Apple Inc.", result.get(0).getCompany()); + // Case insensitive match + result = exchange.findStocks("apple"); assertEquals(1, result.size()); - assertNotEquals(0, result.size()); assertEquals("Apple Inc.", result.get(0).getCompany()); - assertNotEquals("", result.get(0).getCompany()); - assertNotEquals("AAPL", result.get(0).getCompany()); - assertNotEquals("100", result.get(0).getCompany()); + + // Symbol match + result = exchange.findStocks("AAPL"); + assertEquals(1, result.size()); + assertEquals("AAPL", result.get(0).getSymbol()); + + // Partial symbol match + result = exchange.findStocks("AA"); + assertEquals(1, result.size()); + assertEquals("AAPL", result.get(0).getSymbol()); } @Test