Skip to content

Commit

Permalink
DT-636: UC5 - Shipper - Cancel Update to Shipping Instructions
Browse files Browse the repository at this point in the history
Signed-off-by: Niels Thykier <[email protected]>
  • Loading branch information
nt-gt committed Dec 11, 2023
1 parent be2a5f2 commit 9e5cff1
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class EblScenarioListBuilder extends ScenarioListBuilder<EblScenarioListB
private static final String GET_TD_SCHEMA_NAME = "getTransportDocument";
private static final String POST_EBL_SCHEMA_NAME = "ShippingInstructionsRequest";
private static final String PUT_EBL_SCHEMA_NAME = "ShippingInstructionsUpdate";
private static final String PATCH_SI_SCHEMA_NAME = "shippinginstructions_documentReference_body";
private static final String PATCH_TD_SCHEMA_NAME = "transportdocuments_transportDocumentReference_body";
private static final String EBL_REF_STATUS_SCHEMA_NAME = "ShippingInstructionsRefStatus";
private static final String TD_REF_STATUS_SCHEMA_NAME = "TransportDocumentRefStatus";
Expand All @@ -41,7 +42,12 @@ public static EblScenarioListBuilder buildTree(
}

private EblScenarioListBuilder thenAllPathsFrom(
ShippingInstructionsStatus shippingInstructionsStatus) {
ShippingInstructionsStatus shippingInstructionsStatus) {
return thenAllPathsFrom(shippingInstructionsStatus, null);
}

private EblScenarioListBuilder thenAllPathsFrom(
ShippingInstructionsStatus shippingInstructionsStatus, ShippingInstructionsStatus memoryState) {
return switch (shippingInstructionsStatus) {
case SI_START -> then(
uc1_shipper_submitShippingInstructions()
Expand All @@ -50,52 +56,70 @@ private EblScenarioListBuilder thenAllPathsFrom(
.thenAllPathsFrom(SI_RECEIVED)));
case SI_RECEIVED -> thenEither(
uc2_carrier_requestUpdateToShippingInstruction()
.then(shipper_GetShippingInstructions(SI_PENDING_UPDATE, TD_START)
.thenAllPathsFrom(SI_PENDING_UPDATE)),
.then(
shipper_GetShippingInstructions(SI_PENDING_UPDATE, TD_START)
.thenAllPathsFrom(SI_PENDING_UPDATE)),
uc3_shipper_submitUpdatedShippingInstructions()
.then(
shipper_GetShippingInstructions(SI_RECEIVED, SI_UPDATE_RECEIVED, TD_START)
.thenAllPathsFrom(SI_UPDATE_RECEIVED)),
.then(
shipper_GetShippingInstructions(SI_RECEIVED, SI_UPDATE_RECEIVED, TD_START)
.thenAllPathsFrom(SI_UPDATE_RECEIVED, SI_RECEIVED)),
uc6_carrier_publishDraftTransportDocument()
.then(
shipper_GetShippingInstructions(SI_RECEIVED, TD_DRAFT, true)
.then(shipper_GetTransportDocument(TD_DRAFT)
.thenAllPathsFrom(TD_DRAFT))));
case SI_UPDATE_RECEIVED -> thenEither(
uc2_carrier_requestUpdateToShippingInstruction()
.then(shipper_GetShippingInstructions(SI_PENDING_UPDATE, TD_START)
.thenHappyPathFrom(SI_PENDING_UPDATE)),
uc4a_carrier_acceptUpdatedShippingInstructions()
.then(shipper_GetShippingInstructions(SI_RECEIVED, TD_START)
.thenHappyPathFrom(SI_RECEIVED)),
uc4d_carrier_declineUpdatedShippingInstructions()
.then(shipper_GetShippingInstructions(SI_RECEIVED, SI_DECLINED, TD_START)
.thenHappyPathFrom(SI_DECLINED)));
case SI_DECLINED -> thenEither(
uc6_carrier_publishDraftTransportDocument()
.then(shipper_GetShippingInstructions(SI_RECEIVED, TD_DRAFT, true)
.then(shipper_GetTransportDocument(TD_DRAFT)
.thenHappyPathFrom(TD_DRAFT))),
uc2_carrier_requestUpdateToShippingInstruction()
.then(shipper_GetShippingInstructions(SI_PENDING_UPDATE, TD_START)
.thenHappyPathFrom(SI_PENDING_UPDATE)),
uc3_shipper_submitUpdatedShippingInstructions()
.then(shipper_GetShippingInstructions(SI_UPDATE_RECEIVED, TD_START)
.thenAllPathsFrom(SI_UPDATE_RECEIVED)));
case SI_PENDING_UPDATE -> then(uc3_shipper_submitUpdatedShippingInstructions()
.then(
shipper_GetShippingInstructions(SI_RECEIVED, SI_UPDATE_RECEIVED, TD_START)
.thenHappyPathFrom(SI_UPDATE_RECEIVED)));
.then(shipper_GetTransportDocument(TD_DRAFT).thenAllPathsFrom(TD_DRAFT))));
case SI_UPDATE_RECEIVED -> {
if (memoryState == null) {
throw new IllegalArgumentException(
shippingInstructionsStatus.name() + " requires a memory state");
}
yield thenEither(
uc2_carrier_requestUpdateToShippingInstruction()
.then(
shipper_GetShippingInstructions(SI_PENDING_UPDATE, TD_START)
.thenHappyPathFrom(SI_PENDING_UPDATE)),
uc4a_carrier_acceptUpdatedShippingInstructions()
.then(
shipper_GetShippingInstructions(SI_RECEIVED, TD_START)
.thenHappyPathFrom(SI_RECEIVED)),
uc4d_carrier_declineUpdatedShippingInstructions()
.then(
shipper_GetShippingInstructions(memoryState, SI_DECLINED, TD_START)
.thenHappyPathFrom(memoryState)),
uc5_shipper_cancelUpdateToShippingInstructions()
.then(
shipper_GetShippingInstructions(memoryState, SI_CANCELLED, TD_START)
.thenHappyPathFrom(memoryState)));
}
case SI_PENDING_UPDATE -> then(
uc3_shipper_submitUpdatedShippingInstructions()
.then(
shipper_GetShippingInstructions(SI_PENDING_UPDATE, SI_UPDATE_RECEIVED, TD_START)
.thenEither(
noAction().thenHappyPathFrom(SI_PENDING_UPDATE),
// Special-case: UC2 -> UC3 -> UC5 -> ...
// - Doing thenAllPathsFrom(...) from UC2 would cause UC3 -> UC2 -> UC3 -> UC2 -> UC3 -> ...
// patterns (it eventually resolves, but it is unhelpful many cases)
// To ensure that UC2 -> UC3 -> UC5 -> ... works properly we manually the subtree here.
// Otherwise, we would never test the UC2 -> UC3 -> UC5 -> ... flow because neither UC2 and UC5
// are considered happy paths.
uc5_shipper_cancelUpdateToShippingInstructions()
.then(
shipper_GetShippingInstructions(SI_PENDING_UPDATE, SI_CANCELLED, TD_START)
.thenEither(
noAction().thenHappyPathFrom(SI_PENDING_UPDATE),
uc3_shipper_submitUpdatedShippingInstructions().then(
shipper_GetShippingInstructions(SI_PENDING_UPDATE, SI_UPDATE_RECEIVED, TD_START)
.thenHappyPathFrom(SI_UPDATE_RECEIVED)))))));
case SI_CANCELLED, SI_DECLINED -> throw new AssertionError("Please use the black state rather than " + shippingInstructionsStatus.name());
case SI_ANY -> throw new AssertionError("Not a real/reachable state");
case SI_COMPLETED -> then(noAction());
default -> then(noAction()); // TODO
};
}

private EblScenarioListBuilder thenHappyPathFrom(
ShippingInstructionsStatus shippingInstructionsStatus) {
return switch (shippingInstructionsStatus) {
case SI_RECEIVED, SI_DECLINED -> then(uc6_carrier_publishDraftTransportDocument()
case SI_RECEIVED -> then(uc6_carrier_publishDraftTransportDocument()
.then(
shipper_GetShippingInstructions(SI_RECEIVED, TD_DRAFT, true)
.then(shipper_GetTransportDocument(TD_DRAFT)
Expand All @@ -109,8 +133,8 @@ private EblScenarioListBuilder thenHappyPathFrom(
.thenHappyPathFrom(SI_RECEIVED))
);
case SI_COMPLETED -> then(noAction());
case SI_CANCELLED, SI_DECLINED -> throw new AssertionError("Please use the black state rather than DECLINED");
case SI_START, SI_ANY -> throw new AssertionError("Not a real/reachable state");
default -> then(noAction()); // TODO
};
}

Expand Down Expand Up @@ -165,7 +189,6 @@ private EblScenarioListBuilder thenAllPathsFrom(TransportDocumentStatus transpor
case TD_SURRENDERED_FOR_DELIVERY -> thenHappyPathFrom(transportDocumentStatus);
case TD_START, TD_ANY -> throw new AssertionError("Not a real/reachable state");
case TD_VOIDED -> then(noAction());
default -> throw new AssertionError("Not implemented: " + transportDocumentStatus.name());
};
}

Expand Down Expand Up @@ -358,6 +381,24 @@ private static EblScenarioListBuilder uc4d_carrier_declineUpdatedShippingInstruc
false));
}

private static EblScenarioListBuilder uc5_shipper_cancelUpdateToShippingInstructions() {
EblComponentFactory componentFactory = threadLocalComponentFactory.get();
String carrierPartyName = threadLocalCarrierPartyName.get();
String shipperPartyName = threadLocalShipperPartyName.get();
return new EblScenarioListBuilder(
previousAction ->
new UC5_Shipper_CancelUpdateToShippingInstructionsAction(
carrierPartyName,
shipperPartyName,
(EblAction) previousAction,
componentFactory.getMessageSchemaValidator(
EBL_API, PATCH_SI_SCHEMA_NAME),
componentFactory.getMessageSchemaValidator(
EBL_API, EBL_REF_STATUS_SCHEMA_NAME),
componentFactory.getMessageSchemaValidator(
EBL_NOTIFICATIONS_API, EBL_SI_NOTIFICATION_SCHEMA_NAME)));
}

private static EblScenarioListBuilder uc6_carrier_publishDraftTransportDocument() {
EblComponentFactory componentFactory = threadLocalComponentFactory.get();
String carrierPartyName = threadLocalCarrierPartyName.get();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.dcsa.conformance.standards.ebl.action;

import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Objects;
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;
import org.dcsa.conformance.standards.ebl.party.ShippingInstructionsStatus;

@Getter
@Slf4j
public class UC5_Shipper_CancelUpdateToShippingInstructionsAction extends StateChangingSIAction {
private final JsonSchemaValidator requestSchemaValidator;
private final JsonSchemaValidator responseSchemaValidator;
private final JsonSchemaValidator notificationSchemaValidator;

public UC5_Shipper_CancelUpdateToShippingInstructionsAction(
String carrierPartyName,
String shipperPartyName,
EblAction previousAction,
JsonSchemaValidator requestSchemaValidator,
JsonSchemaValidator responseSchemaValidator,
JsonSchemaValidator notificationSchemaValidator) {
super(shipperPartyName, carrierPartyName, previousAction, "UC5", 200);
this.requestSchemaValidator = requestSchemaValidator;
this.responseSchemaValidator = responseSchemaValidator;
this.notificationSchemaValidator = notificationSchemaValidator;
}

@Override
public String getHumanReadablePrompt() {
return ("UC5: Cancel update to shipping instructions the document reference %s".formatted(
getDspSupplier().get().shippingInstructionsReference()
));
}

@Override
public ObjectNode asJsonNode() {
return super.asJsonNode()
.put("documentReference", getDspSupplier().get().shippingInstructionsReference());
}

@Override
protected boolean expectsNotificationExchange() {
return true;
}

@Override
public ConformanceCheck createCheck(String expectedApiVersion) {
return new ConformanceCheck(getActionTitle()) {
@Override
protected Stream<? extends ConformanceCheck> createSubChecks() {
var dsp = getDspSupplier().get();
var sir = dsp.shippingInstructionsReference() != null ? dsp.shippingInstructionsReference() : "<DSP MISSING SI REFERENCE>";
Stream<ActionCheck> primaryExchangeChecks =
Stream.of(
new HttpMethodCheck(EblRole::isShipper, getMatchedExchangeUuid(), "PATCH"),
new UrlPathCheck(EblRole::isShipper, getMatchedExchangeUuid(), "/v3/shipping-instructions/%s".formatted(sir)),
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,
getSINotificationChecks(
expectedApiVersion,
notificationSchemaValidator));
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@

public class CarrierShippingInstructions {

private static final Set<ShippingInstructionsStatus> PENDING_UPDATE_PREREQUISITE_STATES = Set.of(
SI_RECEIVED,
SI_UPDATE_RECEIVED,
SI_DECLINED
);

private static final String SI_STATUS = "shippingInstructionsStatus";
private static final String UPDATED_SI_STATUS = "updatedShippingInstructionsStatus";

Expand Down Expand Up @@ -143,17 +137,14 @@ private void setReason(String reason) {
}
}

public void cancelShippingInstructionsUpdate(String shippingInstructionsReference, String reason) {
public void cancelShippingInstructionsUpdate(String shippingInstructionsReference) {
checkState(shippingInstructionsReference, getShippingInstructionsState(), s -> s == SI_UPDATE_RECEIVED);
changeSIState(UPDATED_SI_STATUS, SI_CANCELLED);
if (reason == null || reason.isBlank()) {
reason = "Update cancelled by shipper (no reason given)";
}
setReason(reason);
setReason(null);
}

public void requestChangesToShippingInstructions(String documentReference, Consumer<ArrayNode> requestedChangesGenerator) {
checkState(documentReference, getShippingInstructionsState(), PENDING_UPDATE_PREREQUISITE_STATES::contains);
checkState(documentReference, getShippingInstructionsState(), s -> s != SI_PENDING_UPDATE && s != SI_COMPLETED );
clearUpdatedShippingInstructions();
changeSIState(SI_STATUS, SI_PENDING_UPDATE);
setReason(null);
Expand Down Expand Up @@ -216,9 +207,11 @@ public void rejectSurrenderForDelivery(String documentReference) {
}

public void publishDraftTransportDocument(String documentReference) {
// SI_DECLINED only applies to the updated SI. UC1 -> UC3 -> UC4 (decline) -> UC6 is applicable and
// in this case, the SI would be in RECEIVED with updated state SI_DECLINED.
checkState(documentReference, getShippingInstructionsState(), s -> s == SI_RECEIVED || s == SI_DECLINED);
// We allow draft when:
// 1) The original ("black") state is RECEIVED, *and*
// 2) There is no update received (that is "grey" is not UPDATE_RECEIVED)
checkState(documentReference, getOriginalShippingInstructionState(), s -> s == SI_RECEIVED);
checkState(documentReference, getOriginalShippingInstructionState(), s -> s != SI_UPDATE_RECEIVED);
this.generateTDFromSI();
var tdData = getTransportDocument().orElseThrow();
var tdr = tdData.required(TRANSPORT_DOCUMENT_REFERENCE).asText();
Expand Down Expand Up @@ -383,7 +376,7 @@ public JsonNode asPersistentState() {
public static CarrierShippingInstructions fromPersistentStore(JsonNodeMap jsonNodeMap, String shippingInstructionsReference) {
var data = jsonNodeMap.load(shippingInstructionsReference);
if (data == null) {
throw new IllegalArgumentException("Unknown CBRR: " + shippingInstructionsReference);
throw new IllegalArgumentException("Unknown SI Reference: " + shippingInstructionsReference);
}
return fromPersistentStore(data);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,39 @@ private ConformanceResponse _handleGetTransportDocument(ConformanceRequest reque
return response;
}


private ConformanceResponse _handlePatchShippingInstructions(ConformanceRequest request, String documentReference) {
var sir = tdrToSir.getOrDefault(documentReference, documentReference);
var persistedSi = persistentMap.load(sir);
if (persistedSi == null) {
return return404(request);
}
var si = CarrierShippingInstructions.fromPersistentStore(persistedSi);
si.cancelShippingInstructionsUpdate(documentReference);
si.save(persistentMap);
var siData = si.getShippingInstructions();
if (isShipperNotificationEnabled) {
executor.schedule(
() ->
asyncCounterpartPost(
"/v3/shipping-instructions-notifications",
ShippingInstructionsNotification.builder()
.apiVersion(apiVersion)
.shippingInstructions(siData)
.build()
.asJsonNode()),
1,
TimeUnit.SECONDS);
}
return returnShippingInstructionsRefStatusResponse(
200,
request,
siData,
documentReference
);
}

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);
Expand Down Expand Up @@ -566,6 +597,9 @@ public ConformanceResponse handleRequest(ConformanceRequest request) {
var url = request.url().replaceAll("/++$", "");
var lastSegment = lastUrlSegment(url);
var urlStem = url.substring(0, url.length() - lastSegment.length()).replaceAll("/++$", "");
if (urlStem.endsWith("/v3/shipping-instructions")) {
yield _handlePatchShippingInstructions(request, lastSegment);
}
if (urlStem.endsWith("/v3/transport-documents")) {
yield _handlePatchTransportDocument(request, lastSegment);
}
Expand Down
Loading

0 comments on commit 9e5cff1

Please sign in to comment.