diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/EblScenarioListBuilder.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/EblScenarioListBuilder.java index e42d9520..5ace6c75 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/EblScenarioListBuilder.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/EblScenarioListBuilder.java @@ -26,7 +26,9 @@ public class EblScenarioListBuilder extends ScenarioListBuilder then( + case TD_DRAFT -> thenEither( uc8_carrier_issueTransportDocument() .then( shipper_GetTransportDocument(TD_ISSUED) - .thenAllPathsFrom(TD_ISSUED))); + // Using happy path here as requested in + // https://github.com/dcsaorg/Conformance-Gateway/pull/29#discussion_r1421732797 + .thenHappyPathFrom(TD_ISSUED)), + uc7_shipper_approveDraftTransportDocument() + .then(shipper_GetTransportDocument(TD_APPROVED) + .thenAllPathsFrom(TD_APPROVED)) + ); + case TD_APPROVED -> then( + uc8_carrier_issueTransportDocument() + .then( + shipper_GetTransportDocument(TD_ISSUED) + .thenAllPathsFrom(TD_ISSUED)) + ); case TD_ISSUED -> thenEither( uc9_carrier_awaitSurrenderRequestForAmendment() .then(shipper_GetTransportDocument(TD_PENDING_SURRENDER_FOR_AMENDMENT) @@ -157,11 +171,20 @@ private EblScenarioListBuilder thenAllPathsFrom(TransportDocumentStatus transpor private EblScenarioListBuilder thenHappyPathFrom(TransportDocumentStatus transportDocumentStatus) { return switch (transportDocumentStatus) { - case TD_DRAFT -> then( + case TD_DRAFT -> thenEither( uc8_carrier_issueTransportDocument() .then( shipper_GetTransportDocument(TD_ISSUED) - .thenHappyPathFrom(TD_ISSUED))); + .thenHappyPathFrom(TD_ISSUED)), + uc7_shipper_approveDraftTransportDocument() + .then(shipper_GetTransportDocument(TD_APPROVED) + .thenHappyPathFrom(TD_APPROVED))); + case TD_APPROVED -> then( + uc8_carrier_issueTransportDocument() + .then( + shipper_GetTransportDocument(TD_ISSUED) + .thenHappyPathFrom(TD_ISSUED)) + ); case TD_ISSUED -> then( uc12_carrier_awaitSurrenderRequestForDelivery() .then(shipper_GetTransportDocument(TD_PENDING_SURRENDER_FOR_DELIVERY) @@ -349,6 +372,24 @@ private static EblScenarioListBuilder uc6_carrier_publishDraftTransportDocument( EBL_NOTIFICATIONS_API, EBL_TD_NOTIFICATION_SCHEMA_NAME))); } + private static EblScenarioListBuilder uc7_shipper_approveDraftTransportDocument() { + EblComponentFactory componentFactory = threadLocalComponentFactory.get(); + String carrierPartyName = threadLocalCarrierPartyName.get(); + String shipperPartyName = threadLocalShipperPartyName.get(); + return new EblScenarioListBuilder( + previousAction -> + new UC7_Shipper_ApproveDraftTransportDocumentAction( + carrierPartyName, + shipperPartyName, + (EblAction) previousAction, + componentFactory.getMessageSchemaValidator( + EBL_API, PATCH_TD_SCHEMA_NAME), + componentFactory.getMessageSchemaValidator( + EBL_API, TD_REF_STATUS_SCHEMA_NAME), + componentFactory.getMessageSchemaValidator( + EBL_NOTIFICATIONS_API, EBL_TD_NOTIFICATION_SCHEMA_NAME))); + } + private static EblScenarioListBuilder uc8_carrier_issueTransportDocument() { EblComponentFactory componentFactory = threadLocalComponentFactory.get(); String carrierPartyName = threadLocalCarrierPartyName.get(); diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/UC7_Shipper_ApproveDraftTransportDocumentAction.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/UC7_Shipper_ApproveDraftTransportDocumentAction.java new file mode 100644 index 00000000..f402226e --- /dev/null +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/UC7_Shipper_ApproveDraftTransportDocumentAction.java @@ -0,0 +1,91 @@ +package org.dcsa.conformance.standards.ebl.action; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.core.check.*; +import org.dcsa.conformance.core.traffic.HttpMessageType; +import org.dcsa.conformance.standards.ebl.party.EblRole; + +@Getter +@Slf4j +public class UC7_Shipper_ApproveDraftTransportDocumentAction extends StateChangingSIAction { + private final JsonSchemaValidator requestSchemaValidator; + private final JsonSchemaValidator responseSchemaValidator; + private final JsonSchemaValidator notificationSchemaValidator; + + public UC7_Shipper_ApproveDraftTransportDocumentAction( + String carrierPartyName, + String shipperPartyName, + EblAction previousAction, + JsonSchemaValidator requestSchemaValidator, + JsonSchemaValidator responseSchemaValidator, + JsonSchemaValidator notificationSchemaValidator) { + super(shipperPartyName, carrierPartyName, previousAction, "UC7", 200); + this.requestSchemaValidator = requestSchemaValidator; + this.responseSchemaValidator = responseSchemaValidator; + this.notificationSchemaValidator = notificationSchemaValidator; + } + + @Override + public String getHumanReadablePrompt() { + return ("UC7: Approve the draft transport document with document reference %s".formatted( + getDspSupplier().get().transportDocumentReference())); + } + + @Override + public ObjectNode asJsonNode() { + return super.asJsonNode() + .put("documentReference", getDspSupplier().get().transportDocumentReference()); + } + + @Override + protected boolean expectsNotificationExchange() { + return true; + } + + @Override + public ConformanceCheck createCheck(String expectedApiVersion) { + return new ConformanceCheck(getActionTitle()) { + @Override + protected Stream createSubChecks() { + var dsp = getDspSupplier().get(); + var tdr = dsp.transportDocumentReference() != null ? dsp.transportDocumentReference() : ""; + Stream primaryExchangeChecks = + Stream.of( + new HttpMethodCheck(EblRole::isShipper, getMatchedExchangeUuid(), "PATCH"), + new UrlPathCheck(EblRole::isShipper, getMatchedExchangeUuid(), "/v3/transport-documents/%s".formatted(tdr)), + new ResponseStatusCheck( + EblRole::isCarrier, getMatchedExchangeUuid(), expectedStatus), + new ApiHeaderCheck( + EblRole::isShipper, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + expectedApiVersion), + new ApiHeaderCheck( + EblRole::isCarrier, + getMatchedExchangeUuid(), + HttpMessageType.RESPONSE, + expectedApiVersion), + // TODO: Add Carrier Ref Status Payload response check + // TODO: Add Shipper content conformance check + new JsonSchemaCheck( + EblRole::isShipper, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + requestSchemaValidator), + new JsonSchemaCheck( + EblRole::isCarrier, + getMatchedExchangeUuid(), + HttpMessageType.RESPONSE, + responseSchemaValidator)); + return Stream.concat( + primaryExchangeChecks, + getTDNotificationChecks( + expectedApiVersion, + notificationSchemaValidator)); + } + }; + } +} diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/models/CarrierShippingInstructions.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/models/CarrierShippingInstructions.java index d3b6b167..79777338 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/models/CarrierShippingInstructions.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/models/CarrierShippingInstructions.java @@ -185,6 +185,11 @@ public void confirmShippingInstructionsComplete(String documentReference) { changeSIState(SI_STATUS, SI_COMPLETED); } + public void approveDraftTransportDocument(String documentReference) { + checkState(documentReference, getTransportDocumentState(), s -> s == TD_DRAFT); + var td = getTransportDocument().orElseThrow(); + td.put(TRANSPORT_DOCUMENT_STATUS, TD_APPROVED.wireName()); + } public void acceptSurrenderForAmendment(String documentReference) { checkState(documentReference, getTransportDocumentState(), s -> s == TD_PENDING_SURRENDER_FOR_AMENDMENT); diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblCarrier.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblCarrier.java index cd7f283b..bbb0ec28 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblCarrier.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblCarrier.java @@ -375,11 +375,11 @@ private ConformanceResponse _handleGetTransportDocument(ConformanceRequest reque if (sir == null) { return return404(request); } - var persistedBookingData = persistentMap.load(sir); - if (persistedBookingData == null) { + var persistedSi = persistentMap.load(sir); + if (persistedSi == null) { throw new IllegalStateException("We had a TDR -> SIR mapping, but there is no data related to that reference"); } - var si = CarrierShippingInstructions.fromPersistentStore(persistedBookingData); + var si = CarrierShippingInstructions.fromPersistentStore(persistedSi); // If the TDR is resolvable, then the document must have a TD. var body = si.getTransportDocument().orElseThrow(); ConformanceResponse response = @@ -393,6 +393,41 @@ private ConformanceResponse _handleGetTransportDocument(ConformanceRequest reque return response; } + private ConformanceResponse _handlePatchTransportDocument(ConformanceRequest request, String documentReference) { + // bookingReference can either be a CBR or CBRR. + var sir = tdrToSir.get(documentReference); + if (sir == null) { + return return404(request); + } + var persistedSi = persistentMap.load(sir); + if (persistedSi == null) { + throw new IllegalStateException("We had a TDR -> SIR mapping, but there is no data related to that reference"); + } + var si = CarrierShippingInstructions.fromPersistentStore(persistedSi); + si.approveDraftTransportDocument(documentReference); + si.save(persistentMap); + var td = si.getTransportDocument().orElseThrow(); + if (isShipperNotificationEnabled) { + executor.schedule( + () -> + asyncCounterpartPost( + "/v3/transport-document-notifications", + TransportDocumentNotification.builder() + .apiVersion(apiVersion) + .transportDocument(td) + .build() + .asJsonNode()), + 1, + TimeUnit.SECONDS); + } + return returnTransportDocumentRefStatusResponse( + 200, + request, + td, + documentReference + ); + } + private ConformanceResponse returnShippingInstructionsRefStatusResponse( int responseCode, ConformanceRequest request, ObjectNode shippingInstructions, String documentReference) { var sir = shippingInstructions.required("shippingInstructionsReference").asText(); @@ -425,6 +460,27 @@ private ConformanceResponse returnShippingInstructionsRefStatusResponse( return response; } + + private ConformanceResponse returnTransportDocumentRefStatusResponse( + int responseCode, ConformanceRequest request, ObjectNode transportDocument, String documentReference) { + var tdr = transportDocument.required("transportDocumentReference").asText(); + var tdStatus = transportDocument.required("transportDocumentStatus").asText(); + var statusObject = + OBJECT_MAPPER + .createObjectNode() + .put("transportDocumentStatus", tdStatus) + .put("transportDocumentReference", tdr); + ConformanceResponse response = + request.createResponse( + responseCode, + Map.of("Api-Version", List.of(apiVersion)), + new ConformanceMessageBody(statusObject)); + addOperatorLogEntry( + "Responded %d to %s TD '%s' (resulting state '%s')" + .formatted(responseCode, request.method(), documentReference, tdStatus)); + return response; + } + @SneakyThrows private ConformanceResponse _handlePostShippingInstructions(ConformanceRequest request) { ObjectNode siPayload = @@ -506,7 +562,15 @@ public ConformanceResponse handleRequest(ConformanceRequest request) { } yield return404(request); } - // case "PATCH" -> _handlePatchRequest(request); + case "PATCH" -> { + var url = request.url().replaceAll("/++$", ""); + var lastSegment = lastUrlSegment(url); + var urlStem = url.substring(0, url.length() - lastSegment.length()).replaceAll("/++$", ""); + if (urlStem.endsWith("/v3/transport-documents")) { + yield _handlePatchTransportDocument(request, lastSegment); + } + yield return404(request); + } case "PUT" -> _handlePutShippingInstructions(request); default -> return405(request, "GET", "POST", "PUT", "PATCH"); }; diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblShipper.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblShipper.java index 1ff847b5..8697f57c 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblShipper.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/party/EblShipper.java @@ -16,10 +16,7 @@ import org.dcsa.conformance.core.traffic.ConformanceMessageBody; import org.dcsa.conformance.core.traffic.ConformanceRequest; import org.dcsa.conformance.core.traffic.ConformanceResponse; -import org.dcsa.conformance.standards.ebl.action.Shipper_GetShippingInstructionsAction; -import org.dcsa.conformance.standards.ebl.action.Shipper_GetTransportDocumentAction; -import org.dcsa.conformance.standards.ebl.action.UC1_Shipper_SubmitShippingInstructionsAction; -import org.dcsa.conformance.standards.ebl.action.UC3_Shipper_SubmitUpdatedShippingInstructionsAction; +import org.dcsa.conformance.standards.ebl.action.*; @Slf4j public class EblShipper extends ConformanceParty { @@ -61,7 +58,8 @@ protected Map, Consumer> getActionP Map.entry(UC1_Shipper_SubmitShippingInstructionsAction.class, this::sendShippingInstructionsRequest), Map.entry(Shipper_GetShippingInstructionsAction.class, this::getShippingInstructionsRequest), Map.entry(Shipper_GetTransportDocumentAction.class, this::getTransportDocument), - Map.entry(UC3_Shipper_SubmitUpdatedShippingInstructionsAction.class, this::sendUpdatedShippingInstructionsRequest) + Map.entry(UC3_Shipper_SubmitUpdatedShippingInstructionsAction.class, this::sendUpdatedShippingInstructionsRequest), + Map.entry(UC7_Shipper_ApproveDraftTransportDocumentAction.class, this::approveDraftTransportDocument) ); } @@ -131,6 +129,23 @@ private void sendUpdatedShippingInstructionsRequest(JsonNode actionPrompt) { .formatted(actionPrompt.toPrettyString())); } + private void approveDraftTransportDocument(JsonNode actionPrompt) { + log.info("Shipper.approveDraftTransportDocument(%s)".formatted(actionPrompt.toPrettyString())); + + var sir = actionPrompt.required("documentReference").asText(); + var approvePayload = new ObjectMapper().createObjectNode() + .put("transportDocumentStatus", TransportDocumentStatus.TD_APPROVED.wireName()); + + asyncCounterpartPatch( + "/v3/transport-documents/%s".formatted(sir), + approvePayload); + + addOperatorLogEntry( + "Approved transport document the parameters: %s" + .formatted(actionPrompt.toPrettyString())); + } + + private void getShippingInstructionsRequest(JsonNode actionPrompt) { log.info("Shipper.getShippingInstructionsRequest(%s)".formatted(actionPrompt.toPrettyString())); String sir = actionPrompt.get("sir").asText();