Compare commits

..

6 Commits

8 changed files with 800 additions and 0 deletions

View File

@ -0,0 +1,8 @@
[ {
"productId" : "P-0003",
"bezeichnung" : "Bohrmaschine",
"einzelpreisNetto" : 89.99,
"mwstSatz" : "NORMAL",
"beschreibung" : "Schlagbohrer",
"kategorie" : "Elektrowerkzeuge"
} ]

View File

@ -0,0 +1,10 @@
package de.hsmannheim.faktura.product;
public enum MwstSatz {
/**
* Enumeration zur Repräsentation der Mehrwertsteuersätze in Deutschland.
*/
NORMAL,
ERMAESSIGT
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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<Product> 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<Product> 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<Product> 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<Product> 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<List<Product>>() {});
} 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());
}
}
}

View File

@ -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<Product> 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<Product> 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<Product> 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());
}
}

View File

@ -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<Product> 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());
}
}

View File

@ -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<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> 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<Product> 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");
}
}

View File

@ -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<Dokument> findByNummer(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'findByNummer'");
}
@Override
public List<Dokument> 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<Product> 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<Product> 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<Product> 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<Dokument> findByNummer(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'findByNummer'");
}
@Override
public List<Dokument> 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.");
}
}