Merge pull request 'feature/kundenverwaltung' (#1) from feature/kundenverwaltung into main

Reviewed-on: #1
pull/3/head
Christopher Lampert 2026-06-25 15:34:20 +02:00
commit 758f98b56f
21 changed files with 681 additions and 101 deletions

7
.gitignore vendored
View File

@ -301,3 +301,10 @@ TSWLatexianTemp*
# Uncomment the next line to have this generated file ignored.
#*Notes.bib
# ---> Java / Maven
target/
*.class
# ---> JetBrains (IntelliJ IDEA)
.idea/
*.iml

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>12.3.1</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds" />
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
</list>
</option>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/SE1_Team_2.iml" filepath="$PROJECT_DIR$/.idea/SE1_Team_2.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -29,8 +29,8 @@ numbersections: false
## Freigabeübersicht
| Rolle | Name | Datum |
|--------------|----------------------------|-------|
| Ersteller | Christopher Lampert | |
|--------------|----------------------------|------------|
| Ersteller | Christopher Lampert | 24.06.2026 |
| Prüfer | Prof. Dr. Gerd Marmitt | |
| Freigebender | SE1 Team 2 (Gruppenleiter) | |
@ -39,9 +39,9 @@ numbersections: false
## Dokumentenhistorie
| Version | Datum | Autor | Änderung |
|---------|------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|---------|------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1.0 | 15.05.2026 | Christopher Lampert | Initiale Erstellung und Konsolidierung des Lastenhefts auf Basis Project Charter v1.1: Erfolgskriterien Z1Z4 SMART; Nicht-Ziele um PDF-/E-Rechnung-Abgrenzung und Authentifizierung präzisiert; BA-PV-01 und BA-KV-01 an Datenobjekt-Pflichtattribute Kap. 6.1 angeglichen; Datenobjekte um Begriffsklärung Bezeichnung/Produktname/Artikelnummer ergänzt; neue Anforderungen BA-DP-07 (PDF-Export, fachlich) und BA-GUI-05 (PDF-Export-Schaltfläche); Modalverben in NF-Anforderungen an Prioritäten angeglichen; NF-TEST-03 Coverage-Schwelle auf 60 % angehoben; NF-MAINT-02 Akzeptanztest auf automatisierte Messung umgestellt; neuer Abschnitt 6.1.7 mit Statusübergangs-Tabelle; Begriff „Auftragsbestätigung" durchgängig verwendet; Traceability-Matrix in Kap. 8.4 entsprechend erweitert. |
| 1.1 | 24.06.2026 | Christopher Lampert | Redaktionelle Überarbeitung nach Review (klärende Änderungen, kein Scope-Eingriff): Freigabeübersicht auf Zeilen-Layout mit Spalten Rolle und Datum umgestellt; Kap. 1.2 um einleitende fachliche Beschreibung der Projektziele ergänzt (Charter-Verweis beibehalten); Kap. 1.4 „Begriffsklärung Lastenheft vs. Pflichtenheft" entfernt und Folgeabschnitt umnummeriert (vormals 1.5 Geltungsbereich → 1.4); Geltungsbereich von Modulumfang auf Zielgruppe/Leserschaft umformuliert; BA-GUI-01 lösungsneutral formuliert („per Dropdown" → „aus den im System vorhandenen Produkten auswählbar"). Offene Punkte und abgeleitete Cross-Dokument-Hinweise in separater Anmerkungsdatei dokumentiert (u. a. Charter-Verweis „siehe Lastenheft Kap. 1.4" sowie Versions-Synchronisation Charter <-> Lastenheft). |
| 1.1 | 25.06.2026 | Christopher Lampert | RÜberarbeitung nach Review: Freigabeübersicht um Datumsangaben ergänzt; Kap. 1.2 um eine fachliche Einleitung zur Herleitung der Projektziele erweitert; Kapitel „Begriffsklärung Lastenheft vs. Pflichtenheft“ entfernt und Kapitelstruktur angepasst; Geltungsbereich auf Zielgruppen und Leserschaft des Dokuments ausgerichtet; organisatorische Rahmenbedingungen aus dem Lastenheft entfernt (Zuordnung zum Project Charter gemäß Review-Hinweis); BA-GUI-01 lösungsneutral formuliert („Dropdown“ entfernt); BA-GUI-02 und BA-GUI-03 hinsichtlich der GUI-Darstellung abstrahiert; technische Rahmenbedingungen präzisiert; Dokumentenhistorie ergänzt sowie redaktionelle Konsistenz- und Strukturverbesserungen durchgeführt. |
---
@ -61,7 +61,7 @@ Ziel dieses Lastenhefts ist die Spezifikation der fachlichen Anforderungen an ei
Die fachlichen Projektziele leiten sich unmittelbar aus dem Geschäftsprozess der Fakturierung ab: Das System soll die durchgängige Bearbeitung von der Pflege der Produkt- und Kundenstammdaten über die Belegerstellung bis zur Rechnungsstellung ermöglichen. Die vier nachfolgenden Ziele (Z1Z4) sind entlang der vier Pflichtmodule gegliedert und jeweils mit messbaren Erfolgskriterien sowie zugeordneten Akzeptanztests hinterlegt. Sie wurden aus Charter v1.1, Kapitel 4, übernommen und für das Lastenheft fachlich verfeinert:
| Nr. | Ziel | Erfolgskriterium |
|-----|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|-----|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Z1 | Produktverwaltung | Alle CRUD-Operationen bei bis zu 1.000 Produkten innerhalb der NF-PERF-01-Antwortzeit von 1 s; Akzeptanztests AT-PV-01 bis AT-PV-08 bestanden; Geschäftsregel GR-05 (Stammdatenschutz) eingehalten. |
| Z2 | Kundenverwaltung | CRUD bei bis zu 1.000 Kunden analog Z1; Akzeptanztests AT-KV-01 bis AT-KV-08 bestanden; DSGVO-konformes Löschen (NF-SEC-02) unter Beachtung der Aufbewahrungspflicht (GR-05) demonstrierbar. |
| Z3 | Dokumentenworkflow | Vollständiger Prozess Angebot &rarr; Auftragsbestätigung &rarr; Lieferschein &rarr; Rechnung an mindestens einem Beispielkunden und -produkt durchgängig demonstrierbar; Rechnung enthält alle Pflichtangaben nach § 14 UStG; Akzeptanztests AT-DP-01 bis AT-DP-07 sowie AT-GR-01 bis AT-GR-06 bestanden. |
@ -80,9 +80,7 @@ Folgende Funktionen sind **explizit nicht** Bestandteil dieses Projekts:
- Mehrsprachigkeit (das System wird ausschließlich in deutscher Sprache realisiert)
- Authentifizierung bzw. Benutzeranmeldung — Begründung: lokale Einbenutzer-Anwendung auf einem PC-Arbeitsplatz; Zugriffsschutz erfolgt durch das Betriebssystem-Login. NF-SEC-01 ergänzt einen Schutz der Datenhaltung auf Dateisystemebene.
### 1.4 Geltungsbereich
Dieses Lastenheft richtet sich an alle Personengruppen, die die fachlichen Anforderungen an das Fakturierungssystem kennen müssen: an den **Auftraggeber** (Prof. Dr. Gerd Marmitt) als Grundlage für die Abnahme, an das **Entwicklungsteam** (Gruppen E, F, G, H) als verbindliche Vorgabe für Pflichtenheft, Architektur und Implementierung sowie an die für die **Akzeptanztests** verantwortlichen Personen. Inhaltlich deckt das Dokument die fachlichen Anforderungen an die vier Pflichtmodule Produktverwaltung, Kundenverwaltung, Dokumentenprozess und Programmoberfläche (GUI) sowie deren Zusammenspiel ab; außerhalb des Geltungsbereichs liegen die unter 1.3 genannten Nicht-Ziele.
---
@ -111,21 +109,11 @@ Da das System gemäß Charter (Nicht-Ziele) ohne externe Systemanbindung betrieb
| Lokales Dateisystem | Persistierung der Daten (Stammdaten, Dokumente) | Datei-Schnittstelle |
| PDF-Export-Schnittstelle | Erzeugung druckbarer Dokumente als PDF | Ausgabeschnittstelle |
### 2.3 Organisatorische Rahmenbedingungen
- **Projektlaufzeit:** 15.04.2026 30.06.2026
- **Teamgröße:** 11 Personen, organisiert in vier Untergruppen E, F, G, H
- **Zeit pro Person:** 23 Stunden pro Woche
- **Vorgehensmodell:** V-Modell
- **Budget:** kein finanzielles Budget
- **Kommunikation:** Discord bzw. WhatsApp (täglich), Gitty (kontinuierlich), wöchentliche Meetings, E-Mail an den Auftraggeber bei Bedarf
### 2.4 Rechtliche Rahmenbedingungen
- Rechnungen müssen die gesetzlichen Pflichtangaben nach **§ 14 Abs. 4 UStG** enthalten (vollständiger Name und Anschrift von Leistungserbringer und -empfänger, Steuernummer bzw. USt-IdNr., Ausstellungsdatum, fortlaufende Rechnungsnummer, Menge und Art der Leistung, Liefer- bzw. Leistungsdatum, Entgelt und Steuersatz).
- Personenbezogene Kundendaten unterliegen der **DSGVO** (Datenminimierung, Auskunfts- und Löschrecht).
### 2.5 Technische Rahmenbedingungen
### 2.3 Technische Rahmenbedingungen
- Das System wird **lokal** ausgeführt (keine zwingende Internetverbindung erforderlich).
- Daten werden persistent gespeichert, sodass sie nach einem Neustart der Anwendung verlustfrei verfügbar sind.
@ -148,19 +136,12 @@ Da das System gemäß Charter (Nicht-Ziele) ohne externe Systemanbindung betrieb
Im laufenden Betrieb des Systems wird folgende Benutzerrolle unterschieden:
| Rolle | Aufgaben | Typische Aktionen |
|---------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
|--------------|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Anwender** | Bedient das System im Geschäftsalltag | Produkt- und Kundenstammdaten anlegen, bearbeiten, löschen; Angebote, Auftragsbestätigungen, Lieferscheine und Rechnungen erstellen; PDF exportieren |
Eine Authentifizierungs- oder Rechtekonzept-Rolle entfällt (siehe Kap. 1.3, Nicht-Ziele).
### 3.3 Teamstruktur
| Gruppe | Verantwortungsbereich | Bezug zu Kapitel |
|-----------|-----------------------------|------------------|
| Gruppe E | Programmoberfläche (GUI) | 4.4 |
| Gruppe F | Dokumentenprozess | 4.3 |
| Gruppe G | Produktverwaltung | 4.1 |
| Gruppe H | Kundenverwaltung | 4.2 |
---
@ -290,13 +271,17 @@ Akzeptanztests: AT-GUI-01 (Live-Summenaktualisierung), AT-GUI-02 (Erfolgsmeldung
**BA-GUI-02 Auftragsbestätigung über GUI (Muss)**
Als Anwender muss ich Angebote über die GUI in Auftragsbestätigungen umwandeln können, ohne Daten manuell neu einzugeben. Die Anforderung gilt, wenn ein Angebot mit Status „angenommen" ausgewählt ist. Die Anforderung gilt als erfüllt, wenn alle Daten in die Auftragsbestätigungs-Maske übernommen werden, die Maske mit dem Titel „Auftragsbestätigung" angezeigt wird und Pflichtfelder für den Liefertermin farblich markiert sind, solange sie leer sind.
Als Anwender muss ich Angebote über die GUI in Auftragsbestätigungen umwandeln können, ohne Daten manuell neu einzugeben. Die Anforderung gilt, wenn ein Angebot mit Status „angenommen" ausgewählt ist. Die Anforderung gilt als erfüllt, wenn alle Daten in die Auftragsbestätigungs-Maske übernommen werden, die Maske mit dem Titel „Auftragsbestätigung" angezeigt wird
und Pflichtfelder für den Liefertermin deutlich hervorgehoben werden,
solange sie leer sind.
Akzeptanztest: AT-GUI-03 (Datenübernahme und Markierung).
**BA-GUI-03 Rechnungslegung und Statusanzeige (Muss)**
Als Anwender muss ich über die GUI aus einem Lieferschein eine Rechnung erzeugen und deren Status einsehen können, um den Abrechnungsfortschritt zu verfolgen. Die Anforderung gilt, wenn ein abrechnungsreifer Lieferschein vorliegt. Die Anforderung gilt als erfüllt, wenn die GUI vor dem finalen Speichern eine Druckvorschau anzeigt und der Auftragsstatus in der Übersicht durch ein grünes Icon als „Abgeschlossen" markiert wird.
Als Anwender muss ich über die GUI aus einem Lieferschein eine Rechnung erzeugen und deren Status einsehen können, um den Abrechnungsfortschritt zu verfolgen. Die Anforderung gilt, wenn ein abrechnungsreifer Lieferschein vorliegt. Die Anforderung gilt als erfüllt, wenn die GUI vor dem finalen Speichern eine Druckvorschau anzeigt und der Auftragsstatus in der Übersicht
eindeutig als „Abgeschlossen"
gekennzeichnet wird.
Akzeptanztests: AT-GUI-04 (Druckvorschau), AT-GUI-05 (Status-Icon).
@ -321,7 +306,7 @@ Modalverben im Anforderungstext entsprechen der jeweiligen Priorität (Muss →
### 5.1 Usability (NF-USE)
| ID | Anforderung | Prio | Akzeptanzkriterium / Test |
|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------------------------------------------------------------------|
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|-------------------------------------------------------------------------------------|
| NF-USE-01 | Das Anlegen eines neuen Kunden MUSS von einem Anwender ohne vorherige Schulung in weniger als 2 Minuten abgeschlossen werden können, wobei höchstens 1 Fehlbedienung pro 10 Testnutzenden zulässig ist. | Muss | AT-NF-01: Usability-Test mit 5 Probanden; Erfolgsquote > 90 %, Median-Zeit < 120 s. |
| NF-USE-02 | Mindestens 80 % der Testnutzenden MÜSSEN die Aufgabe „Aus einem Angebot eine Rechnung erzeugen" im ersten Versuch ohne Hilfetext erfolgreich abschließen. | Muss | AT-NF-02: Usability-Test mit 5 Probanden; mindestens 4 von 5 erfolgreich. |
| NF-USE-03 | Das System MUSS bei jeder validierungspflichtigen Eingabe innerhalb von 1 Sekunde eine sichtbare Rückmeldung (Erfolg oder Fehler) anzeigen. | Muss | AT-NF-03: Manueller Test mit Stoppuhr; Rückmeldezeit < 1 s. |
@ -329,7 +314,7 @@ Modalverben im Anforderungstext entsprechen der jeweiligen Priorität (Muss →
### 5.2 Performance (NF-PERF)
| ID | Anforderung | Prio | Akzeptanzkriterium / Test |
|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------------------------------------------------------------------|
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|-------------------------------------------------------------------------|
| NF-PERF-01 | Das System MUSS jede Benutzeraktion innerhalb von 1 Sekunde mit einer sichtbaren Rückmeldung beantworten, bei einem Datenbestand von bis zu 1.000 Produkten und 1.000 Kunden. | Muss | AT-NF-04: Lasttest mit 1.000 Datensätzen je Entität; Antwortzeit < 1 s. |
| NF-PERF-02 | Das System MUSS die Übersichtsliste von Produkten oder Kunden bei bis zu 1.000 Datensätzen innerhalb von 2 Sekunden vollständig anzeigen. | Muss | AT-NF-05: Lasttest mit 1.000 Einträgen; Anzeigedauer < 2 s. |
| NF-PERF-03 | Das System SOLLTE den Anwendungsstart vom Aufruf bis zur bedienbaren Hauptansicht in höchstens 5 Sekunden abschließen (Referenzhardware: handelsüblicher Bürorechner). | Soll | AT-NF-06: Kaltstart auf Referenzhardware < 5 s. |

44
pom.xml 100644
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.hsmannheim.faktura</groupId>
<artifactId>fakturierungssystem</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
<jackson.version>2.17.1</jackson.version>
</properties>
<dependencies>
<!-- JSON-Persistenz (Jackson) - Pflichtenheft Kap. 2.3 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Tests (JUnit 5 / Jupiter) - NF-TEST-02 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,91 @@
package de.hsmannheim.faktura.kunde.model;
import java.util.Objects;
/**
* Datenobjekt "Kunde" gemäß Pflichtenheft Kap. 6.1.2.
*
* Reiner Datentyp (POJO) ohne fachliche Validierung die Prüfung von
* Pflichtfeldern, der Bedingung firmenname||nachname und des E-Mail-Formats
* erfolgt in Phase 3 im KundeService, nicht in dieser Klasse.
*/
public class Kunde {
private long kundeId; // Pflicht: fortlaufender, eindeutiger Primärschlüssel
private String firmenname; // Pflicht*: sofern kein Nachname (Constraint in Phase 3)
private String nachname; // Pflicht*: sofern kein Firmenname
private String vorname; // Optional
private String strasse; // Pflicht: Straße + Hausnummer
private String plz; // Pflicht: String, um führende Nullen zu erhalten!
private String ort; // Pflicht
private String telefon; // Optional
private String email; // Optional: Formatprüfung in Phase 3
private String ustIdNr; // Optional
private String lieferadresse; // Optional: abweichende Lieferadresse
private String ansprechpartner; // Optional
/** No-arg-Konstruktor von Jackson für die JSON-Deserialisierung benötigt (P1-3). */
public Kunde() {
}
// --- Getter und Setter (P1-2) ---
public long getKundeId() { return kundeId; }
public void setKundeId(long kundeId) { this.kundeId = kundeId; }
public String getFirmenname() { return firmenname; }
public void setFirmenname(String firmenname) { this.firmenname = firmenname; }
public String getNachname() { return nachname; }
public void setNachname(String nachname) { this.nachname = nachname; }
public String getVorname() { return vorname; }
public void setVorname(String vorname) { this.vorname = vorname; }
public String getStrasse() { return strasse; }
public void setStrasse(String strasse) { this.strasse = strasse; }
public String getPlz() { return plz; }
public void setPlz(String plz) { this.plz = plz; }
public String getOrt() { return ort; }
public void setOrt(String ort) { this.ort = ort; }
public String getTelefon() { return telefon; }
public void setTelefon(String telefon) { this.telefon = telefon; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getUstIdNr() { return ustIdNr; }
public void setUstIdNr(String ustIdNr) { this.ustIdNr = ustIdNr; }
public String getLieferadresse() { return lieferadresse; }
public void setLieferadresse(String lieferadresse) { this.lieferadresse = lieferadresse; }
public String getAnsprechpartner() { return ansprechpartner; }
public void setAnsprechpartner(String ansprechpartner) { this.ansprechpartner = ansprechpartner; }
// --- Identität über den Primärschlüssel kundeId (P1-2) ---
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Kunde other = (Kunde) o;
return kundeId == other.kundeId;
}
@Override
public int hashCode() {
return Objects.hash(kundeId);
}
@Override
public String toString() {
return "Kunde{kundeId=" + kundeId
+ ", firmenname='" + firmenname + '\''
+ ", nachname='" + nachname + '\''
+ ", ort='" + ort + '\'' + '}';
}
}

View File

@ -0,0 +1,24 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.ArrayList;
import java.util.List;
/**
* Reiner Persistenz-Container: bündelt die gespeicherten Kunden und den
* fortlaufenden ID-Zähler in einem JSON-Objekt. Der mitgespeicherte Zähler
* garantiert, dass vergebene IDs nach einem Neustart nicht wiederverwendet
* werden (P2-3).
*/
public class KundeContainer {
private List<Kunde> kunden = new ArrayList<>();
private long naechsteId = 1;
public List<Kunde> getKunden() { return kunden; }
public void setKunden(List<Kunde> kunden) { this.kunden = kunden; }
public long getNaechsteId() { return naechsteId; }
public void setNaechsteId(long naechsteId) { this.naechsteId = naechsteId; }
}

View File

@ -0,0 +1,22 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.List;
import java.util.Optional;
/**
* Abstrakte Datenhaltung für Kunden (IF-02, NF-ARCH-02).
* Kapselt die Speichertechnologie, sodass sie austauschbar ist.
*/
public interface KundeRepository {
/** Legt neu an (kundeId == 0) oder aktualisiert; liefert die (ggf. neu vergebene) kundeId. */
long speichern(Kunde kunde);
Optional<Kunde> findeById(long kundeId);
List<Kunde> findeAlle();
void loeschen(long kundeId);
}

View File

@ -0,0 +1,89 @@
package de.hsmannheim.faktura.kunde.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* JSON-basierte Umsetzung von KundeRepository (NF-ARCH-01).
* Hält die Kunden im Speicher und schreibt nach jeder Änderung in eine
* lokale JSON-Datei. Beim Erzeugen wird eine vorhandene Datei geladen.
*/
public class KundeRepositoryJson implements KundeRepository {
private final File datei;
private final ObjectMapper mapper = new ObjectMapper();
private final Map<Long, Kunde> kunden = new LinkedHashMap<>();
private long naechsteId = 1;
public KundeRepositoryJson(File datei) {
this.datei = datei;
laden();
}
@Override
public long speichern(Kunde kunde) {
if (kunde.getKundeId() == 0) {
// Neuanlage: nächste freie ID vergeben, Zähler hochzählen (kein Reuse)
kunde.setKundeId(naechsteId);
naechsteId++;
}
kunden.put(kunde.getKundeId(), kunde);
speichernInDatei();
return kunde.getKundeId();
}
@Override
public Optional<Kunde> findeById(long kundeId) {
return Optional.ofNullable(kunden.get(kundeId));
}
@Override
public List<Kunde> findeAlle() {
return new ArrayList<>(kunden.values());
}
@Override
public void loeschen(long kundeId) {
kunden.remove(kundeId);
speichernInDatei();
}
// --- interne Persistenz-Hilfen ---
private void laden() {
if (!datei.exists()) {
return; // erster Start: leeres Repository
}
try {
KundeContainer container = mapper.readValue(datei, KundeContainer.class);
kunden.clear();
for (Kunde k : container.getKunden()) {
kunden.put(k.getKundeId(), k);
}
naechsteId = container.getNaechsteId();
} catch (IOException e) {
throw new UncheckedIOException("Kundendaten konnten nicht geladen werden: " + datei, e);
}
}
private void speichernInDatei() {
try {
KundeContainer container = new KundeContainer();
container.setKunden(new ArrayList<>(kunden.values()));
container.setNaechsteId(naechsteId);
mapper.writerWithDefaultPrettyPrinter().writeValue(datei, container);
} catch (IOException e) {
throw new UncheckedIOException("Kundendaten konnten nicht gespeichert werden: " + datei, e);
}
}
}

View File

@ -0,0 +1,12 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Schmale Abstraktion über die Belegseite (Gruppe F) für die GR-05-Löschsperre.
* Liefert true, wenn der Kunde in mindestens einem Beleg referenziert ist.
* Die echte Umsetzung liefert Gruppe F (BelegRepository, OP-1/P0-4); bis dahin
* und in Tests wird gegen dieses Interface programmiert.
*/
@FunctionalInterface
public interface BelegReferenzPruefer {
boolean istKundeReferenziert(long kundeId);
}

View File

@ -0,0 +1,89 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Fachliche Logik der Kundenverwaltung (F-KV-0104 / BA-KV-0104).
* Orchestriert Validierung (Phase 3) und Persistenz (Phase 2); enthält
* selbst keine Speicher- oder Prüf-Details (NF-MAINT-03, Schichtentrennung).
* Die vier öffentlichen Methoden entsprechen exakt dem Schnittstellenvertrag K1.
*/
public class KundeService {
private final KundeRepository repository;
private final KundeValidator validator;
private final BelegReferenzPruefer belegPruefer;
public KundeService(KundeRepository repository,
KundeValidator validator,
BelegReferenzPruefer belegPruefer) {
this.repository = repository;
this.validator = validator;
this.belegPruefer = belegPruefer;
}
/** P4-1 · F-KV-01 / BA-KV-01: validieren, ID vergeben lassen, persistieren. */
public long anlegen(Kunde kunde) {
validator.pruefe(kunde); // wirft bei Verstoß -> speichern wird nie erreicht (P3-4)
kunde.setKundeId(0); // Neuanlage erzwingen: Repository vergibt die nächste ID
return repository.speichern(kunde);
}
/** P4-2 · F-KV-02 / BA-KV-02: vorhandenen Kunden laden, validieren, persistieren. */
public void bearbeiten(Kunde kunde) {
if (repository.findeById(kunde.getKundeId()).isEmpty()) {
throw new ValidierungsException(
"Kunde mit ID " + kunde.getKundeId() + " existiert nicht.");
}
validator.pruefe(kunde);
repository.speichern(kunde);
}
/** P4-3 · F-KV-03 / BA-KV-03: Treffer über Firmenname/Nachname UND Kundennummer. */
public List<Kunde> suchen(String suchbegriff) {
if (suchbegriff == null || suchbegriff.isBlank()) {
return List.of();
}
String begriff = suchbegriff.trim().toLowerCase();
return repository.findeAlle().stream()
.filter(k -> passt(k, begriff))
.collect(Collectors.toList());
}
/** P5-1 · K3: Kundenauflösung per ID für Gruppe F (Ersatz für deren FakeCustomerLookup). */
public Optional<Kunde> finde(long kundeId) {
return repository.findeById(kundeId);
}
/** 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) {
Optional<Kunde> vorhanden = repository.findeById(kundeId);
if (vorhanden.isEmpty()) {
throw new ValidierungsException(
"Kunde mit ID " + kundeId + " existiert nicht.");
}
if (belegPruefer.istKundeReferenziert(kundeId)) {
throw new LoeschsperreException(
"Kunde " + kundeId + " ist in mindestens einem Beleg referenziert "
+ "und darf nicht gelöscht werden (GR-05).");
}
repository.loeschen(kundeId);
}
// --- interne Suchhilfen ---
private boolean passt(Kunde k, String begriff) {
if (enthaelt(k.getFirmenname(), begriff)) return true;
if (enthaelt(k.getNachname(), begriff)) return true;
return String.valueOf(k.getKundeId()).equals(begriff); // Suche über Kundennummer
}
private boolean enthaelt(String feld, String begriff) {
return feld != null && feld.toLowerCase().contains(begriff);
}
}

View File

@ -0,0 +1,52 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.regex.Pattern;
/**
* Prüft einen Kunden gegen die fachlichen Regeln aus Pflichtenheft Kap. 6.1.2.
* Wirft bei Verstoß eine ValidierungsException der aufrufende Service
* persistiert dann nicht (P3-4).
*/
public class KundeValidator {
// Pragmatische Prüfung: etwas vor dem @, etwas danach, ein Punkt mit Endung.
// Lehnt z. B. "kunde@" ab (MT-KV-04), ohne RFC-Vollständigkeit zu erzwingen.
private static final Pattern EMAIL_MUSTER =
Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
public void pruefe(Kunde kunde) {
if (kunde == null) {
throw new ValidierungsException("Kunde darf nicht null sein.");
}
// P3-2: mindestens Firmenname ODER Nachname
if (istLeer(kunde.getFirmenname()) && istLeer(kunde.getNachname())) {
throw new ValidierungsException(
"Entweder Firmenname oder Nachname muss angegeben sein.");
}
// P3-1: Pflichtfelder der Adresse
if (istLeer(kunde.getStrasse())) {
throw new ValidierungsException("Straße ist ein Pflichtfeld.");
}
if (istLeer(kunde.getPlz())) {
throw new ValidierungsException("PLZ ist ein Pflichtfeld.");
}
if (istLeer(kunde.getOrt())) {
throw new ValidierungsException("Ort ist ein Pflichtfeld.");
}
// P3-3: E-Mail nur prüfen, wenn überhaupt angegeben
if (!istLeer(kunde.getEmail())
&& !EMAIL_MUSTER.matcher(kunde.getEmail()).matches()) {
throw new ValidierungsException(
"E-Mail-Adresse hat kein gültiges Format: " + kunde.getEmail());
}
}
private boolean istLeer(String wert) {
return wert == null || wert.isBlank();
}
}

View File

@ -0,0 +1,11 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Wird geworfen, wenn ein Kunde wegen Beleg-Referenz (GR-05) nicht
* gelöscht werden darf.
*/
public class LoeschsperreException extends RuntimeException {
public LoeschsperreException(String message) {
super(message);
}
}

View File

@ -0,0 +1,13 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Wird geworfen, wenn ein Kunde gegen eine fachliche Validierungsregel
* verstößt (Pflichtenheft Kap. 6.1.2). Unchecked, damit die im
* Schnittstellenvertrag K1 festgelegten Service-Signaturen ohne
* throws-Klausel auskommen.
*/
public class ValidierungsException extends RuntimeException {
public ValidierungsException(String message) {
super(message);
}
}

View File

@ -0,0 +1,40 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.nio.file.Path;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class KundeRepositoryJsonTest {
@Test
void persistenz_ueberlebt_neustart(@TempDir Path tempDir) {
File datei = tempDir.resolve("kunden.json").toFile();
// 1. Repository anlegen und einen Kunden speichern
KundeRepository repo1 = new KundeRepositoryJson(datei);
Kunde kunde = new Kunde();
kunde.setNachname("Mustermann");
kunde.setStrasse("Hauptstr. 1");
kunde.setPlz("01067"); // führende Null!
kunde.setOrt("Dresden");
long id = repo1.speichern(kunde);
// 2. Neues Repository auf dieselbe Datei = simulierter Neustart
KundeRepository repo2 = new KundeRepositoryJson(datei);
Optional<Kunde> geladen = repo2.findeById(id);
// 3. Kunde ist unverändert wiederhergestellt
assertTrue(geladen.isPresent(), "Kunde sollte nach Neustart vorhanden sein");
assertEquals("Mustermann", geladen.get().getNachname());
assertEquals("01067", geladen.get().getPlz(),
"Führende Null der PLZ muss erhalten bleiben");
assertEquals("Dresden", geladen.get().getOrt());
}
}

View File

@ -0,0 +1,99 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class KundeServiceTest {
private KundeService service;
private boolean kundeIstReferenziert; // steuert den BelegReferenzPruefer im Test
@BeforeEach
void setUp(@TempDir Path tempDir) {
File datei = tempDir.resolve("kunden.json").toFile();
KundeRepository repository = new KundeRepositoryJson(datei);
KundeValidator validator = new KundeValidator();
BelegReferenzPruefer belegPruefer = kundeId -> kundeIstReferenziert;
service = new KundeService(repository, validator, belegPruefer);
kundeIstReferenziert = false;
}
private Kunde gueltigerKunde() {
Kunde k = new Kunde();
k.setNachname("Mustermann");
k.setStrasse("Hauptstr. 1");
k.setPlz("68159");
k.setOrt("Mannheim");
return k;
}
@Test
void anlegen_vergibtIdUndPersistiert() { // BA-KV-01
long id = service.anlegen(gueltigerKunde());
assertTrue(id > 0);
assertEquals(1, service.suchen("Mustermann").size());
}
@Test
void anlegen_ungueltig_wirdNichtGespeichert() { // P3-4 im Service
Kunde k = gueltigerKunde();
k.setNachname(null);
assertThrows(ValidierungsException.class, () -> service.anlegen(k));
assertTrue(service.suchen("Mustermann").isEmpty());
}
@Test
void bearbeiten_aendertGespeichertenKunden() { // BA-KV-02
service.anlegen(gueltigerKunde());
Kunde k = service.suchen("Mustermann").get(0);
k.setTelefon("0621 12345");
service.bearbeiten(k);
assertEquals("0621 12345", service.suchen("Mustermann").get(0).getTelefon());
}
@Test
void suchen_ueberKundennummer_findet() { // BA-KV-03 / MT-KV-08
long id = service.anlegen(gueltigerKunde());
assertEquals(1, service.suchen(String.valueOf(id)).size());
}
@Test
void suchen_ohneTreffer_istLeer() { // BA-KV-03 / MT-KV-09
service.anlegen(gueltigerKunde());
assertTrue(service.suchen("Gibtsnicht").isEmpty());
}
@Test
void loeschen_nichtReferenziert_entfernt() { // BA-KV-04 / MT-KV-10
long id = service.anlegen(gueltigerKunde());
kundeIstReferenziert = false;
service.loeschen(id);
assertTrue(service.suchen("Mustermann").isEmpty());
}
@Test
void loeschen_referenziert_wirdAbgewiesen() { // BA-KV-04 / GR-05 / MT-KV-11
long id = service.anlegen(gueltigerKunde());
kundeIstReferenziert = true;
assertThrows(LoeschsperreException.class, () -> service.loeschen(id));
assertEquals(1, service.suchen("Mustermann").size()); // noch vorhanden
}
@Test
void finde_liefertAngelegtenKunden() { // K3
long id = service.anlegen(gueltigerKunde());
assertTrue(service.finde(id).isPresent());
assertEquals(id, service.finde(id).get().getKundeId());
assertTrue(service.finde(999L).isEmpty()); // unbekannte ID -> leer
}
}

View File

@ -0,0 +1,54 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class KundeValidatorTest {
private final KundeValidator validator = new KundeValidator();
private Kunde gueltigerKunde() {
Kunde k = new Kunde();
k.setNachname("Mustermann");
k.setStrasse("Hauptstr. 1");
k.setPlz("68159");
k.setOrt("Mannheim");
return k;
}
@Test
void gueltigerKunde_wirftNicht() {
assertDoesNotThrow(() -> validator.pruefe(gueltigerKunde()));
}
@Test
void ohneFirmennameUndNachname_wirdAbgelehnt() { // → MT-KV-02
Kunde k = gueltigerKunde();
k.setNachname(null);
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void ohneStrasse_wirdAbgelehnt() { // → MT-KV-03
Kunde k = gueltigerKunde();
k.setStrasse(null);
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void ungueltigeEmail_wirdAbgelehnt() { // → MT-KV-04
Kunde k = gueltigerKunde();
k.setEmail("kunde@");
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void gueltigeEmail_wirftNicht() {
Kunde k = gueltigerKunde();
k.setEmail("kunde@example.com");
assertDoesNotThrow(() -> validator.pruefe(k));
}
}