Skip to content

Commit

Permalink
DT-617: Implement UC12 - cancel by shipper
Browse files Browse the repository at this point in the history
Signed-off-by: Niels Thykier <[email protected]>
  • Loading branch information
nt-gt committed Nov 23, 2023
1 parent be95196 commit 1715053
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class BookingScenarioListBuilder extends ScenarioListBuilder<BookingScena
private static final String POST_SCHEMA_NAME = "postBooking";
private static final String GET_BOOKING_SCHEMA_NAME = "getBooking";

private static final String CANCEL_SCHEMA_NAME = "bookings_bookingReference_body";

private static final String BOOKING_NOTIFICATION_SCHEMA_NAME = "BookingNotification";


Expand Down Expand Up @@ -52,6 +54,8 @@ public static BookingScenarioListBuilder buildTree(
.then(shipper_GetBooking(DECLINED)))),
uc4_carrier_rejectBookingRequest()
.then(shipper_GetBooking(REJECTED)),
uc12_shipper_cancelBooking()
.then(shipper_GetBooking(CANCELLED)),
uc2_carrier_requestUpdateToBookingRequest()
.then(shipper_GetBooking(PENDING_UPDATE))
// Need UC3 to continue after UC2
Expand Down Expand Up @@ -247,7 +251,17 @@ private static BookingScenarioListBuilder uc11_carrier_confirmBookingCompleted()
}

private static BookingScenarioListBuilder uc12_shipper_cancelBooking() {
return tbdShipperAction();
BookingComponentFactory componentFactory = threadLocalComponentFactory.get();
String carrierPartyName = threadLocalCarrierPartyName.get();
String shipperPartyName = threadLocalShipperPartyName.get();
return new BookingScenarioListBuilder(
previousAction ->
new UC12_Shipper_CancelEntireBookingAction(
carrierPartyName,
shipperPartyName,
(BookingAction) previousAction,
componentFactory.getMessageSchemaValidator(
new JsonSchema(BOOKING_API, CANCEL_SCHEMA_NAME))));
}

private static BookingScenarioListBuilder tbdCarrierAction() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.dcsa.conformance.standards.booking.action;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.booking.party.BookingRole;

import java.util.stream.Stream;

@Getter
@Slf4j
public class UC12_Shipper_CancelEntireBookingAction extends BookingAction {
private final JsonSchemaValidator requestSchemaValidator;

public UC12_Shipper_CancelEntireBookingAction(
String carrierPartyName,
String shipperPartyName,
BookingAction previousAction,
JsonSchemaValidator requestSchemaValidator) {
super(shipperPartyName, carrierPartyName, previousAction, "UC12", 200);
this.requestSchemaValidator = requestSchemaValidator;
}

@Override
public String getHumanReadablePrompt() {
return ("UC12: Cancel an entire booking");
}

@Override
public JsonNode getJsonForHumanReadablePrompt() {
return getCspSupplier().get().toJson();
}

@Override
public ObjectNode asJsonNode() {
ObjectNode jsonNode = super.asJsonNode();
jsonNode.put("cbrr", getDspSupplier().get().carrierBookingRequestReference());
return jsonNode;
}

@Override
public ConformanceCheck createCheck(String expectedApiVersion) {
return new ConformanceCheck(getActionTitle()) {
@Override
protected Stream<? extends ConformanceCheck> createSubChecks() {
var cbrr = getDspSupplier().get().carrierBookingRequestReference();
return Stream.of(
new UrlPathCheck(BookingRole::isShipper, getMatchedExchangeUuid(), "/v2/bookings/%s".formatted(cbrr)),
new ResponseStatusCheck(
BookingRole::isCarrier, getMatchedExchangeUuid(), expectedStatus),
new ApiHeaderCheck(
BookingRole::isShipper,
getMatchedExchangeUuid(),
HttpMessageType.REQUEST,
expectedApiVersion),
new ApiHeaderCheck(
BookingRole::isCarrier,
getMatchedExchangeUuid(),
HttpMessageType.RESPONSE,
expectedApiVersion),
new JsonSchemaCheck(
BookingRole::isShipper,
getMatchedExchangeUuid(),
HttpMessageType.REQUEST,
requestSchemaValidator))
// .filter(Objects::nonNull)
;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public ObjectNode asJsonNode() {
@Override
public void doHandleExchange(ConformanceExchange exchange) {
JsonNode responseJsonNode = exchange.getResponse().message().body().getJsonBody();
// FIXME: Guard against non-conformant parties
getDspConsumer()
.accept(
new DynamicScenarioParameters(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public ObjectNode asJsonNode() {
@Override
public void doHandleExchange(ConformanceExchange exchange) {
JsonNode responseJsonNode = exchange.getResponse().message().body().getJsonBody();
// FIXME: Guard against non-conformant parties
var cbr = responseJsonNode.get("carrierBookingReference").asText();
var dsp = getDspSupplier().get();
if (cbr != null && !cbr.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
import org.dcsa.conformance.core.traffic.ConformanceResponse;
import org.dcsa.conformance.standards.booking.action.*;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;

@Slf4j
Expand Down Expand Up @@ -372,6 +375,17 @@ private void requestUpdateToConfirmedBooking(JsonNode actionPrompt) {
addOperatorLogEntry("Requested update to the booking with CBR '%s'".formatted(cbr));
}

private void checkState(String reference, BookingState currentState, Predicate<BookingState> expectedState) {
if (!expectedState.test(currentState)) {
throw new IllegalStateException(
"Booking '%s' is in state '%s'".formatted(reference, currentState));
}
}

private void checkState(String reference, BookingState currentState, Set<BookingState> expectedState) {
checkState(reference, currentState, expectedState::contains);
}

private void processAndEmitNotificationForStateTransition(
JsonNode actionPrompt,
BookingState targetState,
Expand Down Expand Up @@ -399,10 +413,7 @@ private void processAndEmitNotificationForStateTransition(
String cbr = cbrrToCbr.get(cbrr);
BookingState currentState = bookingStatesByCbrr.get(cbrr);
boolean isCorrect = actionPrompt.path("isCorrect").asBoolean(true);
if (!expectedState.contains(currentState)) {
throw new IllegalStateException(
"Booking '%s' is in state '%s'".formatted(cbrr, currentState));
}
checkState(cbrr, currentState, expectedState);

if (isCorrect) {
var booking = (ObjectNode)persistentMap.load(cbrr);
Expand Down Expand Up @@ -481,16 +492,39 @@ private ConformanceResponse return405(ConformanceRequest request, String ... all
}


private ConformanceResponse return404(ConformanceRequest request) {
private ConformanceResponse return400(ConformanceRequest request, String message) {
return request.createResponse(
404,
400,
Map.of("Api-Version", List.of(apiVersion)),
new ConformanceMessageBody(
objectMapper
.createObjectNode()
.put(
"message",
"Returning 404 since the request did not match any known URL")));
"message", message)));
}

private ConformanceResponse return404(ConformanceRequest request) {
return request.createResponse(
404,
Map.of("Api-Version", List.of(apiVersion)),
new ConformanceMessageBody(
objectMapper
.createObjectNode()
.put(
"message",
"Returning 404 since the request did not match any known URL")));
}

private ConformanceResponse return409(ConformanceRequest request, String message) {
return request.createResponse(
409,
Map.of("Api-Version", List.of(apiVersion)),
new ConformanceMessageBody(
objectMapper
.createObjectNode()
.put(
"message",
message)));
}

@Override
Expand All @@ -505,7 +539,8 @@ public ConformanceResponse handleRequest(ConformanceRequest request) {
}
yield return404(request);
}
case "PUT", "PATCH" -> throw new UnsupportedOperationException();
case "PATCH" -> _handlePatchBookingRequest(request);
case "PUT" -> throw new UnsupportedOperationException();
default -> return405(request, "GET", "POST", "PUT", "PATCH");
};
addOperatorLogEntry(
Expand All @@ -519,6 +554,85 @@ private String lastUrlSegment(String url) {
return url.substring(1 + url.replaceAll("/++$", "").lastIndexOf("/"));
}

private String readCancelOperation(ConformanceRequest request) {
var queryParams = request.queryParams();
var operationParams = queryParams.get("operation");
if (operationParams == null || operationParams.isEmpty()) {
return null;
}
var operation = operationParams.iterator().next();
if (operationParams.size() > 1 || !(operation.equals("cancelBooking") || operation.equals("cancelAmendment"))) {
return "!INVALID-VALUE!";
}
return operation;
}

private ConformanceResponse _handlePatchBookingRequest(ConformanceRequest request) {
var cancelOperation = readCancelOperation(request);
if (cancelOperation == null) {
return return400(request, "Missing mandatory 'operation' query parameter");
}
if (!cancelOperation.equals("cancelBooking") && !cancelOperation.equals("cancelAmendment")) {
return return400(request,
"The 'operation' query parameter must be given exactly one and have" +
" value either 'cancelBooking' OR 'cancelAmendment'"
);
}
var bookingReference = lastUrlSegment(request.url());
// bookingReference can either be a CBR or CBRR.
var cbrr = cbrToCbrr.getOrDefault(bookingReference, bookingReference);
if (!bookingStatesByCbrr.containsKey(cbrr)) {
return return404(request);
}
ObjectNode booking;
try {
booking = setState(cbrr, BookingState.CANCELLED, s -> s != BookingState.CANCELLED);
} catch (IllegalStateException e) {
return return409(request, "Booking was not in the correct state");
}
return returnBookingStatusResponse(200, request, booking, bookingReference);
}

private ConformanceResponse returnBookingStatusResponse(int responseCode, ConformanceRequest request, ObjectNode booking, String bookingReference) {
var cbrr = booking.get("carrierBookingRequestReference").asText();
var bookingStatus = booking.get("bookingStatus").asText();
var statusObject = objectMapper.createObjectNode()
.put("bookingStatus", bookingStatus)
.put("carrierBookingRequestReference", cbrr);
var cbr = booking.get("carrierBookingReference");
var reason = booking.get("reason");
if (cbr != null) {
statusObject.set("carrierBookingReference", cbr);
}
if (cbr != null) {
statusObject.set("reason", reason);
}
ConformanceResponse response =
request.createResponse(
responseCode,
Map.of("Api-Version", List.of(apiVersion)),
new ConformanceMessageBody(statusObject));
addOperatorLogEntry(
"Responded %d to %s booking '%s' (resulting state '%s')"
.formatted(responseCode, request.method(), bookingReference, bookingStatus));
return response;
}

private ObjectNode setState(String carrierBookingRequestReference, BookingState newState, Predicate<BookingState> expectedState) {
var booking = persistentMap.load(carrierBookingRequestReference);
if (booking == null) {
throw new IllegalArgumentException("Unknown CBRR: " + carrierBookingRequestReference);
}
checkState(
carrierBookingRequestReference,
BookingState.fromWireName(booking.required("bookingStatus").asText()),
expectedState
);
((ObjectNode)booking).put("bookingStatus", newState.wireName());
bookingStatesByCbrr.put(carrierBookingRequestReference, newState);
return (ObjectNode) booking;
}

private ConformanceResponse _handleGetBookingRequest(ConformanceRequest request) {
var bookingReference = lastUrlSegment(request.url());
// bookingReference can either be a CBR or CBRR.
Expand All @@ -543,26 +657,13 @@ private ConformanceResponse _handlePostBookingRequest(ConformanceRequest request
String cbrr = UUID.randomUUID().toString();
BookingState bookingState = BookingState.RECEIVED;
bookingStatesByCbrr.put(cbrr, bookingState);
ConformanceResponse response =
request.createResponse(
201,
Map.of("Api-Version", List.of(apiVersion)),
new ConformanceMessageBody(
objectMapper
.createObjectNode()
.put("carrierBookingRequestReference", cbrr)
.put("bookingStatus", bookingState.wireName())));

ObjectNode booking =
(ObjectNode) objectMapper.readTree(request.message().body().getJsonBody().toString());
booking.put("carrierBookingRequestReference", cbrr);
booking.put("bookingStatus", bookingState.wireName());
persistentMap.save(cbrr, booking);

addOperatorLogEntry(
"Accepted booking request '%s' (now in state '%s')"
.formatted(cbrr, bookingState.wireName()));
return response;
return returnBookingStatusResponse(201, request, booking, cbrr);
}

@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.dcsa.conformance.core.traffic.ConformanceRequest;
import org.dcsa.conformance.core.traffic.ConformanceResponse;
import org.dcsa.conformance.standards.booking.action.Shipper_GetBookingAction;
import org.dcsa.conformance.standards.booking.action.UC12_Shipper_CancelEntireBookingAction;
import org.dcsa.conformance.standards.booking.action.UC1_Shipper_SubmitBookingRequestAction;

@Slf4j
Expand Down Expand Up @@ -55,7 +56,8 @@ protected void doReset() {
protected Map<Class<? extends ConformanceAction>, Consumer<JsonNode>> getActionPromptHandlers() {
return Map.ofEntries(
Map.entry(UC1_Shipper_SubmitBookingRequestAction.class, this::sendBookingRequest),
Map.entry(Shipper_GetBookingAction.class, this::getBookingRequest));
Map.entry(Shipper_GetBookingAction.class, this::getBookingRequest),
Map.entry(UC12_Shipper_CancelEntireBookingAction.class, this::sendCancelEntireBooking));
}

private void getBookingRequest(JsonNode actionPrompt) {
Expand Down Expand Up @@ -90,6 +92,21 @@ private void sendBookingRequest(JsonNode actionPrompt) {
.formatted(carrierScenarioParameters.toJson()));
}

private void sendCancelEntireBooking(JsonNode actionPrompt) {
log.info("Shipper.sendCancelEntireBooking(%s)".formatted(actionPrompt.toPrettyString()));
String cbrr = actionPrompt.get("cbrr").asText();

asyncCounterpartPatch(
"/v2/bookings/%s?operation=cancelBooking".formatted(cbrr),
new ObjectMapper().createObjectNode()
.put("bookingStatus", BookingState.CANCELLED.wireName())
);

addOperatorLogEntry(
"Sent a cancel booking request of '%s'"
.formatted(cbrr));
}

@Override
public ConformanceResponse handleRequest(ConformanceRequest request) {
log.info("Shipper.handleRequest(%s)".formatted(request));
Expand Down
Loading

0 comments on commit 1715053

Please sign in to comment.