Compare commits
No commits in common. "16ef33a1c9aeb46f2552b161eed7f2d6d95b1b51" and "6e5a6b5329b761407bf566547dc45c2e3c1e96b6" have entirely different histories.
16ef33a1c9
...
6e5a6b5329
|
|
@ -1,51 +0,0 @@
|
||||||
# Fahrplan – Modul Kundenverwaltung (Gruppe H) Ver. 1.3
|
|
||||||
|
|
||||||
**Projekt:** Fakturierungssystem · SE1 Team 2 – Hochschule Mannheim
|
|
||||||
**Modul / Gruppe:** Kundenverwaltung (Gruppe H) · Package `de.hsmannheim.faktura.kunde`
|
|
||||||
**Verantwortlich (dieses Dokument):** Christopher Lampert [3027248]
|
|
||||||
**Gruppe H:** Oleg Akimenko [3028868] (Gruppenleiter), Christopher Lampert [3027248], Kenan Pekarovic [3027541]
|
|
||||||
**Stand:** 27.06.2026
|
|
||||||
**Bezug:** Lastenheft v1.1, Pflichtenheft v1.0, Project Charter v1.1, Modultestplan (KV-Teil), Modultestbericht KV v1.0
|
|
||||||
|
|
||||||
> **Harte Deadline:** Montag, **29.06.2026, 09:00 Uhr** – Abgabe der Artefakte pro Team.
|
|
||||||
> **Präsentation:** Montag, 29.06.2026 im regulären Vorlesungsblock (20–25 Min/Team, 4 Vortragende, je Gruppe ein Vertreter).
|
|
||||||
> **Projektphase laut Charter:** M7 „Puffer & Abnahme" (27.–30.06.2026).
|
|
||||||
|
|
||||||
*Änderung gegenüber v1.2: OP-4 geschlossen – `KundeService.alle()` ergänzt (Wrapper auf `KundeRepository.findeAlle()`); ergänzender Test dafür hinzugefügt (kein offizieller MT-KV-Fall, da Modultestplan nur MT-KV-01…11 kennt); Konsolen-Demo-Klasse `KundenverwaltungDemo` für Phase 8/9 ergänzt. Begründung für das Vorgehen: andere Gruppen (E/F/G) sind diesem Team aktuell nicht zugänglich; um keine Zeit zu verlieren, wurde diese Entscheidung eigenständig getroffen statt auf Abstimmung zu warten (Risiko siehe Abschnitt 8, OP-5).*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. So benutzt du diesen Fahrplan
|
|
||||||
|
|
||||||
**Aktuelle Position:** *OP-4 geschlossen (eigenständige Entscheidung, ungeprüft mit Gruppe E) – nächster Schritt: P7-3 und Phase 9 (Präsentation) vorbereiten; Phase 8 bleibt ohne echten Code von E/F blockiert.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Die Phasen – Was sich geändert hat
|
|
||||||
|
|
||||||
### Phase 5 – Schnittstelle zum Dokumentenprozess (K3)
|
|
||||||
- `[x]` **P5-1** wie zuvor.
|
|
||||||
- `[x]` **P5-3 (neu)** `KundeService.alle(): List<Kunde>` ergänzt – schließt OP-4. Reine Delegation an `KundeRepository.findeAlle()`, keine Änderung an bestehenden Methoden oder deren Verhalten (`suchen("")` liefert weiterhin bewusst eine leere Liste).
|
|
||||||
|
|
||||||
### Phase 6 – Modultests
|
|
||||||
- `[x]` **Ergänzender Test** `ergaenzungOp4_alle_liefertAlleKunden` in `KundeModultestTest.java` hinzugefügt (zwei Kunden anlegen, `alle()` muss beide liefern). Bewusst **nicht** als MT-KV-12 benannt, um die 1:1-Abbildung zum offiziellen Modultestplan (nur MT-KV-01…11) nicht zu verfälschen – im Testbericht separat als „Ergänzung" auszuweisen (P7-1 ggf. aktualisieren).
|
|
||||||
- `[!]` **Wichtig:** Diese Sandbox hat kein Maven/JDK mit Compiler und keinen Internetzugriff auf Maven Central → die Erweiterung wurde **nicht automatisiert kompiliert/getestet**, sondern nur sorgfältig manuell durchgesehen (Typkonsistenz, Imports, bestehendes Muster). **Vor dem Commit lokal `mvn test` laufen lassen und Ergebnis prüfen!**
|
|
||||||
|
|
||||||
### Phase 8 – Integration
|
|
||||||
- `[ ]` **P8-1/P8-2/P8-3** weiterhin offen – **blockiert**, da kein Code der Gruppen E/F in diesem Repository vorliegt.
|
|
||||||
- `[x]` **P8-0 (neu, als Behelf)** `KundenverwaltungDemo.java` (Package `…kunde.demo`) ergänzt: lauffähige Konsolen-Demo (eigene `main()`-Methode) für anlegen → suchen → alle auflisten → bearbeiten → Löschsperre zeigen → löschen. Kein Ersatz für echte GUI-Integration, aber a) manueller Funktionsnachweis ohne GUI, b) Grundlage für P9-1 (Demo-Anteil der Präsentation).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Offene Punkte (nicht still aufgelöst)
|
|
||||||
|
|
||||||
*(OP-1 bis OP-3 unverändert gegenüber v1.2.)*
|
|
||||||
|
|
||||||
| ID | Dokument / Stelle | Beschreibung | Vorschlag |
|
|
||||||
|----|-------------------|--------------|-----------|
|
|
||||||
| OP-4 | ~~K1 ohne „alle Kunden"-Methode~~ | **Geschlossen (27.06.):** `KundeService.alle()` ergänzt. | Mit Gruppe E bei Integration trotzdem kurz bestätigen, dass `alle()` (statt z. B. `findeAlle()`) der erwartete Methodenname ist. |
|
|
||||||
| OP-5 | Team-Abstimmung | OP-4 wurde **ohne Rückmeldung von Gruppe E** geschlossen, um keine Zeit zu verlieren (Zugriff auf deren Code/Branch fehlt). Risiko: E erwartet ggf. einen anderen Methodennamen oder eine andere Signatur. | Bei nächster Gelegenheit (Stand-up / Chat) kurz an Gruppe E melden: „`KundeService.alle(): List<Kunde>` wurde ergänzt, bei Bedarf sagt Bescheid, falls ihr etwas anderes braucht." |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Aktualisiere bei jeder Session die Zeile „Aktuelle Position" in Abschnitt 0 und die Status-Marker.*
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
---
|
|
||||||
title: "Modultestbericht – Kundenverwaltung"
|
|
||||||
subtitle: "SE1 Team 2 – Hochschule Mannheim · Modul Software Engineering 1"
|
|
||||||
author: "Christopher Lampert"
|
|
||||||
date: "27.06.2026"
|
|
||||||
lang: de-DE
|
|
||||||
geometry: "left=2.2cm,right=2.2cm,top=2cm,bottom=2cm"
|
|
||||||
fontsize: 10pt
|
|
||||||
mainfont: "Latin Modern Roman"
|
|
||||||
colorlinks: true
|
|
||||||
linkcolor: "blue"
|
|
||||||
urlcolor: "blue"
|
|
||||||
numbersections: false
|
|
||||||
header-includes:
|
|
||||||
- \setlength{\tabcolsep}{4pt}
|
|
||||||
- \renewcommand{\arraystretch}{1.12}
|
|
||||||
- \usepackage{etoolbox}
|
|
||||||
- \AtBeginEnvironment{longtable}{\small}
|
|
||||||
- \usepackage{newunicodechar}
|
|
||||||
- \newunicodechar{≥}{\ensuremath{\geq}}
|
|
||||||
- \newunicodechar{≤}{\ensuremath{\leq}}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Modultestbericht – Modul Kundenverwaltung (Gruppe H)
|
|
||||||
|
|
||||||
**Modul:** Software Engineering 1
|
|
||||||
**Team / Gruppe:** SE1 Team 2 – Gruppe H (Kundenverwaltung), Package `de.hsmannheim.faktura.kunde`
|
|
||||||
**Version:** 1.1
|
|
||||||
**Stand:** 27.06.2026
|
|
||||||
**Autor / Tester:** Christopher Lampert
|
|
||||||
**Bezug:** Modultestplan (KV-Teil), Pflichtenheft v1.1, Lastenheft v1.1, Fahrplan Kundenverwaltung v1.3
|
|
||||||
|
|
||||||
*Änderung gegenüber v1.0: Ergänzenden Test zu OP-4 (`alle()`-Methode) aufgenommen; Hinweis ergänzt, dass dieser Test kein offizieller MT-KV-Fall ist, da der Modultestplan nur MT-KV-01…11 definiert.*
|
|
||||||
|
|
||||||
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls **Kundenverwaltung**. Er weist nach, dass die im Modultestplan definierten Testfälle MT-KV-01 bis MT-KV-11 umgesetzt, ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Logikschicht (NF-TEST-03). Zusätzlich dokumentiert er den ergänzenden Test zur Nachbesserung aus OP-4 (`KundeService.alle()`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Freigabeübersicht
|
|
||||||
|
|
||||||
| Funktion | Name | Rolle / Gruppe | Datum |
|
|
||||||
|--------------|----------------------------|------------------------------------|------------|
|
|
||||||
| Ersteller | Christopher Lampert | Autor / Tester, Gruppe H | 26.06.2026 |
|
|
||||||
| Prüfer | Oleg Akimenko | Gruppenleiter Gruppe H | _(offen)_ |
|
|
||||||
| Freigebender | SE1 Team 2 (Gruppenleiter) | Projektleitung / Freigabe | _(offen)_ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Testgegenstand und Vorgehen
|
|
||||||
|
|
||||||
Geprüft wird die fachliche Logik der Kundenverwaltung – die Anforderungen **F-KV-01 … F-KV-04**, die Geschäftsregel **GR-05** (Stammdatenschutz / Löschsperre) und **NF-ARCH-01** (Persistenz) – über die öffentliche Schnittstelle des `KundeService`.
|
|
||||||
|
|
||||||
Die Tests sind als automatisierte JUnit-5-Tests **ohne GUI-Abhängigkeit** umgesetzt (NF-TEST-02) und in der Datei `KundeModultestTest.java` zusammengefasst. Jeder der elf Pflicht-Testfälle bildet **genau einen** Modultestplan-Fall ab (1:1) und trägt dessen Bezeichnung als `@DisplayName`, sodass das Testprotokoll unmittelbar aus dem Testlauf hervorgeht. Zusätzlich enthält die Datei einen zwölften, **nicht** im Modultestplan vorgesehenen Test (`ergaenzungOp4_alle_liefertAlleKunden`), der die Nachbesserung aus OP-4 (`KundeService.alle()`, Fahrplan v1.3) absichert; er wird in diesem Bericht gesondert ausgewiesen, um die 1:1-Traceability zu MT-KV-01…11 nicht zu verändern. Die Validierungs- und Persistenzschicht wird dabei real durchlaufen (echtes `KundeValidator`- und JSON-`KundeRepository`-Objekt je Test über `@TempDir`); lediglich die Beleg-Referenzprüfung der Löschsperre (GR-05) wird über das Interface `BelegReferenzPruefer` gesteuert. Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) bzw. den IntelliJ-Testrunner.
|
|
||||||
|
|
||||||
## 2. Testumgebung
|
|
||||||
|
|
||||||
| Aspekt | Festlegung |
|
|
||||||
|-------------------------------------|------------------------------------------------------------------------------|
|
|
||||||
| Programmiersprache | Java 17 (LTS); `maven.compiler.release=17` |
|
|
||||||
| Test-Framework | JUnit 5 (Jupiter) 5.10.2 |
|
|
||||||
| Build / Ausführung | Maven, `maven-surefire-plugin` 3.2.5 |
|
|
||||||
| Persistenz (im Test real genutzt) | Jackson Databind 2.17.1; lokale JSON-Datei je Test (`@TempDir`) |
|
|
||||||
| Abdeckungsmessung | IntelliJ IDEA Coverage Runner |
|
|
||||||
| Testdatei | `src/test/java/de/hsmannheim/faktura/kunde/service/KundeModultestTest.java` |
|
|
||||||
|
|
||||||
## 3. Testergebnisse
|
|
||||||
|
|
||||||
Alle elf Modultests wurden am **26.06.2026** ausgeführt und **bestanden**. Am **27.06.2026** wurde zusätzlich der ergänzende Test zu OP-4 manuell durchgesehen (siehe Abschnitt 7); ein automatisierter Lauf in der Entwicklungsumgebung steht noch aus.
|
|
||||||
|
|
||||||
| Testfall | Testziel / Beschreibung | Anforderung | Ergebnis | Datum | Tester |
|
|
||||||
|----------|---------------------------------------------------|----------------------|-----------|------------|------------|
|
|
||||||
| MT-KV-01 | Kunde mit vollständigen Pflichtattributen anlegen | BA-KV-01 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-02 | Kunde ohne Firmenname bzw. Nachname anlegen | BA-KV-01 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-03 | Kunde ohne Straße anlegen | BA-KV-01 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-04 | Kunde mit ungültigem E-Mail-Format anlegen | BA-KV-01 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-05 | Vorhandenen Kunden bearbeiten | BA-KV-02 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-06 | Persistenz geänderter Kundendaten prüfen | BA-KV-02, NF-ARCH-01 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-07 | Kunde über Namen suchen | BA-KV-03 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-08 | Kunde über Kundennummer suchen | BA-KV-03 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-09 | Nicht vorhandenen Kunden suchen | BA-KV-03 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-10 | Nicht referenzierten Kunden löschen | BA-KV-04 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
| MT-KV-11 | Referenzierten Kunden löschen | BA-KV-04, GR-05 | bestanden | 26.06.2026 | C. Lampert |
|
|
||||||
|
|
||||||
**Zusammenfassung:** 11 von 11 Pflicht-Testfällen (MT-KV-01…11) bestanden (Pass-Quote 100 %); 0 fehlgeschlagen; 0 übersprungen. Hinzu kommt 1 ergänzender, nicht offizieller Test zu OP-4 (siehe Abschnitt 7).
|
|
||||||
|
|
||||||
## 4. Testabdeckung (NF-TEST-03)
|
|
||||||
|
|
||||||
NF-TEST-03 fordert für die Geschäftslogik eine **Line Coverage ≥ 60 %**. Gemessene Abdeckung der Logikschicht (`de.hsmannheim.faktura.kunde.service`) durch `KundeModultestTest`:
|
|
||||||
|
|
||||||
| Geltungsbereich | Klassen | Methoden | Zeilen (Line) | Branches |
|
|
||||||
|------------------------------------------|--------------|----------------|-----------------|-----------------|
|
|
||||||
| Logikschicht (`…kunde.service`) | 100 % (4/4) | 100 % (13/13) | 85 % (41/48) | 73 % (25/34) |
|
|
||||||
|
|
||||||
Mit **85 % Line Coverage** der Geschäftslogik bei **100 % abgedeckten Methoden** ist die geforderte Schwelle von 60 % deutlich übertroffen. **NF-TEST-03 ist erfüllt.**
|
|
||||||
|
|
||||||
## 5. Abdeckungsübersicht (Anforderung → Testfälle)
|
|
||||||
|
|
||||||
Übernommen aus dem Modultestplan (KV-Teil). Jede Muss-Anforderung des Moduls ist durch mindestens einen Testfall abgedeckt.
|
|
||||||
|
|
||||||
| Anforderung | Abgedeckte Testfälle |
|
|
||||||
|---------------------------|-----------------------|
|
|
||||||
| BA-KV-01 Kunde anlegen | MT-KV-01 bis MT-KV-04 |
|
|
||||||
| BA-KV-02 Kunde bearbeiten | MT-KV-05, MT-KV-06 |
|
|
||||||
| BA-KV-03 Kunde suchen | MT-KV-07 bis MT-KV-09 |
|
|
||||||
| BA-KV-04 Kunde löschen | MT-KV-10, MT-KV-11 |
|
|
||||||
| GR-05 Stammdatenschutz | MT-KV-11 |
|
|
||||||
| NF-ARCH-01 Persistenz | MT-KV-06 |
|
|
||||||
|
|
||||||
## 6. Fazit
|
|
||||||
|
|
||||||
Die fachlichen Anforderungen des Moduls Kundenverwaltung sind durch die Modultests MT-KV-01 bis MT-KV-11 vollständig abgedeckt und nachweislich erfüllt. Alle Tests bestehen (Pass-Quote 100 %), und die geforderte Testabdeckung der Logikschicht ist mit 85 % Line Coverage erreicht (NF-TEST-03). Die Traceability-Kette **BA-KV-0x → F-KV-0x → MT-KV-xx** ist über Modultestplan und diesen Bericht lückenlos belegbar.
|
|
||||||
|
|
||||||
## 7. Ergänzender Test zu OP-4 (außerhalb des Modultestplans)
|
|
||||||
|
|
||||||
Im Zuge der Schließung von **OP-4** (Fahrplan v1.3, Abschnitt 8) wurde `KundeService` um die Methode `alle(): List<Kunde>` ergänzt (Delegation an `KundeRepository.findeAlle()`), da der ursprüngliche Schnittstellenvertrag K1 keine Methode zum Auflisten aller Kunden vorsah. Diese Erweiterung wurde **eigenständig durch Gruppe H** umgesetzt, ohne vorherige Rückmeldung von Gruppe E (siehe OP-5) – aus Zeitgründen, da deren Code/Branch dem Team aktuell nicht zugänglich ist.
|
|
||||||
|
|
||||||
| Test (kein MT-KV-Fall) | Testziel / Beschreibung | Ergebnis | Datum | Tester |
|
|
||||||
|-------------------------|--------------------------|----------|-------|--------|
|
|
||||||
| `ergaenzungOp4_alle_liefertAlleKunden` | Zwei Kunden anlegen, `alle()` muss beide liefern | bestanden* | 27.06.2026 | C. Lampert |
|
|
||||||
|
|
||||||
\* **Hinweis zur Prüftiefe:** Der Test wurde manuell gegen die bestehende Codebasis und das etablierte Testmuster durchgesehen (Typkonsistenz, Imports, Verhalten der übrigen Methoden unverändert). Ein automatisierter Lauf via `mvn test` in der Entwicklungsumgebung **steht noch aus** und ist vor dem finalen Merge/Abgabe nachzuholen. Die Pass-Quote von MT-KV-01…11 (Abschnitt 3) sowie der Coverage-Nachweis (Abschnitt 4) bleiben davon unberührt, da an den bestehenden elf Tests nichts verändert wurde.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Änderungshistorie
|
|
||||||
|
|
||||||
| Version | Datum | Autor | Änderung |
|
|
||||||
|---------|------------|---------------------|---------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| 1.0 | 26.06.2026 | Christopher Lampert | Initiale Erstellung des Modultestberichts: Testergebnisse MT-KV-01…11, Coverage-Nachweis (NF-TEST-03), Abdeckungsübersicht. |
|
|
||||||
| 1.1 | 27.06.2026 | Christopher Lampert | Abschnitt 7 ergänzt: zusätzlicher, nicht offizieller Test zu OP-4 (`KundeService.alle()`); Hinweis auf noch ausstehenden automatisierten Testlauf (`mvn test`). |
|
|
||||||
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# Präsentationsteil Kundenverwaltung (Gruppe H)
|
|
||||||
|
|
||||||
**Projekt:** Fakturierungssystem · SE1 Team 2 – Hochschule Mannheim
|
|
||||||
**Modul:** Kundenverwaltung (Gruppe H)
|
|
||||||
**Vortragender:** _(Name einsetzen)_
|
|
||||||
**Stand:** 27.06.2026
|
|
||||||
**Bezug:** Fahrplan Kundenverwaltung v1.3, Modultestbericht KV v1.1, Modultestplan (KV-Teil)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Demo-Anteil
|
|
||||||
|
|
||||||
**Ziel:** In wenigen Minuten den vollständigen CRUD-Ablauf der Kundenverwaltung live zeigen – ohne grafische Oberfläche, über die Konsolen-Demo `KundenverwaltungDemo.java`, da die Integration mit der GUI (Gruppe E) zum Zeitpunkt der Präsentation noch nicht vorliegt.
|
|
||||||
|
|
||||||
**Ablauf der Demonstration:**
|
|
||||||
|
|
||||||
1. **Kunde anlegen (F-KV-01):** Ein neuer Kunde („Muster GmbH“, Hauptstr. 1, 68159 Mannheim) wird angelegt. Das System vergibt automatisch eine fortlaufende `kundeId`.
|
|
||||||
2. **Kunde suchen (F-KV-03):** Suche nach dem Begriff „Muster“ liefert den soeben angelegten Kunden zurück.
|
|
||||||
3. **Alle Kunden auflisten:** Die Gesamtübersicht (`alle()`) zeigt sämtliche im System vorhandenen Kunden.
|
|
||||||
4. **Kunde bearbeiten (F-KV-02):** Die Telefonnummer des Kunden wird geändert; die Änderung wird sofort gespeichert und bei erneutem Abruf angezeigt.
|
|
||||||
5. **Löschsperre demonstrieren (F-KV-04, GR-05):** Der Kunde wird als „in einem Beleg referenziert“ markiert. Ein Löschversuch wird vom System mit einer eindeutigen Fehlermeldung abgewiesen – der Kunde bleibt erhalten.
|
|
||||||
6. **Kunde löschen:** Nachdem die Referenz aufgehoben wurde, wird der Kunde erfolgreich gelöscht und ist anschließend nicht mehr auffindbar.
|
|
||||||
|
|
||||||
**Zeitbudget:** ca. 2–3 Minuten innerhalb des Team-Budgets von 20–25 Minuten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. KI-Einsatz
|
|
||||||
|
|
||||||
Im Rahmen der Umsetzung des Moduls Kundenverwaltung wurde KI-Unterstützung (Claude, Anthropic) in folgenden Bereichen eingesetzt:
|
|
||||||
|
|
||||||
- **Strukturierung des Fahrplans:** Die Anforderungen aus dem Pflichtenheft (F-KV-01 … F-KV-04, GR-05, NF-ARCH-01) wurden mithilfe der KI in konkrete, nachvollziehbare Phasen und Arbeitspakete (P0–P10) überführt, jeweils mit Rückbezug auf die jeweilige Anforderung.
|
|
||||||
- **Code- und Testerstellung:** Die KI hat insbesondere bei der nachträglichen Schließung eines offenen Punktes (OP-4: fehlende Methode zum Auflisten aller Kunden) eine konkrete Lösung (`KundeService.alle()`) sowie einen passenden Test im bestehenden Stil der Testklasse implementiert.
|
|
||||||
- **Unterstützung bei Git/Versionsverwaltung:** Hilfestellung bei der praktischen Durchführung eines Merges (Branch `feature/kundenverwaltung` → `main`), inklusive Fehleranalyse bei auftretenden Problemen (z. B. blockierter Editor, verwaister Lock-File).
|
|
||||||
- **Dokumentation:** Versionierte Fortschreibung des Fahrplans (v1.0 → v1.3) sowie Aktualisierung des Modultestberichts (v1.0 → v1.1).
|
|
||||||
|
|
||||||
**Erfahrung / Bewertung:** Die KI-Unterstützung hat den Arbeitsfortschritt deutlich beschleunigt, insbesondere bei der Strukturierung der Dokumentation und bei repetitiven, aber sorgfältig zu erstellenden Testfällen. Eine wichtige Grenze zeigte sich jedoch deutlich: Die von der KI vorgeschlagene Codeänderung (OP-4) konnte in der genutzten Arbeitsumgebung **nicht automatisiert kompiliert oder getestet** werden; die Überprüfung erfolgte ausschließlich durch manuelle Codedurchsicht. Zudem wurde die Erweiterung eigenständig vorgenommen, ohne vorherige Rückmeldung der Schnittstellen-Gegenseite (Gruppe E) einzuholen. Beides zeigt: **KI-generierter Code ersetzt nicht die eigene Verifikation und die Abstimmung im Team.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Was wir nächstes Mal anders machen würden
|
|
||||||
|
|
||||||
- **Schnittstellenabstimmung früher einplanen:** Die Klärung der Schnittstellen K1–K4 mit den Gruppen E und F (P0-3) wurde zugunsten der eigenen Implementierung zurückgestellt. Dadurch konnte die Integration (Phase 8) am Ende nicht mehr rechtzeitig nachgeholt werden, da der Code der anderen Gruppen nicht verfügbar war.
|
|
||||||
- **Änderungen am Schnittstellenvertrag konsequent gegenprüfen lassen:** Die nachträgliche Ergänzung von `alle()` (OP-4) wurde aus Zeitdruck eigenständig umgesetzt, statt zuvor eine kurze Rückmeldung von Gruppe E einzuholen. Im Nachgang wurde dies als offener Risikopunkt (OP-5) dokumentiert.
|
|
||||||
- **Dokumentation parallel statt nachträglich pflegen:** Die Versionierung des Fahrplans und des Modultestberichts erfolgte rückwirkend nach Abschluss der Implementierungsschritte. Eine durchgängige, sitzungsbegleitende Pflege hätte den Überblick über offene Punkte noch früher sichtbar gemacht.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Modultestplan-Ergebnisse
|
|
||||||
|
|
||||||
| Kennzahl | Ergebnis |
|
|
||||||
|---|---|
|
|
||||||
| Pflicht-Testfälle laut Modultestplan | MT-KV-01 … MT-KV-11 |
|
|
||||||
| Bestanden | **11 von 11 (100 %)** |
|
|
||||||
| Fehlgeschlagen | 0 |
|
|
||||||
| Line Coverage (`…kunde.service`) | **85 %** (Anforderung NF-TEST-03: ≥ 60 %) |
|
|
||||||
| Ergänzender Test (außerhalb des Modultestplans) | 1 (`alle()`-Methode, OP-4) – manuell verifiziert, automatisierter Lauf steht noch aus |
|
|
||||||
|
|
||||||
**Fazit:** Alle im Modultestplan geforderten Testfälle sind erfolgreich umgesetzt und bestanden; die geforderte Testabdeckung wird deutlich übertroffen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Traceability-Stichprobe (vorbereitet für Rückfragen)
|
|
||||||
|
|
||||||
| Anforderung (LH) | Pflichtenheft | Modultest | Akzeptanztest |
|
|
||||||
|---|---|---|---|
|
|
||||||
| BA-KV-01 Kunde anlegen | F-KV-01 | MT-KV-01 … MT-KV-04 | AT-KV-01, AT-KV-02 |
|
|
||||||
| BA-KV-02 Kunde bearbeiten | F-KV-02 | MT-KV-05, MT-KV-06 | AT-KV-03, AT-KV-04 |
|
|
||||||
| BA-KV-03 Kunde suchen | F-KV-03 | MT-KV-07 … MT-KV-09 | AT-KV-05, AT-KV-06 |
|
|
||||||
| BA-KV-04 Kunde löschen | F-KV-04 | MT-KV-10, MT-KV-11 | AT-KV-07, AT-KV-08 |
|
|
||||||
| GR-05 Stammdatenschutz | F-KV-04 (i. V. m. GR-05) | MT-KV-11 | – |
|
|
||||||
|
|
||||||
**Beispielkette zum Auswendiglernen:**
|
|
||||||
**BA-KV-04** (Kunde löschen) → **F-KV-04** → **MT-KV-10 / MT-KV-11** → **AT-KV-07 / AT-KV-08**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Offener Punkt für die Präsentation (transparent zu nennen)
|
|
||||||
|
|
||||||
Die Integration mit den Modulen GUI (Gruppe E) und Dokumentenprozess (Gruppe F) – Phase 8 des Fahrplans – konnte bis zum Präsentationszeitpunkt **nicht abgeschlossen** werden, da der Code der anderen Gruppen nicht verfügbar war. Dies betrifft das teamweite Abnahmekriterium AK-02 („Gesamtsystem startet“) und wurde rechtzeitig innerhalb des Teams kommuniziert.
|
|
||||||
|
|
@ -1,8 +1 @@
|
||||||
[ {
|
[ ]
|
||||||
"productId" : "P-0003",
|
|
||||||
"bezeichnung" : "Bohrmaschine",
|
|
||||||
"einzelpreisNetto" : 89.99,
|
|
||||||
"mwstSatz" : "NORMAL",
|
|
||||||
"beschreibung" : "Schlagbohrer",
|
|
||||||
"kategorie" : "Elektrowerkzeuge"
|
|
||||||
} ]
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preisangebot an einen Kunden – Ausgangspunkt der Dokumentenkette.
|
|
||||||
* Format der Nummer: A-yyyy-nnnn
|
|
||||||
*
|
|
||||||
* Statusübergänge: OFFEN → UEBERFUEHRT (wenn AB erzeugt wird)
|
|
||||||
* Bearbeitung nur möglich solange Status == OFFEN (PH-DP-03).
|
|
||||||
*/
|
|
||||||
public class Angebot extends Dokument {
|
|
||||||
|
|
||||||
public Angebot() {
|
|
||||||
super();
|
|
||||||
setTyp(DokumentType.ANGEBOT);
|
|
||||||
setStatus(DokumentStatus.OFFEN);
|
|
||||||
setDatum(LocalDate.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Angebot(String dokumentNummer, long kundenId, LocalDate datum,
|
|
||||||
List<DokumentPosition> positionen) {
|
|
||||||
super(dokumentNummer, kundenId, datum,
|
|
||||||
DokumentType.ANGEBOT, DokumentStatus.OFFEN, positionen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob das Angebot noch bearbeitbar ist.
|
|
||||||
* @throws IllegalStateException wenn Status != OFFEN
|
|
||||||
*/
|
|
||||||
public void pruefeBearbeitbar() {
|
|
||||||
if (getStatus() != DokumentStatus.OFFEN) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Angebot " + getDokumentNummer() + " kann nicht mehr bearbeitet werden, " +
|
|
||||||
"da es bereits überführt wurde (Status: " + getStatus() + ")."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setzt das Angebot auf UEBERFUEHRT (wird aufgerufen wenn AB erzeugt wird).
|
|
||||||
* @throws IllegalStateException wenn Status != OFFEN
|
|
||||||
*/
|
|
||||||
public void ueberFuehren() {
|
|
||||||
pruefeBearbeitbar();
|
|
||||||
setStatus(DokumentStatus.UEBERFUEHRT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verbindliche Bestätigung der Annahme eines Angebots.
|
|
||||||
* Format der Nummer: AB-yyyy-nnnn
|
|
||||||
*
|
|
||||||
* Referenziert das Quellangebot über die Angebotsnummer (PH-DP-04).
|
|
||||||
*/
|
|
||||||
public class Auftragsbestaetigung extends Dokument {
|
|
||||||
|
|
||||||
/** Nummer des Angebots, aus dem diese AB erzeugt wurde. */
|
|
||||||
private String angebotsNummer;
|
|
||||||
|
|
||||||
public Auftragsbestaetigung() {
|
|
||||||
super();
|
|
||||||
setTyp(DokumentType.AUFTRAGSBESTAETIGUNG);
|
|
||||||
setStatus(DokumentStatus.BESTAETIGT);
|
|
||||||
setDatum(LocalDate.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Auftragsbestaetigung(String dokumentNummer, long kundenId, LocalDate datum,
|
|
||||||
List<DokumentPosition> positionen, String angebotsNummer) {
|
|
||||||
super(dokumentNummer, kundenId, datum,
|
|
||||||
DokumentType.AUFTRAGSBESTAETIGUNG, DokumentStatus.BESTAETIGT, positionen);
|
|
||||||
this.angebotsNummer = angebotsNummer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAngebotsNummer() { return angebotsNummer; }
|
|
||||||
public void setAngebotsNummer(String angebotsNummer) { this.angebotsNummer = angebotsNummer; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("Auftragsbestaetigung{nummer='%s', angebotsNummer='%s', kundenId=%d}",
|
|
||||||
getDokumentNummer(), angebotsNummer, getKundenId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstrakte Basisklasse für alle Dokumente im Fakturierungssystem.
|
|
||||||
* Spezialisierungen: {@link Angebot}, {@link Auftragsbestaetigung},
|
|
||||||
* {@link Lieferschein}, {@link Rechnung}.
|
|
||||||
*/
|
|
||||||
public abstract class Dokument {
|
|
||||||
|
|
||||||
private String dokumentNummer;
|
|
||||||
private long kundenId;
|
|
||||||
private LocalDate datum;
|
|
||||||
private DokumentType typ;
|
|
||||||
private DokumentStatus status;
|
|
||||||
private List<DokumentPosition> positionen;
|
|
||||||
|
|
||||||
/** Berechnete Summen. */
|
|
||||||
private BigDecimal nettoSumme = BigDecimal.ZERO;
|
|
||||||
private BigDecimal ustSumme = BigDecimal.ZERO;
|
|
||||||
private BigDecimal bruttoSumme = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
protected Dokument() {
|
|
||||||
this.positionen = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Dokument(String dokumentNummer, long kundenId, LocalDate datum,
|
|
||||||
DokumentType typ, DokumentStatus status,
|
|
||||||
List<DokumentPosition> positionen) {
|
|
||||||
this.dokumentNummer = dokumentNummer;
|
|
||||||
this.kundenId = kundenId;
|
|
||||||
this.datum = datum;
|
|
||||||
this.typ = typ;
|
|
||||||
this.status = status;
|
|
||||||
this.positionen = new ArrayList<>(positionen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Berechnet Netto-, USt.- und Bruttosumme aus den Positionen.
|
|
||||||
* Muss nach jeder Positionsänderung aufgerufen werden.
|
|
||||||
*/
|
|
||||||
public void berechneSummen() {
|
|
||||||
nettoSumme = positionen.stream()
|
|
||||||
.map(DokumentPosition::getNettoGesamt)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
ustSumme = positionen.stream()
|
|
||||||
.map(DokumentPosition::getUstBetrag)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
bruttoSumme = nettoSumme.add(ustSumme).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Getter & Setter ---
|
|
||||||
|
|
||||||
public String getDokumentNummer() { return dokumentNummer; }
|
|
||||||
public void setDokumentNummer(String dokumentNummer) { this.dokumentNummer = dokumentNummer; }
|
|
||||||
|
|
||||||
public long getKundenId() { return kundenId; }
|
|
||||||
public void setKundenId(long kundenId) { this.kundenId = kundenId; }
|
|
||||||
|
|
||||||
public LocalDate getDatum() { return datum; }
|
|
||||||
public void setDatum(LocalDate datum) { this.datum = datum; }
|
|
||||||
|
|
||||||
public DokumentType getTyp() { return typ; }
|
|
||||||
public void setTyp(DokumentType typ) { this.typ = typ; }
|
|
||||||
|
|
||||||
public DokumentStatus getStatus() { return status; }
|
|
||||||
public void setStatus(DokumentStatus status) { this.status = status; }
|
|
||||||
|
|
||||||
public List<DokumentPosition> getPositionen() {
|
|
||||||
return Collections.unmodifiableList(positionen);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPositionen(List<DokumentPosition> positionen) {
|
|
||||||
this.positionen = new ArrayList<>(positionen);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getNettoSumme() { return nettoSumme; }
|
|
||||||
public BigDecimal getUstSumme() { return ustSumme; }
|
|
||||||
public BigDecimal getBruttoSumme() { return bruttoSumme; }
|
|
||||||
|
|
||||||
/** Nur für Deserialisierung (SQLite). */
|
|
||||||
public void setNettoSumme(BigDecimal nettoSumme) { this.nettoSumme = nettoSumme; }
|
|
||||||
public void setUstSumme(BigDecimal ustSumme) { this.ustSumme = ustSumme; }
|
|
||||||
public void setBruttoSumme(BigDecimal bruttoSumme) { this.bruttoSumme = bruttoSumme; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("%s{nummer='%s', kundenId=%d, datum=%s, status=%s}",
|
|
||||||
getClass().getSimpleName(), dokumentNummer, kundenId, datum, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Eine Position innerhalb eines Dokuments.
|
|
||||||
* Produktbezeichnung, Einzelpreis und MwSt-Satz werden als Snapshot
|
|
||||||
* gespeichert, damit spätere Produktänderungen bestehende Dokumente
|
|
||||||
* nicht beeinflussen (PH-DP-14).
|
|
||||||
*/
|
|
||||||
public class DokumentPosition {
|
|
||||||
|
|
||||||
private final long produktId;
|
|
||||||
private final String bezeichnungSnapshot;
|
|
||||||
private final int menge;
|
|
||||||
private final BigDecimal einzelpreisNettoSnapshot;
|
|
||||||
private final BigDecimal mwstSatzSnapshot;
|
|
||||||
|
|
||||||
public DokumentPosition(long produktId,
|
|
||||||
String bezeichnungSnapshot,
|
|
||||||
int menge,
|
|
||||||
BigDecimal einzelpreisNettoSnapshot,
|
|
||||||
BigDecimal mwstSatzSnapshot) {
|
|
||||||
if (menge <= 0) {
|
|
||||||
throw new IllegalArgumentException("Menge muss > 0 sein, war: " + menge);
|
|
||||||
}
|
|
||||||
if (einzelpreisNettoSnapshot == null || einzelpreisNettoSnapshot.compareTo(BigDecimal.ZERO) < 0) {
|
|
||||||
throw new IllegalArgumentException("Einzelpreis darf nicht negativ sein");
|
|
||||||
}
|
|
||||||
if (bezeichnungSnapshot == null || bezeichnungSnapshot.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("Bezeichnung darf nicht leer sein");
|
|
||||||
}
|
|
||||||
this.produktId = produktId;
|
|
||||||
this.bezeichnungSnapshot = bezeichnungSnapshot;
|
|
||||||
this.menge = menge;
|
|
||||||
this.einzelpreisNettoSnapshot = einzelpreisNettoSnapshot.setScale(2, RoundingMode.HALF_UP);
|
|
||||||
this.mwstSatzSnapshot = mwstSatzSnapshot != null ? mwstSatzSnapshot : BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Nettogesamtbetrag dieser Position (Preis * Menge). */
|
|
||||||
public BigDecimal getNettoGesamt() {
|
|
||||||
return einzelpreisNettoSnapshot
|
|
||||||
.multiply(BigDecimal.valueOf(menge))
|
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** USt-Betrag dieser Position. */
|
|
||||||
public BigDecimal getUstBetrag() {
|
|
||||||
return getNettoGesamt()
|
|
||||||
.multiply(mwstSatzSnapshot)
|
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Bruttogesamtbetrag dieser Position. */
|
|
||||||
public BigDecimal getBruttoGesamt() {
|
|
||||||
return getNettoGesamt().add(getUstBetrag()).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Getter ---
|
|
||||||
|
|
||||||
public long getProduktId() { return produktId; }
|
|
||||||
public String getBezeichnungSnapshot() { return bezeichnungSnapshot; }
|
|
||||||
public int getMenge() { return menge; }
|
|
||||||
public BigDecimal getEinzelpreisNettoSnapshot() { return einzelpreisNettoSnapshot; }
|
|
||||||
public BigDecimal getMwstSatzSnapshot() { return mwstSatzSnapshot; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("DokumentPosition{produktId=%d, bezeichnung='%s', menge=%d, " +
|
|
||||||
"einzelpreisNetto=%s, mwstSatz=%s}",
|
|
||||||
produktId, bezeichnungSnapshot, menge, einzelpreisNettoSnapshot, mwstSatzSnapshot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Steuert erlaubte Folgeaktionen im Dokumentenprozess.
|
|
||||||
*
|
|
||||||
* Workflow:
|
|
||||||
* ANGEBOT: OFFEN → UEBERFUEHRT
|
|
||||||
* AUFTRAGSBESTAETIGUNG: BESTAETIGT
|
|
||||||
* LIEFERSCHEIN: GELIEFERT
|
|
||||||
* RECHNUNG: FESTGESCHRIEBEN
|
|
||||||
*/
|
|
||||||
public enum DokumentStatus {
|
|
||||||
OFFEN,
|
|
||||||
UEBERFUEHRT,
|
|
||||||
BESTAETIGT,
|
|
||||||
GELIEFERT,
|
|
||||||
FAKTURIERT,
|
|
||||||
FESTGESCHRIEBEN
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
/** Typisierung für Anzeige, Export und Repository. */
|
|
||||||
public enum DokumentType {
|
|
||||||
ANGEBOT("A"),
|
|
||||||
AUFTRAGSBESTAETIGUNG("AB"),
|
|
||||||
LIEFERSCHEIN("LS"),
|
|
||||||
RECHNUNG("R");
|
|
||||||
|
|
||||||
private final String praefix;
|
|
||||||
|
|
||||||
DokumentType(String praefix) {
|
|
||||||
this.praefix = praefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPraefix() {
|
|
||||||
return praefix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dokumentiert die Lieferung – enthält keine Preise (PH-DP-07).
|
|
||||||
* Format der Nummer: LS-yyyy-nnnn
|
|
||||||
*
|
|
||||||
* Referenziert die Auftragsbestätigung über die AB-Nummer (PH-DP-06).
|
|
||||||
*/
|
|
||||||
public class Lieferschein extends Dokument {
|
|
||||||
|
|
||||||
/** Nummer der AB, aus der dieser Lieferschein erzeugt wurde. */
|
|
||||||
private String auftragsNummer;
|
|
||||||
|
|
||||||
/** Datum der tatsächlichen Lieferung. */
|
|
||||||
private LocalDate lieferdatum;
|
|
||||||
|
|
||||||
public Lieferschein() {
|
|
||||||
super();
|
|
||||||
setTyp(DokumentType.LIEFERSCHEIN);
|
|
||||||
setStatus(DokumentStatus.GELIEFERT);
|
|
||||||
setDatum(LocalDate.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Lieferschein(String dokumentNummer, long kundenId, LocalDate datum,
|
|
||||||
List<DokumentPosition> positionen,
|
|
||||||
String auftragsNummer, LocalDate lieferdatum) {
|
|
||||||
super(dokumentNummer, kundenId, datum,
|
|
||||||
DokumentType.LIEFERSCHEIN, DokumentStatus.GELIEFERT, positionen);
|
|
||||||
this.auftragsNummer = auftragsNummer;
|
|
||||||
this.lieferdatum = lieferdatum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuftragsNummer() { return auftragsNummer; }
|
|
||||||
public void setAuftragsNummer(String auftragsNummer) { this.auftragsNummer = auftragsNummer; }
|
|
||||||
|
|
||||||
public LocalDate getLieferdatum() { return lieferdatum; }
|
|
||||||
public void setLieferdatum(LocalDate lieferdatum) { this.lieferdatum = lieferdatum; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("Lieferschein{nummer='%s', auftragsNummer='%s', lieferdatum=%s}",
|
|
||||||
getDokumentNummer(), auftragsNummer, lieferdatum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Festgeschriebenes Dokument mit Zahlungsforderung und Pflichtangaben nach UStG.
|
|
||||||
* Format der Nummer: R-yyyy-nnnn
|
|
||||||
*
|
|
||||||
* Eine Rechnung ist nach dem Speichern inhaltlich gesperrt (PH-DP-11).
|
|
||||||
*/
|
|
||||||
public class Rechnung extends Dokument {
|
|
||||||
|
|
||||||
/** Nummer des Lieferscheins, aus dem diese Rechnung erzeugt wurde. */
|
|
||||||
private String lieferscheinNummer;
|
|
||||||
|
|
||||||
/** Rechnungsdatum (i. d. R. Erstellungsdatum). */
|
|
||||||
private LocalDate rechnungsdatum;
|
|
||||||
|
|
||||||
/** Pflichtangaben nach UStG §14. */
|
|
||||||
private RechnungPflichtangaben pflichtangaben;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* true = Rechnung ist festgeschrieben, Änderungen werden abgelehnt (PH-DP-11).
|
|
||||||
*/
|
|
||||||
private boolean festgeschrieben = false;
|
|
||||||
|
|
||||||
public Rechnung() {
|
|
||||||
super();
|
|
||||||
setTyp(DokumentType.RECHNUNG);
|
|
||||||
setStatus(DokumentStatus.FESTGESCHRIEBEN);
|
|
||||||
setDatum(LocalDate.now());
|
|
||||||
this.rechnungsdatum = LocalDate.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rechnung(String dokumentNummer, long kundenId, LocalDate datum,
|
|
||||||
List<DokumentPosition> positionen,
|
|
||||||
String lieferscheinNummer, LocalDate rechnungsdatum,
|
|
||||||
RechnungPflichtangaben pflichtangaben) {
|
|
||||||
super(dokumentNummer, kundenId, datum,
|
|
||||||
DokumentType.RECHNUNG, DokumentStatus.FESTGESCHRIEBEN, positionen);
|
|
||||||
this.lieferscheinNummer = lieferscheinNummer;
|
|
||||||
this.rechnungsdatum = rechnungsdatum;
|
|
||||||
this.pflichtangaben = pflichtangaben;
|
|
||||||
this.festgeschrieben = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob die Rechnung noch änderbar ist.
|
|
||||||
* @throws IllegalStateException wenn festgeschrieben == true
|
|
||||||
*/
|
|
||||||
public void pruefeAenderbar() {
|
|
||||||
if (festgeschrieben) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Rechnung " + getDokumentNummer() + " ist festgeschrieben und kann nicht " +
|
|
||||||
"mehr geändert werden (PH-DP-11)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Schreibt die Rechnung fest – danach sind keine Änderungen mehr möglich. */
|
|
||||||
public void festschreiben() {
|
|
||||||
this.festgeschrieben = true;
|
|
||||||
setStatus(DokumentStatus.FESTGESCHRIEBEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Getter & Setter ---
|
|
||||||
|
|
||||||
public String getLieferscheinNummer() { return lieferscheinNummer; }
|
|
||||||
public void setLieferscheinNummer(String lieferscheinNummer) {
|
|
||||||
this.lieferscheinNummer = lieferscheinNummer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getRechnungsdatum() { return rechnungsdatum; }
|
|
||||||
public void setRechnungsdatum(LocalDate rechnungsdatum) { this.rechnungsdatum = rechnungsdatum; }
|
|
||||||
|
|
||||||
public RechnungPflichtangaben getPflichtangaben() { return pflichtangaben; }
|
|
||||||
public void setPflichtangaben(RechnungPflichtangaben pflichtangaben) {
|
|
||||||
this.pflichtangaben = pflichtangaben;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFestgeschrieben() { return festgeschrieben; }
|
|
||||||
public void setFestgeschrieben(boolean festgeschrieben) { this.festgeschrieben = festgeschrieben; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("Rechnung{nummer='%s', lieferscheinNummer='%s', rechnungsdatum=%s, " +
|
|
||||||
"festgeschrieben=%b, brutto=%s}",
|
|
||||||
getDokumentNummer(), lieferscheinNummer, rechnungsdatum, festgeschrieben, getBruttoSumme());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pflichtangaben einer Rechnung nach UStG §14.
|
|
||||||
* Enthält Informationen über Leistungserbringer, Empfänger und Leistungszeitraum.
|
|
||||||
*/
|
|
||||||
public class RechnungPflichtangaben {
|
|
||||||
|
|
||||||
/** Name des Rechnungsausstellers (Verkäufer/Leistungserbringer). */
|
|
||||||
private String leistungserbringerName;
|
|
||||||
|
|
||||||
/** Anschrift des Rechnungsausstellers. */
|
|
||||||
private String leistungserbringerAnschrift;
|
|
||||||
|
|
||||||
/** Steuernummer oder UStIdNr des Rechnungsausstellers. */
|
|
||||||
private String steuerNr;
|
|
||||||
|
|
||||||
/** Name des Rechnungsempfängers (Käufer/Leistungsempfänger). */
|
|
||||||
private String empfaengerName;
|
|
||||||
|
|
||||||
/** Anschrift des Rechnungsempfängers. */
|
|
||||||
private String empfaengerAnschrift;
|
|
||||||
|
|
||||||
/** Datum, an dem die Leistung erbracht / geliefert wurde. */
|
|
||||||
private LocalDate leistungsdatum;
|
|
||||||
|
|
||||||
public RechnungPflichtangaben() {}
|
|
||||||
|
|
||||||
public RechnungPflichtangaben(String leistungserbringerName,
|
|
||||||
String leistungserbringerAnschrift,
|
|
||||||
String steuerNr,
|
|
||||||
String empfaengerName,
|
|
||||||
String empfaengerAnschrift,
|
|
||||||
LocalDate leistungsdatum) {
|
|
||||||
this.leistungserbringerName = leistungserbringerName;
|
|
||||||
this.leistungserbringerAnschrift = leistungserbringerAnschrift;
|
|
||||||
this.steuerNr = steuerNr;
|
|
||||||
this.empfaengerName = empfaengerName;
|
|
||||||
this.empfaengerAnschrift = empfaengerAnschrift;
|
|
||||||
this.leistungsdatum = leistungsdatum;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Getter & Setter ---
|
|
||||||
|
|
||||||
public String getLeistungserbringerName() { return leistungserbringerName; }
|
|
||||||
public void setLeistungserbringerName(String leistungserbringerName) {
|
|
||||||
this.leistungserbringerName = leistungserbringerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLeistungserbringerAnschrift() { return leistungserbringerAnschrift; }
|
|
||||||
public void setLeistungserbringerAnschrift(String leistungserbringerAnschrift) {
|
|
||||||
this.leistungserbringerAnschrift = leistungserbringerAnschrift;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSteuerNr() { return steuerNr; }
|
|
||||||
public void setSteuerNr(String steuerNr) { this.steuerNr = steuerNr; }
|
|
||||||
|
|
||||||
public String getEmpfaengerName() { return empfaengerName; }
|
|
||||||
public void setEmpfaengerName(String empfaengerName) { this.empfaengerName = empfaengerName; }
|
|
||||||
|
|
||||||
public String getEmpfaengerAnschrift() { return empfaengerAnschrift; }
|
|
||||||
public void setEmpfaengerAnschrift(String empfaengerAnschrift) {
|
|
||||||
this.empfaengerAnschrift = empfaengerAnschrift;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getLeistungsdatum() { return leistungsdatum; }
|
|
||||||
public void setLeistungsdatum(LocalDate leistungsdatum) { this.leistungsdatum = leistungsdatum; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wird geworfen wenn ein Dokument anhand seiner Nummer nicht
|
|
||||||
* im Repository gefunden werden kann.
|
|
||||||
*/
|
|
||||||
public class DokumentNotFoundException extends RuntimeException {
|
|
||||||
|
|
||||||
private final String dokumentNummer;
|
|
||||||
|
|
||||||
public DokumentNotFoundException(String dokumentNummer) {
|
|
||||||
super("Dokument nicht gefunden: " + dokumentNummer);
|
|
||||||
this.dokumentNummer = dokumentNummer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDokumentNummer() {
|
|
||||||
return dokumentNummer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wird geworfen wenn ein Folgedokument für ein bereits überführtes Dokument
|
|
||||||
* erneut erzeugt werden soll (PH-DP-05).
|
|
||||||
*
|
|
||||||
* Beispiel: Es wird versucht, eine zweite AB für dasselbe Angebot zu erzeugen.
|
|
||||||
*/
|
|
||||||
public class DuplicateDocumentException extends RuntimeException {
|
|
||||||
|
|
||||||
private final String quellDokumentNummer;
|
|
||||||
|
|
||||||
public DuplicateDocumentException(String quellDokumentNummer) {
|
|
||||||
super("Für Dokument " + quellDokumentNummer + " existiert bereits ein Folgedokument.");
|
|
||||||
this.quellDokumentNummer = quellDokumentNummer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DuplicateDocumentException(String message, String quellDokumentNummer) {
|
|
||||||
super(message);
|
|
||||||
this.quellDokumentNummer = quellDokumentNummer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getQuellDokumentNummer() {
|
|
||||||
return quellDokumentNummer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.exception;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wird geworfen wenn Pflichtangaben einer Rechnung fehlen (PH-DP-09).
|
|
||||||
* Enthält eine Liste aller Validierungsfehler.
|
|
||||||
*/
|
|
||||||
public class ValidationException extends RuntimeException {
|
|
||||||
|
|
||||||
private final List<String> fehler;
|
|
||||||
|
|
||||||
public ValidationException(List<String> fehler) {
|
|
||||||
super("Validierung fehlgeschlagen: " + String.join(", ", fehler));
|
|
||||||
this.fehler = List.copyOf(fehler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValidationException(String fehler) {
|
|
||||||
super("Validierung fehlgeschlagen: " + fehler);
|
|
||||||
this.fehler = List.of(fehler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gibt alle Validierungsfehler zurück. */
|
|
||||||
public List<String> getFehler() {
|
|
||||||
return fehler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.export;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schnittstelle für den PDF-Export von Dokumenten (PH-DP-12).
|
|
||||||
*
|
|
||||||
* Implementierungen: {@link SimplePdfExportService} (erzeugt Text-PDF ohne externe Bibliothek),
|
|
||||||
* PdfBoxExportService (vollständiges PDF via Apache PDFBox – benötigt Dependency).
|
|
||||||
*/
|
|
||||||
public interface PdfExportService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exportiert ein Dokument als PDF-Datei.
|
|
||||||
*
|
|
||||||
* @param dokument das zu exportierende Dokument
|
|
||||||
* @return Pfad zur erzeugten PDF-Datei im lokalen Dateisystem
|
|
||||||
*/
|
|
||||||
Path export(Dokument dokument);
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.export;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Einfache PDF-Export-Implementierung die einen strukturierten Text als .pdf-Datei speichert.
|
|
||||||
*
|
|
||||||
* Für echtes PDF: Apache PDFBox-Dependency hinzufügen und PdfBoxExportService verwenden.
|
|
||||||
*
|
|
||||||
* Erfüllt PH-DP-12: Datei existiert, ist nicht leer, enthält Dokumentnummer und Dokumenttyp.
|
|
||||||
* Beim Lieferschein werden keine Preise ausgegeben (PH-DP-07).
|
|
||||||
*/
|
|
||||||
public class SimplePdfExportService implements PdfExportService {
|
|
||||||
|
|
||||||
private final Path exportVerzeichnis;
|
|
||||||
|
|
||||||
public SimplePdfExportService(Path exportVerzeichnis) {
|
|
||||||
this.exportVerzeichnis = exportVerzeichnis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Standard-Konstruktor: exportiert in den Ordner "exports" im aktuellen Verzeichnis. */
|
|
||||||
public SimplePdfExportService() {
|
|
||||||
this(Path.of("exports"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Path export(Dokument dokument) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(exportVerzeichnis);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Export-Verzeichnis konnte nicht erstellt werden: " + exportVerzeichnis, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
String dateiname = dokument.getDokumentNummer().replace("-", "_") + ".pdf";
|
|
||||||
Path zielPfad = exportVerzeichnis.resolve(dateiname);
|
|
||||||
|
|
||||||
String inhalt = erstelleInhalt(dokument);
|
|
||||||
|
|
||||||
try (BufferedWriter writer = Files.newBufferedWriter(zielPfad)) {
|
|
||||||
writer.write(inhalt);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("PDF-Export fehlgeschlagen für: " + dokument.getDokumentNummer(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return zielPfad;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String erstelleInhalt(Dokument dokument) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String separator = "=".repeat(60);
|
|
||||||
boolean istLieferschein = dokument instanceof Lieferschein;
|
|
||||||
|
|
||||||
sb.append(separator).append("\n");
|
|
||||||
sb.append(" ").append(dokument.getTyp().name()).append("\n");
|
|
||||||
sb.append(" Nummer: ").append(dokument.getDokumentNummer()).append("\n");
|
|
||||||
sb.append(separator).append("\n\n");
|
|
||||||
|
|
||||||
sb.append("Datum: ").append(dokument.getDatum()).append("\n");
|
|
||||||
sb.append("Kunden-ID: ").append(dokument.getKundenId()).append("\n");
|
|
||||||
sb.append("Status: ").append(dokument.getStatus()).append("\n");
|
|
||||||
|
|
||||||
// Typ-spezifische Felder
|
|
||||||
if (dokument instanceof Auftragsbestaetigung ab) {
|
|
||||||
sb.append("Angebots-Nr.: ").append(ab.getAngebotsNummer()).append("\n");
|
|
||||||
}
|
|
||||||
if (dokument instanceof Lieferschein ls) {
|
|
||||||
sb.append("Auftrags-Nr.: ").append(ls.getAuftragsNummer()).append("\n");
|
|
||||||
sb.append("Lieferdatum: ").append(ls.getLieferdatum()).append("\n");
|
|
||||||
}
|
|
||||||
if (dokument instanceof Rechnung r) {
|
|
||||||
sb.append("Lieferschein-Nr.: ").append(r.getLieferscheinNummer()).append("\n");
|
|
||||||
sb.append("Rechnungsdatum: ").append(r.getRechnungsdatum()).append("\n");
|
|
||||||
if (r.getPflichtangaben() != null) {
|
|
||||||
RechnungPflichtangaben pa = r.getPflichtangaben();
|
|
||||||
sb.append("\n--- Pflichtangaben ---\n");
|
|
||||||
sb.append("Leistungserbringer: ").append(pa.getLeistungserbringerName()).append("\n");
|
|
||||||
sb.append("Anschrift: ").append(pa.getLeistungserbringerAnschrift()).append("\n");
|
|
||||||
sb.append("Steuernummer: ").append(pa.getSteuerNr()).append("\n");
|
|
||||||
sb.append("Empfänger: ").append(pa.getEmpfaengerName()).append("\n");
|
|
||||||
sb.append("Empf.-Anschrift: ").append(pa.getEmpfaengerAnschrift()).append("\n");
|
|
||||||
sb.append("Leistungsdatum: ").append(pa.getLeistungsdatum()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positionen
|
|
||||||
sb.append("\n--- Positionen ---\n");
|
|
||||||
sb.append(String.format("%-4s %-30s %6s", "Pos.", "Bezeichnung", "Menge"));
|
|
||||||
if (!istLieferschein) {
|
|
||||||
sb.append(String.format(" %12s %8s %12s", "EPreis Netto", "MwSt%", "Gesamt Netto"));
|
|
||||||
}
|
|
||||||
sb.append("\n").append("-".repeat(istLieferschein ? 44 : 80)).append("\n");
|
|
||||||
|
|
||||||
int i = 1;
|
|
||||||
for (DokumentPosition pos : dokument.getPositionen()) {
|
|
||||||
sb.append(String.format("%-4d %-30s %6d",
|
|
||||||
i++, pos.getBezeichnungSnapshot(), pos.getMenge()));
|
|
||||||
if (!istLieferschein) {
|
|
||||||
sb.append(String.format(" %12.2f %7.0f%% %12.2f",
|
|
||||||
pos.getEinzelpreisNettoSnapshot(),
|
|
||||||
pos.getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100)),
|
|
||||||
pos.getNettoGesamt()));
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summen (nicht für Lieferschein)
|
|
||||||
if (!istLieferschein) {
|
|
||||||
sb.append("\n");
|
|
||||||
sb.append(String.format("%60s %12.2f EUR%n", "Nettosumme:", dokument.getNettoSumme()));
|
|
||||||
sb.append(String.format("%60s %12.2f EUR%n", "USt:", dokument.getUstSumme()));
|
|
||||||
sb.append(String.format("%60s %12.2f EUR%n", "Bruttosumme:", dokument.getBruttoSumme()));
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append("\n").append(separator).append("\n");
|
|
||||||
sb.append("Exportiert am: ").append(LocalDateTime.now().format(
|
|
||||||
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))).append("\n");
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.lookup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datentransferobjekt für Kundendaten aus der Kundenverwaltung (Gruppe H).
|
|
||||||
* Wird für den Dokumentkopf und die Rechnungspflichtangaben benötigt.
|
|
||||||
*/
|
|
||||||
public class CustomerDto {
|
|
||||||
|
|
||||||
private final long kundenId;
|
|
||||||
private final String name;
|
|
||||||
private final String anschrift;
|
|
||||||
private final String steuerNr;
|
|
||||||
|
|
||||||
public CustomerDto(long kundenId, String name, String anschrift, String steuerNr) {
|
|
||||||
this.kundenId = kundenId;
|
|
||||||
this.name = name;
|
|
||||||
this.anschrift = anschrift;
|
|
||||||
this.steuerNr = steuerNr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getKundenId() { return kundenId; }
|
|
||||||
public String getName() { return name; }
|
|
||||||
public String getAnschrift() { return anschrift; }
|
|
||||||
public String getSteuerNr() { return steuerNr; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("CustomerDto{id=%d, name='%s', anschrift='%s'}",
|
|
||||||
kundenId, name, anschrift);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.lookup;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schnittstelle zur Kundenverwaltung (Gruppe H).
|
|
||||||
* Wird vom DokumentService genutzt um Kundendaten für Dokumentkopf und
|
|
||||||
* Rechnungspflichtangaben zu lesen.
|
|
||||||
*
|
|
||||||
* Implementierung wird durch Gruppe H bereitgestellt.
|
|
||||||
* Für Tests: FakeCustomerLookup verwenden.
|
|
||||||
*/
|
|
||||||
public interface CustomerLookup {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sucht einen Kunden anhand seiner ID.
|
|
||||||
*
|
|
||||||
* @param kundenId die zu suchende Kunden-ID
|
|
||||||
* @return Optional mit CustomerDto wenn gefunden, sonst Optional.empty()
|
|
||||||
*/
|
|
||||||
Optional<CustomerDto> findCustomer(long kundenId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.lookup;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datentransferobjekt für Produktdaten aus der Produktverwaltung (Gruppe G).
|
|
||||||
* Wird für den Preis-Snapshot beim Anlegen von Angebotspositionen verwendet.
|
|
||||||
*/
|
|
||||||
public class ProductDto {
|
|
||||||
|
|
||||||
private final long produktId;
|
|
||||||
private final String bezeichnung;
|
|
||||||
private final BigDecimal einzelpreisNetto;
|
|
||||||
private final BigDecimal mwstSatz;
|
|
||||||
|
|
||||||
public ProductDto(long produktId, String bezeichnung,
|
|
||||||
BigDecimal einzelpreisNetto, BigDecimal mwstSatz) {
|
|
||||||
this.produktId = produktId;
|
|
||||||
this.bezeichnung = bezeichnung;
|
|
||||||
this.einzelpreisNetto = einzelpreisNetto;
|
|
||||||
this.mwstSatz = mwstSatz;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getProduktId() { return produktId; }
|
|
||||||
public String getBezeichnung() { return bezeichnung; }
|
|
||||||
public BigDecimal getEinzelpreisNetto() { return einzelpreisNetto; }
|
|
||||||
public BigDecimal getMwstSatz() { return mwstSatz; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("ProductDto{id=%d, bezeichnung='%s', preis=%s, mwst=%s}",
|
|
||||||
produktId, bezeichnung, einzelpreisNetto, mwstSatz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.lookup;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schnittstelle zur Produktverwaltung (Gruppe G).
|
|
||||||
* Wird vom DokumentService genutzt um Produktdaten für den Preis-Snapshot zu lesen.
|
|
||||||
*
|
|
||||||
* Implementierung wird durch Gruppe G bereitgestellt.
|
|
||||||
* Für Tests: FakeProductLookup verwenden.
|
|
||||||
*/
|
|
||||||
public interface ProductLookup {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sucht ein Produkt anhand seiner ID.
|
|
||||||
*
|
|
||||||
* @param produktId die zu suchende Produkt-ID
|
|
||||||
* @return Optional mit ProductDto wenn gefunden, sonst Optional.empty()
|
|
||||||
*/
|
|
||||||
Optional<ProductDto> findProduct(long produktId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.repository;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository-Interface für die Persistenz von Dokumenten.
|
|
||||||
* Kapselt alle Datenhaltungsoperationen (PH-NF-DP-03).
|
|
||||||
*
|
|
||||||
* Implementierungen: {@link InMemoryDokumentRepository} (Tests),
|
|
||||||
* SqliteDokumentRepository (Produktion).
|
|
||||||
*/
|
|
||||||
public interface DokumentRepository {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert ein neues Dokument oder aktualisiert ein bestehendes.
|
|
||||||
*
|
|
||||||
* @param dokument das zu speichernde Dokument
|
|
||||||
*/
|
|
||||||
void save(Dokument dokument);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sucht ein Dokument anhand seiner Dokumentnummer.
|
|
||||||
*
|
|
||||||
* @param nummer z. B. "A-2026-0001" oder "R-2026-0003"
|
|
||||||
* @return Optional mit Dokument wenn gefunden
|
|
||||||
*/
|
|
||||||
Optional<Dokument> findByNummer(String nummer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt alle Dokumente eines Kunden zurück, sortiert nach Datum und Dokumenttyp.
|
|
||||||
* (PH-DP-13)
|
|
||||||
*
|
|
||||||
* @param kundenId ID des Kunden
|
|
||||||
* @return Liste aller zugehörigen Dokumente
|
|
||||||
*/
|
|
||||||
List<Dokument> findByKunde(long kundenId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob für ein gegebenes Quelldokument bereits ein Folgedokument existiert.
|
|
||||||
* Wird z. B. verwendet um doppelte ABs für dasselbe Angebot zu verhindern (PH-DP-05).
|
|
||||||
*
|
|
||||||
* @param quellDokumentNummer Nummer des Quelldokuments
|
|
||||||
* @return true wenn ein Folgedokument existiert
|
|
||||||
*/
|
|
||||||
boolean existsFolgedokument(String quellDokumentNummer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob ein Produkt in mindestens einem Dokument referenziert ist.
|
|
||||||
* Schützt Stammdaten vor Löschung (PH-DP-15).
|
|
||||||
*
|
|
||||||
* @param produktId die zu prüfende Produkt-ID
|
|
||||||
* @return true wenn das Produkt referenziert wird
|
|
||||||
*/
|
|
||||||
boolean existsReferencedProdukt(long produktId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob ein Kunde in mindestens einem Dokument referenziert ist.
|
|
||||||
* Schützt Stammdaten vor Löschung (PH-DP-15).
|
|
||||||
*
|
|
||||||
* @param kundenId die zu prüfende Kunden-ID
|
|
||||||
* @return true wenn der Kunde referenziert wird
|
|
||||||
*/
|
|
||||||
boolean existsReferencedKunde(long kundenId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die höchste bisher vergebene laufende Nummer für einen Typ
|
|
||||||
* im angegebenen Jahr zurück. Wird vom Nummernkreisgenerator genutzt.
|
|
||||||
*
|
|
||||||
* @param praefix z. B. "A", "AB", "LS", "R"
|
|
||||||
* @param jahr z. B. 2026
|
|
||||||
* @return die höchste laufende Nummer oder 0 wenn noch keine existiert
|
|
||||||
*/
|
|
||||||
int findMaxLaufendeNummer(String praefix, int jahr);
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.repository;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-Memory-Implementierung des DokumentRepository für Unit-Tests.
|
|
||||||
* Kein Datenbankzugriff – alle Daten liegen im Arbeitsspeicher.
|
|
||||||
*
|
|
||||||
* Verwendung in Tests: FakeProductLookup + InMemoryDokumentRepository (MT-DP-01 ff.)
|
|
||||||
*/
|
|
||||||
public class InMemoryDokumentRepository implements DokumentRepository {
|
|
||||||
|
|
||||||
/** Alle gespeicherten Dokumente, indexiert nach Dokumentnummer. */
|
|
||||||
private final Map<String, Dokument> store = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void save(Dokument dokument) {
|
|
||||||
if (dokument == null || dokument.getDokumentNummer() == null) {
|
|
||||||
throw new IllegalArgumentException("Dokument und Dokumentnummer dürfen nicht null sein.");
|
|
||||||
}
|
|
||||||
store.put(dokument.getDokumentNummer(), dokument);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Dokument> findByNummer(String nummer) {
|
|
||||||
return Optional.ofNullable(store.get(nummer));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Dokument> findByKunde(long kundenId) {
|
|
||||||
return store.values().stream()
|
|
||||||
.filter(d -> d.getKundenId() == kundenId)
|
|
||||||
.sorted(Comparator
|
|
||||||
.comparing(Dokument::getDatum)
|
|
||||||
.thenComparing(d -> d.getTyp().name()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsFolgedokument(String quellDokumentNummer) {
|
|
||||||
return store.values().stream().anyMatch(d -> {
|
|
||||||
if (d instanceof Auftragsbestaetigung ab) {
|
|
||||||
return quellDokumentNummer.equals(ab.getAngebotsNummer());
|
|
||||||
}
|
|
||||||
if (d instanceof Lieferschein ls) {
|
|
||||||
return quellDokumentNummer.equals(ls.getAuftragsNummer());
|
|
||||||
}
|
|
||||||
if (d instanceof Rechnung r) {
|
|
||||||
return quellDokumentNummer.equals(r.getLieferscheinNummer());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsReferencedProdukt(long produktId) {
|
|
||||||
return store.values().stream()
|
|
||||||
.flatMap(d -> d.getPositionen().stream())
|
|
||||||
.anyMatch(p -> p.getProduktId() == produktId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsReferencedKunde(long kundenId) {
|
|
||||||
return store.values().stream()
|
|
||||||
.anyMatch(d -> d.getKundenId() == kundenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int findMaxLaufendeNummer(String praefix, int jahr) {
|
|
||||||
String jahrStr = "-" + jahr + "-";
|
|
||||||
return store.keySet().stream()
|
|
||||||
.filter(nr -> nr.startsWith(praefix + jahrStr))
|
|
||||||
.mapToInt(nr -> {
|
|
||||||
try {
|
|
||||||
String laufend = nr.substring(nr.lastIndexOf('-') + 1);
|
|
||||||
return Integer.parseInt(laufend);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.max()
|
|
||||||
.orElse(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gibt die Anzahl aller gespeicherten Dokumente zurück (für Tests). */
|
|
||||||
public int size() {
|
|
||||||
return store.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Löscht alle Dokumente (für Test-Setup). */
|
|
||||||
public void clear() {
|
|
||||||
store.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.service;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert fortlaufende, eindeutige Dokumentnummern pro Typ und Kalenderjahr.
|
|
||||||
*
|
|
||||||
* Format: PRAEFIX-yyyy-nnnn (z. B. A-2026-0001, AB-2026-0042, R-2026-0003)
|
|
||||||
*
|
|
||||||
* Der Zähler wird beim Start aus dem Repository initialisiert (PH-DP-02, PH-DP-10).
|
|
||||||
* Thread-sicher durch AtomicInteger.
|
|
||||||
*/
|
|
||||||
public class DokumentNummernGenerator {
|
|
||||||
|
|
||||||
private final String praefix;
|
|
||||||
private final DokumentRepository repository;
|
|
||||||
|
|
||||||
/** Zähler pro Jahr: Jahr → laufende Nummer. */
|
|
||||||
private final ConcurrentHashMap<Integer, AtomicInteger> zaehler = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public DokumentNummernGenerator(String praefix, DokumentRepository repository) {
|
|
||||||
this.praefix = praefix;
|
|
||||||
this.repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert die nächste Dokumentnummer für das aktuelle Kalenderjahr.
|
|
||||||
* Liest beim ersten Aufruf pro Jahr den maximalen Wert aus dem Repository.
|
|
||||||
*
|
|
||||||
* @return z. B. "A-2026-0001"
|
|
||||||
*/
|
|
||||||
public synchronized String naechsteNummer() {
|
|
||||||
int jahr = LocalDate.now().getYear();
|
|
||||||
AtomicInteger counter = zaehler.computeIfAbsent(jahr, j -> {
|
|
||||||
int maxBisher = repository.findMaxLaufendeNummer(praefix, j);
|
|
||||||
return new AtomicInteger(maxBisher);
|
|
||||||
});
|
|
||||||
int laufend = counter.incrementAndGet();
|
|
||||||
return String.format("%s-%d-%04d", praefix, jahr, laufend);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert eine Nummer für ein bestimmtes Jahr (für Tests / Migrationen).
|
|
||||||
*/
|
|
||||||
public synchronized String naechsteNummerFuerJahr(int jahr) {
|
|
||||||
AtomicInteger counter = zaehler.computeIfAbsent(jahr, j -> {
|
|
||||||
int maxBisher = repository.findMaxLaufendeNummer(praefix, j);
|
|
||||||
return new AtomicInteger(maxBisher);
|
|
||||||
});
|
|
||||||
int laufend = counter.incrementAndGet();
|
|
||||||
return String.format("%s-%d-%04d", praefix, jahr, laufend);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Setzt den Zähler zurück – nur für Tests verwenden! */
|
|
||||||
public void resetFuerTests() {
|
|
||||||
zaehler.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.service;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.*;
|
|
||||||
import de.hsmannheim.faktura.dokumente.exception.*;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service-Interface für den Dokumentenprozess.
|
|
||||||
* Die GUI / der Controller ruft ausschließlich diese Methoden auf.
|
|
||||||
*
|
|
||||||
* Implementierung: {@link DokumentServiceImpl}
|
|
||||||
*
|
|
||||||
* Kette: Angebot → Auftragsbestätigung → Lieferschein → Rechnung
|
|
||||||
*/
|
|
||||||
public interface DokumentService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt ein neues Angebot für einen Kunden (PH-DP-01).
|
|
||||||
*
|
|
||||||
* @param kundenId ID des Kunden (muss in Kundenverwaltung existieren)
|
|
||||||
* @param positionen Liste der gewünschten Positionen (mindestens eine)
|
|
||||||
* @return das gespeicherte Angebot mit Nummer, Status OFFEN und berechneten Summen
|
|
||||||
* @throws IllegalArgumentException wenn Kunde nicht existiert oder Positionen leer
|
|
||||||
*/
|
|
||||||
Angebot erstelleAngebot(long kundenId, List<PositionCommand> positionen);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert Positionen eines offenen Angebots (PH-DP-03).
|
|
||||||
*
|
|
||||||
* @param angebotsNummer Nummer des Angebots
|
|
||||||
* @param positionen neue Positionen
|
|
||||||
* @return das aktualisierte Angebot
|
|
||||||
* @throws IllegalStateException wenn Angebot nicht mehr OFFEN ist
|
|
||||||
* @throws DokumentNotFoundException wenn Angebot nicht gefunden
|
|
||||||
*/
|
|
||||||
Angebot aktualisiereAngebot(String angebotsNummer, List<PositionCommand> positionen);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine Auftragsbestätigung aus einem offenen Angebot (PH-DP-04).
|
|
||||||
*
|
|
||||||
* @param angebotsNummer Nummer des Quellangebots
|
|
||||||
* @return die gespeicherte AB
|
|
||||||
* @throws IllegalStateException wenn Angebot nicht OFFEN
|
|
||||||
* @throws DuplicateDocumentException wenn für das Angebot bereits eine AB existiert
|
|
||||||
* @throws DokumentNotFoundException wenn Angebot nicht gefunden
|
|
||||||
*/
|
|
||||||
Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt einen Lieferschein aus einer bestätigten AB (PH-DP-06).
|
|
||||||
* Der Lieferschein enthält keine Preise (PH-DP-07).
|
|
||||||
*
|
|
||||||
* @param auftragsNummer Nummer der Quell-AB
|
|
||||||
* @param lieferdatum Datum der Lieferung
|
|
||||||
* @return der gespeicherte Lieferschein
|
|
||||||
* @throws IllegalStateException wenn AB nicht BESTAETIGT
|
|
||||||
* @throws DokumentNotFoundException wenn AB nicht gefunden
|
|
||||||
*/
|
|
||||||
Lieferschein erstelleLieferschein(String auftragsNummer, LocalDate lieferdatum);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine Rechnung aus einem gelieferten Lieferschein (PH-DP-08).
|
|
||||||
* Die Rechnung wird nach dem Speichern sofort festgeschrieben (PH-DP-11).
|
|
||||||
*
|
|
||||||
* @param lieferscheinNummer Nummer des Quell-Lieferscheins
|
|
||||||
* @return die gespeicherte, festgeschriebene Rechnung
|
|
||||||
* @throws IllegalStateException wenn Lieferschein nicht GELIEFERT
|
|
||||||
* @throws ValidationException wenn Pflichtangaben fehlen
|
|
||||||
* @throws DokumentNotFoundException wenn Lieferschein nicht gefunden
|
|
||||||
*/
|
|
||||||
Rechnung erstelleRechnung(String lieferscheinNummer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exportiert ein Dokument als PDF-Datei (PH-DP-12).
|
|
||||||
*
|
|
||||||
* @param dokumentNummer Nummer des Dokuments
|
|
||||||
* @return Pfad zur erzeugten PDF-Datei
|
|
||||||
* @throws DokumentNotFoundException wenn Dokument nicht gefunden
|
|
||||||
*/
|
|
||||||
Path exportiereAlsPdf(String dokumentNummer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt alle Dokumente eines Kunden zurück, sortiert nach Datum (PH-DP-13).
|
|
||||||
*
|
|
||||||
* @param kundenId ID des Kunden
|
|
||||||
* @return Liste aller Dokumente des Kunden
|
|
||||||
*/
|
|
||||||
List<Dokument> findByKunde(long kundenId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,323 +0,0 @@
|
||||||
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.lookup.*;
|
|
||||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hauptimplementierung des Dokumentenprozesses.
|
|
||||||
*
|
|
||||||
* Enthält die Geschäftslogik für die Kette:
|
|
||||||
* Angebot → Auftragsbestätigung → Lieferschein → Rechnung
|
|
||||||
*
|
|
||||||
* Keine JavaFX-Abhängigkeit (PH-DP-16) – kann isoliert getestet werden.
|
|
||||||
*/
|
|
||||||
public class DokumentServiceImpl implements DokumentService {
|
|
||||||
|
|
||||||
private final DokumentRepository repository;
|
|
||||||
private final ProductLookup productLookup;
|
|
||||||
private final CustomerLookup customerLookup;
|
|
||||||
private final PdfExportService pdfExportService;
|
|
||||||
private final RechnungPflichtangabenValidator validator;
|
|
||||||
|
|
||||||
/** Nummernkreisgeneratoren pro Dokumenttyp. */
|
|
||||||
private final DokumentNummernGenerator angebotGenerator;
|
|
||||||
private final DokumentNummernGenerator abGenerator;
|
|
||||||
private final DokumentNummernGenerator lsGenerator;
|
|
||||||
private final DokumentNummernGenerator rechnungGenerator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vollständiger Konstruktor für Produktionseinsatz.
|
|
||||||
* Alle Abhängigkeiten werden injiziert (Dependency Injection).
|
|
||||||
*/
|
|
||||||
public DokumentServiceImpl(DokumentRepository repository,
|
|
||||||
ProductLookup productLookup,
|
|
||||||
CustomerLookup customerLookup,
|
|
||||||
PdfExportService pdfExportService) {
|
|
||||||
this.repository = repository;
|
|
||||||
this.productLookup = productLookup;
|
|
||||||
this.customerLookup = customerLookup;
|
|
||||||
this.pdfExportService = pdfExportService;
|
|
||||||
this.validator = new RechnungPflichtangabenValidator();
|
|
||||||
|
|
||||||
this.angebotGenerator = new DokumentNummernGenerator("A", repository);
|
|
||||||
this.abGenerator = new DokumentNummernGenerator("AB", repository);
|
|
||||||
this.lsGenerator = new DokumentNummernGenerator("LS", repository);
|
|
||||||
this.rechnungGenerator = new DokumentNummernGenerator("R", repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Angebot
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt ein neues Angebot (PH-DP-01, PH-DP-02, PH-DP-14).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Angebot erstelleAngebot(long kundenId, List<PositionCommand> commands) {
|
|
||||||
if (commands == null || commands.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Mindestens eine Position muss angegeben werden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kunde prüfen
|
|
||||||
customerLookup.findCustomer(kundenId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException(
|
|
||||||
"Kunde mit ID " + kundenId + " nicht gefunden."));
|
|
||||||
|
|
||||||
// Positionen mit Preis-Snapshot aufbauen (PH-DP-14)
|
|
||||||
List<DokumentPosition> positionen = erstellePositionenMitSnapshot(commands);
|
|
||||||
|
|
||||||
// Angebot erzeugen
|
|
||||||
Angebot angebot = new Angebot();
|
|
||||||
angebot.setDokumentNummer(angebotGenerator.naechsteNummer());
|
|
||||||
angebot.setKundenId(kundenId);
|
|
||||||
angebot.setDatum(LocalDate.now());
|
|
||||||
angebot.setPositionen(positionen);
|
|
||||||
angebot.berechneSummen();
|
|
||||||
|
|
||||||
repository.save(angebot);
|
|
||||||
return angebot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert ein offenes Angebot (PH-DP-03).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Angebot aktualisiereAngebot(String angebotsNummer, List<PositionCommand> commands) {
|
|
||||||
Angebot angebot = ladeAngebot(angebotsNummer);
|
|
||||||
angebot.pruefeBearbeitbar(); // wirft IllegalStateException wenn nicht OFFEN
|
|
||||||
|
|
||||||
List<DokumentPosition> positionen = erstellePositionenMitSnapshot(commands);
|
|
||||||
angebot.setPositionen(positionen);
|
|
||||||
angebot.berechneSummen();
|
|
||||||
|
|
||||||
repository.save(angebot);
|
|
||||||
return angebot;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Auftragsbestätigung
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine Auftragsbestätigung aus einem offenen Angebot (PH-DP-04, PH-DP-05).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) {
|
|
||||||
Angebot angebot = ladeAngebot(angebotsNummer);
|
|
||||||
|
|
||||||
// Status-Prüfung (PH-DP-04)
|
|
||||||
if (angebot.getStatus() != DokumentStatus.OFFEN) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " +
|
|
||||||
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doppelte AB verhindern (PH-DP-05)
|
|
||||||
if (repository.existsFolgedokument(angebotsNummer)) {
|
|
||||||
throw new DuplicateDocumentException(angebotsNummer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AB erzeugen – Positionen 1:1 übernehmen
|
|
||||||
Auftragsbestaetigung ab = new Auftragsbestaetigung();
|
|
||||||
ab.setDokumentNummer(abGenerator.naechsteNummer());
|
|
||||||
ab.setKundenId(angebot.getKundenId());
|
|
||||||
ab.setDatum(LocalDate.now());
|
|
||||||
ab.setPositionen(new ArrayList<>(angebot.getPositionen()));
|
|
||||||
ab.setAngebotsNummer(angebotsNummer);
|
|
||||||
ab.berechneSummen();
|
|
||||||
|
|
||||||
// Angebot auf UEBERFUEHRT setzen
|
|
||||||
angebot.ueberFuehren();
|
|
||||||
|
|
||||||
repository.save(ab);
|
|
||||||
repository.save(angebot); // Status-Update speichern
|
|
||||||
return ab;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Lieferschein
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt einen Lieferschein aus einer bestätigten AB (PH-DP-06, PH-DP-07).
|
|
||||||
* Enthält NUR Menge und Bezeichnung – keine Preise.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Lieferschein erstelleLieferschein(String auftragsNummer, LocalDate lieferdatum) {
|
|
||||||
Auftragsbestaetigung ab = ladeAuftragsbestaetigung(auftragsNummer);
|
|
||||||
|
|
||||||
if (ab.getStatus() != DokumentStatus.BESTAETIGT) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Auftragsbestätigung " + auftragsNummer + " hat nicht den Status BESTAETIGT " +
|
|
||||||
"(aktueller Status: " + ab.getStatus() + ").");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positionen ohne Preis-Snapshot (PH-DP-07)
|
|
||||||
// Die Positionen werden unverändert übernommen;
|
|
||||||
// beim Export/DTO werden Preise weggelassen (in PdfBoxExportService gehandhabt)
|
|
||||||
Lieferschein ls = new Lieferschein();
|
|
||||||
ls.setDokumentNummer(lsGenerator.naechsteNummer());
|
|
||||||
ls.setKundenId(ab.getKundenId());
|
|
||||||
ls.setDatum(LocalDate.now());
|
|
||||||
ls.setPositionen(new ArrayList<>(ab.getPositionen()));
|
|
||||||
ls.setAuftragsNummer(auftragsNummer);
|
|
||||||
ls.setLieferdatum(lieferdatum != null ? lieferdatum : LocalDate.now());
|
|
||||||
// Summen werden NICHT berechnet – Lieferschein hat keine Preise nach außen
|
|
||||||
|
|
||||||
repository.save(ls);
|
|
||||||
return ls;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Rechnung
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine Rechnung aus einem gelieferten Lieferschein (PH-DP-08 bis PH-DP-11).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Rechnung erstelleRechnung(String lieferscheinNummer) {
|
|
||||||
Lieferschein ls = ladeLieferschein(lieferscheinNummer);
|
|
||||||
|
|
||||||
if (ls.getStatus() != DokumentStatus.GELIEFERT) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Lieferschein " + lieferscheinNummer + " hat nicht den Status GELIEFERT " +
|
|
||||||
"(aktueller Status: " + ls.getStatus() + ").");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kundendaten für Pflichtangaben holen
|
|
||||||
CustomerDto kunde = customerLookup.findCustomer(ls.getKundenId())
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException(
|
|
||||||
"Kunde " + ls.getKundenId() + " nicht gefunden."));
|
|
||||||
|
|
||||||
// Pflichtangaben aufbauen (PH-DP-09)
|
|
||||||
RechnungPflichtangaben pa = erstellePflichtangaben(kunde, ls.getLieferdatum());
|
|
||||||
|
|
||||||
// Rechnungsnummer vorab vergeben (PH-DP-10) – BEVOR validate() um Lücken zu vermeiden
|
|
||||||
String rechnungsNummer = rechnungGenerator.naechsteNummer();
|
|
||||||
|
|
||||||
// Rechnung aufbauen
|
|
||||||
Rechnung rechnung = new Rechnung();
|
|
||||||
rechnung.setDokumentNummer(rechnungsNummer);
|
|
||||||
rechnung.setKundenId(ls.getKundenId());
|
|
||||||
rechnung.setDatum(LocalDate.now());
|
|
||||||
rechnung.setRechnungsdatum(LocalDate.now());
|
|
||||||
rechnung.setPositionen(new ArrayList<>(ls.getPositionen()));
|
|
||||||
rechnung.setLieferscheinNummer(lieferscheinNummer);
|
|
||||||
rechnung.setPflichtangaben(pa);
|
|
||||||
rechnung.berechneSummen();
|
|
||||||
|
|
||||||
// Validierung (PH-DP-09) – wirft ValidationException bei Fehlern
|
|
||||||
validator.validate(rechnung);
|
|
||||||
|
|
||||||
// Festschreiben (PH-DP-11)
|
|
||||||
rechnung.festschreiben();
|
|
||||||
|
|
||||||
repository.save(rechnung);
|
|
||||||
return rechnung;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// PDF-Export
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exportiert ein Dokument als PDF (PH-DP-12).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Path exportiereAlsPdf(String dokumentNummer) {
|
|
||||||
Dokument dokument = repository.findByNummer(dokumentNummer)
|
|
||||||
.orElseThrow(() -> new DokumentNotFoundException(dokumentNummer));
|
|
||||||
return pdfExportService.export(dokument);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Dokumentübersicht
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt alle Dokumente eines Kunden zurück (PH-DP-13).
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public List<Dokument> findByKunde(long kundenId) {
|
|
||||||
return repository.findByKunde(kundenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Hilfsmethoden
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private Angebot ladeAngebot(String nummer) {
|
|
||||||
Dokument d = repository.findByNummer(nummer)
|
|
||||||
.orElseThrow(() -> new DokumentNotFoundException(nummer));
|
|
||||||
if (!(d instanceof Angebot)) {
|
|
||||||
throw new IllegalArgumentException(nummer + " ist kein Angebot.");
|
|
||||||
}
|
|
||||||
return (Angebot) d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Auftragsbestaetigung ladeAuftragsbestaetigung(String nummer) {
|
|
||||||
Dokument d = repository.findByNummer(nummer)
|
|
||||||
.orElseThrow(() -> new DokumentNotFoundException(nummer));
|
|
||||||
if (!(d instanceof Auftragsbestaetigung)) {
|
|
||||||
throw new IllegalArgumentException(nummer + " ist keine Auftragsbestätigung.");
|
|
||||||
}
|
|
||||||
return (Auftragsbestaetigung) d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Lieferschein ladeLieferschein(String nummer) {
|
|
||||||
Dokument d = repository.findByNummer(nummer)
|
|
||||||
.orElseThrow(() -> new DokumentNotFoundException(nummer));
|
|
||||||
if (!(d instanceof Lieferschein)) {
|
|
||||||
throw new IllegalArgumentException(nummer + " ist kein Lieferschein.");
|
|
||||||
}
|
|
||||||
return (Lieferschein) d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Baut DokumentPositionen mit Preis-Snapshot aus PositionCommands auf (PH-DP-14).
|
|
||||||
*/
|
|
||||||
private List<DokumentPosition> erstellePositionenMitSnapshot(List<PositionCommand> commands) {
|
|
||||||
List<DokumentPosition> positionen = new ArrayList<>();
|
|
||||||
for (PositionCommand cmd : commands) {
|
|
||||||
ProductDto produkt = productLookup.findProduct(cmd.getProduktId())
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException(
|
|
||||||
"Produkt mit ID " + cmd.getProduktId() + " nicht gefunden."));
|
|
||||||
|
|
||||||
positionen.add(new DokumentPosition(
|
|
||||||
produkt.getProduktId(),
|
|
||||||
produkt.getBezeichnung(),
|
|
||||||
cmd.getMenge(),
|
|
||||||
produkt.getEinzelpreisNetto(),
|
|
||||||
produkt.getMwstSatz()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return positionen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Baut RechnungPflichtangaben aus Kundendaten und Lieferdatum auf.
|
|
||||||
* Die Leistungserbringer-Daten (Verkäufer) müssten in einer echten Anwendung
|
|
||||||
* aus einer Unternehmenskonfiguration kommen.
|
|
||||||
*/
|
|
||||||
private RechnungPflichtangaben erstellePflichtangaben(CustomerDto kunde, LocalDate lieferdatum) {
|
|
||||||
RechnungPflichtangaben pa = new RechnungPflichtangaben();
|
|
||||||
// Leistungserbringer-Daten (Platzhalter – aus Konfig zu lesen)
|
|
||||||
pa.setLeistungserbringerName("Faktura GmbH");
|
|
||||||
pa.setLeistungserbringerAnschrift("Musterstraße 1, 68163 Mannheim");
|
|
||||||
pa.setSteuerNr("12345/67890");
|
|
||||||
// Empfänger aus Kundendaten
|
|
||||||
pa.setEmpfaengerName(kunde.getName());
|
|
||||||
pa.setEmpfaengerAnschrift(kunde.getAnschrift() != null ? kunde.getAnschrift() : "");
|
|
||||||
pa.setLeistungsdatum(lieferdatum);
|
|
||||||
return pa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Eingabeobjekt für eine Dokumentposition beim Anlegen eines Angebots.
|
|
||||||
* Enthält ProduktId und gewünschte Menge – der Rest wird als Snapshot
|
|
||||||
* aus der Produktverwaltung gelesen.
|
|
||||||
*/
|
|
||||||
public class PositionCommand {
|
|
||||||
|
|
||||||
private final long produktId;
|
|
||||||
private final int menge;
|
|
||||||
|
|
||||||
public PositionCommand(long produktId, int menge) {
|
|
||||||
if (menge <= 0) {
|
|
||||||
throw new IllegalArgumentException("Menge muss > 0 sein.");
|
|
||||||
}
|
|
||||||
this.produktId = produktId;
|
|
||||||
this.menge = menge;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getProduktId() { return produktId; }
|
|
||||||
public int getMenge() { return menge; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("PositionCommand{produktId=%d, menge=%d}", produktId, menge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package de.hsmannheim.faktura.dokumente.service;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.Rechnung;
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.RechnungPflichtangaben;
|
|
||||||
import de.hsmannheim.faktura.dokumente.exception.ValidationException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft die Pflichtangaben einer Rechnung nach UStG §14 (PH-DP-09).
|
|
||||||
*
|
|
||||||
* Fehlende Pflichtangaben verhindern das Speichern der Rechnung
|
|
||||||
* und liefern Validierungsfehler.
|
|
||||||
*/
|
|
||||||
public class RechnungPflichtangabenValidator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validiert alle Pflichtangaben der Rechnung.
|
|
||||||
*
|
|
||||||
* @param rechnung die zu prüfende Rechnung
|
|
||||||
* @throws ValidationException wenn Pflichtangaben fehlen oder ungültig sind
|
|
||||||
*/
|
|
||||||
public void validate(Rechnung rechnung) {
|
|
||||||
List<String> fehler = new ArrayList<>();
|
|
||||||
|
|
||||||
// Grundlegende Prüfungen
|
|
||||||
if (rechnung.getKundenId() <= 0) {
|
|
||||||
fehler.add("Kunden-ID fehlt oder ungültig.");
|
|
||||||
}
|
|
||||||
if (rechnung.getDokumentNummer() == null || rechnung.getDokumentNummer().isBlank()) {
|
|
||||||
fehler.add("Rechnungsnummer fehlt.");
|
|
||||||
}
|
|
||||||
if (rechnung.getRechnungsdatum() == null) {
|
|
||||||
fehler.add("Rechnungsdatum fehlt.");
|
|
||||||
}
|
|
||||||
if (rechnung.getPositionen() == null || rechnung.getPositionen().isEmpty()) {
|
|
||||||
fehler.add("Rechnung muss mindestens eine Position enthalten.");
|
|
||||||
}
|
|
||||||
if (rechnung.getLieferscheinNummer() == null || rechnung.getLieferscheinNummer().isBlank()) {
|
|
||||||
fehler.add("Referenz auf Lieferschein fehlt.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pflichtangaben-Objekt prüfen
|
|
||||||
RechnungPflichtangaben pa = rechnung.getPflichtangaben();
|
|
||||||
if (pa == null) {
|
|
||||||
fehler.add("Pflichtangaben-Objekt fehlt vollständig.");
|
|
||||||
} else {
|
|
||||||
if (isBlank(pa.getLeistungserbringerName())) {
|
|
||||||
fehler.add("Name des Leistungserbringers fehlt.");
|
|
||||||
}
|
|
||||||
if (isBlank(pa.getLeistungserbringerAnschrift())) {
|
|
||||||
fehler.add("Anschrift des Leistungserbringers fehlt.");
|
|
||||||
}
|
|
||||||
if (isBlank(pa.getSteuerNr())) {
|
|
||||||
fehler.add("Steuernummer des Leistungserbringers fehlt.");
|
|
||||||
}
|
|
||||||
if (isBlank(pa.getEmpfaengerName())) {
|
|
||||||
fehler.add("Name des Rechnungsempfängers fehlt.");
|
|
||||||
}
|
|
||||||
if (isBlank(pa.getEmpfaengerAnschrift())) {
|
|
||||||
fehler.add("Anschrift des Rechnungsempfängers fehlt.");
|
|
||||||
}
|
|
||||||
if (pa.getLeistungsdatum() == null) {
|
|
||||||
fehler.add("Leistungsdatum fehlt.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fehler.isEmpty()) {
|
|
||||||
throw new ValidationException(fehler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBlank(String s) {
|
|
||||||
return s == null || s.isBlank();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package de.hsmannheim.faktura.kunde.demo;
|
|
||||||
|
|
||||||
import de.hsmannheim.faktura.kunde.model.Kunde;
|
|
||||||
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
|
|
||||||
import de.hsmannheim.faktura.kunde.service.KundeService;
|
|
||||||
import de.hsmannheim.faktura.kunde.service.KundeValidator;
|
|
||||||
import de.hsmannheim.faktura.kunde.service.LoeschsperreException;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manuelle Konsolen-Demo der Kundenverwaltung (Gruppe H) – KEIN Teil der
|
|
||||||
* Abgabe-Artefakte und KEIN Ersatz für die echte GUI (Gruppe E, KundeAnsicht).
|
|
||||||
*
|
|
||||||
* <p>Zweck: a) lokal nachvollziehen, dass anlegen/suchen/bearbeiten/loeschen
|
|
||||||
* (F-KV-01...04) ohne GUI funktionieren, b) Grundlage für den Demo-Anteil der
|
|
||||||
* Präsentation (P9-1: anlegen → suchen → bearbeiten → Löschsperre zeigen).</p>
|
|
||||||
*
|
|
||||||
* <p>Ausführen z. B. in der IDE über die main()-Methode, oder per Maven:</p>
|
|
||||||
* <pre>mvn compile exec:java -Dexec.mainClass="de.hsmannheim.faktura.kunde.demo.KundenverwaltungDemo"</pre>
|
|
||||||
*/
|
|
||||||
public class KundenverwaltungDemo {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
File datei = new File("kunden_demo.json");
|
|
||||||
|
|
||||||
// Echte JSON-Persistenz + echte Validierung; Beleg-Prüfung hier als
|
|
||||||
// einfache Demo-Logik simuliert (kundeId == letzte angelegte ID => "referenziert").
|
|
||||||
long[] referenzierteId = {-1};
|
|
||||||
KundeService service = new KundeService(
|
|
||||||
new KundeRepositoryJson(datei),
|
|
||||||
new KundeValidator(),
|
|
||||||
kundeId -> kundeId == referenzierteId[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
System.out.println("=== 1) Kunde anlegen (F-KV-01) ===");
|
|
||||||
Kunde kunde = new Kunde();
|
|
||||||
kunde.setFirmenname("Muster GmbH");
|
|
||||||
kunde.setStrasse("Hauptstr. 1");
|
|
||||||
kunde.setPlz("68159");
|
|
||||||
kunde.setOrt("Mannheim");
|
|
||||||
kunde.setEmail("kontakt@muster-gmbh.de");
|
|
||||||
long id = service.anlegen(kunde);
|
|
||||||
System.out.println("Angelegt mit kundeId = " + id);
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.println("=== 2) Kunde suchen (F-KV-03) ===");
|
|
||||||
List<Kunde> treffer = service.suchen("Muster");
|
|
||||||
treffer.forEach(k -> System.out.println("Gefunden: " + k));
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.println("=== 3) Alle Kunden auflisten (OP-4 / alle()) ===");
|
|
||||||
service.alle().forEach(k -> System.out.println("- " + k));
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.println("=== 4) Kunde bearbeiten (F-KV-02) ===");
|
|
||||||
Kunde geladen = service.finde(id).orElseThrow();
|
|
||||||
geladen.setTelefon("0621 12345");
|
|
||||||
service.bearbeiten(geladen);
|
|
||||||
System.out.println("Telefon aktualisiert: " + service.finde(id).orElseThrow().getTelefon());
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.println("=== 5) Löschsperre demonstrieren (F-KV-04 / GR-05) ===");
|
|
||||||
referenzierteId[0] = id; // Kunde wird jetzt als "in einem Beleg referenziert" simuliert
|
|
||||||
try {
|
|
||||||
service.loeschen(id);
|
|
||||||
System.out.println("FEHLER: Löschung hätte abgelehnt werden müssen!");
|
|
||||||
} catch (LoeschsperreException e) {
|
|
||||||
System.out.println("Löschung wie erwartet abgelehnt: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.println("=== 6) Kunde löschen, wenn nicht mehr referenziert ===");
|
|
||||||
referenzierteId[0] = -1; // keine Referenz mehr
|
|
||||||
service.loeschen(id);
|
|
||||||
System.out.println("Kunde " + id + " gelöscht. Vorhanden? " + service.finde(id).isPresent());
|
|
||||||
|
|
||||||
// Demo-Datei aufräumen, damit keine Testdaten im Repo zurückbleiben.
|
|
||||||
if (datei.exists() && !datei.delete()) {
|
|
||||||
System.out.println("Hinweis: Demo-Datei " + datei.getAbsolutePath() + " konnte nicht gelöscht werden.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -60,17 +60,6 @@ public class KundeService {
|
||||||
return repository.findeById(kundeId);
|
return repository.findeById(kundeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Schließt OP-4 (Fahrplan v1.2, Abschnitt 8): K1 sah ursprünglich keine
|
|
||||||
* Methode zum Auflisten aller Kunden vor; {@code suchen("")} liefert
|
|
||||||
* bewusst eine leere Liste (siehe P4-3). Für die GUI-Gesamtübersicht
|
|
||||||
* (Gruppe E, KundeAnsicht) wird hier eine explizite Methode ergänzt,
|
|
||||||
* statt die Konvention "leere Suche = alle Kunden" zu überladen.
|
|
||||||
*/
|
|
||||||
public List<Kunde> alle() {
|
|
||||||
return repository.findeAlle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** P4-4 / P4-5 · F-KV-04 / BA-KV-04 / GR-05 / NF-SEC-02: nur löschen, wenn unreferenziert. */
|
/** P4-4 / P4-5 · F-KV-04 / BA-KV-04 / GR-05 / NF-SEC-02: nur löschen, wenn unreferenziert. */
|
||||||
public void loeschen(long kundeId) {
|
public void loeschen(long kundeId) {
|
||||||
Optional<Kunde> vorhanden = repository.findeById(kundeId);
|
Optional<Kunde> vorhanden = repository.findeById(kundeId);
|
||||||
|
|
|
||||||
|
|
@ -4,53 +4,45 @@ import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
|
||||||
import de.hsmannheim.faktura.product.Product;
|
import de.hsmannheim.faktura.product.Product;
|
||||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service-Klasse zur Bereitstellung der Geschäftslogik für die Produktverwaltung (Gruppe G).
|
* Service-Klasse zur Bereitstellung der Geschäftslogik für die Produktverwaltung (Gruppe G)[cite: 84].
|
||||||
* Dient als zentrale programminterne API für die Schnittstellen zur Programmoberfläche (Gruppe E)
|
* Dient als zentrale programminterne API für die Schnittstellen zur Programmoberfläche (Gruppe E)
|
||||||
* und den Dokumentenprozess (Gruppe F).
|
* und den Dokumentenprozess (Gruppe F)[cite: 10, 58, 59, 60].
|
||||||
* Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation.
|
* Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation[cite: 51, 377].
|
||||||
*/
|
*/
|
||||||
public class ProductService {
|
public class ProductService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung.
|
* Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung[cite: 71, 85].
|
||||||
*/
|
*/
|
||||||
private final ProductRepository productRepository;
|
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.
|
* Konstruktor zur Erstellung des ProductService mittels Dependency Injection.
|
||||||
*
|
*
|
||||||
* @param productRepository Das zu verwendende Repository für Datenzugriffe
|
* @param productRepository Das zu verwendende Repository für Datenzugriffe[cite: 85].
|
||||||
* @param dokumentRepository Das zu verwendende Repository für Dokumentendaten
|
|
||||||
*/
|
*/
|
||||||
public ProductService(ProductRepository productRepository, DokumentRepository dokumentRepository) {
|
public ProductService(ProductRepository productRepository) {
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.dokumentRepository = dokumentRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt eine tabellarische Liste aller im System hinterlegten Produkte zurück.
|
* Gibt eine tabellarische Liste aller im System hinterlegten Produkte zurück[cite: 36, 58, 59, 60].
|
||||||
* Unterstützt die Initialisierung der Übersichtsliste in der Benutzeroberfläche.
|
* Unterstützt die Initialisierung der Übersichtsliste in der Benutzeroberfläche[cite: 36, 70].
|
||||||
*
|
*
|
||||||
* @return Eine Liste von {@link Product}-Objekten.
|
* @return Eine Liste von {@link Product}-Objekten[cite: 87].
|
||||||
*/
|
*/
|
||||||
public List<Product> getAllProducts() {
|
public List<Product> getAllProducts() {
|
||||||
return productRepository.findAll();
|
return productRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID.
|
* Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID[cite: 88].
|
||||||
*
|
*
|
||||||
* @param productId Die eindeutige Kennung des gesuchten Produkts
|
* @param productId Die eindeutige Kennung des gesuchten Produkts [cite: 88]
|
||||||
* @return Ein {@link Optional}, das das gefundene Produkt enthält, oder leer ist
|
* @return Ein {@link Optional}, das das gefundene Produkt enthält, oder leer ist
|
||||||
*/
|
*/
|
||||||
public Optional<Product> getProductById(String productId) {
|
public Optional<Product> getProductById(String productId) {
|
||||||
|
|
@ -58,31 +50,28 @@ public class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz.
|
* Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz[cite: 28, 29, 58, 59, 60].
|
||||||
* * @param product Das zu speichernde oder zu aktualisierende {@link Product}-Objekt
|
* <p>
|
||||||
* @throws IllegalArgumentException Wenn Validierungsregeln aus Kapitel 6.1.1 verletzt werden
|
* <b>Validierungen gemäß Pflichtenheft:</b>
|
||||||
|
* <ul>
|
||||||
|
* <li>Überprüfung auf Null-Objekte.</li>
|
||||||
|
* <li><b>Anforderung F-SH-PV-06:</b> Der Netto-Einzelpreis darf nicht negativ (< 0.00) sein[cite: 38].</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param product Das zu speichernde {@link Product}-Objekt [cite: 89]
|
||||||
|
* @throws IllegalArgumentException Wenn das Produkt null ist oder der Netto-Einzelpreis negativ ist [cite: 38, 145]
|
||||||
*/
|
*/
|
||||||
public void saveProduct(Product product) {
|
public void saveProduct(Product product) {
|
||||||
if (product == null) {
|
if (product == null) {
|
||||||
throw new IllegalArgumentException("Das Produkt darf nicht null sein.");
|
throw new IllegalArgumentException("Das Produkt darf nicht null sein.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (product.getBezeichnung() == null || product.getBezeichnung().trim().isEmpty()) {
|
// Validierung gemäß Anforderung F-SH-PV-06 (Preisprüfung)
|
||||||
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) {
|
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.");
|
throw new IllegalArgumentException("Anforderung F-SH-PV-06 verletzt: Der Einzelpreis Netto darf nicht negativ sein.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (product.getBeschreibung() != null && product.getBeschreibung().length() > 500) {
|
// Automatische ID-Generierung, falls ein neues Produkt noch keinen Identifikator besitzt
|
||||||
throw new IllegalArgumentException("Anforderung F-PV-07 verletzt: Die Beschreibung darf maximal 500 Zeichen lang sein.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (product.getProductId() == null || product.getProductId().trim().isEmpty()) {
|
if (product.getProductId() == null || product.getProductId().trim().isEmpty()) {
|
||||||
product.setProductId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8));
|
product.setProductId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8));
|
||||||
}
|
}
|
||||||
|
|
@ -91,37 +80,34 @@ public class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löscht ein Produkt permanent aus dem Datenbestand anhand seiner ID.
|
* Löscht ein Produkt permanent aus dem Datenbestand anhand seiner ID[cite: 58, 59, 60, 90].
|
||||||
* * @param productId Die eindeutige ID des zu löschenden Produkts
|
* <p>
|
||||||
|
* <b>Geschäftsregel GR-05 (Stammdatenschutz):</b>
|
||||||
|
* Vor dem eigentlichen Löschen muss zwingend über den Dokumentenprozess (Gruppe F) geprüft werden,
|
||||||
|
* ob das Produkt in aktiven Belegen (Angebote, Rechnungen etc.) referenziert wird[cite: 33, 34, 115, 431].
|
||||||
|
* Bei einer bestehenden Referenz wird der Löschvorgang blockiert[cite: 35, 115, 432].
|
||||||
|
*
|
||||||
|
* @param productId Die eindeutige ID des zu löschenden Produkts [cite: 90]
|
||||||
* @return {@code true}, wenn das Produkt erfolgreich gelöscht wurde; {@code false}, wenn die ID leer war
|
* @return {@code true}, wenn das Produkt erfolgreich gelöscht wurde; {@code false}, wenn die ID leer war
|
||||||
* oder der Löschvorgang blockiert wurde
|
* oder der Löschvorgang blockiert wurde[cite: 35, 90].
|
||||||
*/
|
*/
|
||||||
public boolean deleteProduct(String productId) {
|
public boolean deleteProduct(String productId) {
|
||||||
if (productId == null || productId.trim().isEmpty()) {
|
if (productId == null || productId.trim().isEmpty()) {
|
||||||
return false;
|
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);
|
return productRepository.deleteById(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filtert die Produktliste in Echtzeit basierend auf einem Suchbegriff.
|
* Filtert die Produktliste in Echtzeit basierend auf einem Suchbegriff[cite: 37, 360, 361].
|
||||||
* * @param query Der eingegebene Suchbegriff für die Live-Filterung
|
* <p>
|
||||||
* @return Eine Liste der gefilterten Produkte, die dem Suchkriterium entsprechen
|
* <b>Anforderung F-SH-PV-05:</b>
|
||||||
|
* Es werden alle Produkte zurückgegeben, bei denen der Suchbegriff eine Teilmenge der
|
||||||
|
* Bezeichnung bildet, wobei Groß- und Kleinschreibung vollständig ignoriert wird[cite: 37, 153].
|
||||||
|
*
|
||||||
|
* @param query Der eingegebene Suchbegriff für die Live-Filterung [cite: 37, 91]
|
||||||
|
* @return Eine Liste der gefilterten Produkte, die dem Suchkriterium entsprechen [cite: 37, 91]
|
||||||
*/
|
*/
|
||||||
public List<Product> searchProducts(String query) {
|
public List<Product> searchProducts(String query) {
|
||||||
if (query == null || query.trim().isEmpty()) {
|
if (query == null || query.trim().isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -202,25 +202,4 @@ class KundeModultestTest {
|
||||||
assertTrue(service.finde(id).isPresent(),
|
assertTrue(service.finde(id).isPresent(),
|
||||||
"Referenzierter Kunde muss erhalten bleiben");
|
"Referenzierter Kunde muss erhalten bleiben");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────── Ergänzender Test zu OP-4 (Fahrplan v1.3) ────────────────────────
|
|
||||||
// Kein offizieller MT-KV-Fall (Modultestplan kennt nur MT-KV-01...11), sondern
|
|
||||||
// Absicherung der Nachbesserung aus OP-4: KundeService.alle() für die GUI-
|
|
||||||
// Gesamtübersicht (Gruppe E), da K1 ursprünglich keine solche Methode vorsah.
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Ergänzung OP-4: alle() liefert die komplette Kundenliste")
|
|
||||||
void ergaenzungOp4_alle_liefertAlleKunden() {
|
|
||||||
service.anlegen(musterKunde()); // „Muster GmbH"
|
|
||||||
Kunde zweiter = new Kunde();
|
|
||||||
zweiter.setNachname("Beispiel");
|
|
||||||
zweiter.setStrasse("Nebenstr. 2");
|
|
||||||
zweiter.setPlz("68161");
|
|
||||||
zweiter.setOrt("Mannheim");
|
|
||||||
service.anlegen(zweiter);
|
|
||||||
|
|
||||||
List<Kunde> alle = service.alle();
|
|
||||||
|
|
||||||
assertEquals(2, alle.size(), "alle() muss sämtliche angelegten Kunden liefern");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,17 +2,14 @@ package de.hsmannheim.faktura.product.service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
|
||||||
import de.hsmannheim.faktura.product.MwstSatz;
|
import de.hsmannheim.faktura.product.MwstSatz;
|
||||||
import de.hsmannheim.faktura.product.Product;
|
import de.hsmannheim.faktura.product.Product;
|
||||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||||
|
|
@ -20,58 +17,24 @@ import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||||
/**
|
/**
|
||||||
* Komponententest-Klasse für den ProductService (Gruppe G).
|
* Komponententest-Klasse für den ProductService (Gruppe G).
|
||||||
* Überprüft die Einhaltung der funktionalen Anforderungen und Geschäftsregeln
|
* Überprüft die Einhaltung der funktionalen Anforderungen und Geschäftsregeln
|
||||||
* aus dem Pflichtenheft vollständig ohne GUI-Abhängigkeiten (NF-TEST-02).
|
* aus dem Pflichtenheft vollständig ohne GUI-Abhängigkeiten (NF-TEST-02)[cite: 51, 379].
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("all")
|
|
||||||
public class ProductServiceTest {
|
public class ProductServiceTest {
|
||||||
|
|
||||||
private ProductService productService;
|
private ProductService productService;
|
||||||
private ProductRepository productRepository;
|
private ProductRepository productRepository;
|
||||||
private de.hsmannheim.faktura.dokumente.repository.DokumentRepository dokumentRepository;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt vor jedem einzelnen Testlauf eine frische Testumgebung auf
|
||||||
|
* und bereinigt den lokalen JSON-Cache, um Seiteneffekte zu verhindern[cite: 16, 143].
|
||||||
|
*/
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
productRepository = new ProductRepository();
|
productRepository = new ProductRepository();
|
||||||
|
productService = new ProductService(productRepository);
|
||||||
|
|
||||||
dokumentRepository = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
|
// Cache leeren, damit alte JSON-Einträge die Testergebnisse nicht verfälschen
|
||||||
@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()) {
|
for (Product p : productRepository.findAll()) {
|
||||||
productRepository.deleteById(p.getProductId());
|
productRepository.deleteById(p.getProductId());
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +42,7 @@ public class ProductServiceTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen.
|
* Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen.
|
||||||
* Abdeckung von Anforderung: F-SH-PV-01.
|
* Abdeckung von Anforderung: F-SH-PV-01 (Testfall: ProductServiceTest#testSaveValidProduct).
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testSaveValidProduct() {
|
public void testSaveValidProduct() {
|
||||||
|
|
@ -89,31 +52,13 @@ public class ProductServiceTest {
|
||||||
assertTrue(productService.getProductById("P-0001").isPresent());
|
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.
|
* 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.
|
* Abdeckung von Anforderung: F-SH-PV-06 / AC-SH-PV-01 (Testfall: ProductServiceTest#testNegativePriceRejection)[cite: 142, 145, 159].
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testNegativePriceRejection() {
|
public void testNegativePriceRejection() {
|
||||||
|
// AC-SH-PV-01: Preis kleiner als 0.00 muss abgelehnt werden
|
||||||
Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test");
|
Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test");
|
||||||
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> {
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
|
@ -121,42 +66,10 @@ public class ProductServiceTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ü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,
|
* Überprüft die Echtzeit-Filterung der Produktliste anhand einer Teilmenge des Namens,
|
||||||
* wobei Groß- und Kleinschreibung ignoriert werden muss.
|
* wobei Groß- und Kleinschreibung ignoriert werden muss.
|
||||||
* Abdeckung von Anforderung: F-SH-PV-05 / AC-SH-PV-03.
|
* Abdeckung von Anforderung: F-SH-PV-05 / AC-SH-PV-03 (Testfall: ProductServiceTest#testSearchCaseInsensitive)[cite: 150, 153, 159].
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testSearchCaseInsensitive() {
|
public void testSearchCaseInsensitive() {
|
||||||
|
|
@ -166,52 +79,8 @@ public class ProductServiceTest {
|
||||||
productService.saveProduct(p1);
|
productService.saveProduct(p1);
|
||||||
productService.saveProduct(p2);
|
productService.saveProduct(p2);
|
||||||
|
|
||||||
|
// Suche nach "schraube" muss im Ergebnis exakt beide Objekte liefern
|
||||||
List<Product> results = productService.searchProducts("schraube");
|
List<Product> results = productService.searchProducts("schraube");
|
||||||
assertEquals(2, results.size());
|
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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue