Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
543d0d2
feat(SPRDT-1011): migrate courtscheduler calls to reshaped contract
ozturkmenb Jun 24, 2026
75c4139
test(SPRDT-1011): align listing tests + IT stubs with reshaped contract
ozturkmenb Jun 25, 2026
9b1661a
fix(SPRDT-1011): match list.hearings-in-sessions stub on request body
ozturkmenb Jun 25, 2026
604ecd0
fix(SPRDT-1011): resolve Sonar quality gate failures on courtschedule…
ozturkmenb Jun 25, 2026
87a173b
fix(SPRDT-1011): send required keys on courtscheduler request bodies
ozturkmenb Jun 25, 2026
8b405b1
fix(CROWN-reschedule): promote nonDefaultDays courtScheduleId over st…
ozturkmenb Jun 30, 2026
04b5567
chore(sonar): clear new-code maintainability smells exposed by the 1-…
ozturkmenb Jun 30, 2026
be002f1
test(SPRDT-1011): lock CROWN single-day update->allocated at IT level…
ozturkmenb Jul 1, 2026
447d334
Merge branch 'team/ccsph2n' into dev/SPRDT-1011-listing-callers
ozturkmenb Jul 1, 2026
409b0c2
fix(SPRDT-1011): allocate CROWN schedule-only update by deriving cour…
ozturkmenb Jul 2, 2026
6ca5fb0
fix(SPRDT-1011): suppress CROWN allocation when update sessions canno…
ozturkmenb Jul 2, 2026
b257ef7
fix(SPRDT-1011): suppress CROWN allocation when booked multi-day bloc…
ozturkmenb Jul 2, 2026
7e3a8d9
fix(SPRDT-1011): CROWN update nonDefaultDays contract — virtual day i…
ozturkmenb Jul 2, 2026
0735d11
feat(SPRDT-987): move-hearing-to-past-date wrapper (MAGS via courtsch…
ozturkmenb Jul 2, 2026
2ea5e95
fix(SPRDT-987): stub get-logged-in-user-permissions query for hasPerm…
ozturkmenb Jul 2, 2026
b035672
fix(SPRDT-987): list hearings through the real flow before moving the…
ozturkmenb Jul 2, 2026
ca11f8b
fix(SPRDT-987): MAGS move re-issues the hearing day on the past date …
ozturkmenb Jul 2, 2026
b77ecc7
refactor(SPRDT-987): sonar criticals - extract move enrichment helper…
ozturkmenb Jul 2, 2026
c454f0f
feat(SPRDT-987): CROWN move re-dates the hearing day too - day rebuil…
ozturkmenb Jul 2, 2026
73a8c7d
fix(SPRDT-987): always compute endTime on the moved day (event schema…
ozturkmenb Jul 2, 2026
87cbc9d
it fix
ozturkmenb Jul 3, 2026
7a3ea5e
fix(SPRDT-987): no-session surfaced as 422 NO_SESSION_FOUND (legacy 4…
ozturkmenb Jul 3, 2026
0d57f1c
fix(SPRDT-987): authorise move-hearing-to-past-date by the same group…
ozturkmenb Jul 3, 2026
1f85d6f
fix(SPRDT-987): reconcile move-hearing-to-past-date onto the SPRDT-10…
ozturkmenb Jul 3, 2026
c95cf7f
fix(SPRDT-1011): drop hearingId from crown/mags search-and-book body …
ozturkmenb Jul 4, 2026
7995a46
chore(SPRDT-1011): add .github secret-scanning workflow from main
ozturkmenb Jul 4, 2026
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
67 changes: 67 additions & 0 deletions .github/actions/secret-scanner/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Secret Scanner

description: Scans the code base to detect for the presence of secrets.

inputs:
github_token:
required: true
description: GitHub token for authentication, required for accessing the repository and posting comments.

gitleaks_license:
required: true
description: Gitleaks license key to use the licensed version.

gitleaks_regex_internal_url:
required: true
description: Regex to identify internal urls

gitleaks_regex_banned_ids:
required: true
description: Regex to identify banned IDs

runs:
using: "composite"
steps:
- name: Prepare Gitleaks config
shell: bash
run: |
echo "Executing step: Prepare Gitleaks config"
set -euo pipefail

# Validate that regex parameters are not empty or null
# Note: Missing secrets evaluate to empty strings in GitHub Actions
GITLEAKS_REGEX_INTERNAL_URL_VALUE="${{ inputs.gitleaks_regex_internal_url }}"
GITLEAKS_REGEX_BANNED_IDS_VALUE="${{ inputs.gitleaks_regex_banned_ids }}"

if [ -z "$GITLEAKS_REGEX_INTERNAL_URL_VALUE" ] || [ "$GITLEAKS_REGEX_INTERNAL_URL_VALUE" = "null" ]; then
echo "::error::gitleaks_regex_internal_url is required and cannot be empty or null. Please ensure the secret is defined and has a value."
exit 1
fi

if [ -z "$GITLEAKS_REGEX_BANNED_IDS_VALUE" ] || [ "$GITLEAKS_REGEX_BANNED_IDS_VALUE" = "null" ]; then
echo "::error::gitleaks_regex_banned_ids is required and cannot be empty or null. Please ensure the secret is defined and has a value."
exit 1
fi

export GITLEAKS_REGEX_INTERNAL_URL="$GITLEAKS_REGEX_INTERNAL_URL_VALUE"
export GITLEAKS_REGEX_BANNED_IDS="$GITLEAKS_REGEX_BANNED_IDS_VALUE"
CUSTOM_CONFIG_FILE="$RUNNER_TEMP/gitleaks-custom-rules.toml"
envsubst < "${{ github.action_path }}/gitleaks-custom-rules-template.toml" > "$CUSTOM_CONFIG_FILE"

# Set GITLEAKS_CONFIG env variable and make it available to subsequent steps
echo "GITLEAKS_CONFIG=$CUSTOM_CONFIG_FILE" >> "$GITHUB_ENV"
echo "------------ Scan will run with builtin + custom rules ------------"

- name: Gitleaks scanning
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
GITLEAKS_LICENSE: ${{ inputs.gitleaks_license }}
GITLEAKS_ENABLE_COMMENTS: "false" # suppress PR comments
GITLEAKS_ENABLE_SUMMARY: "false" # suppress Job Summary

- name: TruffleHog 🐽 scanning
uses: trufflesecurity/trufflehog@main
with:
extra_args: --results=verified

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
title = "Custom rules"

[extend]
useDefault = true

[[rules]]
id = "internal-urls"
description = "Identify internal urls"
regex = '''${GITLEAKS_REGEX_INTERNAL_URL}'''
tags = ["internal-urls"]

[[rules]]
id = "banned-ids"
description = "Identify banned IDs"
regex = '''${GITLEAKS_REGEX_BANNED_IDS}'''
tags = ["banned-ids"]

24 changes: 24 additions & 0 deletions .github/workflows/secret-scanning.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Scanning
on:
pull_request:
branches:
- '**'
schedule:
- cron: '0 4 * * 4' # Every Thursday at 04:00
workflow_dispatch:

jobs:
scan:
name: Secrets Scanner
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ./.github/actions/secret-scanner
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
gitleaks_license: ${{ secrets.GITLEAKS_LICENSE }}
gitleaks_regex_internal_url: ${{ secrets.HMCTS_CP_GITLEAKS_REGEX_INTERNAL_URL }}
gitleaks_regex_banned_ids: ${{ secrets.HMCTS_CP_GITLEAKS_REGEX_BANNED_IDS }}
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 @@ -102,6 +128,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 @@ -332,6 +362,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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ListingCommandCommonProviders extends DefaultCommonProviders {
public Set<Class<?>> providers() {
final Set<Class<?>> providers = super.providers();
providers.add(CrownMultiDayExtensionExceptionMapper.class);
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();
}
}
Loading
Loading