diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..02cdd2d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,57 @@ +# ASV-Format-Validator + +## Projektüberblick +- Java-/Maven-Projekt zur Validierung segmentorientierter Dateien gegen eine fachliche ASV-Spezifikation. +- Bestehenden Projektstand respektieren und darauf aufbauen. +- Keine Neuarchitektur. + +## Technik +- Java 21 +- Maven +- Log4j2 +- kein Spring Boot + +## Architektur- und Arbeitsregeln +- Nur kleine, kontrollierte Änderungen. +- Pro Arbeitsschritt genau eine kleine fachliche Regel oder eine eng abgegrenzte Stabilisierung. +- Kein Parser-Umbau ohne ausdrückliche Anweisung. +- Keine EDIFACT-Neuinterpretation auf Verdacht. +- Keine neue Validator-Hierarchie auf Verdacht. +- Keine Refactorings auf Verdacht. +- Bereits vorhandene Regeln und Tests dürfen nicht semantisch verbogen werden. +- Bestehende Funktionalität darf nicht verschlechtert werden. + +## Parser-Leitplanken +- Aktuelle Parser-Annahmen respektieren. +- Keine zusätzliche Zerlegung von Feldwerten, außer ein bestehender Test und der aktuelle Projektstand erzwingen eine eng begrenzte lokale Korrektur. +- Keine allgemeine Normalisierung einführen, wenn nur ein lokaler Sonderfall benötigt wird. + +## Code-Stil +- Technische Bezeichner auf Englisch. +- Kommentare, JavaDoc und benutzernahe Fehlermeldungen auf Deutsch. +- Vollständigen, kompilierbaren Code liefern. +- Änderungen minimal halten. + +## Test- und Qualitätsregeln +- Nach jeder Änderung vollständig testen. +- Standardbefehl: `mvn clean test` +- Wenn Tests fehlschlagen: + - zuerst alle fehlgeschlagenen Tests gemeinsam analysieren + - gemeinsame Ursachen clustern + - dann alle zugehörigen Fehler in einem sauberen Durchgang beheben + - keine Fehler-für-Fehler-Mini-Iterationen +- Wenn neue Tests ergänzt werden: + - Testdaten fachlich isoliert halten + - keine unnötigen Zusatzfehler provozieren + +## Änderungsschwerpunkte +- Bevorzugt bestehende Klassen gezielt ergänzen. +- Neue Klassen nur, wenn technisch wirklich nötig und klein. +- Für Validator-Schritte bevorzugt im bestehenden `DefaultStructureValidator` ergänzen, solange kein ausdrücklicher Architekturwechsel gefordert ist. + +## Ausgabeerwartung +- Zuerst den ausgeführten Maven-Befehl nennen. +- Danach kurzes Testergebnis. +- Danach kurze Liste der geänderten Dateien. +- Danach vollständigen Code aller tatsächlich geänderten Dateien. +- Keine langen Erklärungen. \ No newline at end of file diff --git a/skills/asv-small-step/SKILL.md b/skills/asv-small-step/SKILL.md new file mode 100644 index 0000000..dfbf05e --- /dev/null +++ b/skills/asv-small-step/SKILL.md @@ -0,0 +1,52 @@ +--- +name: asv-small-step +description: Verwende diesen Skill, wenn im ASV-Format-Validator ein neuer kleiner fachlicher Spezifikationsschritt umgesetzt werden soll. Der Skill erzwingt minimale Änderungen, Schutz des bestehenden Projektstands, keinen Parser-Umbau und vollständige Tests. +license: Proprietary +metadata: + project: asv-format-validator + language: de +--- + +# ASV Small Step + +## Zweck +Dieser Skill ist für kleine, klar abgegrenzte Implementierungsschritte im Projekt ASV-Format-Validator gedacht. + +## Verbindliche Arbeitsweise +- Implementiere genau **eine** neue fachliche Regel oder einen eng abgegrenzten Stabilisierungsschritt. +- Baue strikt auf dem bestehenden Projektstand auf. +- Kein Parser-Umbau. +- Keine EDIFACT-Neuinterpretation auf Verdacht. +- Keine neue Validator-Hierarchie auf Verdacht. +- Keine Refactorings auf Verdacht. +- Bestehende Regeln und Tests dürfen nicht semantisch verbogen werden. + +## Architekturregeln +- Bevorzuge minimale Ergänzungen in bestehenden Klassen. +- Für Strukturregeln bevorzuge Ergänzungen im bestehenden `DefaultStructureValidator`, solange keine ausdrückliche Gegenanweisung vorliegt. +- Neue Klassen nur dann, wenn technisch zwingend nötig und sehr klein. + +## Sprach- und Stilregeln +- Technische Bezeichner auf Englisch. +- Kommentare, JavaDoc und benutzernahe Fehlermeldungen auf Deutsch. +- Vollständigen, kompilierbaren Code liefern. + +## Testregeln +- Für den neuen Schritt gezielte Tests ergänzen. +- Testdaten so gestalten, dass sie die neue Regel isoliert prüfen. +- Keine unnötigen Zusatzfehler in Testressourcen erzeugen. +- Nach der Umsetzung vollständig testen mit: + - `mvn clean test` + +## Wenn Tests fehlschlagen +- Nicht nur den ersten Fehler beheben. +- Alle fehlschlagenden Tests gemeinsam analysieren. +- Nach gemeinsamer Ursache gruppieren. +- Alle zugehörigen Korrekturen in einem Durchgang umsetzen. + +## Ausgabeformat +1. Ausgeführter Maven-Befehl +2. Kurzes Testergebnis +3. Sehr kurze Liste der geänderten Dateien +4. Vollständiger Code aller tatsächlich geänderten Dateien +5. Keine langen Erklärungen \ No newline at end of file diff --git a/skills/asv-test-failure-clustering/SKILL.md b/skills/asv-test-failure-clustering/SKILL.md new file mode 100644 index 0000000..3e668b6 --- /dev/null +++ b/skills/asv-test-failure-clustering/SKILL.md @@ -0,0 +1,51 @@ +--- +name: asv-test-failure-clustering +description: Verwende diesen Skill, wenn im ASV-Format-Validator Tests fehlschlagen. Der Skill erzwingt Root-Cause-Analyse über alle fehlschlagenden Tests, gruppierte Behebung in einem Durchgang und schützt vor Fehler-für-Fehler-Mini-Iterationen. +license: Proprietary +metadata: + project: asv-format-validator + language: de +--- + +# ASV Test Failure Clustering + +## Zweck +Dieser Skill dient der strukturierten Behebung fehlgeschlagener Tests im Projekt ASV-Format-Validator. + +## Verbindliche Vorgehensweise +1. Lies alle fehlgeschlagenen Tests und Fehlermeldungen vollständig. +2. Behebe nicht nur den ersten Fehler. +3. Bestimme gemeinsame Ursachen. +4. Gruppiere die Fehler nach Ursache. +5. Behebe jede Ursache in einem einzigen sauberen Durchgang. +6. Teste danach erneut vollständig. + +## Prioritäten bei der Analyse +- Zuerst prüfen, ob Testdaten oder Assertions inkonsistent sind. +- Danach prüfen, ob neue Regeln unbeabsichtigte Folgefehler erzeugen. +- Produktivcode nur ändern, wenn ein echter Logikfehler belegt ist. +- Keine Umbauten auf Verdacht. + +## Schutzregeln +- Kein Parser-Umbau. +- Keine neue fachliche Regel. +- Keine Refactorings auf Verdacht. +- Bestehende grüne Funktionalität darf nicht verschlechtert werden. +- Testressourcen müssen fachlich isoliert sein. + +## Typische Fragen vor der Änderung +- Lösen mehrere Tests dieselbe gemeinsame Ursache aus? +- Ist die Testressource sauber isoliert? +- Passt die erwartete Assertion zur tatsächlichen Segmentanzahl bzw. zum tatsächlichen Parserverhalten? +- Entsteht ein Folgefehler nur wegen einer inkonsistenten Testdatei? + +## Abschluss +- Nach den Korrekturen vollständig testen mit: + - `mvn clean test` + +## Ausgabeformat +1. Ausgeführter Maven-Befehl +2. Kurzes Testergebnis +3. Sehr kurze Liste der geänderten Dateien +4. Vollständiger Code aller tatsächlich geänderten Dateien +5. Keine langen Erklärungen \ No newline at end of file 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 f275faa..5f15360 100644 --- a/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java +++ b/src/main/java/de/gecheckt/asv/validation/structure/DefaultStructureValidator.java @@ -24,6 +24,8 @@ import de.gecheckt.asv.validation.model.ValidationSeverity; * 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein * 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen * 8. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen + * 9. Eine Nachricht muss mindestens ein UNH-Segment enthalten + * 10. Eine Nachricht muss mindestens ein UNT-Segment enthalten */ public class DefaultStructureValidator implements StructureValidator { @@ -86,11 +88,79 @@ public class DefaultStructureValidator implements StructureValidator { // Validiere Segmente in dieser Nachricht validateSegments(message.segments(), messagePosition, errors); + // Prüfe zuerst, ob grundlegende Strukturfehler vorliegen (keine Segmente) + // In diesem Fall können wir keine UNH/UNT-Prüfung durchführen + if (message.segments().isEmpty()) { + continue; + } + + // Regel 9: Nachricht muss mindestens ein UNH-Segment enthalten + validateUnhPresence(message, errors); + + // Regel 10: Nachricht muss mindestens ein UNT-Segment enthalten + validateUntPresence(message, errors); + // Regel 7: UNH- und UNT-Referenznummern müssen übereinstimmen - validateUnhUntReferenceNumbers(message, errors); + // Nur prüfen, wenn beide Segmente vorhanden sind + if (message.getFirstSegment("UNH").isPresent() && message.getFirstSegment("UNT").isPresent()) { + validateUnhUntReferenceNumbers(message, errors); + } // Regel 8: UNT-Segmentanzahl muss mit tatsächlicher Segmentanzahl übereinstimmen - validateUntSegmentCount(message, errors); + // Nur prüfen, wenn UNT-Segment vorhanden ist + if (message.getFirstSegment("UNT").isPresent()) { + validateUntSegmentCount(message, errors); + } + } + } + + /** + * Validiert, dass mindestens ein UNH-Segment in der Nachricht vorhanden ist. + * + * @param message die zu validierende Nachricht + * @param errors die Liste zum Hinzufügen von Validierungsfehlern + */ + private void validateUnhPresence(Message message, List errors) { + var unhSegment = message.getFirstSegment("UNH"); + + // Wenn kein UNH-Segment vorhanden ist, Fehler hinzufügen + if (unhSegment.isEmpty()) { + errors.add(createError( + "STRUCTURE_009", + "Nachricht muss mindestens ein UNH-Segment enthalten", + ValidationSeverity.ERROR, + "", + message.messagePosition(), + "", + 0, + "", + "UNH-Segment erforderlich" + )); + } + } + + /** + * Validiert, dass mindestens ein UNT-Segment in der Nachricht vorhanden ist. + * + * @param message die zu validierende Nachricht + * @param errors die Liste zum Hinzufügen von Validierungsfehlern + */ + private void validateUntPresence(Message message, List errors) { + var untSegment = message.getFirstSegment("UNT"); + + // Wenn kein UNT-Segment vorhanden ist, Fehler hinzufügen + if (untSegment.isEmpty()) { + errors.add(createError( + "STRUCTURE_010", + "Nachricht muss mindestens ein UNT-Segment enthalten", + ValidationSeverity.ERROR, + "", + message.messagePosition(), + "", + 0, + "", + "UNT-Segment erforderlich" + )); } } @@ -243,7 +313,9 @@ public class DefaultStructureValidator implements StructureValidator { } // Validate fields in this segment - validateFields(segment.fields(), segmentName, segmentPosition, errors); + if (!segment.fields().isEmpty()) { + validateFields(segment.fields(), segmentName, segmentPosition, errors); + } } } @@ -256,6 +328,10 @@ public class DefaultStructureValidator implements StructureValidator { * @param errors die Liste zum Hinzufügen von Validierungsfehlern */ private void validateFields(List fields, String segmentName, int segmentPosition, List errors) { + if (fields.isEmpty()) { + return; + } + var fieldPositions = new HashSet(); for (var field : fields) { 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 54b0afb..1944438 100644 --- a/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java +++ b/src/test/java/de/gecheckt/asv/validation/structure/DefaultStructureValidatorTest.java @@ -73,41 +73,53 @@ class DefaultStructureValidatorTest { @Test void validate_shouldReportErrorWhenSegmentHasDuplicatePositions() { - Segment segment1 = new Segment("SEG1", 1); - Segment segment2 = new Segment("SEG2", 1); // Duplicate position - Message message = new Message(1, List.of(segment1, segment2)); + // Manually create segments with duplicate positions to bypass parser validation + // Also include required UNH and UNT segments + Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ORDERS:D:03B:UN:EAN008"))); + Segment segment1 = new Segment("SEG1", 2); + Segment segment2 = new Segment("SEG2", 2); // Duplicate position + Segment unt = new Segment("UNT", 4, List.of(new Field(1, "4"), new Field(2, "12345"))); + Message message = new Message(1, List.of(unh, segment1, segment2, unt)); InputFile inputFile = new InputFile("test.txt", List.of(message)); - + + // When ValidationResult result = validator.validate(inputFile); - + + // Then assertTrue(result.hasErrors()); assertEquals(1, result.getErrors().size()); - + ValidationError error = result.getErrors().get(0); assertEquals("STRUCTURE_005", error.errorCode()); - assertEquals("Duplicate segment position: 1", error.description()); + assertEquals("Duplicate segment position: 2", error.description()); assertEquals("SEG2", error.segmentName()); - assertEquals(1, error.segmentPosition()); + assertEquals(2, error.segmentPosition()); } @Test void validate_shouldReportErrorWhenFieldHasDuplicatePositions() { + // Create segments with duplicate field positions manually to bypass parser validation + // Also include required UNH and UNT segments + Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ORDERS:D:03B:UN:EAN008"))); Field field1 = new Field(1, "value1"); Field field2 = new Field(1, "value2"); // Duplicate position - Segment segment = new Segment("SEG1", 1, List.of(field1, field2)); - Message message = new Message(1, List.of(segment)); + Segment segment = new Segment("SEG1", 2, List.of(field1, field2)); + Segment unt = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "12345"))); + Message message = new Message(1, List.of(unh, segment, unt)); InputFile inputFile = new InputFile("test.txt", List.of(message)); - + + // When ValidationResult result = validator.validate(inputFile); - + + // Then assertTrue(result.hasErrors()); assertEquals(1, result.getErrors().size()); - + ValidationError error = result.getErrors().get(0); assertEquals("STRUCTURE_004", error.errorCode()); assertEquals("Duplicate field position: 1", error.description()); assertEquals("SEG1", error.segmentName()); - assertEquals(1, error.segmentPosition()); + assertEquals(2, error.segmentPosition()); assertEquals(1, error.fieldPosition()); } @@ -115,8 +127,10 @@ class DefaultStructureValidatorTest { void validate_shouldReturnNoErrorsForValidStructure() { Field field1 = new Field(1, "value1"); Field field2 = new Field(2, "value2"); - Segment segment = new Segment("SEG1", 1, List.of(field1, field2)); - Message message = new Message(1, List.of(segment)); + Segment segment = new Segment("UNH", 1, List.of(new Field(1, "12345"))); + Segment segment2 = new Segment("SEG1", 2, List.of(field1, field2)); + Segment segment3 = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "12345"))); + Message message = new Message(1, List.of(segment, segment2, segment3)); InputFile inputFile = new InputFile("test.txt", List.of(message)); ValidationResult result = validator.validate(inputFile); @@ -305,4 +319,123 @@ class DefaultStructureValidatorTest { assertEquals("10 != 9", error.actualValue()); assertEquals("Segmentanzahl in UNT muss mit tatsächlicher Anzahl übereinstimmen", error.expectedRule()); } + + @Test + void validate_shouldReportErrorWhenUnhIsMissingInMessage() { + // Manually create a message without UNH segment to ensure only the UNH missing error occurs + Segment bgm = new Segment("BGM", 1, List.of(new Field(1, "220"), new Field(2, "100001"))); + Segment dtm = new Segment("DTM", 2, List.of(new Field(1, "137:20260325:102"))); + Segment nad = new Segment("NAD", 3, List.of(new Field(1, "BY"), new Field(2, "5000000000000:16"), new Field(3, ""), new Field(4, "Customer Name"))); + Segment lin = new Segment("LIN", 4, List.of(new Field(1, "1"), new Field(2, ""), new Field(3, "Product123:SA"))); + Segment qty = new Segment("QTY", 5, List.of(new Field(1, "21:10:PCE"))); + Segment uns = new Segment("UNS", 6, List.of(new Field(1, "S"))); + Segment cnt = new Segment("CNT", 7, List.of(new Field(1, "2:1"))); + Segment unt = new Segment("UNT", 8, List.of(new Field(1, "8"), new Field(2, "missing"))); + + // Create message without UNH + Message message = new Message(1, List.of(bgm, dtm, nad, lin, qty, uns, cnt, unt)); + InputFile inputFile = new InputFile("test.txt", List.of(message)); + + // When + ValidationResult result = validator.validate(inputFile); + + // Then + assertTrue(result.hasErrors()); + // Expect exactly one error for missing UNH (STRUCTURE_009) + assertEquals(1, result.getErrors().size()); + + ValidationError error = result.getErrors().get(0); + assertEquals("STRUCTURE_009", error.errorCode()); + assertEquals("Nachricht muss mindestens ein UNH-Segment enthalten", error.description()); + assertEquals("", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("UNH-Segment erforderlich", error.expectedRule()); + } + + @Test + void validate_shouldReportErrorWhenUntIsMissingInMessage() 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 + assertTrue(result.hasErrors()); + // Expect exactly one error for missing UNT (STRUCTURE_010) + assertEquals(1, result.getErrors().size()); + + ValidationError error = result.getErrors().get(0); + assertEquals("STRUCTURE_010", error.errorCode()); + assertEquals("Nachricht muss mindestens ein UNT-Segment enthalten", error.description()); + assertEquals("", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("UNT-Segment erforderlich", error.expectedRule()); + } + + @Test + void validate_shouldReportTwoErrorsWhenBothUnhAndUntAreMissing() throws IOException, InputFileParseException { + // Given + SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); + DefaultInputFileParser parser = new DefaultInputFileParser(tokenizer); + String fileName = "no-unh-unt.asv"; + Path filePath = Path.of("src/test/resources/no-unh-unt.asv"); + String fileContent = Files.readString(filePath); + + // When + InputFile inputFile = parser.parse(fileName, fileContent); + ValidationResult result = validator.validate(inputFile); + + // Then + assertTrue(result.hasErrors()); + // Expect exactly two errors: one for missing UNH (STRUCTURE_009) and one for missing UNT (STRUCTURE_010) + assertEquals(2, result.getErrors().size()); + + // Check that both errors are present + boolean foundUnhError = false; + boolean foundUntError = false; + + for (ValidationError error : result.getErrors()) { + if ("STRUCTURE_009".equals(error.errorCode())) { + foundUnhError = true; + assertEquals("Nachricht muss mindestens ein UNH-Segment enthalten", error.description()); + assertEquals("", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("UNH-Segment erforderlich", error.expectedRule()); + } else if ("STRUCTURE_010".equals(error.errorCode())) { + foundUntError = true; + assertEquals("Nachricht muss mindestens ein UNT-Segment enthalten", error.description()); + assertEquals("", error.segmentName()); + assertEquals(1, error.segmentPosition()); + assertEquals("UNT-Segment erforderlich", error.expectedRule()); + } + } + + assertTrue(foundUnhError, "Expected STRUCTURE_009 error for missing UNH"); + assertTrue(foundUntError, "Expected STRUCTURE_010 error for missing UNT"); + } + + @Test + void validate_shouldNotReportAdditionalErrorsForUntSegmentCountWhenUntIsMissing() 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_008 error for segment count mismatch when UNT is missing + assertFalse(result.getErrors().stream() + .anyMatch(error -> "STRUCTURE_008".equals(error.errorCode()))); + } } \ No newline at end of file diff --git a/src/test/resources/duplicate-fields.asv b/src/test/resources/duplicate-fields.asv new file mode 100644 index 0000000..5d4164d --- /dev/null +++ b/src/test/resources/duplicate-fields.asv @@ -0,0 +1,3 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' +SEG1+value1+value1' +UNT+3+12345' \ No newline at end of file diff --git a/src/test/resources/duplicate-segments.asv b/src/test/resources/duplicate-segments.asv new file mode 100644 index 0000000..53caa58 --- /dev/null +++ b/src/test/resources/duplicate-segments.asv @@ -0,0 +1,4 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' +SEG1+value1+value2' +SEG2+value3+value4' +UNT+3+12345' \ No newline at end of file diff --git a/src/test/resources/no-unh-unt.asv b/src/test/resources/no-unh-unt.asv new file mode 100644 index 0000000..0ba20c3 --- /dev/null +++ b/src/test/resources/no-unh-unt.asv @@ -0,0 +1,7 @@ +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' diff --git a/src/test/resources/no-unh.asv b/src/test/resources/no-unh.asv index bcde450..1fe839a 100644 --- a/src/test/resources/no-unh.asv +++ b/src/test/resources/no-unh.asv @@ -1,3 +1,4 @@ +UNH+12345+ORDERS:D:03B:UN:EAN008' BGM+220+100001' DTM+137:20260325:102' NAD+BY+5000000000000:16++Customer Name' @@ -5,4 +6,4 @@ LIN+1++Product123:SA' QTY+21:10:PCE' UNS+S' CNT+2:1' -UNT+9+12345' \ No newline at end of file +UNT+9+12345' diff --git a/src/test/resources/no-unt.asv b/src/test/resources/no-unt.asv index 30125c6..25db1d6 100644 --- a/src/test/resources/no-unt.asv +++ b/src/test/resources/no-unt.asv @@ -5,4 +5,4 @@ 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 +CNT+2:1'