> getListOrNull() {
return list -> list.isEmpty() ? null : list;
}
diff --git a/progression-command/progression-command-handler/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json b/progression-command/progression-command-handler/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
index b5f025db7..72e73a1f0 100644
--- a/progression-command/progression-command-handler/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
+++ b/progression-command/progression-command-handler/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
@@ -20,6 +20,9 @@
"description": "Indicates that the summons approval process is required to be initiated",
"type": "boolean"
},
+ "applicationSource": {
+ "$ref": "http://moj.gov.uk/cpp/progression/enum-def.json#/definitions/applicationSource"
+ },
"isAmended": {
"type": "boolean"
},
diff --git a/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerMHSourceTest.java b/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerMHSourceTest.java
new file mode 100644
index 000000000..df7a89de7
--- /dev/null
+++ b/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerMHSourceTest.java
@@ -0,0 +1,691 @@
+package uk.gov.moj.cpp.progression.handler;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasNoJsonPath;
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.UUID.randomUUID;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static uk.gov.justice.core.courts.CourtApplication.courtApplication;
+import static uk.gov.justice.core.courts.CourtApplicationCase.courtApplicationCase;
+import static uk.gov.justice.core.courts.CourtApplicationType.courtApplicationType;
+import static uk.gov.justice.core.courts.InitiateCourtApplicationProceedings.initiateCourtApplicationProceedings;
+import static uk.gov.justice.core.courts.Offence.offence;
+import static uk.gov.justice.core.courts.ProsecutionCaseIdentifier.prosecutionCaseIdentifier;
+import static uk.gov.justice.core.courts.SummonsTemplateType.NOT_APPLICABLE;
+import static uk.gov.justice.services.messaging.Envelope.envelopeFrom;
+import static uk.gov.justice.services.test.utils.core.helper.EventStreamMockHelper.verifyAppendAndGetArgumentFrom;
+import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopeMatcher.jsonEnvelope;
+import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopeMetadataMatcher.metadata;
+import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopePayloadMatcher.payload;
+import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopeStreamMatcher.streamContaining;
+import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.STRING;
+import static uk.gov.moj.cpp.progression.enums.ApplicationSource.CAAG;
+import static uk.gov.moj.cpp.progression.enums.ApplicationSource.HOME;
+import static uk.gov.moj.cpp.progression.enums.ApplicationSource.MH;
+import static uk.gov.moj.cpp.progression.enums.ApplicationSource.UNKNOWN;
+
+import uk.gov.justice.core.courts.Address;
+import uk.gov.justice.core.courts.ApplicationReferredToBoxwork;
+import uk.gov.justice.core.courts.ApplicationReferredToCourtHearing;
+import uk.gov.justice.core.courts.ApplicationReferredToExistingHearing;
+import uk.gov.justice.core.courts.BoxHearingRequest;
+import uk.gov.justice.core.courts.CourtApplicationAddedToCase;
+import uk.gov.justice.core.courts.CourtApplicationCreated;
+import uk.gov.justice.core.courts.CourtApplicationParty;
+import uk.gov.justice.core.courts.CourtApplicationProceedingsEdited;
+import uk.gov.justice.core.courts.CourtApplicationProceedingsInitiateIgnored;
+import uk.gov.justice.core.courts.CourtApplicationProceedingsInitiated;
+import uk.gov.justice.core.courts.CourtApplicationSummonsApproved;
+import uk.gov.justice.core.courts.CourtApplicationSummonsRejected;
+import uk.gov.justice.core.courts.CourtApplicationUpdated;
+import uk.gov.justice.core.courts.CourtHearingRequest;
+import uk.gov.justice.core.courts.HearingResultedApplicationUpdated;
+import uk.gov.justice.core.courts.HearingUpdatedWithCourtApplication;
+import uk.gov.justice.core.courts.InitiateCourtApplicationProceedings;
+import uk.gov.justice.core.courts.InitiateCourtHearingAfterSummonsApproved;
+import uk.gov.justice.core.courts.MasterDefendant;
+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.ProsecutingAuthority;
+import uk.gov.justice.core.courts.SendNotificationForApplicationIgnored;
+import uk.gov.justice.core.courts.SendNotificationForApplicationInitiated;
+import uk.gov.justice.core.courts.SendNotificationForAutoApplicationInitiated;
+import uk.gov.justice.progression.courts.HearingPopulatedToProbationCaseworker;
+import uk.gov.justice.progression.courts.VejHearingPopulatedToProbationCaseworker;
+import uk.gov.justice.progression.event.ApplicationHearingDefendantUpdated;
+import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter;
+import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter;
+import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer;
+import uk.gov.justice.services.core.aggregate.AggregateService;
+import uk.gov.justice.services.core.enveloper.Enveloper;
+import uk.gov.justice.services.eventsourcing.source.core.EventSource;
+import uk.gov.justice.services.eventsourcing.source.core.EventStream;
+import uk.gov.justice.services.messaging.Envelope;
+import uk.gov.justice.services.messaging.JsonEnvelope;
+import uk.gov.justice.services.messaging.Metadata;
+import uk.gov.justice.services.test.utils.core.enveloper.EnveloperFactory;
+import uk.gov.justice.services.test.utils.framework.api.JsonObjectConvertersFactory;
+import uk.gov.moj.cpp.progression.aggregate.ApplicationAggregate;
+import uk.gov.moj.cpp.progression.service.ApplicationDetailsEnrichmentService;
+import uk.gov.moj.cpp.progression.service.ProsecutionCaseQueryService;
+import uk.gov.moj.cpp.progression.service.RefDataService;
+import uk.gov.moj.cpp.progression.service.service.ProgressionService;
+
+import java.util.UUID;
+import java.util.stream.Stream;
+
+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.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for the MH (Manage Hearing) applicationSource fix — AC1 and AC2.
+ *
+ * AC1: MH source + ACTIVE case → offences must be nulled (no CCT-2487 offence selection).
+ * AC2: MH source + INACTIVE/CLOSED case → offences must be preserved (as-is).
+ */
+@ExtendWith(MockitoExtension.class)
+class CourtApplicationHandlerMHSourceTest {
+
+ @Spy
+ private final Enveloper enveloper = EnveloperFactory.createEnveloperWithEvents(
+ CourtApplicationCreated.class,
+ CourtApplicationAddedToCase.class,
+ CourtApplicationProceedingsInitiated.class,
+ ApplicationReferredToBoxwork.class,
+ ApplicationReferredToCourtHearing.class,
+ ApplicationReferredToExistingHearing.class,
+ CourtApplicationProceedingsInitiateIgnored.class,
+ CourtApplicationProceedingsEdited.class,
+ CourtApplicationSummonsRejected.class,
+ CourtApplicationSummonsApproved.class,
+ InitiateCourtHearingAfterSummonsApproved.class,
+ HearingResultedApplicationUpdated.class,
+ HearingUpdatedWithCourtApplication.class,
+ HearingPopulatedToProbationCaseworker.class,
+ VejHearingPopulatedToProbationCaseworker.class,
+ SendNotificationForApplicationInitiated.class,
+ SendNotificationForApplicationIgnored.class,
+ SendNotificationForAutoApplicationInitiated.class,
+ CourtApplicationUpdated.class,
+ ApplicationHearingDefendantUpdated.class
+ );
+
+ @Mock
+ private EventSource eventSource;
+
+ @Mock
+ private EventStream eventStream;
+
+ @Mock
+ private AggregateService aggregateService;
+
+ @Mock
+ private RefDataService referenceDataService;
+
+ @Mock
+ private ProgressionService progressionService;
+
+ @Mock
+ private ProsecutionCaseQueryService prosecutionCaseQueryService;
+
+ @Mock
+ private ApplicationDetailsEnrichmentService applicationDetailsEnrichmentService;
+
+ @Spy
+ @InjectMocks
+ private final ObjectToJsonObjectConverter objectToJsonObjectConverter =
+ new ObjectToJsonObjectConverter(new ObjectMapperProducer().objectMapper());
+
+ @Spy
+ private JsonObjectToObjectConverter jsonObjectToObjectConverter =
+ new JsonObjectConvertersFactory().jsonObjectToObjectConverter();
+
+ @Mock
+ private ApplicationAggregate applicationAggregateMock;
+
+ @InjectMocks
+ private CourtApplicationHandler courtApplicationHandler;
+
+ private UUID masterDefendantId;
+
+ @BeforeEach
+ void setUp() {
+ masterDefendantId = randomUUID();
+ }
+
+ // -----------------------------------------------------------------------
+ // AC1: Case hearing — MH source + ACTIVE case → offences must be nulled
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldNullOffencesForActiveCaseWhenApplicationSourceIsMH() throws Exception {
+ final Offence activeOffence = offence()
+ .withId(randomUUID())
+ .withOffenceCode("OF001")
+ .withWording("Some wording")
+ .build();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(activeOffence))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ withJsonPath("$.courtApplication.courtApplicationCases[0].caseStatus", is("ACTIVE")),
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences")
+ ))
+ )));
+ }
+
+ @Test
+ void shouldNullOffencesForMultipleCasesWhenAllAreActiveAndSourceIsMH() throws Exception {
+ final Offence offence = offence().withId(randomUUID()).withOffenceCode("OF002").build();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(asList(
+ courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(offence))
+ .build(),
+ courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(offence))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences"),
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[1].offences")
+ ))
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // AC2: Application hearing — MH source + INACTIVE case → offences kept
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldPreserveOffencesForInactiveCaseWhenApplicationSourceIsMH() throws Exception {
+ final UUID offenceId = randomUUID();
+ final Offence inactiveOffence = offence()
+ .withId(offenceId)
+ .withOffenceCode("OF003")
+ .withWording("Breach wording")
+ .build();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("INACTIVE")
+ .withOffences(singletonList(inactiveOffence))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ withJsonPath("$.courtApplication.courtApplicationCases[0].caseStatus", is("INACTIVE")),
+ withJsonPath("$.courtApplication.courtApplicationCases[0].offences[0].id", is(offenceId.toString()))
+ ))
+ )));
+ }
+
+ @Test
+ void shouldPreserveOffencesForClosedCaseWhenApplicationSourceIsMH() throws Exception {
+ final UUID offenceId = randomUUID();
+ final Offence closedOffence = offence().withId(offenceId).withOffenceCode("OF004").build();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("CLOSED")
+ .withOffences(singletonList(closedOffence))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ withJsonPath("$.courtApplication.courtApplicationCases[0].offences[0].id", is(offenceId.toString()))
+ ))
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // Mixed cases: one ACTIVE (offences nulled), one INACTIVE (offences kept)
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldNullOffencesOnlyForActiveCasesInMixedScenarioWithMHSource() throws Exception {
+ final UUID activeOffenceId = randomUUID();
+ final UUID inactiveOffenceId = randomUUID();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(asList(
+ courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(offence().withId(activeOffenceId).withOffenceCode("OF_A").build()))
+ .build(),
+ courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("INACTIVE")
+ .withOffences(singletonList(offence().withId(inactiveOffenceId).withOffenceCode("OF_I").build()))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences"),
+ withJsonPath("$.courtApplication.courtApplicationCases[1].offences[0].id", is(inactiveOffenceId.toString()))
+ ))
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // Non-MH sources must NOT trigger the suppression (as-is behaviour)
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldPreserveOffencesForActiveCaseWhenApplicationSourceIsHome() throws Exception {
+ final UUID offenceId = randomUUID();
+ shouldPreserveOffencesForActiveCaseWithSource(HOME, offenceId);
+ }
+
+ @Test
+ void shouldPreserveOffencesForActiveCaseWhenApplicationSourceIsCaag() throws Exception {
+ final UUID offenceId = randomUUID();
+ shouldPreserveOffencesForActiveCaseWithSource(CAAG, offenceId);
+ }
+
+ @Test
+ void shouldPreserveOffencesForActiveCaseWhenApplicationSourceIsUnknown() throws Exception {
+ final UUID offenceId = randomUUID();
+ shouldPreserveOffencesForActiveCaseWithSource(UNKNOWN, offenceId);
+ }
+
+ @Test
+ void shouldPreserveOffencesForActiveCaseWhenApplicationSourceIsNull() throws Exception {
+ final UUID offenceId = randomUUID();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ // no .withApplicationSource() — field absent, getApplicationSource() returns null
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(offence().withId(offenceId).withOffenceCode("OF_NULL").build()))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(
+ withJsonPath("$.courtApplication.courtApplicationCases[0].offences[0].id", is(offenceId.toString()))
+ )
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // Edge case: null caseStatus with MH source — treat as non-active (keep offences)
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldPreserveOffencesWhenCaseStatusIsNullAndSourceIsMH() throws Exception {
+ final UUID offenceId = randomUUID();
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ // no .withCaseStatus() — null
+ .withOffences(singletonList(offence().withId(offenceId).withOffenceCode("OF_NULL_STATUS").build()))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences"))
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // MH source — case with no offences at all should not fail
+ // -----------------------------------------------------------------------
+
+ @Test
+ void shouldHandleActiveCaseWithNoOffencesWhenSourceIsMH() throws Exception {
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(MH)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ // deliberately no offences
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences")
+ )
+ )));
+ }
+
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ private void shouldPreserveOffencesForActiveCaseWithSource(
+ final uk.gov.moj.cpp.progression.enums.ApplicationSource source,
+ final UUID offenceId) throws Exception {
+
+ final InitiateCourtApplicationProceedings command = initiateCourtApplicationProceedings()
+ .withApplicationSource(source)
+ .withCourtApplication(courtApplication()
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withProsecutorThirdPartyFlag(false)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildParty())
+ .withSubject(buildParty())
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier().withCaseURN(STRING.next()).build())
+ .withCaseStatus("ACTIVE")
+ .withOffences(singletonList(offence().withId(offenceId).withOffenceCode("OF_NON_MH").build()))
+ .build()))
+ .build())
+ .withCourtHearing(CourtHearingRequest.courtHearingRequest().build())
+ .build();
+
+ final Envelope envelope = buildEnvelope(command);
+
+ final ApplicationAggregate aggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(aggregate);
+ aggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(
+ withJsonPath("$.courtApplication.courtApplicationCases[0].offences[0].id", is(offenceId.toString()))
+ )
+ )));
+ }
+
+ private Envelope buildEnvelope(final InitiateCourtApplicationProceedings command) {
+ final Metadata metadata = Envelope
+ .metadataBuilder()
+ .withName("progression.command.initiate-court-proceedings-for-application")
+ .withId(randomUUID())
+ .build();
+ return envelopeFrom(metadata, command);
+ }
+
+ private CourtApplicationParty buildParty() {
+ return CourtApplicationParty.courtApplicationParty()
+ .withId(randomUUID())
+ .withProsecutingAuthority(ProsecutingAuthority.prosecutingAuthority()
+ .withProsecutionAuthorityId(randomUUID())
+ .withProsecutionAuthorityCode("TEST_CODE")
+ .build())
+ .withMasterDefendant(MasterDefendant.masterDefendant()
+ .withMasterDefendantId(masterDefendantId)
+ .withPersonDefendant(PersonDefendant.personDefendant()
+ .withPersonDetails(Person.person()
+ .withAddress(Address.address()
+ .withAddress1("1 Test Street")
+ .withPostcode("TK1 1AA")
+ .build())
+ .build())
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerTest.java b/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerTest.java
index 3cc975cc0..9167d08fb 100644
--- a/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerTest.java
+++ b/progression-command/progression-command-handler/src/test/java/uk/gov/moj/cpp/progression/handler/CourtApplicationHandlerTest.java
@@ -115,6 +115,7 @@
import uk.gov.moj.cpp.progression.aggregate.ApplicationAggregate;
import uk.gov.moj.cpp.progression.aggregate.HearingAggregate;
import uk.gov.moj.cpp.progression.command.helper.FileResourceObjectMapper;
+import uk.gov.moj.cpp.progression.enums.ApplicationSource;
import uk.gov.moj.cpp.progression.service.ApplicationDetailsEnrichmentService;
import uk.gov.moj.cpp.progression.service.ProsecutionCaseQueryService;
import uk.gov.moj.cpp.progression.service.RefDataService;
@@ -3196,4 +3197,65 @@ public void shouldUpdateOffenceWordingForActiveCaseWhenNoCourtOrder() throws Eve
)
));
}
+
+ @Test
+ public void shouldIgnoreCaseOffencesWhenCaseStatusIsNullAndApplicationSourceIsMH() throws EventStreamException {
+ final UUID caseId = randomUUID();
+ final InitiateCourtApplicationProceedings initiateCourtApplicationProceedings = initiateCourtApplicationProceedings()
+ .withApplicationSource(ApplicationSource.MH)
+ .withCourtApplication(courtApplication()
+ .withApplicationReference(STRING.next())
+ .withId(randomUUID())
+ .withType(courtApplicationType()
+ .withLinkType(LinkType.LINKED)
+ .withProsecutorThirdPartyFlag(true)
+ .withSummonsTemplateType(NOT_APPLICABLE)
+ .build())
+ .withApplicant(buildCourtApplicationParty(prosecutor1))
+ .withRespondents(singletonList(buildCourtApplicationParty(respondent)))
+ .withSubject(buildCourtApplicationParty(subject))
+ .withCourtApplicationCases(singletonList(courtApplicationCase()
+ .withIsSJP(false)
+ .withCaseStatus(null)
+ .withProsecutionCaseId(caseId)
+ .withProsecutionCaseIdentifier(prosecutionCaseIdentifier()
+ .withProsecutionAuthorityId(randomUUID()).build())
+ .withOffences(singletonList(Offence.offence()
+ .withOffenceCode(ORG_OFFENCE_CODE)
+ .withWording(ORG_OFFENCE_WORDING)
+ .withId(offenceId)
+ .build()))
+ .build()))
+ .build())
+ .withCourtHearing(buildCourtHearing(true))
+ .build();
+
+ final Metadata metadata = Envelope
+ .metadataBuilder()
+ .withName("progression.command.initiate-court-proceedings-for-application")
+ .withId(randomUUID())
+ .build();
+ final Envelope envelope = envelopeFrom(metadata, initiateCourtApplicationProceedings);
+
+ final ApplicationAggregate applicationAggregate = new ApplicationAggregate();
+ when(eventSource.getStreamById(any())).thenReturn(eventStream);
+ when(aggregateService.get(eventStream, ApplicationAggregate.class)).thenReturn(applicationAggregate);
+ applicationAggregate.apply(new CourtApplicationProceedingsInitiated.Builder()
+ .withCourtHearing(new CourtHearingRequest.Builder().build())
+ .withBoxHearing(new BoxHearingRequest.Builder().build())
+ .build());
+
+ courtApplicationHandler.initiateCourtApplicationProceedings(envelope);
+
+ final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream);
+ assertThat(envelopeStream, streamContaining(
+ jsonEnvelope(
+ metadata().withName("progression.event.court-application-proceedings-initiated"),
+ payload().isJson(allOf(
+ withJsonPath("$.courtApplication", notNullValue()),
+ hasNoJsonPath("$.courtApplication.courtApplicationCases[0].offences")
+ ))
+ )
+ ));
+ }
}
diff --git a/progression-domain/progression-datatypes-common/src/main/resources/json/schema/enum-def.json b/progression-domain/progression-datatypes-common/src/main/resources/json/schema/enum-def.json
index 5a0695ee9..ed8113ec5 100644
--- a/progression-domain/progression-datatypes-common/src/main/resources/json/schema/enum-def.json
+++ b/progression-domain/progression-datatypes-common/src/main/resources/json/schema/enum-def.json
@@ -66,6 +66,16 @@
"CONFIRMED",
"RESULTED"
]
+ },
+ "applicationSource": {
+ "id": "http://moj.gov.uk/cpp/progression/enums/application-source.json",
+ "type": "string",
+ "enum": [
+ "HOME",
+ "CAAG",
+ "MH",
+ "UNKNOWN"
+ ]
}
}
}
\ No newline at end of file
diff --git a/progression-domain/progression-domain-message/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json b/progression-domain/progression-domain-message/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
index b5f025db7..72e73a1f0 100644
--- a/progression-domain/progression-domain-message/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
+++ b/progression-domain/progression-domain-message/src/raml/json/schema/progression.command.initiate-court-proceedings-for-application.json
@@ -20,6 +20,9 @@
"description": "Indicates that the summons approval process is required to be initiated",
"type": "boolean"
},
+ "applicationSource": {
+ "$ref": "http://moj.gov.uk/cpp/progression/enum-def.json#/definitions/applicationSource"
+ },
"isAmended": {
"type": "boolean"
},
diff --git a/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/CourtApplicationProcessor.java b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/CourtApplicationProcessor.java
index accf39499..3a417c0c7 100644
--- a/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/CourtApplicationProcessor.java
+++ b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/CourtApplicationProcessor.java
@@ -1,6 +1,7 @@
package uk.gov.moj.cpp.progression.processor;
+import static java.lang.Boolean.TRUE;
import static java.time.format.DateTimeFormatter.ofPattern;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
@@ -127,7 +128,6 @@
@SuppressWarnings({"squid:S2789", "squid:CallToDeprecatedMethod", "squid:CommentedOutCodeLine", "squid:UnusedPrivateMethod", "squid:S1192"})
public class CourtApplicationProcessor {
- public static final String PUBLIC_PROGRESSION_APPLICATION_DEFENDANT_CHANGED = "public.progression.application-defendant-changed";
private static final String COURT_APPLICATION = "courtApplication";
private static final String PROSECUTION_CASE = "prosecutionCase";
private static final String OLD_APPLICATION_ID = "oldApplicationId";
@@ -296,7 +296,7 @@ public void processCourtApplicationUpdated(final JsonEnvelope event) {
public void processCourtApplicationInitiated(final JsonEnvelope event) {
final CourtApplicationProceedingsInitiated courtApplicationProceedingsInitiated = jsonObjectToObjectConverter.convert(event.payloadAsJsonObject(), CourtApplicationProceedingsInitiated.class);
- if (Boolean.TRUE.equals(courtApplicationProceedingsInitiated.getIsSJP())) {
+ if (TRUE.equals(courtApplicationProceedingsInitiated.getIsSJP())) {
initiateSJPCase(event, courtApplicationProceedingsInitiated);
} else {
initiateCourtApplication(event, courtApplicationProceedingsInitiated.getCourtApplication(), courtApplicationProceedingsInitiated.getOldApplicationId());
@@ -545,7 +545,9 @@ public void processBoxWorkApplication(final JsonEnvelope jsonEnvelope) {
hearingBuilder.withIsVirtualBoxHearing(true);
}
- final Initiate hearingInitiate = Initiate.initiate().withHearing(hearingBuilder.build()).build();
+ final Hearing hearing = progressionService.shapeHearingForListing(hearingBuilder.build(), jsonEnvelope);
+
+ final Initiate hearingInitiate = Initiate.initiate().withHearing(hearing).build();
progressionService.linkApplicationToHearing(jsonEnvelope, hearingInitiate.getHearing(), HearingListingStatus.HEARING_INITIALISED);
@@ -580,7 +582,7 @@ public void processCourtApplicationReferredToCourtHearing(final JsonEnvelope eve
final List prosecutionCases = getProsecutionCases(event, application);
- final Hearing hearing = hearing()
+ final Hearing rawHearing = hearing()
.withProsecutionCases(prosecutionCases)
.withId(courtHearing.getId())
.withHearingDays(singletonList(hearingDay()
@@ -596,6 +598,8 @@ public void processCourtApplicationReferredToCourtHearing(final JsonEnvelope eve
.withSpecialRequirements(courtHearing.getSpecialRequirements())
.build();
+ final Hearing hearing = progressionService.shapeHearingForListing(rawHearing, event);
+
final Initiate hearingInitiate = Initiate.initiate().withHearing(hearing).build();
progressionService.linkApplicationToHearing(event, hearingInitiate.getHearing(), HearingListingStatus.HEARING_INITIALISED);
@@ -606,7 +610,12 @@ public void processCourtApplicationReferredToCourtHearing(final JsonEnvelope eve
// then update application status - hearing initiated in the Listing hence applicationStatus == UN_ALLOCATED
progressionService.updateCourtApplicationStatus(event, application.getId(), ApplicationStatus.UN_ALLOCATED);
- final ListCourtHearing listCourtHearing = buildDefaultHearingNeeds(applicationReferredToCourtHearing.getCourtHearing(), application, prosecutionCases);
+ // build the listing needs from the filtered hearing so the application/prosecutionCases sent to
+ // listing match the shaped hearing (e.g. an application hearing carries no prosecution case)
+ final List filteredCourtApplications = hearingInitiate.getHearing().getCourtApplications();
+ final CourtApplication listingApplication = isNotEmpty(filteredCourtApplications) ? filteredCourtApplications.get(0) : application;
+ final ListCourtHearing listCourtHearing = buildDefaultHearingNeeds(applicationReferredToCourtHearing.getCourtHearing(),
+ listingApplication, hearingInitiate.getHearing().getProsecutionCases());
// then list hearing
listingService.listCourtHearing(event, listCourtHearing);
}
@@ -874,7 +883,7 @@ public void processBreachApplicationCreationRequested(final JsonEnvelope event)
private List getProsecutionCases(final JsonEnvelope event, final CourtApplication application) {
final List prosecutionCases = new ArrayList<>();
final Stream courtApplicationCases = ofNullable(application.getCourtApplicationCases()).map(Collection::stream).orElseGet(Stream::empty);
- if (isAllActiveCases(courtApplicationCases)) {
+ if (isAllActiveCasesByOffenceStatus(courtApplicationCases)) {
ofNullable(application.getCourtApplicationCases()).map(Collection::stream).orElseGet(Stream::empty).forEach(courtApplicationCase -> {
final Optional prosecutionCaseDetailById = progressionService.getProsecutionCaseDetailById(event, courtApplicationCase.getProsecutionCaseId().toString());
if (prosecutionCaseDetailById.isPresent()) {
@@ -966,14 +975,26 @@ public void processApplicationDefendantUpdateRequested(final JsonEnvelope event)
private Predicate distinctByKey(final Function super T, Object> keyExtractor) {
final Map