Skip to content

Commit

Permalink
DT-612 [6/6]: Add remaining complex validations
Browse files Browse the repository at this point in the history
Signed-off-by: Niels Thykier <[email protected]>
  • Loading branch information
nt-gt committed Jan 9, 2024
1 parent 5ce5d77 commit 3f16e23
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

public class JsonAttribute {

private static final JsonPointer ROOT_PTR = JsonPointer.compile("/");

public static ActionCheck contentChecks(
Predicate<String> isRelevantForRoleName,
UUID matchedExchangeUuid,
Expand Down Expand Up @@ -137,6 +139,54 @@ public static JsonContentCheck allIndividualMatchesMustBeValid(
);
}

public static JsonContentMatchedValidation unique(
String field
) {
return unique(
"field %s with value".formatted(field),
(node) -> node.path(field).asText(null)
);
}

public static JsonContentMatchedValidation unique(
String fieldA,
String fieldB
) {
return unique(
"combination of %s/%s with value".formatted(fieldA, fieldB),
(node) -> {
var valueA = node.path(fieldA).asText(null);
var valueB = node.path(fieldB).asText(null);
if (valueA == null || valueB == null) {
return null;
}
return valueA + "/" + valueB;
}
);
}

public static JsonContentMatchedValidation unique(
String keyDescription,
Function<JsonNode, String> keyFunction
) {
return (array, contextPath) -> {
var seen = new HashSet<String>();
var duplicates = new LinkedHashSet<String>();
for (var node : array) {
var key = keyFunction.apply(node);
if (key == null) {
continue;
}
if (!seen.add(key)) {
duplicates.add(key);
}
}
return duplicates.stream()
.map(dup -> "The %s '%s' must be unique but was used more than once in '%s'".formatted(keyDescription, dup, contextPath))
.collect(Collectors.toSet());
};
}

public static JsonContentMatchedValidation path(String path, JsonContentMatchedValidation delegate) {
return (nodeToValidate, contextPath) -> {
var fullContext = contextPath.isEmpty() ? path : contextPath + "." + path;
Expand Down Expand Up @@ -512,6 +562,13 @@ public static JsonContentCheck customValidator(
return JsonContentCheckImpl.of(description, validator);
}

public static JsonContentCheck customValidator(
@NonNull String description,
@NonNull JsonContentMatchedValidation validator
) {
return JsonContentCheckImpl.of(description, ROOT_PTR, validator);
}

private static Function<JsonNode, JsonNode> at(JsonPointer jsonPointer) {
return (refNode) -> refNode.at(jsonPointer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public JsonNode getJsonForHumanReadablePrompt() {
"DKAAR",
"640510",
"Shoes - black, 400 boxes",
"ServiceContractReference-1234"
"ServiceContractReference-1234",
"QuotationReference-1234"
);
case REEFER -> new CarrierScenarioParameters(
"Booking Reference",
Expand All @@ -85,7 +86,8 @@ public JsonNode getJsonForHumanReadablePrompt() {
"DKAAR",
"04052090",
"Dairy products",
"ServiceContractReference-1234"
"ServiceContractReference-1234AR",
"QuotationReference-1234AR"
);
case DG -> new CarrierScenarioParameters(
"Booking Reference",
Expand All @@ -95,7 +97,8 @@ public JsonNode getJsonForHumanReadablePrompt() {
"DKAAR",
"293499",
"Environmentally hazardous substance, liquid, N.O.S (Propiconazole)",
"ServiceContractReference-1234"
"ServiceContractReference-1234DG",
"QuotationReference-1234DG"
);
};
return csp.toJson();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,43 @@ public class EBLChecks {
JsonAttribute.matchedMustBeDatasetKeywordIfPresent(EblDatasets.REFERENCE_TYPE)
);

private static JsonContentMatchedValidation combineAndValidateAgainstDataset(
KeywordDataset dataset,
String nameA,
String nameB
) {
return (nodeToValidate, contextPath) -> {
var codeA = nodeToValidate.path(nameA).asText("");
var codeB = nodeToValidate.path(nameB).asText("");
var combined = codeA + "/" + codeB;
if (!dataset.contains(combined)) {
return Set.of(
"The combination of '%s' ('%s') and '%s' ('%s') used in '%s' is not known to be a valid combination.".
formatted(codeA, nameA, codeB, nameB, contextPath)
);
}
return Set.of();
};
}

private static final JsonContentCheck TLR_CC_T_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid(
"Validate combination of 'countryCode' and 'type' in 'taxAndLegalReferences'",
(mav) -> {
mav.submitAllMatching("issuingParty.taxLegalReferences.*");
mav.submitAllMatching("documentParties.*.party.taxLegalReferences.*");
},
combineAndValidateAgainstDataset(EblDatasets.LTR_CC_T_COMBINATIONS, "countryCode", "type")
);

private static final JsonContentCheck TLR_CC_T_COMBINATION_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid(
"Each document party can be used at most once",
(mav) -> {
mav.submitAllMatching("issuingParty.taxLegalReferences");
mav.submitAllMatching("documentParties.*.party.taxLegalReferences");
},
JsonAttribute.unique("countryCode", "type")
);


private static final Consumer<MultiAttributeValidator> ALL_UTE = (mav) -> mav.submitAllMatching("utilizedTransportEquipments.*");

Expand Down Expand Up @@ -140,18 +177,8 @@ public class EBLChecks {
private static final JsonContentCheck AMF_CC_MTC_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid(
"Validate combination of 'countryCode' and 'manifestTypeCode' in 'advanceManifestFilings'",
ALL_AMF,
(nodeToValidate, contextPath) -> {
var country = nodeToValidate.path("countryCode").asText("");
var manifestTypeCode = nodeToValidate.path("manifestTypeCode").asText("");
var combined = country + "/" + manifestTypeCode;
if (!EblDatasets.AMF_CC_MTC_COMBINATIONS.contains(combined)) {
return Set.of(
"The combination of '%s' ('countryCounty') and '%s' ('manifestTypeCode') used in '%s' is not known to be a valid combination.".
formatted(country, manifestTypeCode, contextPath)
);
}
return Set.of();
});
combineAndValidateAgainstDataset(EblDatasets.AMF_CC_MTC_COMBINATIONS, "countryCode", "manifestTypeCode")
);

private static final Consumer<MultiAttributeValidator> ALL_CUSTOMS_REFERENCES = (mav) -> {
mav.submitAllMatching("customsReferences.*");
Expand All @@ -163,18 +190,8 @@ public class EBLChecks {
private static final JsonContentCheck CR_CC_T_COMBINATION_KNOWN = JsonAttribute.allIndividualMatchesMustBeValid(
"The combination of 'countryCode' and 'type' in 'customsReferences' must be valid",
ALL_CUSTOMS_REFERENCES,
(nodeToValidate, contextPath) -> {
var country = nodeToValidate.path("countryCode").asText("");
var type = nodeToValidate.path("type").asText("");
var combined = country + "/" + type;
if (!EblDatasets.CUSTOMS_REFERENCE_CC_RTC_COMBINATIONS.contains(combined)) {
return Set.of(
"The combination of '%s' ('countryCounty') and '%s' ('type') used in '%s' is not known to be a valid combination.".
formatted(country, type, contextPath)
);
}
return Set.of();
});
combineAndValidateAgainstDataset(EblDatasets.CUSTOMS_REFERENCE_CC_RTC_COMBINATIONS, "countryCode", "type")
);

private static final JsonContentCheck CR_CC_T_CODES_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid(
"The combination of 'countryCode' and 'type' in '*.customsReferences' must be unique",
Expand All @@ -184,25 +201,7 @@ public class EBLChecks {
mav.submitAllMatching("consignmentItems.*.cargoItems.*.customsReferences");
mav.submitAllMatching("utilizedTransportEquipments.*.customsReferences");
},
(nodeToValidate, contextPath) -> {
var seen = new HashSet<String>();
var duplicates = new LinkedHashSet<String>();

for (var cr : nodeToValidate) {
var cc = cr.path("countryCode").asText(null);
var type = cr.path("type").asText(null);
if (cc == null || type == null) {
continue;
}
var combined = cc + "/" + type;
if (!seen.add(combined)) {
duplicates.add(combined);
}
}
return duplicates.stream()
.map("The countryCode/type combination '%s' was used more than once in 'customsReferences'"::formatted)
.collect(Collectors.toSet());
}
JsonAttribute.unique("countryCode", "type")
);

private static final JsonContentCheck COUNTRY_CODE_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid(
Expand All @@ -225,6 +224,13 @@ public class EBLChecks {
JsonAttribute.matchedMustBeDatasetKeywordIfPresent(EblDatasets.OUTER_PACKAGING_CODE)
);

private static final JsonContentCheck DOCUMENT_PARTY_FUNCTIONS_MUST_BE_UNIQUE = JsonAttribute.customValidator(
"Each document party can be used at most once",
JsonAttribute.path(
"documentParties",
JsonAttribute.unique("partyFunction")
));

private static final JsonContentCheck VALIDATE_DOCUMENT_PARTIES = JsonAttribute.customValidator(
"Validate documentParties",
body -> {
Expand Down Expand Up @@ -254,12 +260,16 @@ public class EBLChecks {
}
}

if (!partyFunctions.contains("SCO") && !body.path("serviceContractReference").isTextual()) {
issues.add("The 'SCO' party is mandatory when 'serviceContractReference' is absent");
if (!partyFunctions.contains("SCO")) {
if (!body.path("serviceContractReference").isTextual()) {
issues.add("The 'SCO' party is mandatory when 'serviceContractReference' is absent");
}
if (!body.path("contractQuotationReference").isTextual()) {
issues.add("The 'SCO' party is mandatory when 'contractQuotationReference' is absent");
}
}
return issues;
}
);
});

private static Consumer<MultiAttributeValidator> allDg(Consumer<MultiAttributeValidator.AttributePathBuilder> consumer) {
return (mav) -> consumer.accept(mav.path("consignmentItems").all().path("cargoItems").all().path("outerPackaging").path("dangerousGoods").all());
Expand Down Expand Up @@ -482,27 +492,9 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment(
RATIO_VOLUME
));


private static final JsonContentCheck ADVANCED_MANIFEST_FILING_CODES_UNIQUE = JsonAttribute.customValidator(
"The combination of 'countryCode' and 'manifestTypeCode' in 'advanceManifestFilings' must be unique",
(body) -> {
var seen = new HashSet<String>();
var duplicates = new LinkedHashSet<String>();
for (var amf : body.path("advanceManifestFilings")) {
var cc = amf.path("countryCode").asText(null);
var mtc = amf.path("manifestTypeCode").asText(null);
if (cc == null || mtc == null) {
continue;
}
var combined = cc + "/" + mtc;
if (!seen.add(combined)) {
duplicates.add(combined);
}
}
return duplicates.stream()
.map("The countryCode/manifestTypeCode combination '%s' was used more than once in 'advanceManifestFilings'"::formatted)
.collect(Collectors.toSet());
}
JsonAttribute.unique("countryCode", "manifestTypeCode")
);

private static final List<JsonContentCheck> STATIC_SI_CHECKS = Arrays.asList(
Expand Down Expand Up @@ -534,6 +526,9 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment(
VOLUME_IMPLIES_VOLUME_UNIT,
CONSIGNMENT_ITEM_VS_CARGO_ITEM_WEIGHT_IS_ALIGNED,
CONSIGNMENT_ITEM_VS_CARGO_ITEM_VOLUME_IS_ALIGNED,
TLR_CC_T_COMBINATION_VALIDATIONS,
TLR_CC_T_COMBINATION_UNIQUE,
DOCUMENT_PARTY_FUNCTIONS_MUST_BE_UNIQUE,
VALIDATE_DOCUMENT_PARTIES
);

Expand Down Expand Up @@ -651,6 +646,9 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment(
RATIO_VOLUME
)
),
TLR_CC_T_COMBINATION_VALIDATIONS,
TLR_CC_T_COMBINATION_UNIQUE,
DOCUMENT_PARTY_FUNCTIONS_MUST_BE_UNIQUE,
VALIDATE_DOCUMENT_PARTIES
);

Expand Down Expand Up @@ -720,6 +718,11 @@ private static void generateCSPRelatedChecks(List<JsonContentCheck> checks, Supp
"serviceContractReference",
cspValue(cspSupplier, CarrierScenarioParameters::serviceContractReference)
));
checks.add(JsonAttribute.mustEqual(
"[Scenario] Verify that the correct 'contractQuotationReference' is used",
"contractQuotationReference",
cspValue(cspSupplier, CarrierScenarioParameters::contractQuotationReference)
));

checks.add(JsonAttribute.mustEqual(
"[Scenario] Verify that the correct 'invoicePayableAt' location is used",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class EblDatasets {
"CA/ACI"
);
public static final KeywordDataset AMF_CC_MTC_COMBINATIONS = KeywordDataset.fromCSVCombiningColumns(EblDatasets.class, "/standards/ebl/datasets/advancemanifestfilings-v3.0.0-b1.csv", "/", "Country Code", "Advance Manifest Filing Type Code");
public static final KeywordDataset LTR_CC_T_COMBINATIONS = KeywordDataset.fromCSVCombiningColumns(EblDatasets.class, "/standards/ebl/datasets/taxandlegalreferences-v3.0.0-b1.csv", "/", "Tax and Legal Reference Country Code", "Tax and Legal Reference Type Code");
public static final KeywordDataset CUSTOMS_REFERENCE_CC_RTC_COMBINATIONS = KeywordDataset.fromCSVCombiningColumns(EblDatasets.class, "/standards/ebl/datasets/customsreferences-v3.0.0-b1.csv", "/", "Customs Reference Country Code", "Customs Reference Type Code");

public static final KeywordDataset OUTER_PACKAGING_CODE = KeywordDataset.fromCSV(EblDatasets.class, "/standards/ebl/datasets/rec21_Rev12e_Annex-V-VI_2021.csv", "Code");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public record CarrierScenarioParameters(
String invoicePayableAtUNLocationCode,
String consignmentItemHSCode,
String descriptionOfGoods,
String serviceContractReference
String serviceContractReference,
String contractQuotationReference
) {
public ObjectNode toJson() {
return new ObjectMapper()
Expand All @@ -22,7 +23,8 @@ public ObjectNode toJson() {
.put("invoicePayableAtUNLocationCode", invoicePayableAtUNLocationCode)
.put("consignmentItemHSCode", consignmentItemHSCode)
.put("descriptionOfGoods", descriptionOfGoods)
.put("serviceContractReference", serviceContractReference);
.put("serviceContractReference", serviceContractReference)
.put("contractQuotationReference", contractQuotationReference);
}

public static CarrierScenarioParameters fromJson(JsonNode jsonNode) {
Expand All @@ -33,7 +35,8 @@ public static CarrierScenarioParameters fromJson(JsonNode jsonNode) {
jsonNode.required("invoicePayableAtUNLocationCode").asText(),
jsonNode.required("consignmentItemHSCode").asText(),
jsonNode.required("descriptionOfGoods").asText(),
jsonNode.required("serviceContractReference").asText()
jsonNode.required("serviceContractReference").asText(),
jsonNode.required("contractQuotationReference").asText()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ private void supplyScenarioParameters(JsonNode actionPrompt) {
"DKAAR",
"640510",
"Shoes - black, 400 boxes",
"SCR-1234-REGULAR"
"SCR-1234-REGULAR",
"QR-1234-REGULAR"
);
case REEFER -> new CarrierScenarioParameters(
"CBR_123_REEFER",
Expand All @@ -103,7 +104,8 @@ private void supplyScenarioParameters(JsonNode actionPrompt) {
"DKAAR",
"04052090",
"Dairy products",
"SCR-1234-REEFER"
"SCR-1234-REEFER",
"QR-1234-REEFER"
);
case DG -> new CarrierScenarioParameters(
"RTM1234567",
Expand All @@ -113,7 +115,8 @@ private void supplyScenarioParameters(JsonNode actionPrompt) {
"DKAAR",
"293499",
"Environmentally hazardous substance, liquid, N.O.S (Propiconazole)",
"SCR-1234-DG"
"SCR-1234-DG",
"QR-1234-DG"
);
};
asyncOrchestratorPostPartyInput(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ private void sendShippingInstructionsRequest(JsonNode actionPrompt) {
Map.entry("INVOICE_PAYABLE_AT_UNLOCATION_CODE", carrierScenarioParameters.invoicePayableAtUNLocationCode()),
Map.entry("CONSIGNMENT_ITEM_HS_CODE", carrierScenarioParameters.consignmentItemHSCode()),
Map.entry("DESCRIPTION_OF_GOODS_PLACEHOLDER", carrierScenarioParameters.descriptionOfGoods()),
Map.entry("SERVICE_CONTRACT_REFERENCE_PLACEHOLDER", carrierScenarioParameters.serviceContractReference())
Map.entry("SERVICE_CONTRACT_REFERENCE_PLACEHOLDER", carrierScenarioParameters.serviceContractReference()),
Map.entry("CONTRACT_QUOTATION_REFERENCE_PLACEHOLDER", carrierScenarioParameters.contractQuotationReference())
));

ConformanceResponse conformanceResponse = syncCounterpartPost("/v3/shipping-instructions", jsonRequestBody);
Expand Down
Loading

0 comments on commit 3f16e23

Please sign in to comment.