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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions listing-command/listing-command-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@
<artifactId>listing-domain-aggregate</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>uk.gov.moj.cpp.listing</groupId>
<artifactId>listing-query-view</artifactId>
<version>${project.version}</version>
</dependency>
<!-- End of Listing Dependencies -->

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@
import uk.gov.justice.services.messaging.JsonEnvelope;
import uk.gov.moj.cpp.listing.command.api.courtcentre.CourtCentreFactory;
import uk.gov.moj.cpp.listing.command.api.service.HearingEnrichmentOrchestrator;
import uk.gov.moj.cpp.listing.command.api.service.HearingLookupService;
import uk.gov.moj.cpp.listing.common.pastdate.MoveHearingToPastDateException;
import uk.gov.moj.cpp.listing.common.pastdate.MoveHearingToPastDateResult;
import uk.gov.moj.cpp.listing.common.service.CourtSchedulerServiceAdapter;
import uk.gov.moj.cpp.listing.common.service.HearingSlotsService;
import uk.gov.moj.cpp.listing.domain.VacateTrialEnriched;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -71,6 +76,27 @@ public class ListingCommandApi {
private static final String LISTING_COMMAND_LIST_UNSCHEDULED_NEXT_HEARINGS_ENRICHED = "listing.command.list-unscheduled-next-hearings-enriched";
private static final String LISTING_COMMAND_EXTEND_HEARING_FOR_HEARING_ENRICHED = "listing.command.extend-hearing-for-hearing-enriched";
private static final String LISTING_COMMAND_VACATE_TRIAL = "listing.command.vacate-trial-enriched";
private static final String LISTING_COMMAND_MOVE_HEARING_TO_PAST_DATE_ENRICHED = "listing.command.move-hearing-to-past-date-enriched";
private static final String COURT_CENTRE_ID = "courtCentreId";
private static final String START_DATE = "startDate";
private static final String JURISDICTION = "jurisdiction";
private static final String JURISDICTION_TYPE = "jurisdictionType";
private static final String ESTIMATED_MINUTES = "estimatedMinutes";
private static final String COURT_SCHEDULE_ID = "courtScheduleId";
private static final String COURT_ROOM_ID = "courtRoomId";
private static final String SESSION_DATE = "sessionDate";
private static final String SESSION_START_TIME = "sessionStartTime";
private static final String SESSION_END_TIME = "sessionEndTime";
private static final String DURATION_IN_MINUTES = "durationInMinutes";
private static final String HEARING_DAYS = "hearingDays";
private static final String DAY_START_TIME = "startTime";
private static final String DAY_END_TIME = "endTime";
private static final String DAY_DURATION_MINUTES = "durationMinutes";
private static final String ERROR_CODE = "errorCode";
private static final String MESSAGE = "message";
public static final String HEARING_ID_NOT_FOUND = "HEARING_ID_NOT_FOUND";
public static final String FUTURE_DATE_NOT_ALLOWED = "FUTURE_DATE_NOT_ALLOWED";
private static final String CROWN_JURISDICTION = "CROWN";
private static final String LISTING_COMMAND_CORRECT_HEARING_DAYS_WO_CC = "listing.command.correct-hearing-days-without-court-centre";
private static final String LISTING_COMMAND_DUPLICATE_UNALLOCATED_HEARING = "listing.command.mark-unallocated-hearing-as-duplicate";
private static final String LISTING_COMMAND_UPDATE_EXISTING_HEARING = "listing.command.update-existing-hearing";
Expand Down Expand Up @@ -101,6 +127,10 @@ public class ListingCommandApi {
private HearingSlotsService hearingSlotsService;
@Inject
private HearingEnrichmentOrchestrator hearingEnrichmentOrchestrator;
@Inject
private CourtSchedulerServiceAdapter courtSchedulerServiceAdapter;
@Inject
private HearingLookupService hearingLookupService;

@Handles("listing.command.list-court-hearing")
public void handleListCourtHearing(final JsonEnvelope envelope) {
Expand Down Expand Up @@ -329,6 +359,113 @@ public void handleVacateTrial(final JsonEnvelope envelope) {
envelope.payload()));
}

@Handles("listing.command.move-hearing-to-past-date")
public void handleMoveHearingToPastDate(final JsonEnvelope envelope) {
final JsonObject payload = envelope.payloadAsJsonObject();

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("'listing.command.move-hearing-to-past-date' received with payload {}", envelope.toObfuscatedDebugString());
}

final UUID hearingId = fromString(payload.getString(HEARING_ID));
final UUID courtCentreId = fromString(payload.getString(COURT_CENTRE_ID));
final LocalDate startDate = LocalDate.parse(payload.getString(START_DATE));

final JsonObject hearing = hearingLookupService.findHearing(hearingId, envelope)
.orElseThrow(() -> new MoveHearingToPastDateException(422,
buildMoveHearingToPastDateErrorBody(HEARING_ID_NOT_FOUND, "No hearing found for hearingId " + hearingId),
"No hearing found for hearingId " + hearingId));

final String jurisdictionType = hearing.getString(JURISDICTION_TYPE, null);

final JsonObjectBuilder enrichedBuilder = createObjectBuilder()
.add(HEARING_ID, hearingId.toString())
.add(JURISDICTION, jurisdictionType == null ? "" : jurisdictionType)
.add(START_DATE, startDate.toString())
.add(COURT_CENTRE_ID, courtCentreId.toString());

if (CROWN_JURISDICTION.equals(jurisdictionType)) {
// Baris decision D1: CROWN moves are listing-side only, courtscheduler is never called.
rejectCrownMoveToFutureDate(startDate);
enrichWithExistingDayDetails(enrichedBuilder, hearing, startDate);
} else {
enrichWithBookedPastDateSlot(enrichedBuilder, hearingId, courtCentreId, startDate, hearing);
}

sender.send(envelopeFrom(metadataFrom(envelope.metadata()).withName(LISTING_COMMAND_MOVE_HEARING_TO_PAST_DATE_ENRICHED),
enrichedBuilder.build()));
}

/**
* CROWN moves never call courtscheduler, so the re-dated hearing day is rebuilt from the
* hearing's own current first sitting day — same room and time-of-day, on the new past date.
*/
private static void enrichWithExistingDayDetails(final JsonObjectBuilder enrichedBuilder, final JsonObject hearing, final LocalDate startDate) {
final JsonArray hearingDays = hearing.containsKey(HEARING_DAYS) ? hearing.getJsonArray(HEARING_DAYS) : null;
if (hearingDays == null || hearingDays.isEmpty()) {
return;
}
final JsonObject day = hearingDays.getJsonObject(0);
enrichedBuilder.add(SESSION_DATE, startDate.toString());
if (day.containsKey(COURT_ROOM_ID) && !day.isNull(COURT_ROOM_ID)) {
enrichedBuilder.add(COURT_ROOM_ID, day.getString(COURT_ROOM_ID));
}
if (day.containsKey(DAY_START_TIME) && !day.isNull(DAY_START_TIME)) {
enrichedBuilder.add(SESSION_START_TIME,
java.time.ZonedDateTime.parse(day.getString(DAY_START_TIME)).with(startDate).toString());
}
if (day.containsKey(DAY_END_TIME) && !day.isNull(DAY_END_TIME)) {
enrichedBuilder.add(SESSION_END_TIME,
java.time.ZonedDateTime.parse(day.getString(DAY_END_TIME)).with(startDate).toString());
}
if (day.containsKey(DAY_DURATION_MINUTES) && !day.isNull(DAY_DURATION_MINUTES)) {
enrichedBuilder.add(DURATION_IN_MINUTES, day.getInt(DAY_DURATION_MINUTES));
}
}

private static void rejectCrownMoveToFutureDate(final LocalDate startDate) {
if (startDate.isAfter(LocalDate.now())) {
throw new MoveHearingToPastDateException(422,
buildMoveHearingToPastDateErrorBody(FUTURE_DATE_NOT_ALLOWED, "Hearings can only be moved to today or an earlier date"),
"Hearings can only be moved to today or an earlier date");
}
}

private void enrichWithBookedPastDateSlot(final JsonObjectBuilder enrichedBuilder, final UUID hearingId,
final UUID courtCentreId, final LocalDate startDate, final JsonObject hearing) {
final Integer durationInMinutes = (hearing.containsKey(ESTIMATED_MINUTES) && !hearing.isNull(ESTIMATED_MINUTES))
? hearing.getInt(ESTIMATED_MINUTES) : null;

final MoveHearingToPastDateResult slot =
courtSchedulerServiceAdapter.moveHearingToPastDate(hearingId, courtCentreId, startDate, durationInMinutes);

if (slot.courtScheduleId() != null) {
enrichedBuilder.add(COURT_SCHEDULE_ID, slot.courtScheduleId().toString());
}
if (slot.courtRoomId() != null) {
enrichedBuilder.add(COURT_ROOM_ID, slot.courtRoomId());
}
if (slot.sessionDate() != null) {
enrichedBuilder.add(SESSION_DATE, slot.sessionDate().toString());
}
if (slot.sessionStartTime() != null) {
enrichedBuilder.add(SESSION_START_TIME, slot.sessionStartTime());
}
if (slot.sessionEndTime() != null) {
enrichedBuilder.add(SESSION_END_TIME, slot.sessionEndTime());
}
if (slot.durationInMinutes() != null) {
enrichedBuilder.add(DURATION_IN_MINUTES, slot.durationInMinutes());
}
}

private static JsonObject buildMoveHearingToPastDateErrorBody(final String errorCode, final String message) {
return createObjectBuilder()
.add(ERROR_CODE, errorCode)
.add(MESSAGE, message)
.build();
}

@Handles("listing.command.extend-hearing-for-hearing")
public void handleExtendHearingForHearing(final JsonEnvelope envelope) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package uk.gov.moj.cpp.listing.command.api.mapper;

import uk.gov.justice.services.adapter.rest.application.DefaultCommonProviders;

import java.util.Set;

import javax.enterprise.inject.Specializes;

/**
* Registers the listing command-api's custom JAX-RS providers with the generated REST
* {@code Application}. The framework-generated Application injects {@code CommonProviders} and
* builds its {@code getClasses()} set solely from {@link #providers()} — providers are NOT
* classpath-scanned, so a {@code @Provider} annotation alone does nothing. {@code @Specializes}
* makes this bean replace the framework's {@code @Default DefaultCommonProviders} at that
* injection point while inheriting its qualifiers.
*
* <p><strong>This is the single specialization point for command-api JAX-RS providers.</strong>
* CDI allows only ONE bean to specialize {@link DefaultCommonProviders} per deployment — adding a
* second {@code @Specializes DefaultCommonProviders} subclass anywhere on the classpath makes the
* whole WAR fail to deploy (inconsistent specialization). Register any future command-api
* provider (exception mappers, filters, features) by adding its class here; do NOT create another
* specializing subclass.
*/
@Specializes
public class ListingCommandCommonProviders extends DefaultCommonProviders {

@Override
public Set<Class<?>> providers() {
final Set<Class<?>> providers = super.providers();
providers.add(MoveHearingToPastDateExceptionMapper.class);
return providers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package uk.gov.moj.cpp.listing.command.api.mapper;

import static javax.json.Json.createObjectBuilder;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.status;
import static uk.gov.justice.services.messaging.JsonObjects.getString;

import uk.gov.moj.cpp.listing.common.pastdate.MoveHearingToPastDateException;

import javax.inject.Inject;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import org.slf4j.Logger;

@Provider
public class MoveHearingToPastDateExceptionMapper implements ExceptionMapper<MoveHearingToPastDateException> {

@Inject
Logger logger;

@Override
public Response toResponse(final MoveHearingToPastDateException exception) {
logger.debug("move-hearing-to-past-date rejected", exception);

final JsonObjectBuilder builder = createObjectBuilder();
if (exception.getErrorCode() != null) {
builder.add("errorCode", exception.getErrorCode());
}
final JsonObject responseBody = exception.getResponseBody();
final String message = responseBody == null
? exception.getMessage()
: getString(responseBody, "message").orElse(exception.getMessage());
if (message != null) {
builder.add("message", message);
}

return status(exception.getHttpStatus())
.entity(builder.build().toString())
.type(APPLICATION_JSON)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package uk.gov.moj.cpp.listing.command.api.service;

import static uk.gov.justice.services.core.annotation.Component.QUERY_API;
import static uk.gov.justice.services.messaging.JsonEnvelope.metadataFrom;
import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder;

import uk.gov.justice.services.core.annotation.ServiceComponent;
import uk.gov.justice.services.core.enveloper.Enveloper;
import uk.gov.justice.services.messaging.JsonEnvelope;
import uk.gov.justice.services.messaging.Metadata;
import uk.gov.moj.cpp.listing.query.view.HearingQueryView;

import java.util.Optional;
import java.util.UUID;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.JsonObject;
import javax.ws.rs.NotFoundException;

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

/**
* Synchronous, in-process lookup of a hearing from the listing viewstore, for command-api-side
* existence/validation checks that must happen before a command is sent (e.g.
* move-hearing-to-past-date's unknown-hearingId 422). Mirrors the
* {@code uk.gov.moj.cpp.listing.command.service.HearingService} pattern already used from
* listing-command-handler, moved into command-api since the check has to happen here to be able
* to reject with a synchronous 422 before anything is sent.
*/
@ApplicationScoped
public class HearingLookupService {

private static final Logger LOGGER = LoggerFactory.getLogger(HearingLookupService.class);
private static final String HEARING_QUERY_BY_HEARING_ID = "listing.search.hearing";

@Inject
private Enveloper enveloper;

@Inject
@ServiceComponent(QUERY_API)
private HearingQueryView hearingQueryView;

public Optional<JsonObject> findHearing(final UUID hearingId, final JsonEnvelope envelope) {
final JsonObject payload = createObjectBuilder().add("id", hearingId.toString()).build();

final Metadata metadata = metadataFrom(envelope.metadata()).withName(HEARING_QUERY_BY_HEARING_ID).build();
final JsonEnvelope query = JsonEnvelope.envelopeFrom(metadata, payload);
final JsonEnvelope request = enveloper.withMetadataFrom(query, HEARING_QUERY_BY_HEARING_ID).apply(payload);

try {
final JsonEnvelope response = hearingQueryView.getHearingById(request);
return Optional.of(response.payloadAsJsonObject());
} catch (final NotFoundException e) {
LOGGER.debug("No hearing found for hearingId {}", hearingId, e);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,12 @@ rule "Command - API - Action - listing.update-hearing-add-case-bdf"
$outcome.setSuccess(true);
end

rule "Command - API - Action - listing.command.move-hearing-to-past-date"
when
$outcome: Outcome();
$action: Action(name == "listing.command.move-hearing-to-past-date");
eval(userAndGroupProvider.isMemberOfAnyOfTheSuppliedGroups($action, RuleConstants.LISTING_OFFICERS, RuleConstants.CROWN_COURT_ADMIN, RuleConstants.COURT_ADMINISTRATORS, RuleConstants.COURT_CLERKS, RuleConstants.LEGAL_ADVISERS, RuleConstants.COURT_ASSOCIATE));
then
$outcome.setSuccess(true);
end

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"courtCentreId": "07e45c88-9e5d-3e44-b664-d5345bb13be2",
"startDate": "2026-05-01"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://justice.gov.uk/listing/courts/listing.command.move-hearing-to-past-date.json",
"type": "object",
"properties": {
"courtCentreId": {
"$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid"
},
"startDate": {
"type": "string",
"format": "date"
}
},
"required": [
"courtCentreId",
"startDate"
],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ protocols: [ HTTP, HTTPS ]
(mapping):
requestType: application/vnd.listing.command.vacate-trial+json
name: listing.command.vacate-trial
(mapping):
requestType: application/vnd.listing.command.move-hearing-to-past-date+json
name: listing.command.move-hearing-to-past-date
(mapping):
requestType: application/vnd.listing.duplicate-unallocated-hearing+json
name: listing.mark-unallocated-hearing-as-duplicate
Expand Down Expand Up @@ -144,6 +147,9 @@ protocols: [ HTTP, HTTPS ]
application/vnd.listing.command.vacate-trial+json:
example: !include json/listing.command.vacate-trial.json
schema: !include json/schema/listing.command.vacate-trial.json
application/vnd.listing.command.move-hearing-to-past-date+json:
example: !include json/listing.command.move-hearing-to-past-date.json
schema: !include json/schema/listing.command.move-hearing-to-past-date.json
application/vnd.listing.duplicate-unallocated-hearing+json: !!null
application/vnd.listing.next-hearings-v2+json:
example: !include json/listing.list-next-hearings-v2.json
Expand Down
Loading
Loading