From 915bbf2e8df2abe2023dc2523b181fd3283b2252 Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Tue, 14 Jul 2026 03:37:21 +0100 Subject: [PATCH 1/5] fix(ccsph2n): de-duplicate court-calendar courtscheduler hearings per day and exclude drafts [SPRDT-1164] The court-calendar range-search courtscheduler path (RangeSearchQuery PATH A, getCourtSchedulerHearings) mutated and re-added a single shared Hearing instance per returned hearing-day, collapsing multi-day hearings onto their last day; it also never applied the allocated filter, so draft/unallocated sessions leaked into the allocated view. - Hearing: add copy constructor that deep-copies properties (per-row flatten mutates it), so day-copies cannot corrupt each other. - RangeSearchQuery.enrichAllCourtSchedulerHearingIdsIntoHearings: build one Hearing per IdResponse (distinct per-day rows) and skip non-allocated base hearings. - getCourtSchedulerHearings: report the de-duplicated/allocated row count when the result set fits on one page; keep the courtscheduler total for genuinely paged results. - Unit tests for per-day split and draft exclusion; CROWN PATH-A integration test. ouCode routing is deliberately left unchanged (deferred). --- .../RangeSearchQueryForCourtCalendarIT.java | 70 +++++++++++++- .../utils/CourtSchedulerServiceStub.java | 15 +++ .../listing/query/view/RangeSearchQuery.java | 27 ++++-- .../query/view/RangeSearchQueryTest.java | 92 +++++++++++++++++++ .../listing/persistence/entity/Hearing.java | 34 +++++++ 5 files changed, 230 insertions(+), 8 deletions(-) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java index cbe83d4f6..c0d0b9c5f 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java @@ -8,10 +8,22 @@ import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.STRING; import static uk.gov.moj.cpp.listing.it.SearchAvailableHearingIT.CASE_IN_HEARING; import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataWithAllocationDataAndJudiciaryWithCourtCenterForMagistrate; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetHearingIdsWithBody; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.getRandomCourtCenterId; - +import static uk.gov.justice.services.test.utils.core.http.BaseUriProvider.getBaseUri; +import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; +import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; +import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; +import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static javax.ws.rs.core.Response.Status.OK; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; + +import uk.gov.justice.services.test.utils.core.http.RequestParams; import uk.gov.justice.core.courts.JurisdictionType; import uk.gov.justice.services.test.utils.persistence.DatabaseCleaner; import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; @@ -127,6 +139,62 @@ public void shouldRangeSearchCourtCalendarForCrown() throws JsonProcessingExcept checkPayload(payload2, 2, testDataList, 5, 2); } + /** + * PATH A (courtscheduler-backed) regression for SPRDT-1164. When the courtscheduler returns the same + * allocated CROWN hearing id for more than one hearing-day, each day must render as its own row with its + * own hearing-day data. Before the fix, the shared Hearing/properties instance was mutated and re-added, + * so the day-of-week the enrichment finished on won: every row collapsed onto the last date and the real + * date {@code dA} disappeared. The assertion below (dA present in the rendered hearing-days) fails on the + * old code and passes on the fixed code. + */ + @Test + public void crownCourtSchedulerPathKeepsEachHearingDayDistinct() { + final String jurisdictionType = JurisdictionType.CROWN.name(); + final UUID courtCentreId = getRandomCourtCenterId(); + final UUID courtRoomId = new ArrayList<>(COURT_ROOMS.keySet()).get(0); + final UUID hearingId = randomUUID(); + final UUID masterDefendantId = randomUUID(); + final String caseUrn = STRING.next(); + + final ZonedDateTime hearingStartTime = ItClock.nowUtc(); + final LocalDate hearingEndDate = ItClock.today().plusDays(1); + final LocalDate dA = hearingStartTime.toLocalDate(); // the hearing's real (seeded) date + final LocalDate dB = dA.plusDays(1); // a second courtscheduler day for the same id + + final CaseAndDefendantData caseAndDefendantData = new CaseAndDefendantData(hearingId, caseUrn, caseUrn, + masterDefendantId, CASE_IN_HEARING, jurisdictionType, jurisdictionType, null, null); + final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps( + HearingsData.hearingsDataWithAllocationDataAndJudiciary(caseAndDefendantData, courtCentreId, courtRoomId, hearingEndDate, hearingStartTime)); + listCourtHearingSteps.whenCaseIsSubmittedForListing(); + new UpdateHearingSteps().pollUntilHearingIsPresentWithHearingId(courtCentreId.toString(), ALLOCATED, getLoggedInUser().toString(), hearingId.toString()); + + // Courtscheduler returns the SAME allocated hearing id for two hearing-days. + final String body = "{ \"results\": 2, \"pageCount\": 1, \"hearingIds\": [" + + "{\"hearingId\":\"" + hearingId + "\",\"courtScheduleId\":\"" + randomUUID() + "\",\"hearingDate\":\"" + dA + "\",\"hearingDayCount\":2,\"hearingDayPosition\":1}," + + "{\"hearingId\":\"" + hearingId + "\",\"courtScheduleId\":\"" + randomUUID() + "\",\"hearingDate\":\"" + dB + "\",\"hearingDayCount\":2,\"hearingDayPosition\":2}" + + "] }"; + stubGetHearingIdsWithBody(body); + + final String url = String.format("%s/listing-query-api/query/api/rest/listing/hearings/range-search" + + "?allocated=true&jurisdictionType=CROWN&courtSession=AD&ouCode=C01CY00&courtCentreId=%s" + + "&startDate=%s&endDate=%s&pageSize=40&pageNumber=1", + getBaseUri(), courtCentreId, ItClock.today(), ItClock.today().plusDays(3)); + final RequestParams requestParams = requestParams(url, "application/vnd.listing.search.hearings.court.calendar+json") + .withHeader(CPP_UID_HEADER.getName(), CPP_UID_HEADER.getValue()) + .build(); + + pollWithDefaults(requestParams).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearings.size()", is(2)), + withJsonPath("$.hearings[0].id", is(hearingId.toString())), + withJsonPath("$.hearings[0].allocated", is(true)), + withJsonPath("$.hearings[0].jurisdictionType", is("CROWN")), + // the real date survives per-row de-duplication (collapses to a single wrong date pre-fix) + withJsonPath("$.hearings[*].hearingDays[*].hearingDate", hasItem(dA.toString())) + )) + ); + } + private static void checkPayload(final String payload, final int hearingCount, List testDataList, final int pageSize, final int pageNumber) throws JsonProcessingException { final Map jObj = new ObjectMapper().readValue(payload, Map.class); assertThat(jObj, Matchers.is(notNullValue())); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java index b9e0a8cf1..81f88b569 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java @@ -568,6 +568,21 @@ public static void stubGetHearingIds(boolean isEmpty) { )); } + /** + * Stubs the courtscheduler get-hearing-ids endpoint with a caller-supplied body, matching any query + * params (only the endpoint + get.hearing.ids Accept header). Lets a test drive the exact + * (hearingId, courtScheduleId, hearingDate, hearingDayCount, hearingDayPosition) tuples returned to + * the court-calendar PATH A enrichment. + */ + public static void stubGetHearingIdsWithBody(final String body) { + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + HEARING_SLOTS))) + .withHeader("Accept", containing("application/vnd.courtscheduler.get.hearing.ids+json")) + .willReturn(aResponse().withStatus(OK.getStatusCode()) + .withBody(body) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + )); + } + public static void stubGetHearingIds(final Instant exactHearingStartDateTime) { stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + HEARING_SLOTS))) .withQueryParam("sessionStartDate", matching("1900-01-01")) diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java index 0b07c8cb3..8a8e57be3 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java @@ -316,7 +316,15 @@ private JsonEnvelope getCourtSchedulerHearings(final JsonEnvelope query, final b final List enrichedHearingList = isEmpty(hearingIdsResponse.getUuids()) ? emptyList() : enrichAllCourtSchedulerHearingIdsIntoHearings(hearingIdsResponse.getUuids()); logger.info("getCourtSchedulerHearings found {} hearings", hearingIdsResponse.getResults()); - return buildHearingsResponse(query, allocated, courtRoomId, startDate, enrichedHearingList, hearingIdsResponse.getResults(), hearingIdsResponse, paginationParameter); + // When the whole result set fits on one page, the rows we actually return (allocated, + // de-duplicated per hearing-day) ARE the true total, so report that instead of the raw + // courtscheduler count which still includes draft sessions. For genuinely paged results we + // cannot derive the allocated total from a single page, so keep the courtscheduler total + // (draft exclusion for the multi-page count is a courtscheduler-side follow-up). + final long courtSchedulerTotal = hearingIdsResponse.getResults(); + final long results = courtSchedulerTotal <= paginationParameter.getPageSize() + ? enrichedHearingList.size() : courtSchedulerTotal; + return buildHearingsResponse(query, allocated, courtRoomId, startDate, enrichedHearingList, results, hearingIdsResponse, paginationParameter); } @@ -328,12 +336,17 @@ private List enrichAllCourtSchedulerHearingIdsIntoHearings(final List enrichedHearings = new ArrayList<>(); hearingIds.forEach(id -> { - final Hearing hearing = hearingsById.get(id.hearingId()); - if (hearing != null) { - hearing.setHearingDate(id.hearingDate()); - hearing.setHearingDayCount(id.hearingDayCount()); - hearing.setHearingDayPosition(id.hearingDayPosition()); - enrichedHearings.add(hearing); + final Hearing base = hearingsById.get(id.hearingId()); + // The court-calendar allocated view must show confirmed-allocated hearings only; the + // courtscheduler id list can include draft (unallocated) sessions, so skip those. + if (base != null && Boolean.TRUE.equals(base.getAllocated())) { + // Give each hearing-day its own Hearing (deep-copied properties) so a multi-day + // hearing renders one distinct row per day rather than collapsing onto the last day. + final Hearing perDay = new Hearing(base); + perDay.setHearingDate(id.hearingDate()); + perDay.setHearingDayCount(id.hearingDayCount()); + perDay.setHearingDayPosition(id.hearingDayPosition()); + enrichedHearings.add(perDay); } }); diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java index c44522cb6..3036cd6bb 100644 --- a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java @@ -862,6 +862,98 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndHearingTypeIdShouldPa 1); } + @Test + void rangeSearchCourtCalendarShouldSplitMultiDayCrownHearingIntoDistinctPerDayRows() { + final UUID multiDayId = randomUUID(); + final String props = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [" + + "{\"hearingDate\": \"2026-07-14\"}, {\"hearingDate\": \"2026-07-15\"}, {\"hearingDate\": \"2026-07-16\"}] }"; + final Hearing multiDay = new Hearing(multiDayId, JacksonUtil.toJsonNode(props)); + multiDay.setAllocated(true); + + final List hearingIds = new ArrayList<>(); + hearingIds.add(new IdResponse(multiDayId, randomUUID(), LocalDate.parse("2026-07-14"), 3, 1)); + hearingIds.add(new IdResponse(multiDayId, randomUUID(), LocalDate.parse("2026-07-15"), 3, 2)); + hearingIds.add(new IdResponse(multiDayId, randomUUID(), LocalDate.parse("2026-07-16"), 3, 3)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(multiDay)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); + final JsonArray hearings = payload.getJsonArray("hearings"); + + final java.util.Set dates = new java.util.HashSet<>(); + for (int i = 0; i < hearings.size(); i++) { + dates.add(hearings.getJsonObject(i).getJsonArray("hearingDays").getJsonObject(0).getString("hearingDate")); + } + assertThat(hearings.size(), is(3)); + assertThat(dates.size(), is(3)); // aliasing bug collapses all rows onto the last day -> size 1 + assertThat(dates.contains("2026-07-14") && dates.contains("2026-07-15") && dates.contains("2026-07-16"), is(true)); + assertThat(payload.getInt("results"), is(3)); + } + + @Test + void rangeSearchCourtCalendarShouldExcludeDraftUnallocatedHearingsFromAllocatedView() { + final UUID allocatedId = randomUUID(); + final UUID draftId = randomUUID(); + final String allocatedProps = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; + final String draftProps = "{ \"allocated\":\"false\", \"startDate\": \"2026-07-02\", \"courtApplications\": [{}], " + + "\"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; + final Hearing allocated = new Hearing(allocatedId, JacksonUtil.toJsonNode(allocatedProps)); + allocated.setAllocated(true); + final Hearing draft = new Hearing(draftId, JacksonUtil.toJsonNode(draftProps)); + draft.setAllocated(false); + + final List hearingIds = new ArrayList<>(); + hearingIds.add(new IdResponse(allocatedId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); + hearingIds.add(new IdResponse(draftId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(allocated, draft)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); + final JsonArray hearings = payload.getJsonArray("hearings"); + + assertThat(hearings.size(), is(1)); // draft (allocated=false) must not appear on the allocated view + for (int i = 0; i < hearings.size(); i++) { + assertThat(hearings.getJsonObject(i).getString("allocated"), is("true")); + } + assertThat(payload.getInt("results"), is(1)); + } + // ----------------------------------------------------------------------- // rangeSearchCourtCalendar – missing branch coverage // ----------------------------------------------------------------------- diff --git a/listing-viewstore/listing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/listing/persistence/entity/Hearing.java b/listing-viewstore/listing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/listing/persistence/entity/Hearing.java index c47c9aa38..e7aaca400 100644 --- a/listing-viewstore/listing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/listing/persistence/entity/Hearing.java +++ b/listing-viewstore/listing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/listing/persistence/entity/Hearing.java @@ -125,6 +125,40 @@ public Hearing(final UUID id, final JsonNode properties) { } + /** + * Copy constructor. Produces an independent {@link Hearing} carrying the same data as + * {@code source}, with a DEEP COPY of {@code properties}. This matters for the court-calendar + * courtscheduler path: a multi-day hearing is returned once per hearing-day and each day must + * render as its own row. Per-row flattening mutates the {@code properties} JsonNode in place, so + * sharing one node across the day-copies would let one row corrupt the others. Collections are + * defensively copied; the remaining fields are immutable value types safe to share. + */ + public Hearing(final Hearing source) { + this.id = source.id; + this.courtCentreId = source.courtCentreId; + this.courtRoomId = source.courtRoomId; + this.isVacatedTrial = source.isVacatedTrial; + this.unscheduled = source.unscheduled; + this.typeId = source.typeId; + this.jurisdictionType = source.jurisdictionType; + this.weekCommencingStartDate = source.weekCommencingStartDate; + this.weekCommencingEndDate = source.weekCommencingEndDate; + this.startDate = source.startDate; + this.endDate = source.endDate; + this.hearingDays = source.hearingDays == null ? new HashSet<>() : new HashSet<>(source.hearingDays); + this.listedCases = source.listedCases == null ? new HashSet<>() : new HashSet<>(source.listedCases); + this.allocated = source.allocated; + this.courtApplications = source.courtApplications == null ? new HashSet<>() : new HashSet<>(source.courtApplications); + this.typeOfListId = source.typeOfListId; + this.properties = source.properties == null ? null : source.properties.deepCopy(); + this.estimatedMinutes = source.estimatedMinutes; + this.totalCount = source.totalCount; + this.isPossibleDisqualification = source.isPossibleDisqualification; + this.hearingDayCount = source.hearingDayCount; + this.hearingDayPosition = source.hearingDayPosition; + this.hearingDate = source.hearingDate; + } + public Hearing(final UUID id, final UUID courtCentreId, final UUID courtRoomId, From 7fa40ab842e1f91fed27bd70f9c4f54bd0ba9337 Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Tue, 14 Jul 2026 09:18:12 +0100 Subject: [PATCH 2/5] test(ccsph2n): add explicit assertion to crown court-calendar PATH-A IT (Sonar S2699) [SPRDT-1164] --- .../listing/it/RangeSearchQueryForCourtCalendarIT.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java index c0d0b9c5f..7dd830179 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java @@ -17,6 +17,7 @@ import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static javax.ws.rs.core.Response.Status.OK; import static org.hamcrest.CoreMatchers.allOf; @@ -24,6 +25,7 @@ import static org.hamcrest.CoreMatchers.is; import uk.gov.justice.services.test.utils.core.http.RequestParams; +import uk.gov.justice.services.test.utils.core.http.ResponseData; import uk.gov.justice.core.courts.JurisdictionType; import uk.gov.justice.services.test.utils.persistence.DatabaseCleaner; import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; @@ -183,7 +185,7 @@ public void crownCourtSchedulerPathKeepsEachHearingDayDistinct() { .withHeader(CPP_UID_HEADER.getName(), CPP_UID_HEADER.getValue()) .build(); - pollWithDefaults(requestParams).until(status().is(OK), + final ResponseData response = pollWithDefaults(requestParams).until(status().is(OK), payload().isJson(allOf( withJsonPath("$.hearings.size()", is(2)), withJsonPath("$.hearings[0].id", is(hearingId.toString())), @@ -193,6 +195,11 @@ public void crownCourtSchedulerPathKeepsEachHearingDayDistinct() { withJsonPath("$.hearings[*].hearingDays[*].hearingDate", hasItem(dA.toString())) )) ); + + // Explicit assertion on the settled response (also satisfies Sonar java:S2699): the seeded date + // dA is still present after per-day de-duplication instead of collapsing onto a single date. + assertThat(response.getPayload(), + isJson(withJsonPath("$.hearings[*].hearingDays[*].hearingDate", hasItem(dA.toString())))); } private static void checkPayload(final String payload, final int hearingCount, List testDataList, final int pageSize, final int pageNumber) throws JsonProcessingException { From 6886262c24d2f26a221871cefa4a9691dc3ac211 Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Wed, 15 Jul 2026 01:50:49 +0100 Subject: [PATCH 3/5] =?UTF-8?q?feat(ccsph2n):=20court-calendar=20PATH=20A?= =?UTF-8?q?=20=E2=80=94=20courtscheduler-side=20draft=20filter=20+=20listi?= =?UTF-8?q?ng-side=20hearingType=20filter=20+=20dedupe=20[SPRDT-1164]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the court-calendar range-search courtscheduler path (RangeSearchQuery PATH A): - Draft (unallocated) court-schedule sessions are now excluded at source: when the request is allocated=true, isDraft=false is passed to courtscheduler get.hearing.ids. The previous listing-side allocated filter and single-page count adjustment are removed (single filter home). Pagination/count are left as main: the caller's pageSize is forwarded and the courtscheduler results count is passed straight through. - hearingType is filtered listing-side (viewstore typeId), since courtscheduler has no hearing type. - hearingIds are de-duplicated before the viewstore fetch. - the per-day Hearing copy (multi-day split) is retained. The plain (non-court-calendar) range-search path is unchanged (isDraft/hearingType passed as null). CourtSchedulerServiceAdapter.getCourtSchedulerHearings gains a nullable isDraft param, added to the courtscheduler query only when non-null. Tests: RangeSearchQueryTest (+5), CourtSchedulerServiceAdapterTest (+2); court-calendar IT shouldRouteCourtCalendarPathAByCourtSessionAndFilterByHearingType covers Any/AM/AD and the hearingType filter (PATH A + PATH B routing). Full listing IT suite: 258 passed, 0 failures, 3 skipped. --- .../service/CourtSchedulerServiceAdapter.java | 6 + .../CourtSchedulerServiceAdapterTest.java | 55 ++++- .../RangeSearchQueryForCourtCalendarIT.java | 115 +++++++++ .../cpp/listing/steps/data/HearingsData.java | 11 + .../data/factory/HearingsDataFactory.java | 47 +++- .../utils/CourtSchedulerServiceStub.java | 17 ++ .../listing/query/view/RangeSearchQuery.java | 43 ++-- .../query/view/RangeSearchQueryTest.java | 226 ++++++++++++++++-- 8 files changed, 485 insertions(+), 35 deletions(-) diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java index 7822d9ed1..dc05ec3be 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java @@ -438,6 +438,7 @@ public HearingIdsResponse getCourtSchedulerHearings(final String ouCode, final String endDate, final Optional exactHearingStartDateTime, final Optional businessTypeOptional, final Optional jurisdiction, + final Boolean isDraft, final String panel, final Integer pageSize, final Integer pageNumber ) { final Map queryParams = new HashMap<>(); @@ -452,6 +453,11 @@ public HearingIdsResponse getCourtSchedulerHearings(final String ouCode, businessTypeOptional.ifPresent(businessType -> queryParams.put(BUSINESS_TYPE, businessType)); jurisdiction.ifPresent(j -> queryParams.put(JURISDICTION, j)); exactHearingStartDateTime.ifPresent(s -> queryParams.put(EXACT_HEARING_START_DATETIME, s.toString())); + // Draft (unallocated) court-schedule sessions are excluded at source when isDraft is supplied; + // a null isDraft leaves the courtscheduler result unfiltered (returns ALL). + if (isDraft != null) { + queryParams.put(IS_DRAFT, isDraft.toString()); + } final Response hearingsResponse = getCourtSchedulerHearingIds(queryParams); diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java index 9f4eb253a..9201c3383 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.moj.cpp.listing.common.utils.FileUtil.givenPayload; @@ -19,6 +20,7 @@ import java.time.Instant; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.IntStream; @@ -31,6 +33,7 @@ import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -216,13 +219,63 @@ void shouldGetHearingIds() { when(response.getEntity()).thenReturn(hearingIdsResponse); when(hearingSlotsService.getCourtSchedulerHearingIds(anyMap())).thenReturn(response); - final HearingIdsResponse finalResp = courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), "ADULT,YOUTH", pageSize, pageNumber); + final HearingIdsResponse finalResp = courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), Boolean.FALSE, "ADULT,YOUTH", pageSize, pageNumber); assertThat(finalResp.getUuids().size(), is(4)); assertThat(finalResp.getPageCount(), is(1L)); assertThat(finalResp.getResults(), is(4L)); } + @Test + void getCourtSchedulerHearingsShouldAddIsDraftFalseToQueryParamsWhenIsDraftIsFalse() { + final String courtCentreId = UUID.randomUUID().toString(); + final Optional courtSessionOptional = Optional.of("AD"); + final String courtRoomId = UUID.randomUUID().toString(); + final String startDate = LocalDate.now().toString(); + final String endDate = LocalDate.now().plusDays(7).toString(); + final Optional businessTypeOptional = Optional.of("BA123"); + final Integer pageSize = 50; + final Integer pageNumber = 1; + + final JsonObject hearingIdsResponse = givenPayload("/mock-data/azure.rotasl.getHearingIds.stub-data.json"); + + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(response.getEntity()).thenReturn(hearingIdsResponse); + when(hearingSlotsService.getCourtSchedulerHearingIds(anyMap())).thenReturn(response); + + courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), Boolean.FALSE, "ADULT,YOUTH", pageSize, pageNumber); + + final ArgumentCaptor> queryParamsCaptor = ArgumentCaptor.forClass(Map.class); + verify(hearingSlotsService).getCourtSchedulerHearingIds(queryParamsCaptor.capture()); + + assertThat(queryParamsCaptor.getValue().get("isDraft"), is("false")); + } + + @Test + void getCourtSchedulerHearingsShouldOmitIsDraftFromQueryParamsWhenIsDraftIsNull() { + final String courtCentreId = UUID.randomUUID().toString(); + final Optional courtSessionOptional = Optional.of("AD"); + final String courtRoomId = UUID.randomUUID().toString(); + final String startDate = LocalDate.now().toString(); + final String endDate = LocalDate.now().plusDays(7).toString(); + final Optional businessTypeOptional = Optional.of("BA123"); + final Integer pageSize = 50; + final Integer pageNumber = 1; + + final JsonObject hearingIdsResponse = givenPayload("/mock-data/azure.rotasl.getHearingIds.stub-data.json"); + + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(response.getEntity()).thenReturn(hearingIdsResponse); + when(hearingSlotsService.getCourtSchedulerHearingIds(anyMap())).thenReturn(response); + + courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), null, "ADULT,YOUTH", pageSize, pageNumber); + + final ArgumentCaptor> queryParamsCaptor = ArgumentCaptor.forClass(Map.class); + verify(hearingSlotsService).getCourtSchedulerHearingIds(queryParamsCaptor.capture()); + + assertFalse(queryParamsCaptor.getValue().containsKey("isDraft")); + } + @Test void shouldValidateSessionAvailability() { final JsonObject validateResponse = givenPayload("/mock-data/azure.rotasl.getHearingSlots.stub-data.json"); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java index 7dd830179..db62e9712 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java @@ -8,6 +8,8 @@ import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.STRING; import static uk.gov.moj.cpp.listing.it.SearchAvailableHearingIT.CASE_IN_HEARING; import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataWithAllocationDataAndJudiciaryWithCourtCenterForMagistrate; +import static uk.gov.moj.cpp.listing.steps.data.factory.HearingsDataFactory.PLEA_HEARING_TYPE; +import static uk.gov.moj.cpp.listing.steps.data.factory.HearingsDataFactory.TRIAL_HEARING_TYPE; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetHearingIdsWithBody; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; @@ -202,6 +204,119 @@ public void crownCourtSchedulerPathKeepsEachHearingDayDistinct() { isJson(withJsonPath("$.hearings[*].hearingDays[*].hearingDate", hasItem(dA.toString())))); } + /** + * PATH A / PATH B routing regression for SPRDT-1164. Two allocated multi-day CROWN hearings — one + * Trial, one Plea — are seeded in the same court centre over the same 3 consecutive working days. + * courtscheduler's get.hearing.ids is stubbed so a courtSession=AD query returns hearing #1's + * day-tuples and courtSession=AM returns hearing #2's — the AM/AD split is enforced purely by the + * stub, since courtscheduler itself has no notion of hearing type. Covers: + *
    + *
  • Any (no courtSession) — PATH B (listing viewstore), both hearings visible
  • + *
  • AM / AD — PATH A (courtscheduler), only the matching hearing's rows come back
  • + *
  • Any + hearingTypeId=Trial — PATH B's hearingTypeId filter narrows to hearing #1 only
  • + *
  • AM + hearingTypeId=Trial — PATH A returns hearing #2's rows, but listing's + * post-enrichment hearingTypeId filter removes them (hearing #2 is Plea, not Trial) — empty
  • + *
+ */ + @Test + public void shouldRouteCourtCalendarPathAByCourtSessionAndFilterByHearingType() { + final String jurisdictionType = JurisdictionType.CROWN.name(); + final UUID courtCentreId = getRandomCourtCenterId(); + final UUID courtRoomId = new ArrayList<>(COURT_ROOMS.keySet()).get(0); + final String ouCode = "C01CY00"; + + final LocalDate day0 = ItClock.nextWorkingDay(); + final LocalDate day1 = ItClock.plusWorkingDays(day0, 1); + final LocalDate day2 = ItClock.plusWorkingDays(day0, 2); + final List hearingDates = List.of(day0, day1, day2); + final ZonedDateTime hearingStartTime = day0.atTime(10, 0).atZone(ItClock.LONDON).withZoneSameInstant(ItClock.UTC); + + // HEARING #1 -- Trial, representing courtscheduler AD sessions + final UUID hearingId1 = randomUUID(); + final CaseAndDefendantData caseAndDefendantData1 = new CaseAndDefendantData(hearingId1, STRING.next(), STRING.next(), + randomUUID(), CASE_IN_HEARING, jurisdictionType, jurisdictionType, null, null); + new ListCourtHearingSteps(HearingsData.hearingsDataWithAllocationDataAndJudiciary( + caseAndDefendantData1, courtCentreId, courtRoomId, day2, hearingStartTime, TRIAL_HEARING_TYPE)) + .whenCaseIsSubmittedForListing(); + new UpdateHearingSteps().pollUntilHearingIsPresentWithHearingId(courtCentreId.toString(), ALLOCATED, getLoggedInUser().toString(), hearingId1.toString()); + + // HEARING #2 -- Plea, representing courtscheduler AM sessions + final UUID hearingId2 = randomUUID(); + final CaseAndDefendantData caseAndDefendantData2 = new CaseAndDefendantData(hearingId2, STRING.next(), STRING.next(), + randomUUID(), CASE_IN_HEARING, jurisdictionType, jurisdictionType, null, null); + new ListCourtHearingSteps(HearingsData.hearingsDataWithAllocationDataAndJudiciary( + caseAndDefendantData2, courtCentreId, courtRoomId, day2, hearingStartTime, PLEA_HEARING_TYPE)) + .whenCaseIsSubmittedForListing(); + new UpdateHearingSteps().pollUntilHearingIsPresentWithHearingId(courtCentreId.toString(), ALLOCATED, getLoggedInUser().toString(), hearingId2.toString()); + + // PATH A stubs: courtSession=AD -> hearing #1's day-tuples, courtSession=AM -> hearing #2's + stubGetHearingIdsWithBody("AD", hearingIdsBodyForDays(hearingId1, hearingDates)); + stubGetHearingIdsWithBody("AM", hearingIdsBodyForDays(hearingId2, hearingDates)); + + final String baseUrl = String.format("%s/listing-query-api/query/api/rest/listing/hearings/range-search" + + "?allocated=true&jurisdictionType=CROWN&courtCentreId=%s&ouCode=%s" + + "&startDate=%s&endDate=%s&pageSize=50&pageNumber=1", + getBaseUri(), courtCentreId, ouCode, day0, day2); + + // 3. Any (no courtSession) -> PATH B, both hearings present + pollWithDefaults(courtCalendarRequest(baseUrl)).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearings[*].id", hasItem(hearingId1.toString())), + withJsonPath("$.hearings[*].id", hasItem(hearingId2.toString())) + ))); + + // 4. AM -> PATH A, only hearing #2 + pollWithDefaults(courtCalendarRequest(baseUrl + "&courtSession=AM")).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearings[*].id", Matchers.everyItem(is(hearingId2.toString()))), + withJsonPath("$.hearings[0].id", is(hearingId2.toString())) + ))); + + // 5. AD -> PATH A, only hearing #1 + pollWithDefaults(courtCalendarRequest(baseUrl + "&courtSession=AD")).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearings[*].id", Matchers.everyItem(is(hearingId1.toString()))), + withJsonPath("$.hearings[0].id", is(hearingId1.toString())) + ))); + + // 6. Any + hearingTypeId=Trial -> PATH B's viewstore type filter -> only hearing #1 + pollWithDefaults(courtCalendarRequest(baseUrl + "&hearingTypeId=" + TRIAL_HEARING_TYPE.getTypeId())).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearings[*].id", hasItem(hearingId1.toString())), + withJsonPath("$.hearings[*].id", Matchers.not(hasItem(hearingId2.toString()))) + ))); + + // 7. AM + hearingTypeId=Trial -> PATH A returns hearing #2's rows, but the post-enrichment + // hearingTypeId filter removes them (hearing #2 is Plea, not Trial) -> empty + pollWithDefaults(courtCalendarRequest(baseUrl + "&courtSession=AM&hearingTypeId=" + TRIAL_HEARING_TYPE.getTypeId())).until(status().is(OK), + payload().isJson(withJsonPath("$.hearings.size()", is(0)))); + } + + private static RequestParams courtCalendarRequest(final String url) { + return requestParams(url, "application/vnd.listing.search.hearings.court.calendar+json") + .withHeader(CPP_UID_HEADER.getName(), CPP_UID_HEADER.getValue()) + .build(); + } + + /** Builds a get.hearing.ids stub body with one IdResponse tuple per supplied date, all sharing the + * same hearingId (mirrors how courtscheduler represents a single multi-day hearing). */ + private static String hearingIdsBodyForDays(final UUID hearingId, final List hearingDates) { + final StringBuilder body = new StringBuilder("{\"results\":").append(hearingDates.size()) + .append(",\"pageCount\":1,\"hearingIds\":["); + for (int i = 0; i < hearingDates.size(); i++) { + if (i > 0) { + body.append(","); + } + body.append("{\"hearingId\":\"").append(hearingId).append("\",") + .append("\"courtScheduleId\":\"").append(randomUUID()).append("\",") + .append("\"hearingDate\":\"").append(hearingDates.get(i)).append("\",") + .append("\"hearingDayCount\":").append(hearingDates.size()).append(",") + .append("\"hearingDayPosition\":").append(i + 1).append("}"); + } + body.append("]}"); + return body.toString(); + } + private static void checkPayload(final String payload, final int hearingCount, List testDataList, final int pageSize, final int pageNumber) throws JsonProcessingException { final Map jObj = new ObjectMapper().readValue(payload, Map.class); assertThat(jObj, Matchers.is(notNullValue())); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java index a8645c6af..61d9ec872 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java @@ -207,6 +207,17 @@ public static HearingsData hearingsDataWithAllocationDataAndJudiciary(final Case return new HearingsData(HearingsDataFactory.hearingsDataWithAllocationDataAndJudiciaryWithDate(caseAndDefendantData, courtCentreId, courtRoomId, hearingEndDate, hearingStartTime)); } + /** SPRDT-1164: variant that pins the seeded hearing's hearing type (e.g. Trial/Plea), so the + * viewstore type_id can be asserted on directly by court-calendar hearingTypeId-filter tests. */ + public static HearingsData hearingsDataWithAllocationDataAndJudiciary(final CaseAndDefendantData caseAndDefendantData, + final UUID courtCentreId, + final UUID courtRoomId, + final LocalDate hearingEndDate, + final ZonedDateTime hearingStartTime, + final HearingTypeData hearingTypeData) { + return new HearingsData(HearingsDataFactory.hearingsDataWithAllocationDataAndJudiciaryWithDate(caseAndDefendantData, courtCentreId, courtRoomId, hearingEndDate, hearingStartTime, hearingTypeData)); + } + public static HearingsData hearingsDataWithUnAllocationDataAndJudiciary(final CaseAndDefendantData caseAndDefendantData) { return new HearingsData(HearingsDataFactory.hearingsDataWithUnAllocationDataAndJudiciary(caseAndDefendantData)); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java index 5442cc27e..d1fde0d7e 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java @@ -56,7 +56,11 @@ public class HearingsDataFactory { private static final int HEARING_ESTIMATE_MINUTES = 30; public static final String ESTIMATED_DURATION = "1 week"; private static final HearingTypeData PTP_HEARING_TYPE = new HearingTypeData(UUID.fromString("52edf232-3c09-4c74-a6ad-737985c2e662"), "PTP", "welshPTP"); - private static final HearingTypeData TRIAL_HEARING_TYPE = new HearingTypeData(UUID.fromString("bf8155e1-90b9-4080-b133-bfbad895d6e4"), "Trial", "welsh trial"); + public static final HearingTypeData TRIAL_HEARING_TYPE = new HearingTypeData(UUID.fromString("bf8155e1-90b9-4080-b133-bfbad895d6e4"), "Trial", "welsh trial"); + /** SPRDT-1164: second, distinct hearing type used to discriminate the court-calendar PATH A + * courtSession=AM/AD scenarios from the courtSession=AD/Trial hearing via the listing-side + * hearingTypeId viewstore filter (courtscheduler itself has no notion of hearing type). */ + public static final HearingTypeData PLEA_HEARING_TYPE = new HearingTypeData(UUID.fromString("eab66a6c-bbcf-4dc9-8291-36def87bcfd5"), "Plea", "welsh plea"); private static final BailStatus BAIL_CONDITIONAL = new BailStatus.Builder().withId(fromString("34443c87-fa6f-34c0-897f-0cce45773df5")).withCode("P").withDescription("Custody or remanded into custody").build(); public static final ZonedDateTime SLOT_START_TIME = ItClock.nowUtc(); @@ -259,6 +263,18 @@ public static List hearingsDataWithAllocationDataAndJudiciaryWithDa return manyRandomHearingsWithAllocationDataWithDate(1, caseAndDefendantData, courtCentreId, courtRoomId, hearingEndDate, hearingStartTime); } + /** SPRDT-1164: variant that lets the caller pin the hearing type (e.g. Trial/Plea) so the + * seeded hearing's viewstore type_id can be asserted on by the court-calendar PATH B hearingTypeId + * filter and used to discriminate PATH A courtSession stub responses. */ + public static List hearingsDataWithAllocationDataAndJudiciaryWithDate(final CaseAndDefendantData caseAndDefendantData, + final UUID courtCentreId, + final UUID courtRoomId, + final LocalDate hearingEndDate, + final ZonedDateTime hearingStartTime, + final HearingTypeData hearingTypeData) { + return manyRandomHearingsWithAllocationDataWithDate(1, caseAndDefendantData, courtCentreId, courtRoomId, hearingEndDate, hearingStartTime, hearingTypeData); + } + public static List hearingsDataWithUnAllocationDataAndJudiciary(final CaseAndDefendantData caseAndDefendantData) { return manyRandomHearingsWithUnAllocationData(2, caseAndDefendantData); } @@ -423,6 +439,19 @@ private static List manyRandomHearingsWithAllocationDataWithDate(fi .collect(toList()); } + private static List manyRandomHearingsWithAllocationDataWithDate(final Integer numberOfHearings, + final CaseAndDefendantData caseAndDefendantData, + final UUID courtCentreId, + final UUID courtRoomId, + final LocalDate hearingEndDate, + final ZonedDateTime hearingStartTime, + final HearingTypeData hearingTypeData) { + + return IntStream.range(0, numberOfHearings) + .mapToObj((int i) -> randomHearing(hearingStartTime, hearingEndDate, courtCentreId, courtRoomId, singletonList(randomJudicialRole()), caseAndDefendantData, hearingTypeData)) + .collect(toList()); + } + private static List manyRandomHearingsWithUnAllocationData(final Integer numberOfHearings, final CaseAndDefendantData caseAndDefendantData) { final LocalDate hearingEndDate = ItClock.today().plusDays(1); @@ -1416,6 +1445,22 @@ private static HearingData randomHearing(final ZonedDateTime hearingStartTime, singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); } + /** SPRDT-1164: same as above but with a caller-supplied hearing type, so the seeded hearing's + * viewstore type_id can be pinned to a specific value (e.g. Trial vs Plea). */ + private static HearingData randomHearing(final ZonedDateTime hearingStartTime, + final LocalDate hearingEndDate, + final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, + final CaseAndDefendantData caseAndDefendantData, final HearingTypeData hearingTypeData) { + final List listedCaseData = manyRandomListingCases(1, caseAndDefendantData); + final LocalDate hearingStartDate = hearingStartTime.toLocalDate(); + return new HearingData(caseAndDefendantData.getHearingId(), courtCentreId, hearingTypeData, hearingStartDate, + hearingEndDate, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, + courtRoomId, hearingStartTime, listedCaseData, + judicialRoles, caseAndDefendantData.getJurisdictionType(), STRING.next(), + singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); + } + private static HearingData randomHearingWithHearingDate(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final String jurisdictionType, final ZonedDateTime hearingStartTime, diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java index 81f88b569..e6b326700 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java @@ -583,6 +583,23 @@ public static void stubGetHearingIdsWithBody(final String body) { )); } + /** + * SPRDT-1164: stub the courtscheduler get-hearing-ids endpoint scoped to a specific courtSession + * (AM/AD), so two different courtSession queries in the same test can be driven to two different + * bodies (e.g. one hearing's day-tuples for AD, another hearing's for AM). isDraft and pageSize are + * left unconstrained so this still matches production's court-calendar allocated path, which now + * sends isDraft=false. + */ + public static void stubGetHearingIdsWithBody(final String courtSession, final String body) { + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + HEARING_SLOTS))) + .withHeader("Accept", containing("application/vnd.courtscheduler.get.hearing.ids+json")) + .withQueryParam("courtSession", WireMock.equalTo(courtSession)) + .willReturn(aResponse().withStatus(OK.getStatusCode()) + .withBody(body) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + )); + } + public static void stubGetHearingIds(final Instant exactHearingStartDateTime) { stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + HEARING_SLOTS))) .withQueryParam("sessionStartDate", matching("1900-01-01")) diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java index 8a8e57be3..7ea7abf19 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java @@ -124,7 +124,7 @@ public JsonEnvelope rangeSearchHearings(final JsonEnvelope query) { if (params.courtSessionOptional().isPresent() || params.businessType().isPresent()) { if(isMags(params.jurisdictionType()) && params.allocated() && params.ouCode() != null ) { - return getCourtSchedulerHearings(query, params.allocated(), params.ouCode(), params.courtSessionOptional(), params.courtRoomId(), params.startDate(), params.endDate(), params.exactHearingStartDateTime(), params.businessType(), Optional.ofNullable(params.jurisdictionType()), PANEL_ADULT_YOUTH, params.paginationParameter()); + return getCourtSchedulerHearings(query, params.allocated(), params.ouCode(), params.courtSessionOptional(), params.courtRoomId(), params.startDate(), params.endDate(), params.exactHearingStartDateTime(), params.businessType(), Optional.ofNullable(params.jurisdictionType()), null, null, PANEL_ADULT_YOUTH, params.paginationParameter()); } throw new BadRequestException("courtSession or businessType are only relevant to allocated MAGs with ouCode"); @@ -310,36 +310,36 @@ private List findHearings(final boolean allocated, final String courtCe } - private JsonEnvelope getCourtSchedulerHearings(final JsonEnvelope query, final boolean allocated, final String ouCode, final Optional courtSessionOptional, final String courtRoomId, final String startDate, final String endDate, final Optional startDateTime, final Optional businessType, final Optional jurisdiction, final String panel, final PaginationParameter paginationParameter) { - final HearingIdsResponse hearingIdsResponse = courtSchedulerServiceAdapter.getCourtSchedulerHearings(ouCode, courtSessionOptional, courtRoomId, startDate, endDate, startDateTime, businessType, jurisdiction, panel, paginationParameter.getPageSize(), paginationParameter.getPageNumber()); + private JsonEnvelope getCourtSchedulerHearings(final JsonEnvelope query, final boolean allocated, final String ouCode, final Optional courtSessionOptional, final String courtRoomId, final String startDate, final String endDate, final Optional startDateTime, final Optional businessType, final Optional jurisdiction, final Boolean isDraft, final String hearingTypeId, final String panel, final PaginationParameter paginationParameter) { + final HearingIdsResponse hearingIdsResponse = courtSchedulerServiceAdapter.getCourtSchedulerHearings(ouCode, courtSessionOptional, courtRoomId, startDate, endDate, startDateTime, businessType, jurisdiction, isDraft, panel, paginationParameter.getPageSize(), paginationParameter.getPageNumber()); logger.info("CourtScheduler Hearings response : {}", hearingIdsResponse); + // hearingType is held only in the listing viewstore (not courtscheduler), so it is filtered + // here during enrichment. Draft exclusion is applied courtscheduler-side via the isDraft flag, + // so no allocated/draft filter is applied here. Pagination and the result count are left + // exactly as before: the caller's pageSize is forwarded to courtscheduler and its results + // count is passed straight through. final List enrichedHearingList = isEmpty(hearingIdsResponse.getUuids()) - ? emptyList() : enrichAllCourtSchedulerHearingIdsIntoHearings(hearingIdsResponse.getUuids()); + ? emptyList() : enrichAllCourtSchedulerHearingIdsIntoHearings(hearingIdsResponse.getUuids(), hearingTypeId); logger.info("getCourtSchedulerHearings found {} hearings", hearingIdsResponse.getResults()); - // When the whole result set fits on one page, the rows we actually return (allocated, - // de-duplicated per hearing-day) ARE the true total, so report that instead of the raw - // courtscheduler count which still includes draft sessions. For genuinely paged results we - // cannot derive the allocated total from a single page, so keep the courtscheduler total - // (draft exclusion for the multi-page count is a courtscheduler-side follow-up). - final long courtSchedulerTotal = hearingIdsResponse.getResults(); - final long results = courtSchedulerTotal <= paginationParameter.getPageSize() - ? enrichedHearingList.size() : courtSchedulerTotal; - return buildHearingsResponse(query, allocated, courtRoomId, startDate, enrichedHearingList, results, hearingIdsResponse, paginationParameter); + return buildHearingsResponse(query, allocated, courtRoomId, startDate, enrichedHearingList, hearingIdsResponse.getResults(), hearingIdsResponse, paginationParameter); } - private List enrichAllCourtSchedulerHearingIdsIntoHearings(final List hearingIds) { + private List enrichAllCourtSchedulerHearingIdsIntoHearings(final List hearingIds, final String hearingTypeId) { - final List hearings = repository.findAllCourtSchedulerHearingByIds(hearingIds.stream().map(a -> a.hearingId()).toList()); + // De-duplicate hearing ids before hitting the listing viewstore: a multi-day hearing arrives + // as one IdResponse per day, all sharing the same hearingId. + final List distinctHearingIds = hearingIds.stream().map(IdResponse::hearingId).distinct().toList(); + final List hearings = repository.findAllCourtSchedulerHearingByIds(distinctHearingIds); final HashMap hearingsById = new HashMap<>(); hearings.forEach(h -> hearingsById.put(h.getId(), h)); final List enrichedHearings = new ArrayList<>(); hearingIds.forEach(id -> { final Hearing base = hearingsById.get(id.hearingId()); - // The court-calendar allocated view must show confirmed-allocated hearings only; the - // courtscheduler id list can include draft (unallocated) sessions, so skip those. - if (base != null && Boolean.TRUE.equals(base.getAllocated())) { + // hearingType is not known to courtscheduler, so it is filtered listing-side against the + // viewstore. Draft/allocated filtering is handled courtscheduler-side (the isDraft flag). + if (base != null && matchesHearingType(base, hearingTypeId)) { // Give each hearing-day its own Hearing (deep-copied properties) so a multi-day // hearing renders one distinct row per day rather than collapsing onto the last day. final Hearing perDay = new Hearing(base); @@ -353,6 +353,11 @@ private List enrichAllCourtSchedulerHearingIdsIntoHearings(final List hearings = hearingsJson(ALLOCATEDSTR); + // hearingTypeId is supplied on the query below and is now enforced listing-side against + // the viewstore typeId (matchesHearingType), so the mocked hearings must carry a matching + // typeId or they would be filtered out of the enriched result. + hearings.forEach(hearing -> hearing.setTypeId(HEARING_TYPE_ID)); final List hearingIds = new ArrayList<>(); hearings.forEach(hearing -> hearingIds.add(new IdResponse(hearing.getId(), UUID.randomUUID(), LocalDate.now(), 1,1))); @@ -280,6 +285,7 @@ public void searchMagistratesHearings() { Optional.empty(), Optional.of(BUSINESS_TYPE), Optional.of(MAGISTRATES_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 50, 1)).thenReturn(response); when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(hearings); @@ -765,6 +771,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndBusinessTypeShouldUse Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1 @@ -800,6 +807,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndBusinessTypeShouldUse Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1); @@ -821,6 +829,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndHearingTypeIdShouldPa Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1 @@ -857,6 +866,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndHearingTypeIdShouldPa Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1); @@ -878,7 +888,7 @@ void rangeSearchCourtCalendarShouldSplitMultiDayCrownHearingIntoDistinctPerDayRo final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( - any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(multiDay)); final JsonEnvelope query = envelopeFrom( @@ -910,25 +920,24 @@ void rangeSearchCourtCalendarShouldSplitMultiDayCrownHearingIntoDistinctPerDayRo @Test void rangeSearchCourtCalendarShouldExcludeDraftUnallocatedHearingsFromAllocatedView() { + // Draft (unallocated) exclusion moved from listing-side enrich to the courtscheduler call + // itself: the allocated court-calendar view now requests isDraft=false so drafts never + // come back in the hearingId list to begin with. This test pins that contract at the + // adapter-invocation boundary rather than asserting listing-side enrich drops anything + // (enrich no longer filters on allocated at all - see matchesHearingType). final UUID allocatedId = randomUUID(); - final UUID draftId = randomUUID(); final String allocatedProps = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; - final String draftProps = "{ \"allocated\":\"false\", \"startDate\": \"2026-07-02\", \"courtApplications\": [{}], " - + "\"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; final Hearing allocated = new Hearing(allocatedId, JacksonUtil.toJsonNode(allocatedProps)); allocated.setAllocated(true); - final Hearing draft = new Hearing(draftId, JacksonUtil.toJsonNode(draftProps)); - draft.setAllocated(false); final List hearingIds = new ArrayList<>(); hearingIds.add(new IdResponse(allocatedId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); - hearingIds.add(new IdResponse(draftId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( - any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); - when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(allocated, draft)); + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(allocated)); final JsonEnvelope query = envelopeFrom( metadataBuilder().withId(randomUUID()).withName("event.name"), @@ -944,16 +953,199 @@ void rangeSearchCourtCalendarShouldExcludeDraftUnallocatedHearingsFromAllocatedV .add(PAGE_NUMBER, 1) .build()); + rangeSearchQuery.rangeSearchCourtCalendar(query); + + verify(courtSchedulerServiceAdapter).getCourtSchedulerHearings( + eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq(Boolean.FALSE), any(), any(), any()); + } + + @Test + void rangeSearchCourtCalendarAllocatedShouldRequestIsDraftFalseFromCourtScheduler() { + final List mockHearings = hearingsJson(ALLOCATEDSTR); + final List hearingIds = new ArrayList<>(); + mockHearings.forEach(h -> hearingIds.add(new IdResponse(h.getId(), UUID.randomUUID(), LocalDate.now(), 1, 1))); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, mockHearings.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(mockHearings); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(START_DATE_QUERY_PARAMETER, SEARCH_DATE.toString()) + .add(END_DATE_QUERY_PARAMETER, SEARCH_DATE.toString()) + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + rangeSearchQuery.rangeSearchCourtCalendar(query); + + // allocated=true on the court-calendar view must always ask courtscheduler to exclude drafts. + verify(courtSchedulerServiceAdapter).getCourtSchedulerHearings( + eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq(Boolean.FALSE), any(), any(), any()); + } + + @Test + void rangeSearchCourtCalendarWithMatchingHearingTypeIdShouldReturnHearing() { + final UUID hearingId = randomUUID(); + final String props = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; + final Hearing hearing = new Hearing(hearingId, JacksonUtil.toJsonNode(props)); + hearing.setAllocated(true); + hearing.setTypeId(HEARING_TYPE_ID); + + final List hearingIds = List.of(new IdResponse(hearingId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(hearing)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(HEARING_TYPE_QUERY_PARAMETER, HEARING_TYPE_ID.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); - final JsonArray hearings = payload.getJsonArray("hearings"); - assertThat(hearings.size(), is(1)); // draft (allocated=false) must not appear on the allocated view - for (int i = 0; i < hearings.size(); i++) { - assertThat(hearings.getJsonObject(i).getString("allocated"), is("true")); - } + assertThat(payload.getJsonArray("hearings").size(), is(1)); assertThat(payload.getInt("results"), is(1)); } + @Test + void rangeSearchCourtCalendarWithNonMatchingHearingTypeIdShouldFilterOutHearing() { + final UUID hearingId = randomUUID(); + final String props = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; + final Hearing hearing = new Hearing(hearingId, JacksonUtil.toJsonNode(props)); + hearing.setAllocated(true); + hearing.setTypeId(randomUUID()); // deliberately different from the queried hearingTypeId + + final List hearingIds = List.of(new IdResponse(hearingId, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(hearing)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(HEARING_TYPE_QUERY_PARAMETER, HEARING_TYPE_ID.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); + + assertThat(payload.getJsonArray("hearings").size(), is(0)); + assertThat(payload.getInt("results"), is(1)); // courtscheduler's raw count is unfiltered; only the enriched rows drop the mismatch + } + + @Test + void rangeSearchCourtCalendarWithNoHearingTypeIdShouldNotFilterByType() { + final UUID hearingId1 = randomUUID(); + final UUID hearingId2 = randomUUID(); + final String props = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [{\"hearingDate\": \"2026-07-14\"}] }"; + final Hearing hearing1 = new Hearing(hearingId1, JacksonUtil.toJsonNode(props)); + hearing1.setAllocated(true); + hearing1.setTypeId(HEARING_TYPE_ID); + final Hearing hearing2 = new Hearing(hearingId2, JacksonUtil.toJsonNode(props)); + hearing2.setAllocated(true); + hearing2.setTypeId(randomUUID()); + + final List hearingIds = List.of( + new IdResponse(hearingId1, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1), + new IdResponse(hearingId2, randomUUID(), LocalDate.parse("2026-07-14"), 1, 1)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(hearing1, hearing2)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); + + assertThat(payload.getJsonArray("hearings").size(), is(2)); + } + + @Test + void rangeSearchCourtCalendarShouldDedupeMultiDayHearingIdsBeforeFetchingFromViewstore() { + final UUID multiDayId = randomUUID(); + final String props = "{ \"allocated\":\"true\", \"startDate\": \"2026-07-14\", \"courtRoomId\": \"" + COURT_ROOM_ID + + "\", \"courtApplications\": [{}], \"listedCases\": [{}], \"hearingDays\": [" + + "{\"hearingDate\": \"2026-07-14\"}, {\"hearingDate\": \"2026-07-15\"}] }"; + final Hearing multiDay = new Hearing(multiDayId, JacksonUtil.toJsonNode(props)); + multiDay.setAllocated(true); + + final List hearingIds = new ArrayList<>(); + hearingIds.add(new IdResponse(multiDayId, randomUUID(), LocalDate.parse("2026-07-14"), 2, 1)); + hearingIds.add(new IdResponse(multiDayId, randomUUID(), LocalDate.parse("2026-07-15"), 2, 2)); + final HearingIdsResponse response = new HearingIdsResponse(hearingIds, hearingIds.size(), 1); + + when(courtSchedulerServiceAdapter.getCourtSchedulerHearings( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(response); + when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(newArrayList(multiDay)); + + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("event.name"), + createObjectBuilder() + .add(ALLOCATED_QUERY_PARAMETER, true) + .add(COURT_SESSION_QUERY_PARAMETER, "AD") + .add(COURT_CENTRE_QUERY_PARAMETER, COURT_CENTRE_ID.toString()) + .add(JURISDICTION_TYPE_QUERY_PARAMETER, JURISDICTION_TYPE.toString()) + .add(START_DATE_QUERY_PARAMETER, "2026-07-14") + .add(END_DATE_QUERY_PARAMETER, "2026-07-17") + .add(OU_CODE_QUERY_PARAMETER, "C01CY00") + .add(PAGE_SIZE, 40) + .add(PAGE_NUMBER, 1) + .build()); + + final JsonObject payload = rangeSearchQuery.rangeSearchCourtCalendar(query).payloadAsJsonObject(); + + // 2 IdResponses share the same hearingId (a 2-day multiday hearing) -> the repository must + // be asked for only 1 distinct id, while the response still carries one row per day. + verify(hearingRepository).findAllCourtSchedulerHearingByIds(argThat(ids -> ids.size() == 1)); + assertThat(payload.getJsonArray("hearings").size(), is(2)); + } + // ----------------------------------------------------------------------- // rangeSearchCourtCalendar – missing branch coverage // ----------------------------------------------------------------------- @@ -974,6 +1166,7 @@ void rangeSearchCourtCalendarWithCrownAndBusinessTypeShouldUseCourtScheduler() { Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1 @@ -1010,6 +1203,7 @@ void rangeSearchCourtCalendarWithCrownAndBusinessTypeShouldUseCourtScheduler() { Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), + Boolean.FALSE, "ADULT,YOUTH", 40, 1); @@ -1296,6 +1490,10 @@ void rangeSearchHearingsWithMagsAllocatedOuCodeCourtSessionAndBusinessTypeShould Optional.empty(), Optional.of(BUSINESS_TYPE), Optional.of(MAGISTRATES_TYPE.toString()), + // The plain (non-calendar) range-search path always passes isDraft=null - draft + // exclusion is a court-calendar-only concern - so the adapter stub must match null + // here, not Boolean.FALSE. + null, "ADULT,YOUTH", 50, 1) ).thenReturn(response); when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(hearings); From 83b00802cd7ce8b4fb765f5da884f8e5fae786a3 Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Wed, 15 Jul 2026 02:17:34 +0100 Subject: [PATCH 4/5] refactor(ccsph2n): use courtscheduler's existing `status` param for draft filter, drop duplicate isDraft [SPRDT-1164] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The court-calendar PATH A draft filter now rides the courtscheduler get.hearing.ids endpoint's pre-existing `status` param (DRAFT/FINAL/ALL) instead of a dedicated isDraft flag: PATH A sends status=FINAL when allocated=true. CourtSchedulerServiceAdapter.getCourtSchedulerHearings takes a nullable `status` (added to the query only when non-null); the pre-existing IS_DRAFT constant is left untouched (used by unrelated wire fields). hearingType (listing-side) filter, dedupe, per-day copy and main-parity pagination/count are unchanged. Behaviourally identical to the previous isDraft=false (FINAL == non-draft); removes duplicate API surface. Tests: CourtSchedulerServiceAdapterTest 24/24 (asserts status=FINAL on the query), RangeSearchQueryTest 38/38. Court-calendar IT unchanged (stubs on courtSession) — full listing IT suite still 258/0. --- .../service/CourtSchedulerServiceAdapter.java | 11 +++++---- .../CourtSchedulerServiceAdapterTest.java | 12 +++++----- .../listing/query/view/RangeSearchQuery.java | 12 +++++----- .../query/view/RangeSearchQueryTest.java | 24 +++++++++---------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java index dc05ec3be..9f70a0c75 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java @@ -58,6 +58,7 @@ public class CourtSchedulerServiceAdapter { public static final String COURT_SESSION = "courtSession"; public static final String BUSINESS_TYPE = "businessType"; public static final String JURISDICTION = "jurisdiction"; + public static final String STATUS = "status"; public static final String PANEL_ADULT_YOUTH = "ADULT,YOUTH"; private static final String PANEL = "panel"; public static final String HEARING_ID = "hearingId"; @@ -438,7 +439,7 @@ public HearingIdsResponse getCourtSchedulerHearings(final String ouCode, final String endDate, final Optional exactHearingStartDateTime, final Optional businessTypeOptional, final Optional jurisdiction, - final Boolean isDraft, + final String status, final String panel, final Integer pageSize, final Integer pageNumber ) { final Map queryParams = new HashMap<>(); @@ -453,10 +454,10 @@ public HearingIdsResponse getCourtSchedulerHearings(final String ouCode, businessTypeOptional.ifPresent(businessType -> queryParams.put(BUSINESS_TYPE, businessType)); jurisdiction.ifPresent(j -> queryParams.put(JURISDICTION, j)); exactHearingStartDateTime.ifPresent(s -> queryParams.put(EXACT_HEARING_START_DATETIME, s.toString())); - // Draft (unallocated) court-schedule sessions are excluded at source when isDraft is supplied; - // a null isDraft leaves the courtscheduler result unfiltered (returns ALL). - if (isDraft != null) { - queryParams.put(IS_DRAFT, isDraft.toString()); + // Draft (unallocated) court-schedule sessions are excluded at source when status="FINAL" is + // supplied; a null status leaves the courtscheduler result unfiltered (returns ALL). + if (status != null) { + queryParams.put(STATUS, status); } final Response hearingsResponse = getCourtSchedulerHearingIds(queryParams); diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java index 9201c3383..6055057a1 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java @@ -219,7 +219,7 @@ void shouldGetHearingIds() { when(response.getEntity()).thenReturn(hearingIdsResponse); when(hearingSlotsService.getCourtSchedulerHearingIds(anyMap())).thenReturn(response); - final HearingIdsResponse finalResp = courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), Boolean.FALSE, "ADULT,YOUTH", pageSize, pageNumber); + final HearingIdsResponse finalResp = courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), "FINAL", "ADULT,YOUTH", pageSize, pageNumber); assertThat(finalResp.getUuids().size(), is(4)); assertThat(finalResp.getPageCount(), is(1L)); @@ -227,7 +227,7 @@ void shouldGetHearingIds() { } @Test - void getCourtSchedulerHearingsShouldAddIsDraftFalseToQueryParamsWhenIsDraftIsFalse() { + void getCourtSchedulerHearingsShouldAddStatusFinalToQueryParamsWhenStatusIsFinal() { final String courtCentreId = UUID.randomUUID().toString(); final Optional courtSessionOptional = Optional.of("AD"); final String courtRoomId = UUID.randomUUID().toString(); @@ -243,16 +243,16 @@ void getCourtSchedulerHearingsShouldAddIsDraftFalseToQueryParamsWhenIsDraftIsFal when(response.getEntity()).thenReturn(hearingIdsResponse); when(hearingSlotsService.getCourtSchedulerHearingIds(anyMap())).thenReturn(response); - courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), Boolean.FALSE, "ADULT,YOUTH", pageSize, pageNumber); + courtSchedulerServiceAdapter.getCourtSchedulerHearings(courtCentreId, courtSessionOptional, courtRoomId, startDate, endDate, Optional.of(Instant.now()), businessTypeOptional, Optional.of("MAGISTRATES"), "FINAL", "ADULT,YOUTH", pageSize, pageNumber); final ArgumentCaptor> queryParamsCaptor = ArgumentCaptor.forClass(Map.class); verify(hearingSlotsService).getCourtSchedulerHearingIds(queryParamsCaptor.capture()); - assertThat(queryParamsCaptor.getValue().get("isDraft"), is("false")); + assertThat(queryParamsCaptor.getValue().get("status"), is("FINAL")); } @Test - void getCourtSchedulerHearingsShouldOmitIsDraftFromQueryParamsWhenIsDraftIsNull() { + void getCourtSchedulerHearingsShouldOmitStatusFromQueryParamsWhenStatusIsNull() { final String courtCentreId = UUID.randomUUID().toString(); final Optional courtSessionOptional = Optional.of("AD"); final String courtRoomId = UUID.randomUUID().toString(); @@ -273,7 +273,7 @@ void getCourtSchedulerHearingsShouldOmitIsDraftFromQueryParamsWhenIsDraftIsNull( final ArgumentCaptor> queryParamsCaptor = ArgumentCaptor.forClass(Map.class); verify(hearingSlotsService).getCourtSchedulerHearingIds(queryParamsCaptor.capture()); - assertFalse(queryParamsCaptor.getValue().containsKey("isDraft")); + assertFalse(queryParamsCaptor.getValue().containsKey("status")); } @Test diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java index 7ea7abf19..39b952638 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java @@ -310,13 +310,13 @@ private List findHearings(final boolean allocated, final String courtCe } - private JsonEnvelope getCourtSchedulerHearings(final JsonEnvelope query, final boolean allocated, final String ouCode, final Optional courtSessionOptional, final String courtRoomId, final String startDate, final String endDate, final Optional startDateTime, final Optional businessType, final Optional jurisdiction, final Boolean isDraft, final String hearingTypeId, final String panel, final PaginationParameter paginationParameter) { - final HearingIdsResponse hearingIdsResponse = courtSchedulerServiceAdapter.getCourtSchedulerHearings(ouCode, courtSessionOptional, courtRoomId, startDate, endDate, startDateTime, businessType, jurisdiction, isDraft, panel, paginationParameter.getPageSize(), paginationParameter.getPageNumber()); + private JsonEnvelope getCourtSchedulerHearings(final JsonEnvelope query, final boolean allocated, final String ouCode, final Optional courtSessionOptional, final String courtRoomId, final String startDate, final String endDate, final Optional startDateTime, final Optional businessType, final Optional jurisdiction, final String status, final String hearingTypeId, final String panel, final PaginationParameter paginationParameter) { + final HearingIdsResponse hearingIdsResponse = courtSchedulerServiceAdapter.getCourtSchedulerHearings(ouCode, courtSessionOptional, courtRoomId, startDate, endDate, startDateTime, businessType, jurisdiction, status, panel, paginationParameter.getPageSize(), paginationParameter.getPageNumber()); logger.info("CourtScheduler Hearings response : {}", hearingIdsResponse); // hearingType is held only in the listing viewstore (not courtscheduler), so it is filtered - // here during enrichment. Draft exclusion is applied courtscheduler-side via the isDraft flag, - // so no allocated/draft filter is applied here. Pagination and the result count are left - // exactly as before: the caller's pageSize is forwarded to courtscheduler and its results + // here during enrichment. Draft exclusion is applied courtscheduler-side via the status=FINAL + // filter, so no allocated/draft filter is applied here. Pagination and the result count are + // left exactly as before: the caller's pageSize is forwarded to courtscheduler and its results // count is passed straight through. final List enrichedHearingList = isEmpty(hearingIdsResponse.getUuids()) ? emptyList() : enrichAllCourtSchedulerHearingIdsIntoHearings(hearingIdsResponse.getUuids(), hearingTypeId); @@ -376,7 +376,7 @@ public JsonEnvelope rangeSearchCourtCalendar(final JsonEnvelope query) { if (params.courtSessionOptional().isPresent() || params.businessType().isPresent()) { if(params.allocated() && params.ouCode() != null ) { - return getCourtSchedulerHearings(query, params.allocated(), params.ouCode(), params.courtSessionOptional(), params.courtRoomId(), params.startDate(), params.endDate(), params.exactHearingStartDateTime(), params.businessType(), Optional.ofNullable(params.jurisdictionType()), params.allocated() ? Boolean.FALSE : null, params.hearingTypeId(), PANEL_ADULT_YOUTH, params.paginationParameter()); + return getCourtSchedulerHearings(query, params.allocated(), params.ouCode(), params.courtSessionOptional(), params.courtRoomId(), params.startDate(), params.endDate(), params.exactHearingStartDateTime(), params.businessType(), Optional.ofNullable(params.jurisdictionType()), params.allocated() ? "FINAL" : null, params.hearingTypeId(), PANEL_ADULT_YOUTH, params.paginationParameter()); } } diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java index c800f6931..36ead3316 100644 --- a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java @@ -285,7 +285,7 @@ public void searchMagistratesHearings() { Optional.empty(), Optional.of(BUSINESS_TYPE), Optional.of(MAGISTRATES_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 50, 1)).thenReturn(response); when(hearingRepository.findAllCourtSchedulerHearingByIds(anyList())).thenReturn(hearings); @@ -771,7 +771,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndBusinessTypeShouldUse Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1 @@ -807,7 +807,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndBusinessTypeShouldUse Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1); @@ -829,7 +829,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndHearingTypeIdShouldPa Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1 @@ -866,7 +866,7 @@ void rangeSearchCourtCalendarWithCrownAllocatedAndOuCodeAndHearingTypeIdShouldPa Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1); @@ -921,7 +921,7 @@ void rangeSearchCourtCalendarShouldSplitMultiDayCrownHearingIntoDistinctPerDayRo @Test void rangeSearchCourtCalendarShouldExcludeDraftUnallocatedHearingsFromAllocatedView() { // Draft (unallocated) exclusion moved from listing-side enrich to the courtscheduler call - // itself: the allocated court-calendar view now requests isDraft=false so drafts never + // itself: the allocated court-calendar view now requests status=FINAL so drafts never // come back in the hearingId list to begin with. This test pins that contract at the // adapter-invocation boundary rather than asserting listing-side enrich drops anything // (enrich no longer filters on allocated at all - see matchesHearingType). @@ -956,7 +956,7 @@ void rangeSearchCourtCalendarShouldExcludeDraftUnallocatedHearingsFromAllocatedV rangeSearchQuery.rangeSearchCourtCalendar(query); verify(courtSchedulerServiceAdapter).getCourtSchedulerHearings( - eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq(Boolean.FALSE), any(), any(), any()); + eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq("FINAL"), any(), any(), any()); } @Test @@ -988,7 +988,7 @@ void rangeSearchCourtCalendarAllocatedShouldRequestIsDraftFalseFromCourtSchedule // allocated=true on the court-calendar view must always ask courtscheduler to exclude drafts. verify(courtSchedulerServiceAdapter).getCourtSchedulerHearings( - eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq(Boolean.FALSE), any(), any(), any()); + eq("C01CY00"), any(), any(), any(), any(), any(), any(), any(), eq("FINAL"), any(), any(), any()); } @Test @@ -1166,7 +1166,7 @@ void rangeSearchCourtCalendarWithCrownAndBusinessTypeShouldUseCourtScheduler() { Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1 @@ -1203,7 +1203,7 @@ void rangeSearchCourtCalendarWithCrownAndBusinessTypeShouldUseCourtScheduler() { Optional.empty(), Optional.of("GENC"), Optional.of(JURISDICTION_TYPE.toString()), - Boolean.FALSE, + "FINAL", "ADULT,YOUTH", 40, 1); @@ -1490,9 +1490,9 @@ void rangeSearchHearingsWithMagsAllocatedOuCodeCourtSessionAndBusinessTypeShould Optional.empty(), Optional.of(BUSINESS_TYPE), Optional.of(MAGISTRATES_TYPE.toString()), - // The plain (non-calendar) range-search path always passes isDraft=null - draft + // The plain (non-calendar) range-search path always passes status=null - draft // exclusion is a court-calendar-only concern - so the adapter stub must match null - // here, not Boolean.FALSE. + // here, not "FINAL". null, "ADULT,YOUTH", 50, 1) ).thenReturn(response); From 73ed52b023dccc8b21c7ff97a368d195fff496cc Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Wed, 15 Jul 2026 08:12:20 +0100 Subject: [PATCH 5/5] test(ccsph2n): add explicit assertion to court-calendar routing IT (Sonar java:S2699) [SPRDT-1164] shouldRouteCourtCalendarPathAByCourtSessionAndFilterByHearingType relied solely on pollWithDefaults(...).until(...) matchers, which SonarQube's java:S2699 does not recognise as assertions. Capture the final polled ResponseData and assert on its payload explicitly. --- .../cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java index db62e9712..0743db41f 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java @@ -288,8 +288,11 @@ public void shouldRouteCourtCalendarPathAByCourtSessionAndFilterByHearingType() // 7. AM + hearingTypeId=Trial -> PATH A returns hearing #2's rows, but the post-enrichment // hearingTypeId filter removes them (hearing #2 is Plea, not Trial) -> empty - pollWithDefaults(courtCalendarRequest(baseUrl + "&courtSession=AM&hearingTypeId=" + TRIAL_HEARING_TYPE.getTypeId())).until(status().is(OK), + final ResponseData response = pollWithDefaults(courtCalendarRequest(baseUrl + "&courtSession=AM&hearingTypeId=" + TRIAL_HEARING_TYPE.getTypeId())).until(status().is(OK), payload().isJson(withJsonPath("$.hearings.size()", is(0)))); + // explicit assertion on the final polled payload (Sonar java:S2699 does not recognise the + // pollWithDefaults(...).until(...) matchers as assertions) + assertThat(response.getPayload(), isJson(withJsonPath("$.hearings.size()", is(0)))); } private static RequestParams courtCalendarRequest(final String url) {