From 29498a02d4ee364f3e6eb669dc9bc7114e3bcd4e Mon Sep 17 00:00:00 2001 From: aykutdanisman Date: Fri, 5 Jun 2026 09:21:56 +0100 Subject: [PATCH] rebase team/CCT-2353 from main --- .../hearing-command-handler/pom.xml | 12 + .../handler/HearingEventCommandHandler.java | 38 +- .../handler/ShareResultsCommandHandler.java | 60 ++- .../handler/service/ReferenceDataService.java | 69 ++++ .../service/validation/DefendantDto.java | 66 +++ .../validation/HttpClientProducer.java | 85 ++++ .../service/validation/OffenceDto.java | 68 ++++ .../service/validation/ResultLineDto.java | 83 ++++ .../validation/ResultsValidationClient.java | 87 ++++ .../service/validation/ResultsValidator.java | 6 + .../service/validation/ValidationIssue.java | 67 ++++ .../service/validation/ValidationRequest.java | 62 +++ .../validation/ValidationRequestMapper.java | 128 ++++++ .../validation/ValidationResponse.java | 45 +++ .../HearingEventCommandHandlerTest.java | 164 ++++++++ .../ShareResultsCommandHandlerTest.java | 316 ++++++++++++++- .../service/ReferenceDataServiceTest.java | 86 ++++ .../validation/HttpClientProducerTest.java | 125 ++++++ .../ResultsValidationClientTest.java | 209 ++++++++++ .../ValidationRequestMapperTest.java | 378 ++++++++++++++++++ .../validation/ValidationResponseTest.java | 106 +++++ .../domain/aggregate/HearingAggregate.java | 16 +- .../hearing/HearingEventDelegate.java | 60 ++- .../hearing/HearingEventDelegateTest.java | 87 ++++ .../event/result/ResultsValidationFailed.java | 144 +++++++ .../result/ResultsValidationFailedTest.java | 107 +++++ .../listener/AddDefendantEventListener.java | 4 +- .../listener/HearingDeletedEventListener.java | 13 + .../AddDefendantEventListenerTest.java | 45 ++- .../HearingDeletedEventListenerTest.java | 31 +- .../HearingEventListenerYamlConfigTest.java | 4 +- .../hearing/event/HearingEventProcessor.java | 11 + .../event/HearingEventProcessorTest.java | 26 ++ ...ring.events.results-validation-failed.json | 14 + ...lic.hearing.results-validation-failed.json | 14 + .../yaml/public-publications-descriptor.yaml | 3 + .../src/yaml/subscriptions-descriptor.yaml | 4 + .../hearing/it/CourtListRestrictionIT.java | 17 + ...blishLatestCourtCentreHearingEventsIT.java | 8 + .../query/api/HearingQueryApiTest.java | 80 ++++ .../query/view/HearingEventQueryView.java | 14 +- .../HearingListXhibitResponseTransformer.java | 67 +++- .../query/view/service/HearingService.java | 9 +- .../query/view/HearingEventQueryViewTest.java | 32 ++ ...ringListXhibitResponseTransformerTest.java | 57 +++ .../view/service/HearingServiceTest.java | 365 ++++++++++++++++- .../hearing/mapping/DefendantJPAMapper.java | 4 +- .../cpp/hearing/mapping/HearingJPAMapper.java | 4 + .../repository/HearingEventRepository.java | 17 +- .../hearing/repository/HearingRepository.java | 6 + .../mapping/DefendantJPAMapperTest.java | 15 + .../hearing/mapping/HearingJPAMapperTest.java | 93 +++++ .../HearingEventRepositoryTest.java | 62 +++ .../repository/HearingRepositoryTest.java | 26 ++ .../hearing/utils/HearingJPADataTemplate.java | 10 +- .../moj/cpp/hearing/test/TestTemplates.java | 6 +- 56 files changed, 3666 insertions(+), 69 deletions(-) create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/DefendantDto.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducer.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/OffenceDto.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultLineDto.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClient.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidator.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationIssue.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequest.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapper.java create mode 100644 hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponse.java create mode 100644 hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducerTest.java create mode 100644 hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClientTest.java create mode 100644 hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapperTest.java create mode 100644 hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponseTest.java create mode 100644 hearing-domain/hearing-domain-event/src/main/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailed.java create mode 100644 hearing-domain/hearing-domain-event/src/test/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailedTest.java create mode 100644 hearing-event/hearing-event-processor/src/yaml/json/schema/hearing.events.results-validation-failed.json create mode 100644 hearing-event/hearing-event-processor/src/yaml/json/schema/public.hearing.results-validation-failed.json diff --git a/hearing-command/hearing-command-handler/pom.xml b/hearing-command/hearing-command-handler/pom.xml index 18a434b96b..c2eb77becf 100644 --- a/hearing-command/hearing-command-handler/pom.xml +++ b/hearing-command/hearing-command-handler/pom.xml @@ -92,8 +92,20 @@ ${project.version} + + org.apache.httpcomponents + httpclient + + + + uk.gov.justice.utils + test-utils-logging-log4j + pom + test + + uk.gov.justice.services test-utils-core diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandler.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandler.java index 6303cff766..6331593358 100644 --- a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandler.java +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandler.java @@ -3,21 +3,27 @@ import static java.util.UUID.fromString; import static uk.gov.justice.services.core.annotation.Component.COMMAND_HANDLER; +import uk.gov.justice.core.courts.HearingDay; import uk.gov.justice.services.core.annotation.Handles; import uk.gov.justice.services.core.annotation.ServiceComponent; import uk.gov.justice.services.eventsourcing.source.core.exception.EventStreamException; import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.moj.cpp.hearing.command.handler.service.ReferenceDataService; import uk.gov.moj.cpp.hearing.command.logEvent.CorrectLogEventCommand; import uk.gov.moj.cpp.hearing.command.logEvent.CreateHearingEventDefinitionsCommand; import uk.gov.moj.cpp.hearing.command.logEvent.LogEventCommand; import uk.gov.moj.cpp.hearing.command.updateEvent.UpdateHearingEventsCommand; +import uk.gov.moj.cpp.hearing.domain.CourtCentre; import uk.gov.moj.cpp.hearing.domain.aggregate.HearingAggregate; import uk.gov.moj.cpp.hearing.domain.aggregate.HearingEventDefinitionAggregate; import uk.gov.moj.cpp.hearing.eventlog.HearingEvent; +import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import java.util.UUID; +import javax.inject.Inject; import javax.json.JsonArray; import javax.json.JsonObject; @@ -38,6 +44,9 @@ public class HearingEventCommandHandler extends AbstractCommandHandler { private static final UUID PAUSE_HEARING_EVENT_DEFINITION_ID = UUID.fromString("160ecb51-29ee-4954-bbbf-daab18a24fbb"); + @Inject + private ReferenceDataService referenceDataService; + @Handles("hearing.create-hearing-event-definitions") public void createHearingEventDefinitions(final JsonEnvelope jsonEnvelope) throws EventStreamException { if (LOGGER.isDebugEnabled()) { @@ -94,12 +103,14 @@ public void logHearingEvent(final JsonEnvelope jsonEnvelope) throws EventStreamE .build(); final UUID activeHearingId = UUID.fromString(activeHearings.getString(index)); + final CourtCentre pauseCourtCentre = resolveCourtCentre(activeHearingId, logEventCommand.getEventTime()); - aggregate(HearingAggregate.class, activeHearingId, jsonEnvelope, a -> a.logHearingEvent(activeHearingId, PAUSE_HEARING_EVENT_DEFINITION_ID, alterable, defenceCounselId, pauseHearingEvent, hearingTypeIds, userId)); + aggregate(HearingAggregate.class, activeHearingId, jsonEnvelope, a -> a.logHearingEvent(activeHearingId, PAUSE_HEARING_EVENT_DEFINITION_ID, alterable, defenceCounselId, pauseHearingEvent, hearingTypeIds, userId, pauseCourtCentre)); } } - aggregate(HearingAggregate.class, logEventCommand.getHearingId(), jsonEnvelope, a -> a.logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, hearingTypeIds, userId)); + final CourtCentre courtCentre = resolveCourtCentre(hearingId, logEventCommand.getEventTime()); + aggregate(HearingAggregate.class, logEventCommand.getHearingId(), jsonEnvelope, a -> a.logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, hearingTypeIds, userId, courtCentre)); } @Handles("hearing.command.update-hearing-events") @@ -130,12 +141,33 @@ public void correctEvent(final JsonEnvelope jsonEnvelope) throws EventStreamExce .withLastModifiedTime(correctLogEventCommand.getLastModifiedTime()) .withRecordedLabel(correctLogEventCommand.getRecordedLabel()) .withNote(correctLogEventCommand.getNote()).build(); + final CourtCentre courtCentre = resolveCourtCentre(correctLogEventCommand.getHearingId(), correctLogEventCommand.getEventTime()); aggregate(HearingAggregate.class, correctLogEventCommand.getHearingId(), jsonEnvelope, a -> a.correctHearingEvent(correctLogEventCommand.getLatestHearingEventId(), correctLogEventCommand.getHearingId(), correctLogEventCommand.getHearingEventDefinitionId(), correctLogEventCommand.getAlterable(), correctLogEventCommand.getDefenceCounselId(), hearingEvent, - userId)); + userId, + courtCentre)); + } + + private CourtCentre resolveCourtCentre(final UUID hearingId, final ZonedDateTime eventTime) { + try { + final HearingAggregate aggregate = aggregate(HearingAggregate.class, hearingId); + final Optional matchedDay = aggregate.findHearingDayFor(eventTime); + if (matchedDay.isEmpty()) { + return null; + } + final UUID centreId = matchedDay.get().getCourtCentreId(); + final UUID roomId = matchedDay.get().getCourtRoomId(); + if (centreId == null || roomId == null) { + return null; + } + return referenceDataService.resolveCourtCentre(centreId, roomId).orElse(null); + } catch (Exception e) { + LOGGER.warn("Failed to resolve court centre for hearing {} at {}; falling back to top-level", hearingId, eventTime, e); + return null; + } } } diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandler.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandler.java index b015a34d4e..6cbd41c254 100644 --- a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandler.java +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandler.java @@ -26,8 +26,13 @@ import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandResultLineV2; import uk.gov.moj.cpp.hearing.command.result.UpdateDaysResultLinesStatusCommand; import uk.gov.moj.cpp.hearing.command.result.UpdateResultLinesStatusCommand; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ResultsValidator; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationRequest; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationRequestMapper; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationResponse; import uk.gov.moj.cpp.hearing.domain.aggregate.ApplicationAggregate; import uk.gov.moj.cpp.hearing.domain.aggregate.HearingAggregate; +import uk.gov.moj.cpp.hearing.domain.event.result.ResultsValidationFailed; import java.util.HashSet; import java.util.List; @@ -35,7 +40,6 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; @@ -56,6 +60,12 @@ public class ShareResultsCommandHandler extends AbstractCommandHandler { @Inject private ReferenceDataService referenceDataService; + @Inject + private ResultsValidator resultsValidationClient; + + @Inject + private ValidationRequestMapper validationRequestMapper; + @Handles("hearing.command.save-draft-result") public void saveDraftResult(final JsonEnvelope envelope) throws EventStreamException { @@ -174,10 +184,28 @@ public void shareResultForDay(final JsonEnvelope envelope) throws EventStreamExc } final ShareDaysResultsCommand command = convertToObject(envelope, ShareDaysResultsCommand.class); final UUID userId = envelope.metadata().userId().map(UUID::fromString).orElse(null); + long start = System.currentTimeMillis(); + final EventStream eventStream = eventSource.getStreamById(command.getHearingId()); + final HearingAggregate hearingAggregate = aggregateService.get(eventStream, HearingAggregate.class); - aggregate(HearingAggregate.class, command.getHearingId(), envelope, - aggregate -> shareDaysResultsEnrichedWithYouthCourt(aggregate, command, userId)); + final ValidationRequest validationRequest = validationRequestMapper.toValidationRequest(command, hearingAggregate.getHearing()); + final String userIdString = userId != null ? userId.toString() : ""; + + final ValidationResponse validationResponse = resultsValidationClient.validate(validationRequest, userIdString); + long end = System.currentTimeMillis(); + LOGGER.info("Validation API call took {} ms for userId={} and for hearingId={}", end - start, userIdString, validationRequest.getHearingId()); + + if (validationResponse.hasErrors()) { + LOGGER.info("Share blocked by validation errors for hearing {}", command.getHearingId()); + final ResultsValidationFailed failedEvent = buildValidationFailedEvent(command, userIdString, validationResponse); + eventStream.append(Stream.of(failedEvent).map(enveloper.withMetadataFrom(envelope))); + return; + } + + eventStream.append( + shareDaysResultsEnrichedWithYouthCourt(hearingAggregate, command, userId) + .map(enveloper.withMetadataFrom(envelope))); } @Handles("hearing.command.update-result-lines-status") @@ -215,6 +243,30 @@ public void replicateAllSharedResultsForHearing(final JsonEnvelope envelope) thr aggregate -> aggregate.replicateSharedResultsForHearing(hearingId)); } + private ResultsValidationFailed buildValidationFailedEvent(final ShareDaysResultsCommand command, + final String userId, + final ValidationResponse validationResponse) { + return ResultsValidationFailed.builder() + .withHearingId(command.getHearingId()) + .withHearingDay(command.getHearingDay()) + .withUserId(userId) + .withErrors(validationResponse.getErrors().stream() + .map(e -> new ResultsValidationFailed.ValidationError( + e.getRuleId(), e.getSeverity(), e.getMessage(), + e.getAffectedOffences().stream() + .map(o -> o.getId()) + .toList())) + .toList()) + .withWarnings(validationResponse.getWarnings().stream() + .map(w -> new ResultsValidationFailed.ValidationError( + w.getRuleId(), w.getSeverity(), w.getMessage(), + w.getAffectedOffences().stream() + .map(o -> o.getId()) + .toList())) + .toList()) + .build(); + } + private Stream shareResultsEnrichedWithYouthCourt(final HearingAggregate hearingAggregate, final ShareResultsCommand command ) { final Hearing hearing = hearingAggregate.getHearing(); @@ -278,7 +330,7 @@ List getAdditionalApplications(final Set distinctApplica return null; }) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); } } diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataService.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataService.java index f78bb8511c..41ad6c4597 100644 --- a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataService.java +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataService.java @@ -8,15 +8,18 @@ import uk.gov.justice.services.messaging.Envelope; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.messaging.MetadataBuilder; +import uk.gov.moj.cpp.hearing.domain.CourtCentre; import javax.inject.Inject; import javax.json.JsonArray; import javax.json.JsonObject; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static java.util.Objects.isNull; import static java.util.UUID.randomUUID; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.justice.services.core.annotation.Component.COMMAND_API; @@ -129,6 +132,72 @@ public void setId(UUID id) { } + public Optional resolveCourtCentre(final UUID courtCentreId, final UUID courtRoomId) { + if (courtCentreId == null || courtRoomId == null) { + return Optional.empty(); + } + + final JsonArray organisationUnits = queryCourtCentresFor(courtCentreId); + if (organisationUnits == null || organisationUnits.isEmpty()) { + return Optional.empty(); + } + + return findMatchingOrganisationUnit(organisationUnits, courtCentreId) + .flatMap(ou -> buildCourtCentreFromRoom(ou, courtCentreId, courtRoomId)); + } + + private JsonArray queryCourtCentresFor(final UUID courtCentreId) { + final JsonObject payload = createObjectBuilder() + .add("courtCentreId", courtCentreId.toString()) + .build(); + + final JsonEnvelope requestEnvelope = envelopeFrom( + metadataBuilder() + .withName(REFERENCEDATA_QUERY_COURT_CENTRES) + .withId(randomUUID()) + .build(), + payload); + + return requester.requestAsAdmin(requestEnvelope) + .payloadAsJsonObject() + .getJsonArray("organisationunits"); + } + + private Optional findMatchingOrganisationUnit(final JsonArray organisationUnits, final UUID courtCentreId) { + final String idAsString = courtCentreId.toString(); + return organisationUnits.getValuesAs(JsonObject.class).stream() + .filter(unit -> idAsString.equals(getStringOrNull(unit, "id"))) + .findFirst(); + } + + private Optional buildCourtCentreFromRoom(final JsonObject ou, final UUID courtCentreId, final UUID courtRoomId) { + final JsonArray courtrooms = ou.getJsonArray("courtrooms"); + if (isNull(courtrooms)) { + return Optional.empty(); + } + + return courtrooms.getValuesAs(JsonObject.class).stream() + .filter(room -> roomIdMatches(room, courtRoomId)) + .findFirst() + .map(room -> CourtCentre.courtCentre() + .withId(courtCentreId) + .withName(getStringOrNull(ou, "oucodeL3Name")) + .withWelshName(getStringOrNull(ou, "oucodeL3WelshName")) + .withRoomId(courtRoomId) + .withRoomName(getStringOrNull(room, "courtroomName")) + .withWelshRoomName(getStringOrNull(room, "welshCourtroomName")) + .build()); + } + + private static boolean roomIdMatches(final JsonObject room, final UUID courtRoomId) { + final String id = getStringOrNull(room, "id"); + return id != null && courtRoomId.equals(UUID.fromString(id)); + } + + private static String getStringOrNull(final JsonObject json, final String key) { + return json.containsKey(key) ? json.getString(key) : null; + } + public Set retrieveGuiltyPleaTypes() { final MetadataBuilder metadataBuilder = metadataBuilder() .withId(randomUUID()) diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/DefendantDto.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/DefendantDto.java new file mode 100644 index 0000000000..3aa6d701e6 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/DefendantDto.java @@ -0,0 +1,66 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +public class DefendantDto { + private final String id; + private final String firstName; + private final String lastName; + private final String masterDefendantId; + + private DefendantDto(Builder builder) { + this.id = builder.id; + this.firstName = builder.firstName; + this.lastName = builder.lastName; + this.masterDefendantId = builder.masterDefendantId; + } + + public String getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getMasterDefendantId() { + return masterDefendantId; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String firstName; + private String lastName; + private String masterDefendantId; + + public Builder withId(String id) { + this.id = id; + return this; + } + + public Builder withFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public Builder withLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Builder withMasterDefendantId(String masterDefendantId) { + this.masterDefendantId = masterDefendantId; + return this; + } + + public DefendantDto build() { + return new DefendantDto(this); + } + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducer.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducer.java new file mode 100644 index 0000000000..b1890c9d78 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducer.java @@ -0,0 +1,85 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import uk.gov.justice.services.common.configuration.Value; + +import java.io.Closeable; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class HttpClientProducer { + + private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientProducer.class); + + @Inject + @Value(key = "resultsvalidator.timeout.ms", defaultValue = "5000") + private String socketTimeoutMs; + + @Inject + @Value(key = "resultsvalidator.timeout.connect.ms", defaultValue = "1000") + private String connectTimeoutMs; + + @Inject + @Value(key = "resultsvalidator.timeout.connection.request.ms", defaultValue = "1000") + private String connectionRequestTimeoutMs; + + @Inject + @Value(key = "resultsvalidator.pool.max.total", defaultValue = "400") + private String poolMaxTotal; + + @Inject + @Value(key = "resultsvalidator.pool.max.per.route", defaultValue = "200") + private String poolMaxPerRoute; + + @Inject + @Value(key = "resultsvalidator.evict.idle.seconds", defaultValue = "30") + private String evictIdleSeconds; + + private Closeable client; + + @Produces + @ApplicationScoped + public HttpClient createHttpClient() { + final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(Integer.parseInt(poolMaxTotal)); + connectionManager.setDefaultMaxPerRoute(Integer.parseInt(poolMaxPerRoute)); + + final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(Integer.parseInt(connectTimeoutMs)) + .setSocketTimeout(Integer.parseInt(socketTimeoutMs)) + .setConnectionRequestTimeout(Integer.parseInt(connectionRequestTimeoutMs)) + .build(); + + final CloseableHttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .evictIdleConnections(Long.parseLong(evictIdleSeconds), TimeUnit.SECONDS) + .evictExpiredConnections() + .build(); + client = httpClient; + return httpClient; + } + + @PreDestroy + public void close() { + if (client != null) { + try { + client.close(); + } catch (final Exception e) { + LOGGER.warn("Failed to close HttpClient", e); + } + } + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/OffenceDto.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/OffenceDto.java new file mode 100644 index 0000000000..b1bbb07fed --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/OffenceDto.java @@ -0,0 +1,68 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; +public class OffenceDto { + + private final String id; + private final String offenceCode; + private final String offenceTitle; + private final Integer orderIndex; + private final String caseUrn; + + private OffenceDto(Builder builder) { + this.id = builder.id; + this.offenceCode = builder.offenceCode; + this.offenceTitle = builder.offenceTitle; + this.orderIndex = builder.orderIndex; + this.caseUrn = builder.caseUrn; + } + + // Getters + public String getId() { return id; } + public String getOffenceCode() { return offenceCode; } + public String getOffenceTitle() { return offenceTitle; } + public Integer getOrderIndex() { return orderIndex; } + public String getCaseUrn() { return caseUrn; } + + // Builder + public static class Builder { + private String id; + private String offenceCode; + private String offenceTitle; + private Integer orderIndex; + private String caseUrn; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder offenceCode(String offenceCode) { + this.offenceCode = offenceCode; + return this; + } + + public Builder offenceTitle(String offenceTitle) { + this.offenceTitle = offenceTitle; + return this; + } + + public Builder orderIndex(Integer orderIndex) { + this.orderIndex = orderIndex; + return this; + } + + public Builder caseUrn(String caseUrn) { + this.caseUrn = caseUrn; + return this; + } + + public OffenceDto build() { + return new OffenceDto(this); + } + } + + // Optional convenience method + public static Builder builder() { + return new Builder(); + } +} + diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultLineDto.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultLineDto.java new file mode 100644 index 0000000000..62f6868c8c --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultLineDto.java @@ -0,0 +1,83 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import com.fasterxml.jackson.annotation.JsonProperty; +public class ResultLineDto { + + private final String id; + private final String shortCode; + private final String label; + private final String defendantId; + private final String offenceId; + @JsonProperty("isConcurrent") + private final Boolean isConcurrent; + private final String consecutiveToOffence; + + private ResultLineDto(Builder builder) { + this.id = builder.id; + this.shortCode = builder.shortCode; + this.label = builder.label; + this.defendantId = builder.defendantId; + this.offenceId = builder.offenceId; + this.isConcurrent = builder.isConcurrent; + this.consecutiveToOffence = builder.consecutiveToOffence; + } + + // Getters + public String getId() { return id; } + public String getShortCode() { return shortCode; } + public String getLabel() { return label; } + public String getDefendantId() { return defendantId; } + public String getOffenceId() { return offenceId; } + public Boolean getIsConcurrent() { return isConcurrent; } + public String getConsecutiveToOffence() { return consecutiveToOffence; } + + // Builder + public static class Builder { + private String id; + private String shortCode; + private String label; + private String defendantId; + private String offenceId; + private Boolean isConcurrent; + private String consecutiveToOffence; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder shortCode(String shortCode) { + this.shortCode = shortCode; + return this; + } + + public Builder label(String label) { + this.label = label; + return this; + } + + public Builder defendantId(String defendantId) { + this.defendantId = defendantId; + return this; + } + + public Builder offenceId(String offenceId) { + this.offenceId = offenceId; + return this; + } + + public Builder isConcurrent(Boolean isConcurrent) { + this.isConcurrent = isConcurrent; + return this; + } + + public Builder consecutiveToOffence(String consecutiveToOffence) { + this.consecutiveToOffence = consecutiveToOffence; + return this; + } + + public ResultLineDto build() { + return new ResultLineDto(this); + } + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClient.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClient.java new file mode 100644 index 0000000000..b974924f63 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClient.java @@ -0,0 +1,87 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import uk.gov.justice.services.common.configuration.Value; +import uk.gov.justice.services.core.featurecontrol.FeatureControlGuard; + +import java.io.InputStream; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class ResultsValidationClient implements ResultsValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger(ResultsValidationClient.class); + private static final String CJSCPPUID = "CJSCPPUID"; + + @Inject + @Value(key = "resultsvalidator.base.url", defaultValue = "http://localhost:8080/results-validator/api/validation/validate") + protected String validationUrl; + + @Inject + @Value(key = "resultsvalidator.enabled", defaultValue = "true") + protected String enabled; + + @Inject + @Value(key = "resultsvalidator.timeout.ms", defaultValue = "5000") + protected String timeoutMs; + + @Inject + private ObjectMapper objectMapper; + + @Inject + private HttpClient httpClient; + + @Inject + private FeatureControlGuard featureControlGuard; + + public ResultsValidationClient() { + } + + public ValidationResponse validate(final ValidationRequest request, final String userId) { + try { + if (!featureControlGuard.isFeatureEnabled("ResultsValidation")) { + LOGGER.debug("ResultsValidation feature toggle is OFF, skipping validation"); + return ValidationResponse.passThrough(); + } + } catch (final Exception ex) { + LOGGER.warn("ResultsValidation feature toggle lookup failed, proceeding with validation (fail-open)", ex); + } + + if (!"true".equalsIgnoreCase(enabled)) { + LOGGER.debug("Results validation is disabled, proceeding with share"); + return ValidationResponse.passThrough(); + } + + try { + final HttpPost httpPost = new HttpPost(validationUrl); + httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(request), ContentType.APPLICATION_JSON)); + httpPost.addHeader(CJSCPPUID, userId); + + final HttpResponse httpResponse = httpClient.execute(httpPost); + + if (httpResponse.getStatusLine().getStatusCode() == Response.Status.OK.getStatusCode()) { + try (final InputStream content = httpResponse.getEntity().getContent()) { + return objectMapper.readValue(content, ValidationResponse.class); + } + } else { + LOGGER.error("Results validation service returned status {}, proceeding with share (fail-open)", + httpResponse.getStatusLine().getStatusCode()); + return ValidationResponse.passThrough(); + } + } catch (final Exception ex) { + LOGGER.error("Results validation service call failed, proceeding with share (fail-open)", ex); + return ValidationResponse.passThrough(); + } + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidator.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidator.java new file mode 100644 index 0000000000..3e6baa1943 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidator.java @@ -0,0 +1,6 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +public interface ResultsValidator { + + ValidationResponse validate(ValidationRequest request, String userId); +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationIssue.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationIssue.java new file mode 100644 index 0000000000..c681bc394b --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationIssue.java @@ -0,0 +1,67 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ValidationIssue { + + private final String ruleId; + private final String severity; + private final String message; + private final List affectedOffences; + + @JsonCreator + public ValidationIssue( + @JsonProperty("ruleId") final String ruleId, + @JsonProperty("severity") final String severity, + @JsonProperty("message") final String message, + @JsonProperty("affectedOffences") final List affectedOffences) { + this.ruleId = ruleId; + this.severity = severity; + this.message = message; + this.affectedOffences = affectedOffences != null ? affectedOffences : List.of(); + } + + public String getRuleId() { + return ruleId; + } + + public String getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + public List getAffectedOffences() { + return affectedOffences; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AffectedOffence { + + private final String id; + private final String title; + + @JsonCreator + public AffectedOffence( + @JsonProperty("id") final String id, + @JsonProperty("title") final String title) { + this.id = id; + this.title = title; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequest.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequest.java new file mode 100644 index 0000000000..45ba11d2f9 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequest.java @@ -0,0 +1,62 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import java.time.LocalDate; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ValidationRequest { + + private final String hearingId; + private final LocalDate hearingDay; + private final String courtType; + private final String caseId; + private final List resultLines; + private final List offences; + private final List defendants; + + public ValidationRequest( + final String hearingId, + final LocalDate hearingDay, + final String courtType, + final String caseId, + final List resultLines, + final List offences, + final List defendants) { + this.hearingId = hearingId; + this.hearingDay = hearingDay; + this.courtType = courtType; + this.caseId = caseId; + this.resultLines = resultLines; + this.offences = offences; + this.defendants = defendants; + } + + public String getHearingId() { + return hearingId; + } + + public LocalDate getHearingDay() { + return hearingDay; + } + + public String getCourtType() { + return courtType; + } + + public String getCaseId() { + return caseId; + } + + public List getResultLines() { + return resultLines; + } + + public List getOffences() { + return offences; + } + + public List getDefendants() { + return defendants; + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapper.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapper.java new file mode 100644 index 0000000000..fc5bc7e6d5 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapper.java @@ -0,0 +1,128 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import uk.gov.justice.core.courts.Defendant; +import uk.gov.justice.core.courts.Hearing; +import uk.gov.justice.core.courts.Offence; +import uk.gov.justice.core.courts.Person; +import uk.gov.justice.core.courts.PersonDefendant; +import uk.gov.justice.core.courts.ProsecutionCase; +import uk.gov.justice.core.courts.ProsecutionCaseIdentifier; +import uk.gov.moj.cpp.hearing.command.result.ShareDaysResultsCommand; +import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandPrompt; +import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandResultLineV2; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ValidationRequestMapper { + + public ValidationRequest toValidationRequest(final ShareDaysResultsCommand command, final Hearing hearing) { + + final String courtType = hearing.getJurisdictionType() != null + ? hearing.getJurisdictionType().name() + : null; + + final List defendants = new ArrayList<>(); + final List offences = new ArrayList<>(); + String caseId = null; + + if (hearing != null && hearing.getProsecutionCases() != null) { + hearing.getProsecutionCases() + .stream() + .forEach(prosecutionCase -> { + final String caseUrn = extractCaseUrn(prosecutionCase); + + prosecutionCase.getDefendants() + .stream() + .forEach(defendant -> { + final Person personDetails = extractPersonDetails(defendant); + defendants.add(DefendantDto.builder() + .withId(uuidToString(defendant.getId())) + .withFirstName(personDetails != null ? personDetails.getFirstName() : null) + .withLastName(personDetails != null ? personDetails.getLastName() : null) + .withMasterDefendantId(uuidToString(defendant.getMasterDefendantId())) + .build()); + + if (defendant != null && defendant.getOffences()!= null) { + defendant.getOffences() + .stream() + .forEach(offence -> offences.add(new OffenceDto.Builder() + .id(uuidToString(offence.getId())) + .offenceCode(offence.getOffenceCode()) + .offenceTitle(offence.getOffenceTitle()) + .orderIndex(offence.getOrderIndex()) + .caseUrn(caseUrn) + .build())); + } + }); + }); + } + final List resultLines = new ArrayList<>(); + if (command.getResultLines() != null) { + for (final SharedResultsCommandResultLineV2 line : command.getResultLines()) { + if (caseId == null && line.getCaseId() != null) { + caseId = line.getCaseId().toString(); + } + resultLines.add(new ResultLineDto.Builder() + .id(uuidToString(line.getResultLineId())) + .shortCode(line.getShortCode()) + .label(line.getResultLabel()) + .defendantId(uuidToString(line.getDefendantId())) + .offenceId(uuidToString(line.getOffenceId())) + .consecutiveToOffence(extractConsecutiveToOffence(line.getPrompts())) + .isConcurrent(extractIsConcurrent(line.getPrompts())) + .build()); + } + } + + return new ValidationRequest( + uuidToString(command.getHearingId()), + command.getHearingDay(), + courtType, + caseId, + resultLines, + offences, + defendants); + } + + private Person extractPersonDetails(final Defendant defendant) { + final PersonDefendant personDefendant = defendant.getPersonDefendant(); + return personDefendant != null ? personDefendant.getPersonDetails() : null; + } + + private String extractCaseUrn(final ProsecutionCase prosecutionCase) { + final ProsecutionCaseIdentifier identifier = prosecutionCase.getProsecutionCaseIdentifier(); + return identifier != null ? identifier.getCaseURN() : null; + } + + private String uuidToString(final UUID uuid) { + return uuid != null ? uuid.toString() : null; + } + + private Boolean extractIsConcurrent(final List prompts) { + if (prompts == null) { + return null; + } + return prompts.stream() + .filter(p -> "concurrent".equals(p.getPromptRef())) + .findFirst() + .map(p -> "true".equalsIgnoreCase(p.getValue())) + .orElse(null); + } + + private String extractConsecutiveToOffence(final List prompts) { + if (prompts == null) { + return null; + } + return prompts.stream() + .filter(p -> "consecutiveToOffenceNumber".equals(p.getPromptRef())) + .findFirst() + .map(SharedResultsCommandPrompt::getValue) + .filter(v -> v != null && !v.isBlank()) + .orElse(null); + } +} diff --git a/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponse.java b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponse.java new file mode 100644 index 0000000000..76f7ef6e73 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/main/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponse.java @@ -0,0 +1,45 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ValidationResponse { + + private final boolean isValid; + private final List errors; + private final List warnings; + + @JsonCreator + public ValidationResponse( + @JsonProperty("isValid") final boolean isValid, + @JsonProperty("errors") final List errors, + @JsonProperty("warnings") final List warnings) { + this.isValid = isValid; + this.errors = errors != null ? errors : List.of(); + this.warnings = warnings != null ? warnings : List.of(); + } + + public static ValidationResponse passThrough() { + return new ValidationResponse(true, List.of(), List.of()); + } + + public boolean isValid() { + return isValid; + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public List getErrors() { + return errors; + } + + public List getWarnings() { + return warnings; + } +} diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandlerTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandlerTest.java index ec60590c9b..21b3969b2d 100644 --- a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandlerTest.java +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/HearingEventCommandHandlerTest.java @@ -1,8 +1,11 @@ package uk.gov.moj.cpp.hearing.command.handler; import static java.util.UUID.randomUUID; +import static javax.json.Json.createArrayBuilder; +import static javax.json.Json.createObjectBuilder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.test.utils.core.enveloper.EnveloperFactory.createEnveloperWithEvents; @@ -18,6 +21,7 @@ import static uk.gov.moj.cpp.hearing.test.TestUtilities.with; import static uk.gov.moj.cpp.hearing.test.matchers.BeanMatcher.isBean; +import uk.gov.justice.core.courts.HearingDay; import uk.gov.justice.core.courts.JurisdictionType; import uk.gov.justice.domain.aggregate.Aggregate; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; @@ -29,6 +33,7 @@ import uk.gov.justice.services.eventsourcing.source.core.EventStream; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.test.utils.framework.api.JsonObjectConvertersFactory; +import uk.gov.moj.cpp.hearing.command.handler.service.ReferenceDataService; import uk.gov.moj.cpp.hearing.command.initiate.InitiateHearingCommand; import uk.gov.moj.cpp.hearing.command.logEvent.CorrectLogEventCommand; import uk.gov.moj.cpp.hearing.command.logEvent.CreateHearingEventDefinitionsCommand; @@ -48,6 +53,7 @@ import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.stream.Collectors; @@ -75,6 +81,8 @@ public class HearingEventCommandHandlerTest { private EventSource eventSource; @Mock private AggregateService aggregateService; + @Mock + private ReferenceDataService referenceDataService; @Spy private ObjectToJsonObjectConverter objectToJsonObjectConverter = new JsonObjectConvertersFactory().objectToJsonObjectConverter(); @Spy @@ -156,6 +164,162 @@ public void logHearingEvent_shouldRaiseHearingEventLogged() throws Exception { ); } + @Test + public void logHearingEvent_shouldUseDayCourtCentreWithLookedUpNames_whenHearingDayHasDifferentRoomFromTopLevel() throws Exception { + + final InitiateHearingCommand initiateHearingCommand = standardInitiateHearingTemplate(); + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + + final ZonedDateTime day1Time = new UtcClock().now().minusDays(2); + final ZonedDateTime day2Time = new UtcClock().now().minusDays(1); + final UUID day2CentreId = randomUUID(); + final UUID day2RoomId = randomUUID(); + + initiateHearingCommand.getHearing().setHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withSittingDay(day1Time) + .withCourtCentreId(randomUUID()) + .withCourtRoomId(randomUUID()) + .withListedDurationMinutes(60) + .build(), + HearingDay.hearingDay() + .withSittingDay(day2Time) + .withCourtCentreId(day2CentreId) + .withCourtRoomId(day2RoomId) + .withListedDurationMinutes(60) + .build() + )); + + final CourtCentre lookedUp = CourtCentre.courtCentre() + .withId(day2CentreId) + .withName("Day 2 Centre") + .withWelshName("Welsh Day 2 Centre") + .withRoomId(day2RoomId) + .withRoomName("Day 2 Room") + .withWelshRoomName("Welsh Day 2 Room") + .build(); + when(referenceDataService.resolveCourtCentre(day2CentreId, day2RoomId)).thenReturn(Optional.of(lookedUp)); + + final LogEventCommand logEventCommand = new LogEventCommand(randomUUID(), hearingId, randomUUID(), STRING.next(), STRING.next(), + day2Time, day2Time, false, randomUUID(), Arrays.asList(randomUUID()), randomUUID()); + + setupMockedEventStream(hearingId, this.eventStream, with(new HearingAggregate(), a -> { + a.apply(new HearingInitiated(initiateHearingCommand.getHearing())); + })); + + final JsonEnvelope jsonEnvelopCommand = envelopeFrom(metadataWithRandomUUID("hearing.log-hearing-event"), objectToJsonObjectConverter.convert(logEventCommand)); + + hearingEventCommandHandler.logHearingEvent(jsonEnvelopCommand); + + final JsonEnvelope jsonEnvelopeEvent = verifyAppendAndGetArgumentFrom(eventStream).findFirst().get(); + + assertThat(uk.gov.moj.cpp.hearing.test.ObjectConverters.asPojo(jsonEnvelopeEvent, HearingEventLogged.class), isBean(HearingEventLogged.class) + .with(HearingEventLogged::getCourtCentre, isBean(uk.gov.moj.cpp.hearing.domain.CourtCentre.class) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getId, is(day2CentreId)) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getRoomId, is(day2RoomId)) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getName, is("Day 2 Centre")) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getRoomName, is("Day 2 Room")) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getWelshName, is("Welsh Day 2 Centre")) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getWelshRoomName, is("Welsh Day 2 Room"))) + ); + } + + @Test + public void logHearingEvent_shouldFallBackToTopLevelCourtCentre_whenNoHearingDayMatchesEventDate() throws Exception { + + final InitiateHearingCommand initiateHearingCommand = standardInitiateHearingTemplate(); + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + + // Single day on a date that won't match the event time + initiateHearingCommand.getHearing().setHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withSittingDay(new UtcClock().now().minusDays(30)) + .withCourtCentreId(randomUUID()) + .withCourtRoomId(randomUUID()) + .withListedDurationMinutes(60) + .build() + )); + + final LogEventCommand logEventCommand = new LogEventCommand(randomUUID(), hearingId, randomUUID(), STRING.next(), STRING.next(), + new UtcClock().now().minusDays(1), new UtcClock().now().minusDays(1), false, randomUUID(), Arrays.asList(randomUUID()), randomUUID()); + + setupMockedEventStream(hearingId, this.eventStream, with(new HearingAggregate(), a -> { + a.apply(new HearingInitiated(initiateHearingCommand.getHearing())); + })); + + final JsonEnvelope jsonEnvelopCommand = envelopeFrom(metadataWithRandomUUID("hearing.log-hearing-event"), objectToJsonObjectConverter.convert(logEventCommand)); + + hearingEventCommandHandler.logHearingEvent(jsonEnvelopCommand); + + final JsonEnvelope jsonEnvelopeEvent = verifyAppendAndGetArgumentFrom(eventStream).findFirst().get(); + + assertThat(uk.gov.moj.cpp.hearing.test.ObjectConverters.asPojo(jsonEnvelopeEvent, HearingEventLogged.class), isBean(HearingEventLogged.class) + .with(HearingEventLogged::getCourtCentre, isBean(uk.gov.moj.cpp.hearing.domain.CourtCentre.class) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getId, is(initiateHearingCommand.getHearing().getCourtCentre().getId())) + .with(uk.gov.moj.cpp.hearing.domain.CourtCentre::getRoomId, is(initiateHearingCommand.getHearing().getCourtCentre().getRoomId()))) + ); + } + + /** + * Covers the override flow that {@code OverrideCourtRoomActiveHearingsIT} + * used to cover end-to-end: when the {@code log-hearing-event} command + * arrives with {@code override=true} and a list of {@code activeHearings}, + * the handler must emit a PAUSE event against each active hearing's + * aggregate, on top of the target hearing's own event. + */ + @Test + public void logHearingEvent_shouldPauseEachActiveHearing_whenOverrideIsTrue() throws Exception { + + final UUID PAUSE_HEARING_EVENT_DEFINITION_ID = UUID.fromString("160ecb51-29ee-4954-bbbf-daab18a24fbb"); + + final InitiateHearingCommand targetInit = standardInitiateHearingTemplate(); + final UUID targetHearingId = targetInit.getHearing().getId(); + + final InitiateHearingCommand activeInit = standardInitiateHearingTemplate(); + final UUID activeHearingId = activeInit.getHearing().getId(); + + final EventStream activeHearingStream = mock(EventStream.class); + setupMockedEventStream(targetHearingId, this.eventStream, with(new HearingAggregate(), a -> { + a.apply(new HearingInitiated(targetInit.getHearing())); + })); + setupMockedEventStream(activeHearingId, activeHearingStream, with(new HearingAggregate(), a -> { + a.apply(new HearingInitiated(activeInit.getHearing())); + })); + + final ZonedDateTime eventTime = getPastDate(); + final UUID targetEventDefId = randomUUID(); + final LogEventCommand logEventCommand = new LogEventCommand(randomUUID(), targetHearingId, targetEventDefId, + STRING.next(), STRING.next(), eventTime, eventTime, false, randomUUID(), + Arrays.asList(randomUUID()), randomUUID()); + + // Same shape as the regular log-hearing-event command, but with override=true and + // activeHearings populated by the Command-API layer (the production code path). + final javax.json.JsonObject baseCommandPayload = objectToJsonObjectConverter.convert(logEventCommand); + final javax.json.JsonObjectBuilder payloadBuilder = createObjectBuilder(); + baseCommandPayload.forEach(payloadBuilder::add); + final javax.json.JsonObject payload = payloadBuilder + .add("override", true) + .add("activeHearings", createArrayBuilder().add(activeHearingId.toString())) + .build(); + + final JsonEnvelope jsonEnvelopCommand = envelopeFrom(metadataWithRandomUUID("hearing.log-hearing-event"), payload); + + hearingEventCommandHandler.logHearingEvent(jsonEnvelopCommand); + + // Target hearing got its own (non-pause) event. + final JsonEnvelope targetEnvelope = verifyAppendAndGetArgumentFrom(eventStream).findFirst().get(); + assertThat(uk.gov.moj.cpp.hearing.test.ObjectConverters.asPojo(targetEnvelope, HearingEventLogged.class), isBean(HearingEventLogged.class) + .with(HearingEventLogged::getHearingId, is(targetHearingId)) + .with(HearingEventLogged::getHearingEventDefinitionId, is(targetEventDefId))); + + // Active hearing was paused via its own aggregate stream. + final JsonEnvelope pauseEnvelope = verifyAppendAndGetArgumentFrom(activeHearingStream).findFirst().get(); + assertThat(uk.gov.moj.cpp.hearing.test.ObjectConverters.asPojo(pauseEnvelope, HearingEventLogged.class), isBean(HearingEventLogged.class) + .with(HearingEventLogged::getHearingId, is(activeHearingId)) + .with(HearingEventLogged::getHearingEventDefinitionId, is(PAUSE_HEARING_EVENT_DEFINITION_ID)) + .with(HearingEventLogged::getRecordedLabel, is("Hearing paused"))); + } + @Test public void logHearingEvent_shouldIgnoreLogEvent_givenEventHasAlreadyBeenLogged() throws Exception { diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandlerTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandlerTest.java index b4dd2ac208..ada8b2ffbe 100644 --- a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandlerTest.java +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/ShareResultsCommandHandlerTest.java @@ -7,6 +7,10 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.test.utils.core.enveloper.EnveloperFactory.createEnveloperWithEvents; @@ -24,6 +28,16 @@ import uk.gov.justice.core.courts.DefenceCounsel; import uk.gov.justice.core.courts.DelegatedPowers; import uk.gov.justice.core.courts.Hearing; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationIssue; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationRequest; +import uk.gov.moj.cpp.hearing.command.result.DeleteDraftResultV2Command; +import uk.gov.moj.cpp.hearing.command.result.SaveDraftResultV2Command; +import uk.gov.moj.cpp.hearing.command.result.SaveMultipleResultsCommand; +import uk.gov.moj.cpp.hearing.command.result.ShareDaysResultsCommand; +import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandResultLineV2; +import uk.gov.moj.cpp.hearing.command.result.UpdateDaysResultLinesStatusCommand; +import uk.gov.moj.cpp.hearing.command.result.UpdateResultLinesStatusCommand; +import uk.gov.moj.cpp.hearing.domain.aggregate.ApplicationAggregate; import uk.gov.justice.core.courts.Person; import uk.gov.justice.core.courts.PersonDefendant; import uk.gov.justice.core.courts.Prompt; @@ -58,8 +72,12 @@ import uk.gov.moj.cpp.hearing.domain.event.HearingInitiated; import uk.gov.moj.cpp.hearing.domain.event.NowsVariantsSavedEvent; import uk.gov.moj.cpp.hearing.domain.event.ProsecutionCounselAdded; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ResultsValidator; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationRequestMapper; +import uk.gov.moj.cpp.hearing.command.handler.service.validation.ValidationResponse; import uk.gov.moj.cpp.hearing.domain.event.result.DraftResultSaved; import uk.gov.moj.cpp.hearing.domain.event.result.ResultsShared; +import uk.gov.moj.cpp.hearing.domain.event.result.ResultsValidationFailed; import uk.gov.moj.cpp.hearing.domain.event.result.SaveDraftResultFailed; import uk.gov.moj.cpp.hearing.test.TestTemplates; @@ -68,10 +86,13 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.json.Json; + import org.hamcrest.core.IsNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -96,7 +117,7 @@ public class ShareResultsCommandHandlerTest { private static UUID metadataId; private static ZonedDateTime sharedTime; @Spy - private final Enveloper enveloper = createEnveloperWithEvents(ResultsShared.class, SaveDraftResultFailed.class); + private final Enveloper enveloper = createEnveloperWithEvents(ResultsShared.class, SaveDraftResultFailed.class, ResultsValidationFailed.class); private DefendantDetailsUpdated defendantDetailsUpdated; @InjectMocks private ShareResultsCommandHandler shareResultsCommandHandler; @@ -110,6 +131,10 @@ public class ShareResultsCommandHandlerTest { private AggregateService aggregateService; @Mock private Clock clock; + @Mock + private ResultsValidator resultsValidationClient; + @Mock + private ValidationRequestMapper validationRequestMapper; @Spy private JsonObjectToObjectConverter jsonObjectToObjectConverter; @Spy @@ -183,7 +208,7 @@ private static Defendant convert(final uk.gov.justice.core.courts.Defendant curr public void setup() { setField(this.jsonObjectToObjectConverter, "objectMapper", new ObjectMapperProducer().objectMapper()); setField(this.objectToJsonObjectConverter, "mapper", new ObjectMapperProducer().objectMapper()); - when(this.eventSource.getStreamById(initiateHearingCommand.getHearing().getId())).thenReturn(this.hearingEventStream); + lenient().when(this.eventSource.getStreamById(initiateHearingCommand.getHearing().getId())).thenReturn(this.hearingEventStream); defendantDetailsUpdated = new DefendantDetailsUpdated(initiateHearingCommand.getHearing().getId(), convert(initiateHearingCommand.getHearing().getProsecutionCases().get(0).getDefendants().get(0), "Test")); } @@ -410,4 +435,291 @@ private Target getNewTarget(final Target targetToCopyFrom) { .withHearingDay(targetToCopyFrom.getHearingDay()) .build(); } + + @Test + public void shouldSaveDraftResult() throws Exception { + final Target target = saveDraftResultCommandTemplate(initiateHearingCommand, LocalDate.now(), LocalDate.now()).getTarget(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.saveDraftResults(any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.save-draft-result").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(target)); + + shareResultsCommandHandler.saveDraftResult(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldSaveDraftResultV2() throws Exception { + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + final SaveDraftResultV2Command command = SaveDraftResultV2Command.saveDraftResultCommand() + .setHearingId(hearingId) + .setHearingDay(LocalDate.now()) + .setVersion(1); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.saveDraftResultV2(any(), any(), any(), any(), any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.save-draft-result-v2").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.saveDraftResultV2(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldDeleteDraftResultV2() throws Exception { + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + final DeleteDraftResultV2Command command = DeleteDraftResultV2Command.deleteDraftResultCommand() + .setHearingId(hearingId) + .setHearingDay(LocalDate.now()); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.deleteDraftResultV2(any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.delete-draft-result-v2").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.deleteDraftResultV2(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldSaveDraftResultForHearingDay() throws Exception { + final Target target = saveDraftResultCommandTemplate(initiateHearingCommand, LocalDate.now(), LocalDate.now()).getTarget(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.saveDraftResultForHearingDay(any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.save-days-draft-result").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(target)); + + shareResultsCommandHandler.saveDraftResultForHearingDay(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldSaveMultipleDraftResult() throws Exception { + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + final Target target = Target.target() + .withHearingId(hearingId) + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .withTargetId(randomUUID()) + .build(); + final SaveMultipleResultsCommand command = new SaveMultipleResultsCommand(hearingId, List.of(target)); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.saveAllDraftResults(any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.save-multiple-draft-results").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.saveMultipleDraftResult(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldShareResultV2() throws Exception { + final ShareDaysResultsCommand command = TestTemplates.ShareResultsCommandTemplates + .standardShareResultsPerDaysCommandTemplate(initiateHearingCommand.getHearing().getId()) + .setResultLines(List.of()); + command.setHearingDay(LocalDate.now()); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.shareResultsV2(any(), any(), any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + when(clock.now()).thenReturn(sharedTime); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.share-results-v2"), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.shareResultV2(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldShareDaysResultsWhenValidationPasses() throws Exception { + final ShareDaysResultsCommand command = TestTemplates.ShareResultsCommandTemplates + .standardShareResultsPerDaysCommandTemplate(initiateHearingCommand.getHearing().getId()) + .setResultLines(List.of()); + command.setHearingDay(LocalDate.now()); + final Hearing hearing = initiateHearingCommand.getHearing(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.getHearing()).thenReturn(hearing); + when(mockAggregate.shareResultForDay(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + final ValidationRequest validationRequest = new ValidationRequest( + command.getHearingId().toString(), command.getHearingDay(), "MAGISTRATE", null, List.of(), List.of(), List.of()); + when(validationRequestMapper.toValidationRequest(any(), any())).thenReturn(validationRequest); + when(resultsValidationClient.validate(any(), any())).thenReturn(ValidationResponse.passThrough()); + when(clock.now()).thenReturn(sharedTime); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.share-days-results").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.shareResultForDay(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldRaiseValidationFailedEventWhenShareDaysResultsHasErrors() throws Exception { + final ShareDaysResultsCommand command = TestTemplates.ShareResultsCommandTemplates + .standardShareResultsPerDaysCommandTemplate(initiateHearingCommand.getHearing().getId()) + .setResultLines(List.of()); + command.setHearingDay(LocalDate.now()); + final Hearing hearing = initiateHearingCommand.getHearing(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.getHearing()).thenReturn(hearing); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + final ValidationRequest validationRequest = new ValidationRequest( + command.getHearingId().toString(), command.getHearingDay(), "MAGISTRATE", null, List.of(), List.of(), List.of()); + when(validationRequestMapper.toValidationRequest(any(), any())).thenReturn(validationRequest); + final ValidationIssue error = new ValidationIssue("RULE1", "ERROR", "Validation error", List.of()); + when(resultsValidationClient.validate(any(), any())).thenReturn(new ValidationResponse(false, List.of(error), List.of())); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.share-days-results").withUserId(randomUUID().toString()), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.shareResultForDay(envelope); + + final Stream appended = verifyAppendAndGetArgumentFrom(hearingEventStream); + final Optional validationFailed = appended + .filter(e -> "hearing.events.results-validation-failed".equals(e.metadata().name())) + .findFirst(); + assertThat(validationFailed.isPresent(), is(true)); + } + + @Test + public void shouldUpdateResultLinesStatus() throws Exception { + final UpdateResultLinesStatusCommand command = UpdateResultLinesStatusCommand.builder() + .withHearingId(initiateHearingCommand.getHearing().getId()) + .withCourtClerk(DelegatedPowers.delegatedPowers().withUserId(randomUUID()).withFirstName("test").withLastName("test").build()) + .withLastSharedDateTime(ZonedDateTime.now()) + .withSharedResultLines(List.of()) + .build(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.updateResultLinesStatus(any(), any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.update-result-lines-status"), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.updateResultLinesStatus(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldUpdateDaysResultLinesStatus() throws Exception { + final UpdateDaysResultLinesStatusCommand command = UpdateDaysResultLinesStatusCommand.builder() + .withHearingId(initiateHearingCommand.getHearing().getId()) + .withCourtClerk(DelegatedPowers.delegatedPowers().withUserId(randomUUID()).withFirstName("test").withLastName("test").build()) + .withLastSharedDateTime(ZonedDateTime.now()) + .withSharedResultLines(List.of()) + .withHearingDay(LocalDate.now()) + .build(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.updateDaysResultLinesStatus(any(), any(), any(), any(), any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.update-days-result-lines-status"), + objectToJsonObjectConverter.convert(command)); + + shareResultsCommandHandler.updateDaysResultLinesStatus(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldReplicateSharedResultsForHearing() throws Exception { + final UUID hearingId = initiateHearingCommand.getHearing().getId(); + final HearingAggregate mockAggregate = mock(HearingAggregate.class); + when(mockAggregate.replicateSharedResultsForHearing(any())).thenReturn(Stream.empty()); + when(aggregateService.get(hearingEventStream, HearingAggregate.class)).thenReturn(mockAggregate); + + final JsonEnvelope envelope = envelopeFrom( + metadataOf(metadataId, "hearing.command.replicate-results"), + Json.createObjectBuilder().add("hearingId", hearingId.toString()).build()); + + shareResultsCommandHandler.replicateAllSharedResultsForHearing(envelope); + + verify(hearingEventStream).append(any()); + } + + @Test + public void shouldGetDistinctApplicationIdsFromResultLines() { + final UUID appId1 = randomUUID(); + final UUID appId2 = randomUUID(); + + final SharedResultsCommandResultLineV2 line1 = SharedResultsCommandResultLineV2.sharedResultsCommandResultLine() + .withApplicationIds(appId1).build(); + final SharedResultsCommandResultLineV2 line2 = SharedResultsCommandResultLineV2.sharedResultsCommandResultLine() + .withApplicationIds(appId2).build(); + final SharedResultsCommandResultLineV2 lineDuplicate = SharedResultsCommandResultLineV2.sharedResultsCommandResultLine() + .withApplicationIds(appId1).build(); + final SharedResultsCommandResultLineV2 lineNullApp = SharedResultsCommandResultLineV2.sharedResultsCommandResultLine() + .withApplicationIds(null).build(); + + final Set result = ShareResultsCommandHandler.getDistinctApplicationIdsFromResultLines( + List.of(line1, line2, lineDuplicate, lineNullApp)); + + assertThat(result.size(), is(2)); + assertThat(result.contains(appId1), is(true)); + assertThat(result.contains(appId2), is(true)); + } + + @Test + public void shouldReturnEmptyListFromGetAdditionalApplicationsWhenNoIds() { + final List result = shareResultsCommandHandler.getAdditionalApplications(Set.of(), randomUUID()); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + public void shouldReturnApplicationFromGetAdditionalApplicationsWhenLatestHearingDiffers() { + final UUID applicationId = randomUUID(); + final UUID resultedHearingId = initiateHearingCommand.getHearing().getId(); + final UUID latestHearingId = randomUUID(); + + final uk.gov.justice.services.eventsourcing.source.core.EventStream applicationStream = mock(uk.gov.justice.services.eventsourcing.source.core.EventStream.class); + final uk.gov.justice.services.eventsourcing.source.core.EventStream latestHearingStream = mock(uk.gov.justice.services.eventsourcing.source.core.EventStream.class); + + final ApplicationAggregate appAggregate = mock(ApplicationAggregate.class); + when(appAggregate.getHearingIds()).thenReturn(List.of(latestHearingId)); + + final CourtApplication expectedApp = CourtApplication.courtApplication().withId(applicationId).build(); + final Hearing latestHearing = mock(Hearing.class); + when(latestHearing.getCourtApplications()).thenReturn(List.of(expectedApp)); + final HearingAggregate latestHearingAggregate = mock(HearingAggregate.class); + when(latestHearingAggregate.getHearing()).thenReturn(latestHearing); + + when(eventSource.getStreamById(applicationId)).thenReturn(applicationStream); + when(eventSource.getStreamById(latestHearingId)).thenReturn(latestHearingStream); + when(aggregateService.get(applicationStream, ApplicationAggregate.class)).thenReturn(appAggregate); + when(aggregateService.get(latestHearingStream, HearingAggregate.class)).thenReturn(latestHearingAggregate); + + final List result = shareResultsCommandHandler.getAdditionalApplications( + Set.of(applicationId), resultedHearingId); + + assertThat(result.size(), is(1)); + assertThat(result.get(0).getId(), is(applicationId)); + } } diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataServiceTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataServiceTest.java index 39763eab3e..f24a745d65 100644 --- a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataServiceTest.java +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/ReferenceDataServiceTest.java @@ -17,9 +17,11 @@ import uk.gov.justice.services.core.requester.Requester; import uk.gov.justice.services.messaging.Envelope; import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.moj.cpp.hearing.domain.CourtCentre; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -105,4 +107,88 @@ private JsonObject buildOrgUnit(final UUID courtCentreId) { .add("id", courtCentreId.toString()) .build(); } + + @Test + public void resolveCourtCentre_shouldReturnFullCourtCentreFromReferenceData() { + final UUID courtCentreId = randomUUID(); + final UUID courtRoomId = randomUUID(); + + final JsonEnvelope responseEnvelope = createEnvelope(".", + createObjectBuilder() + .add("organisationunits", createArrayBuilder() + .add(createObjectBuilder() + .add("id", courtCentreId.toString()) + .add("oucodeL3Name", "Centre English") + .add("oucodeL3WelshName", "Centre Welsh") + .add("courtrooms", createArrayBuilder() + .add(createObjectBuilder() + .add("id", randomUUID().toString()) + .add("courtroomName", "Other Room") + .add("welshCourtroomName", "Other Welsh Room")) + .add(createObjectBuilder() + .add("id", courtRoomId.toString()) + .add("courtroomName", "Target Room") + .add("welshCourtroomName", "Welsh Target Room"))))) + .build()); + when(requester.requestAsAdmin(any(JsonEnvelope.class))).thenReturn(responseEnvelope); + ArgumentCaptor captor = ArgumentCaptor.forClass(JsonEnvelope.class); + + final Optional result = referenceDataService.resolveCourtCentre(courtCentreId, courtRoomId); + + verify(requester).requestAsAdmin(captor.capture()); + assertThat(captor.getValue().metadata().name(), is("referencedata.query.courtrooms")); + assertThat(captor.getValue().payloadAsJsonObject().getString("courtCentreId"), is(courtCentreId.toString())); + + assertThat(result.isPresent(), is(true)); + final CourtCentre cc = result.get(); + assertThat(cc.getId(), is(courtCentreId)); + assertThat(cc.getName(), is("Centre English")); + assertThat(cc.getWelshName(), is("Centre Welsh")); + assertThat(cc.getRoomId(), is(courtRoomId)); + assertThat(cc.getRoomName(), is("Target Room")); + assertThat(cc.getWelshRoomName(), is("Welsh Target Room")); + } + + @Test + public void resolveCourtCentre_shouldReturnEmpty_whenRoomNotFoundInOU() { + final UUID courtCentreId = randomUUID(); + final UUID courtRoomId = randomUUID(); + + final JsonEnvelope responseEnvelope = createEnvelope(".", + createObjectBuilder() + .add("organisationunits", createArrayBuilder() + .add(createObjectBuilder() + .add("id", courtCentreId.toString()) + .add("oucodeL3Name", "Centre") + .add("courtrooms", createArrayBuilder() + .add(createObjectBuilder() + .add("id", randomUUID().toString()) + .add("courtroomName", "Other Room"))))) + .build()); + when(requester.requestAsAdmin(any(JsonEnvelope.class))).thenReturn(responseEnvelope); + + final Optional result = referenceDataService.resolveCourtCentre(courtCentreId, courtRoomId); + + assertThat(result.isPresent(), is(false)); + } + + @Test + public void resolveCourtCentre_shouldReturnEmpty_whenInputsAreNull() { + assertThat(referenceDataService.resolveCourtCentre(null, randomUUID()).isPresent(), is(false)); + assertThat(referenceDataService.resolveCourtCentre(randomUUID(), null).isPresent(), is(false)); + assertThat(referenceDataService.resolveCourtCentre(null, null).isPresent(), is(false)); + } + + @Test + public void resolveCourtCentre_shouldReturnEmpty_whenNoOrganisationUnits() { + final JsonEnvelope responseEnvelope = createEnvelope(".", + createObjectBuilder() + .add("organisationunits", createArrayBuilder()) + .build()); + when(requester.requestAsAdmin(any(JsonEnvelope.class))).thenReturn(responseEnvelope); + + final Optional result = referenceDataService.resolveCourtCentre(randomUUID(), randomUUID()); + + assertThat(result.isPresent(), is(false)); + } } diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducerTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducerTest.java new file mode 100644 index 0000000000..a66d14c5fc --- /dev/null +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/HttpClientProducerTest.java @@ -0,0 +1,125 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.HttpContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HttpClientProducerTest { + + private static class FakeCloseableHttpClient extends CloseableHttpClient { + boolean closeCalled = false; + boolean throwOnClose = false; + + @Override + protected CloseableHttpResponse doExecute(final HttpHost target, final HttpRequest request, final HttpContext context) { + return null; + } + + @Override + @Deprecated + public ClientConnectionManager getConnectionManager() { + return null; + } + + @Override + @Deprecated + public org.apache.http.params.HttpParams getParams() { + return null; + } + + @Override + public void close() throws IOException { + closeCalled = true; + if (throwOnClose) { + throw new IOException("connection reset"); + } + } + } + + @InjectMocks + private HttpClientProducer httpClientProducer; + + private FakeCloseableHttpClient closeableHttpClient; + + @BeforeEach + void setUp() { + closeableHttpClient = new FakeCloseableHttpClient(); + setField(httpClientProducer, "socketTimeoutMs", "5000"); + setField(httpClientProducer, "connectTimeoutMs", "1000"); + setField(httpClientProducer, "connectionRequestTimeoutMs", "500"); + setField(httpClientProducer, "poolMaxTotal", "200"); + setField(httpClientProducer, "poolMaxPerRoute", "100"); + setField(httpClientProducer, "evictIdleSeconds", "30"); + } + + @Test + void createHttpClient_shouldReturnNonNullHttpClient() { + final HttpClient client = httpClientProducer.createHttpClient(); + + assertThat(client, is(notNullValue())); + } + + @Test + void createHttpClient_shouldReturnCloseableHttpClient() { + final HttpClient client = httpClientProducer.createHttpClient(); + + assertThat(client, instanceOf(CloseableHttpClient.class)); + } + + @Test + void createHttpClient_shouldReturnHttpClient_withCustomValues() { + setField(httpClientProducer, "socketTimeoutMs", "3000"); + setField(httpClientProducer, "connectTimeoutMs", "2000"); + setField(httpClientProducer, "connectionRequestTimeoutMs", "750"); + setField(httpClientProducer, "poolMaxTotal", "50"); + setField(httpClientProducer, "poolMaxPerRoute", "25"); + setField(httpClientProducer, "evictIdleSeconds", "60"); + + final HttpClient client = httpClientProducer.createHttpClient(); + + assertThat(client, is(notNullValue())); + } + + @Test + void close_whenClientIsNotNull_shouldCloseClient() { + setField(httpClientProducer, "client", closeableHttpClient); + + httpClientProducer.close(); + + assertThat(closeableHttpClient.closeCalled, is(true)); + } + + @Test + void close_whenClientIsNull_shouldNotThrow() { + assertDoesNotThrow(() -> httpClientProducer.close()); + } + + @Test + void close_whenClientThrowsException_shouldNotPropagate() { + setField(httpClientProducer, "client", closeableHttpClient); + closeableHttpClient.throwOnClose = true; + + httpClientProducer.close(); + + assertThat(closeableHttpClient.closeCalled, is(true)); + } +} diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClientTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClientTest.java new file mode 100644 index 0000000000..1103ea665c --- /dev/null +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ResultsValidationClientTest.java @@ -0,0 +1,209 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; + +import uk.gov.justice.services.core.featurecontrol.FeatureControlGuard; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; + +@ExtendWith(MockitoExtension.class) +class ResultsValidationClientTest { + + @InjectMocks + private ResultsValidationClient resultsValidationClient; + + @Mock + private HttpClient httpClient; + + @Mock + private FeatureControlGuard featureControlGuard; + + private final ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); + + @BeforeEach + void setUp() { + setField(resultsValidationClient, "objectMapper", objectMapper); + setField(resultsValidationClient, "validationUrl", "http://localhost:8082/api/validation/validate"); + setField(resultsValidationClient, "enabled", "true"); + setField(resultsValidationClient, "timeoutMs", "5000"); + lenient().when(featureControlGuard.isFeatureEnabled("ResultsValidation")).thenReturn(true); + } + + @Test + void shouldReturnValidResponseWhenServiceReturns200WithNoErrors() throws Exception { + final String responseJson = """ + {"validationId":"abc","isValid":true,"errors":[],"warnings":[],"rulesEvaluated":["DR-SENT-002"],"processingTimeMs":10} + """; + mockHttpResponse(200, responseJson); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + } + + @Test + void shouldReturnErrorsWhenServiceReturns200WithErrors() throws Exception { + final String responseJson = """ + {"validationId":"abc","isValid":false,"errors":[{"ruleId":"DR-SENT-002","severity":"ERROR","message":"Missing info","affectedOffences":[]}],"warnings":[],"rulesEvaluated":["DR-SENT-002"],"processingTimeMs":10} + """; + mockHttpResponse(200, responseJson); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(false)); + assertThat(response.hasErrors(), is(true)); + assertThat(response.getErrors(), hasSize(1)); + } + + @Test + void shouldReturnNoErrorsWhenServiceReturns200WithWarningsOnly() throws Exception { + final String responseJson = """ + {"validationId":"abc","isValid":true,"errors":[],"warnings":[{"ruleId":"DR-SENT-002","severity":"WARNING","message":"Advisory","affectedOffences":[]}],"rulesEvaluated":["DR-SENT-002"],"processingTimeMs":10} + """; + mockHttpResponse(200, responseJson); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.hasErrors(), is(false)); + assertThat(response.getWarnings(), hasSize(1)); + } + + @Test + void shouldReturnPassThroughWhenServiceThrowsIOException() throws Exception { + when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Connection refused")); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + assertThat(response.getErrors(), is(empty())); + } + + @Test + void shouldReturnPassThroughWhenServiceReturnsNon200Status() throws Exception { + final HttpResponse httpResponse = mock(HttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + + when(statusLine.getStatusCode()).thenReturn(500); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + } + + @Test + void shouldReturnPassThroughWithoutHttpCallWhenDisabled() throws Exception { + setField(resultsValidationClient, "enabled", "false"); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + verify(httpClient, never()).execute(any()); + } + + @Test + void toggle_off_returns_passThrough_without_http_call() throws Exception { + when(featureControlGuard.isFeatureEnabled("ResultsValidation")).thenReturn(false); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + verify(httpClient, never()).execute(any()); + } + + @Test + void toggle_on_invokes_http_client() throws Exception { + final String responseJson = """ + {"validationId":"abc","isValid":true,"errors":[],"warnings":[],"rulesEvaluated":["DR-SENT-002"],"processingTimeMs":10} + """; + mockHttpResponse(200, responseJson); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + verify(httpClient).execute(any(HttpPost.class)); + } + + @Test + void toggle_lookup_failure_falls_open_and_invokes_http_client() throws Exception { + when(featureControlGuard.isFeatureEnabled("ResultsValidation")).thenThrow(new RuntimeException("feature store unavailable")); + final String responseJson = """ + {"validationId":"abc","isValid":true,"errors":[],"warnings":[],"rulesEvaluated":["DR-SENT-002"],"processingTimeMs":10} + """; + mockHttpResponse(200, responseJson); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + verify(httpClient).execute(any(HttpPost.class)); + } + + @Test + void existing_static_disabled_path_still_short_circuits() throws Exception { + setField(resultsValidationClient, "enabled", "false"); + + final ValidationResponse response = resultsValidationClient.validate(buildRequest(), "user-123"); + + assertThat(response.isValid(), is(true)); + assertThat(response.hasErrors(), is(false)); + verify(httpClient, never()).execute(any()); + } + + private void mockHttpResponse(final int statusCode, final String body) throws IOException { + final HttpResponse httpResponse = mock(HttpResponse.class); + final StatusLine statusLine = mock(StatusLine.class); + final HttpEntity entity = mock(HttpEntity.class); + + when(statusLine.getStatusCode()).thenReturn(statusCode); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(httpResponse.getEntity()).thenReturn(entity); + when(entity.getContent()).thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + } + + private ValidationRequest buildRequest() { + return new ValidationRequest( + "hearing-1", + LocalDate.of(2026, 3, 16), + "MAGISTRATES", + null, + List.of(), + List.of(), + List.of() + ); + } +} diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapperTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapperTest.java new file mode 100644 index 0000000000..1dd3c3d3c7 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationRequestMapperTest.java @@ -0,0 +1,378 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import static java.util.Collections.emptyList; +import static java.util.UUID.randomUUID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import uk.gov.justice.core.courts.Defendant; +import uk.gov.justice.core.courts.Hearing; +import uk.gov.justice.core.courts.JurisdictionType; +import uk.gov.justice.core.courts.Offence; +import uk.gov.justice.core.courts.Person; +import uk.gov.justice.core.courts.PersonDefendant; +import uk.gov.justice.core.courts.ProsecutionCase; +import uk.gov.justice.core.courts.ProsecutionCaseIdentifier; +import uk.gov.moj.cpp.hearing.command.result.ShareDaysResultsCommand; +import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandPrompt; +import uk.gov.moj.cpp.hearing.command.result.SharedResultsCommandResultLineV2; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class ValidationRequestMapperTest { + + private final ValidationRequestMapper mapper = new ValidationRequestMapper(); + + @Test + void shouldMapHearingIdAndHearingDayFromCommand() { + final UUID hearingId = randomUUID(); + final LocalDate hearingDay = LocalDate.of(2026, 3, 16); + + final ShareDaysResultsCommand command = buildCommand(hearingId, hearingDay, emptyList()); + + final Hearing hearing = Hearing.hearing().build(); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getHearingId(), is(hearingId.toString())); + assertThat(request.getHearingDay(), is(hearingDay)); + } + + @Test + void shouldMapCourtTypeFromHearingJurisdictionType() { + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final Hearing hearing = Hearing.hearing() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .build(); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getCourtType(), is("MAGISTRATES")); + } + + @Test + void shouldMapDefendantsFromHearingProsecutionCases() { + final UUID defendantId = randomUUID(); + + final Defendant defendant = Defendant.defendant() + .withId(defendantId) + .build(); + + final ProsecutionCase prosecutionCase = ProsecutionCase.prosecutionCase() + .withDefendants(List.of(defendant)) + .build(); + + final Hearing hearing = Hearing.hearing() + .withProsecutionCases(List.of(prosecutionCase)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getDefendants(), hasSize(1)); + assertThat(request.getDefendants().get(0).getId(), is(defendantId.toString())); + } + + @Test + void shouldMapDefendantFirstNameAndLastNameFromPersonDetails() { + final UUID defendantId = randomUUID(); + + final Person person = Person.person() + .withFirstName("John") + .withLastName("Smith") + .build(); + + final PersonDefendant personDefendant = PersonDefendant.personDefendant() + .withPersonDetails(person) + .build(); + + final Defendant defendant = Defendant.defendant() + .withId(defendantId) + .withPersonDefendant(personDefendant) + .build(); + + final ProsecutionCase prosecutionCase = ProsecutionCase.prosecutionCase() + .withDefendants(List.of(defendant)) + .build(); + + final Hearing hearing = Hearing.hearing() + .withProsecutionCases(List.of(prosecutionCase)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getDefendants(), hasSize(1)); + assertThat(request.getDefendants().get(0).getFirstName(), is("John")); + assertThat(request.getDefendants().get(0).getLastName(), is("Smith")); + } + + @Test + void shouldHandleNullPersonDefendantGracefully() { + final UUID defendantId = randomUUID(); + + final Defendant defendant = Defendant.defendant() + .withId(defendantId) + .build(); + + final ProsecutionCase prosecutionCase = ProsecutionCase.prosecutionCase() + .withDefendants(List.of(defendant)) + .build(); + + final Hearing hearing = Hearing.hearing() + .withProsecutionCases(List.of(prosecutionCase)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getDefendants(), hasSize(1)); + assertThat(request.getDefendants().get(0).getFirstName(), is(nullValue())); + assertThat(request.getDefendants().get(0).getLastName(), is(nullValue())); + } + + @Test + void shouldMapOffencesFromHearing() { + final UUID offenceId = randomUUID(); + + final Offence offence = Offence.offence() + .withId(offenceId) + .withOffenceCode("TH68001") + .withOffenceTitle("Theft") + .withOrderIndex(1) + .build(); + + final Defendant defendant = Defendant.defendant() + .withId(randomUUID()) + .withOffences(List.of(offence)) + .build(); + + final ProsecutionCase prosecutionCase = ProsecutionCase.prosecutionCase() + .withDefendants(List.of(defendant)) + .build(); + + final Hearing hearing = Hearing.hearing() + .withProsecutionCases(List.of(prosecutionCase)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getOffences(), hasSize(1)); + assertThat(request.getOffences().get(0).getId(), is(offenceId.toString())); + assertThat(request.getOffences().get(0).getOffenceCode(), is("TH68001")); + assertThat(request.getOffences().get(0).getOffenceTitle(), is("Theft")); + assertThat(request.getOffences().get(0).getOrderIndex(), is(1)); + } + + @Test + void shouldMapCaseUrnFromProsecutionCaseIdentifier() { + final UUID offenceId = randomUUID(); + final String caseUrn = "32AH9105826"; + + final Offence offence = Offence.offence() + .withId(offenceId) + .withOffenceCode("TH68001") + .withOffenceTitle("Theft") + .withOrderIndex(1) + .build(); + + final Defendant defendant = Defendant.defendant() + .withId(randomUUID()) + .withOffences(List.of(offence)) + .build(); + + final ProsecutionCase prosecutionCase = ProsecutionCase.prosecutionCase() + .withProsecutionCaseIdentifier(ProsecutionCaseIdentifier.prosecutionCaseIdentifier() + .withCaseURN(caseUrn) + .build()) + .withDefendants(List.of(defendant)) + .build(); + + final Hearing hearing = Hearing.hearing() + .withProsecutionCases(List.of(prosecutionCase)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getOffences(), hasSize(1)); + assertThat(request.getOffences().get(0).getCaseUrn(), is(caseUrn)); + } + + @Test + void shouldMapResultLinesFromCommand() { + final UUID resultLineId = randomUUID(); + final UUID offenceId = randomUUID(); + final UUID defendantId = randomUUID(); + + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(resultLineId) + .withShortCode("IMP") + .withResultLabel("Imprisonment") + .withDefendantId(defendantId) + .withOffenceId(offenceId) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + + final Hearing hearing = Hearing.hearing().build(); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getResultLines(), hasSize(1)); + assertThat(request.getResultLines().get(0).getId(), is(resultLineId.toString())); + assertThat(request.getResultLines().get(0).getShortCode(), is("IMP")); + assertThat(request.getResultLines().get(0).getLabel(), is("Imprisonment")); + assertThat(request.getResultLines().get(0).getDefendantId(), is(defendantId.toString())); + assertThat(request.getResultLines().get(0).getOffenceId(), is(offenceId.toString())); + } + + @Test + void shouldHandleNullProsecutionCasesGracefully() { + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final Hearing hearing = Hearing.hearing().build(); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getDefendants(), is(empty())); + assertThat(request.getOffences(), is(empty())); + } + + @Test + void shouldHandleNullJurisdictionTypeGracefully() { + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), emptyList()); + + final Hearing hearing = Hearing.hearing().build(); + + final ValidationRequest request = mapper.toValidationRequest(command, hearing); + + assertThat(request.getCourtType(), is(nullValue())); + } + + @Test + void shouldMapIsConcurrentFromPrompts() { + final SharedResultsCommandPrompt concurrentPrompt = new SharedResultsCommandPrompt( + randomUUID(), "Concurrent", null, "true", null, null, "concurrent"); + + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(randomUUID()) + .withShortCode("IMP") + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .withPrompts(List.of(concurrentPrompt)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + final ValidationRequest request = mapper.toValidationRequest(command, Hearing.hearing().build()); + + assertThat(request.getResultLines().get(0).getIsConcurrent(), is(true)); + assertThat(request.getResultLines().get(0).getConsecutiveToOffence(), is(nullValue())); + } + + @Test + void shouldMapConsecutiveToOffenceFromPrompts() { + final String consecutiveOffenceId = randomUUID().toString(); + final SharedResultsCommandPrompt consecutivePrompt = new SharedResultsCommandPrompt( + randomUUID(), "Consecutive to", null, consecutiveOffenceId, null, null, "consecutiveToOffenceNumber"); + + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(randomUUID()) + .withShortCode("IMP") + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .withPrompts(List.of(consecutivePrompt)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + final ValidationRequest request = mapper.toValidationRequest(command, Hearing.hearing().build()); + + assertThat(request.getResultLines().get(0).getIsConcurrent(), is(nullValue())); + assertThat(request.getResultLines().get(0).getConsecutiveToOffence(), is(consecutiveOffenceId)); + } + + @Test + void shouldMapBothConcurrentAndConsecutiveFromPrompts() { + final String consecutiveOffenceId = randomUUID().toString(); + final SharedResultsCommandPrompt concurrentPrompt = new SharedResultsCommandPrompt( + randomUUID(), "Concurrent", null, "false", null, null, "concurrent"); + final SharedResultsCommandPrompt consecutivePrompt = new SharedResultsCommandPrompt( + randomUUID(), "Consecutive to", null, consecutiveOffenceId, null, null, "consecutiveToOffenceNumber"); + + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(randomUUID()) + .withShortCode("IMP") + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .withPrompts(List.of(concurrentPrompt, consecutivePrompt)) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + final ValidationRequest request = mapper.toValidationRequest(command, Hearing.hearing().build()); + + assertThat(request.getResultLines().get(0).getIsConcurrent(), is(false)); + assertThat(request.getResultLines().get(0).getConsecutiveToOffence(), is(consecutiveOffenceId)); + } + + @Test + void shouldHandleNullPromptsGracefully() { + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(randomUUID()) + .withShortCode("IMP") + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + final ValidationRequest request = mapper.toValidationRequest(command, Hearing.hearing().build()); + + assertThat(request.getResultLines().get(0).getIsConcurrent(), is(nullValue())); + assertThat(request.getResultLines().get(0).getConsecutiveToOffence(), is(nullValue())); + } + + @Test + void shouldHandleEmptyPromptsGracefully() { + final SharedResultsCommandResultLineV2 resultLine = SharedResultsCommandResultLineV2 + .sharedResultsCommandResultLine() + .withResultLineId(randomUUID()) + .withShortCode("IMP") + .withDefendantId(randomUUID()) + .withOffenceId(randomUUID()) + .withPrompts(emptyList()) + .build(); + + final ShareDaysResultsCommand command = buildCommand(randomUUID(), LocalDate.now(), List.of(resultLine)); + final ValidationRequest request = mapper.toValidationRequest(command, Hearing.hearing().build()); + + assertThat(request.getResultLines().get(0).getIsConcurrent(), is(nullValue())); + assertThat(request.getResultLines().get(0).getConsecutiveToOffence(), is(nullValue())); + } + + private ShareDaysResultsCommand buildCommand(final UUID hearingId, final LocalDate hearingDay, + final List resultLines) { + final ShareDaysResultsCommand command = ShareDaysResultsCommand.shareResultsCommand() + .setHearingId(hearingId) + .setResultLines(resultLines); + command.setHearingDay(hearingDay); + return command; + } +} diff --git a/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponseTest.java b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponseTest.java new file mode 100644 index 0000000000..1432b836e5 --- /dev/null +++ b/hearing-command/hearing-command-handler/src/test/java/uk/gov/moj/cpp/hearing/command/handler/service/validation/ValidationResponseTest.java @@ -0,0 +1,106 @@ +package uk.gov.moj.cpp.hearing.command.handler.service.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +class ValidationResponseTest { + + private final ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); + + @Test + void shouldDeserializeValidResponse() throws Exception { + final String json = """ + { + "validationId": "abc-123", + "isValid": true, + "errors": [], + "warnings": [], + "rulesEvaluated": ["DR-SENT-002"], + "processingTimeMs": 42 + } + """; + + final ValidationResponse response = objectMapper.readValue(json, ValidationResponse.class); + + assertThat(response.isValid(), is(true)); + assertThat(response.getErrors(), is(empty())); + assertThat(response.getWarnings(), is(empty())); + } + + @Test + void shouldDeserializeResponseWithErrors() throws Exception { + final String json = """ + { + "validationId": "abc-123", + "isValid": false, + "errors": [ + { + "ruleId": "DR-SENT-002", + "severity": "ERROR", + "message": "Offences 1, 2 missing info", + "affectedOffences": [ + {"id": "off-1", "title": "Offence 1"} + ] + } + ], + "warnings": [], + "rulesEvaluated": ["DR-SENT-002"], + "processingTimeMs": 42 + } + """; + + final ValidationResponse response = objectMapper.readValue(json, ValidationResponse.class); + + assertThat(response.isValid(), is(false)); + assertThat(response.hasErrors(), is(true)); + assertThat(response.getErrors(), hasSize(1)); + assertThat(response.getErrors().get(0).getRuleId(), is("DR-SENT-002")); + assertThat(response.getErrors().get(0).getSeverity(), is("ERROR")); + assertThat(response.getErrors().get(0).getMessage(), is("Offences 1, 2 missing info")); + } + + @Test + void shouldReturnHasErrorsFalseWhenOnlyWarnings() throws Exception { + final String json = """ + { + "validationId": "abc-123", + "isValid": true, + "errors": [], + "warnings": [ + { + "ruleId": "DR-SENT-002", + "severity": "WARNING", + "message": "Both concurrent and consecutive", + "affectedOffences": [] + } + ], + "rulesEvaluated": ["DR-SENT-002"], + "processingTimeMs": 42 + } + """; + + final ValidationResponse response = objectMapper.readValue(json, ValidationResponse.class); + + assertThat(response.hasErrors(), is(false)); + assertThat(response.getWarnings(), hasSize(1)); + } + + @Test + void shouldCreatePassThroughResponse() { + final ValidationResponse passThrough = ValidationResponse.passThrough(); + + assertThat(passThrough.isValid(), is(true)); + assertThat(passThrough.hasErrors(), is(false)); + assertThat(passThrough.getErrors(), is(empty())); + assertThat(passThrough.getWarnings(), is(empty())); + } +} diff --git a/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/HearingAggregate.java b/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/HearingAggregate.java index c03fbe7104..462bdac068 100644 --- a/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/HearingAggregate.java +++ b/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/HearingAggregate.java @@ -618,11 +618,15 @@ public Stream updateHearingWithIndicatedPlea(final UUID hearingId, final } public Stream logHearingEvent(final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final HearingEvent hearingEvent, final List hearingTypeIds, final UUID userId) { + return logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, hearingTypeIds, userId, null); + } + + public Stream logHearingEvent(final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final HearingEvent hearingEvent, final List hearingTypeIds, final UUID userId, final uk.gov.moj.cpp.hearing.domain.CourtCentre suppliedCourtCentre) { if (this.momento.isDeletedOrDuplicated()) { return warnEventIgnored(hearingId, "logHearingEvent"); } - return apply(Stream.concat(this.hearingEventDelegate.logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId), + return apply(Stream.concat(this.hearingEventDelegate.logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId, suppliedCourtCentre), CustodyTimeLimitUtil.stopCTLExpiryForTrialHearingUser(this.momento, hearingEvent, hearingTypeIds))); } @@ -631,7 +635,15 @@ public Stream updateHearingEvents(final UUID hearingId, final List correctHearingEvent(final UUID latestHearingEventId, final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final HearingEvent hearingEvent, final UUID userId) { - return apply(this.hearingEventDelegate.correctHearingEvent(latestHearingEventId, hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId)); + return correctHearingEvent(latestHearingEventId, hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId, null); + } + + public Stream correctHearingEvent(final UUID latestHearingEventId, final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final HearingEvent hearingEvent, final UUID userId, final uk.gov.moj.cpp.hearing.domain.CourtCentre suppliedCourtCentre) { + return apply(this.hearingEventDelegate.correctHearingEvent(latestHearingEventId, hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId, suppliedCourtCentre)); + } + + public Optional findHearingDayFor(final ZonedDateTime eventTime) { + return this.hearingEventDelegate.findHearingDayFor(eventTime); } public Stream deleteCourtApplicationHearing(final UUID hearingId) { diff --git a/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegate.java b/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegate.java index 96c2afafbd..0cdb37bdab 100644 --- a/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegate.java +++ b/hearing-domain/hearing-domain-aggregate/src/main/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegate.java @@ -5,6 +5,7 @@ import static java.util.Optional.ofNullable; import uk.gov.justice.core.courts.CourtApplication; +import uk.gov.justice.core.courts.HearingDay; import uk.gov.justice.core.courts.ProsecutionCaseIdentifier; import uk.gov.moj.cpp.hearing.domain.CourtCentre; import uk.gov.moj.cpp.hearing.domain.HearingType; @@ -14,6 +15,8 @@ import uk.gov.moj.cpp.hearing.domain.event.HearingEventsUpdated; import java.io.Serializable; +import java.time.LocalDate; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -47,6 +50,10 @@ public void handleHearingEventDeleted(HearingEventDeleted hearingEventDeleted) { } public Stream logHearingEvent(final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final uk.gov.moj.cpp.hearing.eventlog.HearingEvent hearingEvent, final UUID userId) { + return logHearingEvent(hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId, null); + } + + public Stream logHearingEvent(final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final uk.gov.moj.cpp.hearing.eventlog.HearingEvent hearingEvent, final UUID userId, final CourtCentre suppliedCourtCentre) { Optional ignoreReason = validateHearingEvent(hearingEvent.getHearingEventId()); if (ignoreReason.isPresent()) { @@ -61,14 +68,7 @@ public Stream logHearingEvent(final UUID hearingId, final UUID hearingEv } - final CourtCentre courtCentre = CourtCentre.courtCentre() - .withId(momento.getHearing().getCourtCentre().getId()) - .withName(momento.getHearing().getCourtCentre().getName()) - .withRoomId(momento.getHearing().getCourtCentre().getRoomId()) - .withRoomName(momento.getHearing().getCourtCentre().getRoomName()) - .withWelshName(momento.getHearing().getCourtCentre().getWelshName()) - .withWelshRoomName(momento.getHearing().getCourtCentre().getWelshRoomName()) - .build(); + final CourtCentre courtCentre = chooseCourtCentre(suppliedCourtCentre, hearingEvent.getEventTime()); final HearingType hearingType = HearingType.hearingType() .withId(momento.getHearing().getType().getId()) @@ -108,6 +108,10 @@ public Stream updateHearingEvents(final UUID hearingId, List correctHearingEvent(final UUID latestHearingEventId, final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final uk.gov.moj.cpp.hearing.eventlog.HearingEvent hearingEvent, final UUID userId) { + return correctHearingEvent(latestHearingEventId, hearingId, hearingEventDefinitionId, alterable, defenceCounselId, hearingEvent, userId, null); + } + + public Stream correctHearingEvent(final UUID latestHearingEventId, final UUID hearingId, final UUID hearingEventDefinitionId, final Boolean alterable, final UUID defenceCounselId, final uk.gov.moj.cpp.hearing.eventlog.HearingEvent hearingEvent, final UUID userId, final CourtCentre suppliedCourtCentre) { final Optional ignoreReason = validateHearingEventBeforeApplyCorrection(hearingEvent.getHearingEventId()); if (ignoreReason.isPresent()) { @@ -126,14 +130,7 @@ public Stream correctHearingEvent(final UUID latestHearingEventId, final hearingEvent.getEventTime(), hearingEvent.getLastModifiedTime(), alterable, - CourtCentre.courtCentre() - .withId(momento.getHearing().getCourtCentre().getId()) - .withName(momento.getHearing().getCourtCentre().getName()) - .withRoomId(momento.getHearing().getCourtCentre().getRoomId()) - .withRoomName(momento.getHearing().getCourtCentre().getRoomName()) - .withWelshName(momento.getHearing().getCourtCentre().getWelshName()) - .withWelshRoomName(momento.getHearing().getCourtCentre().getWelshRoomName()) - .build(), + chooseCourtCentre(suppliedCourtCentre, hearingEvent.getEventTime()), HearingType.hearingType() .withId(momento.getHearing().getType().getId()) .withDescription(momento.getHearing().getType().getDescription()) @@ -192,6 +189,37 @@ private Optional validateHearingEventBeforeApplyCorrection(final UUID he return Optional.empty(); } + private CourtCentre chooseCourtCentre(final CourtCentre suppliedCourtCentre, final ZonedDateTime eventTime) { + if (nonNull(suppliedCourtCentre) && nonNull(suppliedCourtCentre.getRoomId())) { + return suppliedCourtCentre; + } + return resolveCourtCentre(eventTime); + } + + private CourtCentre resolveCourtCentre(final ZonedDateTime eventTime) { + final uk.gov.justice.core.courts.CourtCentre topLevel = momento.getHearing().getCourtCentre(); + + return CourtCentre.courtCentre() + .withId(topLevel.getId()) + .withName(topLevel.getName()) + .withRoomId(topLevel.getRoomId()) + .withRoomName(topLevel.getRoomName()) + .withWelshName(topLevel.getWelshName()) + .withWelshRoomName(topLevel.getWelshRoomName()) + .build(); + } + + public Optional findHearingDayFor(final ZonedDateTime eventTime) { + if (isNull(eventTime) || isNull(momento.getHearing()) || isNull(momento.getHearing().getHearingDays())) { + return Optional.empty(); + } + final LocalDate eventDate = eventTime.toLocalDate(); + return momento.getHearing().getHearingDays().stream() + .filter(day -> nonNull(day.getSittingDay()) + && day.getSittingDay().toLocalDate().equals(eventDate)) + .findFirst(); + } + public static final class HearingEvent implements Serializable { private static final long serialVersionUID = 1L; diff --git a/hearing-domain/hearing-domain-aggregate/src/test/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegateTest.java b/hearing-domain/hearing-domain-aggregate/src/test/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegateTest.java index 0bcf9ba351..8823be29d8 100644 --- a/hearing-domain/hearing-domain-aggregate/src/test/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegateTest.java +++ b/hearing-domain/hearing-domain-aggregate/src/test/java/uk/gov/moj/cpp/hearing/domain/aggregate/hearing/HearingEventDelegateTest.java @@ -4,10 +4,14 @@ import org.junit.jupiter.api.Test; import uk.gov.justice.core.courts.CourtApplication; import uk.gov.justice.core.courts.Hearing; +import uk.gov.justice.core.courts.HearingDay; import uk.gov.justice.core.courts.ProsecutionCase; import uk.gov.justice.core.courts.ProsecutionCaseIdentifier; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.hamcrest.MatcherAssert.assertThat; @@ -102,6 +106,89 @@ public void handleMomentoHearingWhenCourtApplicationsEmptyCaseURNAndReferenceIsE } + // ------------------------------------------------------------------------ + // findHearingDayFor: per-day resolution used by the override / log-event path + // ------------------------------------------------------------------------ + + @Test + public void findHearingDayFor_shouldReturnEmpty_whenEventTimeIsNull() { + momento.setHearing(Hearing.hearing() + .withId(UUID.randomUUID()) + .withHearingDays(List.of(hearingDayAt(ZonedDateTime.now(ZoneOffset.UTC)))) + .build()); + + assertThat(hearingDelegate.findHearingDayFor(null), is(Optional.empty())); + } + + @Test + public void findHearingDayFor_shouldReturnEmpty_whenMomentoHasNoHearing() { + // momento.getHearing() is null (no setHearing call) + assertThat(hearingDelegate.findHearingDayFor(ZonedDateTime.now(ZoneOffset.UTC)), is(Optional.empty())); + } + + @Test + public void findHearingDayFor_shouldReturnEmpty_whenHearingDaysIsNull() { + momento.setHearing(Hearing.hearing() + .withId(UUID.randomUUID()) + .withHearingDays(null) + .build()); + + assertThat(hearingDelegate.findHearingDayFor(ZonedDateTime.now(ZoneOffset.UTC)), is(Optional.empty())); + } + + @Test + public void findHearingDayFor_shouldReturnDay_whoseSittingDayMatchesEventTimeDate() { + final ZonedDateTime today = ZonedDateTime.now(ZoneOffset.UTC); + final HearingDay yesterdayDay = hearingDayAt(today.minusDays(1)); + final HearingDay todayDay = hearingDayAt(today); + final HearingDay tomorrowDay = hearingDayAt(today.plusDays(1)); + + momento.setHearing(Hearing.hearing() + .withId(UUID.randomUUID()) + .withHearingDays(List.of(yesterdayDay, todayDay, tomorrowDay)) + .build()); + + final Optional matched = hearingDelegate.findHearingDayFor(today); + + assertThat(matched.isPresent(), is(true)); + assertThat(matched.get().getSittingDay().toLocalDate(), is(today.toLocalDate())); + } + + @Test + public void findHearingDayFor_shouldReturnEmpty_whenNoDayMatchesEventTimeDate() { + final ZonedDateTime today = ZonedDateTime.now(ZoneOffset.UTC); + + momento.setHearing(Hearing.hearing() + .withId(UUID.randomUUID()) + .withHearingDays(List.of(hearingDayAt(today.minusDays(1)), hearingDayAt(today.plusDays(1)))) + .build()); + + assertThat(hearingDelegate.findHearingDayFor(today), is(Optional.empty())); + } + + @Test + public void findHearingDayFor_shouldSkipDays_whoseSittingDayIsNull_andReturnMatchAmongstThem() { + final ZonedDateTime today = ZonedDateTime.now(ZoneOffset.UTC); + final HearingDay nullSittingDay = HearingDay.hearingDay().build(); + final HearingDay todayDay = hearingDayAt(today); + + momento.setHearing(Hearing.hearing() + .withId(UUID.randomUUID()) + .withHearingDays(List.of(nullSittingDay, todayDay)) + .build()); + + final Optional matched = hearingDelegate.findHearingDayFor(today); + + assertThat(matched.isPresent(), is(true)); + assertThat(matched.get().getSittingDay().toLocalDate(), is(today.toLocalDate())); + } + + private HearingDay hearingDayAt(final ZonedDateTime sittingDay) { + return HearingDay.hearingDay() + .withSittingDay(sittingDay) + .build(); + } + private CourtApplication createCourtApplication(final UUID id) { return new CourtApplication.Builder() .withId(id) diff --git a/hearing-domain/hearing-domain-event/src/main/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailed.java b/hearing-domain/hearing-domain-event/src/main/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailed.java new file mode 100644 index 0000000000..607f743f68 --- /dev/null +++ b/hearing-domain/hearing-domain-event/src/main/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailed.java @@ -0,0 +1,144 @@ +package uk.gov.moj.cpp.hearing.domain.event.result; + +import uk.gov.justice.domain.annotation.Event; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings({"squid:S2384", "PMD.BeanMembersShouldSerialize"}) +@Event("hearing.events.results-validation-failed") +public class ResultsValidationFailed implements Serializable { + + private static final long serialVersionUID = 1L; + + private UUID hearingId; + private LocalDate hearingDay; + private String userId; + private List errors; + private List warnings; + + @JsonCreator + private ResultsValidationFailed( + @JsonProperty("hearingId") final UUID hearingId, + @JsonProperty("hearingDay") final LocalDate hearingDay, + @JsonProperty("userId") final String userId, + @JsonProperty("errors") final List errors, + @JsonProperty("warnings") final List warnings) { + this.hearingId = hearingId; + this.hearingDay = hearingDay; + this.userId = userId; + this.errors = errors; + this.warnings = warnings; + } + + public ResultsValidationFailed() { + } + + public UUID getHearingId() { + return hearingId; + } + + public LocalDate getHearingDay() { + return hearingDay; + } + + public String getUserId() { + return userId; + } + + public List getErrors() { + return errors; + } + + public List getWarnings() { + return warnings; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class ValidationError implements Serializable { + + private static final long serialVersionUID = 1L; + + private String ruleId; + private String severity; + private String message; + private List affectedOffences; + + @JsonCreator + public ValidationError( + @JsonProperty("ruleId") final String ruleId, + @JsonProperty("severity") final String severity, + @JsonProperty("message") final String message, + @JsonProperty("affectedOffences") final List affectedOffences) { + this.ruleId = ruleId; + this.severity = severity; + this.message = message; + this.affectedOffences = affectedOffences; + } + + public ValidationError() { + } + + public String getRuleId() { + return ruleId; + } + + public String getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + public List getAffectedOffences() { + return affectedOffences; + } + } + + public static final class Builder { + + private UUID hearingId; + private LocalDate hearingDay; + private String userId; + private List errors; + private List warnings; + + public Builder withHearingId(final UUID hearingId) { + this.hearingId = hearingId; + return this; + } + + public Builder withHearingDay(final LocalDate hearingDay) { + this.hearingDay = hearingDay; + return this; + } + + public Builder withUserId(final String userId) { + this.userId = userId; + return this; + } + + public Builder withErrors(final List errors) { + this.errors = errors; + return this; + } + + public Builder withWarnings(final List warnings) { + this.warnings = warnings; + return this; + } + + public ResultsValidationFailed build() { + return new ResultsValidationFailed(hearingId, hearingDay, userId, errors, warnings); + } + } +} diff --git a/hearing-domain/hearing-domain-event/src/test/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailedTest.java b/hearing-domain/hearing-domain-event/src/test/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailedTest.java new file mode 100644 index 0000000000..0a2bc58e07 --- /dev/null +++ b/hearing-domain/hearing-domain-event/src/test/java/uk/gov/moj/cpp/hearing/domain/event/result/ResultsValidationFailedTest.java @@ -0,0 +1,107 @@ +package uk.gov.moj.cpp.hearing.domain.event.result; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import uk.gov.justice.domain.annotation.Event; +import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +class ResultsValidationFailedTest { + + private final ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); + + @Test + void shouldHaveCorrectEventAnnotation() { + final Event event = ResultsValidationFailed.class.getAnnotation(Event.class); + assertThat(event, is(notNullValue())); + assertThat(event.value(), is("hearing.events.results-validation-failed")); + } + + @Test + void shouldBuildWithAllFields() { + final UUID hearingId = UUID.randomUUID(); + final LocalDate hearingDay = LocalDate.of(2026, 3, 16); + final String userId = UUID.randomUUID().toString(); + + final ResultsValidationFailed.ValidationError error = new ResultsValidationFailed.ValidationError( + "DR-SENT-002", "ERROR", "Offences 1, 2 missing concurrent/consecutive info", + List.of("offence-1", "offence-2")); + + final ResultsValidationFailed.ValidationError warning = new ResultsValidationFailed.ValidationError( + "DR-SENT-002", "WARNING", "Offences show both concurrent and consecutive", + List.of("offence-3")); + + final ResultsValidationFailed result = ResultsValidationFailed.builder() + .withHearingId(hearingId) + .withHearingDay(hearingDay) + .withUserId(userId) + .withErrors(List.of(error)) + .withWarnings(List.of(warning)) + .build(); + + assertThat(result.getHearingId(), is(hearingId)); + assertThat(result.getHearingDay(), is(hearingDay)); + assertThat(result.getUserId(), is(userId)); + assertThat(result.getErrors(), hasSize(1)); + assertThat(result.getErrors().get(0).getRuleId(), is("DR-SENT-002")); + assertThat(result.getErrors().get(0).getSeverity(), is("ERROR")); + assertThat(result.getErrors().get(0).getMessage(), is("Offences 1, 2 missing concurrent/consecutive info")); + assertThat(result.getErrors().get(0).getAffectedOffences(), hasSize(2)); + assertThat(result.getWarnings(), hasSize(1)); + assertThat(result.getWarnings().get(0).getSeverity(), is("WARNING")); + } + + @Test + void shouldBuildWithEmptyErrorsAndWarnings() { + final ResultsValidationFailed result = ResultsValidationFailed.builder() + .withHearingId(UUID.randomUUID()) + .withHearingDay(LocalDate.now()) + .withUserId(UUID.randomUUID().toString()) + .withErrors(List.of()) + .withWarnings(List.of()) + .build(); + + assertThat(result.getErrors(), is(empty())); + assertThat(result.getWarnings(), is(empty())); + } + + @Test + void shouldSerializeAndDeserialize() throws Exception { + final UUID hearingId = UUID.randomUUID(); + final LocalDate hearingDay = LocalDate.of(2026, 3, 16); + final String userId = UUID.randomUUID().toString(); + + final ResultsValidationFailed.ValidationError error = new ResultsValidationFailed.ValidationError( + "DR-SENT-002", "ERROR", "Test message", List.of("offence-1")); + + final ResultsValidationFailed original = ResultsValidationFailed.builder() + .withHearingId(hearingId) + .withHearingDay(hearingDay) + .withUserId(userId) + .withErrors(List.of(error)) + .withWarnings(List.of()) + .build(); + + final String json = objectMapper.writeValueAsString(original); + final ResultsValidationFailed deserialized = objectMapper.readValue(json, ResultsValidationFailed.class); + + assertThat(deserialized.getHearingId(), is(hearingId)); + assertThat(deserialized.getHearingDay(), is(hearingDay)); + assertThat(deserialized.getUserId(), is(userId)); + assertThat(deserialized.getErrors(), hasSize(1)); + assertThat(deserialized.getErrors().get(0).getRuleId(), is("DR-SENT-002")); + assertThat(deserialized.getErrors().get(0).getAffectedOffences(), hasSize(1)); + assertThat(deserialized.getWarnings(), is(empty())); + } +} diff --git a/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListener.java b/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListener.java index f4ea5f46b9..2edc3905ea 100644 --- a/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListener.java +++ b/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListener.java @@ -45,7 +45,9 @@ public void caseDefendantAdded(final JsonEnvelope envelope) { return; } - hearingEntity.getProsecutionCases().forEach(prosecutionCase -> prosecutionCase.getDefendants().add(defendantJPAMapper.toJPA(hearingEntity, prosecutionCase, defendantIn))); + hearingEntity.getProsecutionCases() + .stream().filter(pCase -> pCase.getId().getId().equals(caseDefendantAdded.getDefendant().getProsecutionCaseId())) + .forEach(prosecutionCase -> prosecutionCase.getDefendants().add(defendantJPAMapper.toJPA(hearingEntity, prosecutionCase, defendantIn))); hearingRepository.save(hearingEntity); } diff --git a/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListener.java b/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListener.java index ca7bde007d..a745d19987 100644 --- a/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListener.java +++ b/hearing-event/hearing-event-listener/src/main/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListener.java @@ -8,8 +8,11 @@ import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.moj.cpp.hearing.domain.event.CourtApplicationHearingDeleted; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; +import uk.gov.moj.cpp.hearing.persist.entity.ha.ProsecutionCase; import uk.gov.moj.cpp.hearing.repository.HearingRepository; +import uk.gov.moj.cpp.hearing.repository.ProsecutionCaseRepository; +import java.util.List; import java.util.Objects; import java.util.UUID; import javax.inject.Inject; @@ -28,6 +31,9 @@ public class HearingDeletedEventListener { @Inject private HearingRepository hearingRepository; + @Inject + private ProsecutionCaseRepository pcRepository; + @Handles(HEARING_EVENT_HEARING_DELETED) public void hearingDeleted(final JsonEnvelope event) { @@ -50,6 +56,13 @@ public void hearingDeletedBdf(final JsonEnvelope event) { LOGGER.info("Received event '{}' hearingId: {}", HEARING_EVENT_HEARING_DELETED_BDF, hearingId); + final List prosecutionCases = hearingRepository.findProsecutionCasesByHearingId(hearingId); + + if(!prosecutionCases.isEmpty()) { + prosecutionCases.forEach(pcRepository::remove); + pcRepository.flush(); + } + final Hearing hearing = hearingRepository.findBy(hearingId); if (hearing != null) { diff --git a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListenerTest.java b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListenerTest.java index b168851639..d36b05a1d7 100644 --- a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListenerTest.java +++ b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/AddDefendantEventListenerTest.java @@ -1,13 +1,16 @@ package uk.gov.moj.cpp.hearing.event.listener; + import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; import static uk.gov.moj.cpp.hearing.test.TestTemplates.defendantTemplate; +import static uk.gov.moj.cpp.hearing.test.TestUtilities.asSet; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; @@ -17,8 +20,11 @@ import uk.gov.moj.cpp.hearing.mapping.DefendantJPAMapper; import uk.gov.moj.cpp.hearing.mapping.HearingJPAMapper; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; +import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingSnapshotKey; +import uk.gov.moj.cpp.hearing.persist.entity.ha.ProsecutionCase; import uk.gov.moj.cpp.hearing.repository.HearingRepository; +import java.util.HashSet; import java.util.UUID; @@ -75,6 +81,39 @@ public void shouldInsertNewDefendant() { verify(hearingRepository, times(1)).save(hearingExArgumentCaptor.capture()); } + @Test + void shouldInsertNewDefendantToOnlyOneCaseInMultiCaseHearing() { + final UUID arbitraryHearingId = UUID.randomUUID(); + final UUID caseId1 = UUID.randomUUID(); + final UUID caseId2 = UUID.randomUUID(); + + HearingSnapshotKey key1 = new HearingSnapshotKey(caseId1, arbitraryHearingId); + HearingSnapshotKey key2 = new HearingSnapshotKey(caseId2, arbitraryHearingId); + + ProsecutionCase pc1 = new ProsecutionCase(); + pc1.setId(key1); + pc1.setDefendants(new HashSet<>()); + + ProsecutionCase pc2 = new ProsecutionCase(); + pc2.setId(key2); + pc2.setDefendants(new HashSet<>()); + + final Hearing hearing = new Hearing(); + hearing.setProsecutionCases(asSet(pc1, pc2)); + + //given + when(hearingRepository.findBy(arbitraryHearingId)).thenReturn(hearing); + + //when + addCaseDefendantEventListener.caseDefendantAdded(getDefendantAddedJsonEnvelope(arbitraryHearingId, caseId1)); + + //then + final ArgumentCaptor hearingExArgumentCaptor = ArgumentCaptor.forClass(Hearing.class); + + verify(defendantJPAMapper).toJPA(any(Hearing.class), any(ProsecutionCase.class), any(uk.gov.justice.core.courts.Defendant.class)); + verify(hearingRepository, times(1)).save(hearingExArgumentCaptor.capture()); + } + @Test public void shouldNotInsertNewDefendantWhenThereIsNoHearing() { //given @@ -92,7 +131,11 @@ public void shouldNotInsertNewDefendantWhenThereIsNoHearing() { private JsonEnvelope getDefendantAddedJsonEnvelope(final UUID arbitraryHearingId) { - final uk.gov.moj.cpp.hearing.command.defendant.Defendant arbitraryDefendant = defendantTemplate(); + return getDefendantAddedJsonEnvelope(arbitraryHearingId, UUID.randomUUID()); + } + + private JsonEnvelope getDefendantAddedJsonEnvelope(final UUID arbitraryHearingId, final UUID caseId) { + final uk.gov.moj.cpp.hearing.command.defendant.Defendant arbitraryDefendant = defendantTemplate(caseId); JsonObject payload = createObjectBuilder() .add("hearingId", arbitraryHearingId.toString()) .add("defendant", objectToJsonObjectConverter.convert(arbitraryDefendant)) diff --git a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListenerTest.java b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListenerTest.java index 5f5c4266e1..7ee0a438d0 100644 --- a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListenerTest.java +++ b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingDeletedEventListenerTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataWithDefaults; @@ -14,15 +15,18 @@ import uk.gov.justice.services.messaging.Envelope; import uk.gov.moj.cpp.hearing.domain.event.CourtApplicationHearingDeleted; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; +import uk.gov.moj.cpp.hearing.persist.entity.ha.ProsecutionCase; import uk.gov.moj.cpp.hearing.repository.HearingRepository; +import uk.gov.moj.cpp.hearing.repository.ProsecutionCaseRepository; +import java.util.Collections; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; - import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -31,6 +35,9 @@ public class HearingDeletedEventListenerTest { @Mock private HearingRepository hearingRepository; + @Mock + private ProsecutionCaseRepository pcRepository; + @InjectMocks private HearingDeletedEventListener hearingDeletedEventListener; @@ -67,6 +74,27 @@ public void shouldDeleteHearingBdfWhenExistsInViewStore() { final UUID hearingId = randomUUID(); final Hearing hearing = new Hearing(); + final ProsecutionCase pc = new ProsecutionCase(); + + when(hearingRepository.findProsecutionCasesByHearingId(hearingId)).thenReturn(List.of(pc)); + + when(hearingRepository.findBy(hearingId)).thenReturn(hearing); + + hearingDeletedEventListener.hearingDeletedBdf(envelopeFrom(metadataWithDefaults().build(), createObjectBuilder() + .add("hearingId", hearingId.toString()) + .build())); + + verify(hearingRepository).remove(hearing); + verify(pcRepository).remove(pc); + verify(pcRepository).flush(); + } + + @Test + public void shouldDeleteHearingBdfWhenPcDontExists() { + final UUID hearingId = randomUUID(); + final Hearing hearing = new Hearing(); + + when(hearingRepository.findProsecutionCasesByHearingId(hearingId)).thenReturn(Collections.emptyList()); when(hearingRepository.findBy(hearingId)).thenReturn(hearing); hearingDeletedEventListener.hearingDeletedBdf(envelopeFrom(metadataWithDefaults().build(), createObjectBuilder() @@ -74,6 +102,7 @@ public void shouldDeleteHearingBdfWhenExistsInViewStore() { .build())); verify(hearingRepository).remove(hearing); + verifyNoInteractions(pcRepository); } @Test diff --git a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingEventListenerYamlConfigTest.java b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingEventListenerYamlConfigTest.java index b23e669a5c..b683155379 100644 --- a/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingEventListenerYamlConfigTest.java +++ b/hearing-event/hearing-event-listener/src/test/java/uk/gov/moj/cpp/hearing/event/listener/HearingEventListenerYamlConfigTest.java @@ -83,6 +83,7 @@ import uk.gov.moj.cpp.hearing.domain.event.result.ReplicationOfShareResultsFailed; import uk.gov.moj.cpp.hearing.domain.event.result.ResultAmendmentsCancellationFailed; import uk.gov.moj.cpp.hearing.domain.event.result.ResultAmendmentsValidationFailed; +import uk.gov.moj.cpp.hearing.domain.event.result.ResultsValidationFailed; import uk.gov.moj.cpp.hearing.domain.event.result.ResultLinesStatusUpdated; import uk.gov.moj.cpp.hearing.domain.event.result.ResultsSharedSuccess; import uk.gov.moj.cpp.hearing.domain.event.result.ManageResultsFailed; @@ -195,7 +196,8 @@ public class HearingEventListenerYamlConfigTest { CaseRemovedFromGroupCases.class.getAnnotation(Event.class).value(), HearingBreachApplicationsAdded.class.getAnnotation(Event.class).value(), HearingBreachApplicationsToBeAddedReceived.class.getAnnotation(Event.class).value(), - MasterCaseUpdatedForHearing.class.getAnnotation(Event.class).value() + MasterCaseUpdatedForHearing.class.getAnnotation(Event.class).value(), + ResultsValidationFailed.class.getAnnotation(Event.class).value() ); diff --git a/hearing-event/hearing-event-processor/src/main/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessor.java b/hearing-event/hearing-event-processor/src/main/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessor.java index 2bbfb175dc..42b78954b7 100644 --- a/hearing-event/hearing-event-processor/src/main/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessor.java +++ b/hearing-event/hearing-event-processor/src/main/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessor.java @@ -48,6 +48,7 @@ public class HearingEventProcessor { public static final String PUBLIC_HEARING_SAVE_DRAFT_RESULT_FAILED = "public.hearing.save-draft-result-failed"; public static final String PUBLIC_HEARING_MANAGE_RESULTS_FAILED = "public.hearing.manage-results-failed"; public static final String PUBLIC_HEARING_SHARE_RESULTS_FAILED = "public.hearing.share-results-failed"; + public static final String PUBLIC_HEARING_RESULTS_VALIDATION_FAILED = "public.hearing.results-validation-failed"; public static final String PUBLIC_HEARING_TRIAL_VACATED = "public.hearing.trial-vacated"; public static final String PUBLIC_LISTING_HEARING_RESCHEDULED = "public.listing.hearing-rescheduled"; public static final String PUBLIC_PROGRESSION_EVENTS_BREACH_APPLICATIONS_TO_BE_ADDED_TO_HEARING = "public.progression.breach-applications-to-be-added-to-hearing"; @@ -198,6 +199,16 @@ public void handleShareResultsFailedEvent(final JsonEnvelope event) { sender.send(envelopeFrom(metadata, publicEventPayload)); } + @Handles("hearing.events.results-validation-failed") + public void handleResultsValidationFailed(final JsonEnvelope event) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("hearing.events.results-validation-failed event received {}", event.toObfuscatedDebugString()); + } + sender.send(envelopeFrom( + metadataFrom(event.metadata()).withName(PUBLIC_HEARING_RESULTS_VALIDATION_FAILED), + event.payloadAsJsonObject())); + } + @Handles("hearing.multiple-draft-results-saved") public void handleMultipleDraftResultFailedEvent(final JsonEnvelope event) { if (LOGGER.isDebugEnabled()) { diff --git a/hearing-event/hearing-event-processor/src/test/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessorTest.java b/hearing-event/hearing-event-processor/src/test/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessorTest.java index 6aac63472d..4a83972459 100644 --- a/hearing-event/hearing-event-processor/src/test/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessorTest.java +++ b/hearing-event/hearing-event-processor/src/test/java/uk/gov/moj/cpp/hearing/event/HearingEventProcessorTest.java @@ -300,6 +300,32 @@ public void shouldHearingAmendedPublicEvent() { } + @Test + public void shouldForwardResultsValidationFailedToPublicTopic() { + final JsonObjectBuilder payload = createObjectBuilder() + .add(FIELD_HEARING_ID, HEARING_ID.toString()) + .add(FIELD_HEARING_DAY, "2026-05-08") + .add("userId", USER_ID.toString()); + final JsonEnvelope eventIn = envelopeFrom( + metadataWithRandomUUID("hearing.events.results-validation-failed"), + payload.build()); + + this.hearingEventProcessor.handleResultsValidationFailed(eventIn); + + verify(this.sender, times(1)).send(this.envelopeArgumentCaptor.capture()); + + final JsonEnvelope envelopeOut = this.envelopeArgumentCaptor.getValue(); + assertThat(envelopeOut.metadata().name(), + is(HearingEventProcessor.PUBLIC_HEARING_RESULTS_VALIDATION_FAILED)); + // The processor is a pure passthrough — payload preserved unchanged. + assertThat(envelopeOut.payloadAsJsonObject().getString(FIELD_HEARING_ID), + is(HEARING_ID.toString())); + assertThat(envelopeOut.payloadAsJsonObject().getString(FIELD_HEARING_DAY), + is("2026-05-08")); + assertThat(envelopeOut.payloadAsJsonObject().getString("userId"), + is(USER_ID.toString())); + } + @Test public void shouldPublishDraftResultDeletedV2PublicEvent() { diff --git a/hearing-event/hearing-event-processor/src/yaml/json/schema/hearing.events.results-validation-failed.json b/hearing-event/hearing-event-processor/src/yaml/json/schema/hearing.events.results-validation-failed.json new file mode 100644 index 0000000000..4d679fcdf3 --- /dev/null +++ b/hearing-event/hearing-event-processor/src/yaml/json/schema/hearing.events.results-validation-failed.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://cpp.moj.gov.uk/hearing/json/schema/event/hearing.events.results-validation-failed.json", + "type": "object", + "properties": { + "hearingId": { + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + } + }, + "required": [ + "hearingId" + ], + "additionalProperties": true +} diff --git a/hearing-event/hearing-event-processor/src/yaml/json/schema/public.hearing.results-validation-failed.json b/hearing-event/hearing-event-processor/src/yaml/json/schema/public.hearing.results-validation-failed.json new file mode 100644 index 0000000000..b455981ac7 --- /dev/null +++ b/hearing-event/hearing-event-processor/src/yaml/json/schema/public.hearing.results-validation-failed.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://cpp.moj.gov.uk/hearing/json/schema/event/public.hearing.results-validation-failed.json", + "type": "object", + "properties": { + "hearingId": { + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + } + }, + "required": [ + "hearingId" + ], + "additionalProperties": true +} diff --git a/hearing-event/hearing-event-processor/src/yaml/public-publications-descriptor.yaml b/hearing-event/hearing-event-processor/src/yaml/public-publications-descriptor.yaml index c745ea3ce5..34f9f16e87 100644 --- a/hearing-event/hearing-event-processor/src/yaml/public-publications-descriptor.yaml +++ b/hearing-event/hearing-event-processor/src/yaml/public-publications-descriptor.yaml @@ -63,6 +63,9 @@ subscriptions_descriptor: - name: public.hearing.manage-results-failed schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/public-manage-results-failed.json + - name: public.hearing.results-validation-failed + schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/public.hearing.results-validation-failed.json + # Pleas - name: public.hearing.plea-updated schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/public-plea-updated.json diff --git a/hearing-event/hearing-event-processor/src/yaml/subscriptions-descriptor.yaml b/hearing-event/hearing-event-processor/src/yaml/subscriptions-descriptor.yaml index f99c72e77f..6bf407c210 100644 --- a/hearing-event/hearing-event-processor/src/yaml/subscriptions-descriptor.yaml +++ b/hearing-event/hearing-event-processor/src/yaml/subscriptions-descriptor.yaml @@ -317,6 +317,10 @@ subscriptions_descriptor: - name: hearing.share-results-failed schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/hearing.share-results-failed.json + # Results Validation Failed + - name: hearing.events.results-validation-failed + schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/hearing.events.results-validation-failed.json + - name: hearing.event.approval-rejected schema_uri: http://cpp.moj.gov.uk/hearing/json/schema/event/hearing.event.approval-rejected.json diff --git a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java index c3f66e811f..00ea86154f 100644 --- a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java +++ b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java @@ -25,6 +25,7 @@ import javax.annotation.concurrent.NotThreadSafe; import javax.json.JsonObject; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +41,22 @@ public void setUpTest() { eventTime = new UtcClock().now().minusMinutes(5L); } + /** + * Hearings created here use templates that explicitly set + * {@code reportingRestrictionReason=""} (the + * {@code initiateHearingTemplateWithParamNoReportingRestriction*} variants). + * They sit in the same {@code courtCentreId}/{@code courtRoom2Id} bucket + * as + * {@code PublishLatestCourtCentreHearingEventsIT.shouldRequestToPublishCourtListOpenCaseProsecution} + * and would otherwise leak into that test's PUB-DISPLAY XML, breaking its + * defendant-redaction assertion. Clean up after each method so nothing + * survives this class. + */ + @AfterEach + public void tearDownTest() { + cleanDatabase("ha_hearing"); + } + @Test public void shouldRequestToPublishCourtListWithCaseRestriction() throws Exception { final CourtListRestrictionSteps courtListRestrictionSteps = new CourtListRestrictionSteps(); diff --git a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/PublishLatestCourtCentreHearingEventsIT.java b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/PublishLatestCourtCentreHearingEventsIT.java index f3aedb7aff..1192b9d1e1 100644 --- a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/PublishLatestCourtCentreHearingEventsIT.java +++ b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/PublishLatestCourtCentreHearingEventsIT.java @@ -60,6 +60,14 @@ public void setUpTest() { @Test public void shouldRequestToPublishCourtListOpenCaseProsecution() throws NoSuchAlgorithmException { + // CourtListRestrictionIT (which runs alphabetically before this class) + // creates hearings in the same courtCentreId/courtRoom2Id with + // reportingRestrictionReason explicitly empty, and event-sourced inserts + // can land in ha_hearing after CourtListRestrictionIT's @AfterEach + // cleanup has run. Without clearing the table here, those leaked + // hearings appear in this test's PUB-DISPLAY query and break the + // empty-defendant assertion below. + cleanDatabase("ha_hearing"); stubOrganisationalUnit(fromString(courtCentreId), "OUCODE"); createHearingEvent(randomUUID(), courtRoom2Id, randomUUID().toString(), OPEN_CASE_PROSECUTION_EVENT_DEFINITION_ID, eventTime, of(hearingTypeId), courtCentreId); diff --git a/hearing-query/hearing-query-api/src/test/java/uk/gov/moj/cpp/hearing/query/api/HearingQueryApiTest.java b/hearing-query/hearing-query-api/src/test/java/uk/gov/moj/cpp/hearing/query/api/HearingQueryApiTest.java index 35f63d2620..699faebfca 100644 --- a/hearing-query/hearing-query-api/src/test/java/uk/gov/moj/cpp/hearing/query/api/HearingQueryApiTest.java +++ b/hearing-query/hearing-query-api/src/test/java/uk/gov/moj/cpp/hearing/query/api/HearingQueryApiTest.java @@ -9,8 +9,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -18,6 +22,8 @@ import uk.gov.justice.core.courts.CrackedIneffectiveTrial; import uk.gov.justice.hearing.courts.GetHearings; +import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; +import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; import uk.gov.justice.services.core.annotation.Handles; import uk.gov.justice.services.core.dispatcher.EnvelopePayloadTypeConverter; import uk.gov.justice.services.core.dispatcher.JsonEnvelopeRepacker; @@ -29,6 +35,12 @@ import uk.gov.moj.cpp.external.domain.progression.prosecutioncases.ProsecutionCase; import uk.gov.moj.cpp.hearing.event.nowsdomain.referencedata.nows.CrackedIneffectiveVacatedTrialTypes; import uk.gov.moj.cpp.hearing.event.nowsdomain.referencedata.resultdefinition.Prompt; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.AccessibleApplications; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.AccessibleCases; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.DDJChecker; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.RecorderChecker; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.UsersAndGroupsService; +import uk.gov.moj.cpp.hearing.query.api.service.accessfilter.vo.Permissions; import uk.gov.moj.cpp.hearing.query.api.service.progression.ProgressionService; import uk.gov.moj.cpp.hearing.query.api.service.referencedata.PIEventMapperCache; import uk.gov.moj.cpp.hearing.query.api.service.referencedata.ReferenceDataService; @@ -41,9 +53,11 @@ import uk.gov.moj.cpp.hearing.query.view.response.Timeline; import uk.gov.moj.cpp.hearing.query.view.response.TimelineHearingSummary; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.GetShareResultsV2Response; +import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.HearingDetailsResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.NowListResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.ProsecutionCaseResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.TargetListResponse; +import uk.gov.moj.cpp.hearing.query.view.service.HearingService; import java.io.File; import java.lang.reflect.Method; @@ -52,6 +66,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -165,6 +180,36 @@ public class HearingQueryApiTest { @Mock private UserGroupQueryService userGroupQueryService; + @Mock + private UsersAndGroupsService usersAndGroupsService; + + @Mock + private DDJChecker ddjChecker; + + @Mock + private RecorderChecker recorderChecker; + + @Mock + private AccessibleCases accessibleCases; + + @Mock + private AccessibleApplications accessibleApplications; + + @Mock + private HearingService hearingService; + + @Mock + private JsonObjectToObjectConverter jsonObjectToObjectConverter; + + @Mock + private ObjectToJsonObjectConverter objectToJsonObjectConverter; + + @Mock + private Permissions mockPermissions; + + @Mock + private Envelope mockHearingDetailsResponseEnvelope; + private Map apiMethodsToHandlerNames; @BeforeEach @@ -440,6 +485,41 @@ public void shouldNotProcessGetHearingEventLogCountForNonHMCTSUser() { verify(hearingEventQueryView, times(0)).getHearingEventLogCount(any(JsonEnvelope.class)); } + @Test + public void shouldFindHearingForManageHearing() { + final UUID userId = randomUUID(); + final UUID hearingId = randomUUID(); + + final JsonEnvelope query = mock(JsonEnvelope.class, RETURNS_DEEP_STUBS); + when(query.metadata().userId()).thenReturn(Optional.of(userId.toString())); + when(query.payloadAsJsonObject()).thenReturn(createObjectBuilder().add("hearingId", hearingId.toString()).build()); + + when(referenceDataService.listAllCrackedIneffectiveVacatedTrialTypes()).thenReturn(crackedIneffectiveVacatedTrialTypes); + when(usersAndGroupsService.permissions(userId.toString())).thenReturn(mockPermissions); + when(ddjChecker.isDDJ(mockPermissions)).thenReturn(false); + when(recorderChecker.isRecorder(mockPermissions)).thenReturn(false); + when(hearingQueryView.findHearing(any(), any(), any(), anyBoolean())).thenReturn(mockHearingDetailsResponseEnvelope); + when(mockEnvelopePayloadTypeConverter.convert(any(), any(Class.class))).thenReturn(mockJsonValueEnvelope); + + final JsonEnvelope repackedEnvelope = EnvelopeFactory.createEnvelope("hearing.get.hearing", createObjectBuilder().add("hearingId", hearingId.toString()).build()); + when(mockJsonEnvelopeRepacker.repack(mockJsonValueEnvelope)).thenReturn(repackedEnvelope); + + final HearingDetailsResponse hearingDetailsResponse = mock(HearingDetailsResponse.class); + final HearingDetailsResponse filteredResponse = mock(HearingDetailsResponse.class); + final javax.json.JsonObject filteredJsonObject = createObjectBuilder().add("hearingId", hearingId.toString()).build(); + + when(jsonObjectToObjectConverter.convert(any(javax.json.JsonObject.class), eq(HearingDetailsResponse.class))).thenReturn(hearingDetailsResponse); + when(hearingService.filterOutProsecutionCases(hearingDetailsResponse)).thenReturn(filteredResponse); + when(objectToJsonObjectConverter.convert(filteredResponse)).thenReturn(filteredJsonObject); + + final JsonEnvelope result = hearingQueryApi.findHearingForManageHearing(query); + + verify(hearingService).validateUserPermissionForApplicationType(query); + verify(hearingService).filterOutProsecutionCases(hearingDetailsResponse); + verify(objectToJsonObjectConverter).convert(filteredResponse); + assertThat(result, is(notNullValue())); + } + private Set buildPIEventCache() { final UUID cpHearingEventId_1 = randomUUID(); final UUID cpHearingEventId_2 = UUID.fromString("abdaeb88-8952-4c07-99c4-d27c39d4e63a"); diff --git a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryView.java b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryView.java index 01001161e0..67db383ab8 100644 --- a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryView.java +++ b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryView.java @@ -26,6 +26,7 @@ import uk.gov.moj.cpp.hearing.mapping.CourtApplicationsSerializer; import uk.gov.moj.cpp.hearing.persist.entity.ha.CourtCentre; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; +import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingDay; import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingDefenceCounsel; import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingEvent; import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingProsecutionCounsel; @@ -555,13 +556,14 @@ private List getActiveHearingsForCourtRoom(final UUID hearingId, final Loc return Collections.emptyList(); } - final CourtCentre courtCentre = optionalCourtCentre.get(); + final CourtCentre topLevel = optionalCourtCentre.get(); + final Optional matchedDay = hearingService.getHearingDayByHearingIdAndDate(hearingId, date); + + final UUID centreId = matchedDay.map(HearingDay::getCourtCentreId).filter(java.util.Objects::nonNull).orElse(topLevel.getId()); + final UUID roomId = matchedDay.map(HearingDay::getCourtRoomId).filter(java.util.Objects::nonNull).orElse(topLevel.getRoomId()); + final List hearingEvents = - hearingService - .getHearingEvents( - courtCentre.getId(), - courtCentre.getRoomId(), - date); + hearingService.getHearingEvents(centreId, roomId, date); return getActiveHearingIdsByHearingEvents(hearingEvents); } diff --git a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformer.java b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformer.java index 11a1aab736..20129223ee 100644 --- a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformer.java +++ b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformer.java @@ -41,6 +41,7 @@ import uk.gov.moj.cpp.listing.domain.referencedata.CourtRoomMapping; import java.math.BigInteger; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; @@ -74,30 +75,35 @@ public class HearingListXhibitResponseTransformer { private static final DateTimeFormatter dateTimeFormatter = ofPattern("yyyy-MM-dd'T'HH:mm'Z'"); public CurrentCourtStatus transformFrom(final HearingEventsToHearingMapper hearingEventsToHearingMapper) { + return transformFrom(hearingEventsToHearingMapper, null); + } + + public CurrentCourtStatus transformFrom(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final LocalDate publishDate) { return currentCourtStatus() - .withCourt(getCourt(hearingEventsToHearingMapper)) + .withCourt(getCourt(hearingEventsToHearingMapper, publishDate)) .build(); } - private Court getCourt(final HearingEventsToHearingMapper hearingEventsToHearingMapper) { + private Court getCourt(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final LocalDate publishDate) { final Map courtSiteMap = new HashMap<>(); return court() //Logically all hearings will belong to single court centre, therefore we pick up the first one .withCourtName(hearingEventsToHearingMapper.getHearingList().get(0).getCourtCentre().getName()) - .withCourtSites(getCourtSites(hearingEventsToHearingMapper, courtSiteMap)) + .withCourtSites(getCourtSites(hearingEventsToHearingMapper, courtSiteMap, publishDate)) .build(); } - private List getCourtSites(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final Map courtSiteMap) { + private List getCourtSites(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final Map courtSiteMap, final LocalDate publishDate) { return hearingEventsToHearingMapper.getHearingList() .stream() - .map(hearing -> getCourtSite(hearingEventsToHearingMapper, hearing, courtSiteMap)) + .map(hearing -> getCourtSite(hearingEventsToHearingMapper, hearing, courtSiteMap, publishDate)) .distinct() .collect(toList()); } - private CourtSite getCourtSite(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final Hearing hearing, final Map courtSiteMap) { - final CourtRoomMapping courtRoomMapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(hearing.getCourtCentre().getId(), hearing.getCourtCentre().getRoomId()); + private CourtSite getCourtSite(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final Hearing hearing, final Map courtSiteMap, final LocalDate publishDate) { + final ResolvedRoom resolved = resolveCentreAndRoom(hearing, publishDate); + final CourtRoomMapping courtRoomMapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(resolved.centreId, resolved.roomId); CourtSite courtSite = courtSiteMap.get(courtRoomMapping.getCrestCourtSiteUUID()); if (courtSite == null) { courtSite = courtSite() @@ -106,31 +112,34 @@ private CourtSite getCourtSite(final HearingEventsToHearingMapper hearingEventsT .withCourtRooms(new ArrayList<>()) .build(); } - courtSite.getCourtRooms().addAll(getCourtRoomsForCourtSite(hearingEventsToHearingMapper, courtRoomMapping.getCrestCourtSiteUUID())); + courtSite.getCourtRooms().addAll(getCourtRoomsForCourtSite(hearingEventsToHearingMapper, courtRoomMapping.getCrestCourtSiteUUID(), publishDate)); return courtSite; } - private List getCourtRoomsForCourtSite(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final UUID crestCourtSiteId) { + private List getCourtRoomsForCourtSite(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final UUID crestCourtSiteId, final LocalDate publishDate) { final Map courtRoomMap = new HashMap<>(); return hearingEventsToHearingMapper.getHearingList() .stream() - .filter(hearing -> isHearingForCourtSite(crestCourtSiteId, hearing)) - .map(hearing -> getCourtRoom(hearingEventsToHearingMapper, hearing, courtRoomMap)) + .filter(hearing -> isHearingForCourtSite(crestCourtSiteId, hearing, publishDate)) + .map(hearing -> getCourtRoom(hearingEventsToHearingMapper, hearing, courtRoomMap, publishDate)) .distinct() .collect(toList()); } - private boolean isHearingForCourtSite(final UUID crestCourtSiteId, final Hearing hearing) { - final CourtRoomMapping mapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(hearing.getCourtCentre().getId(), hearing.getCourtCentre().getRoomId()); + private boolean isHearingForCourtSite(final UUID crestCourtSiteId, final Hearing hearing, final LocalDate publishDate) { + final ResolvedRoom resolved = resolveCentreAndRoom(hearing, publishDate); + final CourtRoomMapping mapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(resolved.centreId, resolved.roomId); final UUID siteId = mapping.getCrestCourtSiteUUID(); return crestCourtSiteId != null && crestCourtSiteId.equals(siteId); } private CourtRoom getCourtRoom(final HearingEventsToHearingMapper hearingEventsToHearingMapper, final Hearing hearing, - final Map courtRoomMap) { - final UUID courtRoomKey = hearing.getCourtCentre().getRoomId(); - final CourtRoomMapping courtRoomMapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(hearing.getCourtCentre().getId(), hearing.getCourtCentre().getRoomId()); + final Map courtRoomMap, + final LocalDate publishDate) { + final ResolvedRoom resolved = resolveCentreAndRoom(hearing, publishDate); + final UUID courtRoomKey = resolved.roomId; + final CourtRoomMapping courtRoomMapping = commonXhibitReferenceDataService.getCourtRoomMappingBy(resolved.centreId, resolved.roomId); CourtRoom courtRoom = courtRoomMap.get(courtRoomKey); final Set activeHearingIds = hearingEventsToHearingMapper.getActiveHearingIds(); @@ -364,4 +373,30 @@ private Set getReportingRestrictionLabel(final Offence offence, final Se ofNullable(reportingRestriction).ifPresent(restriction -> publicNoticesValue.add(restriction.getLabel()))); return publicNoticesValue; } + + private ResolvedRoom resolveCentreAndRoom(final Hearing hearing, final LocalDate publishDate) { + final UUID topCentreId = hearing.getCourtCentre().getId(); + final UUID topRoomId = hearing.getCourtCentre().getRoomId(); + if (publishDate == null || hearing.getHearingDays() == null) { + return new ResolvedRoom(topCentreId, topRoomId); + } + return hearing.getHearingDays().stream() + .filter(day -> nonNull(day.getSittingDay()) + && day.getSittingDay().toLocalDate().equals(publishDate)) + .findFirst() + .map(day -> new ResolvedRoom( + nonNull(day.getCourtCentreId()) ? day.getCourtCentreId() : topCentreId, + nonNull(day.getCourtRoomId()) ? day.getCourtRoomId() : topRoomId)) + .orElse(new ResolvedRoom(topCentreId, topRoomId)); + } + + private static final class ResolvedRoom { + private final UUID centreId; + private final UUID roomId; + + ResolvedRoom(final UUID centreId, final UUID roomId) { + this.centreId = centreId; + this.roomId = roomId; + } + } } diff --git a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingService.java b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingService.java index a1b8692d87..a5ae1adfca 100644 --- a/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingService.java +++ b/hearing-query/hearing-query-view/src/main/java/uk/gov/moj/cpp/hearing/query/view/service/HearingService.java @@ -227,7 +227,7 @@ public Optional getHearingsForWebPage(final List court if (!hearingList.isEmpty()) { final HearingEventsToHearingMapper hearingEventsToHearingMapper = new HearingEventsToHearingMapper(activeHearingEventList, hearingList, activeHearingEventList); - return Optional.of(hearingListXhibitResponseTransformer.transformFrom(hearingEventsToHearingMapper)); + return Optional.of(hearingListXhibitResponseTransformer.transformFrom(hearingEventsToHearingMapper, localDate)); } return empty(); } @@ -332,7 +332,7 @@ public Optional getHearingsByDate(final List courtCent if (!hearingList.isEmpty()) { final HearingEventsToHearingMapper hearingEventsToHearingMapper = new HearingEventsToHearingMapper(activeHearingEventList, hearingList, allHearingEvents); - final CurrentCourtStatus currentCourtStatus = hearingListXhibitResponseTransformer.transformFrom(hearingEventsToHearingMapper); + final CurrentCourtStatus currentCourtStatus = hearingListXhibitResponseTransformer.transformFrom(hearingEventsToHearingMapper, localDate); return Optional.of(currentCourtStatus); } return empty(); @@ -596,6 +596,11 @@ public Optional getCourtCenterByHearingId(UUID hearingId) { return Optional.ofNullable(hearingRepository.findCourtCenterByHearingId(hearingId)); } + @Transactional + public Optional getHearingDayByHearingIdAndDate(final UUID hearingId, final LocalDate date) { + return Optional.ofNullable(hearingRepository.findHearingDayByHearingIdAndDate(hearingId, date)); + } + @Transactional public Optional getHearingEventDefinition(UUID definitionId) { final HearingEventDefinition hearingEventDefinition = hearingEventDefinitionRepository.findBy(definitionId); diff --git a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryViewTest.java b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryViewTest.java index 09549b5d4a..14833c9620 100644 --- a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryViewTest.java +++ b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/HearingEventQueryViewTest.java @@ -433,6 +433,38 @@ public void shouldGetActiveHearingIdsWhenAnotherHearingIsActiveInTheSameCourtRoo ))); } + @Test + public void shouldUsePerDayCourtRoom_whenHearingDayHasOverrideRoom() { + final UUID dayCentreId = randomUUID(); + final UUID dayRoomId = randomUUID(); + + final HearingDay matchedDay = new HearingDay(); + matchedDay.setCourtCentreId(dayCentreId); + matchedDay.setCourtRoomId(dayRoomId); + + when(hearingService.getCourtCenterByHearingId(HEARING_ID_1)).thenReturn(Optional.of(mockHearing().getCourtCentre())); + when(hearingService.getHearingDayByHearingIdAndDate(HEARING_ID_1, EVENT_TIME.toLocalDate())) + .thenReturn(Optional.of(matchedDay)); + when(hearingService.getHearingEvents(dayCentreId, dayRoomId, EVENT_TIME.toLocalDate())) + .thenReturn(mockActiveHearingEvents(HEARING_ID_2)); + + final JsonEnvelope query = envelopeFrom( + metadataWithRandomUUIDAndName(), + createObjectBuilder() + .add(FIELD_HEARING_ID, HEARING_ID_1.toString()) + .add(FIELD_EVENT_DATE, EVENT_TIME.toLocalDate().toString()) + .build()); + + final Envelope actualActiveHearingIdsForCourtRoom = target.getActiveHearingsForCourtRoom(query); + + verify(hearingService).getHearingDayByHearingIdAndDate(HEARING_ID_1, EVENT_TIME.toLocalDate()); + verify(hearingService).getHearingEvents(dayCentreId, dayRoomId, EVENT_TIME.toLocalDate()); + assertThat(actualActiveHearingIdsForCourtRoom.payload().toString(), allOf( + hasJsonPath(format("$.%s", FIELD_ACTIVE_HEARINGS), hasSize(1)), + hasJsonPath(format("$.%s[0]", FIELD_ACTIVE_HEARINGS), equalTo(HEARING_ID_2.toString())) + )); + } + @Test public void shouldGetActiveHearingIdsInCaseOfSamePauseAndResumeEventsRecorded() { when(hearingService.getCourtCenterByHearingId(HEARING_ID_1)).thenReturn(Optional.of(mockHearing().getCourtCentre())); diff --git a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformerTest.java b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformerTest.java index 043c2b759f..5991c4e129 100644 --- a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformerTest.java +++ b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingListXhibitResponseTransformerTest.java @@ -8,6 +8,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; @@ -45,6 +47,7 @@ import uk.gov.moj.cpp.listing.domain.referencedata.CourtRoomMapping; import java.math.BigInteger; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; @@ -176,6 +179,60 @@ public void shouldTransformFrom() { } + @Test + public void shouldUseDayCourtCentreAndRoom_whenHearingDayHasOverrideRoom() { + final UUID topCentreId = randomUUID(); + final UUID topRoomId = randomUUID(); + final UUID dayCentreId = randomUUID(); + final UUID dayRoomId = randomUUID(); + final UUID hearingId = randomUUID(); + final LocalDate publishDate = LocalDate.now(); + final ZonedDateTime sittingDay = publishDate.atStartOfDay(ZonedDateTime.now().getZone()); + + final HearingEvent hearingEvent = HearingEvent.hearingEvent().build(); + final List hearingList = asList(hearing); + final List prosecutionCases = asList(prosecutionCase); + final List defendantList = asList(defendant); + final List hearingDays = asList(hearingDay); + final ProsecutionCaseIdentifier prosecutionCaseIdentifier = ProsecutionCaseIdentifier + .prosecutionCaseIdentifier().withCaseURN("caseURN").build(); + + when(hearingEventsToHearingMapper.getActiveHearingIds()).thenReturn(new HashSet<>()); + when(prosecutionCase.getProsecutionCaseIdentifier()).thenReturn(prosecutionCaseIdentifier); + when(prosecutionCase.getDefendants()).thenReturn(defendantList); + when(defendant.getPersonDefendant()).thenReturn(personDefendant); + when(personDefendant.getPersonDetails()).thenReturn(person); + when(person.getFirstName()).thenReturn("firstName"); + when(person.getMiddleName()).thenReturn("middleName"); + when(person.getLastName()).thenReturn("lastName"); + + when(hearing.getHearingDays()).thenReturn(hearingDays); + when(hearingDay.getSittingDay()).thenReturn(sittingDay); + when(hearingDay.getCourtCentreId()).thenReturn(dayCentreId); + when(hearingDay.getCourtRoomId()).thenReturn(dayRoomId); + + when(hearing.getId()).thenReturn(hearingId); + when(hearing.getType()).thenReturn(HearingType.hearingType().withDescription("hearingTypeDescription").build()); + when(hearing.getProsecutionCases()).thenReturn(prosecutionCases); + when(hearing.getCourtCentre()).thenReturn(CourtCentre.courtCentre().withName(COURT_NAME).withRoomId(topRoomId).withId(topCentreId).build()); + when(hearingEventsToHearingMapper.getHearingList()).thenReturn(hearingList); + when(hearingEventsToHearingMapper.getAllHearingEventBy(hearingId)).thenReturn(Optional.of(hearingEvent)); + + when(commonXhibitReferenceDataService.getCourtRoomMappingBy(eq(dayCentreId), eq(dayRoomId))).thenReturn(courtRoomMapping); + when(courtRoomMapping.getCrestCourtRoomName()).thenReturn("Day room name"); + when(courtRoomMapping.getCrestCourtSiteUUID()).thenReturn(randomUUID()); + + mockHearingTypeId(); + when(commonXhibitReferenceDataService.getXhibitHearingType(hearingTypeId).getExhibitHearingDescription()).thenReturn("Plea"); + + final CurrentCourtStatus currentCourtStatus = hearingListXhibitResponseTransformer.transformFrom(hearingEventsToHearingMapper, publishDate); + + verify(commonXhibitReferenceDataService, atLeastOnce()).getCourtRoomMappingBy(eq(dayCentreId), eq(dayRoomId)); + final CourtRoom courtRoom = currentCourtStatus.getCourt().getCourtSites().get(0).getCourtRooms().get(0); + assertThat(courtRoom.getCourtRoomName(), is("Day room name")); + assertThat(courtRoom.getCourtRoomId(), is(dayRoomId)); + } + @Test public void shouldTransformFromWithInProgressEventAndActiveCase() { final UUID courtCentreId = randomUUID(); diff --git a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingServiceTest.java b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingServiceTest.java index 2edc7741ec..c3e6d111af 100644 --- a/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingServiceTest.java +++ b/hearing-query/hearing-query-view/src/test/java/uk/gov/moj/cpp/hearing/query/view/service/HearingServiceTest.java @@ -34,6 +34,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.justice.core.courts.ApplicationStatus.FINALISED; @@ -72,6 +73,7 @@ import static uk.gov.moj.cpp.hearing.test.matchers.ElementAtListMatcher.first; import uk.gov.justice.core.courts.Address; +import uk.gov.justice.core.courts.CrackedIneffectiveTrial; import uk.gov.justice.core.courts.CourtApplication; import uk.gov.justice.core.courts.CourtApplicationCase; import uk.gov.justice.core.courts.CourtApplicationParty; @@ -119,6 +121,8 @@ import uk.gov.moj.cpp.hearing.persist.NowsRepository; import uk.gov.moj.cpp.hearing.persist.entity.application.ApplicationDraftResult; import uk.gov.moj.cpp.hearing.persist.entity.ha.CourtCentre; +import uk.gov.moj.cpp.hearing.persist.entity.ha.DraftResult; +import uk.gov.moj.cpp.hearing.persist.entity.ha.Now; import uk.gov.moj.cpp.hearing.persist.entity.ha.Defendant; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingApplication; @@ -140,8 +144,11 @@ import uk.gov.moj.cpp.hearing.query.view.response.TimelineHearingSummary; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.ApplicationTarget; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.ApplicationTargetListResponse; +import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.DraftResultResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.GetShareResultsV2Response; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.HearingDetailsResponse; +import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.NowListResponse; +import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.NowResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.ProsecutionCaseResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.TargetListResponse; import uk.gov.moj.cpp.hearing.query.view.response.hearingresponse.xhibit.CaseDetail; @@ -152,12 +159,14 @@ import uk.gov.moj.cpp.hearing.query.view.service.userdata.UserDataService; import uk.gov.moj.cpp.hearing.query.view.service.ctl.ReferenceDataService; import uk.gov.moj.cpp.hearing.repository.DocumentRepository; +import uk.gov.moj.cpp.hearing.repository.DraftResultRepository; import uk.gov.moj.cpp.hearing.repository.HearingApplicationRepository; import uk.gov.moj.cpp.hearing.repository.HearingEventDefinitionRepository; import uk.gov.moj.cpp.hearing.repository.HearingEventPojo; import uk.gov.moj.cpp.hearing.repository.HearingEventRepository; import uk.gov.moj.cpp.hearing.repository.HearingRepository; import uk.gov.moj.cpp.hearing.repository.HearingYouthCourtDefendantsRepository; +import uk.gov.moj.cpp.hearing.repository.NowRepository; import uk.gov.moj.cpp.hearing.repository.NowsMaterialRepository; import java.io.IOException; @@ -265,6 +274,12 @@ public class HearingServiceTest { @Mock private ProgressionService progressionService; + @Mock + private NowRepository nowRepository; + + @Mock + private DraftResultRepository draftResultRepository; + protected static String getStringFromResource(final String path) throws IOException { return Resources.toString(getResource(path), defaultCharset()); } @@ -1514,7 +1529,7 @@ public void shouldReturnLatestHearingByCourtCentreIdsAndLatestModifiedTime() { when(hearingEventRepository.findLatestHearingsForThatDayByCourt(courtCentreIds.get(0), now, hearingEventRequiredDefinitionsIds)).thenReturn(hearingEventResult); when(hearingRepository.findBy(hearingEvent.getHearingId())).thenReturn(hearing); when(hearingJPAMapper.fromJPAWithCourtListRestrictions(hearing)).thenReturn(hearinPojo); - when(hearingListXhibitResponseTransformer.transformFrom(any(HearingEventsToHearingMapper.class))).thenReturn(expectedCurrentCourtStatus); + when(hearingListXhibitResponseTransformer.transformFrom(any(HearingEventsToHearingMapper.class), any(LocalDate.class))).thenReturn(expectedCurrentCourtStatus); final Optional response = hearingService.getHearingsForWebPage(courtCentreIds, now, hearingEventRequiredDefinitionsIds); @@ -1557,7 +1572,7 @@ public void shouldReturnHearingsByCounrtCentreIdsAndDate() { when(hearingEventRepository.findLatestHearingsForThatDayByCourts(courtCentreIds, now, hearingEventRequiredDefinitionsIds)).thenReturn(hearingEventResult); final CurrentCourtStatus expectedCurrentCourtStatus = getCurrentCourtStatusWithMultipleCases(hearingEvent); - when(hearingListXhibitResponseTransformer.transformFrom(any(HearingEventsToHearingMapper.class))).thenReturn(expectedCurrentCourtStatus); + when(hearingListXhibitResponseTransformer.transformFrom(any(HearingEventsToHearingMapper.class), any(LocalDate.class))).thenReturn(expectedCurrentCourtStatus); final Optional response = hearingService.getHearingsByDate(courtCentreIds, now, hearingEventRequiredDefinitionsIds); assertCurrentCourtStatus(response.get(), expectedCurrentCourtStatus); @@ -1670,6 +1685,29 @@ public void shouldNotGetCourtCenterByNonExistingHearingId() { assertThat(optionalCourtCentre.isPresent(), is(false)); } + @Test + public void shouldGetHearingDayByHearingIdAndDate() { + final UUID hearingId = randomUUID(); + final LocalDate date = LocalDate.now(); + final HearingDay dayStub = new HearingDay(); + when(hearingRepository.findHearingDayByHearingIdAndDate(hearingId, date)).thenReturn(dayStub); + + final Optional result = hearingService.getHearingDayByHearingIdAndDate(hearingId, date); + + verify(hearingRepository).findHearingDayByHearingIdAndDate(hearingId, date); + assertTrue(result.isPresent()); + assertThat(dayStub, is(result.get())); + } + + @Test + public void shouldReturnEmpty_whenHearingDayNotFound() { + when(hearingRepository.findHearingDayByHearingIdAndDate(Mockito.any(UUID.class), Mockito.any(LocalDate.class))).thenReturn(null); + + final Optional result = hearingService.getHearingDayByHearingIdAndDate(randomUUID(), LocalDate.now()); + + assertThat(result.isPresent(), is(false)); + } + @Test public void shouldGetFutureHearingsByCaseIds() { final UUID caseId = randomUUID(); @@ -2404,9 +2442,328 @@ public void shouldLeaveEmptyDefendantOffencesWhenNoneMatchCourtApplicationOffenc assertThat(result.getHearing().getProsecutionCases(), hasSize(1)); assertThat(result.getHearing().getProsecutionCases().get(0) - .getDefendants().get(0).getOffences(), empty()); + .getDefendants().get(0).getOffences(), empty()); assertThat(result.getHearing().getCourtApplications() - .get(0).getCourtApplicationCases().get(0).getOffences(), hasSize(1)); + .get(0).getCourtApplicationCases().get(0).getOffences(), hasSize(1)); + } + + // ── toBoolean ────────────────────────────────────────────────────────── + + @Test + public void toBoolean_shouldReturnFalse_whenNull() { + assertFalse(HearingService.toBoolean(null)); + } + + @Test + public void toBoolean_shouldReturnTrue_whenBooleanTrue() { + assertTrue(HearingService.toBoolean(Boolean.TRUE)); + } + + @Test + public void toBoolean_shouldReturnFalse_whenBooleanFalse() { + assertFalse(HearingService.toBoolean(Boolean.FALSE)); + } + + @Test + public void toBoolean_shouldReturnFalse_whenNumberZero() { + assertFalse(HearingService.toBoolean(0)); + } + + @Test + public void toBoolean_shouldReturnTrue_whenNumberNonZero() { + assertTrue(HearingService.toBoolean(1)); + assertTrue(HearingService.toBoolean(-5)); + } + + @Test + public void toBoolean_shouldReturnTrue_whenStringTrueVariants() { + assertTrue(HearingService.toBoolean("true")); + assertTrue(HearingService.toBoolean("t")); + assertTrue(HearingService.toBoolean("yes")); + assertTrue(HearingService.toBoolean("y")); + assertTrue(HearingService.toBoolean("1")); + assertTrue(HearingService.toBoolean(" TRUE ")); + } + + @Test + public void toBoolean_shouldReturnFalse_whenStringFalseVariants() { + assertFalse(HearingService.toBoolean("false")); + assertFalse(HearingService.toBoolean("no")); + assertFalse(HearingService.toBoolean("0")); + } + + @Test + public void toBoolean_shouldReturnFalse_whenUnrecognisedType() { + assertFalse(HearingService.toBoolean(new Object())); + } + + // ── getHearingDomainById ─────────────────────────────────────────────── + + @Test + public void shouldReturnMappedDomainHearing_whenHearingFound() { + final UUID hearingId = randomUUID(); + final Hearing hearingEntity = new Hearing(); + final uk.gov.justice.core.courts.Hearing domainHearing = hearing().build(); + when(hearingRepository.findBy(hearingId)).thenReturn(hearingEntity); + when(hearingJPAMapper.fromJPA(hearingEntity)).thenReturn(domainHearing); + + final Optional result = hearingService.getHearingDomainById(hearingId); + + assertTrue(result.isPresent()); + assertThat(result.get(), is(domainHearing)); + } + + @Test + public void shouldReturnEmpty_whenHearingNotFound_getHearingDomainById() { + final UUID hearingId = randomUUID(); + when(hearingRepository.findBy(hearingId)).thenReturn(null); + + final Optional result = hearingService.getHearingDomainById(hearingId); + + assertFalse(result.isPresent()); + } + + // ── fetchCrackedIneffectiveTrial ─────────────────────────────────────── + + @Test + public void shouldReturnNull_whenTrialTypeIdIsNull() { + assertNull(hearingService.fetchCrackedIneffectiveTrial(null, buildCrackedIneffectiveVacatedTrialTypes(randomUUID()))); + } + + @Test + public void shouldReturnNull_whenTrialTypeListIsEmpty() { + final CrackedIneffectiveVacatedTrialTypes emptyTypes = new CrackedIneffectiveVacatedTrialTypes() + .setCrackedIneffectiveVacatedTrialTypes(Collections.emptyList()); + + assertNull(hearingService.fetchCrackedIneffectiveTrial(randomUUID(), emptyTypes)); + } + + @Test + public void shouldReturnNull_whenNoMatchingTrialTypeFound() { + final CrackedIneffectiveVacatedTrialTypes types = buildCrackedIneffectiveVacatedTrialTypes(randomUUID()); + + assertNull(hearingService.fetchCrackedIneffectiveTrial(randomUUID(), types)); + } + + @Test + public void shouldReturnCrackedIneffectiveTrial_whenMatchingTrialTypeFound() { + final UUID trialTypeId = randomUUID(); + final CrackedIneffectiveVacatedTrialTypes types = buildCrackedIneffectiveVacatedTrialTypes(trialTypeId); + + final CrackedIneffectiveTrial result = hearingService.fetchCrackedIneffectiveTrial(trialTypeId, types); + + assertNotNull(result); + assertThat(result.getCode(), is("code")); + assertThat(result.getType(), is("InEffective")); + } + + // ── getNows ──────────────────────────────────────────────────────────── + + @Test + public void shouldReturnEmptyNowListResponse_whenNoNowsFound() { + final UUID hearingId = randomUUID(); + when(nowRepository.findByHearingId(hearingId)).thenReturn(Collections.emptyList()); + + final NowListResponse result = hearingService.getNows(hearingId); + + assertNotNull(result); + assertNull(result.getNows()); + } + + @Test + public void shouldReturnNowListResponse_withMappedNows() { + final UUID hearingId = randomUUID(); + final UUID nowId = randomUUID(); + final Now now = new Now(); + now.setId(nowId); + now.setHearingId(hearingId); + when(nowRepository.findByHearingId(hearingId)).thenReturn(singletonList(now)); + + final NowListResponse result = hearingService.getNows(hearingId); + + assertThat(result.getNows(), hasSize(1)); + assertThat(result.getNows().get(0).getId(), is(nowId)); + assertThat(result.getNows().get(0).getHearingId(), is(hearingId)); + } + + // ── getDraftResult ───────────────────────────────────────────────────── + + @Test + public void shouldReturnDraftResultFromTargets_whenNoDraftResultInRepository() { + final UUID hearingId = randomUUID(); + final String hearingDay = "1"; + when(draftResultRepository.findDraftResultByFilter(hearingId, hearingDay)).thenReturn(Collections.emptyList()); + when(hearingRepository.findTargetsByFilters(hearingId, hearingDay)).thenReturn(Collections.emptyList()); + when(hearingRepository.findProsecutionCasesByHearingId(hearingId)).thenReturn(Collections.emptyList()); + when(targetJPAMapper.fromJPA(anySet(), anySet())).thenReturn(Collections.emptyList()); + + final DraftResultResponse result = hearingService.getDraftResult(hearingId, hearingDay); + + assertNotNull(result); + assertTrue(result.isTarget()); + verify(draftResultRepository).findDraftResultByFilter(hearingId, hearingDay); + verify(hearingRepository).findTargetsByFilters(hearingId, hearingDay); + } + + @Test + public void shouldReturnDraftResultFromRepository_whenDraftResultExists() { + final UUID hearingId = randomUUID(); + final String hearingDay = "1"; + final DraftResult draftResult = mock(DraftResult.class); + when(draftResult.getDraftResultPayload()).thenReturn(objectMapper.createObjectNode().put("key", "value")); + when(draftResultRepository.findDraftResultByFilter(hearingId, hearingDay)).thenReturn(singletonList(draftResult)); + + final DraftResultResponse result = hearingService.getDraftResult(hearingId, hearingDay); + + assertNotNull(result); + assertTrue(result.isTarget()); + verify(draftResultRepository).findDraftResultByFilter(hearingId, hearingDay); + verify(hearingRepository, never()).findTargetsByFilters(any(), any()); + } + + // ── isUserHasPermissionForApplicationTypeCode ────────────────────────── + + @Test + public void shouldReturnTrue_whenPermissionResponsePayloadIsEmpty() { + final Metadata metadata = DefaultJsonMetadata.metadataBuilder() + .withId(randomUUID()).withName("hearing.get.hearing").build(); + when(requester.request(any(), any())).thenReturn( + Envelope.envelopeFrom(Envelope.metadataBuilder().withId(randomUUID()).withName("test").build(), + createObjectBuilder().build())); + + assertTrue(HearingService.isUserHasPermissionForApplicationTypeCode(metadata, requester, "PL302487")); + } + + @Test + public void shouldReturnTrue_whenHasPermissionIsTrue() { + final Metadata metadata = DefaultJsonMetadata.metadataBuilder() + .withId(randomUUID()).withName("hearing.get.hearing").build(); + when(requester.request(any(), any())).thenReturn( + Envelope.envelopeFrom(Envelope.metadataBuilder().withId(randomUUID()).withName("test").build(), + createObjectBuilder().add("hasPermission", true).build())); + + assertTrue(HearingService.isUserHasPermissionForApplicationTypeCode(metadata, requester, "PL302487")); + } + + @Test + public void shouldReturnFalse_whenHasPermissionIsFalse() { + final Metadata metadata = DefaultJsonMetadata.metadataBuilder() + .withId(randomUUID()).withName("hearing.get.hearing").build(); + when(requester.request(any(), any())).thenReturn( + Envelope.envelopeFrom(Envelope.metadataBuilder().withId(randomUUID()).withName("test").build(), + createObjectBuilder().add("hasPermission", false).build())); + + assertFalse(HearingService.isUserHasPermissionForApplicationTypeCode(metadata, requester, "PL302487")); + } + + // ── validateUserPermissionForApplicationType – missing branches ───────── + + @Test + public void shouldNotThrow_whenHearingIdIsAbsentFromPayload() { + final JsonEnvelope envelope = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("hearing.get.hearing").build(), + createObjectBuilder().build()); + + assertDoesNotThrow(() -> hearingService.validateUserPermissionForApplicationType(envelope)); + verify(hearingRepository, never()).findBy(any(UUID.class)); + } + + @Test + public void shouldNotThrow_whenHearingIsNull() { + final UUID hearingId = randomUUID(); + final JsonEnvelope envelope = envelopeFrom( + metadataBuilder().withId(randomUUID()).withName("hearing.get.hearing").build(), + createObjectBuilder().add("hearingId", hearingId.toString()).build()); + when(hearingRepository.findBy(hearingId)).thenReturn(null); + + assertDoesNotThrow(() -> hearingService.validateUserPermissionForApplicationType(envelope)); + verify(courtApplicationsSerializer, never()).courtApplications(anyString()); + } + + // ── getHearingsByDate – missing branches ─────────────────────────────── + + @Test + public void shouldReturnEmpty_whenCourtCentreListIsEmpty_getHearingsByDate() { + final Optional result = hearingService.getHearingsByDate( + Collections.emptyList(), LocalDate.now(), Collections.emptySet()); + + assertFalse(result.isPresent()); + } + + @Test + public void shouldReturnEmpty_whenHearingListIsEmpty_getHearingsByDate() { + final UUID courtCentreId = randomUUID(); + final LocalDate date = LocalDate.now(); + when(hearingEventRepository.findLatestHearingsForThatDayByCourts(any(), any(), any())) + .thenReturn(Collections.emptyList()); + when(hearingRepository.findHearingsByDateAndCourtCentreList(any(), any())) + .thenReturn(Collections.emptyList()); + when(hearingEventRepository.findBy(any(List.class), any(), anySet())) + .thenReturn(Collections.emptyList()); + + final Optional result = hearingService.getHearingsByDate( + singletonList(courtCentreId), date, Collections.singleton(randomUUID())); + + assertFalse(result.isPresent()); + } + + // ── getHearingsForWebPage – missing branches ─────────────────────────── + + @Test + public void shouldReturnEmpty_whenCourtCentreListIsEmpty_getHearingsForWebPage() { + final Optional result = hearingService.getHearingsForWebPage( + Collections.emptyList(), LocalDate.now(), Collections.emptySet()); + + assertFalse(result.isPresent()); + } + + @Test + public void shouldReturnEmpty_whenNoActiveHearings_getHearingsForWebPage() { + final UUID courtCentreId = randomUUID(); + when(hearingEventRepository.findLatestHearingsForThatDayByCourt(any(), any(), any())) + .thenReturn(Collections.emptyList()); + + final Optional result = hearingService.getHearingsForWebPage( + singletonList(courtCentreId), LocalDate.now(), Collections.singleton(randomUUID())); + + assertFalse(result.isPresent()); + } + + // ── filterOutProsecutionCases – empty offences branch ────────────────── + + @Test + public void shouldKeepProsecutionCases_whenApplicationHasCourtApplicationCasesWithNoOffences() { + final uk.gov.justice.core.courts.Hearing hearing = hearing() + .withCourtApplications(singletonList(CourtApplication.courtApplication() + .withCourtApplicationCases(singletonList(CourtApplicationCase.courtApplicationCase() + .withOffences(Collections.emptyList()) + .build())) + .build())) + .withProsecutionCases(singletonList(ProsecutionCase.prosecutionCase() + .withId(randomUUID()) + .build())) + .build(); + final HearingDetailsResponse payload = new HearingDetailsResponse(); + payload.setHearing(hearing); + + final HearingDetailsResponse result = hearingService.filterOutProsecutionCases(payload); + + assertThat(result, is(payload)); + assertThat(result.getHearing().getProsecutionCases(), hasSize(1)); + } + + // ── getTimelineHearingSummariesByApplicationId ────────────────────────── + + @Test + public void shouldReturnEmptyList_whenNoHearingsFoundForApplication() { + final UUID applicationId = randomUUID(); + when(hearingRepository.findAllHearingsByApplicationId(applicationId)).thenReturn(Collections.emptyList()); + + final List result = hearingService.getTimelineHearingSummariesByApplicationId( + applicationId, buildCrackedIneffectiveVacatedTrialTypes(randomUUID()), createObjectBuilder().build()); + + assertNotNull(result); + assertTrue(result.isEmpty()); } } diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapper.java b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapper.java index 6b7fddf052..911c1e0c11 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapper.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapper.java @@ -102,7 +102,9 @@ public Set toJPA(Hearing hearing, final ProsecutionCase prosecutionCa if (null == pojos) { return new HashSet<>(); } - return pojos.stream().map(pojo -> toJPA(hearing, prosecutionCase, pojo)).collect(Collectors.toSet()); + return pojos.stream().filter(p -> prosecutionCase.getId().getId().equals(p.getProsecutionCaseId())) + .map(pojo -> toJPA(hearing, prosecutionCase, pojo)) + .collect(Collectors.toSet()); } uk.gov.justice.core.courts.Defendant fromJPA(final Defendant pojo) { diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapper.java b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapper.java index 6240912288..570950f3df 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapper.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapper.java @@ -257,6 +257,10 @@ public String updateLinkedApplicationStatus(final String courtApplicationsJson, if (courtApplications == null) { courtApplications = emptyList(); } + courtApplications.stream() + .filter(ca -> ofNullable(ca.getCourtApplicationCases()).orElse(emptyList()).stream() + .anyMatch(cac -> prosecutionCaseId.equals(cac.getProsecutionCaseId()))) + .forEach(ca -> ca.setApplicationStatus(status)); return courtApplicationsSerializer.json(courtApplications); } diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepository.java b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepository.java index a2ff2d4a44..554e0f306a 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepository.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/main/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepository.java @@ -38,9 +38,10 @@ public abstract class HearingEventRepository extends AbstractEntityRepository findByFilters(@QueryParam("date") final LocalDate "WHERE hearing.id = :hearingId", singleResult = OPTIONAL) public abstract CourtCentre findCourtCenterByHearingId(@QueryParam("hearingId") final UUID hearingId); + @Query(value = "SELECT day FROM Hearing hearing INNER JOIN hearing.hearingDays day " + + "WHERE hearing.id = :hearingId AND day.date = :date", singleResult = OPTIONAL) + public abstract HearingDay findHearingDayByHearingIdAndDate(@QueryParam("hearingId") final UUID hearingId, + @QueryParam("date") final LocalDate date); + @Query(value = "SELECT target FROM Target target " + "WHERE target.hearing.id = :hearingId") public abstract List findTargetsByHearingId(@QueryParam("hearingId") final UUID hearingId); diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapperTest.java b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapperTest.java index 139ae41214..be91823738 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapperTest.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/DefendantJPAMapperTest.java @@ -1,5 +1,6 @@ package uk.gov.moj.cpp.hearing.mapping; +import static java.util.Arrays.asList; import static org.apache.deltaspike.core.util.ArraysUtils.asSet; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -58,6 +59,20 @@ public void testToJPA() { assertThat(defendantJPAMapper.toJPA(hearingEntity, prosecutionCaseEntity, defendantPojo), whenDefendant(isBean(uk.gov.moj.cpp.hearing.persist.entity.ha.Defendant.class), defendantPojo)); } + @Test + void testToJPAWithMultipleCaseAndDefendants() { + final uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing hearingEntity = aNewHearingJPADataTemplate(2).getHearing(); + final List prosecutionCaseList = hearingEntity.getProsecutionCases().stream().toList(); + final uk.gov.moj.cpp.hearing.persist.entity.ha.Defendant defendantEntity = prosecutionCaseList.get(0).getDefendants().iterator().next(); + final Defendant defendantPojo = defendantJPAMapper.fromJPA(defendantEntity); + final Defendant defendant2 = Defendant.defendant().withValuesFrom(defendantPojo).withId(UUID.randomUUID()).withProsecutionCaseId(prosecutionCaseList.get(1).getId().getId()).build(); + + final List defendants = defendantJPAMapper.toJPA(hearingEntity, prosecutionCaseList.get(1), asList(defendantPojo, defendant2)).stream().toList(); + + assertThat(defendants.size(), is(1)); + assertThat(defendants.get(0).getProsecutionCaseId(), is(prosecutionCaseList.get(1).getId().getId())); + } + @SuppressWarnings("unchecked") public static ElementAtListMatcher whenFirstDefendant(final BeanMatcher m, final uk.gov.moj.cpp.hearing.persist.entity.ha.Defendant entity) { return ElementAtListMatcher.first(whenDefendant((BeanMatcher) m, entity)); diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapperTest.java b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapperTest.java index 527313647b..33be3dd1ee 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapperTest.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/mapping/HearingJPAMapperTest.java @@ -1031,4 +1031,97 @@ public void givenApplicationFinalisedWhenGetHearingShouldReturnIsAmendmentAllowe assertThat(actualHearing.getCourtApplications().size(), is(1)); assertThat(actualHearing.getCourtApplications().get(0).getId(), is(applicationId)); } + + @Test + public void shouldMarkLinkedApplicationAsEjectedWhenProsecutionCaseMatches() { + final UUID prosecutionCaseId = randomUUID(); + final UUID linkedAppId = randomUUID(); + final UUID unlinkedAppId = randomUUID(); + + final List courtApplications = new ArrayList<>(); + courtApplications.add(CourtApplication.courtApplication() + .withId(linkedAppId) + .withApplicationStatus(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED) + .withCourtApplicationCases(singletonList(CourtApplicationCase.courtApplicationCase() + .withProsecutionCaseId(prosecutionCaseId) + .build())) + .build()); + courtApplications.add(CourtApplication.courtApplication() + .withId(unlinkedAppId) + .withApplicationStatus(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED) + .withCourtApplicationCases(singletonList(CourtApplicationCase.courtApplicationCase() + .withProsecutionCaseId(randomUUID()) + .build())) + .build()); + + when(courtApplicationsSerializer.courtApplications(any(String.class))).thenReturn(courtApplications); + when(courtApplicationsSerializer.json(any())).thenReturn("result"); + + hearingJPAMapper.updateLinkedApplicationStatus("json", prosecutionCaseId, uk.gov.justice.core.courts.ApplicationStatus.EJECTED); + + verify(courtApplicationsSerializer).json(courtApplicationCaptor.capture()); + final List captured = courtApplicationCaptor.getValue(); + + assertThat(captured.stream().filter(ca -> ca.getId().equals(linkedAppId)).findFirst().get().getApplicationStatus(), + is(uk.gov.justice.core.courts.ApplicationStatus.EJECTED)); + assertThat(captured.stream().filter(ca -> ca.getId().equals(unlinkedAppId)).findFirst().get().getApplicationStatus(), + is(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED)); + } + + @Test + public void shouldNotMarkApplicationWhenNoProsecutionCaseMatches() { + final UUID prosecutionCaseId = randomUUID(); + final UUID appId = randomUUID(); + + final List courtApplications = new ArrayList<>(); + courtApplications.add(CourtApplication.courtApplication() + .withId(appId) + .withApplicationStatus(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED) + .withCourtApplicationCases(singletonList(CourtApplicationCase.courtApplicationCase() + .withProsecutionCaseId(randomUUID()) + .build())) + .build()); + + when(courtApplicationsSerializer.courtApplications(any(String.class))).thenReturn(courtApplications); + when(courtApplicationsSerializer.json(any())).thenReturn("result"); + + hearingJPAMapper.updateLinkedApplicationStatus("json", prosecutionCaseId, uk.gov.justice.core.courts.ApplicationStatus.EJECTED); + + verify(courtApplicationsSerializer).json(courtApplicationCaptor.capture()); + assertThat(courtApplicationCaptor.getValue().get(0).getApplicationStatus(), + is(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED)); + } + + @Test + public void shouldNotThrowWhenCourtApplicationCasesIsNull() { + final UUID prosecutionCaseId = randomUUID(); + + final List courtApplications = new ArrayList<>(); + courtApplications.add(CourtApplication.courtApplication() + .withId(randomUUID()) + .withApplicationStatus(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED) + .withCourtApplicationCases(null) + .build()); + + when(courtApplicationsSerializer.courtApplications(any(String.class))).thenReturn(courtApplications); + when(courtApplicationsSerializer.json(any())).thenReturn("result"); + + hearingJPAMapper.updateLinkedApplicationStatus("json", prosecutionCaseId, uk.gov.justice.core.courts.ApplicationStatus.EJECTED); + + verify(courtApplicationsSerializer).json(courtApplicationCaptor.capture()); + assertThat(courtApplicationCaptor.getValue().get(0).getApplicationStatus(), + is(uk.gov.justice.core.courts.ApplicationStatus.UN_ALLOCATED)); + } + + @Test + public void shouldReturnEmptyJsonWhenCourtApplicationsIsNull() { + when(courtApplicationsSerializer.courtApplications(any(String.class))).thenReturn(null); + when(courtApplicationsSerializer.json(any())).thenReturn("[]"); + + final String result = hearingJPAMapper.updateLinkedApplicationStatus("json", randomUUID(), uk.gov.justice.core.courts.ApplicationStatus.EJECTED); + + assertThat(result, is("[]")); + verify(courtApplicationsSerializer).json(courtApplicationCaptor.capture()); + assertThat(courtApplicationCaptor.getValue().isEmpty(), is(true)); + } } diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepositoryTest.java b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepositoryTest.java index 975e96331f..278650fca0 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepositoryTest.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingEventRepositoryTest.java @@ -17,7 +17,9 @@ import uk.gov.justice.services.test.utils.persistence.BaseTransactionalTest; import uk.gov.moj.cpp.hearing.persist.entity.ha.CourtCentre; import uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing; +import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingDay; import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingEvent; +import uk.gov.moj.cpp.hearing.persist.entity.ha.HearingSnapshotKey; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -247,6 +249,33 @@ public void shouldReturnEmptyListForHearingEvents_AsNoHearingEventsRecodedForThe assertThat(hearingEvents.size(), is(0)); } + @Test + public void shouldFindHearingEvents_byPerDayCourtRoom_whenHearingDayOverridesTopLevelRoom() { + final UUID dayRoomId = randomUUID(); + givenHearingExistsWithCourtCentreAndDayOverride(EVENT_TIME.toLocalDate(), COURT_CENTRE_ID, dayRoomId); + givenSingleAlterableFalseHearingEvent(HEARING_EVENT_ID_1, HEARING_ID_1, EVENT_TIME); + + final List matchedByDayRoom = hearingEventRepository + .findHearingEvents(COURT_CENTRE_ID, dayRoomId, EVENT_TIME.toLocalDate()); + assertThat(matchedByDayRoom.size(), is(1)); + assertThat(matchedByDayRoom.get(0).getHearingId(), is(HEARING_ID_1)); + + final List matchedByTopLevelRoom = hearingEventRepository + .findHearingEvents(COURT_CENTRE_ID, COURT_ROOM_ID, EVENT_TIME.toLocalDate()); + assertThat(matchedByTopLevelRoom.size(), is(0)); + } + + @Test + public void shouldFindHearingEvents_byTopLevelRoom_whenHearingDayHasNullCourtRoomId() { + givenHearingExistsWithCourtCentreAndDayOverride(EVENT_TIME.toLocalDate(), COURT_CENTRE_ID, null); + givenSingleAlterableFalseHearingEvent(HEARING_EVENT_ID_1, HEARING_ID_1, EVENT_TIME); + + final List hearingEvents = hearingEventRepository + .findHearingEvents(COURT_CENTRE_ID, COURT_ROOM_ID, EVENT_TIME.toLocalDate()); + assertThat(hearingEvents.size(), is(1)); + assertThat(hearingEvents.get(0).getHearingId(), is(HEARING_ID_1)); + } + @Test public void shouldFindHearingEventByCourCentreId() { givenHearingExistsWithCourtCentre(); @@ -518,6 +547,39 @@ private void givenHearingExistsWithCourtCentre() { hearingRepository.save(hearing); } + private void givenHearingExistsWithCourtCentreAndDayOverride(final LocalDate date, final UUID dayCentreId, final UUID dayRoomId) { + final Hearing hearing = new Hearing(); + final CourtCentre courtCentre = new CourtCentre(); + courtCentre.setId(COURT_CENTRE_ID); + courtCentre.setRoomId(COURT_ROOM_ID); + hearing.setId(HEARING_ID_1); + hearing.setCourtCentre(courtCentre); + + final HearingDay hearingDay = new HearingDay(); + hearingDay.setId(new HearingSnapshotKey(randomUUID(), HEARING_ID_1)); + hearingDay.setHearing(hearing); + hearingDay.setDate(date); + hearingDay.setCourtCentreId(dayCentreId); + hearingDay.setCourtRoomId(dayRoomId); + hearing.getHearingDays().add(hearingDay); + + hearingRepository.save(hearing); + } + + private void givenSingleAlterableFalseHearingEvent(final UUID hearingEventId, final UUID hearingId, final ZonedDateTime eventTime) { + hearingEventRepository.save( + HearingEvent.hearingEvent() + .setId(hearingEventId) + .setHearingId(hearingId) + .setHearingEventDefinitionId(HEARING_EVENT_DEFINITION_ID_1) + .setRecordedLabel(RECORDED_LABEL) + .setEventDate(eventTime.toLocalDate()) + .setEventTime(eventTime) + .setLastModifiedTime(LAST_MODIFIED_TIME) + .setDeleted(false) + .setAlterable(false)); + } + private void givenHearingEventsForDifferentHearings() { final List hearingEvents = newArrayList( HearingEvent.hearingEvent() diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingRepositoryTest.java b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingRepositoryTest.java index 20a39aa401..0534eaa9fb 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingRepositoryTest.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/repository/HearingRepositoryTest.java @@ -211,6 +211,32 @@ public void shouldReturnNullWhenHearingIdIsAbsent() { assertNull(courtCenter); } + @Test + public void shouldFindHearingDayByHearingIdAndDate() { + final uk.gov.justice.core.courts.Hearing hearing = hearings.get(0); + final UUID hearingId = hearing.getId(); + final LocalDate date = hearing.getHearingDays().get(0).getSittingDay().toLocalDate(); + + final HearingDay matched = hearingRepository.findHearingDayByHearingIdAndDate(hearingId, date); + + assertNotNull(matched); + assertEquals(date, matched.getDate()); + } + + @Test + public void shouldReturnNullForFindHearingDayByHearingIdAndDate_whenNoDayMatchesDate() { + final UUID hearingId = hearings.get(0).getId(); + final LocalDate unknownDate = LocalDate.of(1999, 1, 1); + + assertNull(hearingRepository.findHearingDayByHearingIdAndDate(hearingId, unknownDate)); + } + + @Test + public void shouldReturnNullForFindHearingDayByHearingIdAndDate_whenHearingIdIsUnknown() { + final LocalDate anyDate = hearings.get(0).getHearingDays().get(0).getSittingDay().toLocalDate(); + assertNull(hearingRepository.findHearingDayByHearingIdAndDate(randomUUID(), anyDate)); + } + @Test public void shouldFindTargetsByHearingId() { final UUID hearingId = hearings.get(0).getId(); diff --git a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/utils/HearingJPADataTemplate.java b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/utils/HearingJPADataTemplate.java index 4fad541894..7d0e6a3cfe 100644 --- a/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/utils/HearingJPADataTemplate.java +++ b/hearing-viewstore/hearing-viewstore-persistence/src/test/java/uk/gov/moj/cpp/hearing/utils/HearingJPADataTemplate.java @@ -35,6 +35,10 @@ private HearingJPADataTemplate() { } private HearingJPADataTemplate(final boolean sysoutPrint) { + this(sysoutPrint, 1); + } + + private HearingJPADataTemplate(final boolean sysoutPrint, final int cases) { // final uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing hearingEntity = aNewEnhancedRandom().nextObject(uk.gov.moj.cpp.hearing.persist.entity.ha.Hearing.class); hearingEntity.setHearingLanguage(RandomGenerator.values(HearingLanguage.values()).next()); @@ -55,7 +59,7 @@ private HearingJPADataTemplate(final boolean sysoutPrint) { }); // - randomStreamOf(1, uk.gov.moj.cpp.hearing.persist.entity.ha.ProsecutionCase.class) + randomStreamOf(cases, uk.gov.moj.cpp.hearing.persist.entity.ha.ProsecutionCase.class) .forEach(prosecutionCase -> { prosecutionCase.setId(aNewHearingSnapshotKey(hearingEntity.getId())); prosecutionCase.setHearing(hearingEntity); @@ -196,6 +200,10 @@ public static HearingJPADataTemplate aNewHearingJPADataTemplate() { return new HearingJPADataTemplate(); } + public static HearingJPADataTemplate aNewHearingJPADataTemplate(final int cases) { + return new HearingJPADataTemplate(false, cases); + } + public static HearingJPADataTemplate aNewHearingJPADataTemplate(final boolean sysoutPrint) { return new HearingJPADataTemplate(sysoutPrint); } diff --git a/test-utilities/src/main/java/uk/gov/moj/cpp/hearing/test/TestTemplates.java b/test-utilities/src/main/java/uk/gov/moj/cpp/hearing/test/TestTemplates.java index 86377e5b2e..c30d23effb 100644 --- a/test-utilities/src/main/java/uk/gov/moj/cpp/hearing/test/TestTemplates.java +++ b/test-utilities/src/main/java/uk/gov/moj/cpp/hearing/test/TestTemplates.java @@ -284,7 +284,11 @@ public static CaseDefendantDetailsWithHearingCommand initiateDefendantCommandTem .setDefendant(defendantTemplate()); } + public static uk.gov.moj.cpp.hearing.command.defendant.Defendant defendantTemplate() { + return defendantTemplate(randomUUID()); + } + public static uk.gov.moj.cpp.hearing.command.defendant.Defendant defendantTemplate(final UUID caseId) { final Defendant defendant = new Defendant(); @@ -292,7 +296,7 @@ public static uk.gov.moj.cpp.hearing.command.defendant.Defendant defendantTempla defendant.setMasterDefendantId(randomUUID()); - defendant.setProsecutionCaseId(randomUUID()); + defendant.setProsecutionCaseId(caseId); defendant.setNumberOfPreviousConvictionsCited(INTEGER.next());