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] =?UTF-8?q?Refactoring:=20Ordnerstrukturen=20angepasst=20u?= =?UTF-8?q?nd=20sinnvolle=20Dokumentationskommentare=20hinzugef=C3=BCgt?= 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