Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<access.control.version>6.4.1</access.control.version>
<referencedata.version>17.103.131</referencedata.version>
<listing.version>17.103.163</listing.version>
<hearing.version>17.0.149</hearing.version>
<hearing.version>17.104.168</hearing.version>
<usersgroups.version>17.104.49</usersgroups.version>
<defence.version>17.0.85</defence.version>
<referencedata.offence.version>17.103.42</referencedata.offence.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import uk.gov.justice.core.courts.DefendantsToRemove;
import uk.gov.justice.core.courts.Hearing;
import uk.gov.justice.core.courts.HearingDay;
import uk.gov.justice.core.courts.JurisdictionType;
import uk.gov.justice.core.courts.ListCourtHearing;
import uk.gov.justice.core.courts.ListDefendantRequest;
import uk.gov.justice.core.courts.ListHearingRequested;
import uk.gov.justice.core.courts.Offence;
import uk.gov.justice.core.courts.OffencesToRemove;
import uk.gov.justice.core.courts.ProsecutionCase;
import uk.gov.justice.core.courts.ProsecutionCasesToRemove;
import uk.gov.justice.core.courts.RotaSlot;
import uk.gov.justice.core.courts.UpdateHearingForPartialAllocation;
import uk.gov.justice.listing.courts.HearingPartiallyUpdated;
import uk.gov.justice.listing.events.HearingRequestedForListing;
Expand All @@ -32,6 +34,7 @@
import uk.gov.justice.services.messaging.JsonEnvelope;
import uk.gov.moj.cpp.progression.helper.HearingNotificationHelper;
import uk.gov.moj.cpp.progression.service.ApplicationParameters;
import uk.gov.moj.cpp.progression.service.CourtScheduleQueryAdapter;
import uk.gov.moj.cpp.progression.service.ListingService;
import uk.gov.moj.cpp.progression.service.ProgressionService;
import uk.gov.moj.cpp.progression.service.dto.HearingNotificationInputData;
Expand Down Expand Up @@ -90,6 +93,9 @@ public class ListHearingRequestedProcessor {
@Inject
private HearingNotificationHelper hearingNotificationHelper;

@Inject
private CourtScheduleQueryAdapter courtScheduleQueryAdapter;

@ServiceComponent(EVENT_PROCESSOR)
@Inject
private Sender sender;
Expand All @@ -98,7 +104,18 @@ public class ListHearingRequestedProcessor {
@Handles("progression.event.list-hearing-requested")
public void handle(final JsonEnvelope jsonEnvelope) {

final ListHearingRequested listHearingRequested = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), ListHearingRequested.class);
ListHearingRequested listHearingRequested = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), ListHearingRequested.class);

// Defence-in-depth for unallocated CROWN: when the user picks a DRAFT court-schedule
// slot via the CCP2 UI, the payload arrives with courtCentre.roomId / roomName
// populated (denormalised from bookedSlots for display purposes). That data must
// not be written to hearing.payload or surfaced in the new-hearing notification
// email - for an unallocated hearing there is no confirmed courtroom yet. Listing
// strips this on its side (HearingEnrichmentOrchestrator.stripRoomInfoIfAnyDraft,
// SPRDT-858) but progression sees the payload first and would otherwise leak it
// into hearing.payload and the notification. Mirror the strip here by asking
// listing-query-api whether any of the booked sessions is DRAFT.
listHearingRequested = stripCourtCentreRoomIfAnyDraftSession(listHearingRequested, jsonEnvelope);

final ListCourtHearing listCourtHearing = convertListCourtHearing(listHearingRequested, jsonEnvelope);

Expand All @@ -116,6 +133,48 @@ public void handle(final JsonEnvelope jsonEnvelope) {
}
}

private ListHearingRequested stripCourtCentreRoomIfAnyDraftSession(final ListHearingRequested original, final JsonEnvelope jsonEnvelope) {
final CourtHearingRequest listNewHearing = original.getListNewHearing();
if (listNewHearing == null
|| !JurisdictionType.CROWN.equals(listNewHearing.getJurisdictionType())
|| listNewHearing.getBookedSlots() == null
|| listNewHearing.getBookedSlots().isEmpty()
|| listNewHearing.getCourtCentre() == null
|| listNewHearing.getCourtCentre().getRoomId() == null) {
return original;
}

final List<UUID> courtScheduleIds = listNewHearing.getBookedSlots().stream()
.map(RotaSlot::getCourtScheduleId)
.filter(Objects::nonNull)
.filter(s -> !s.isBlank())
.map(UUID::fromString)
.collect(toList());

if (courtScheduleIds.isEmpty() || !courtScheduleQueryAdapter.anySessionIsDraft(jsonEnvelope, courtScheduleIds)) {
return original;
}

LOGGER.info("Stripping courtCentre.roomId/roomName for hearingId={} - at least one booked court-schedule session is DRAFT (unallocated CROWN)",
original.getHearingId());

final CourtCentre strippedCourtCentre = CourtCentre.courtCentre()
.withValuesFrom(listNewHearing.getCourtCentre())
.withRoomId(null)
.withRoomName(null)
.build();

final CourtHearingRequest strippedListNewHearing = CourtHearingRequest.courtHearingRequest()
.withValuesFrom(listNewHearing)
.withCourtCentre(strippedCourtCentre)
.build();

return ListHearingRequested.listHearingRequested()
.withValuesFrom(original)
.withListNewHearing(strippedListNewHearing)
.build();
}

@Handles("public.listing.hearing-requested-for-listing")
public void handlePublicEvent(final JsonEnvelope jsonEnvelope) {
final HearingRequestedForListing hearingRequestedForListing = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), HearingRequestedForListing.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package uk.gov.moj.cpp.progression.service;

import static javax.json.Json.createArrayBuilder;
import static javax.json.Json.createObjectBuilder;
import static uk.gov.justice.services.core.annotation.Component.EVENT_PROCESSOR;

import uk.gov.justice.services.core.annotation.ServiceComponent;
import uk.gov.justice.services.core.enveloper.Enveloper;
import uk.gov.justice.services.core.requester.Requester;
import uk.gov.justice.services.messaging.JsonEnvelope;

import java.util.List;
import java.util.UUID;

import javax.inject.Inject;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Calls cpp-context-listing's {@code listing.query.court.schedule.draft.status} query to
* decide whether any of a hearing's booked court-schedule sessions is DRAFT.
*
* <p>Used by {@code ListHearingRequestedProcessor} to detect unallocated CROWN hearings so
* the courtroom info denormalised onto {@code courtCentre} by the UI can be stripped before
* progression writes {@code hearing.payload} or sends the new-hearing notification email.
* Mirrors the intent of listing-side
* {@code HearingEnrichmentOrchestrator.stripRoomInfoIfAnyDraft} (SPRDT-858) so the strip
* happens regardless of which side observes the payload first.
*
* <p>Goes through listing's query-api (not directly to listingcourtscheduler) so the cross-context
* boundary stays clean - progression only ever talks to listing-context, never bypasses to
* listingcourtscheduler-api. Fails-safe by returning {@code true} ("treat as DRAFT") if the
* remote call returns an unexpected payload; leaking a phantom courtroom into a notification
* email or persisted hearing snapshot is worse than dropping room info for what may be a
* confirmed-allocated hearing.
*/
public class CourtScheduleQueryAdapter {

private static final Logger LOGGER = LoggerFactory.getLogger(CourtScheduleQueryAdapter.class);

private static final String LISTING_QUERY_COURT_SCHEDULE_DRAFT_STATUS = "listing.query.court.schedule.draft.status";
private static final String COURT_SCHEDULE_ID_LIST = "courtScheduleIdList";
private static final String ANY_DRAFT = "anyDraft";

@Inject
@ServiceComponent(EVENT_PROCESSOR)
private Requester requester;

@Inject
private Enveloper enveloper;

public boolean anySessionIsDraft(final JsonEnvelope sourceEnvelope, final List<UUID> courtScheduleIds) {
if (courtScheduleIds == null || courtScheduleIds.isEmpty()) {
return false;
}

final JsonArrayBuilder list = createArrayBuilder();
courtScheduleIds.forEach(id -> list.add(id.toString()));
final JsonObject requestPayload = createObjectBuilder()
.add(COURT_SCHEDULE_ID_LIST, list)
.build();

final JsonEnvelope response;
try {
response = requester.requestAsAdmin(enveloper
.withMetadataFrom(sourceEnvelope, LISTING_QUERY_COURT_SCHEDULE_DRAFT_STATUS)
.apply(requestPayload));
} catch (Exception ex) {
// Fail-open: if we cannot prove a session is draft (e.g. listing not yet deployed
// with the endpoint, dispatch error, network blip) we preserve the original
// payload by returning false. The original unallocated-CROWN leak resumes during
// the outage, but allocated CROWN hearings keep their courtroom info - which is
// the much more common case. Operators will see this WARN if the call is
// persistently failing.
LOGGER.warn("listing.query.court.schedule.draft.status threw {} for {} ids - failing-open (anyDraft=false, no strip)",
ex.getClass().getSimpleName(), courtScheduleIds.size());
return false;
}

final JsonObject responseBody = response == null ? null : response.payloadAsJsonObject();
if (responseBody == null || !responseBody.containsKey(ANY_DRAFT) || responseBody.isNull(ANY_DRAFT)) {
// Same fail-open rationale as the catch block above - if listing's response is
// malformed we cannot prove draft state, so preserve the input.
LOGGER.warn("listing.query.court.schedule.draft.status returned an unexpected payload for {} ids - failing-open (anyDraft=false, no strip)",
courtScheduleIds.size());
return false;
}

return responseBody.getBoolean(ANY_DRAFT);
}
}
Loading
Loading