From b04757f9db5bf7d6ae93b3d3ed6e5bb72b057c5d Mon Sep 17 00:00:00 2001 From: Andreas Ivanovic <3028874@stud.th-mannheim.de> Date: Sun, 28 Jun 2026 08:02:19 +0200 Subject: [PATCH] =?UTF-8?q?test:=20DokumentServiceTest=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DokumentServiceTest.java | 498 ++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 src/test/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceTest.java diff --git a/src/test/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceTest.java b/src/test/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceTest.java new file mode 100644 index 0000000..44d638f --- /dev/null +++ b/src/test/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceTest.java @@ -0,0 +1,498 @@ +package de.hsmannheim.faktura.dokumente.service; + +import de.hsmannheim.faktura.dokumente.domain.*; +import de.hsmannheim.faktura.dokumente.exception.*; +import de.hsmannheim.faktura.dokumente.export.PdfExportService; +import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService; +import de.hsmannheim.faktura.dokumente.lookup.*; +import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; + +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Modultestplan für den Dokumentenprozess (Gruppe F). + * Alle 16 Testfälle MT-DP-01 bis MT-DP-16 aus dem Pflichtenheft. + * + * Keine JavaFX-Abhängigkeit – alle Tests laufen ohne GUI-Start (PH-DP-16, PH-NF-DP-05). + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DokumentServiceTest { + + // ------------------------------------------------------------------------- + // Test-Infrastruktur: Fake-Implementierungen + // ------------------------------------------------------------------------- + + /** Fake-Produktverwaltung mit festen Testprodukten (MT-DP-01). */ + static class FakeProductLookup implements ProductLookup { + private final Map produkte = new HashMap<>(); + + FakeProductLookup() { + produkte.put(20001L, new ProductDto(20001L, "Produkt Alpha", + new BigDecimal("100.00"), new BigDecimal("0.19"))); + produkte.put(20002L, new ProductDto(20002L, "Produkt Beta", + new BigDecimal("50.00"), new BigDecimal("0.07"))); + produkte.put(20003L, new ProductDto(20003L, "Produkt Gamma", + new BigDecimal("200.00"), new BigDecimal("0.19"))); + } + + @Override + public Optional findProduct(long produktId) { + return Optional.ofNullable(produkte.get(produktId)); + } + + /** Ändert den Preis eines Produkts – für MT-DP-13 (Snapshot-Test). */ + void setztePreis(long produktId, BigDecimal neuerPreis) { + ProductDto alt = produkte.get(produktId); + if (alt != null) { + produkte.put(produktId, new ProductDto( + alt.getProduktId(), alt.getBezeichnung(), neuerPreis, alt.getMwstSatz())); + } + } + } + + /** Fake-Kundenverwaltung mit festen Testkunden. */ + static class FakeCustomerLookup implements CustomerLookup { + private final Map kunden = new HashMap<>(); + + FakeCustomerLookup() { + kunden.put(10001L, new CustomerDto(10001L, "Max Mustermann", + "Musterweg 5, 68159 Mannheim", "DE123456789")); + kunden.put(10002L, new CustomerDto(10002L, "Erika Muster", + "Beispielgasse 3, 68161 Mannheim", null)); + // Kunde 10002 hat keine Steuernummer → für MT-DP-10 (fehlende Pflichtangaben) + } + + @Override + public Optional findCustomer(long kundenId) { + return Optional.ofNullable(kunden.get(kundenId)); + } + } + + // ------------------------------------------------------------------------- + // Setup + // ------------------------------------------------------------------------- + + private InMemoryDokumentRepository repository; + private FakeProductLookup productLookup; + private FakeCustomerLookup customerLookup; + private DokumentService service; + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + repository = new InMemoryDokumentRepository(); + productLookup = new FakeProductLookup(); + customerLookup = new FakeCustomerLookup(); + PdfExportService pdfExport = new SimplePdfExportService(tempDir); + service = new DokumentServiceImpl(repository, productLookup, customerLookup, pdfExport); + } + + // ========================================================================= + // MT-DP-01: Angebot anlegen – Happy Path (PH-DP-01) + // ========================================================================= + @Test + @Order(1) + @DisplayName("MT-DP-01: Angebot anlegen – Happy Path") + void testAngebotAnlegen_HappyPath() { + List positionen = List.of( + new PositionCommand(20001L, 2), + new PositionCommand(20002L, 1) + ); + + Angebot angebot = service.erstelleAngebot(10001L, positionen); + + assertNotNull(angebot, "Angebot darf nicht null sein"); + assertTrue(angebot.getDokumentNummer().startsWith("A-2026-"), + "Nummer muss mit A-2026- beginnen, war: " + angebot.getDokumentNummer()); + assertEquals(DokumentStatus.OFFEN, angebot.getStatus(), "Status muss OFFEN sein"); + assertEquals(2, angebot.getPositionen().size(), "Muss genau 2 Positionen haben"); + assertEquals(10001L, angebot.getKundenId(), "Kunden-ID muss stimmen"); + assertNotNull(angebot.getDatum(), "Datum darf nicht null sein"); + } + + // ========================================================================= + // MT-DP-02: Summenberechnung Angebot (PH-DP-01, PH-DP-08) + // ========================================================================= + @Test + @Order(2) + @DisplayName("MT-DP-02: Summenberechnung – Netto 250,00, USt 41,50, Brutto 291,50") + void testSummenberechnung_Angebot() { + // 2 x 100,00 EUR mit 19%, 1 x 50,00 EUR mit 7% + List positionen = List.of( + new PositionCommand(20001L, 2), // 2 x 100 @ 19% + new PositionCommand(20002L, 1) // 1 x 50 @ 7% + ); + + Angebot angebot = service.erstelleAngebot(10001L, positionen); + + assertEquals(0, new BigDecimal("250.00").compareTo(angebot.getNettoSumme()), + "Nettosumme muss 250,00 EUR sein, war: " + angebot.getNettoSumme()); + assertEquals(0, new BigDecimal("41.50").compareTo(angebot.getUstSumme()), + "USt-Summe muss 41,50 EUR sein, war: " + angebot.getUstSumme()); + assertEquals(0, new BigDecimal("291.50").compareTo(angebot.getBruttoSumme()), + "Bruttosumme muss 291,50 EUR sein, war: " + angebot.getBruttoSumme()); + } + + // ========================================================================= + // MT-DP-03: Fortlaufende Angebotsnummern (PH-DP-02) + // ========================================================================= + @Test + @Order(3) + @DisplayName("MT-DP-03: Angebotsnummern sind fortlaufend und eindeutig") + void testFortlaufendeAngebotsnummern() { + List pos = List.of(new PositionCommand(20001L, 1)); + + Angebot a1 = service.erstelleAngebot(10001L, pos); + Angebot a2 = service.erstelleAngebot(10001L, pos); + + assertNotEquals(a1.getDokumentNummer(), a2.getDokumentNummer(), + "Nummern müssen unterschiedlich sein"); + assertTrue(a1.getDokumentNummer().endsWith("0001"), + "Erste Nummer muss auf 0001 enden, war: " + a1.getDokumentNummer()); + assertTrue(a2.getDokumentNummer().endsWith("0002"), + "Zweite Nummer muss auf 0002 enden, war: " + a2.getDokumentNummer()); + } + + // ========================================================================= + // MT-DP-04: Angebot nach Überführung sperren (PH-DP-03) + // ========================================================================= + @Test + @Order(4) + @DisplayName("MT-DP-04: Überführtes Angebot kann nicht mehr bearbeitet werden") + void testAngebotNachUeberfuehrungGesperrt() { + List pos = List.of(new PositionCommand(20001L, 1)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + + // Versuch Angebot nach Überführung zu bearbeiten → IllegalStateException + assertThrows(IllegalStateException.class, + () -> service.aktualisiereAngebot(angebot.getDokumentNummer(), pos), + "Bearbeitung eines überführten Angebots muss IllegalStateException werfen"); + } + + // ========================================================================= + // MT-DP-05: Auftragsbestätigung erzeugen (PH-DP-04) + // ========================================================================= + @Test + @Order(5) + @DisplayName("MT-DP-05: Auftragsbestätigung aus offenem Angebot erzeugen") + void testAuftragsbestaetigung_Erzeugen() { + List pos = List.of( + new PositionCommand(20001L, 2), + new PositionCommand(20002L, 1) + ); + Angebot angebot = service.erstelleAngebot(10001L, pos); + + Auftragsbestaetigung ab = service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + + assertNotNull(ab, "AB darf nicht null sein"); + assertTrue(ab.getDokumentNummer().startsWith("AB-"), + "AB-Nummer muss mit AB- beginnen"); + assertEquals(angebot.getDokumentNummer(), ab.getAngebotsNummer(), + "AB muss Angebotsnummer referenzieren"); + assertEquals(2, ab.getPositionen().size(), + "AB muss dieselbe Anzahl Positionen wie Angebot haben"); + assertEquals(DokumentStatus.UEBERFUEHRT, + ((Angebot) repository.findByNummer(angebot.getDokumentNummer()).get()).getStatus(), + "Angebot muss nach AB-Erstellung auf UEBERFUEHRT stehen"); + } + + // ========================================================================= + // MT-DP-06: Doppelte AB verhindern (PH-DP-05) + // ========================================================================= + @Test + @Order(6) + @DisplayName("MT-DP-06: Zweite AB für dasselbe Angebot wird abgelehnt") + void testDoppelteAuftragsbestaetigung_Verhindern() { + List pos = List.of(new PositionCommand(20001L, 1)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + + // Zweiter Versuch → DuplicateDocumentException + assertThrows(DuplicateDocumentException.class, + () -> service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()), + "Zweite AB für dasselbe Angebot muss DuplicateDocumentException werfen"); + } + + // ========================================================================= + // MT-DP-07: Lieferschein erzeugen (PH-DP-06) + // ========================================================================= + @Test + @Order(7) + @DisplayName("MT-DP-07: Lieferschein aus bestätigter AB erzeugen") + void testLieferschein_Erzeugen() { + List pos = List.of(new PositionCommand(20001L, 3)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + Auftragsbestaetigung ab = service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + LocalDate lieferdatum = LocalDate.now().plusDays(3); + + Lieferschein ls = service.erstelleLieferschein(ab.getDokumentNummer(), lieferdatum); + + assertNotNull(ls, "Lieferschein darf nicht null sein"); + assertTrue(ls.getDokumentNummer().startsWith("LS-"), + "LS-Nummer muss mit LS- beginnen"); + assertEquals(ab.getDokumentNummer(), ls.getAuftragsNummer(), + "Lieferschein muss AB-Nummer referenzieren"); + assertEquals(lieferdatum, ls.getLieferdatum(), "Lieferdatum muss übereinstimmen"); + assertEquals(DokumentStatus.GELIEFERT, ls.getStatus(), "Status muss GELIEFERT sein"); + assertEquals(1, ls.getPositionen().size(), "Muss 1 Position enthalten"); + } + + // ========================================================================= + // MT-DP-08: Lieferschein enthält keine Preise (PH-DP-07) + // ========================================================================= + @Test + @Order(8) + @DisplayName("MT-DP-08: Lieferschein-Export enthält keine Preisfelder") + void testLieferschein_EnthältKeinePreise() throws Exception { + List pos = List.of(new PositionCommand(20001L, 2)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + Auftragsbestaetigung ab = service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + Lieferschein ls = service.erstelleLieferschein(ab.getDokumentNummer(), LocalDate.now()); + + // PDF exportieren und Inhalt prüfen + Path pdfPfad = service.exportiereAlsPdf(ls.getDokumentNummer()); + String inhalt = Files.readString(pdfPfad); + + assertTrue(inhalt.contains("Produkt Alpha"), "Bezeichnung muss enthalten sein"); + assertFalse(inhalt.contains("Nettosumme:"), "Nettosumme darf nicht enthalten sein"); + assertFalse(inhalt.contains("Bruttosumme:"), "Bruttosumme darf nicht enthalten sein"); + assertFalse(inhalt.contains("USt:"), "USt-Betrag darf nicht enthalten sein"); + } + + // ========================================================================= + // MT-DP-09: Rechnung erzeugen – Happy Path (PH-DP-08, PH-DP-09) + // ========================================================================= + @Test + @Order(9) + @DisplayName("MT-DP-09: Rechnung aus Lieferschein erzeugen (Happy Path)") + void testRechnung_Erzeugen_HappyPath() { + Lieferschein ls = erstelleKompletteDokumentenkette(10001L); + + Rechnung rechnung = service.erstelleRechnung(ls.getDokumentNummer()); + + assertAll("Rechnung muss vollständig sein", + () -> assertNotNull(rechnung, "Rechnung darf nicht null sein"), + () -> assertTrue(rechnung.getDokumentNummer().startsWith("R-"), + "Rechnungsnummer muss mit R- beginnen"), + () -> assertEquals(DokumentStatus.FESTGESCHRIEBEN, rechnung.getStatus(), + "Status muss FESTGESCHRIEBEN sein"), + () -> assertTrue(rechnung.isFestgeschrieben(), "Rechnung muss festgeschrieben sein"), + () -> assertNotNull(rechnung.getPflichtangaben(), "Pflichtangaben dürfen nicht null sein"), + () -> assertEquals(0, new BigDecimal("250.00").compareTo(rechnung.getNettoSumme()), + "Nettosumme muss 250,00 EUR sein"), + () -> assertEquals(0, new BigDecimal("291.50").compareTo(rechnung.getBruttoSumme()), + "Bruttosumme muss 291,50 EUR sein") + ); + } + + // ========================================================================= + // MT-DP-10: Pflichtangaben fehlen (PH-DP-09) + // ========================================================================= + @Test + @Order(10) + @DisplayName("MT-DP-10: Fehlende Pflichtangaben verhindern Rechnungserstellung") + void testRechnung_PflichtangabenFehlen() { + // Wir manipulieren den CustomerLookup so, dass er einen Kunden + // mit leerer Anschrift zurückgibt → Validierung schlägt fehl + CustomerLookup schlechterLookup = id -> + Optional.of(new CustomerDto(id, "", "", null)); + + DokumentService serviceOhneKunde = new DokumentServiceImpl( + repository, productLookup, schlechterLookup, new SimplePdfExportService(tempDir)); + + List pos = List.of(new PositionCommand(20001L, 2), + new PositionCommand(20002L, 1)); + + // Erst Angebot → AB → LS aufbauen + Angebot angebot = serviceOhneKunde.erstelleAngebot(10001L, pos); + Auftragsbestaetigung ab = serviceOhneKunde.erstelleAuftragsbestaetigung( + angebot.getDokumentNummer()); + Lieferschein ls = serviceOhneKunde.erstelleLieferschein( + ab.getDokumentNummer(), LocalDate.now()); + + // Rechnung erstellen → soll ValidationException werfen + assertThrows(ValidationException.class, + () -> serviceOhneKunde.erstelleRechnung(ls.getDokumentNummer()), + "Rechnung mit fehlenden Pflichtangaben muss ValidationException werfen"); + } + + // ========================================================================= + // MT-DP-11: Rechnungsnummern lückenlos (PH-DP-10) + // ========================================================================= + @Test + @Order(11) + @DisplayName("MT-DP-11: Drei Rechnungen erhalten Nummern 0001, 0002, 0003") + void testRechnungsnummern_Lueckenlos() { + List pos = List.of( + new PositionCommand(20001L, 2), + new PositionCommand(20002L, 1) + ); + + // Drei vollständige Dokumentenketten erzeugen + List rechnungsNummern = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Lieferschein ls = erstelleKompletteDokumentenkette(10001L); + Rechnung rechnung = service.erstelleRechnung(ls.getDokumentNummer()); + rechnungsNummern.add(rechnung.getDokumentNummer()); + } + + assertTrue(rechnungsNummern.get(0).endsWith("0001"), + "Erste Rechnung muss auf 0001 enden, war: " + rechnungsNummern.get(0)); + assertTrue(rechnungsNummern.get(1).endsWith("0002"), + "Zweite Rechnung muss auf 0002 enden, war: " + rechnungsNummern.get(1)); + assertTrue(rechnungsNummern.get(2).endsWith("0003"), + "Dritte Rechnung muss auf 0003 enden, war: " + rechnungsNummern.get(2)); + } + + // ========================================================================= + // MT-DP-12: Rechnung unveränderlich (PH-DP-11) + // ========================================================================= + @Test + @Order(12) + @DisplayName("MT-DP-12: Festgeschriebene Rechnung kann nicht mehr geändert werden") + void testRechnung_Unveraenderlich() { + Lieferschein ls = erstelleKompletteDokumentenkette(10001L); + Rechnung rechnung = service.erstelleRechnung(ls.getDokumentNummer()); + + assertTrue(rechnung.isFestgeschrieben(), "Rechnung muss festgeschrieben sein"); + + // Versuch Positionen zu ändern → IllegalStateException + assertThrows(IllegalStateException.class, + () -> rechnung.pruefeAenderbar(), + "Änderung einer festgeschriebenen Rechnung muss IllegalStateException werfen"); + + // Rechnung erneut aus Repository laden → unverändert + Rechnung geladen = (Rechnung) repository.findByNummer(rechnung.getDokumentNummer()).get(); + assertTrue(geladen.isFestgeschrieben(), "Geladene Rechnung muss noch festgeschrieben sein"); + } + + // ========================================================================= + // MT-DP-13: Preis-Snapshot bleibt stabil (PH-DP-14) + // ========================================================================= + @Test + @Order(13) + @DisplayName("MT-DP-13: Spätere Produktpreisänderung beeinflusst bestehende Angebote nicht") + void testPreisSnapshot_BleibtStabil() { + // Angebot mit Preis 100,00 EUR erzeugen + List pos = List.of(new PositionCommand(20001L, 1)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + + BigDecimal preisImAngebot = angebot.getPositionen().get(0).getEinzelpreisNettoSnapshot(); + assertEquals(0, new BigDecimal("100.00").compareTo(preisImAngebot), + "Preis im Angebot muss 100,00 EUR sein"); + + // Produktpreis ändern (Snapshot darf nicht beeinflusst werden) + productLookup.setztePreis(20001L, new BigDecimal("120.00")); + + // Angebot erneut aus Repository laden + Angebot geladen = (Angebot) repository.findByNummer(angebot.getDokumentNummer()).get(); + BigDecimal preisNachAenderung = geladen.getPositionen().get(0).getEinzelpreisNettoSnapshot(); + + assertEquals(0, new BigDecimal("100.00").compareTo(preisNachAenderung), + "Snapshot-Preis muss weiterhin 100,00 EUR sein (nicht 120,00)"); + } + + // ========================================================================= + // MT-DP-14: PDF-Export erzeugt Datei (PH-DP-12) + // ========================================================================= + @Test + @Order(14) + @DisplayName("MT-DP-14: PDF-Export erzeugt vorhandene, nicht-leere Datei") + void testPdfExport_DateiErzeugt() throws Exception { + List pos = List.of(new PositionCommand(20001L, 1)); + Angebot angebot = service.erstelleAngebot(10001L, pos); + + Path pdfPfad = service.exportiereAlsPdf(angebot.getDokumentNummer()); + + assertNotNull(pdfPfad, "Rückgabepfad darf nicht null sein"); + assertTrue(Files.exists(pdfPfad), "PDF-Datei muss existieren: " + pdfPfad); + assertTrue(Files.size(pdfPfad) > 0, "PDF-Datei darf nicht leer sein"); + + String inhalt = Files.readString(pdfPfad); + assertTrue(inhalt.contains(angebot.getDokumentNummer()), + "PDF muss Dokumentnummer enthalten"); + assertTrue(inhalt.contains("ANGEBOT"), + "PDF muss Dokumenttyp enthalten"); + } + + // ========================================================================= + // MT-DP-15: Dokumentübersicht pro Kunde (PH-DP-13) + // ========================================================================= + @Test + @Order(15) + @DisplayName("MT-DP-15: findByKunde liefert nur Dokumente des richtigen Kunden") + void testDokumentenuebersicht_ProKunde() { + List pos = List.of(new PositionCommand(20001L, 1)); + + // Dokumente für Kunde 10001 + service.erstelleAngebot(10001L, pos); + service.erstelleAngebot(10001L, pos); + + // Dokument für Kunde 10002 (anderen Kunden) + service.erstelleAngebot(10002L, pos); + + List dokKunde1 = service.findByKunde(10001L); + List dokKunde2 = service.findByKunde(10002L); + + assertEquals(2, dokKunde1.size(), + "Kunde 10001 muss genau 2 Dokumente haben"); + assertEquals(1, dokKunde2.size(), + "Kunde 10002 muss genau 1 Dokument haben"); + + assertTrue(dokKunde1.stream().allMatch(d -> d.getKundenId() == 10001L), + "Alle Dokumente in der Liste müssen Kunden-ID 10001 haben"); + } + + // ========================================================================= + // MT-DP-16: Stammdatenreferenz prüfen (PH-DP-15) + // ========================================================================= + @Test + @Order(16) + @DisplayName("MT-DP-16: isProductReferenced gibt true/false korrekt zurück") + void testStammdatenreferenz_Pruefen() { + // Angebot mit Produkt 20001 anlegen + List pos = List.of(new PositionCommand(20001L, 1)); + service.erstelleAngebot(10001L, pos); + + // Produkt 20001 wird referenziert → true + assertTrue(repository.existsReferencedProdukt(20001L), + "Produkt 20001 muss als referenziert erkannt werden"); + + // Produkt 99999 existiert nicht in Dokumenten → false + assertFalse(repository.existsReferencedProdukt(99999L), + "Produkt 99999 darf nicht als referenziert gelten"); + + // Kunde 10001 wird referenziert → true + assertTrue(repository.existsReferencedKunde(10001L), + "Kunde 10001 muss als referenziert erkannt werden"); + + // Kunde 99998 nicht → false + assertFalse(repository.existsReferencedKunde(99998L), + "Kunde 99998 darf nicht als referenziert gelten"); + } + + // ========================================================================= + // Hilfsmethode: Erstellt eine vollständige Dokumentenkette bis zum Lieferschein + // ========================================================================= + private Lieferschein erstelleKompletteDokumentenkette(long kundenId) { + List pos = List.of( + new PositionCommand(20001L, 2), + new PositionCommand(20002L, 1) + ); + Angebot angebot = service.erstelleAngebot(kundenId, pos); + Auftragsbestaetigung ab = service.erstelleAuftragsbestaetigung(angebot.getDokumentNummer()); + return service.erstelleLieferschein(ab.getDokumentNummer(), LocalDate.now()); + } +}