diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 5441d96..87a23a5 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -12,6 +12,7 @@ import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.data.connector.SQLiteConnector; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.processing.OrderManager; import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.service.writer.CatalogueWriter; @@ -58,5 +59,7 @@ public final class Main { String pCatStr = pCat.toString(); LOGGER.info(pCatStr); } + + OrderManager.createOrders(pCats); } } diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java new file mode 100644 index 0000000..69e582a --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Product are invalid. + * + * @since 0.3.0 + */ +public class InvalidCatalogueException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public InvalidCatalogueException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public InvalidCatalogueException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/model/ProductPair.java b/src/main/java/de/rwu/easydrop/model/ProductPair.java new file mode 100644 index 0000000..70140c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/ProductPair.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * Associates two related Products to one another. + * + * @since 0.3.0 + */ +@Data +public class ProductPair { + /** + * The first product. + */ + private final Product product1; + /** + * The second product. + */ + private final Product product2; + + /** + * Constructs a Product Pair. + * + * @param a First product + * @param b Second product + */ + public ProductPair(final Product a, final Product b) { + this.product1 = a; + this.product2 = b; + } +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java index 4d373b2..d841505 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java @@ -1,10 +1,87 @@ package de.rwu.easydrop.service.processing; -/** - * Processes dropshipping orders. - * - * TODO implement - */ -public class OrderManager { +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Creates dropshipping orders based on price margin. + * + * @since 0.3.0 + */ +public final class OrderManager { + + /** + * Temporary logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(OrderManager.class); + + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OrderManager() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a stateless class, don't instantiate it."); + } + + /** + * Creates orders for products with sufficient margin. + * + * @param pCats Product Catalogues + */ + public static void createOrders(final List pCats) { + for (ProductCatalogue pCat : pCats) { + ProductPair pair = getHighestMarginProducts(pCat); + + // #12: Create actual orders/transactions, remove logger + double margin = pair.getProduct2().getCurrentPrice() + - pair.getProduct1().getCurrentPrice(); + String marginFormatted = FormattingUtil.formatEuro(margin); + LOGGER.info("{}: Margin {} ({} to {})", + pCat.getProductName(), + marginFormatted, + pair.getProduct1().getDataOrigin(), + pair.getProduct2().getDataOrigin()); + } + } + + /** + * Returns the cheapest and most expensive product of a catalogue to guarantee + * the biggest margin as a pair. + * + * @param pCat Product Catalogue + * @return Cheapest Product, Most Expensive Product as a Product Pair + */ + public static ProductPair getHighestMarginProducts(final ProductCatalogue pCat) { + if (pCat.getProducts().size() < 2) { + throw new InvalidCatalogueException("Product Catalogue holds less than 2 products!"); + } + + // Initialize indexes + Product cheapestProduct = pCat.getProducts().get(0); + Product mostExpensiveProduct = pCat.getProducts().get(0); + + for (Product product : pCat.getProducts()) { + if (product.getCurrentPrice() < cheapestProduct.getCurrentPrice()) { + cheapestProduct = product; + } + if (product.getCurrentPrice() > mostExpensiveProduct.getCurrentPrice()) { + mostExpensiveProduct = product; + } + } + + if (cheapestProduct.getCurrentPrice() == mostExpensiveProduct.getCurrentPrice()) { + throw new InvalidCatalogueException("Price margin is zero!"); + } + + return new ProductPair(cheapestProduct, mostExpensiveProduct); + } } diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java index 7ef0e69..e8ea662 100644 --- a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -25,7 +25,7 @@ public final class FormattingUtil { * @return formatted price */ public static String formatEuro(final double amount) { - return String.format(Locale.GERMAN, "%,.2f", amount) + " €"; + return String.format(Locale.GERMAN, "%,.2f", amount) + " Euro"; } /** diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java new file mode 100644 index 0000000..684bc57 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.exception; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class InvalidCatalogueExceptionTest { + + @Test + void testConstructorWithMessage_ExceptionWithMessageCreated() { + // Arrange + String message = "Invalid catalogue"; + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + } + + @Test + void testConstructorWithMessageAndCause_ExceptionWithMessageAndCauseCreated() { + // Arrange + String message = "Invalid catalogue"; + Throwable cause = new RuntimeException("Cause exception"); + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message, cause); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index f070e2d..c13dcfe 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -85,8 +85,8 @@ class ProductCatalogueTest { String expectedString = "Product Catalogue: GPU\n" + "Description: Graphics Processing Units\n" + "Products:\n" + - "Product: [12345 from AmazonSeller (Amazon) at 0,00 € (available: no)]\n" + - "Product: [54321 from eBaySeller (eBay) at 0,00 € (available: no)]\n"; + "Product: [12345 from AmazonSeller (Amazon) at 0,00 Euro (available: no)]\n" + + "Product: [54321 from eBaySeller (eBay) at 0,00 Euro (available: no)]\n"; Assertions.assertEquals(expectedString, productCatalogue.toString()); } diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java index 1845b65..cf79119 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -16,7 +16,7 @@ class ProductTest { product1.setCurrentPrice(19.99); product1.setAvailable(true); - String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 € (available: yes)]"; + String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 Euro (available: yes)]"; String result1 = product1.toString(); assertEquals(expectedString1, result1); @@ -31,7 +31,7 @@ class ProductTest { product2.setCurrentPrice(9.99); product2.setAvailable(false); - String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 € (available: no)]"; + String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 Euro (available: no)]"; String result2 = product2.toString(); assertEquals(expectedString2, result2); diff --git a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java new file mode 100644 index 0000000..2ffb76a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java @@ -0,0 +1,130 @@ +package de.rwu.easydrop.service.processing; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; + +class OrderManagerTest { + + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OrderManager.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void testCreateOrders_ValidCatalogues_OrdersCreated() { + // Arrange + List catalogues = createSampleCatalogues(); + + // Act + assertDoesNotThrow(() -> { + OrderManager.createOrders(catalogues); + }); + } + + @Test + void testGetHighestMarginProducts_ValidCatalogue_CheapestAndMostExpensiveProductsReturned() { + // Arrange + ProductCatalogue catalogue = createSampleCatalogue(); + + // Act + ProductPair pair = OrderManager.getHighestMarginProducts(catalogue); + + // Assert + assertNotNull(pair); + assertNotNull(pair.getProduct1()); + assertNotNull(pair.getProduct2()); + assertEquals("Product2", pair.getProduct1().getProductId()); + assertEquals("Product3", pair.getProduct2().getProductId()); + } + + @Test + void testGetHighestMarginProducts_InvalidCatalogue_ThrowsInvalidCatalogueException() { + // Arrange + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + catalogue.addProduct(new Product()); + // The catalogue has only one product, which is invalid + + // Act & Assert + Exception e = assertThrows(InvalidCatalogueException.class, + () -> OrderManager.getHighestMarginProducts(catalogue)); + + assertEquals("Product Catalogue holds less than 2 products!", e.getMessage()); + } + + @Test + void testGetHighestMarginProducts_InvalidCatalogue_NoMargin() { + // Arrange + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + catalogue.addProduct(new Product()); + catalogue.addProduct(new Product()); + // The catalogue has only one product, which is invalid + + // Act & Assert + Exception e = assertThrows(InvalidCatalogueException.class, + () -> OrderManager.getHighestMarginProducts(catalogue)); + + assertEquals("Price margin is zero!", e.getMessage()); + } + + private List createSampleCatalogues() { + List catalogues = new ArrayList<>(); + + ProductCatalogue catalogue1 = createSampleCatalogue(); + catalogues.add(catalogue1); + + ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue2", "Sample catalogue 2"); + Product product4 = new Product(); + product4.setProductId("Product4"); + product4.setCurrentPrice(6.78); + catalogue2.addProduct(product4); + Product product5 = new Product(); + product5.setProductId("Product5"); + product5.setCurrentPrice(7.89); + catalogue2.addProduct(product5); + catalogues.add(catalogue2); + + return catalogues; + } + + private ProductCatalogue createSampleCatalogue() { + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + Product product1 = new Product(); + product1.setProductId("Product1"); + product1.setCurrentPrice(1.23); + catalogue.addProduct(product1); + Product product2 = new Product(); + product2.setProductId("Product2"); + product2.setCurrentPrice(0.89); + catalogue.addProduct(product2); + Product product3 = new Product(); + product3.setProductId("Product3"); + product3.setCurrentPrice(4.56); + catalogue.addProduct(product3); + return catalogue; + } +} diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java index 1c7d4f9..4b1dfa1 100644 --- a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java +++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java @@ -29,7 +29,7 @@ class FormattingUtilTest { @Test void testFormatEuro_positiveAmount() { double amount = 1234.56; - String expectedFormattedAmount = "1.234,56 €"; + String expectedFormattedAmount = "1.234,56 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -39,7 +39,7 @@ class FormattingUtilTest { @Test void testFormatEuro_zeroAmount() { double amount = 0.0; - String expectedFormattedAmount = "0,00 €"; + String expectedFormattedAmount = "0,00 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -49,7 +49,7 @@ class FormattingUtilTest { @Test void testFormatEuro_negativeAmount() { double amount = -789.12; - String expectedFormattedAmount = "-789,12 €"; + String expectedFormattedAmount = "-789,12 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount);