Erste fachliche Strukturregel ergänzen: UNH-/UNT-Referenznummer prüfen

This commit is contained in:
2026-03-26 08:21:13 +01:00
parent 1b4b22b073
commit 6a7d150007
6 changed files with 222 additions and 47 deletions

View File

@@ -13,15 +13,16 @@ import de.gecheckt.asv.validation.model.ValidationResult;
import de.gecheckt.asv.validation.model.ValidationSeverity; 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: * Geprüfte Regeln:
* 1. InputFile must contain at least one Message * 1. Die Eingabedatei muss mindestens eine Nachricht enthalten
* 2. Each Message must contain at least one Segment * 2. Jede Nachricht muss mindestens ein Segment enthalten
* 3. Segment names must not be empty * 3. Segmentnamen dürfen nicht leer sein
* 4. Field positions within a Segment must be unique and positive * 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein
* 5. Segment positions within a Message must be unique and positive * 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein
* 6. Message positions within an InputFile must be unique and positive * 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 { public class DefaultStructureValidator implements StructureValidator {
@@ -44,10 +45,10 @@ public class DefaultStructureValidator implements StructureValidator {
"", "",
0, 0,
"", "",
"At least one message required" "Mindestens eine Nachricht erforderlich"
)); ));
} else { } else {
// Process messages if they exist // Verarbeite Nachrichten, wenn sie vorhanden sind
validateMessages(inputFile.messages(), errors); 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 messages die Liste der zu validierenden Nachrichten
* @param errors the list to add validation errors to * @param errors die Liste zum Hinzufügen von Validierungsfehlern
*/ */
private void validateMessages(List<Message> messages, List<ValidationError> errors) { private void validateMessages(List<Message> messages, List<ValidationError> errors) {
var messagePositions = new HashSet<Integer>(); var messagePositions = new HashSet<Integer>();
@@ -66,35 +67,83 @@ public class DefaultStructureValidator implements StructureValidator {
for (var message : messages) { for (var message : messages) {
var messagePosition = message.messagePosition(); 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)) { if (!messagePositions.add(messagePosition)) {
errors.add(createError( errors.add(createError(
"STRUCTURE_006", "STRUCTURE_006",
"Duplicate message position: " + messagePosition, "Doppelte Nachrichtenposition: " + messagePosition,
ValidationSeverity.ERROR, ValidationSeverity.ERROR,
"", "",
messagePosition, messagePosition,
"", "",
0, 0,
String.valueOf(messagePosition), 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); 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 message die zu validierende Nachricht
* @param messagePosition the position of the parent message * @param errors die Liste zum Hinzufügen von Validierungsfehlern
* @param errors the list to add validation errors to */
private void validateUnhUntReferenceNumbers(Message message, List<ValidationError> 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<Segment> segments, int messagePosition, List<ValidationError> errors) { private void validateSegments(List<Segment> segments, int messagePosition, List<ValidationError> errors) {
// Rule 2: Each Message must contain at least one Segment // Regel 2: Jede Nachricht muss mindestens ein Segment enthalten
if (segments.isEmpty()) { if (segments.isEmpty()) {
errors.add(createError( errors.add(createError(
"STRUCTURE_002", "STRUCTURE_002",
@@ -105,9 +154,9 @@ public class DefaultStructureValidator implements StructureValidator {
"", "",
0, 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<Integer>(); var segmentPositions = new HashSet<Integer>();
@@ -116,22 +165,22 @@ public class DefaultStructureValidator implements StructureValidator {
var segmentName = segment.segmentName(); var segmentName = segment.segmentName();
var segmentPosition = segment.segmentPosition(); 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()) { if (segmentName == null || segmentName.isEmpty()) {
errors.add(createError( errors.add(createError(
"STRUCTURE_003", "STRUCTURE_003",
"Segment name must not be empty", "Segmentname darf nicht leer sein",
ValidationSeverity.ERROR, ValidationSeverity.ERROR,
segmentName != null ? segmentName : "", segmentName != null ? segmentName : "",
segmentPosition, segmentPosition,
"", "",
0, 0,
segmentName != null ? segmentName : "null", 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)) { if (!segmentPositions.add(segmentPosition)) {
errors.add(createError( errors.add(createError(
"STRUCTURE_005", "STRUCTURE_005",
@@ -142,7 +191,7 @@ public class DefaultStructureValidator implements StructureValidator {
"", "",
0, 0,
String.valueOf(segmentPosition), 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 fields die Liste der zu validierenden Felder
* @param segmentName the name of the parent segment * @param segmentName der Name des übergeordneten Segments
* @param segmentPosition the position of the parent segment * @param segmentPosition die Position des übergeordneten Segments
* @param errors the list to add validation errors to * @param errors die Liste zum Hinzufügen von Validierungsfehlern
*/ */
private void validateFields(List<Field> fields, String segmentName, int segmentPosition, List<ValidationError> errors) { private void validateFields(List<Field> fields, String segmentName, int segmentPosition, List<ValidationError> errors) {
var fieldPositions = new HashSet<Integer>(); var fieldPositions = new HashSet<Integer>();
@@ -165,7 +214,7 @@ public class DefaultStructureValidator implements StructureValidator {
for (var field : fields) { for (var field : fields) {
var fieldPosition = field.fieldPosition(); 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)) { if (!fieldPositions.add(fieldPosition)) {
errors.add(createError( errors.add(createError(
"STRUCTURE_004", "STRUCTURE_004",
@@ -176,25 +225,25 @@ public class DefaultStructureValidator implements StructureValidator {
field.getFieldName().orElse(""), field.getFieldName().orElse(""),
fieldPosition, fieldPosition,
String.valueOf(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 errorCode der Fehlercode
* @param description the error description * @param description die Fehlerbeschreibung
* @param severity the validation severity * @param severity die Validierungsschweregrad
* @param segmentName the segment name * @param segmentName der Segmentname
* @param segmentPosition the segment position * @param segmentPosition die Segmentposition
* @param fieldName the field name * @param fieldName der Feldname
* @param fieldPosition the field position * @param fieldPosition die Feldposition
* @param actualValue the actual value * @param actualValue der tatsächliche Wert
* @param expectedRule the expected rule * @param expectedRule die erwartete Regel
* @return a new ValidationError instance * @return eine neue ValidationError-Instanz
*/ */
private ValidationError createError(String errorCode, private ValidationError createError(String errorCode,
String description, String description,

View File

@@ -2,6 +2,9 @@ package de.gecheckt.asv.validation.structure;
import static org.junit.jupiter.api.Assertions.*; 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.Collections;
import java.util.List; 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.InputFile;
import de.gecheckt.asv.domain.model.Message; import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment; 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.ValidationError;
import de.gecheckt.asv.validation.model.ValidationResult; import de.gecheckt.asv.validation.model.ValidationResult;
@@ -119,4 +126,89 @@ class DefaultStructureValidatorTest {
assertFalse(result.hasInfos()); assertFalse(result.hasInfos());
assertTrue(result.getAllErrors().isEmpty()); 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())));
}
} }

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'