Skip to content

Commit

Permalink
Add extra validation for the booking returned on GET
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 16, 2023
1 parent 220f40f commit b2bb295
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import org.dcsa.conformance.core.toolkit.JsonToolkit;
import org.dcsa.conformance.core.traffic.ConformanceExchange;
import org.dcsa.conformance.core.traffic.HttpMessageType;
import org.dcsa.conformance.standards.booking.checks.CarrierGetBookingPayloadResponseConformanceCheck;
import org.dcsa.conformance.standards.booking.party.BookingRole;
import org.dcsa.conformance.standards.booking.party.BookingState;

public class Shipper_GetBookingAction extends BookingAction {

private final BookingState expectedState;
private final JsonSchemaValidator responseSchemaValidator;

Expand Down Expand Up @@ -67,6 +69,10 @@ protected Stream<? extends ConformanceCheck> createSubChecks() {
getMatchedExchangeUuid(),
HttpMessageType.REQUEST,
responseSchemaValidator),
new CarrierGetBookingPayloadResponseConformanceCheck(
getMatchedExchangeUuid(),
expectedState
),
new ActionCheck(
"GET returns the expected Booking data",
BookingRole::isCarrier,
Expand All @@ -77,20 +83,7 @@ protected Set<String> checkConformance(
Function<UUID, ConformanceExchange> getExchangeByUuid) {
ConformanceExchange getExchange = getExchangeByUuid.apply(getMatchedExchangeUuid());
if (getExchange == null) return Set.of();
String exchangeState =
getExchange
.getResponse()
.message()
.body()
.getJsonBody()
.get("bookingStatus")
.asText();
Set<String> conformanceErrors = new HashSet<>();
if (!Objects.equals(exchangeState, expectedState.wireName())) {
conformanceErrors.add(
"Expected bookingStatus '%s' but found '%s'"
.formatted(expectedState.wireName(), exchangeState));
}
if (previousAction
instanceof UC1_Shipper_SubmitBookingRequestAction submitBookingRequestAction) {
ConformanceExchange submitBookingRequestExchange =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package org.dcsa.conformance.standards.booking.checks;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import org.dcsa.conformance.core.check.ActionCheck;
import org.dcsa.conformance.core.check.ConformanceCheck;
import org.dcsa.conformance.core.traffic.ConformanceExchange;
import org.dcsa.conformance.core.traffic.HttpMessageType;
import org.dcsa.conformance.standards.booking.party.BookingRole;
import org.dcsa.conformance.standards.booking.party.BookingState;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

public class CarrierGetBookingPayloadResponseConformanceCheck extends ActionCheck {


private static final Set<BookingState> PENDING_CHANGES_STATES = Set.of(
BookingState.PENDING_UPDATE
);

private static final Set<BookingState> CONFIRMED_BOOKING_STATES = Set.of(
BookingState.CONFIRMED,
BookingState.COMPLETED
);

private static final Set<String> MANDATORY_ON_CONFIRMED_BOOKING = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
"confirmedEquipments",
"transportPlan",
"shipmentCutOffTimes",
"charges",
"carrierClauses",
"termsAndConditions"
)));

private static final Function<ObjectNode, Set<String>> ALL_OK = unused -> Collections.emptySet();

private final BookingState expectedState;

public CarrierGetBookingPayloadResponseConformanceCheck(UUID matchedExchangeUuid, BookingState bookingState) {
super(
"Validate the carrier response",
BookingRole::isCarrier,
matchedExchangeUuid,
HttpMessageType.RESPONSE
);
this.expectedState = bookingState;
}

@Override
protected Stream<? extends ConformanceCheck> createSubChecks() {
List<ConformanceCheck> checks = new ArrayList<>();
/*
* Checks for fields that the *Carrier* is responsible for. That is things like "requestedChanges"
* or the "transportPlan". If the Shipper should conditionally provide a field, then that is
* validation should go somewhere else.
*
* As an example, the Carrier does *not* get flagged for not providing "either serviceContractReference
* OR contractQuotationReference" if the Shipper failed to provide those in the last POST/PUT call
*/
addSubCheck("Validate bookingState", this::ensureBookingStateIsCorrect, checks::add);
addSubCheck(
"Validate requestedChanges is only present on states where it is allowed",
this::checkPendingUpdates,
checks::add
);
for (var fieldName : MANDATORY_ON_CONFIRMED_BOOKING) {
addSubCheck(fieldName + " is required for confirmed bookings",
requiredIfState(CONFIRMED_BOOKING_STATES, fieldName),
checks::add
);
}
return checks.stream();
}

private void addSubCheck(String subtitle, Function<ObjectNode, Set<String>> subCheck, Consumer<ConformanceCheck> addCheck) {
var check = new ActionCheck(subtitle, this::isRelevantForRole, this.matchedExchangeUuid, this.httpMessageType) {
@Override
protected Set<String> checkConformance(Function<UUID, ConformanceExchange> getExchangeByUuid) {
ConformanceExchange exchange = getExchangeByUuid.apply(matchedExchangeUuid);
if (exchange == null) return Collections.emptySet();
var responsePayload =
exchange
.getResponse()
.message()
.body()
.getJsonBody();
if (responsePayload instanceof ObjectNode booking) {
return subCheck.apply(booking);
}
return Set.of("Could not perform the check as the payload was not correct format");
}
};
addCheck.accept(check);
}

@Override
protected Set<String> checkConformance(Function<UUID, ConformanceExchange> getExchangeByUuid) {
// All checks are delegated to sub-checks; nothing to do in here.
return Collections.emptySet();
}

private Set<String> ensureBookingStateIsCorrect(JsonNode responsePayload) {
String actualState = null;
if (responsePayload.get("bookingStatus") instanceof TextNode statusNode) {
actualState = statusNode.asText();
if (actualState.equals(this.expectedState.wireName())) {
return Collections.emptySet();
}
}
return Set.of("Expected bookingStatus '%s' but found '%s'"
.formatted(expectedState.wireName(), actualState));
}

private Set<String> checkPendingUpdates(JsonNode responsePayload) {
var requestedChangesKey = "requestedChanges";
if (PENDING_CHANGES_STATES.contains(this.expectedState)) {
return nonEmptyField(responsePayload, requestedChangesKey);
}
return fieldIsOmitted(responsePayload, requestedChangesKey);
}

private Function<ObjectNode, Set<String>> requiredIfState(Set<BookingState> conditionalInTheseStates, String fieldName) {
if (conditionalInTheseStates.contains(this.expectedState)) {
return booking -> nonEmptyField(booking, fieldName);
}
return ALL_OK;
}

private Set<String> fieldIsOmitted(JsonNode responsePayload, String key) {
if (responsePayload.has(key) || responsePayload.get(key) != null) {
return Set.of("The field '%s' must *NOT* be a present for a booking in status '%s'".formatted(
key, this.expectedState.wireName()
));
}
return Collections.emptySet();
}

private Set<String> nonEmptyField(JsonNode responsePayload, String key) {
var field = responsePayload.get(key);
if (field != null) {
if (field.isTextual() && !field.asText().isBlank()) {
return Collections.emptySet();
}
if (!field.isEmpty() || field.isValueNode()) {
return Collections.emptySet();
}
}
return Set.of("The field '%s' must be a present and non-empty for a booking in status '%s'".formatted(
key, this.expectedState.wireName()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,14 @@ private void confirmBookingRequest(JsonNode actionPrompt) {
BookingState.CONFIRMED,
Set.of(BookingState.RECEIVED, BookingState.PENDING_UPDATE_CONFIRMATION),
ReferenceState.GENERATE_IF_MISSING,
true);
true,
booking -> {
var clauses = booking.putArray("carrierClauses");
booking.put("termsAndConditions", termsAndConditions());
for (var clause : carrierClauses()) {
clauses.add(clause);
}
});
// processAndEmitNotificationForStateTransition will insert a CBR for the cbrr if needed,
// so this lookup has to happen after.
String cbr = cbrrToCbr.get(cbrr);
Expand Down Expand Up @@ -160,7 +167,10 @@ private void requestUpdateToBookingRequest(JsonNode actionPrompt) {
BookingState.PENDING_UPDATE,
Set.of(BookingState.RECEIVED, BookingState.PENDING_UPDATE, BookingState.PENDING_UPDATE_CONFIRMATION),
ReferenceState.PROVIDE_IF_EXIST,
true);
true,
booking -> booking.putArray("requestedChanges")
.addObject()
.put("message", "Please perform the changes requested by the Conformance orchestrator"));
addOperatorLogEntry("Requested update to the booking request with CBRR '%s'".formatted(cbrr));
}

Expand All @@ -179,12 +189,28 @@ private void confirmBookingCompleted(JsonNode actionPrompt) {
addOperatorLogEntry("Completed the booking request with CBR '%s'".formatted(cbr));
}

private void processAndEmitNotificationForStateTransition(
JsonNode actionPrompt,
BookingState targetState,
Set<BookingState> expectedState,
ReferenceState cbrHandling,
boolean includeCbrr) {
processAndEmitNotificationForStateTransition(
actionPrompt,
targetState,
expectedState,
cbrHandling,
includeCbrr,
null
);
}
private void processAndEmitNotificationForStateTransition(
JsonNode actionPrompt,
BookingState targetState,
Set<BookingState> expectedState,
ReferenceState cbrHandling,
boolean includeCbrr) {
boolean includeCbrr,
Consumer<ObjectNode> bookingMutator) {
String cbrr = actionPrompt.get("cbrr").asText();
String cbr = cbrrToCbr.get(cbrr);
BookingState currentState = bookingStatesByCbrr.get(cbrr);
Expand All @@ -195,6 +221,8 @@ private void processAndEmitNotificationForStateTransition(
}

if (isCorrect) {
var booking = (ObjectNode)persistentMap.load(cbrr);
boolean generatedCBR = false;
bookingStatesByCbrr.put(cbrr, targetState);
switch (cbrHandling) {
case MUST_EXIST -> {
Expand All @@ -208,6 +236,7 @@ private void processAndEmitNotificationForStateTransition(
if (cbr == null) {
cbr = UUID.randomUUID().toString().replace("-", "").toUpperCase();
cbrrToCbr.put(cbrr, cbr);
generatedCBR = true;
}
}
case PROVIDE_IF_EXIST -> {
Expand All @@ -219,6 +248,17 @@ private void processAndEmitNotificationForStateTransition(
"If includeCbrr is false, then cbrHandling must ensure"
+ " that a carrierBookingReference is provided");
}

if (booking != null) {
booking.put("bookingStatus", targetState.wireName());
if (generatedCBR) {
booking.put("carrierBookingReference", cbr);
}
if (bookingMutator != null) {
bookingMutator.accept(booking);
}
persistentMap.save(cbrr, booking);
}
}
var notification =
BookingNotification.builder()
Expand Down Expand Up @@ -359,4 +399,38 @@ private enum ReferenceState {
GENERATE_IF_MISSING,
;
}

private static List<String> carrierClauses() {
return List.of(
"Per terms and conditions (see the termsAndConditions field), this is not a real booking.",
"A real booking would probably have more legal text here."
);
}

private static String termsAndConditions() {
return """
You agree that this booking exist is name only for the sake of
testing your conformance with the DCSA BKG API. This booking is NOT backed
by a real booking with ANY carrier and NONE of the requested services will be
carried out in real life.
Unless required by applicable law or agreed to in writing, DCSA provides
this JSON data on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied, including, without limitation, any
warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY,
or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
determining the appropriateness of using or redistributing this JSON
data and assume any risks associated with Your usage of this data.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall DCSA be liable to
You for damages, including any direct, indirect, special, incidental, or
consequential damages of any character arising as a result of this terms or conditions
or out of the use or inability to use the provided JSON data (including but not limited
to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any
and all other commercial damages or losses), even if DCSA has been advised of the
possibility of such damages.
""";
}
}

0 comments on commit b2bb295

Please sign in to comment.