From 9410509fb91c0f0b2ec40ef5e779514a3f203a47 Mon Sep 17 00:00:00 2001
From: preetamnpr <128618622+preetamnpr@users.noreply.github.com>
Date: Sun, 15 Dec 2024 15:43:42 +0100
Subject: [PATCH] SD-1823 Revisit the OVS checks with unit test cases (#235)

---
 .../core/check/JsonAttributeBasedCheck.java   |   1 +
 ovs/pom.xml                                   |   6 +
 .../standards/ovs/OvsScenarioListBuilder.java |  24 +-
 .../SupplyScenarioParametersAction.java       |  51 +-
 .../standards/ovs/checks/OvsChecks.java       | 450 +++++++++--------
 .../ovs/party/OvsFilterParameter.java         |  48 +-
 .../standards/ovs/party/OvsPublisher.java     |   4 +-
 .../ovs/messages/ovs-300-response.json        |  44 +-
 .../standards/ovs/schemas/OVS_v3.0.0.yaml     |   1 +
 .../standards/ovs/checks/OvsChecksTest.java   | 465 ++++++++++++++++++
 ...s-300-response-wrong-attribute-values.json | 119 +++++
 .../ovs-300-response-wrong-date-times.json    | 118 +++++
 .../ovs-300-response-wrong-structure.json     | 119 +++++
 13 files changed, 1195 insertions(+), 255 deletions(-)
 create mode 100644 ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java
 create mode 100644 ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json
 create mode 100644 ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json
 create mode 100644 ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json

diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java
index 63bc1052..1a496759 100644
--- a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java
+++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java
@@ -13,6 +13,7 @@
 import org.dcsa.conformance.core.traffic.ConformanceExchange;
 import org.dcsa.conformance.core.traffic.HttpMessageType;
 
+
 class JsonAttributeBasedCheck extends ActionCheck {
 
   private final String standardsVersion;
diff --git a/ovs/pom.xml b/ovs/pom.xml
index 34701936..3af3f6e4 100644
--- a/ovs/pom.xml
+++ b/ovs/pom.xml
@@ -23,5 +23,11 @@
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
 		</dependency>
+		<!-- Test scoped dependencies -->
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter</artifactId>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 </project>
diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java
index 819ad016..4a973c5b 100644
--- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java
+++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java
@@ -35,38 +35,38 @@ public static LinkedHashMap<String, OvsScenarioListBuilder> createModuleScenario
                     .thenEither(
                         scenarioWithParameters(Map.of(CARRIER_SERVICE_NAME, "Great Lion Service")),
                         scenarioWithParameters(
-                            Map.of(CARRIER_SERVICE_NAME, "Blue Whale Service", LIMIT, "1")),
+                            Map.of(CARRIER_SERVICE_NAME, "Blue Whale Service", LIMIT, "5")),
                         scenarioWithParameters(
                             Map.of(
                                 CARRIER_SERVICE_NAME,
                                 "Red Falcon Service",
                                 START_DATE,
-                                "2026-01-01")),
+                                "2024-01-01")),
                         scenarioWithParameters(
                             Map.of(
                                 CARRIER_SERVICE_NAME,
                                 "Great Lion Service",
                                 END_DATE,
-                                "2021-01-01")),
+                                "2025-01-01")),
                         scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1")),
-                        scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1", LIMIT, "1")),
+                        scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1", LIMIT, "5")),
                         scenarioWithParameters(Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR12345A")),
                         scenarioWithParameters(
-                            Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR67890B", LIMIT, "1")))),
+                            Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR67890B", LIMIT, "5")))),
             Map.entry(
                 "Vessel schedules",
                 noAction()
                     .thenEither(
                         scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9456789")),
-                        scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9876543", LIMIT, "1")))),
+                        scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9876543", LIMIT, "5")))),
             Map.entry(
                 "Location schedules",
                 noAction()
                     .thenEither(
                         scenarioWithParameters(Map.of(UN_LOCATION_CODE, "NLAMS")),
-                        scenarioWithParameters(Map.of(UN_LOCATION_CODE, "USNYC", LIMIT, "1")),
+                        scenarioWithParameters(Map.of(UN_LOCATION_CODE, "USNYC", LIMIT, "5")),
                         scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM")),
-                        scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM", LIMIT, "1")))),
+                        scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM", LIMIT, "5")))),
             Map.entry(
                 "Voyage schedules",
                 noAction()
@@ -79,7 +79,7 @@ public static LinkedHashMap<String, OvsScenarioListBuilder> createModuleScenario
                             Map.of(
                                 CARRIER_VOYAGE_NUMBER, "2104S",
                                 CARRIER_SERVICE_CODE, "BW1",
-                                LIMIT, "1")),
+                                LIMIT, "5")),
                         scenarioWithParameters(
                             Map.of(
                                 CARRIER_VOYAGE_NUMBER, "2103N",
@@ -88,7 +88,7 @@ public static LinkedHashMap<String, OvsScenarioListBuilder> createModuleScenario
                             Map.of(
                                 CARRIER_VOYAGE_NUMBER, "2103S",
                                 UNIVERSAL_SERVICE_REFERENCE, "SR12345A",
-                                LIMIT, "1")),
+                                LIMIT, "5")),
                         scenarioWithParameters(
                             Map.of(
                                 UNIVERSAL_VOYAGE_REFERENCE, "2103N",
@@ -97,7 +97,7 @@ public static LinkedHashMap<String, OvsScenarioListBuilder> createModuleScenario
                             Map.of(
                                 UNIVERSAL_VOYAGE_REFERENCE, "2103S",
                                 CARRIER_SERVICE_CODE, "FE1",
-                                LIMIT, "1")),
+                                LIMIT, "5")),
                         scenarioWithParameters(
                             Map.of(
                                 UNIVERSAL_VOYAGE_REFERENCE, "2105N",
@@ -106,7 +106,7 @@ public static LinkedHashMap<String, OvsScenarioListBuilder> createModuleScenario
                             Map.of(
                                 UNIVERSAL_VOYAGE_REFERENCE, "2105S",
                                 UNIVERSAL_SERVICE_REFERENCE, "SR54321C",
-                                LIMIT, "1")))))
+                                LIMIT, "5")))))
         .collect(
             Collectors.toMap(
                 Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java
index 6896ae0d..699ef07e 100644
--- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java
+++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java
@@ -4,10 +4,17 @@
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import lombok.Getter;
+import org.dcsa.conformance.core.UserFacingException;
 import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter;
 import org.dcsa.conformance.standards.ovs.party.SuppliedScenarioParameters;
 
@@ -90,6 +97,48 @@ public boolean isInputRequired() {
   @Override
   public void handlePartyInput(JsonNode partyInput) {
     super.handlePartyInput(partyInput);
-    suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(partyInput.get("input"));
+    JsonNode inputNode = partyInput.get("input");
+    Set<String> inputKeys =
+      StreamSupport.stream(
+          ((Iterable<String>) inputNode::fieldNames)
+            .spliterator(),
+          false)
+        .collect(Collectors.toSet());
+
+    Set<String> missingKeys =
+      StreamSupport.stream(
+          ((Iterable<String>) () -> getJsonForHumanReadablePrompt().fieldNames())
+            .spliterator(),
+          false)
+        .collect(Collectors.toSet());
+    missingKeys.removeAll(inputKeys);
+    if (!missingKeys.isEmpty()) {
+      throw new UserFacingException(
+        "The input must contain: %s".formatted(String.join(", ", missingKeys)));
+    }
+
+    Arrays.stream(OvsFilterParameter.values())
+        .map(OvsFilterParameter::getQueryParamName)
+        .filter(
+            queryParamName ->
+                queryParamName.startsWith(
+                    OvsFilterParameter.START_DATE.getQueryParamName())
+              || queryParamName.startsWith(OvsFilterParameter.END_DATE.getQueryParamName()))
+        .filter(inputNode::hasNonNull)
+        .forEach(
+            queryParamName -> {
+              String dateValue = inputNode.path(queryParamName).asText();
+              try {
+                LocalDate.parse(dateValue, DateTimeFormatter.ISO_DATE);
+              } catch (DateTimeParseException e) {
+                throw new UserFacingException(
+                    "Invalid date-time format '%s' for input parameter '%s'"
+                        .formatted(dateValue, queryParamName),
+                    e);
+              }
+            });
+
+    suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(inputNode);
+
   }
 }
diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java
index 5cff8bd2..b02fbb4d 100644
--- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java
+++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
 import org.dcsa.conformance.core.check.ActionCheck;
 import org.dcsa.conformance.core.check.JsonAttribute;
 import org.dcsa.conformance.core.check.JsonContentCheck;
@@ -13,6 +14,7 @@
 import java.time.LocalDate;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.function.BiPredicate;
 import java.util.function.Supplier;
@@ -20,227 +22,176 @@
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
+@Slf4j
 @UtilityClass
 public class OvsChecks {
 
-  public static ActionCheck responseContentChecks(
-      UUID matched, String standardVersion, Supplier<SuppliedScenarioParameters> sspSupplier) {
-
+  public List<JsonContentCheck> buildResponseContentChecks(Map<OvsFilterParameter, String> filterParametersMap) {
     var checks = new ArrayList<JsonContentCheck>();
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate carrierServiceCode exists and "
-                + "matches in JSON response if request parameter has carrierServiceCode",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.CARRIER_SERVICE_CODE,
-                    "*/carrierServiceCode")));
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate universalServiceReference exists and "
-                + "matches in JSON response if request parameter has universalServiceReference",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE,
-                    "*/universalServiceReference")));
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate vesselIMONumber exists and"
-                + " matches in JSON response if request parameter has vesselIMONumber",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.VESSEL_IMO_NUMBER,
-                    "*/vesselSchedules/*/vesselIMONumber")));
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate vesselName exists and"
-                + " matches in JSON response if request parameter has vesselName",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.VESSEL_NAME,
-                    "*/vesselSchedules/*/vesselName")));
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate carrierVoyageNumber exists and "
-                + "matches in JSON response if request parameter has carrierVoyageNumber",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.CARRIER_VOYAGE_NUMBER,
-                    "*/vesselSchedules/*/transportCalls/*/carrierExportVoyageNumber",
-                    "*/vesselSchedules/*/transportCalls/*/carrierImportVoyageNumber")));
-
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate universalVoyageReference exists and"
-                + " matches in JSON response if request parameter has universalVoyageReference",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE,
-                    "*/vesselSchedules/*/transportCalls/*/universalImportVoyageReference",
-                    "*/vesselSchedules/*/transportCalls/*/universalExportVoyageReference")));
+      JsonAttribute.customValidator(
+        "Every response received during a conformance test must contain schedules",
+        body -> {
+          Set<String> validationErrors = new LinkedHashSet<>();
+          checkServiceSchedulesExist(body)
+            .forEach(
+              validationError ->
+                validationErrors.add(
+                  "CheckServiceSchedules failed: %s".formatted(validationError)));
+          return validationErrors;
+        }));
 
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate UNLocationCode exists and "
-                + "matches in JSON response if request parameter has UNLocationCode",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.UN_LOCATION_CODE,
-                    "*/vesselSchedules/*/transportCalls/*/location/UNLocationCode")));
+      JsonAttribute.customValidator(
+        "If present, at least one schedule attribute must match the corresponding query parameters",
+        body -> {
+          Set<String> validationErrors = new LinkedHashSet<>();
+          checkThatScheduleValuesMatchParamValues(body, filterParametersMap)
+            .forEach(
+              validationError ->
+                validationErrors.add(
+                  "Schedule Param Value Validation failed: %s"
+                    .formatted(validationError)));
+          return validationErrors;
+        }));
 
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate facilitySMDGCode exists and "
-                + "matches in JSON response if request parameter has facilitySMDGCode",
-            body ->
-                validateParameter(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.FACILITY_SMDG_CODE,
-                    "*/vesselSchedules/*/transportCalls/*/location/facilitySMDGCode")));
+      JsonAttribute.customValidator(
+        "Check eventDateTime is greater than startDate filter parameter if present",
+        body -> {
+          Set<String> validationErrors = new LinkedHashSet<>();
+          validateDate(
+            body, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore)
+            .forEach(
+              validationError ->
+                validationErrors.add(
+                  "Start Date EventDateTime validation failed: %s"
+                    .formatted(validationError)));
+          return validationErrors;
+        }));
 
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate startDate exists and "
-                + "matches in JSON response if request parameter has startDate",
-            body ->
-                validateDate(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.START_DATE,
-                    (eventDate, expectedStartDate) ->
-                        !eventDate.isAfter(
-                            expectedStartDate)))); // Check if eventDate is before or equal to
-    // startDate
+      JsonAttribute.customValidator(
+        "Check eventDateTime is less than endDate filter parameter if present",
+        body -> {
+          Set<String> validationErrors = new LinkedHashSet<>();
+          validateDate(
+            body, filterParametersMap, OvsFilterParameter.END_DATE, LocalDate::isAfter)
+            .forEach(
+              validationError ->
+                validationErrors.add(
+                  "EndDate EventDateTime validation failed: %s"
+                    .formatted(validationError)));
+          return validationErrors;
+        }));
 
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate endDate exists and "
-                + "matches in JSON response if request parameter has endDate",
-            body ->
-                validateDate(
-                    body,
-                    sspSupplier,
-                    OvsFilterParameter.END_DATE,
-                    (eventDate, expectedEndDate) ->
-                        !eventDate.isBefore(
-                            expectedEndDate)))); // Check if eventDate is after or equal to endDate
+      JsonAttribute.customValidator(
+        "Check transportCallReference is unique across each service schedules",
+        OvsChecks::validateUniqueTransportCallReference));
 
     checks.add(
-        JsonAttribute.customValidator(
-            "Validate transportCallReference is unique across each array node",
-            OvsChecks::validateUniqueTransportCallReference));
-
-    checks.add(
-        JsonAttribute.customValidator(
-            "Validate limit exists and the number of schedules does not exceed the limit",
-            body -> {
-              Optional<Map.Entry<OvsFilterParameter, String>> limitParam =
-                  sspSupplier.get().getMap().entrySet().stream()
-                      .filter(e -> e.getKey().equals(OvsFilterParameter.LIMIT))
-                      .findFirst();
-
-              if (limitParam.isPresent()) {
-                int expectedLimit = Integer.parseInt(limitParam.get().getValue().trim());
-                if (body.size() > expectedLimit) {
-                  return Set.of(
-                      "The number of schedules exceeds the limit parameter: " + expectedLimit);
-                }
-              }
-
-              return Set.of();
-            }));
+      JsonAttribute.customValidator(
+        "Validate limit exists and the number of schedules does not exceed the limit",
+        body -> {
+          Optional<Map.Entry<OvsFilterParameter, String>> limitParam =
+            filterParametersMap.entrySet().stream()
+              .filter(e -> e.getKey().equals(OvsFilterParameter.LIMIT))
+              .findFirst();
+
+          if (limitParam.isPresent()) {
+            int expectedLimit = Integer.parseInt(limitParam.get().getValue().trim());
+            if (body.size() > expectedLimit) {
+              return Set.of(
+                "The number of service schedules exceeds the limit parameter: "
+                  + expectedLimit);
+            }
+          }
+          return Set.of();
+        }));
+    return checks;
+  }
 
+  public static ActionCheck responseContentChecks(
+      UUID matched, String standardVersion, Supplier<SuppliedScenarioParameters> sspSupplier) {
+    Map<OvsFilterParameter, String> filterParametersMap = sspSupplier.get() != null
+      ? sspSupplier.get().getMap()
+      : Map.of();
+    var checks = buildResponseContentChecks(filterParametersMap);
     return JsonAttribute.contentChecks(
         OvsRole::isPublisher, matched, HttpMessageType.RESPONSE, standardVersion, checks);
   }
 
-  private Set<String> validateParameter(
-      JsonNode body,
-      Supplier<SuppliedScenarioParameters> sspSupplier,
-      OvsFilterParameter parameter,
-      String... jsonPaths) {
-    Optional<Map.Entry<OvsFilterParameter, String>> param =
-        sspSupplier.get().getMap().entrySet().stream()
-            .filter(e -> e.getKey().equals(parameter))
-            .findFirst();
-
-    if (param.isPresent()) {
-      Set<String> expectedValues =
-          Arrays.stream(param.get().getValue().split(","))
-              .map(String::trim)
-              .collect(Collectors.toSet());
-
-      // Check if ANY of the jsonPaths match the expectedValues
-      if (jsonPaths.length > 1) {
-        boolean anyMatch =
-            Stream.of(jsonPaths)
-                .anyMatch(
-                    jsonPath ->
-                        findMatchingNodes(body, jsonPath)
-                            .anyMatch(
-                                valueNode ->
-                                    !(valueNode.isMissingNode() || valueNode.isNull())
-                                        && expectedValues.contains(valueNode.asText())));
-
-        if (!anyMatch) {
-          return Set.of(
-              "Missing or mismatched values for parameter: "
-                  + parameter.getQueryParamName()
-                  + " in any of the paths: "
-                  + Arrays.toString(jsonPaths));
-        }
-      } else {
-        Set<String> errors =
-            Stream.of(jsonPaths)
-                .flatMap(
-                    jsonPath ->
-                        findMatchingNodes(body, jsonPath)
-                            .filter(
-                                valueNode ->
-                                    !(valueNode.isMissingNode() || valueNode.isNull())
-                                        && !expectedValues.contains(valueNode.asText()))
-                            .map(
-                                valueNode ->
-                                    "Missing or mismatched "
-                                        + jsonPath
-                                        + ": "
-                                        + valueNode.asText()))
-                .collect(Collectors.toSet());
+  public Set<String> checkThatScheduleValuesMatchParamValues(
+      JsonNode schedulesNode, Map<OvsFilterParameter, String> filterParametersMap) {
+    Set<String> validationErrors = new LinkedHashSet<>();
+    Arrays.stream(OvsFilterParameter.values())
+        .filter(param -> !param.getJsonPaths().isEmpty())
+        .filter(param -> !param.isSeparateCheckRequired())
+        .filter(filterParametersMap::containsKey)
+        .forEach(
+            filterParameter -> {
+              Set<String> parameterValues =
+                  Arrays.stream(filterParametersMap.get(filterParameter).split(","))
+                      .collect(Collectors.toSet());
+              Set<Map.Entry<String, JsonNode>> attributeValues = new HashSet<>();
+              Set<String> jsonPaths = filterParameter.getJsonPaths();
+              jsonPaths.forEach(
+                  jsonPathExpression -> {
+                    findMatchingNodes(schedulesNode, jsonPathExpression)
+                        .forEach(
+                            result -> {
+                              if (!result.getValue().isMissingNode()
+                                  && !result.getValue().isNull()) {
+                                attributeValues.add(result);
+                              }
+                            });
+                  });
+              if (!attributeValues.isEmpty()
+                  && parameterValues.stream()
+                      .noneMatch(
+                          parameterValue ->
+                              attributeValues.stream()
+                                  .anyMatch(
+                                      entry ->
+                                          entry
+                                              .getValue()
+                                              .asText()
+                                              .trim()
+                                              .equals(
+                                                  parameterValue.trim())))) { // Trim and compare
+
+                String errorMessage =
+                    String.format(
+                        "Value%s '%s' at path%s '%s' do%s not match value%s '%s' of query parameter '%s'",
+                        attributeValues.size() > 1 ? "s" : "",
+                        attributeValues.stream()
+                            .map(e -> e.getValue().asText())
+                            .collect(Collectors.joining(", ")),
+                        attributeValues.size() > 1 ? "s" : "",
+                        attributeValues.stream()
+                            .map(Map.Entry::getKey)
+                            .collect(Collectors.joining(", ")), // Get keys here
+                        attributeValues.size() > 1 ? "" : "es",
+                        parameterValues.size() > 1 ? "s" : "",
+                        String.join(", ", parameterValues),
+                        filterParameter.getQueryParamName());
+
+                validationErrors.add(errorMessage);
+              }
+            });
 
-        return errors.isEmpty() ? Set.of() : errors;
-      }
-    }
-    return Set.of();
+    return validationErrors;
   }
 
-  private Set<String> validateDate(
+  public Set<String> validateDate(
       JsonNode body,
-      Supplier<SuppliedScenarioParameters> sspSupplier,
+      Map<OvsFilterParameter, String> filterParametersMap,
       OvsFilterParameter dateParameter,
       BiPredicate<LocalDate, LocalDate> dateComparison) {
 
     Optional<Map.Entry<OvsFilterParameter, String>> dateParam =
-        sspSupplier.get().getMap().entrySet().stream()
+        filterParametersMap.entrySet().stream()
             .filter(e -> e.getKey().equals(dateParameter))
             .findFirst();
 
@@ -255,73 +206,116 @@ private Set<String> validateDate(
         findMatchingNodes(body, "*/vesselSchedules/*/transportCalls/*/timestamps")
             .flatMap(
                 timestampsNode ->
-                    StreamSupport.stream(timestampsNode.spliterator(), false)
+                    StreamSupport.stream(timestampsNode.getValue().spliterator(), false)
+                        .filter(
+                            eventDateTimeNode ->
+                                !eventDateTimeNode.isMissingNode() && !eventDateTimeNode.isNull())
                         .filter(
                             timestampNode -> {
-                              OffsetDateTime eventDateTime =
-                                  OffsetDateTime.parse(
-                                      timestampNode.path("eventDateTime").asText(),
-                                      DateTimeFormatter.ISO_DATE_TIME);
-                              return dateComparison.test(eventDateTime.toLocalDate(), expectedDate);
+                              LocalDate eventDateTime =
+                                  stringToISODateTime(timestampNode.path("eventDateTime").asText());
+                              if (eventDateTime != null) {
+                                return dateComparison.test(eventDateTime, expectedDate);
+                              }
+                              return false;
                             }))
             .map(
                 timestampNode ->
                     "Event DateTime "
                         + timestampNode.path("eventDateTime").asText()
                         + (dateParameter == OvsFilterParameter.START_DATE
-                            ? " is before or equal to the startDate: "
-                            : " is after or equal to the endDate: ")
+                            ? " is before the startDate: "
+                            : " is after the endDate: ")
                         + expectedDate)
             .collect(Collectors.toSet());
 
     return errors.isEmpty() ? Set.of() : errors;
   }
 
-  private Set<String> validateUniqueTransportCallReference(JsonNode body) {
-    Set<String> transportCallReferences = new HashSet<>();
-    Set<String> errors = new HashSet<>();
+  private static LocalDate stringToISODateTime(String dateTimeString) {
+    try {
+      return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME).toLocalDate();
+    } catch (DateTimeParseException e) {
+      log.error("Failed to parse date time string: {}", dateTimeString, e);
+      return null;
+    }
+  }
 
-    // Iterate over each array node in the response body
+  public Set<String> validateUniqueTransportCallReference(JsonNode body) {
+    Set<String> errors = new HashSet<>();
+    // Iterate over each service schedule in the response body
     for (JsonNode node : body) {
-      // Assuming the path to transportCallReference is consistent across array nodes
-      findMatchingNodes(node, "*/vesselSchedules/*/transportCalls/*/transportCallReference")
+      Set<String> transportCallReferences = new HashSet<>();
+      findMatchingNodes(node, "vesselSchedules/*/transportCalls/*/transportCallReference")
+          .filter(tcrNode -> !tcrNode.getValue().isMissingNode() && !tcrNode.getValue().isNull())
           .forEach(
               transportCallReferenceNode -> {
-                String transportCallReference = transportCallReferenceNode.asText();
+                String transportCallReference = transportCallReferenceNode.getValue().asText();
                 if (!transportCallReferences.add(transportCallReference)) {
-                  errors.add("Duplicate transportCallReference found: " + transportCallReference);
+                  errors.add(
+                      ("Duplicate transportCallReference %s " + "found at %s")
+                          .formatted(transportCallReference, transportCallReferenceNode.getKey()));
                 }
               });
     }
-
     return errors;
   }
 
-  private Stream<JsonNode> findMatchingNodes(JsonNode node, String jsonPath) {
+  public Stream<Map.Entry<String, JsonNode>> findMatchingNodes(JsonNode node, String jsonPath) {
+    return findMatchingNodes(node, jsonPath, "");
+  }
+
+  private Stream<Map.Entry<String, JsonNode>> findMatchingNodes(
+      JsonNode node, String jsonPath, String currentPath) {
     if (jsonPath.isEmpty() || jsonPath.equals("/")) {
-      return Stream.of(node);
+      return Stream.of(Map.entry(currentPath, node));
     }
 
-    String[] pathSegments = jsonPath.split("/");
-    if (pathSegments[0].equals("*")) {
+    String[] pathSegments = jsonPath.split("/", 2);
+    String segment = pathSegments[0];
+    String remainingPath = pathSegments.length > 1 ? pathSegments[1] : "";
+
+    if (segment.equals("*")) {
       if (node.isArray()) {
-        // If the node is an array, iterate over its elements
-        return StreamSupport.stream(node.spliterator(), false)
-            .flatMap(
-                childNode ->
-                    findMatchingNodes(
-                        childNode,
-                        String.join(
-                            "/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length))));
+        List<Map.Entry<String, JsonNode>> results = new ArrayList<>();
+        for (int i = 0; i < node.size(); i++) {
+          JsonNode childNode = node.get(i);
+          String newPath = currentPath.isEmpty() ? String.valueOf(i) : currentPath + "/" + i;
+          results.addAll(findMatchingNodes(childNode, remainingPath, newPath).toList());
+        }
+        return results.stream();
+      } else if (node.isObject()) {
+        return findMatchingNodes(node, remainingPath, currentPath);
       } else {
-        // If not an array, treat it as a single node
-        return findMatchingNodes(
-            node, String.join("/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length)));
+        return Stream.of();
       }
     } else {
-      return findMatchingNodes(
-          node.path(pathSegments[0]),
-          String.join("/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length)));
+      JsonNode childNode = node.path(segment);
+      if (!childNode.isMissingNode()) {
+        String newPath = currentPath.isEmpty() ? segment : currentPath + "/" + segment;
+        return findMatchingNodes(childNode, remainingPath, newPath);
+      } else {
+        return Stream.of();
+      }
     }
   }
+
+  public Set<String> checkServiceSchedulesExist(JsonNode body) {
+
+    if (body == null || body.isMissingNode() || body.isNull()) {
+      return Set.of("Response body is missing or null.");
+    } else {
+      boolean hasVesselSchedules =
+          findMatchingNodes(body, "*/vesselSchedules")
+              .anyMatch(
+                  node ->
+                      !node.getValue().isMissingNode()
+                          && node.getValue().isArray()
+                          && !node.getValue().isEmpty());
+      if (!hasVesselSchedules) {
+        return Set.of("Response doesn't have schedules.");
+      }
+    }
+    return Set.of();
+  }
 }
diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java
index fdb2abe2..b761b177 100644
--- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java
+++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java
@@ -4,22 +4,34 @@
 
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+@Getter
 public enum OvsFilterParameter {
   CARRIER_SERVICE_NAME("carrierServiceName"),
   CARRIER_SERVICE_CODE("carrierServiceCode"),
   UNIVERSAL_SERVICE_REFERENCE("universalServiceReference"),
-  VESSEL_IMO_NUMBER("vesselIMONumber"),
-  VESSEL_NAME("vesselName"),
-  CARRIER_VOYAGE_NUMBER("carrierVoyageNumber"),
-  UNIVERSAL_VOYAGE_REFERENCE("universalVoyageReference"),
-  UN_LOCATION_CODE("UNLocationCode"),
-  FACILITY_SMDG_CODE("facilitySMDGCode"),
-  START_DATE("startDate"),
-  END_DATE("endDate"),
-  LIMIT("limit"),
+  VESSEL_IMO_NUMBER("vesselIMONumber", false, "*/vesselSchedules/*/vesselIMONumber"),
+  VESSEL_NAME("vesselName", false, "*/vesselSchedules/*/vesselName"),
+  CARRIER_VOYAGE_NUMBER(
+      "carrierVoyageNumber",
+      false,
+      "*/vesselSchedules/*/transportCalls/*/carrierExportVoyageNumber",
+      "*/vesselSchedules/*/transportCalls/*/carrierImportVoyageNumber"),
+  UNIVERSAL_VOYAGE_REFERENCE(
+      "universalVoyageReference",
+      false,
+      "*/vesselSchedules/*/transportCalls/*/universalImportVoyageReference",
+      "*/vesselSchedules/*/transportCalls/*/universalExportVoyageReference"),
+  UN_LOCATION_CODE(
+      "UNLocationCode", false, "*/vesselSchedules/*/transportCalls/*/location/UNLocationCode"),
+  FACILITY_SMDG_CODE(
+      "facilitySMDGCode", false, "*/vesselSchedules/*/transportCalls/*/location/facilitySMDGCode"),
+  START_DATE("startDate", true, "*/vesselSchedules/*/transportCalls/*/timestamps/*/eventDateTime"),
+  END_DATE("endDate", true, "*/vesselSchedules/*/transportCalls/*/timestamps/*/eventDateTime"),
+  LIMIT("limit", true),
   ;
 
   public static final Map<String, OvsFilterParameter> byQueryParamName =
@@ -28,9 +40,23 @@ public enum OvsFilterParameter {
               Collectors.toUnmodifiableMap(
                   OvsFilterParameter::getQueryParamName, Function.identity()));
 
-  @Getter private final String queryParamName;
+  private final String queryParamName;
 
-  OvsFilterParameter(String queryParamName) {
+  private final Set<String> jsonPaths;
+
+  private final boolean isSeparateCheckRequired;
+
+  OvsFilterParameter(String queryParamName, boolean isSeparateCheckRequired, String... jsonPaths) {
     this.queryParamName = queryParamName;
+    this.jsonPaths = Set.of(jsonPaths);
+    this.isSeparateCheckRequired = isSeparateCheckRequired;
+  }
+
+  OvsFilterParameter(String queryParamName) {
+    this(queryParamName, false, "*/" + queryParamName);
+  }
+
+  OvsFilterParameter(String queryParamName, boolean isSeparateCheckRequired) {
+    this(queryParamName, isSeparateCheckRequired, "*/" + queryParamName);
   }
 }
diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java
index 31ae5a7b..a3db8b72 100644
--- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java
+++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java
@@ -120,6 +120,7 @@ public ConformanceResponse handleRequest(ConformanceRequest request) {
             request.queryParams().containsKey("limit")
                 ? request.queryParams().get("limit").iterator().next()
                 : "100");
+
     if (filteredArray.size() > limit) {
       ArrayNode limitedArray = OBJECT_MAPPER.createArrayNode();
       for (int i = 0; i < limit; i++) {
@@ -129,7 +130,8 @@ public ConformanceResponse handleRequest(ConformanceRequest request) {
     }
 
     Map<String, Collection<String>> headers =
-      new HashMap<>(Map.of(API_VERSION, List.of(apiVersion)));
+        new HashMap<>(Map.of(API_VERSION, List.of(apiVersion)));
+
     return request.createResponse(200, headers, new ConformanceMessageBody(filteredArray));
   }
 
diff --git a/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json b/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json
index 2f261703..447c6198 100644
--- a/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json
+++ b/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json
@@ -13,7 +13,7 @@
         "transportCalls": [
           {
             "portVisitReference": "NLAMS1234589",
-            "transportCallReference": "SR11111X-9321483-2107W-NLAMS-ACT-1-1",
+            "transportCallReference": "SR12345X-9321483-2107W-NLAMS-ACT-1-1",
             "carrierImportVoyageNumber": "2103N",
             "carrierExportVoyageNumber": "2103S",
             "universalImportVoyageReference": "2103N",
@@ -28,7 +28,7 @@
               {
                 "eventTypeCode": "ARRI",
                 "eventClassifierCode": "PLN",
-                "eventDateTime": "2025-01-14T09:21:00+01:00",
+                "eventDateTime": "2024-01-14T09:21:00+01:00",
                 "delayReasonCode": "WEA",
                 "changeRemark": "Bad weather"
               }
@@ -115,5 +115,45 @@
         ]
       }
     ]
+  },
+  {
+    "carrierServiceName": "Red Eagle Service",
+    "carrierServiceCode": "RF1",
+    "universalServiceReference": "SR54821C",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MAE",
+        "vesselIMONumber": "9876543",
+        "vesselName": "Eagle of the Seas",
+        "vesselCallSign": "EGLS",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "USNYC1234567",
+            "transportCallReference": "SR12345Z-9876543-2109W-USNYC-ACT-3-3",
+            "carrierImportVoyageNumber": "2105N",
+            "carrierExportVoyageNumber": "2105S",
+            "universalImportVoyageReference": "2105N",
+            "universalExportVoyageReference": "2105S",
+            "location": {
+              "locationType": "FACS",
+              "UNLocationCode": "USNYC",
+              "facilitySMDGCode": "APM"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025-03-15T10:00:00-05:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Heavy traffic"
+              }
+            ]
+          }
+        ]
+      }
+    ]
   }
+
 ]
diff --git a/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml b/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml
index d82950fd..eefd2af6 100644
--- a/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml
+++ b/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml
@@ -482,6 +482,7 @@ components:
               maxLength: 75
     UNLocationLocation:
       title: UNLocation Location
+      additionalProperties: false
       x-stoplight:
         id: x4suin19xkq6q
       type: object
diff --git a/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java b/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java
new file mode 100644
index 00000000..c5d3e6dd
--- /dev/null
+++ b/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java
@@ -0,0 +1,465 @@
+package org.dcsa.conformance.standards.ovs.checks;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.dcsa.conformance.core.toolkit.JsonToolkit;
+import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.util.*;
+import java.util.stream.Stream;
+
+import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER;
+import static org.junit.jupiter.api.Assertions.*;
+
+class OvsChecksTest {
+
+  private JsonNode serviceNodes;
+
+  @BeforeEach
+  void setUp() {
+    JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel");
+    serviceNodes = createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules);
+  }
+
+  @Test
+  void testResponseContentChecks_validResponse() {
+    Set<String> issues =
+        executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "GLS"), serviceNodes);
+    assertTrue(issues.isEmpty());
+  }
+
+  @Test
+  void testResponseContentChecks_withWrongAttributesValuesResponse() {
+    JsonNode jsonBody = JsonToolkit.templateFileToJsonNode(
+      "/messages/ovs-300-response-wrong-attribute-values.json",
+      Map.ofEntries());
+
+    Set<String> issues =
+      executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "GLS"), jsonBody);
+    assertFalse(issues.isEmpty());
+  }
+
+  @Test
+  void testResponseContentChecks_withWrongDateTimesResponse() {
+    JsonNode jsonBody = JsonToolkit.templateFileToJsonNode(
+      "/messages/ovs-300-response-wrong-date-times.json",
+      Map.ofEntries());
+
+    Set<String> issues =
+      executeResponseChecks(Map.of(OvsFilterParameter.START_DATE, "2027-07-19"), jsonBody);
+    assertTrue(issues.isEmpty());
+  }
+
+  @Test
+  void testResponseContentChecks_withWrongStructureResponse() {
+    JsonNode jsonBody = JsonToolkit.templateFileToJsonNode(
+      "/messages/ovs-300-response-wrong-structure.json",
+      Map.ofEntries());
+
+    Set<String> issues =
+      executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, ""), jsonBody);
+    assertFalse(issues.isEmpty());
+  }
+
+  private Set<String> executeResponseChecks(
+      Map<OvsFilterParameter, String> ovsFilterParameterStringMap, JsonNode serviceNodes) {
+    Set<String> issues = new HashSet<>();
+
+    OvsChecks.buildResponseContentChecks(ovsFilterParameterStringMap)
+        .forEach(
+            validator -> {
+              issues.addAll(validator.validate(serviceNodes));
+            });
+    return issues;
+  }
+
+  @Test
+  void testCheckThatScheduleValuesMatchParamValues_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(
+            OvsFilterParameter.CARRIER_SERVICE_CODE,
+            "GLS",
+            OvsFilterParameter.CARRIER_SERVICE_NAME,
+            "Great Lion Service",
+            OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE,
+            "SR12345A");
+
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckThatScheduleValuesMatchParamValues_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "BW1");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckCarrierServiceName_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.CARRIER_SERVICE_NAME, "Great Lion Service");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckCarrierServiceName_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.CARRIER_SERVICE_NAME, "Great Tiger Service");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUniversalServiceReference_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, "SR12345A");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUniversalServiceReference_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, "SRA");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckVesselIMONumber_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.VESSEL_IMO_NUMBER, "1234567");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckVesselIMONumber_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.VESSEL_IMO_NUMBER, "1234");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckVesselName_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.VESSEL_NAME, "Great Vessel");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckVesselName_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.VESSEL_NAME, "Great Bowl");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckCarrierVoyageNumber_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.CARRIER_VOYAGE_NUMBER, "2104N,2104S");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckCarrierVoyageNumber_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.CARRIER_VOYAGE_NUMBER, "2104P,2104Q");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUniversalVoyageReference_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE, "SR12345A,SR45678A");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUniversalVoyageReference_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE, "SR1245A,SR458A");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUNLocationCode_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UN_LOCATION_CODE, "NLAMS");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckUNLocationCode_noMatch() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.UN_LOCATION_CODE, "USNYC");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckFacilitySMDGCode_match() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.FACILITY_SMDG_CODE, "APM");
+    JsonNode transportCall =
+        serviceNodes.get(0).get("vesselSchedules").get(0).get("transportCalls").get(0);
+    ((ObjectNode) transportCall.get("location")).put("facilitySMDGCode", "APM");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testCheckFacilitySMDGCode_noMatch() {
+    JsonNode transportCall =
+        serviceNodes.get(0).get("vesselSchedules").get(0).get("transportCalls").get(0);
+    ((ObjectNode) transportCall.get("location")).put("facilitySMDGCode", "APM");
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.FACILITY_SMDG_CODE, "APP");
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testCheckThatScheduleValuesMatchParamValues_emptyParams() {
+    Map<OvsFilterParameter, String> filterParametersMap = Collections.emptyMap();
+    Set<String> result =
+        OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testValidateDate_dateWithinRange() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.START_DATE, "2024-07-19");
+    Set<String> result =
+        OvsChecks.validateDate(
+            serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testValidateDate_dateOutsideRange() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.START_DATE, "2024-07-30");
+    Set<String> result =
+        OvsChecks.validateDate(
+            serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore);
+    assertFalse(result.isEmpty());
+  }
+
+  @Test
+  void testValidateDate_noStartDate() {
+    Map<OvsFilterParameter, String> filterParametersMap = Collections.emptyMap();
+    Set<String> result =
+        OvsChecks.validateDate(
+            serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testValidateUniqueTransportCallReference_unique() {
+    JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel");
+    JsonNode serviceNodes =
+        createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules);
+    JsonNode transportCalls = vesselSchedules.get(0).get("transportCalls");
+
+    ObjectNode newTransportCall =
+        new ObjectMapper().createObjectNode().put("transportCallReference", "TCREF2");
+    ((ArrayNode) transportCalls).add(newTransportCall);
+    Set<String> result = OvsChecks.validateUniqueTransportCallReference(serviceNodes);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testValidateUniqueTransportCallReference_duplicate() {
+    JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel");
+
+    JsonNode serviceNodes =
+        createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules);
+    JsonNode transportCalls = vesselSchedules.get(0).get("transportCalls");
+
+    ObjectNode newTransportCall =
+      OBJECT_MAPPER.createObjectNode().put("transportCallReference", "TCREF1");
+    ((ArrayNode) transportCalls).add(newTransportCall);
+
+    Set<String> result = OvsChecks.validateUniqueTransportCallReference(serviceNodes);
+    assertFalse(result.isEmpty());
+    assertEquals(1, result.size());
+  }
+
+  @Test
+  void testValidateUniqueTransportCallReference_noVesselSchedules() {
+    ((ObjectNode) serviceNodes.get(0)).remove("vesselSchedules");
+    Set<String> result = OvsChecks.validateUniqueTransportCallReference(serviceNodes);
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void testFindMatchingNodes_rootMatch() {
+    JsonNode root = OBJECT_MAPPER.createObjectNode().put("value", "test");
+    Stream<JsonNode> result = OvsChecks.findMatchingNodes(root, "/").map(Map.Entry::getValue);
+    assertEquals(1, result.count());
+  }
+
+  @Test
+  void testFindMatchingNodes_arrayMatch() throws IOException {
+    JsonNode root = OBJECT_MAPPER.readTree("[{\"a\": 1}, {\"b\": 2}]");
+    Stream<JsonNode> result = OvsChecks.findMatchingNodes(root, "*").map(Map.Entry::getValue);
+    ;
+    assertEquals(2, result.count());
+  }
+
+  @Test
+  void testFindMatchingNodes_emptyArrayMatch() throws IOException {
+    JsonNode root = OBJECT_MAPPER.readTree("[]");
+    Stream<JsonNode> result = OvsChecks.findMatchingNodes(root, "*").map(Map.Entry::getValue);
+    ;
+    assertEquals(0, result.count());
+  }
+
+  @Test
+  void testCheckServiceSchedulesExist_emptyServiceSchedules() {
+    JsonNode body = OBJECT_MAPPER.createArrayNode();
+    Set<String> result = OvsChecks.checkServiceSchedulesExist(body);
+    assertEquals(1, result.size());
+  }
+
+  @Test
+  void testCheckServiceSchedulesExist_nullServiceNode() {
+    Set<String> result = OvsChecks.checkServiceSchedulesExist(null);
+    assertEquals(1, result.size());
+  }
+
+  @Test
+  void testValidateDate_invalidDateFormat() {
+    Map<OvsFilterParameter, String> filterParametersMap =
+        Map.of(OvsFilterParameter.START_DATE, "2024-07-19");
+    JsonNode timeStamps =
+        serviceNodes
+            .get(0)
+            .get("vesselSchedules")
+            .get(0)
+            .get("transportCalls")
+            .get(0)
+            .get("timestamps");
+
+    ObjectNode invalidTimeStamp =
+      OBJECT_MAPPER.createObjectNode().put("eventDateTime", "TCREF1");
+    ((ArrayNode) timeStamps).add(invalidTimeStamp);
+
+    Set<String> result =
+        OvsChecks.validateDate(
+            serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore);
+    assertTrue(result.isEmpty());
+  }
+
+  // Helper method to create a sample JsonNode for vessel schedules
+  private JsonNode createServiceNodes(
+      String carrierServiceName,
+      String carrierServiceCode,
+      String universalServiceReference,
+      JsonNode vesselSchedules) {
+
+    // Create the root ArrayNode
+    ArrayNode rootArrayNode = OBJECT_MAPPER.createArrayNode();
+
+    // Create the first ObjectNode
+    ObjectNode firstObjectNode = OBJECT_MAPPER.createObjectNode();
+    firstObjectNode.put("carrierServiceName", carrierServiceName);
+    firstObjectNode.put("carrierServiceCode", carrierServiceCode);
+    firstObjectNode.put("universalServiceReference", universalServiceReference);
+    firstObjectNode.set("vesselSchedules", vesselSchedules);
+
+    rootArrayNode.add(firstObjectNode);
+    return rootArrayNode;
+  }
+
+  private JsonNode createServiceVesselSchedules(String vesselIMONumber, String vesselName) {
+    ArrayNode vesselSchedulesArrayNode = OBJECT_MAPPER.createArrayNode();
+    ObjectNode vesselSchedule = OBJECT_MAPPER.createObjectNode();
+    vesselSchedule.put("vesselIMONumber", vesselIMONumber);
+    vesselSchedule.put("vesselName", vesselName);
+    vesselSchedule.set(
+        "transportCalls",
+        createTransportCalls("TCREF1", "2104N", "2104S", "SR12345A", "SR45678A", "NLAMS"));
+    vesselSchedulesArrayNode.add(vesselSchedule);
+    return vesselSchedulesArrayNode;
+  }
+
+  private JsonNode createTransportCalls(
+      String transportCallReference,
+      String carrierImportVoyageNumber,
+      String carrierExportVoyageNumber,
+      String universalImportVoyageReference,
+      String universalExportVoyageReference,
+      String UNLocationCode) {
+    // Create the transportCalls ArrayNode for the vesselSchedule
+    ArrayNode transportCallsArrayNode = OBJECT_MAPPER.createArrayNode();
+    ObjectNode transportCall = OBJECT_MAPPER.createObjectNode();
+    transportCall.put("transportCallReference", transportCallReference);
+    transportCall.put("carrierImportVoyageNumber", carrierImportVoyageNumber);
+    transportCall.put("carrierExportVoyageNumber", carrierExportVoyageNumber);
+    transportCall.put("universalImportVoyageReference", universalImportVoyageReference);
+    transportCall.put("universalExportVoyageReference", universalExportVoyageReference);
+
+    // Create the location ObjectNode for the first transportCall
+    ObjectNode location = OBJECT_MAPPER.createObjectNode();
+    location.put("UNLocationCode", UNLocationCode);
+    transportCall.set("location", location);
+    transportCall.set("timestamps", createTimestamps());
+    transportCallsArrayNode.add(transportCall);
+    return transportCallsArrayNode;
+  }
+
+  private JsonNode createEventDateTime(String eventDateTime) {
+    // Create a timestamp for timestamps ArrayNode
+    ObjectNode timestamp = OBJECT_MAPPER.createObjectNode();
+    timestamp.put("eventTypeCode", "ARRI");
+    timestamp.put("eventClassifierCode", "PLN");
+    timestamp.put("eventDateTime", eventDateTime);
+    return timestamp;
+  }
+
+  private JsonNode createTimestamps() {
+    // Create the timestamps ArrayNode
+    ArrayNode timestampsArrayNode = OBJECT_MAPPER.createArrayNode();
+    timestampsArrayNode.add(createEventDateTime("2024-07-21T10:00:00Z"));
+    timestampsArrayNode.add(createEventDateTime("2024-07-22T10:00:00Z"));
+    timestampsArrayNode.add(createEventDateTime("2024-07-23T10:00:00Z"));
+    return timestampsArrayNode;
+  }
+}
diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json
new file mode 100644
index 00000000..4b92b204
--- /dev/null
+++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json
@@ -0,0 +1,119 @@
+[
+  {
+    "carrierServiceName": "Great Lioness Service ",
+    "carrierServiceCode": "AA1",
+    "universalServiceReference": "SRSSS12345A",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "HLC",
+        "vesselIMONumber": "9321483",
+        "vesselName": "King of the Roads",
+        "vesselCallSign": "NCVV",
+        "isDummyVessel": true,
+        "transportCalls": [
+          {
+            "portVisitReference": "NLAMS1234589",
+            "transportCallReference": "SR11X-9321483-2107W-NLAMS-ACT-1-1",
+            "carrierImportVoyageNumber": "2755N",
+            "carrierExportVoyageNumber": "2654S",
+            "universalImportVoyageReference": "2544N",
+            "universalExportVoyageReference": "2765S",
+            "location": {
+              "locationName": "Port of Amsterdam",
+              "locationType": "UNLO",
+              "UNLocationCode": "NLERRAMS"
+            },
+            "statusCode": "OMIT",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "PLN",
+                "eventDateTime": "2025-01-14T09:21:00+01:00",
+                "delayReasonCode": "WEA",
+                "changeRemark": "Bad weather"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Great Tiger Service",
+    "carrierServiceCode": "FK1",
+    "universalServiceReference": "SR6789SSS0B",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MSC",
+        "vesselIMONumber": "9456789",
+        "vesselName": "Eyes of the Tiger",
+        "vesselCallSign": "QOCE",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "SGSIN1234567",
+            "transportCallReference": "SR22XXXY-9456789-2108E-SGSIN-ACT-2-2",
+            "carrierImportVoyageNumber": "2348N",
+            "carrierExportVoyageNumber": "2876S",
+            "universalImportVoyageReference": "2887N",
+            "universalExportVoyageReference": "2343S",
+            "location": {
+              "locationName": "Port of Singapore",
+              "locationType": "UNLO",
+              "UNLocationCode": "SGSIN"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "DEPA",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025-02-20T15:30:00+08:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Traffic congestion"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Red Eagle Service",
+    "carrierServiceCode": "RF1",
+    "universalServiceReference": "SR54321C",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MAE",
+        "vesselIMONumber": "9876543",
+        "vesselName": "Eagle of the Rats",
+        "vesselCallSign": "EGLS",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "USNYC1234567",
+            "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3",
+            "carrierImportVoyageNumber": "2765N",
+            "carrierExportVoyageNumber": "2889S",
+            "universalImportVoyageReference": "2444N",
+            "universalExportVoyageReference": "2555S",
+            "location": {
+              "locationName": "Port of New York",
+              "locationType": "UNLO",
+              "UNLocationCode": "USSSYC"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025-03-15T10:00:00-05:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Heavy traffic"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json
new file mode 100644
index 00000000..257bc336
--- /dev/null
+++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json
@@ -0,0 +1,118 @@
+[
+  {
+    "carrierServiceName": "Great Lion Servicedd",
+    "carrierServiceCode": "FE1",
+    "universalServiceReference": "SR12345A",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "HLC",
+        "vesselIMONumber": "9321483",
+        "vesselName": "King of the Seas",
+        "vesselCallSign": "NCVV",
+        "isDummyVessel": true,
+        "transportCalls": [
+          {
+            "portVisitReference": "NLAMS1234589",
+            "transportCallReference": "SR11111X-9321483-2107W-NLAMS-ACT-1-1",
+            "carrierImportVoyageNumber": "2103N",
+            "carrierExportVoyageNumber": "2103S",
+            "universalImportVoyageReference": "2103N",
+            "universalExportVoyageReference": "2103S",
+            "location": {
+              "locationName": "Port of Amsterdam",
+              "locationType": "UNLO",
+              "UNLocationCode": "NLAMS"
+            },
+            "statusCode": "OMIT",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "PLN",
+                "delayReasonCode": "WEA",
+                "changeRemark": "Bad weather"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Great Lion Servicedd",
+    "carrierServiceCode": "BW1",
+    "universalServiceReference": "SR67890B",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MSC",
+        "vesselIMONumber": "9456789",
+        "vesselName": "Queen of the Oceans",
+        "vesselCallSign": "QOCE",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "SGSIN1234567",
+            "transportCallReference": "SR22222Y-9456789-2108E-SGSIN-ACT-2-2",
+            "carrierImportVoyageNumber": "2104N",
+            "carrierExportVoyageNumber": "2104S",
+            "universalImportVoyageReference": "2104N",
+            "universalExportVoyageReference": "2104S",
+            "location": {
+              "locationName": "Port of Singapore",
+              "locationType": "UNLO",
+              "UNLocationCode": "SGSIN"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "DEPA",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025/02/20T15:30:00+08:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Traffic congestion"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Red Falcon Service",
+    "carrierServiceCode": "RF1",
+    "universalServiceReference": "SR54321C",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MAE",
+        "vesselIMONumber": "9876543",
+        "vesselName": "Eagle of the Seas",
+        "vesselCallSign": "EGLS",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "USNYC1234567",
+            "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3",
+            "carrierImportVoyageNumber": "2105N",
+            "carrierExportVoyageNumber": "2105S",
+            "universalImportVoyageReference": "2105N",
+            "universalExportVoyageReference": "2105S",
+            "location": {
+              "locationName": "Port of New York",
+              "locationType": "UNLO",
+              "UNLocationCode": "USNYC"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025.03.15T10:00:00-05:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Heavy traffic"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json
new file mode 100644
index 00000000..a94748c6
--- /dev/null
+++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json
@@ -0,0 +1,119 @@
+[
+  {
+    "carrierServiceName": "Great Lion Servicedd",
+    "carrierServiceCode": "FE1",
+    "universalServiceReference": "SR12345A",
+    "vessels": [
+      {
+        "vesselOperatorSMDGLinerCode": "HLC",
+        "vesselIMONumber": "9321483",
+        "vesselName": "King of the Seas",
+        "vesselCallSign": "NCVV",
+        "isDummyVessel": true,
+        "transportCalls": [
+          {
+            "portVisitReference": "NLAMS1234589",
+            "transportCallReference": "SR11111X-9321483-2107W-NLAMS-ACT-1-1",
+            "carrierImportVoyageNumber": "2103N",
+            "carrierExportVoyageNumber": "2103S",
+            "universalImportVoyageReference": "2103N",
+            "universalExportVoyageReference": "2103S",
+            "location": {
+              "locationName": "Port of Amsterdam",
+              "locationType": "UNLO",
+              "UNLocationCode": "NLAMS"
+            },
+            "statusCode": "OMIT",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "PLN",
+                "eventDateTime": "2025-01-14T09:21:00+01:00",
+                "delayReasonCode": "WEA",
+                "changeRemark": "Bad weather"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Great Lion Servicedd",
+    "carrierServiceCode": "BW1",
+    "universalServiceReference": "SR67890B",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MSC",
+        "vesselIMONumber": "9456789",
+        "vesselName": "Queen of the Oceans",
+        "vesselCallSign": "QOCE",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "SGSIN1234567",
+            "transportCallReference": "SR22222Y-9456789-2108E-SGSIN-ACT-2-2",
+            "carrierImportVoyageNumber": "2104N",
+            "carrierExportVoyageNumber": "2104S",
+            "universalImportVoyageReference": "2104N",
+            "universalExportVoyageReference": "2104S",
+            "location": {
+              "locationName": "Port of Singapore",
+              "locationType": "UNLO",
+              "UNLocationCode": "SGSIN"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "DEPA",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025-02-20T15:30:00+08:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Traffic congestion"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "carrierServiceName": "Red Falcon Service",
+    "carrierServiceCode": "RF1",
+    "universalServiceReference": "SR54321C",
+    "vesselSchedules": [
+      {
+        "vesselOperatorSMDGLinerCode": "MAE",
+        "vesselIMONumber": "9876543",
+        "vesselName": "Eagle of the Seas",
+        "vesselCallSign": "EGLS",
+        "isDummyVessel": false,
+        "transportCalls": [
+          {
+            "portVisitReference": "USNYC1234567",
+            "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3",
+            "carrierImportVoyageNumber": "2105N",
+            "carrierExportVoyageNumber": "2105S",
+            "universalImportVoyageReference": "2105N",
+            "universalExportVoyageReference": "2105S",
+            "location": {
+              "locationName": "Port of New York",
+              "locationType": "UNLO",
+              "UNLocationCode": "USNYC"
+            },
+            "statusCode": "ARRI",
+            "timestamps": [
+              {
+                "eventTypeCode": "ARRI",
+                "eventClassifierCode": "ACT",
+                "eventDateTime": "2025-03-15T10:00:00-05:00",
+                "delayReasonCode": "TRF",
+                "changeRemark": "Heavy traffic"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+]