From 4299baeec0dd4333781f01432f908cb4b3ea1828 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Thu, 26 Mar 2026 19:53:51 +0100 Subject: [PATCH] =?UTF-8?q?REA-Feldpositionen=20f=C3=BCr=20Kennzeichen=20u?= =?UTF-8?q?nd=20Rechnungsbetrag=20korrigieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../structure/DefaultStructureValidator.java | 84 +++++- ...ureValidatorAsvrecRechnungsbetragTest.java | 28 +- ...lidatorAsvrecRechnungskennzeichenTest.java | 19 +- ...ValidatorAsvrecSegmentCardinalityTest.java | 278 ++++++++++++++++++ 4 files changed, 390 insertions(+), 19 deletions(-) create mode 100644 src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecSegmentCardinalityTest.java diff --git a/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java b/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java index 222a444..d827af1 100644 --- a/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java +++ b/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java @@ -33,6 +33,7 @@ import de.gecheckt.asv.validation.model.ValidationSeverity; * 15. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein * 16. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein * 17. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein + * 18. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen */ public class DefaultStructureValidator implements StructureValidator { @@ -142,6 +143,9 @@ public class DefaultStructureValidator implements StructureValidator { // Neue Regel: Für ASVREC mit Kennzeichen "1" muss Rechnungsbetrag "0,00" sein validateAsvrecRechnungsbetragBeiKennzeichen1(message, errors); + + // Neue Regel: Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen + validateAsvrecSegmentCardinality(message, errors); } } } @@ -600,10 +604,11 @@ public class DefaultStructureValidator implements StructureValidator { return; } var reaFields = reaOpt.get().fields(); - if (reaFields.isEmpty()) { + // Rechnungskennzeichen ist das 4. Feld in REA (Index 3) + if (reaFields.size() < 4) { return; } - String kennzeichen = reaFields.get(0).rawValue(); + String kennzeichen = reaFields.get(3).rawValue(); if ("0".equals(kennzeichen)) { // DGN muss vorhanden sein @@ -693,19 +698,17 @@ public class DefaultStructureValidator implements StructureValidator { return; } var reaFields = reaOpt.get().fields(); - if (reaFields.size() < 1) { + // Rechnungskennzeichen ist das 4. Feld in REA (Index 3) + if (reaFields.size() < 4) { return; } - String kennzeichen = reaFields.get(0).rawValue(); + String kennzeichen = reaFields.get(3).rawValue(); if (!"1".equals(kennzeichen)) { return; } - // Rechnungsbetrag ist das zweite Feld von REA - if (reaFields.size() < 2) { - return; - } - String betrag = reaFields.get(1).rawValue(); + // Rechnungsbetrag ist das 1. Feld in REA (Index 0) + String betrag = reaFields.get(0).rawValue(); if (!"0,00".equals(betrag)) { errors.add(createError( "STRUCTURE_021", @@ -721,6 +724,69 @@ public class DefaultStructureValidator implements StructureValidator { } } + /** + * Prüft für ASVREC-Nachrichten, dass IFA, REA und IVA jeweils genau einmal vorkommen. + * Bei 0 Vorkommen greift bereits die Präsenzregel (STRUCTURE_013/014/015); + * diese Methode erzeugt einen zusätzlichen Fehler nur bei mehr als einem Vorkommen. + * Bei genau einem Vorkommen wird kein Fehler erzeugt. + * + * @param message die zu validierende Nachricht + * @param errors die Liste zum Hinzufügen von Validierungsfehlern + */ + private void validateAsvrecSegmentCardinality(Message message, List errors) { + var unhSegment = message.getFirstSegment("UNH"); + if (unhSegment.isEmpty()) { + return; + } + if (!"ASVREC".equals(extractMessageType(unhSegment.get()))) { + return; + } + + checkExactlyOnce(message, "IFA", "STRUCTURE_022", + "ASVREC-Nachricht darf IFA nur genau einmal enthalten", + "IFA muss genau einmal vorkommen", errors); + + checkExactlyOnce(message, "REA", "STRUCTURE_023", + "ASVREC-Nachricht darf REA nur genau einmal enthalten", + "REA muss genau einmal vorkommen", errors); + + checkExactlyOnce(message, "IVA", "STRUCTURE_024", + "ASVREC-Nachricht darf IVA nur genau einmal enthalten", + "IVA muss genau einmal vorkommen", errors); + } + + /** + * Hilfsmethode: Erzeugt einen Fehler, wenn das Segment mit dem angegebenen Namen + * nicht genau einmal in der Nachricht vorkommt. + * Bei 0 Vorkommen wird kein Fehler erzeugt (Präsenzregel greift bereits). + * Bei mehr als einem Vorkommen wird genau ein Fehler erzeugt. + * + * @param message die zu validierende Nachricht + * @param segmentName der zu prüfende Segmentname + * @param errorCode der Fehlercode + * @param description die Fehlerbeschreibung + * @param expectedRule die erwartete Regel + * @param errors die Liste zum Hinzufügen von Validierungsfehlern + */ + private void checkExactlyOnce(Message message, String segmentName, String errorCode, + String description, String expectedRule, + List errors) { + int count = message.getSegments(segmentName).size(); + if (count > 1) { + errors.add(createError( + errorCode, + description, + ValidationSeverity.ERROR, + segmentName, + message.messagePosition(), + "", + 0, + String.valueOf(count), + expectedRule + )); + } + } + /** * Extrahiert den Nachrichtentyp aus dem UNH-Segment. * diff --git a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungsbetragTest.java b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungsbetragTest.java index 9f759a0..978d6ce 100644 --- a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungsbetragTest.java +++ b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungsbetragTest.java @@ -57,18 +57,28 @@ class DefaultStructureValidatorAsvrecRechnungsbetragTest { } /** - * Erstellt ein REA-Segment mit Rechnungskennzeichen und Rechnungsbetrag. + * Erstellt ein REA-Segment mit Rechnungsbetrag und Rechnungskennzeichen. + * Felder: 1=Rechnungsbetrag, 2=Quartal, 3=Rechnungsnummer, 4=Rechnungskennzeichen. */ private Segment rea(int pos, String kennzeichen, String betrag) { - return new Segment("REA", pos, - List.of(new Field(1, kennzeichen), new Field(2, betrag))); + return new Segment("REA", pos, List.of( + new Field(1, betrag), + new Field(2, "2024Q1"), + new Field(3, "RNR-001"), + new Field(4, kennzeichen) + )); } /** - * Erstellt ein REA-Segment mit nur dem Rechnungskennzeichen (kein Betrag). + * Erstellt ein REA-Segment mit nur dem Rechnungskennzeichen (kein Betrag, nur 3 Felder). + * Simuliert einen unvollständigen REA ohne auswertbares Kennzeichen (< 4 Felder). */ private Segment reaOhneBetrag(int pos, String kennzeichen) { - return new Segment("REA", pos, List.of(new Field(1, kennzeichen))); + return new Segment("REA", pos, List.of( + new Field(1, ""), + new Field(2, "2024Q1"), + new Field(3, "RNR-001") + )); } // --- Test 1: Kennzeichen "1" + Betrag "0,00" -> kein neuer Fehler --- @@ -157,8 +167,12 @@ class DefaultStructureValidatorAsvrecRechnungsbetragTest { void validate_asvfeh_keinFehlerDurchDieseRegel() { Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ASVFEH:D:03B:UN:EAN008"))); - Segment rea = new Segment("REA", 2, - List.of(new Field(1, "1"), new Field(2, "999,99"))); + Segment rea = new Segment("REA", 2, List.of( + new Field(1, "999,99"), + new Field(2, "2024Q1"), + new Field(3, "RNR-001"), + new Field(4, "1") + )); Segment unt = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "12345"))); Message message = new Message(1, List.of(unh, rea, unt)); diff --git a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungskennzeichenTest.java b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungskennzeichenTest.java index 6528bdf..b21db69 100644 --- a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungskennzeichenTest.java +++ b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecRechnungskennzeichenTest.java @@ -56,9 +56,17 @@ class DefaultStructureValidatorAsvrecRechnungskennzeichenTest { return new InputFile("test.txt", List.of(message)); } - /** Erstellt ein REA-Segment mit dem angegebenen Rechnungskennzeichen. */ + /** + * Erstellt ein REA-Segment mit dem angegebenen Rechnungskennzeichen. + * Felder: 1=Rechnungsbetrag, 2=Quartal, 3=Rechnungsnummer, 4=Rechnungskennzeichen. + */ private Segment rea(int pos, String kennzeichen) { - return new Segment("REA", pos, List.of(new Field(1, kennzeichen))); + return new Segment("REA", pos, List.of( + new Field(1, "0,00"), + new Field(2, "2024Q1"), + new Field(3, "RNR-001"), + new Field(4, kennzeichen) + )); } // --- Test 1: Kennzeichen "0" + DGN und LEA vorhanden -> kein neuer Fehler --- @@ -208,7 +216,12 @@ class DefaultStructureValidatorAsvrecRechnungskennzeichenTest { // ASVFEH mit REA Kennzeichen "0" und ohne DGN/LEA -> kein STRUCTURE_017/018 Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ASVFEH:D:03B:UN:EAN008"))); - Segment rea = new Segment("REA", 2, List.of(new Field(1, "0"))); + Segment rea = new Segment("REA", 2, List.of( + new Field(1, "0,00"), + new Field(2, "2024Q1"), + new Field(3, "RNR-001"), + new Field(4, "0") + )); Segment unt = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "12345"))); Message message = new Message(1, List.of(unh, rea, unt)); diff --git a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecSegmentCardinalityTest.java b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecSegmentCardinalityTest.java new file mode 100644 index 0000000..a272132 --- /dev/null +++ b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorAsvrecSegmentCardinalityTest.java @@ -0,0 +1,278 @@ +package de.gecheckt.asv.validation.structure; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.gecheckt.asv.domain.model.Field; +import de.gecheckt.asv.domain.model.InputFile; +import de.gecheckt.asv.domain.model.Message; +import de.gecheckt.asv.domain.model.Segment; +import de.gecheckt.asv.validation.model.ValidationError; +import de.gecheckt.asv.validation.model.ValidationResult; + +/** + * Tests für die Kardinalitätsregel in ASVREC-Nachrichten: + * IFA, REA und IVA müssen jeweils genau einmal vorkommen. + */ +class DefaultStructureValidatorAsvrecSegmentCardinalityTest { + + private DefaultStructureValidator validator; + + @BeforeEach + void setUp() { + validator = new DefaultStructureValidator(); + } + + // --- Hilfsmethoden --- + + /** + * Erstellt eine minimale ASVREC-Nachricht mit den angegebenen inneren Segmenten. + * UNH an Position 1, UNT an letzter Position; Segmentanzahl wird automatisch gesetzt. + */ + private InputFile buildAsvrec(List innerSegments) { + Segment unh = new Segment("UNH", 1, + List.of(new Field(1, "12345"), new Field(2, "ASVREC:D:03B:UN:EAN008"))); + + int untPos = 2 + innerSegments.size(); + int totalSegments = 1 + innerSegments.size() + 1; + Segment unt = new Segment("UNT", untPos, + List.of(new Field(1, String.valueOf(totalSegments)), new Field(2, "12345"))); + + var allSegments = new ArrayList(); + allSegments.add(unh); + allSegments.addAll(innerSegments); + allSegments.add(unt); + + Message message = new Message(1, allSegments); + return new InputFile("test.txt", List.of(message)); + } + + // --- Test 1: ASVREC mit genau einem IFA, REA, IVA -> kein neuer Fehler --- + + @Test + void validate_asvrecMitGenauEinemIFAReaIva_keinFehler() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("REA", 3), + new Segment("IVA", 4) + )); + + ValidationResult result = validator.validate(inputFile); + + assertFalse(result.hasErrors(), + "Kein Fehler erwartet bei ASVREC mit genau einem IFA, REA und IVA"); + } + + // --- Test 2: ASVREC mit doppeltem IFA -> genau ein Fehler STRUCTURE_022 --- + + @Test + void validate_asvrecMitDoppeltemIfa_fehlerSTRUCTURE022() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("IFA", 3), + new Segment("REA", 4), + new Segment("IVA", 5) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + + long count022 = result.getErrors().stream() + .filter(e -> "STRUCTURE_022".equals(e.errorCode())) + .count(); + assertEquals(1, count022, "Genau ein STRUCTURE_022-Fehler erwartet"); + + ValidationError error = result.getErrors().stream() + .filter(e -> "STRUCTURE_022".equals(e.errorCode())) + .findFirst().orElseThrow(); + assertEquals("ASVREC-Nachricht darf IFA nur genau einmal enthalten", error.description()); + assertEquals("IFA", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("2", error.actualValue()); + assertEquals("IFA muss genau einmal vorkommen", error.expectedRule()); + } + + // --- Test 3: ASVREC mit doppeltem REA -> genau ein Fehler STRUCTURE_023 --- + + @Test + void validate_asvrecMitDoppeltemRea_fehlerSTRUCTURE023() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("REA", 3), + new Segment("REA", 4), + new Segment("IVA", 5) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + + long count023 = result.getErrors().stream() + .filter(e -> "STRUCTURE_023".equals(e.errorCode())) + .count(); + assertEquals(1, count023, "Genau ein STRUCTURE_023-Fehler erwartet"); + + ValidationError error = result.getErrors().stream() + .filter(e -> "STRUCTURE_023".equals(e.errorCode())) + .findFirst().orElseThrow(); + assertEquals("ASVREC-Nachricht darf REA nur genau einmal enthalten", error.description()); + assertEquals("REA", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("2", error.actualValue()); + assertEquals("REA muss genau einmal vorkommen", error.expectedRule()); + } + + // --- Test 4: ASVREC mit doppeltem IVA -> genau ein Fehler STRUCTURE_024 --- + + @Test + void validate_asvrecMitDoppeltemIva_fehlerSTRUCTURE024() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("REA", 3), + new Segment("IVA", 4), + new Segment("IVA", 5) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + + long count024 = result.getErrors().stream() + .filter(e -> "STRUCTURE_024".equals(e.errorCode())) + .count(); + assertEquals(1, count024, "Genau ein STRUCTURE_024-Fehler erwartet"); + + ValidationError error = result.getErrors().stream() + .filter(e -> "STRUCTURE_024".equals(e.errorCode())) + .findFirst().orElseThrow(); + assertEquals("ASVREC-Nachricht darf IVA nur genau einmal enthalten", error.description()); + assertEquals("IVA", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("2", error.actualValue()); + assertEquals("IVA muss genau einmal vorkommen", error.expectedRule()); + } + + // --- Test 5: ASVREC ohne IFA -> kein STRUCTURE_022 (Präsenzregel STRUCTURE_013 greift) --- + + @Test + void validate_asvrecOhneIfa_keinSTRUCTURE022() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("REA", 2), + new Segment("IVA", 3) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + // Präsenzregel STRUCTURE_013 muss vorhanden sein + assertTrue(result.getErrors().stream().anyMatch(e -> "STRUCTURE_013".equals(e.errorCode())), + "STRUCTURE_013 erwartet wenn IFA fehlt"); + // Kardinalitätsregel STRUCTURE_022 darf nicht vorhanden sein + assertFalse(result.getErrors().stream().anyMatch(e -> "STRUCTURE_022".equals(e.errorCode())), + "Kein STRUCTURE_022 erwartet wenn IFA fehlt (0 Vorkommen)"); + } + + // --- Test 6: ASVREC ohne REA -> kein STRUCTURE_023 (Präsenzregel STRUCTURE_014 greift) --- + + @Test + void validate_asvrecOhneRea_keinSTRUCTURE023() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("IVA", 3) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + // Präsenzregel STRUCTURE_014 muss vorhanden sein + assertTrue(result.getErrors().stream().anyMatch(e -> "STRUCTURE_014".equals(e.errorCode())), + "STRUCTURE_014 erwartet wenn REA fehlt"); + // Kardinalitätsregel STRUCTURE_023 darf nicht vorhanden sein + assertFalse(result.getErrors().stream().anyMatch(e -> "STRUCTURE_023".equals(e.errorCode())), + "Kein STRUCTURE_023 erwartet wenn REA fehlt (0 Vorkommen)"); + } + + // --- Test 7: ASVREC ohne IVA -> kein STRUCTURE_024 (Präsenzregel STRUCTURE_015 greift) --- + + @Test + void validate_asvrecOhneIva_keinSTRUCTURE024() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("REA", 3) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + // Präsenzregel STRUCTURE_015 muss vorhanden sein + assertTrue(result.getErrors().stream().anyMatch(e -> "STRUCTURE_015".equals(e.errorCode())), + "STRUCTURE_015 erwartet wenn IVA fehlt"); + // Kardinalitätsregel STRUCTURE_024 darf nicht vorhanden sein + assertFalse(result.getErrors().stream().anyMatch(e -> "STRUCTURE_024".equals(e.errorCode())), + "Kein STRUCTURE_024 erwartet wenn IVA fehlt (0 Vorkommen)"); + } + + // --- Test 8: ASVREC mit allen drei doppelt -> je ein Fehler pro Segment --- + + @Test + void validate_asvrecAlleDreiDoppelt_dreiKardinalitaetsfehler() { + InputFile inputFile = buildAsvrec(List.of( + new Segment("IFA", 2), + new Segment("IFA", 3), + new Segment("REA", 4), + new Segment("REA", 5), + new Segment("IVA", 6), + new Segment("IVA", 7) + )); + + ValidationResult result = validator.validate(inputFile); + + assertTrue(result.hasErrors()); + + long count022 = result.getErrors().stream() + .filter(e -> "STRUCTURE_022".equals(e.errorCode())).count(); + long count023 = result.getErrors().stream() + .filter(e -> "STRUCTURE_023".equals(e.errorCode())).count(); + long count024 = result.getErrors().stream() + .filter(e -> "STRUCTURE_024".equals(e.errorCode())).count(); + + assertEquals(1, count022, "Genau ein STRUCTURE_022-Fehler erwartet"); + assertEquals(1, count023, "Genau ein STRUCTURE_023-Fehler erwartet"); + assertEquals(1, count024, "Genau ein STRUCTURE_024-Fehler erwartet"); + } + + // --- Test 9: ASVFEH mit mehrfachen IFA/REA/IVA -> kein Fehler durch diese Regel --- + + @Test + void validate_asvfehMitMehrfachenSegmenten_keinKardinalitaetsfehler() { + Segment unh = new Segment("UNH", 1, + List.of(new Field(1, "12345"), new Field(2, "ASVFEH:D:03B:UN:EAN008"))); + Segment ifa1 = new Segment("IFA", 2); + Segment ifa2 = new Segment("IFA", 3); + Segment rea1 = new Segment("REA", 4); + Segment rea2 = new Segment("REA", 5); + Segment iva1 = new Segment("IVA", 6); + Segment iva2 = new Segment("IVA", 7); + Segment unt = new Segment("UNT", 8, + List.of(new Field(1, "8"), new Field(2, "12345"))); + + Message message = new Message(1, List.of(unh, ifa1, ifa2, rea1, rea2, iva1, iva2, unt)); + InputFile inputFile = new InputFile("test.txt", List.of(message)); + + ValidationResult result = validator.validate(inputFile); + + assertFalse(result.getErrors().stream() + .anyMatch(e -> "STRUCTURE_022".equals(e.errorCode()) + || "STRUCTURE_023".equals(e.errorCode()) + || "STRUCTURE_024".equals(e.errorCode())), + "Kein Kardinalitätsfehler erwartet für ASVFEH"); + assertFalse(result.hasErrors(), + "Kein Fehler erwartet für ASVFEH mit mehrfachen Segmenten"); + } +}