From 6a7d150007356d0a45efa06a19a24376d1c4afdf Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Thu, 26 Mar 2026 08:21:13 +0100 Subject: [PATCH] =?UTF-8?q?Erste=20fachliche=20Strukturregel=20erg=C3=A4nz?= =?UTF-8?q?en:=20UNH-/UNT-Referenznummer=20pr=C3=BCfen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../structure/DefaultStructureValidator.java | 143 ++++++++++++------ .../DefaultStructureValidatorTest.java | 92 +++++++++++ src/test/resources/no-unh.asv | 8 + src/test/resources/no-unt.asv | 8 + src/test/resources/unh-unt-match.asv | 9 ++ src/test/resources/unh-unt-mismatch.asv | 9 ++ 6 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 src/test/resources/no-unh.asv create mode 100644 src/test/resources/no-unt.asv create mode 100644 src/test/resources/unh-unt-match.asv create mode 100644 src/test/resources/unh-unt-mismatch.asv 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 877bf72..6af5de3 100644 --- a/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java +++ b/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java @@ -13,15 +13,16 @@ import de.gecheckt.asv.validation.model.ValidationResult; import de.gecheckt.asv.validation.model.ValidationSeverity; /** - * Default implementation of StructureValidator that checks general structural rules. + * Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft. * - * Rules checked: - * 1. InputFile must contain at least one Message - * 2. Each Message must contain at least one Segment - * 3. Segment names must not be empty - * 4. Field positions within a Segment must be unique and positive - * 5. Segment positions within a Message must be unique and positive - * 6. Message positions within an InputFile must be unique and positive + * Geprüfte Regeln: + * 1. Die Eingabedatei muss mindestens eine Nachricht enthalten + * 2. Jede Nachricht muss mindestens ein Segment enthalten + * 3. Segmentnamen dürfen nicht leer sein + * 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein + * 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein + * 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein + * 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen */ public class DefaultStructureValidator implements StructureValidator { @@ -44,10 +45,10 @@ public class DefaultStructureValidator implements StructureValidator { "", 0, "", - "At least one message required" + "Mindestens eine Nachricht erforderlich" )); } else { - // Process messages if they exist + // Verarbeite Nachrichten, wenn sie vorhanden sind validateMessages(inputFile.messages(), errors); } @@ -55,10 +56,10 @@ public class DefaultStructureValidator implements StructureValidator { } /** - * Validates all messages in the input file. + * Validiert alle Nachrichten in der Eingabedatei. * - * @param messages the list of messages to validate - * @param errors the list to add validation errors to + * @param messages die Liste der zu validierenden Nachrichten + * @param errors die Liste zum Hinzufügen von Validierungsfehlern */ private void validateMessages(List messages, List errors) { var messagePositions = new HashSet(); @@ -66,35 +67,83 @@ public class DefaultStructureValidator implements StructureValidator { for (var message : messages) { var messagePosition = message.messagePosition(); - // Rule 6: Message positions must be unique and positive + // Regel 6: Nachrichtenpositionen müssen eindeutig und positiv sein if (!messagePositions.add(messagePosition)) { errors.add(createError( "STRUCTURE_006", - "Duplicate message position: " + messagePosition, + "Doppelte Nachrichtenposition: " + messagePosition, ValidationSeverity.ERROR, "", messagePosition, "", 0, String.valueOf(messagePosition), - "Unique positive message positions required" + "Einzigartige positive Nachrichtenpositionen erforderlich" )); } - // Validate segments in this message + // Validiere Segmente in dieser Nachricht validateSegments(message.segments(), messagePosition, errors); + + // Regel 7: UNH- und UNT-Referenznummern müssen übereinstimmen + validateUnhUntReferenceNumbers(message, errors); } } /** - * Validates all segments in a message. + * Validiert, dass die Referenznummern in UNH und UNT übereinstimmen. * - * @param segments the list of segments to validate - * @param messagePosition the position of the parent message - * @param errors the list to add validation errors to + * @param message die zu validierende Nachricht + * @param errors die Liste zum Hinzufügen von Validierungsfehlern + */ + private void validateUnhUntReferenceNumbers(Message message, List errors) { + var unhSegment = message.getFirstSegment("UNH"); + var untSegment = message.getFirstSegment("UNT"); + + // Prüft, ob beide Segmente vorhanden sind, bevor eine Validierung erfolgt + if (unhSegment.isPresent() && untSegment.isPresent()) { + var unhFields = unhSegment.get().fields(); + var untFields = untSegment.get().fields(); + + // UNH-Referenznummer ist im 1. Feld (erstes Feld nach dem Segmentnamen) + // UNT-Referenznummer ist im 2. Feld (zweites Feld nach dem Segmentnamen) + if (!unhFields.isEmpty() && untFields.size() >= 2) { + var unhReference = unhFields.get(0).rawValue(); + var untReference = untFields.get(1).rawValue(); + + // Entferne möglicherweise vorhandenes abschließendes Hochkomma bei untReference + String normalizedUntReference = untReference; + if (untReference.endsWith("'")) { + normalizedUntReference = untReference.substring(0, untReference.length() - 1); + } + + // Vergleiche die Referenznummern + if (!unhReference.equals(normalizedUntReference)) { + errors.add(createError( + "STRUCTURE_007", + "UNH and UNT reference numbers do not match", + ValidationSeverity.ERROR, + "UNH/UNT", + message.messagePosition(), + "", + 0, + unhReference + " != " + normalizedUntReference, + "Reference numbers in UNH and UNT must match" + )); + } + } + } + } + + /** + * Validiert alle Segmente einer Nachricht. + * + * @param segments die Liste der zu validierenden Segmente + * @param messagePosition die Position der übergeordneten Nachricht + * @param errors die Liste zum Hinzufügen von Validierungsfehlern */ private void validateSegments(List segments, int messagePosition, List errors) { - // Rule 2: Each Message must contain at least one Segment + // Regel 2: Jede Nachricht muss mindestens ein Segment enthalten if (segments.isEmpty()) { errors.add(createError( "STRUCTURE_002", @@ -105,9 +154,9 @@ public class DefaultStructureValidator implements StructureValidator { "", 0, "", - "At least one segment required per message" + "Mindestens ein Segment pro Nachricht erforderlich" )); - return; // No need to validate segments if there are none + return; // Keine Validierung der Segmente notwendig, wenn keine vorhanden sind } var segmentPositions = new HashSet(); @@ -116,22 +165,22 @@ public class DefaultStructureValidator implements StructureValidator { var segmentName = segment.segmentName(); var segmentPosition = segment.segmentPosition(); - // Rule 3: Segment names must not be empty + // Regel 3: Segmentnamen dürfen nicht leer sein if (segmentName == null || segmentName.isEmpty()) { errors.add(createError( "STRUCTURE_003", - "Segment name must not be empty", + "Segmentname darf nicht leer sein", ValidationSeverity.ERROR, segmentName != null ? segmentName : "", segmentPosition, "", 0, segmentName != null ? segmentName : "null", - "Non-empty segment name required" + "Nicht-leerer Segmentname erforderlich" )); } - // Rule 5: Segment positions must be unique and positive + // Regel 5: Segmentpositionen müssen eindeutig und positiv sein if (!segmentPositions.add(segmentPosition)) { errors.add(createError( "STRUCTURE_005", @@ -142,7 +191,7 @@ public class DefaultStructureValidator implements StructureValidator { "", 0, String.valueOf(segmentPosition), - "Unique positive segment positions required" + "Einzigartige positive Segmentpositionen erforderlich" )); } @@ -152,12 +201,12 @@ public class DefaultStructureValidator implements StructureValidator { } /** - * Validates all fields in a segment. + * Validiert alle Felder eines Segments. * - * @param fields the list of fields to validate - * @param segmentName the name of the parent segment - * @param segmentPosition the position of the parent segment - * @param errors the list to add validation errors to + * @param fields die Liste der zu validierenden Felder + * @param segmentName der Name des übergeordneten Segments + * @param segmentPosition die Position des übergeordneten Segments + * @param errors die Liste zum Hinzufügen von Validierungsfehlern */ private void validateFields(List fields, String segmentName, int segmentPosition, List errors) { var fieldPositions = new HashSet(); @@ -165,7 +214,7 @@ public class DefaultStructureValidator implements StructureValidator { for (var field : fields) { var fieldPosition = field.fieldPosition(); - // Rule 4: Field positions must be unique and positive + // Regel 4: Feldpositionen müssen eindeutig und positiv sein if (!fieldPositions.add(fieldPosition)) { errors.add(createError( "STRUCTURE_004", @@ -176,25 +225,25 @@ public class DefaultStructureValidator implements StructureValidator { field.getFieldName().orElse(""), fieldPosition, String.valueOf(fieldPosition), - "Unique positive field positions required" + "Einzigartige positive Feldpositionen erforderlich" )); } } } /** - * Helper method to create a ValidationError with consistent parameters. + * Hilfsmethode zur Erstellung eines ValidationError mit konsistenten Parametern. * - * @param errorCode the error code - * @param description the error description - * @param severity the validation severity - * @param segmentName the segment name - * @param segmentPosition the segment position - * @param fieldName the field name - * @param fieldPosition the field position - * @param actualValue the actual value - * @param expectedRule the expected rule - * @return a new ValidationError instance + * @param errorCode der Fehlercode + * @param description die Fehlerbeschreibung + * @param severity die Validierungsschweregrad + * @param segmentName der Segmentname + * @param segmentPosition die Segmentposition + * @param fieldName der Feldname + * @param fieldPosition die Feldposition + * @param actualValue der tatsächliche Wert + * @param expectedRule die erwartete Regel + * @return eine neue ValidationError-Instanz */ private ValidationError createError(String errorCode, String description, diff --git a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java index 734567b..c2b3438 100644 --- a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java +++ b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java @@ -2,6 +2,9 @@ package de.gecheckt.asv.validation.structure; import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -12,6 +15,10 @@ 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.parser.DefaultInputFileParser; +import de.gecheckt.asv.parser.DefaultSegmentLineTokenizer; +import de.gecheckt.asv.parser.InputFileParseException; +import de.gecheckt.asv.parser.SegmentLineTokenizer; import de.gecheckt.asv.validation.model.ValidationError; import de.gecheckt.asv.validation.model.ValidationResult; @@ -119,4 +126,89 @@ class DefaultStructureValidatorTest { assertFalse(result.hasInfos()); assertTrue(result.getAllErrors().isEmpty()); } + + @Test + void validate_shouldReportErrorWhenUnhAndUntReferenceNumbersDoNotMatch() throws IOException, InputFileParseException { + // Given + SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); + DefaultInputFileParser parser = new DefaultInputFileParser(tokenizer); + String fileName = "unh-unt-mismatch.asv"; + Path filePath = Path.of("src/test/resources/unh-unt-mismatch.asv"); + String fileContent = Files.readString(filePath); + + // When + InputFile inputFile = parser.parse(fileName, fileContent); + ValidationResult result = validator.validate(inputFile); + + // Then + assertTrue(result.hasErrors()); + assertEquals(1, result.getErrors().size()); + + ValidationError error = result.getErrors().get(0); + assertEquals("STRUCTURE_007", error.errorCode()); + assertEquals("UNH and UNT reference numbers do not match", error.description()); + assertEquals("UNH/UNT", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("12345 != 54321", error.actualValue()); + assertEquals("Reference numbers in UNH and UNT must match", error.expectedRule()); + } + + @Test + void validate_shouldNotReportErrorWhenUnhAndUntReferenceNumbersMatch() throws IOException, InputFileParseException { + // Given + SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); + DefaultInputFileParser parser = new DefaultInputFileParser(tokenizer); + String fileName = "unh-unt-match.asv"; + Path filePath = Path.of("src/test/resources/unh-unt-match.asv"); + String fileContent = Files.readString(filePath); + + // When + InputFile inputFile = parser.parse(fileName, fileContent); + ValidationResult result = validator.validate(inputFile); + + // Then + // Check that we don't have the STRUCTURE_007 error + assertFalse(result.getErrors().stream() + .anyMatch(error -> "STRUCTURE_007".equals(error.errorCode()))); + } + + @Test + void validate_shouldNotReportAdditionalErrorWhenUnhIsMissing() throws IOException, InputFileParseException { + // Given + SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); + DefaultInputFileParser parser = new DefaultInputFileParser(tokenizer); + String fileName = "no-unh.asv"; + Path filePath = Path.of("src/test/resources/no-unh.asv"); + String fileContent = Files.readString(filePath); + + // When + InputFile inputFile = parser.parse(fileName, fileContent); + ValidationResult result = validator.validate(inputFile); + + // Then + // Should not have STRUCTURE_007 error for mismatch + // Any existing structural errors are acceptable but not our new error + assertFalse(result.getErrors().stream() + .anyMatch(error -> "STRUCTURE_007".equals(error.errorCode()))); + } + + @Test + void validate_shouldNotReportAdditionalErrorWhenUntIsMissing() throws IOException, InputFileParseException { + // Given + SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); + DefaultInputFileParser parser = new DefaultInputFileParser(tokenizer); + String fileName = "no-unt.asv"; + Path filePath = Path.of("src/test/resources/no-unt.asv"); + String fileContent = Files.readString(filePath); + + // When + InputFile inputFile = parser.parse(fileName, fileContent); + ValidationResult result = validator.validate(inputFile); + + // Then + // Should not have STRUCTURE_007 error for mismatch + // Any existing structural errors are acceptable but not our new error + assertFalse(result.getErrors().stream() + .anyMatch(error -> "STRUCTURE_007".equals(error.errorCode()))); + } } \ No newline at end of file diff --git a/src/test/resources/no-unh.asv b/src/test/resources/no-unh.asv new file mode 100644 index 0000000..bcde450 --- /dev/null +++ b/src/test/resources/no-unh.asv @@ -0,0 +1,8 @@ +BGM+220+100001' +DTM+137:20260325:102' +NAD+BY+5000000000000:16++Customer Name' +LIN+1++Product123:SA' +QTY+21:10:PCE' +UNS+S' +CNT+2:1' +UNT+9+12345' \ No newline at end of file diff --git a/src/test/resources/no-unt.asv b/src/test/resources/no-unt.asv new file mode 100644 index 0000000..30125c6 --- /dev/null +++ b/src/test/resources/no-unt.asv @@ -0,0 +1,8 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' +BGM+220+100001' +DTM+137:20260325:102' +NAD+BY+5000000000000:16++Customer Name' +LIN+1++Product123:SA' +QTY+21:10:PCE' +UNS+S' +CNT+2:1' \ No newline at end of file diff --git a/src/test/resources/unh-unt-match.asv b/src/test/resources/unh-unt-match.asv new file mode 100644 index 0000000..92b1928 --- /dev/null +++ b/src/test/resources/unh-unt-match.asv @@ -0,0 +1,9 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' +BGM+220+100001' +DTM+137:20260325:102' +NAD+BY+5000000000000:16++Customer Name' +LIN+1++Product123:SA' +QTY+21:10:PCE' +UNS+S' +CNT+2:1' +UNT+9+12345' \ No newline at end of file diff --git a/src/test/resources/unh-unt-mismatch.asv b/src/test/resources/unh-unt-mismatch.asv new file mode 100644 index 0000000..fd00d7d --- /dev/null +++ b/src/test/resources/unh-unt-mismatch.asv @@ -0,0 +1,9 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' +BGM+220+100001' +DTM+137:20260325:102' +NAD+BY+5000000000000:16++Customer Name' +LIN+1++Product123:SA' +QTY+21:10:PCE' +UNS+S' +CNT+2:1' +UNT+6+54321' \ No newline at end of file