diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..1d70858 --- /dev/null +++ b/data/products.json @@ -0,0 +1,8 @@ +[ { + "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/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..4763e77 --- /dev/null +++ b/src/main/java/de/hsmannheim/faktura/product/service/ProductService.java @@ -0,0 +1,135 @@ +package de.hsmannheim.faktura.product.service; + +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). + * Dient als zentrale programminterne API für die Schnittstellen zur Programmoberfläche (Gruppe E) + * 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. + */ + 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 + * @param dokumentRepository Das zu verwendende Repository für Dokumentendaten + */ + public ProductService(ProductRepository productRepository, DokumentRepository dokumentRepository) { + this.productRepository = productRepository; + this.dokumentRepository = dokumentRepository; + } + + /** + * 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. + */ + public List getAllProducts() { + return productRepository.findAll(); + } + + /** + * Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID. + * + * @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) { + return productRepository.findById(productId); + } + + /** + * 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."); + } + + 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."); + } + + 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)); + } + + productRepository.save(product); + } + + /** + * 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 + */ + 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. + * * @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()) { + 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/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 new file mode 100644 index 0000000..e03d1a6 --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/product/service/ProductServiceTest.java @@ -0,0 +1,217 @@ +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; + +/** + * 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). + */ +@SuppressWarnings("all") +public class ProductServiceTest { + + private ProductService productService; + private ProductRepository productRepository; + private de.hsmannheim.faktura.dokumente.repository.DokumentRepository dokumentRepository; + + @BeforeEach + public void setUp() { + productRepository = new ProductRepository(); + + 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()); + } + } + + /** + * Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen. + * Abdeckung von Anforderung: F-SH-PV-01. + */ + @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()); + } + + /** + * 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. + */ + @Test + public void testNegativePriceRejection() { + Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test"); + + assertThrows(IllegalArgumentException.class, () -> { + productService.saveProduct(invalidProduct); + }); + } + + /** + * Ü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. + */ + @Test + public void testSearchCaseInsensitive() { + 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()); + } + + /** + * Ü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