test: DokumentServiceTest hinzugefügt

pull/4/head
Andreas Ivanovic 2026-06-28 08:02:19 +02:00
parent d73ad1457b
commit b04757f9db
1 changed files with 498 additions and 0 deletions

View File

@ -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<Long, ProductDto> 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<ProductDto> 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<Long, CustomerDto> 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<CustomerDto> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> pos = List.of(
new PositionCommand(20001L, 2),
new PositionCommand(20002L, 1)
);
// Drei vollständige Dokumentenketten erzeugen
List<String> 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<PositionCommand> 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<PositionCommand> 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<PositionCommand> 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<Dokument> dokKunde1 = service.findByKunde(10001L);
List<Dokument> 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<PositionCommand> 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<PositionCommand> 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());
}
}