diff --git a/data/products.json b/data/products.json index 8878e54..1d70858 100644 --- a/data/products.json +++ b/data/products.json @@ -1 +1,8 @@ -[ ] \ No newline at end of file +[ { + "productId" : "P-0003", + "bezeichnung" : "Bohrmaschine", + "einzelpreisNetto" : 89.99, + "mwstSatz" : "NORMAL", + "beschreibung" : "Schlagbohrer", + "kategorie" : "Elektrowerkzeuge" +} ] \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java b/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java index a1f9c61..4763e77 100644 --- a/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java +++ b/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java @@ -4,45 +4,53 @@ import java.math.BigDecimal; import java.util.List; import java.util.Optional; +import de.hsmannheim.faktura.dokumente.repository.DokumentRepository; import de.hsmannheim.faktura.product.Product; import de.hsmannheim.faktura.product.repository.ProductRepository; /** - * Service-Klasse zur Bereitstellung der Geschäftslogik für die Produktverwaltung (Gruppe G)[cite: 84]. + * Service-Klasse zur Bereitstellung der Geschäftslogik für die Produktverwaltung (Gruppe G). * Dient als zentrale programminterne API für die Schnittstellen zur Programmoberfläche (Gruppe E) - * und den Dokumentenprozess (Gruppe F)[cite: 10, 58, 59, 60]. - * Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation[cite: 51, 377]. + * und den Dokumentenprozess (Gruppe F). + * Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation. */ public class ProductService { /** - * Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung[cite: 71, 85]. + * Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung. */ private final ProductRepository productRepository; - + + /** + * Referenz auf das Dokumenten-Repository zur Durchführung der Referenzprüfung. + */ + private final DokumentRepository dokumentRepository; + /** * Konstruktor zur Erstellung des ProductService mittels Dependency Injection. * - * @param productRepository Das zu verwendende Repository für Datenzugriffe[cite: 85]. + * @param productRepository Das zu verwendende Repository für Datenzugriffe + * @param dokumentRepository Das zu verwendende Repository für Dokumentendaten */ - public ProductService(ProductRepository productRepository) { + public ProductService(ProductRepository productRepository, DokumentRepository dokumentRepository) { this.productRepository = productRepository; + this.dokumentRepository = dokumentRepository; } /** - * Gibt eine tabellarische Liste aller im System hinterlegten Produkte zurück[cite: 36, 58, 59, 60]. - * Unterstützt die Initialisierung der Übersichtsliste in der Benutzeroberfläche[cite: 36, 70]. + * Gibt eine tabellarische Liste aller im System hinterlegten Produkte zurück. + * Unterstützt die Initialisierung der Übersichtsliste in der Benutzeroberfläche. * - * @return Eine Liste von {@link Product}-Objekten[cite: 87]. + * @return Eine Liste von {@link Product}-Objekten. */ public List getAllProducts() { return productRepository.findAll(); } /** - * Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID[cite: 88]. + * Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID. * - * @param productId Die eindeutige Kennung des gesuchten Produkts [cite: 88] + * @param productId Die eindeutige Kennung des gesuchten Produkts * @return Ein {@link Optional}, das das gefundene Produkt enthält, oder leer ist */ public Optional getProductById(String productId) { @@ -50,28 +58,31 @@ public class ProductService { } /** - * Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz[cite: 28, 29, 58, 59, 60]. - *

- * Validierungen gemäß Pflichtenheft: - *

- * - * @param product Das zu speichernde {@link Product}-Objekt [cite: 89] - * @throws IllegalArgumentException Wenn das Produkt null ist oder der Netto-Einzelpreis negativ ist [cite: 38, 145] + * Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz. + * * @param product Das zu speichernde oder zu aktualisierende {@link Product}-Objekt + * @throws IllegalArgumentException Wenn Validierungsregeln aus Kapitel 6.1.1 verletzt werden */ public void saveProduct(Product product) { if (product == null) { throw new IllegalArgumentException("Das Produkt darf nicht null sein."); } - // Validierung gemäß Anforderung F-SH-PV-06 (Preisprüfung) + if (product.getBezeichnung() == null || product.getBezeichnung().trim().isEmpty()) { + throw new IllegalArgumentException("Anforderung F-SH-PV-01 verletzt: Die Bezeichnung ist ein Pflichtfeld."); + } + + if (product.getBezeichnung().length() > 100) { + throw new IllegalArgumentException("Anforderung F-SH-PV-01 verletzt: Die Bezeichnung darf maximal 100 Zeichen lang sein."); + } + if (product.getEinzelpreisNetto() == null || product.getEinzelpreisNetto().compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Anforderung F-SH-PV-06 verletzt: Der Einzelpreis Netto darf nicht negativ sein."); } - // Automatische ID-Generierung, falls ein neues Produkt noch keinen Identifikator besitzt + if (product.getBeschreibung() != null && product.getBeschreibung().length() > 500) { + throw new IllegalArgumentException("Anforderung F-PV-07 verletzt: Die Beschreibung darf maximal 500 Zeichen lang sein."); + } + if (product.getProductId() == null || product.getProductId().trim().isEmpty()) { product.setProductId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8)); } @@ -80,34 +91,37 @@ public class ProductService { } /** - * Löscht ein Produkt permanent aus dem Datenbestand anhand seiner ID[cite: 58, 59, 60, 90]. - *

- * Geschäftsregel GR-05 (Stammdatenschutz): - * Vor dem eigentlichen Löschen muss zwingend über den Dokumentenprozess (Gruppe F) geprüft werden, - * ob das Produkt in aktiven Belegen (Angebote, Rechnungen etc.) referenziert wird[cite: 33, 34, 115, 431]. - * Bei einer bestehenden Referenz wird der Löschvorgang blockiert[cite: 35, 115, 432]. - * - * @param productId Die eindeutige ID des zu löschenden Produkts [cite: 90] + * Löscht ein Produkt permanent aus dem Datenbestand anhand seiner ID. + * * @param productId Die eindeutige ID des zu löschenden Produkts * @return {@code true}, wenn das Produkt erfolgreich gelöscht wurde; {@code false}, wenn die ID leer war - * oder der Löschvorgang blockiert wurde[cite: 35, 90]. + * oder der Löschvorgang blockiert wurde */ public boolean deleteProduct(String productId) { if (productId == null || productId.trim().isEmpty()) { return false; } + try { + String cleanId = productId.replace("PROD-", "").replace("P-", "").trim(); + long idAsLong = Long.parseLong(cleanId); + + boolean isReferenced = dokumentRepository.existsReferencedProdukt(idAsLong); + + if (isReferenced) { + return false; + } + } catch (NumberFormatException e) { + System.err.println("ID-Format ungültig für Referenzprüfung: " + e.getMessage()); + return false; + } + return productRepository.deleteById(productId); } /** - * Filtert die Produktliste in Echtzeit basierend auf einem Suchbegriff[cite: 37, 360, 361]. - *

- * Anforderung F-SH-PV-05: - * Es werden alle Produkte zurückgegeben, bei denen der Suchbegriff eine Teilmenge der - * Bezeichnung bildet, wobei Groß- und Kleinschreibung vollständig ignoriert wird[cite: 37, 153]. - * - * @param query Der eingegebene Suchbegriff für die Live-Filterung [cite: 37, 91] - * @return Eine Liste der gefilterten Produkte, die dem Suchkriterium entsprechen [cite: 37, 91] + * Filtert die Produktliste in Echtzeit basierend auf einem Suchbegriff. + * * @param query Der eingegebene Suchbegriff für die Live-Filterung + * @return Eine Liste der gefilterten Produkte, die dem Suchkriterium entsprechen */ public List searchProducts(String query) { if (query == null || query.trim().isEmpty()) { diff --git a/src/test/java/de/hsmannheim/faktura/product/repository/ProductRepositoryTest.java b/src/test/java/de/hsmannheim/faktura/product/repository/ProductRepositoryTest.java new file mode 100644 index 0000000..209ca18 --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/product/repository/ProductRepositoryTest.java @@ -0,0 +1,63 @@ +package de.hsmannheim.faktura.product.repository; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.hsmannheim.faktura.product.MwstSatz; +import de.hsmannheim.faktura.product.Product; + +/** + * Testklasse für das ProductRepository (Gruppe G). + * Überprüft die korrekte Tabelleninitialisierung und die verlustfreie + * dateibasierte Persistenz der Stammdaten (NF-SH-ARCH-01). + */ +public class ProductRepositoryTest { + + private ProductRepository productRepository; + + @BeforeEach + public void setUp() { + productRepository = new ProductRepository(); + + for (Product p : productRepository.findAll()) { + productRepository.deleteById(p.getProductId()); + } + } + + /** + * Testet die tabellarische Bereitstellung aller Produkte für die Übersichtsliste. + * Abdeckung von Anforderung: F-SH-PV-04. + */ + @Test + public void testGetAllAndSorting() { + Product p1 = new Product("P-0001", "A-Schraube", new BigDecimal("1.50"), MwstSatz.NORMAL, "", ""); + Product p2 = new Product("P-0002", "B-Mutter", new BigDecimal("0.80"), MwstSatz.NORMAL, "", ""); + + productRepository.save(p1); + productRepository.save(p2); + + List allProducts = productRepository.findAll(); + assertEquals(2, allProducts.size()); + } + + /** + * Testet die verlustfreie Wiederherstellung der Produktdaten aus dem JSON-Speicher nach einem Systemneustart. + * Abdeckung von Anforderung: NF-SH-ARCH-01. + */ + @Test + public void testPersistenceOnRestart() { + Product product = new Product("P-1000", "Persistenz-Test", new BigDecimal("49.99"), MwstSatz.NORMAL, "Sicherung auf Dateiebene", "Test"); + productRepository.save(product); + + // Simulation des Anwendungs-Neustarts durch Neuinitialisierung des Repositories + ProductRepository newRepositoryInstance = new ProductRepository(); + + assertTrue(newRepositoryInstance.findById("P-1000").isPresent()); + assertEquals("Persistenz-Test", newRepositoryInstance.findById("P-1000").get().getBezeichnung()); + } +} \ No newline at end of file diff --git a/src/test/java/de/hsmannheim/faktura/product/service/ProductPerformanceTest.java b/src/test/java/de/hsmannheim/faktura/product/service/ProductPerformanceTest.java new file mode 100644 index 0000000..587c8a3 --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/product/service/ProductPerformanceTest.java @@ -0,0 +1,61 @@ +package de.hsmannheim.faktura.product.service; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import de.hsmannheim.faktura.dokumente.repository.DokumentRepository; +import de.hsmannheim.faktura.product.MwstSatz; +import de.hsmannheim.faktura.product.Product; +import de.hsmannheim.faktura.product.repository.ProductRepository; + +/** + * Performancetest-Klasse für die Produktverwaltung (Gruppe G). + * Verifiziert die Zeitschranken beim Laden und Filtern von großen Datenmengen (NF-SH-PERF-01). + */ +@SuppressWarnings("all") +public class ProductPerformanceTest { + + /** + * Überprüft, ob das Durchsuchen und Echtzeit-Filtern bei einem Datenbestand von 1.000 + * Produkten in weniger als 2 Sekunden vollständig abgeschlossen wird. + * Abdeckung von Anforderung: NF-SH-PERF-01. + */ + @Test + public void testSearchPerformance() { + ProductRepository repo = new ProductRepository(); + DokumentRepository dummyDocRepo = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() { + @Override public boolean existsReferencedProdukt(long id) { return false; } + @Override public boolean existsReferencedKunde(long id) { return false; } + @Override public int findMaxLaufendeNummer(String p, int j) { return 0; } + @Override public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {} + @Override public java.util.Optional findByNummer(String n) { return java.util.Optional.empty(); } + @Override public List findByKunde(long id) { return java.util.List.of(); } + @Override public boolean existsFolgedokument(String q) { return false; } + }; + + ProductService service = new ProductService(repo, dummyDocRepo); + + // Bereinigen und künstliches Befüllen mit exakt 1.000 Produkten + for (Product p : repo.findAll()) { + repo.deleteById(p.getProductId()); + } + + for (int i = 1; i <= 1000; i++) { + repo.save(new Product("P-" + i, "Produkt Nummer " + i, new BigDecimal("10.00"), MwstSatz.NORMAL, "", "Kategorie")); + } + + // Zeitmessung starten + long startTime = System.currentTimeMillis(); + + List results = service.searchProducts("Nummer 999"); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + // Überprüfung der Zeitschranke (2000 Millisekunden = 2 Sekunden) + assertTrue(duration < 2000, "Der Lasttest wurde nicht in der geforderten Zeit von 2 Sekunden erfüllt. Benötigt: " + duration + "ms"); + } +} \ No newline at end of file diff --git a/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java b/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java index 94fc34d..e03d1a6 100644 --- a/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java +++ b/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java @@ -2,14 +2,17 @@ package de.hsmannheim.faktura.product.service; import java.math.BigDecimal; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.hsmannheim.faktura.dokumente.domain.Dokument; import de.hsmannheim.faktura.product.MwstSatz; import de.hsmannheim.faktura.product.Product; import de.hsmannheim.faktura.product.repository.ProductRepository; @@ -17,24 +20,58 @@ import de.hsmannheim.faktura.product.repository.ProductRepository; /** * Komponententest-Klasse für den ProductService (Gruppe G). * Überprüft die Einhaltung der funktionalen Anforderungen und Geschäftsregeln - * aus dem Pflichtenheft vollständig ohne GUI-Abhängigkeiten (NF-TEST-02)[cite: 51, 379]. + * aus dem Pflichtenheft vollständig ohne GUI-Abhängigkeiten (NF-TEST-02). */ - +@SuppressWarnings("all") public class ProductServiceTest { private ProductService productService; private ProductRepository productRepository; + private de.hsmannheim.faktura.dokumente.repository.DokumentRepository dokumentRepository; - /** - * Setzt vor jedem einzelnen Testlauf eine frische Testumgebung auf - * und bereinigt den lokalen JSON-Cache, um Seiteneffekte zu verhindern[cite: 16, 143]. - */ @BeforeEach public void setUp() { productRepository = new ProductRepository(); - productService = new ProductService(productRepository); - // Cache leeren, damit alte JSON-Einträge die Testergebnisse nicht verfälschen + dokumentRepository = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() { + @Override + public boolean existsReferencedProdukt(long produktId) { + return false; + } + + @Override + public boolean existsReferencedKunde(long kundenId) { + return false; + } + + @Override + public int findMaxLaufendeNummer(String praefix, int jahr) { + return 0; + } + + @Override + public void save(Dokument dokument) { + throw new UnsupportedOperationException("Unimplemented method 'save'"); + } + + @Override + public Optional findByNummer(String nummer) { + throw new UnsupportedOperationException("Unimplemented method 'findByNummer'"); + } + + @Override + public List findByKunde(long kundenId) { + throw new UnsupportedOperationException("Unimplemented method 'findByKunde'"); + } + + @Override + public boolean existsFolgedokument(String quellDokumentNummer) { + throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'"); + } + }; + + productService = new ProductService(productRepository, dokumentRepository); + for (Product p : productRepository.findAll()) { productRepository.deleteById(p.getProductId()); } @@ -42,7 +79,7 @@ public class ProductServiceTest { /** * Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen. - * Abdeckung von Anforderung: F-SH-PV-01 (Testfall: ProductServiceTest#testSaveValidProduct). + * Abdeckung von Anforderung: F-SH-PV-01. */ @Test public void testSaveValidProduct() { @@ -52,13 +89,31 @@ public class ProductServiceTest { assertTrue(productService.getProductById("P-0001").isPresent()); } + /** + * Testet das erfolgreiche Modifizieren und Überschreiben bestehender Produktdatensätze. + * Abdeckung von Anforderung: F-SH-PV-02. + */ + @Test + public void testUpdateProductAttributes() { + Product product = new Product("P-0001", "Alte Bezeichnung", new BigDecimal("5.00"), MwstSatz.NORMAL, "Alt", "Alt"); + productService.saveProduct(product); + + Product updatedProduct = new Product("P-0001", "Neue Bezeichnung", new BigDecimal("12.50"), MwstSatz.ERMAESSIGT, "Neu", "Neu"); + productService.saveProduct(updatedProduct); + + Optional result = productService.getProductById("P-0001"); + assertTrue(result.isPresent()); + assertEquals("Neue Bezeichnung", result.get().getBezeichnung()); + assertEquals(new BigDecimal("12.50"), result.get().getEinzelpreisNetto()); + assertEquals(MwstSatz.ERMAESSIGT, result.get().getMwstSatz()); + } + /** * Prüft, ob Preise kleiner als 0.00 wie gefordert blockiert und mit einer Exception abgewiesen werden. - * Abdeckung von Anforderung: F-SH-PV-06 / AC-SH-PV-01 (Testfall: ProductServiceTest#testNegativePriceRejection)[cite: 142, 145, 159]. + * Abdeckung von Anforderung: F-SH-PV-06 / AC-SH-PV-01. */ @Test public void testNegativePriceRejection() { - // AC-SH-PV-01: Preis kleiner als 0.00 muss abgelehnt werden Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test"); assertThrows(IllegalArgumentException.class, () -> { @@ -66,10 +121,42 @@ public class ProductServiceTest { }); } + /** + * Überprüft, ob Beschreibungen mit mehr als 500 Zeichen blockiert und mit einer Exception abgewiesen werden. + * Abdeckung von Anforderung: F-PV-07. + */ + @Test + public void testDescriptionLengthBounds() { + StringBuilder longDescription = new StringBuilder(); + for (int i = 0; i < 501; i++) { + longDescription.append("A"); + } + + Product invalidProduct = new Product("P-0002", "Produkt mit langer Beschreibung", new BigDecimal("10.00"), MwstSatz.NORMAL, longDescription.toString(), "Test"); + + assertThrows(IllegalArgumentException.class, () -> { + productService.saveProduct(invalidProduct); + }); + } + + /** + * Prüft, ob die optionale Kategoriezuweisung korrekt verarbeitet und persistent im Objekt hinterlegt wird. + * Abdeckung von Anforderung: F-PV-08. + */ + @Test + public void testCategoryAssignment() { + Product product = new Product("P-0003", "Bohrmaschine", new BigDecimal("89.99"), MwstSatz.NORMAL, "Schlagbohrer", "Elektrowerkzeuge"); + productService.saveProduct(product); + + Optional result = productService.getProductById("P-0003"); + assertTrue(result.isPresent()); + assertEquals("Elektrowerkzeuge", result.get().getKategorie()); + } + /** * Überprüft die Echtzeit-Filterung der Produktliste anhand einer Teilmenge des Namens, * wobei Groß- und Kleinschreibung ignoriert werden muss. - * Abdeckung von Anforderung: F-SH-PV-05 / AC-SH-PV-03 (Testfall: ProductServiceTest#testSearchCaseInsensitive)[cite: 150, 153, 159]. + * Abdeckung von Anforderung: F-SH-PV-05 / AC-SH-PV-03. */ @Test public void testSearchCaseInsensitive() { @@ -79,8 +166,52 @@ public class ProductServiceTest { productService.saveProduct(p1); productService.saveProduct(p2); - // Suche nach "schraube" muss im Ergebnis exakt beide Objekte liefern List results = productService.searchProducts("schraube"); assertEquals(2, results.size()); } + + /** + * Überprüft die Blockierung des Löschvorgangs, falls das Produkt in Belegen referenziert wird. + * Abdeckung von Geschäftsregel: GR-05 / AC-SH-PV-02 / F-SH-PV-03. + */ + @Test + public void testDeleteReferencedProduct() { + de.hsmannheim.faktura.dokumente.repository.DokumentRepository mockRepo = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() { + @Override + public boolean existsReferencedProdukt(long produktId) { + return true; + } + @Override + public boolean existsReferencedKunde(long kundenId) { + return false; + } + @Override + public int findMaxLaufendeNummer(String praefix, int jahr) { + return 0; + } + @Override + public void save(Dokument dokument) { + throw new UnsupportedOperationException("Unimplemented method 'save'"); + } + @Override + public Optional findByNummer(String nummer) { + throw new UnsupportedOperationException("Unimplemented method 'findByNummer'"); + } + @Override + public List findByKunde(long kundenId) { + throw new UnsupportedOperationException("Unimplemented method 'findByKunde'"); + } + @Override + public boolean existsFolgedokument(String quellDokumentNummer) { + throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'"); + } + }; + + ProductService serviceWithSperre = new ProductService(productRepository, mockRepo); + Product product = new Product("P-0001", "Gesperrtes Produkt", new BigDecimal("10.00"), MwstSatz.NORMAL, "", ""); + productRepository.save(product); + + boolean result = serviceWithSperre.deleteProduct("P-0001"); + assertFalse(result, "Das Löschen hätte blockiert werden müssen, da das Produkt referenziert wird."); + } } \ No newline at end of file