From a29c99d9e8514dac04cc700cd7d091845887f05b Mon Sep 17 00:00:00 2001 From: Lulia <3030489@stud.th-mannheim.de> Date: Sat, 27 Jun 2026 21:25:50 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Produktverwaltung:=20Unit-Tests=20f=C3=BCr?= =?UTF-8?q?=20Service=20erfolgreich=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/products.json | 8 ++ .../hsmannheim/faktura/produkt/MwstSatz.java | 8 ++ .../hsmannheim/faktura/produkt/Produkt.java | 91 ++++++++++++++++ .../produkt/repository/ProduktRepository.java | 101 ++++++++++++++++++ .../produkt/service/ProduktService.java | 64 +++++++++++ .../produkt/service/ProduktServiceTest.java | 58 ++++++++++ 6 files changed, 330 insertions(+) create mode 100644 data/products.json create mode 100644 src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java create mode 100644 src/main/java/de/hsmannheim/faktura/produkt/Produkt.java create mode 100644 src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java create mode 100644 src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java create mode 100644 src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..e4001c2 --- /dev/null +++ b/data/products.json @@ -0,0 +1,8 @@ +[ { + "produktId" : "P-0001", + "bezeichnung" : "M6 Schraube", + "einzelpreisNetto" : 10.50, + "mwstSatz" : "NORMAL", + "beschreibung" : "Beschreibung", + "kategorie" : "Werkzeug" +} ] \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java b/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java new file mode 100644 index 0000000..8b7f93e --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java @@ -0,0 +1,8 @@ +package de.hsmannheim.faktura.produkt; + +public enum MwstSatz { + + NORMAL, + ERMAESSIGT +} + diff --git a/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java b/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java new file mode 100644 index 0000000..0618dff --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java @@ -0,0 +1,91 @@ +package de.hsmannheim.faktura.produkt; + +import java.math.BigDecimal; + +public class Produkt { + + private String produktId; + private String bezeichnung; + private BigDecimal einzelpreisNetto; + private MwstSatz mwstSatz; + private String beschreibung; + private String kategorie; + + public Produkt() { + + } + + public Produkt(String produktId, String bezeichnung, BigDecimal einzelpreisNetto, + MwstSatz mwstSatz, String beschreibung, String kategorie) { + this.produktId = produktId; + this.bezeichnung = bezeichnung; + this.einzelpreisNetto = einzelpreisNetto; + this.mwstSatz = mwstSatz; + this.beschreibung = beschreibung; + this.kategorie = kategorie; + } + + // ========================================== + // Getter und Setter + // ========================================== + + public String getProduktId() { + return produktId; + } + + public void setProduktId(String produktId) { + this.produktId = produktId; + } + + public String getBezeichnung() { + return bezeichnung; + } + + public void setBezeichnung(String bezeichnung) { + this.bezeichnung = bezeichnung; + } + + public BigDecimal getEinzelpreisNetto() { + return einzelpreisNetto; + } + + public void setEinzelpreisNetto(BigDecimal einzelpreisNetto) { + this.einzelpreisNetto = einzelpreisNetto; + } + + public MwstSatz getMwstSatz() { + return mwstSatz; + } + + public void setMwstSatz(MwstSatz mwstSatz) { + this.mwstSatz = mwstSatz; + } + + public String getBeschreibung() { + return beschreibung; + } + + public void setBeschreibung(String beschreibung) { + this.beschreibung = beschreibung; + } + + public String getKategorie() { + return kategorie; + } + + public void setKategorie(String kategorie) { + this.kategorie = kategorie; + } + + @Override + public String toString() { + return "Produkt{" + + "produktId='" + produktId + '\'' + + ", bezeichnung='" + bezeichnung + '\'' + + ", einzelpreisNetto=" + einzelpreisNetto + + ", mwstSatz=" + mwstSatz + + ", kategorie='" + kategorie + '\'' + + '}'; + } +} + diff --git a/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java b/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java new file mode 100644 index 0000000..80f2f49 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java @@ -0,0 +1,101 @@ +package de.hsmannheim.faktura.produkt.repository; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import de.hsmannheim.faktura.produkt.Produkt; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + + +public class ProduktRepository { + + + private static final String FILE_PATH = "./data/products.json"; + private final ObjectMapper objectMapper; + private List cache; + + public ProduktRepository() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + this.cache = new ArrayList<>(); + + + loadData(); + } + + + public List findAll() { + return new ArrayList<>(cache); + } + + + public Optional findById(String produktId) { + return cache.stream() + .filter(p -> p.getProduktId().equals(produktId)) + .findFirst(); + } + + public void save(Produkt product) { + + Optional existingProduct = findById(product.getProduktId()); + + if (existingProduct.isPresent()) { + int index = cache.indexOf(existingProduct.get()); + cache.set(index, product); + } else { + cache.add(product); + } + + + saveData(); + } + + + public boolean deleteById(String produktId) { + boolean removed = cache.removeIf(p -> p.getProduktId().equals(produktId)); + if (removed) { + saveData(); + } + return removed; + } + + + private void loadData() { + File file = new File(FILE_PATH); + + + if (!file.exists()) { + this.cache = new ArrayList<>(); + return; + } + + try { + this.cache = objectMapper.readValue(file, new TypeReference>() {}); + } catch (IOException e) { + System.err.println("Fehler beim Laden der Produktdaten: " + e.getMessage()); + + this.cache = new ArrayList<>(); + } + } + + + private void saveData() { + File file = new File(FILE_PATH); + + File directory = file.getParentFile(); + if (directory != null && !directory.exists()) { + directory.mkdirs(); + } + + try { + objectMapper.writeValue(file, cache); + } catch (IOException e) { + System.err.println("Fehler beim Speichern der Produktdaten: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java b/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java new file mode 100644 index 0000000..f496af9 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java @@ -0,0 +1,64 @@ +package de.hsmannheim.faktura.produkt.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import de.hsmannheim.faktura.produkt.Produkt; +import de.hsmannheim.faktura.produkt.repository.ProduktRepository; + + +public class ProduktService { + + private final ProduktRepository produktRepository; + + public ProduktService(ProduktRepository produktRepository) { + this.produktRepository = produktRepository; + } + + + public List getAllProducts() { + return produktRepository.findAll(); + } + + + public Optional getProductById(String produktId) { + return produktRepository.findById(produktId); + } + + + public void saveProduct(Produkt product) { + if (product == null) { + throw new IllegalArgumentException("Das Produkt darf nicht null 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."); + } + + if (product.getProduktId() == null || product.getProduktId().trim().isEmpty()) { + + product.setProduktId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8)); + } + + produktRepository.save(product); + } + + public boolean deleteProduct(String produktId) { + if (produktId == null || produktId.trim().isEmpty()) { + return false; + } + + return produktRepository.deleteById(produktId); + } + + public List searchProducts(String query) { + if (query == null || query.trim().isEmpty()) { + return getAllProducts(); + } + String lowerQuery = query.toLowerCase(); + return getAllProducts().stream() + .filter(p -> p.getBezeichnung() != null && p.getBezeichnung().toLowerCase().contains(lowerQuery)) + .collect(java.util.stream.Collectors.toList()); +} +} \ No newline at end of file diff --git a/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java b/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java new file mode 100644 index 0000000..01fd6a1 --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java @@ -0,0 +1,58 @@ +package de.hsmannheim.faktura.produkt.service; + +import de.hsmannheim.faktura.produkt.Produkt; +import de.hsmannheim.faktura.produkt.MwstSatz; +import de.hsmannheim.faktura.produkt.repository.ProduktRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProduktServiceTest { + + private ProduktService produktService; + private ProduktRepository produktRepository; + + @BeforeEach + public void setUp() { + produktRepository = new ProduktRepository(); + produktService = new ProduktService(produktRepository); + + // Cache leeren, damit alte JSON-Einträge die Tests nicht verfälschen + for (Produkt p : produktRepository.findAll()) { + produktRepository.deleteById(p.getProduktId()); + } + } + + @Test + public void testSaveValidProduct() { + Produkt valid = new Produkt("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug"); + assertDoesNotThrow(() -> produktService.saveProduct(valid)); + assertTrue(produktService.getProductById("P-0001").isPresent()); + } + + @Test + public void testNegativePriceRejection() { + // AC-SH-PV-01: Preis kleiner als 0.00 muss abgelehnt werden + Produkt invalidProduct = new Produkt("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test"); + + assertThrows(IllegalArgumentException.class, () -> produktService.saveProduct(invalidProduct)); + } + + @Test + public void testSearchCaseInsensitive() { + // AC-SH-PV-03: Suchtest für Groß-/Kleinschreibung + Produkt p1 = new Produkt("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", ""); + Produkt p2 = new Produkt("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", ""); + + produktService.saveProduct(p1); + produktService.saveProduct(p2); + + List results = produktService.searchProducts("schraube"); + assertEquals(2, results.size()); + } +} \ No newline at end of file -- 2.51.0 From 0b53d089bc52387cd6a5a6b8f0a9e096bec982ec Mon Sep 17 00:00:00 2001 From: Lulia <3030489@stud.th-mannheim.de> Date: Sat, 27 Jun 2026 22:41:01 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Refactoring:=20Ordnerstrukturen=20angepasst?= =?UTF-8?q?=20und=20sinnvolle=20Dokumentationskommentare=20hinzugef=C3=BCg?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/products.json | 15 +- .../hsmannheim/faktura/product/MwstSatz.java | 10 ++ .../hsmannheim/faktura/product/Product.java | 156 ++++++++++++++++++ .../product/repository/ProductRepository.java | 150 +++++++++++++++++ .../product/service/ProductService.java | 121 ++++++++++++++ .../hsmannheim/faktura/produkt/MwstSatz.java | 8 - .../hsmannheim/faktura/produkt/Produkt.java | 91 ---------- .../produkt/repository/ProduktRepository.java | 101 ------------ .../produkt/service/ProduktService.java | 64 ------- .../product/service/ProductServiceTest.java | 61 +++++++ .../produkt/service/ProduktServiceTest.java | 58 ------- 11 files changed, 509 insertions(+), 326 deletions(-) create mode 100644 src/main/java/de/hsmannheim/faktura/product/MwstSatz.java create mode 100644 src/main/java/de/hsmannheim/faktura/product/Product.java create mode 100644 src/main/java/de/hsmannheim/faktura/product/repository/ProductRepository.java create mode 100644 src/main/java/de/hsmannheim/faktura/product/service/ProductService.java delete mode 100644 src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java delete mode 100644 src/main/java/de/hsmannheim/faktura/produkt/Produkt.java delete mode 100644 src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java delete mode 100644 src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java create mode 100644 src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java delete mode 100644 src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java diff --git a/data/products.json b/data/products.json index e4001c2..787b843 100644 --- a/data/products.json +++ b/data/products.json @@ -1,8 +1,15 @@ [ { - "produktId" : "P-0001", + "productId" : "P-0001", "bezeichnung" : "M6 Schraube", - "einzelpreisNetto" : 10.50, + "einzelpreisNetto" : 1.00, "mwstSatz" : "NORMAL", - "beschreibung" : "Beschreibung", - "kategorie" : "Werkzeug" + "beschreibung" : "", + "kategorie" : "" +}, { + "productId" : "P-0002", + "bezeichnung" : "M8 SCHRAUBE", + "einzelpreisNetto" : 2.00, + "mwstSatz" : "NORMAL", + "beschreibung" : "", + "kategorie" : "" } ] \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/product/MwstSatz.java b/src/main/java/de/hsmannheim/faktura/product/MwstSatz.java new file mode 100644 index 0000000..72ba1b7 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/product/MwstSatz.java @@ -0,0 +1,10 @@ +package de.hsmannheim.faktura.product; + +public enum MwstSatz { +/** + * Enumeration zur Repräsentation der Mehrwertsteuersätze in Deutschland. + */ + NORMAL, + ERMAESSIGT +} + diff --git a/src/main/java/de/hsmannheim/faktura/product/Product.java b/src/main/java/de/hsmannheim/faktura/product/Product.java new file mode 100644 index 0000000..3082b73 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/product/Product.java @@ -0,0 +1,156 @@ +package de.hsmannheim.faktura.product; + +import java.math.BigDecimal; + +/** + * Entitätsklasse zur Repräsentation eines Produkts in der Produktverwaltung (Gruppe G)[cite: 54, 71]. + * Bildet die Stammdaten eines Artikels mit Netto-Einzelpreis, Bezeichnung und Mehrwertsteuersatz ab[cite: 55]. + * Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation. + */ +public class Product { + + private String productId; + private String bezeichnung; + private BigDecimal einzelpreisNetto; + private MwstSatz mwstSatz; + private String beschreibung; + private String kategorie; + + /** + * Standardkonstruktor. + * Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt[cite: 71]. + */ + public Product() { + } + + /** + * All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts[cite: 55]. + * + * @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001) [cite: 55, 392] + * @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen) [cite: 55, 456] + * @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein) [cite: 55] + * @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT) [cite: 55] + * @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen) [cite: 55] + * @param kategorie Optionale Warengruppe oder hierarchische Zuordnung [cite: 55] + */ + public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto, + MwstSatz mwstSatz, String beschreibung, String kategorie) { + this.productId = productId; + this.bezeichnung = bezeichnung; + this.einzelpreisNetto = einzelpreisNetto; + this.mwstSatz = mwstSatz; + this.beschreibung = beschreibung; + this.kategorie = kategorie; + } + + /** + * Gibt die eindeutige Produkt-ID zurück[cite: 55, 392]. + * * @return Die productId als String [cite: 55] + */ + public String getProductId() { + return productId; + } + + /** + * Setzt die eindeutige Produkt-ID[cite: 55, 392]. + * * @param productId Die neu zu setzende ID [cite: 55] + */ + public void setProductId(String productId) { + this.productId = productId; + } + + /** + * Gibt den Anzeigenamen (die Bezeichnung) des Produkts zurück[cite: 55, 456]. + * * @return Die Produktbezeichnung als String [cite: 55] + */ + public String getBezeichnung() { + return bezeichnung; + } + + /** + * Setzt die Produktbezeichnung[cite: 55, 456]. + * * @param bezeichnung Der neue Produktname (Pflichtfeld) [cite: 55, 393] + */ + public void setBezeichnung(String bezeichnung) { + this.bezeichnung = bezeichnung; + } + + /** + * Gibt den Netto-Einzelpreis des Produkts zurück[cite: 55, 393]. + * * @return Der einzelpreisNetto als BigDecimal zur Vermeidung von Rundungsfehlern [cite: 55] + */ + public BigDecimal getEinzelpreisNetto() { + return einzelpreisNetto; + } + + /** + * Setzt den Netto-Einzelpreis des Produkts[cite: 55, 393]. + * * @param einzelpreisNetto Der neue Netto-Preis (muss numerisch >= 0.00 sein) [cite: 55] + */ + public void setEinzelpreisNetto(BigDecimal einzelpreisNetto) { + this.einzelpreisNetto = einzelpreisNetto; + } + + /** + * Gibt den hinterlegten Mehrwertsteuersatz des Artikels zurück[cite: 55, 393]. + * * @return Der mwstSatz als Enum-Konstante (NORMAL oder ERMAESSIGT) [cite: 55] + */ + public MwstSatz getMwstSatz() { + return mwstSatz; + } + + /** + * Setzt den Mehrwertsteuersatz des Artikels[cite: 55, 393]. + * * @param mwstSatz Der anzuwendende Steuersatz [cite: 55] + */ + public void setMwstSatz(MwstSatz mwstSatz) { + this.mwstSatz = mwstSatz; + } + + /** + * Gibt die optionale Beschreibung des Artikels zurück[cite: 55, 394]. + * * @return Der Beschreibungstext oder null, falls nicht angegeben [cite: 55] + */ + public String getBeschreibung() { + return beschreibung; + } + + /** + * Setzt den optionalen Beschreibungstext[cite: 55, 394]. + * * @param beschreibung Freitext zur Produktbeschreibung (max. 500 Zeichen) [cite: 55] + */ + public void setBeschreibung(String beschreibung) { + this.beschreibung = beschreibung; + } + + /** + * Gibt die optionale Produktkategorie zurück[cite: 55, 394]. + * * @return Die Kategoriebezeichnung als String [cite: 55] + */ + public String getKategorie() { + return kategorie; + } + + /** + * Setzt die optionale Produktkategorie[cite: 55, 394]. + * * @param kategorie Name der Kategorie zur Gruppierung [cite: 55] + */ + public void setKategorie(String kategorie) { + this.kategorie = kategorie; + } + + /** + * Erzeugt eine String-Repräsentation des Produkt-Objekts für Debugging- und Testzwecke. + * * @return Ein String mit den wichtigsten Attributwerten des Produkts + */ + @Override + public String toString() { + return "Product{" + + "productId='" + productId + '\'' + + ", bezeichnung='" + bezeichnung + '\'' + + ", einzelpreisNetto=" + einzelpreisNetto + + ", mwstSatz=" + mwstSatz + + ", kategorie='" + kategorie + '\'' + + '}'; + } +} diff --git a/src/main/java/de/hsmannheim/faktura/product/repository/ProductRepository.java b/src/main/java/de/hsmannheim/faktura/product/repository/ProductRepository.java new file mode 100644 index 0000000..9958285 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/product/repository/ProductRepository.java @@ -0,0 +1,150 @@ +package de.hsmannheim.faktura.product.repository; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import de.hsmannheim.faktura.product.Product; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Datenhaltungsschicht (Persistenz) für das Modul Produktverwaltung (Gruppe G)[cite: 51, 71]. + * Realisiert die datenbanklose Speicherung von Produkt-Entitäten im lokalen Dateisystem[cite: 13, 71]. + * Um die Antwortzeiten (NF-SH-PERF-01) zu minimieren, werden alle Lesezugriffe über einen + * In-Memory-Arbeitsspeicher-Cache abgewickelt, welcher synchron zur JSON-Datei gehalten wird[cite: 16, 44, 71]. + * Folgt den Architekturvorgaben NF-SH-MAINT-01 (Schichtentrennung) und NF-SH-ARCH-01 (Verlustfreie Speicherung)[cite: 50, 51]. + */ +public class ProductRepository { + + /** + * Relativer Systempfad zur lokalen JSON-Datei im Projektverzeichnis[cite: 16, 61]. + */ + private static final String FILE_PATH = "./data/products.json"; + + /** + * Zentraler Serialisierungs-Dienst des Jackson-Frameworks für die JSON-Objekt-Abbildung. + */ + private final ObjectMapper objectMapper; + + /** + * Lokaler Cache im Arbeitsspeicher zur Reduzierung von I/O-Festplattenzugriffen[cite: 16, 71]. + */ + private List cache; + + /** + * Initialisiert das Repository, konfiguriert das JSON-Ausgabeformat und + * lädt den bestehenden Datenbestand direkt beim Anwendungsstart in den Cache[cite: 16, 36]. + */ + public ProductRepository() { + this.objectMapper = new ObjectMapper(); + + // Aktiviert das Pretty-Printing für eine menschenlesbare Strukturierung der JSON-Datei + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + this.cache = new ArrayList<>(); + + loadData(); + } + + /** + * Gibt eine entkoppelte Kopie des aktuellen Zustands aller Produkte zurück. + * Schützt den internen Cache vor unkontrollierten Modifikationen von außen. + * + * @return Eine {@link List} aller im System registrierten {@link Product}-Objekte[cite: 36, 87]. + */ + public List findAll() { + return new ArrayList<>(cache); + } + + /** + * Sucht ein Produkt anhand des eindeutigen Identifikators im Cache[cite: 34, 103]. + * + * @param productId Der eindeutige Primärschlüssel des Produkts [cite: 34, 392] + * @return Ein {@link Optional}, das die Produkt-Entität umschließt, sofern gefunden [cite: 88, 99] + */ + public Optional findById(String productId) { + return cache.stream() + .filter(p -> p.getProductId().equals(productId)) + .findFirst(); + } + + /** + * Speichert oder aktualisiert ein Produkt im System (Idempotente Operation). + * Existiert die ID bereits, wird der alte Datensatz im Cache überschrieben[cite: 29]. + * Andernfalls wird ein neuer Eintrag angelegt[cite: 17, 28]. + * Nach der Cache-Modifikation wird der Zustand sofort atomar in der JSON-Datei persistiert[cite: 29, 61]. + * + * @param product Die zu speichernde oder zu aktualisierende Produkt-Entität [cite: 28, 29, 89] + */ + public void save(Product product) { + Optional existingProduct = findById(product.getProductId()); + + if (existingProduct.isPresent()) { + int index = cache.indexOf(existingProduct.get()); + cache.set(index, product); + } else { + cache.add(product); + } + + saveData(); + } + + /** + * Entfernt ein Produkt anhand seiner ID aus dem lokalen Cache und der Datei[cite: 17, 61]. + * Der Aufruf sollte erst nach erfolgreicher Referenzprüfung (F-SH-PV-03) erfolgen[cite: 33, 34]. + * + * @param productId Der eindeutige Identifikator des zu löschenden Produkts [cite: 34, 98] + * @return {@code true}, wenn das Produkt im Cache existierte und entfernt wurde; andernfalls {@code false} [cite: 90] + */ + public boolean deleteById(String productId) { + boolean removed = cache.removeIf(p -> p.getProductId().equals(productId)); + if (removed) { + saveData(); + } + return removed; + } + + /** + * Deserialisiert den Inhalt der lokalen JSON-Datei in den Arbeitsspeicher[cite: 16, 61]. + * Erstellt eine leere Struktur, falls die Datei noch nicht existiert oder korrupt ist, + * um Ausfälle während des Programmstarts (Kaltstart-Performance) zu verhindern[cite: 375]. + */ + private void loadData() { + File file = new File(FILE_PATH); + + if (!file.exists()) { + this.cache = new ArrayList<>(); + return; + } + + try { + this.cache = objectMapper.readValue(file, new TypeReference>() {}); + } catch (IOException e) { + System.err.println("Fehler beim Laden der Produktdaten: " + e.getMessage()); + this.cache = new ArrayList<>(); + } + } + + /** + * Serialisiert den gesamten Arbeitsspeicher-Cache synchron zurück in das Dateisystem[cite: 61, 71]. + * Stellt automatisch sicher, dass die übergeordneten Verzeichnisstrukturen (`./data/`) + * existieren, bevor der Schreibvorgang initiiert wird. + */ + private void saveData() { + File file = new File(FILE_PATH); + + File directory = file.getParentFile(); + if (directory != null && !directory.exists()) { + directory.mkdirs(); + } + + try { + objectMapper.writeValue(file, cache); + } catch (IOException e) { + System.err.println("Fehler beim Speichern der Produktdaten: " + e.getMessage()); + } + } +} \ 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 new file mode 100644 index 0000000..a1f9c61 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java @@ -0,0 +1,121 @@ +package de.hsmannheim.faktura.product.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +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]. + * 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]. + */ +public class ProductService { + + /** + * Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung[cite: 71, 85]. + */ + private final ProductRepository productRepository; + + /** + * Konstruktor zur Erstellung des ProductService mittels Dependency Injection. + * + * @param productRepository Das zu verwendende Repository für Datenzugriffe[cite: 85]. + */ + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + /** + * 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]. + * + * @return Eine Liste von {@link Product}-Objekten[cite: 87]. + */ + public List getAllProducts() { + return productRepository.findAll(); + } + + /** + * Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID[cite: 88]. + * + * @param productId Die eindeutige Kennung des gesuchten Produkts [cite: 88] + * @return Ein {@link Optional}, das das gefundene Produkt enthält, oder leer ist + */ + public Optional getProductById(String productId) { + return productRepository.findById(productId); + } + + /** + * Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz[cite: 28, 29, 58, 59, 60]. + *

+ * Validierungen gemäß Pflichtenheft: + *

    + *
  • Überprüfung auf Null-Objekte.
  • + *
  • Anforderung F-SH-PV-06: Der Netto-Einzelpreis darf nicht negativ (< 0.00) sein[cite: 38].
  • + *
+ * + * @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] + */ + 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.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.getProductId() == null || product.getProductId().trim().isEmpty()) { + product.setProductId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8)); + } + + productRepository.save(product); + } + + /** + * 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] + * @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]. + */ + public boolean deleteProduct(String productId) { + if (productId == null || productId.trim().isEmpty()) { + 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] + */ + public List searchProducts(String query) { + if (query == null || query.trim().isEmpty()) { + return getAllProducts(); + } + String lowerQuery = query.toLowerCase(); + return getAllProducts().stream() + .filter(p -> p.getBezeichnung() != null && p.getBezeichnung().toLowerCase().contains(lowerQuery)) + .collect(java.util.stream.Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java b/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java deleted file mode 100644 index 8b7f93e..0000000 --- a/src/main/java/de/hsmannheim/faktura/produkt/MwstSatz.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.hsmannheim.faktura.produkt; - -public enum MwstSatz { - - NORMAL, - ERMAESSIGT -} - diff --git a/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java b/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java deleted file mode 100644 index 0618dff..0000000 --- a/src/main/java/de/hsmannheim/faktura/produkt/Produkt.java +++ /dev/null @@ -1,91 +0,0 @@ -package de.hsmannheim.faktura.produkt; - -import java.math.BigDecimal; - -public class Produkt { - - private String produktId; - private String bezeichnung; - private BigDecimal einzelpreisNetto; - private MwstSatz mwstSatz; - private String beschreibung; - private String kategorie; - - public Produkt() { - - } - - public Produkt(String produktId, String bezeichnung, BigDecimal einzelpreisNetto, - MwstSatz mwstSatz, String beschreibung, String kategorie) { - this.produktId = produktId; - this.bezeichnung = bezeichnung; - this.einzelpreisNetto = einzelpreisNetto; - this.mwstSatz = mwstSatz; - this.beschreibung = beschreibung; - this.kategorie = kategorie; - } - - // ========================================== - // Getter und Setter - // ========================================== - - public String getProduktId() { - return produktId; - } - - public void setProduktId(String produktId) { - this.produktId = produktId; - } - - public String getBezeichnung() { - return bezeichnung; - } - - public void setBezeichnung(String bezeichnung) { - this.bezeichnung = bezeichnung; - } - - public BigDecimal getEinzelpreisNetto() { - return einzelpreisNetto; - } - - public void setEinzelpreisNetto(BigDecimal einzelpreisNetto) { - this.einzelpreisNetto = einzelpreisNetto; - } - - public MwstSatz getMwstSatz() { - return mwstSatz; - } - - public void setMwstSatz(MwstSatz mwstSatz) { - this.mwstSatz = mwstSatz; - } - - public String getBeschreibung() { - return beschreibung; - } - - public void setBeschreibung(String beschreibung) { - this.beschreibung = beschreibung; - } - - public String getKategorie() { - return kategorie; - } - - public void setKategorie(String kategorie) { - this.kategorie = kategorie; - } - - @Override - public String toString() { - return "Produkt{" + - "produktId='" + produktId + '\'' + - ", bezeichnung='" + bezeichnung + '\'' + - ", einzelpreisNetto=" + einzelpreisNetto + - ", mwstSatz=" + mwstSatz + - ", kategorie='" + kategorie + '\'' + - '}'; - } -} - diff --git a/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java b/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java deleted file mode 100644 index 80f2f49..0000000 --- a/src/main/java/de/hsmannheim/faktura/produkt/repository/ProduktRepository.java +++ /dev/null @@ -1,101 +0,0 @@ -package de.hsmannheim.faktura.produkt.repository; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import de.hsmannheim.faktura.produkt.Produkt; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - - -public class ProduktRepository { - - - private static final String FILE_PATH = "./data/products.json"; - private final ObjectMapper objectMapper; - private List cache; - - public ProduktRepository() { - this.objectMapper = new ObjectMapper(); - this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); - this.cache = new ArrayList<>(); - - - loadData(); - } - - - public List findAll() { - return new ArrayList<>(cache); - } - - - public Optional findById(String produktId) { - return cache.stream() - .filter(p -> p.getProduktId().equals(produktId)) - .findFirst(); - } - - public void save(Produkt product) { - - Optional existingProduct = findById(product.getProduktId()); - - if (existingProduct.isPresent()) { - int index = cache.indexOf(existingProduct.get()); - cache.set(index, product); - } else { - cache.add(product); - } - - - saveData(); - } - - - public boolean deleteById(String produktId) { - boolean removed = cache.removeIf(p -> p.getProduktId().equals(produktId)); - if (removed) { - saveData(); - } - return removed; - } - - - private void loadData() { - File file = new File(FILE_PATH); - - - if (!file.exists()) { - this.cache = new ArrayList<>(); - return; - } - - try { - this.cache = objectMapper.readValue(file, new TypeReference>() {}); - } catch (IOException e) { - System.err.println("Fehler beim Laden der Produktdaten: " + e.getMessage()); - - this.cache = new ArrayList<>(); - } - } - - - private void saveData() { - File file = new File(FILE_PATH); - - File directory = file.getParentFile(); - if (directory != null && !directory.exists()) { - directory.mkdirs(); - } - - try { - objectMapper.writeValue(file, cache); - } catch (IOException e) { - System.err.println("Fehler beim Speichern der Produktdaten: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java b/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java deleted file mode 100644 index f496af9..0000000 --- a/src/main/java/de/hsmannheim/faktura/produkt/service/ProduktService.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.hsmannheim.faktura.produkt.service; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Optional; - -import de.hsmannheim.faktura.produkt.Produkt; -import de.hsmannheim.faktura.produkt.repository.ProduktRepository; - - -public class ProduktService { - - private final ProduktRepository produktRepository; - - public ProduktService(ProduktRepository produktRepository) { - this.produktRepository = produktRepository; - } - - - public List getAllProducts() { - return produktRepository.findAll(); - } - - - public Optional getProductById(String produktId) { - return produktRepository.findById(produktId); - } - - - public void saveProduct(Produkt product) { - if (product == null) { - throw new IllegalArgumentException("Das Produkt darf nicht null 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."); - } - - if (product.getProduktId() == null || product.getProduktId().trim().isEmpty()) { - - product.setProduktId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8)); - } - - produktRepository.save(product); - } - - public boolean deleteProduct(String produktId) { - if (produktId == null || produktId.trim().isEmpty()) { - return false; - } - - return produktRepository.deleteById(produktId); - } - - public List searchProducts(String query) { - if (query == null || query.trim().isEmpty()) { - return getAllProducts(); - } - String lowerQuery = query.toLowerCase(); - return getAllProducts().stream() - .filter(p -> p.getBezeichnung() != null && p.getBezeichnung().toLowerCase().contains(lowerQuery)) - .collect(java.util.stream.Collectors.toList()); -} -} \ 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 new file mode 100644 index 0000000..b69028e --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.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.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.product.MwstSatz; +import de.hsmannheim.faktura.product.Product; +import de.hsmannheim.faktura.product.repository.ProductRepository; + +public class ProductServiceTest { + + private ProductService productService; + private ProductRepository productRepository; + + @BeforeEach + public void setUp() { + productRepository = new ProductRepository(); + productService = new ProductService(productRepository); + + // Cache leeren, damit alte JSON-Einträge die Tests nicht verfälschen + for (Product p : productRepository.findAll()) { + productRepository.deleteById(p.getProductId()); + } + } + + @Test + public void testSaveValidProduct() { + Product valid = new Product("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug"); + assertDoesNotThrow(() -> productService.saveProduct(valid)); + assertTrue(productService.getProductById("P-0001").isPresent()); + } + + @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"); + + // Kompakt ohne Rumpf-Klammern löst die Warnung auf + assertThrows(IllegalArgumentException.class, () -> productService.saveProduct(invalidProduct)); + } + + @Test + public void testSearchCaseInsensitive() { + // AC-SH-PV-03: Suchtest für Groß-/Kleinschreibung + Product p1 = new Product("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", ""); + Product p2 = new Product("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", ""); + + productService.saveProduct(p1); + productService.saveProduct(p2); + + List results = productService.searchProducts("schraube"); + assertEquals(2, results.size()); + } +} \ No newline at end of file diff --git a/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java b/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java deleted file mode 100644 index 01fd6a1..0000000 --- a/src/test/java/de/hsmannheim/faktura/produkt/service/ProduktServiceTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.hsmannheim.faktura.produkt.service; - -import de.hsmannheim.faktura.produkt.Produkt; -import de.hsmannheim.faktura.produkt.MwstSatz; -import de.hsmannheim.faktura.produkt.repository.ProduktRepository; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -public class ProduktServiceTest { - - private ProduktService produktService; - private ProduktRepository produktRepository; - - @BeforeEach - public void setUp() { - produktRepository = new ProduktRepository(); - produktService = new ProduktService(produktRepository); - - // Cache leeren, damit alte JSON-Einträge die Tests nicht verfälschen - for (Produkt p : produktRepository.findAll()) { - produktRepository.deleteById(p.getProduktId()); - } - } - - @Test - public void testSaveValidProduct() { - Produkt valid = new Produkt("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug"); - assertDoesNotThrow(() -> produktService.saveProduct(valid)); - assertTrue(produktService.getProductById("P-0001").isPresent()); - } - - @Test - public void testNegativePriceRejection() { - // AC-SH-PV-01: Preis kleiner als 0.00 muss abgelehnt werden - Produkt invalidProduct = new Produkt("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test"); - - assertThrows(IllegalArgumentException.class, () -> produktService.saveProduct(invalidProduct)); - } - - @Test - public void testSearchCaseInsensitive() { - // AC-SH-PV-03: Suchtest für Groß-/Kleinschreibung - Produkt p1 = new Produkt("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", ""); - Produkt p2 = new Produkt("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", ""); - - produktService.saveProduct(p1); - produktService.saveProduct(p2); - - List results = produktService.searchProducts("schraube"); - assertEquals(2, results.size()); - } -} \ No newline at end of file -- 2.51.0 From 6e5a6b5329b761407bf566547dc45c2e3c1e96b6 Mon Sep 17 00:00:00 2001 From: Lulia <3030489@stud.th-mannheim.de> Date: Sat, 27 Jun 2026 22:58:17 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feat(product):=20Kernkomponenten=20implemen?= =?UTF-8?q?tiert,=20JavaDoc=20erg=C3=A4nzt=20und=20Unit-Tests=20hinzugef?= =?UTF-8?q?=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/products.json | 16 +-------- .../product/service/ProductServiceTest.java | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/data/products.json b/data/products.json index 787b843..8878e54 100644 --- a/data/products.json +++ b/data/products.json @@ -1,15 +1 @@ -[ { - "productId" : "P-0001", - "bezeichnung" : "M6 Schraube", - "einzelpreisNetto" : 1.00, - "mwstSatz" : "NORMAL", - "beschreibung" : "", - "kategorie" : "" -}, { - "productId" : "P-0002", - "bezeichnung" : "M8 SCHRAUBE", - "einzelpreisNetto" : 2.00, - "mwstSatz" : "NORMAL", - "beschreibung" : "", - "kategorie" : "" -} ] \ No newline at end of file +[ ] \ 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 b69028e..94fc34d 100644 --- a/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java +++ b/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java @@ -14,47 +14,72 @@ import de.hsmannheim.faktura.product.MwstSatz; import de.hsmannheim.faktura.product.Product; 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]. + */ + public class ProductServiceTest { private ProductService productService; private ProductRepository productRepository; + /** + * 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 Tests nicht verfälschen + // Cache leeren, damit alte JSON-Einträge die Testergebnisse nicht verfälschen for (Product p : productRepository.findAll()) { productRepository.deleteById(p.getProductId()); } } + /** + * Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen. + * Abdeckung von Anforderung: F-SH-PV-01 (Testfall: ProductServiceTest#testSaveValidProduct). + */ @Test public void testSaveValidProduct() { Product valid = new Product("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug"); + assertDoesNotThrow(() -> productService.saveProduct(valid)); assertTrue(productService.getProductById("P-0001").isPresent()); } - @Test + /** + * 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]. + */ + @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"); - // Kompakt ohne Rumpf-Klammern löst die Warnung auf - assertThrows(IllegalArgumentException.class, () -> productService.saveProduct(invalidProduct)); + assertThrows(IllegalArgumentException.class, () -> { + productService.saveProduct(invalidProduct); + }); } + /** + * Ü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]. + */ @Test public void testSearchCaseInsensitive() { - // AC-SH-PV-03: Suchtest für Groß-/Kleinschreibung Product p1 = new Product("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", ""); Product p2 = new Product("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", ""); 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()); } -- 2.51.0 From 16ef33a1c9aeb46f2552b161eed7f2d6d95b1b51 Mon Sep 17 00:00:00 2001 From: Lulia <3030489@stud.th-mannheim.de> Date: Sun, 28 Jun 2026 00:21:06 +0200 Subject: [PATCH 4/4] feat(product): Testabdeckung und Validierungen gemaess Pflichtenheft vervollstaendigt --- data/products.json | 9 +- .../product/service/ProductService.java | 96 ++++++----- .../repository/ProductRepositoryTest.java | 63 +++++++ .../service/ProductPerformanceTest.java | 61 +++++++ .../product/service/ProductServiceTest.java | 157 ++++++++++++++++-- 5 files changed, 331 insertions(+), 55 deletions(-) create mode 100644 src/test/java/de/hsmannheim/faktura/product/repository/ProductRepositoryTest.java create mode 100644 src/test/java/de/hsmannheim/faktura/product/service/ProductPerformanceTest.java 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: - *

    - *
  • Überprüfung auf Null-Objekte.
  • - *
  • Anforderung F-SH-PV-06: Der Netto-Einzelpreis darf nicht negativ (< 0.00) sein[cite: 38].
  • - *
- * - * @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 -- 2.51.0