package exceptions;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import se.kth.iv1350.pos.integration.InventorySystem;

import static org.junit.jupiter.api.Assertions.*;

class ProductIDMissingExceptionTest extends Exception{

    private InventorySystem inventorySystem = InventorySystem.getInventorySystemInstance();

    @AfterEach
    public void cleanup()
    {
        inventorySystem = InventorySystem.getInventorySystemInstance();
    }

    @Test
    void testGetProductIDShouldThrowException()
    {
        int falseProductID = 12;

        RuntimeException productIDMissingException = assertThrows(RuntimeException.class, () -> inventorySystem.getProductDTOFromDB(falseProductID));

        assertEquals(productIDMissingException.getMessage(),"Product with ID " + falseProductID + " not found in external inventory system.", "The exception message should match the expected message when a product ID is missing.");
    }


    @Test
    void testGetItemIDShouldNotThrowException()
    {
        int realProductID = 121;

        assertDoesNotThrow(() -> inventorySystem.getProductDTOFromDB(realProductID));
    }
}package exceptions;
import org.junit.jupiter.api.*;
import se.kth.iv1350.pos.integration.InventorySystem;

import static org.junit.jupiter.api.Assertions.*;

public class InventoryDBExceptionTest {
    private InventorySystem inventorySystem = InventorySystem.getInventorySystemInstance();

    @AfterEach
    void cleanup(){
        inventorySystem = InventorySystem.getInventorySystemInstance();
    }

    @Test
    void testInventoryDataShouldThrowServerDownException(){
        int productIdDown = 404;
        RuntimeException serverDownProductId = assertThrows(RuntimeException.class, () -> inventorySystem.getProductDTOFromDB(productIdDown));

        assertEquals(serverDownProductId.getMessage(),"The inventory system server is currently unavailable.", "The exception message should match the expected message when the inventory server is down.");
    }

    @Test
    void testInventoryDataShouldNotThrowServerDownException(){
        int productIDNotDown = 44;

        RuntimeException serverDownItemID = assertThrows(RuntimeException.class, () -> inventorySystem.getProductDTOFromDB(productIDNotDown));

        assertNotEquals(serverDownItemID.getMessage(),"The inventory system server is currently unavailable.");
    }

}
package model;

import org.junit.Before;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import se.kth.iv1350.pos.model.Product;
import se.kth.iv1350.pos.model.Sale;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class SaleTest {

    private Sale sale = new Sale();

    @AfterEach
    public void cleanUp() {
        sale = new Sale();
    }

    @Test
    void isProductInSaleFail() {
        Product productInSale = new Product(132,2,"socks",50,8);
        sale.addProductToSale(productInSale);
        Product productNotInSale = new Product(111,2,"apple", 50,8);
        boolean productIsInSale = sale.isProductInSale(productNotInSale);
        assertEquals(productIsInSale,false, "The product should not be in the sale if it has a different product ID.");
    }

    @Test
    void isProductInSaleSuccess() {
        Product productInSale = new Product(132,2,"socks",50,8);
        sale.addProductToSale(productInSale);
        Product productNotInSale = new Product(132,1,"socks",50,8);
        boolean productIsInSale = sale.isProductInSale(productNotInSale);
        assertEquals(productIsInSale,true);
    }

    @Test
    void addNewProductToSaleSuccess(){
        Product productInSale = new Product(132,2,"socks",50,8);
        sale.addProductToSale(productInSale);

        Product nextProduct = new Product(sale.getCurrentProducts().get(0));

        assertEquals(nextProduct.getProductId(),productInSale.getProductId());
    }
}package model;

import org.junit.jupiter.api.Test;
import se.kth.iv1350.pos.model.*;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

public class PaymentTest {
    private Payment payment = new Payment();

    @Test
    void increaseAmountToPaySuccess(){
        payment.addToPayAmount(10);
        double amountTest = payment.getPayAmount();
        assertEquals(10, amountTest, "The pay amount should be increased by 10 after adding 10.");
    }

    @Test
    void increaseAmountToPayFail(){
        payment.addToPayAmount(10);
        double amountTest = payment.getPayAmount();
        assertNotEquals(9, amountTest, "The pay amount should not be 9 after adding 10.");
    }

    @Test
    void payAmountFail(){
        payment.addToPayAmount(10);
        payment.calculateChange(9);
        double changeTest = payment.getChange();
        assertNotEquals(2, changeTest);
    }

    @Test
    void payAmountSuccess(){
        payment.addToPayAmount(10);
        payment.calculateChange(9);
        double changeTest = payment.getChange();
        assertEquals(1, changeTest);
    }
}
package se.kth.iv1350.pos.integration;

/**
 * This class represents a receipt printer used for printing receipts during a sale.
 */
public class ReceiptPrinter {
    /**
     * Creates a new instance of the ReceiptPrinter class.
     */
    public ReceiptPrinter(){
    }
}
package se.kth.iv1350.pos.integration;

import se.kth.iv1350.pos.model.Product;
import se.kth.iv1350.pos.model.ProductDTO;
import se.kth.iv1350.pos.model.SaleDTO;
import se.kth.iv1350.pos.exceptions.*;

/**

 Represents an inventory system that is used for fetching products.
 */
public class InventorySystem {
    private Product[] productsInDB;
    private static final int SERVER_DOWN_PRODUCT_ID = 404;
    private static final InventorySystem instance = new InventorySystem();

    private InventorySystem() {
        productsInDB = new Product[] {
                new Product(121, 3, "Eggs", 25, 30),
                new Product(123, 3, "Lemon",10, 3.33)
        };
    }

    /**

     Updates the quantity of products in the inventory system based on the completed sale.
     @param processedSale The completed sale to update the product quantities for.
     */
    public void updateQuantityFromSale(SaleDTO processedSale) {
    }
    /**

     Retrieves the {@link ProductDTO} from the inventory system based on the product ID.

     @param productID The ID of the product to be found in the inventory system.

     @return A {@link ProductDTO} representing the found product, or null if not found.

     @throws ProductIDMissingException If the given product ID was not found in the inventory system.

     @throws InventoryDBException If the inventory system is unavailable.
     */
    public ProductDTO getProductDTOFromDB(int productID) throws ProductIDMissingException, InventoryDBException {
        if (productID == SERVER_DOWN_PRODUCT_ID) {
            throw new InventoryDBException();
        }

        Product productToReturn = null;

        for (Product product : productsInDB) {
            if (product.getProductId() == productID) {
                productToReturn = product;
            }
        }

        if (productToReturn == null) {
            throw new ProductIDMissingException(productID);
        }

        return new ProductDTO(productToReturn);
    }

    /**

     Retrieves the singleton instance of the InventorySystem class.
     @return The singleton instance of the InventorySystem.
     */
    public static InventorySystem getInventorySystemInstance() {
        return instance;
    }
}package se.kth.iv1350.pos.integration;


import se.kth.iv1350.pos.model.SaleDTO;

/**
 * The AccountingSystem class provides methods to update the accounting system with sales information.
 */

public class AccountingSystem {
    private static final AccountingSystem accountingSystemInstance = new AccountingSystem();

    /**
     * Creates a new instance of AccountingSystem.
     */
    private AccountingSystem(){
    }

    public static AccountingSystem getAccountingSystemInstance() {
        return accountingSystemInstance;
    }

    /**
     * Updates the accounting system with the sale information.
     *
     * @param saleDTOLog The {@link SaleDTO} object representing the sale to update the accounting system with.
     */
    public void updateAccountingSystem(SaleDTO saleDTOLog){
    }
}package se.kth.iv1350.pos.integration;

import se.kth.iv1350.pos.model.PaymentObserver;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalTime;

/**
 * A class that outputs total revenue to a file and implements the {@link PaymentObserver} interface.
 */
public class TotalRevenueFileOutput implements PaymentObserver {
    private double totalRevenue;
    private PrintWriter logStream;

    /**
     * Creates a new file in the reports folder to write the revenue log.
     */
    public TotalRevenueFileOutput() {
        try {
            logStream = new PrintWriter(new FileWriter("revenue.txt"), true);
        } catch (IOException ioException) {
            System.out.println("Could not create revenue log file");
            ioException.printStackTrace();
        }
    }

    /**
     * Writes the updated total revenue to the revenue log file.
     *
     * @param newPayment the new payment that increases the total revenue
     */
    @Override
    public void newRevenue(double newPayment) {
        totalRevenue += newPayment;
        logStream.println("Time: " + LocalTime.now() + " | Total Revenue: " + totalRevenue);
    }
}package se.kth.iv1350.pos.integration;

import java.util.*;
/**
 * The DiscountSystem class provides discount information based on customer IDs.
 */
public class DiscountSystem {

    private Map<String, Integer> discountMap;

    /**
     * Creates a new instance of DiscountSystem and initializes the discount map.
     */
    public DiscountSystem() {
        discountMap = new HashMap<>();
        discountMap.put("123", 10);
        discountMap.put("456", 15);
        discountMap.put("789", 25);
    }

    /**
     * Gets the discount percentage for a customer based on their customer ID.
     *
     * @param customerId The ID of the customer to get the discount for.
     * @return The discount percentage as an integer value. If no discount is found for the customer, 0 is returned.
     */
    public int getDiscount(String customerId) {
        return discountMap.getOrDefault(customerId, 0);
    }
}package se.kth.iv1350.pos.integration;

/**
 * A logger for developers that writes to the console.
 * When exceptions are thrown, the logger is written to.
 */

public class DeveloperLog {

    /**
     * Creates a new instance of the developer logger.
     */
    public DeveloperLog() {}

    /**
     * Writes the exception message to the developer log, which in this case is the console.
     *
     * @param exceptionMessage the message from the exception to be logged
     */
    public void logException(String exceptionMessage) {
        System.out.println("Developer log: " + exceptionMessage + "\n");
    }
}package se.kth.iv1350.pos.exceptions;

/**
 * Exception thrown when an item with a specific ID is not found.
 */
public class ProductIDMissingException extends RuntimeException {
    private int productIDMissing;

    /**
     * Constructs a new instance of the exception with the specified product ID.
     *
     * @param productIDMissing the ID of the missing product
     */
    public ProductIDMissingException(int productIDMissing) {
        super("Product with ID " + productIDMissing + " not found in external inventory system.");
        this.productIDMissing = productIDMissing;
    }
    /**
     * Returns the ID of the missing product.
     *
     * @return the ID as an integer
     */
    public int getMissingProductID() {
        return productIDMissing;
    }
}package se.kth.iv1350.pos.exceptions;
/**
 * Exception thrown when the inventory database is not available.
 */
public class InventoryDBException extends RuntimeException{
    /**
     * Creates a new instance of the exception with a default message.
     */
    public InventoryDBException() {
        super("The inventory system server is currently unavailable.");
    }
}package se.kth.iv1350.pos.controller;

import se.kth.iv1350.pos.exceptions.*;
import se.kth.iv1350.pos.integration.*;
import se.kth.iv1350.pos.model.*;

import java.math.BigDecimal;
import java.util.*;

/**
 * The Controller class is responsible for controlling the POS system flow and interacting with external systems.
 */
public class Controller {
    private Sale saleCont;
    private AccountingSystem accountingSCont;
    private ReceiptPrinter printerCont;
    private InventorySystem inventorySCont;
    private DiscountSystem discountSCont;

    private List<PaymentObserver> paymentObserverList = new ArrayList<>();
    private TotalRevenueFileOutput totalRevenueFileOutput = new TotalRevenueFileOutput();
    private DeveloperLog developerLog = new DeveloperLog();

    /**
     * Creates a new instance of Controller.
     *
     * @param accountingS The accounting system.
     * @param inventoryS The inventory system.
     * @param receiptPrinter The receipt printer.
     */
    public Controller(AccountingSystem accountingS, InventorySystem inventoryS, ReceiptPrinter receiptPrinter){
        accountingSCont = AccountingSystem.getAccountingSystemInstance();
        inventorySCont = inventoryS;
        printerCont = receiptPrinter;
        discountSCont = new DiscountSystem();
    }

    /**
     * Adds a new observer for the CustomerPayment class to the controller.
     *
     * @param observer the observer to be added
     */
    public void addPaymentObserver(PaymentObserver observer) {
        paymentObserverList.add(observer);
    }

    /**
     * Adds an item found in the InventorySystem connected by the itemID to the Sale.
     *
     * @param productId the productId to be searched for
     * @throws OperationFailedException thrown if there are server issues or if there is no match for productID in the inventory system
     */
    public void scanProduct(int productId) throws OperationFailedException {
        ProductDTO productDTOFromDB = null;

        try {
            productDTOFromDB = inventorySCont.getProductDTOFromDB(productId);
        } catch (ProductIDMissingException productIDMissing) {
            throw new OperationFailedException("Product ID was not found in the inventory system.", productIDMissing);
        } catch (InventoryDBException inventoryDBDown) {
            developerLog.logException(inventoryDBDown.getMessage());
            throw new OperationFailedException("Inventory system server is down.", inventoryDBDown);
        }

        Product productFromDB = new Product(productDTOFromDB);
        productFromDB.increaseQuantity(1);

        saleCont.addProductToSale(productFromDB);
    }

    /**
     * Start a new <code>Sale</code>.
     */

    public void initializeNewSale() {
        saleCont = new Sale();
        saleCont.getPayment().addPaymentObservers(paymentObserverList);
        saleCont.getPayment().setTotalRevenue(totalRevenueFileOutput);
    }

    /**
     * A method to end the <code>Sale</code> and update the external systems with the <code>Sale</code> details.
     */
    public void finalizeSaleAndUpdateSystems(){
        SaleDTO saleDTO = new SaleDTO(saleCont);
        inventorySCont.updateQuantityFromSale(saleDTO);
        AccountingSystem.getAccountingSystemInstance().updateAccountingSystem(saleDTO);
    }

    /**
     * Calculates the change for the customer's payment and returns it.
     *
     * @param amountPaid The amount paid by the customer.
     * @return The change to give back to the customer.
     */
    public double processPaymentAndReturnChange(double amountPaid){
        saleCont.getPayment().calculateChange(amountPaid);
        return saleCont.getPayment().getChange();
    }

    /**
     * Get <code>SaleDTO</code> for current <code>Sale</code> linked to <code>Controller</code>.
     *
     * @return The <code>SaleDTO</code> object representing the current sale.
     */

    public SaleDTO getSaleDTO(){
        SaleDTO saleDTO = new SaleDTO(saleCont);
        return saleDTO;
    }

    // A method that creates a new sale and applies a discount to it and handles any exceptions that may occur
    public void createSaleAndApplyDiscount() {

        Sale sale = new Sale();
        sale.setDiscounter(new QuantityDiscounter(3));
        BigDecimal amount = sale.getTotalAmount();
        BigDecimal discountedPrice = sale.applyDiscount(amount);
        System.out.println("The discounted price is: " + discountedPrice);
    }
    public void setDiscounter(Discounter discounter) {
        saleCont.setDiscounter(discounter);
    }
}package se.kth.iv1350.pos.controller;
/**
 * Exception thrown when an operation fails.
 */
public class OperationFailedException extends Exception {

    /**
     * Creates a new instance of the exception with a message and a cause.
     *
     * @param message the message for the exception
     * @param cause the cause of the exception
     */
    public OperationFailedException(String message, Exception cause){
        super(message, cause);
    }
}package se.kth.iv1350.pos.model;

import java.time.LocalTime;

/**
 * This class represents a receipt and the information it contains.
 */
public class Receipt {
    private double saleTotal;
    private double change;
    private double totalVat;
    private String[] name = new String[0];
    private double[] price = new double[0];
    private double[] vat = new double[0];
    private int[] quantity = new int[0];
    private LocalTime receiptCreationTime;
    private int receiptRowPos = 0;

    /**
     * Creates a new instance of the Receipt class with the current time of creation.
     */

    public Receipt(){
        this.receiptCreationTime = LocalTime.now();
    }
    public int[] getQuantity(){
        return quantity;
    }
    public void setChange(double changeFromSale){
        change = changeFromSale;
    }
    public double getChange(){
        return change;
    }
    public String getProductNameBasedOnIndex(int indexInName){
        return name[indexInName];
    }
    public double[] getPrice() {
        return price;
    }
    public String[] getNameList(){
        return name;
    }
    public double[] getVatList(){
        return vat;
    }
    public double getTotalVat(){
        return totalVat;
    }
    public double getSaleTotal() {
        return saleTotal;
    }
    public LocalTime getReceiptCreationTime(){
        return receiptCreationTime;
    }
    /**
     * Adds a product to the receipt.
     *
     * @param addProductToReceipt the product to add to the receipt.
     */
    public void addRow(Product addProductToReceipt){
    }
}package se.kth.iv1350.pos.model;


/**
 * A Data Transfer Object (DTO) for a product in the sale.
 */

public class ProductDTO {
    private int productID;
    private double productVat;
    private int productQuantity;
    private double productPrice;
    private String productName;


    /**
     * Creates a new instance of the ProductDTO.
     *
     * @param productID       the identification number of the product.
     * @param productPrice    the price of the product.
     * @param productVat      the VAT of the product.
     * @param productQuantity the quantity of the product.
     * @param productName     the name of the product.
     */

    public ProductDTO(int productID, double productPrice, double productVat, int productQuantity, String productName){
        this.productID = productID;
        this.productPrice = productPrice;
        this.productVat = productVat;
        this.productQuantity = productQuantity;
        this.productName = productName;
    }

    /**
     * Creates a new instance of the ProductDTO based on a Product object.
     *
     * @param product the Product object to create the DTO from.
     */

    public ProductDTO(Product product){
        this.productName = product.getProductName();
        this.productPrice = product.getProductPrice();
        this.productID = product.getProductId();
        this.productQuantity = product.getProductQuantity();
        this.productVat = product.getVat();
    }
    public int getProductID(){
        return productID;
    }

    public double getProductPrice(){
        return productPrice;
    }
    public double getProductVat(){
        return productVat;
    }

    public String getProductName(){
        return productName;
    }

    public int getProductQuantity(){
        return productQuantity;
    }
}package se.kth.iv1350.pos.model;

import java.math.BigDecimal;

// Another concrete strategy class that implements a discount algorithm based on the customer id
public class CustomerDiscounter implements Discounter {
    @Override
    public BigDecimal applyDiscount(BigDecimal amount) {
        // Fetch the discount rate from the database based on the customer id
        BigDecimal discountRate = getDiscountRateFromDatabase();
        // Apply the discount rate to the amount
        return amount.multiply(discountRate);
    }

    // A method that queries the database for the discount rate based on the customer id
    private BigDecimal getDiscountRateFromDatabase() {
        // Connect to the database and execute a query
        // For example, you can use JDBC or JPA to access the database
        // Return the discount rate as a BigDecimal
        // For example, if the customer id is 1234, return 0.9 as the discount rate (10% off)
        return null;
    }
}package se.kth.iv1350.pos.model;

public class Product {
    private int productId;
    private int productQuantity = 0;
    private String productName;
    private double vat;
    private double productPrice;

    /**
     * Creates a new instance of Product based on a ProductDTO.
     *
     * @param productDTO the product data transfer object.
     */

    public Product(ProductDTO productDTO){
        this.productName = productDTO.getProductName();
        this.productId = productDTO.getProductID();
        this.productPrice = productDTO.getProductPrice();
        this.vat = productDTO.getProductVat();
    }

    /**
     * Creates a new instance of Product based on another Product.
     *
     * @param product the product to create a new instance from.
     */

    public Product(Product product){
        this.productId = product.productId;
        this.productPrice = product.productPrice;
        this.productName = product.productName;
        this.vat = product.vat;
    }

    /**
     * Creates a new instance of Product with the specified parameters.
     *
     * @param productId       the ID of the product.
     * @param productQuantity the quantity of the product.
     * @param productName     the name of the product.
     * @param productPrice    the price of the product.
     * @param vat             the VAT of the product.
     */

    public Product(int productId, int productQuantity, String productName, double productPrice, double vat){
        this.productId = productId;
        this.vat = vat;
        this.productQuantity = productQuantity;
        this.productName = productName;
        this.productPrice = productPrice;
    }

    public double getProductPrice(){
        return productPrice;
    }
    public int getProductId(){
        return productId;
    }
    public double getVat(){
        return vat;
    }
    public int getProductQuantity(){
        return productQuantity;
    }
    public String getProductName(){
        return productName;
    }

    /**
     * Increases the quantity of the product
     * @param moreQuantity The amount to increase the quantity by
     */
    public void increaseQuantity (int moreQuantity){
        this.productQuantity += moreQuantity;
    }

}package se.kth.iv1350.pos.model;
import java.time.LocalTime;

/**
 * A Data Transfer Object (DTO) for the receipt of the sale.
 */
public class ReceiptDTO {
    private double saleTotal;
    private double change;
    private double totalVat;
    private String[] name;
    private double[] price;
    private double[] vat;
    private int[] quantity;
    private LocalTime date;

    /**
     * Creates a new instance of the <code>ReceiptDTO</code> class using a <code>Receipt</code> object.
     *
     * @param receipt the receipt to create a DTO from.
     */

    public ReceiptDTO(Receipt receipt){
        saleTotal = receipt.getSaleTotal();
        change = receipt.getChange();
        totalVat = receipt.getTotalVat();
        name = receipt.getNameList();
        price = receipt.getPrice();
        vat = receipt.getVatList();
        quantity = receipt.getQuantity();
        date = receipt.getReceiptCreationTime();
    }
}package se.kth.iv1350.pos.model;

import java.math.BigDecimal;
import java.sql.SQLException;
// A concrete strategy class that implements a discount algorithm based on the number of items bought
public class QuantityDiscounter implements Discounter {
    // Add a field to store the number of items bought
    private int quantity;

    // Add a constructor that takes the number of items bought as a parameter
    public QuantityDiscounter(int quantity) {
        this.quantity = quantity;
    }

    @Override
    public BigDecimal applyDiscount(BigDecimal amount) {
        try {
            // Fetch the discount rate from the database based on the number of items bought
            // Pass the quantity field to the getDiscountRateFromDatabase method
            BigDecimal discountRate = getDiscountRateFromDatabase(quantity);
            // Check if the discount rate is null before multiplying it with the amount
            if (discountRate != null) {
                // Apply the discount rate to the amount
                return amount.multiply(discountRate);
            } else {
                // Return the original amount without any discount
                return amount;
            }
        } catch (DatabaseFailureException e) {
            // Wrap the checked exception in a runtime exception
            throw new RuntimeException(e);
        }
    }

    // A method that queries the database for the discount rate based on the number of items bought and throws an exception if there is a database failure
    private BigDecimal getDiscountRateFromDatabase(int quantity) throws DatabaseFailureException {
// Connect to the database and execute a query
// For example, you can use JDBC or JPA to access the database
// Return the discount rate as a BigDecimal
// For example, if the number of items bought is 3 or more, return 0.9 as the discount rate (10% off)
// If there is no discount for the given quantity, return null
        switch (quantity) {
            case 1:
                return BigDecimal.valueOf(0.95); // 5% off for one item
            case 2:
                return BigDecimal.valueOf(0.9); // 10% off for two items
            case 3:
                return BigDecimal.valueOf(0.85); // 15% off for three items
            default:
                return null; // no discount for other quantities
        }
    }
}package se.kth.iv1350.pos.model;
import se.kth.iv1350.pos.integration.TotalRevenueFileOutput;

import java.util.*;

/**
 * This class represents a payment made by a customer.
 */

public class Payment {
    private double payAmount;
    private double change;
    private double paidAmount;
    private List<PaymentObserver>
            paymentObservers = new ArrayList<>();
    private TotalRevenueFileOutput totalRevenueFileOutput;

    public static final double FIRST_PAYMENT = 250;
    public static final double SECOND_PAYMENT = 300;
    /**
     * Creates a new instance of Payment with payAmount and change initialized to 0.
     */
    public Payment(){
        this.payAmount = 0;
        this.change = 0;
    }

    /**
     * Increases the total amount paid by the customer by the specified amount.
     *
     * @param addPayAmount the amount to add to the total amount paid.
     */

    public void addToPayAmount(double addPayAmount){
        this.payAmount += addPayAmount;
    }

    /**
     * Returns the total amount paid by the customer.
     *
     * @return the total amount paid by the customer.
     */

    public double getPayAmount(){
        return payAmount;
    }

    /**
     * Calculates the change to give back to the customer based on the amount received.
     *
     * @param amountReceived the amount received from the customer.
     */

    public void calculateChange(double amountReceived){
        change = amountReceived - payAmount;
        paidAmount = amountReceived;
        notifyPaymentObservers();
        updateRecordedPaymentsLog();
    }

    /**
     * Sets a class that can output to a file for this class.
     *
     * @param totalRevenueFileOutput the class that outputs to a file.
     */

    public void setTotalRevenue(TotalRevenueFileOutput totalRevenueFileOutput) {
        this.totalRevenueFileOutput = totalRevenueFileOutput;
    }

    /**
     * Registers observers that will be notified when Payment changes state.
     *
     * @param paymentObserver the observer that will be notified of the change.
     */
    public void addPaymentObservers(List<PaymentObserver> paymentObserver) {
        paymentObservers.addAll(paymentObserver);
    }

    /**
     * Notifies observers for the Payment class with the amount for sale.
     */
    private void notifyPaymentObservers(){
        for (PaymentObserver paymentObserver : paymentObservers) {
            paymentObserver.newRevenue(paidAmount - change);
        }
    }

    /**
     * Updates the amount for sale that updates the log.
     */
    private void updateRecordedPaymentsLog() {
        totalRevenueFileOutput.newRevenue(paidAmount - change);
    }

    /**
     * Returns the amount of change to give back to the customer.
     *
     * @return the amount of change to give back to the customer.
     */
    public double getChange(){
        return change;
    }

    /**
     * Returns the amount of money paid by the customer.
     *
     * @return the amount of money paid by the customer.
     */

    public double getPaidAmount() {
        return paidAmount;
    }
}package se.kth.iv1350.pos.model;

/**
 * This class represents a data transfer object (DTO) for a payment.
 */
public class PaymentDTO {
    private double totalAmount;
    private double change;

    /**
     * Creates a new instance of the <code>PaymentDTO</code> class based on the information in a {@link Payment} object.
     *
     * @param paymentToDTO the payment object to convert to a DTO.
     */

    public PaymentDTO(Payment paymentToDTO){
        change = paymentToDTO.getChange();
        totalAmount = paymentToDTO.getPayAmount();
    }
    public double getTotalAmount() {
        return totalAmount;
    }
}package se.kth.iv1350.pos.model;

/**
 * Represents an interface for an observer that observes customer payments.
 */
public interface PaymentObserver {

    /**
     * Called by the observed object when a new payment is made.
     *
     * @param newPayment the amount of the new payment.
     */
    void newRevenue(double newPayment);
}package se.kth.iv1350.pos.model;

import java.time.LocalTime;
import java.util.ArrayList;

/**
 * The SaleDTO class represents a data transfer object for a sale made in the point of sale system.
 * It contains information about the products included in the sale, any discounts applied, payment information,
 * time of sale, and the receipt for the sale.
 */

public class SaleDTO {
    /**
     * ArrayList that stores the current list of products in the sale.
     */
    private ArrayList<ProductDTO> currentProducts = new ArrayList<>();

    /**
     * The payment information related to the sale.
     */
    private PaymentDTO paymentDTO;
    /**
     * The time when the sale was initiated.
     */
    private LocalTime timeOfSale;
    /**
     * The receipt information related to the sale.
     */
    private ReceiptDTO receiptDTO;

    /**
     * Constructs a new instance of SaleDTO based on the specified Sale object.
     *
     * @param saleToDTO the Sale object to create the SaleDTO from.
     */
    public SaleDTO(Sale saleToDTO){
        this.timeOfSale = saleToDTO.getTimeOfSale();
        for(Product product : saleToDTO.getCurrentProducts()){
            this.currentProducts.add(new ProductDTO(product));
        }

        this.receiptDTO = new ReceiptDTO(saleToDTO.getSalesReceipt());
        this.paymentDTO = new PaymentDTO(saleToDTO.getPayment());
    }
    public ArrayList<ProductDTO> getCurrentProducts() {
        return currentProducts;
    }
    public PaymentDTO getPaymentDTO() {
        return paymentDTO;
    }

    public LocalTime getTimeOfSale() {
        return timeOfSale;
    }

    public ReceiptDTO getReceiptDTO() {
        return receiptDTO;
    }
}package se.kth.iv1350.pos.model;

import java.math.BigDecimal;

// The strategy interface that declares a common method for all discount algorithms
public interface Discounter {
    // Declare that the method may throw a DatabaseFailureException
    BigDecimal applyDiscount(BigDecimal amount) throws DatabaseFailureException;
}package se.kth.iv1350.pos.model;

import java.math.BigDecimal;
import java.time.LocalTime;
import java.util.ArrayList;

/**
 * Represents a sale in the point of sale system.
 */
public class Sale {
    private LocalTime timeOfSale;
    private Payment payment;
    private Receipt salesReceipt;
    private String customerId;
    private Discounter discounter;
    private BigDecimal totalAmount;

    private ArrayList<Product> currentProducts = new ArrayList<>();

    /**
     * Creates a new instance of the <code>Sale</code> class.
     */
    public Sale(){
        this.timeOfSale = LocalTime.now();
        salesReceipt = new Receipt();
        payment = new Payment();
        this.customerId = customerId;
    }
    public LocalTime getTimeOfSale(){
        return timeOfSale;
    }
    public Payment getPayment(){
        return payment;
    }
    public ArrayList<Product> getCurrentProducts() {
        return currentProducts;
    }
    public Receipt getSalesReceipt(){
        return salesReceipt;
    }

    /**
     * Adds a product to the current sale.
     *
     * @param productToAddToSale the <code>Product</code> to be added to the sale.
     */

    public void addProductToSale(Product productToAddToSale){
        if(isProductInSale(productToAddToSale)){
            for (Product product : currentProducts){
                if(product.getProductId() == productToAddToSale.getProductId()){
                    product.increaseQuantity(1);
                }
            }
        }

        else{
            currentProducts.add(productToAddToSale);
        }
        payment.addToPayAmount(productToAddToSale.getProductPrice());
        salesReceipt.addRow(productToAddToSale);

    }

    /**
     * Checks if a product is already in the current sale.
     *
     * @param product the <code>Product</code> to check.
     * @return <code>true</code> if the product is already in the sale, <code>false</code> otherwise.
     */
    public boolean isProductInSale(Product product) {
        boolean isProductInSale = false;
        for(Product productSearch : currentProducts){
            if(productSearch.getProductId() == product.getProductId())
                isProductInSale = true;
        }
        return isProductInSale;
    }
    // A method that sets the strategy object
    public void setDiscounter(Discounter discounter) {
        this.discounter = discounter;
    }

    // A method that gets the total amount of the sale
    public BigDecimal getTotalAmount() {
        return totalAmount;
    }

    // A method that uses the strategy object to apply the discount and handles any exceptions that may occur
    public BigDecimal applyDiscount(BigDecimal amount) {
        try {
            // Use the discounter object to apply the discount
            // Convert the double value to a BigDecimal value
            // For example, you can use the BigDecimal.valueOf method
            BigDecimal payAmount = BigDecimal.valueOf(payment.getPayAmount());
            return discounter.applyDiscount(payAmount);
        } catch (DatabaseFailureException e) {
            // Handle the exception here
            // For example, you can print or log an error message for developers and users
            System.out.println("Developer log: " + e.getMessage());
            System.out.println("User message: There was an error when fetching discounts from database.");
            // You can also return a default value or do something else
            // For example, you can return the original amount without any discount
            return amount;
        }
    }
}package se.kth.iv1350.pos.model;

// A checked exception class that indicates a database failure
public class DatabaseFailureException extends Exception {
    public DatabaseFailureException(String message) {
        super(message);
    }
}
package se.kth.iv1350.pos.view;

import se.kth.iv1350.pos.model.PaymentObserver;

/**
 * Represents a view that displays the total revenue.
 */
public class TotalRevenueObserver implements PaymentObserver {
    private double totalRevenue;

    /**
     * The TotalRevenueObserver class represents a view that displays the total revenue.
     */
    public TotalRevenueObserver() {
        totalRevenue = 0;
    }

    /**
     * Updates the total revenue and prints the current state.
     *
     * @param newPayment the amount of payment to add to the total revenue
     */
    @Override
    public void newRevenue(double newPayment) {
        totalRevenue += newPayment;
        printCurrentState();
    }

    /**
     * Prints the current state of the total revenue.
     */
    private void printCurrentState() {
        System.out.println("\n---------------------");
        System.out.println("Total revenue: " + totalRevenue);
        System.out.println("---------------------\n");
    }
}package se.kth.iv1350.pos.view;

import se.kth.iv1350.pos.controller.Controller;
import se.kth.iv1350.pos.model.Payment;
import se.kth.iv1350.pos.model.ProductDTO;
import se.kth.iv1350.pos.model.SaleDTO;

/**
 * The View class represents the cashier's view.
 */

public class View {

    private Controller cont;
    int firstProductId;
    int secondProductId;

    /**
     * Creates a new instance of the View class.
     *
     * @param cont the controller instance to be associated with this view
     */

    public View(Controller cont) {
        this.cont = cont;
        cont.addPaymentObserver(new TotalRevenueObserver());
        secondProductId = 404;
        firstProductId = 101;
    }
    private void getProductById(int productId) {
        try {
            cont.scanProduct(productId);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    /**
     * Simulates a sale process with three purchased products,
     * where two share the same product ID and payment,
     * and prints to the terminal.
     */
    public void simulateSaleProcess() {

        cont.initializeNewSale();
        getProductById(firstProductId);
        getProductById(secondProductId);
        getProductById(firstProductId);
        cont.finalizeSaleAndUpdateSystems();
        sendProductInfoToDisplay();
        cont.createSaleAndApplyDiscount();
        cont.processPaymentAndReturnChange(Payment.FIRST_PAYMENT);

        cont.initializeNewSale();
        getProductById(firstProductId);
        getProductById(firstProductId);
        getProductById(121);
        cont.finalizeSaleAndUpdateSystems();
        sendProductInfoToDisplay();
        cont.createSaleAndApplyDiscount();
        cont.processPaymentAndReturnChange(Payment.SECOND_PAYMENT);
    }

    private void sendProductInfoToDisplay() {
        showProductsInSale(cont.getSaleDTO());
        showPriceOfProductsInSale(cont.getSaleDTO());
        showQuantityOfProductsInSale(cont.getSaleDTO());
        showProductPrice(cont.getSaleDTO());
    }

    /**
     * Displays the products in the sale.
     *
     * @param saleDTO the SaleDTO object containing the products in the sale
     */
    public void showProductsInSale(SaleDTO saleDTO) {
        System.out.println("Products in sale:");

        for (ProductDTO product : saleDTO.getCurrentProducts())
            System.out.println(product.getProductName());
    }

    /**
     * Displays the price of the products in the sale.
     *
     * @param saleDTO the SaleDTO object containing the products in the sale
     */
    public void showPriceOfProductsInSale(SaleDTO saleDTO) {
        System.out.println("Price of products in sale: ");
        for (ProductDTO productDTO : saleDTO.getCurrentProducts()) {
            System.out.println(productDTO.getProductPrice());
        }
    }

    /**
     * Displays the quantity of the products in the sale.
     *
     * @param saleDTO the SaleDTO object containing the products in the sale
     */
    public void showQuantityOfProductsInSale(SaleDTO saleDTO) {
        System.out.println("Quantity of products in sale: ");
        for (ProductDTO productDTO : saleDTO.getCurrentProducts()) {
            System.out.println(productDTO.getProductQuantity());
        }
    }
    /**
     * Displays the total price of the products in the sale.
     *
     * @param saleDTO the SaleDTO object containing the payment information
     */
    public void showProductPrice(SaleDTO saleDTO) {
        System.out.println("Price of products in sale: " + saleDTO.getPaymentDTO().getTotalAmount());
    }
}package se.kth.iv1350.pos.startup;

import java.lang.String;
import se.kth.iv1350.pos.controller.Controller;
import se.kth.iv1350.pos.integration.AccountingSystem;
import se.kth.iv1350.pos.integration.InventorySystem;
import se.kth.iv1350.pos.integration.ReceiptPrinter;
import se.kth.iv1350.pos.model.Discounter;
import se.kth.iv1350.pos.model.QuantityDiscounter;
import se.kth.iv1350.pos.view.View;


/**
 * The Main class contains the main method that starts up the program.
 */

public class Main {
    /**
     *
     * @param args contains the arguments sent in command line when running the program.
     *             This won't be used since no commands will be sent in this case.
     */
    public static void main(String[] args) {
        AccountingSystem accountingSystem = AccountingSystem.getAccountingSystemInstance();
        InventorySystem inventorySystem = InventorySystem.getInventorySystemInstance();
        ReceiptPrinter receiptPrinter = new ReceiptPrinter();
        Controller cont = new Controller(accountingSystem, inventorySystem, receiptPrinter);
        View view = new View(cont);
        cont.initializeNewSale();
        Discounter discounter = new QuantityDiscounter(1);
        cont.setDiscounter(discounter);
        view.simulateSaleProcess();
    }
}