diff --git a/.specify/feature.json b/.specify/feature.json new file mode 100644 index 000000000..c0de09054 --- /dev/null +++ b/.specify/feature.json @@ -0,0 +1,3 @@ +{ + "feature_directory": "specs/001-informant-register-local-schema" +} diff --git a/CLAUDE.md b/CLAUDE.md index 38922dfe3..079c6e8c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,4 +134,9 @@ This repo uses Spec Kit. For non-trivial work use: For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan + +**Active feature**: CIMD-3915 — Include Verdict in SJP Results +**Branch**: `CIMD-3915-informant-register-local-schema` +**Implementation plan**: [specs/001-informant-register-local-schema/plan.md](specs/001-informant-register-local-schema/plan.md) +**Quickstart**: [specs/001-informant-register-local-schema/quickstart.md](specs/001-informant-register-local-schema/quickstart.md) diff --git a/pom.xml b/pom.xml index aafb5cf4f..aa344b4f4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT pom Results Service - Parent Module @@ -41,7 +41,7 @@ 17.103.133 17.104.44 17.104.111 - 17.0.262 + 17.0.266 17.0.79 17.104.47 7.0.0 diff --git a/results-command/pom.xml b/results-command/pom.xml index f543b87b2..f218335d9 100644 --- a/results-command/pom.xml +++ b/results-command/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-command pom diff --git a/results-command/results-command-api/pom.xml b/results-command/results-command-api/pom.xml index 3e4e1ba3f..2875615f8 100644 --- a/results-command/results-command-api/pom.xml +++ b/results-command/results-command-api/pom.xml @@ -3,7 +3,7 @@ uk.gov.moj.cpp.results results-command - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-command-api war diff --git a/results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json b/results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json index 1407cdea5..fa19d611f 100644 --- a/results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json +++ b/results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json @@ -1,5 +1,5 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "id": "http://moj.gov.uk/cpp/results/command/add-informant-register-request.json", - "$ref": "http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" } \ No newline at end of file diff --git a/results-command/results-command-handler/pom.xml b/results-command/results-command-handler/pom.xml index 16b7a02e7..4399de6d8 100644 --- a/results-command/results-command-handler/pom.xml +++ b/results-command/results-command-handler/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-command - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-command-handler war diff --git a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java index c7158641a..556155c87 100644 --- a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java +++ b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java @@ -13,12 +13,12 @@ import static uk.gov.moj.cpp.domains.constant.RegisterStatus.RECORDED; import static uk.gov.moj.cpp.results.command.util.DefendantMapper.getDefendants; -import uk.gov.justice.core.courts.InformantRegisterRecorded; import uk.gov.justice.core.courts.ProsecutionCase; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDefendant; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.InformantRegisterRecordedV2; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; import uk.gov.justice.results.courts.GenerateInformantRegister; -import uk.gov.justice.results.courts.InformantRegisterGenerated; import uk.gov.justice.results.courts.NotifyInformantRegister; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; @@ -95,7 +95,10 @@ public void handleAddInformantRegisterToEventStream(final Envelope events = Stream.of(new InformantRegisterRecorded(informantRegisterDocumentRequest, prosecutionAuthorityId)); + final Stream events = Stream.of(InformantRegisterRecordedV2.informantRegisterRecordedV2() + .withProsecutionAuthorityId(prosecutionAuthorityId) + .withInformantRegister(informantRegisterDocumentRequest) + .build()); appendEventsToStream(envelope, eventStream, events); } @@ -163,7 +166,7 @@ private void processRequests(final UUID informantRegisterId, final List jsonObjectToObjectConverter.convert(informantRegister, InformantRegisterDocumentRequest.class)) .toList(); - final Stream events = Stream.of(InformantRegisterGenerated.informantRegisterGenerated() + final Stream events = Stream.of(InformantRegisterGeneratedV2.informantRegisterGeneratedV2() .withInformantRegisterDocumentRequests(informantRegisterDocumentRequests) .withSystemGenerated(systemGenerated) .build()); diff --git a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/DefendantMapper.java b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/DefendantMapper.java index daa9be564..361730571 100644 --- a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/DefendantMapper.java +++ b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/DefendantMapper.java @@ -10,7 +10,7 @@ import uk.gov.justice.core.courts.Defendant; import uk.gov.justice.core.courts.PersonDefendant; import uk.gov.justice.core.courts.ProsecutionCase; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; import java.time.format.DateTimeFormatter; import java.util.ArrayList; diff --git a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/ProsecutionCaseOrApplicationMapper.java b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/ProsecutionCaseOrApplicationMapper.java index eb62aebe3..d782080a9 100644 --- a/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/ProsecutionCaseOrApplicationMapper.java +++ b/results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/util/ProsecutionCaseOrApplicationMapper.java @@ -7,10 +7,10 @@ import uk.gov.justice.core.courts.Defendant; import uk.gov.justice.core.courts.ProsecutionCase; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDefendant; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterOffence; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterResult; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult; import java.util.ArrayList; import java.util.List; diff --git a/results-command/results-command-handler/src/raml/json/schema/results.command.add-informant-register.json b/results-command/results-command-handler/src/raml/json/schema/results.command.add-informant-register.json index b846213d4..3b7888bda 100644 --- a/results-command/results-command-handler/src/raml/json/schema/results.command.add-informant-register.json +++ b/results-command/results-command-handler/src/raml/json/schema/results.command.add-informant-register.json @@ -1,5 +1,5 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://justice.gov.uk/core/courts/informantRegisterDocument/informant-register-document-request.json", - "$ref": "http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/command-add-informant-register-document-request.json", + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" } \ No newline at end of file diff --git a/results-command/results-command-handler/src/test/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandlerTest.java b/results-command/results-command-handler/src/test/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandlerTest.java index f6521f79d..1f022c87c 100644 --- a/results-command/results-command-handler/src/test/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandlerTest.java +++ b/results-command/results-command-handler/src/test/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandlerTest.java @@ -15,8 +15,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient; import static uk.gov.justice.services.core.annotation.Component.COMMAND_HANDLER; import static uk.gov.justice.services.messaging.Envelope.envelopeFrom; import static uk.gov.justice.services.messaging.Envelope.metadataFrom; @@ -31,16 +31,18 @@ import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; import static uk.gov.moj.cpp.domains.InformantRegisterHelper.getInformantRegisterStreamId; -import uk.gov.justice.core.courts.InformantRegisterRecorded; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDefendant; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterHearing; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterHearingVenue; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterOffence; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterResult; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterResultData; +import uk.gov.justice.results.courts.InformantRegisterRecordedV2; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearingVenue; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResultData; +import uk.gov.justice.results.courts.informantRegisterDocument.Verdict; import uk.gov.justice.results.courts.GenerateInformantRegister; import uk.gov.justice.results.courts.InformantRegisterGenerated; import uk.gov.justice.results.courts.InformantRegisterNotificationIgnored; @@ -138,7 +140,7 @@ public class InformantRegisterHandlerTest { private final StringToJsonObjectConverter stringToJsonObjectConverter = new StringToJsonObjectConverter(); @Spy - private Enveloper enveloper = EnveloperFactory.createEnveloperWithEvents(InformantRegisterRecorded.class, InformantRegisterGenerated.class, InformantRegisterNotified.class, InformantRegisterNotifiedV2.class, InformantRegisterNotificationIgnored.class); + private Enveloper enveloper = EnveloperFactory.createEnveloperWithEvents(InformantRegisterGenerated.class, InformantRegisterNotified.class, InformantRegisterNotifiedV2.class, InformantRegisterNotificationIgnored.class, InformantRegisterRecordedV2.class, InformantRegisterGeneratedV2.class); @BeforeEach public void setup() { @@ -167,7 +169,7 @@ public void shouldProcessCommand() throws Exception { assertThat(envelopeStream, streamContaining( jsonEnvelope( - metadata().withName("results.event.informant-register-recorded"), + metadata().withName("results.event.informant-register-recorded-v2"), JsonEnvelopePayloadMatcher.payload().isJson(allOf( withJsonPath("$.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())), withJsonPath("$.informantRegister.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())), @@ -189,7 +191,7 @@ public void shouldProcessCommandForGroupCases() throws Exception { MatcherAssert.assertThat(envelopeStream, streamContaining( jsonEnvelope( - metadata().withName("results.event.informant-register-recorded"), + metadata().withName("results.event.informant-register-recorded-v2"), JsonEnvelopePayloadMatcher.payload().isJson(allOf( withJsonPath("$.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())), withJsonPath("$.informantRegister", notNullValue()), @@ -250,7 +252,7 @@ public void shouldProcessCommandForGroupCasesWithoutDefAndCaseResults() throws E MatcherAssert.assertThat(envelopeStream, streamContaining( jsonEnvelope( - metadata().withName("results.event.informant-register-recorded"), + metadata().withName("results.event.informant-register-recorded-v2"), JsonEnvelopePayloadMatcher.payload().isJson(allOf( withJsonPath("$.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())), withJsonPath("$.informantRegister", notNullValue()), @@ -282,6 +284,25 @@ public void shouldProcessCommandForGroupCasesWithoutDefAndCaseResults() throws E ); } + @Test + public void handleAddInformantRegisterToEventStream_withVerdictObject_shouldEmitV2RecordedEvent() throws Exception { + final UUID informantRegisterId = getInformantRegisterStreamId(PROSECUTION_AUTHORITY_ID.toString(), REGISTER_DATE.toLocalDate().toString()); + when(eventSource.getStreamById(informantRegisterId)).thenReturn(eventStream); + + informantRegisterHandler.handleAddInformantRegisterToEventStream(buildEnvelope()); + + final Stream envelopeStream = verifyAppendAndGetArgumentFrom(eventStream); + assertThat(envelopeStream, streamContaining( + jsonEnvelope( + metadata().withName("results.event.informant-register-recorded-v2"), + JsonEnvelopePayloadMatcher.payload().isJson(allOf( + withJsonPath("$.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())), + withJsonPath("$.informantRegister.prosecutionAuthorityId", is(PROSECUTION_AUTHORITY_ID.toString())) + )) + ) + )); + } + @Test public void generateInformantRegister() throws EventStreamException { final Envelope generateInformantRegisterEnvelope = prepareEnvelope(); @@ -412,7 +433,7 @@ private void verifyInformantRegisterDocumentRequestHandlerResults() throws Event assertThat(envelopeStream, streamContaining( jsonEnvelope( metadata() - .withName("results.event.informant-register-generated"), + .withName("results.event.informant-register-generated-v2"), JsonEnvelopePayloadMatcher.payload().isJson(anyOf( withJsonPath("$.informantRegisterDocumentRequests.length()", is(greaterThan(0))) ) @@ -506,7 +527,7 @@ private final List getCaseOrApplications(fin .withOrderIndex(1) .withOffenceTitle("offenceTitle_Main_" + i) .withOffenceCode("offenceCode_Main_" + i) - .withVerdictCode("verdictCode_Main_" + i) + .withVerdict(Verdict.verdict().withVerdictCode("verdictCode_Main_" + i).build()) .withOriginatingCaseUrn("originatingCaseUrn_Main_" + i) .withPleaValue("pleaValue_Main_" + i) .withOffenceResults(getResults(RESULT_TYPE_OFFENCE, i, 1)) diff --git a/results-domain/pom.xml b/results-domain/pom.xml index bcda8d2fe..864842d8f 100644 --- a/results-domain/pom.xml +++ b/results-domain/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-domain pom diff --git a/results-domain/results-domain-aggregate/pom.xml b/results-domain/results-domain-aggregate/pom.xml index 0e846678e..85a235476 100644 --- a/results-domain/results-domain-aggregate/pom.xml +++ b/results-domain/results-domain-aggregate/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-domain - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-domain-aggregate diff --git a/results-domain/results-domain-aggregate/src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregate.java b/results-domain/results-domain-aggregate/src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregate.java index e13c236d4..ccbf79ad7 100644 --- a/results-domain/results-domain-aggregate/src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregate.java +++ b/results-domain/results-domain-aggregate/src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregate.java @@ -11,13 +11,14 @@ import uk.gov.justice.core.courts.InformantRegisterRecorded; import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient; import uk.gov.justice.domain.aggregate.Aggregate; import uk.gov.justice.results.courts.InformantRegisterGenerated; import uk.gov.justice.results.courts.InformantRegisterNotificationIgnored; import uk.gov.justice.results.courts.InformantRegisterNotified; import uk.gov.justice.results.courts.InformantRegisterNotifiedV2; import uk.gov.justice.results.courts.NotifyInformantRegister; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient; import java.util.Collections; import java.util.List; @@ -25,7 +26,7 @@ import java.util.stream.Stream; public class ProsecutionAuthorityAggregate implements Aggregate { - private static final long serialVersionUID = 102L; + private static final long serialVersionUID = -2650621888595752644L; private List informantRegisterRecipients; @Override @@ -38,7 +39,22 @@ public Object apply(final Object event) { informantRegisterDocumentRequest -> nonNull(informantRegisterDocumentRequest.getRecipients()) && !informantRegisterDocumentRequest.getRecipients().isEmpty()) .collect(Collectors.toList()); if (isNotEmpty(informantRegisterWithRecipients)) { - this.informantRegisterRecipients = informantRegisterWithRecipients.get(0).getRecipients(); + this.informantRegisterRecipients = informantRegisterWithRecipients.get(0).getRecipients().stream() + .map(r -> InformantRegisterRecipient.informantRegisterRecipient() + .withRecipientName(r.getRecipientName()) + .withEmailAddress1(r.getEmailAddress1()) + .withEmailAddress2(r.getEmailAddress2()) + .withEmailTemplateName(r.getEmailTemplateName()) + .build()) + .collect(Collectors.toList()); + } + }), + when(InformantRegisterGeneratedV2.class).apply(e -> { + final List informantRegisterWithRecipients = e.getInformantRegisterDocumentRequests().stream().filter( + informantRegisterDocumentRequest -> nonNull(informantRegisterDocumentRequest.getRecipients()) && !informantRegisterDocumentRequest.getRecipients().isEmpty()) + .collect(Collectors.toList()); + if (isNotEmpty(informantRegisterWithRecipients)) { + this.informantRegisterRecipients = Collections.unmodifiableList(informantRegisterWithRecipients.get(0).getRecipients()); } }), otherwiseDoNothing() @@ -54,7 +70,15 @@ public Stream notifyProsecutingAuthority(final NotifyInformantRegister n .withProsecutionAuthorityId(notifyInformantRegister.getProsecutionAuthorityId()).build())); } - return apply(Stream.of(InformantRegisterNotifiedV2.informantRegisterNotifiedV2().withRecipients(informantRegisterRecipients) + final List coreRecipients = informantRegisterRecipients.stream() + .map(r -> uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient() + .withRecipientName(r.getRecipientName()) + .withEmailAddress1(r.getEmailAddress1()) + .withEmailAddress2(r.getEmailAddress2()) + .withEmailTemplateName(r.getEmailTemplateName()) + .build()) + .collect(Collectors.toList()); + return apply(Stream.of(InformantRegisterNotifiedV2.informantRegisterNotifiedV2().withRecipients(coreRecipients) .withFileId(notifyInformantRegister.getFileId()) .withTemplateId(notifyInformantRegister.getTemplateId()) .withProsecutionAuthorityId(notifyInformantRegister.getProsecutionAuthorityId()) @@ -62,7 +86,7 @@ public Stream notifyProsecutingAuthority(final NotifyInformantRegister n .build())); } - public void setInformantRegisterRecipients(List informantRegisterRecipients) { + public void setInformantRegisterRecipients(final List informantRegisterRecipients) { this.informantRegisterRecipients = Collections.unmodifiableList(informantRegisterRecipients); } diff --git a/results-domain/results-domain-aggregate/src/test/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregateTest.java b/results-domain/results-domain-aggregate/src/test/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregateTest.java index c2512b1d4..c328733b1 100644 --- a/results-domain/results-domain-aggregate/src/test/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregateTest.java +++ b/results-domain/results-domain-aggregate/src/test/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregateTest.java @@ -6,19 +6,18 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterHearingVenue.informantRegisterHearingVenue; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient; +import static org.hamcrest.Matchers.hasSize; + import static uk.gov.justice.results.courts.NotifyInformantRegister.notifyInformantRegister; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient; -import uk.gov.justice.core.courts.InformantRegisterRecorded; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterRecipient; -import uk.gov.justice.results.courts.InformantRegisterGenerated; import uk.gov.justice.results.courts.InformantRegisterNotificationIgnored; -import uk.gov.justice.results.courts.InformantRegisterNotified; import uk.gov.justice.results.courts.InformantRegisterNotifiedV2; import uk.gov.justice.results.courts.NotifyInformantRegister; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient; import java.util.List; import java.util.UUID; @@ -56,6 +55,34 @@ public void shouldReturnInformantRegisterNotified() { assertThat(object.getClass(), is(equalTo(InformantRegisterNotifiedV2.class))); } + @Test + public void apply_informantRegisterGeneratedV2_shouldSetRecipientsFromLocalTypes() { + final InformantRegisterRecipient recipient = informantRegisterRecipient() + .withRecipientName("Jane") + .withEmailAddress1("jane@hmcts.net") + .withEmailTemplateName("template") + .build(); + final InformantRegisterDocumentRequest documentRequest = informantRegisterDocumentRequest() + .withRecipients(singletonList(recipient)) + .build(); + final InformantRegisterGeneratedV2 event = InformantRegisterGeneratedV2.informantRegisterGeneratedV2() + .withInformantRegisterDocumentRequests(singletonList(documentRequest)) + .withSystemGenerated(false) + .build(); + + aggregate.apply(event); + + final NotifyInformantRegister notifyInformantRegister = notifyInformantRegister() + .withProsecutionAuthorityId(randomUUID()) + .withTemplateId("template Id") + .withFileId(randomUUID()) + .build(); + + final List eventStream = aggregate.notifyProsecutingAuthority(notifyInformantRegister).collect(toList()); + assertThat(eventStream.size(), is(1)); + assertThat(eventStream.get(0).getClass(), is(equalTo(InformantRegisterNotifiedV2.class))); + } + @Test public void shouldReturnInformantRegisterIgnored() { final NotifyInformantRegister notifyInformantRegister = notifyInformantRegister() diff --git a/results-domain/results-domain-common/pom.xml b/results-domain/results-domain-common/pom.xml index a4dcdbc19..0159551d4 100644 --- a/results-domain/results-domain-common/pom.xml +++ b/results-domain/results-domain-common/pom.xml @@ -4,7 +4,7 @@ results-domain uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-domain-common diff --git a/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-generated-v2.json b/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-generated-v2.json new file mode 100644 index 000000000..8c8144a5f --- /dev/null +++ b/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-generated-v2.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informant-register-generated-v2.json", + "description": "Event schema for generating informant register document request with structured verdict", + "type": "object", + "properties": { + "informantRegisterDocumentRequests": { + "type": "array", + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" + } + }, + "systemGenerated": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-recorded-v2.json b/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-recorded-v2.json new file mode 100644 index 000000000..8137cb78d --- /dev/null +++ b/results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-recorded-v2.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informant-register-recorded-v2.json", + "description": "Event schema for recording informant register document request with structured verdict", + "type": "object", + "properties": { + "prosecutionAuthorityId": { + "description": "The prosecuting authority reference data identifier", + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "informantRegister": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" + } + }, + "required": [ + "prosecutionAuthorityId", + "informantRegister" + ], + "additionalProperties": false +} diff --git a/results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/VerdictSchemaDependencyTest.java b/results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/VerdictSchemaDependencyTest.java new file mode 100644 index 000000000..5ffa164ba --- /dev/null +++ b/results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/VerdictSchemaDependencyTest.java @@ -0,0 +1,69 @@ +package uk.gov.moj.cpp.results.domain.informant.model; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.InputStream; + +import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * T071 (FR-004): the verdict schema's {@code dependencies} block enforces that whenever + * {@code verdictDate} is present, {@code verdictCode} must also be present. The reverse is not + * required — {@code verdictCode} may stand alone without a {@code verdictDate}. This schema-level + * constraint is applied by the framework at the envelope-validation boundary — no production + * handler guard is required. + * + *

This is verified at the schema layer (not in {@code InformantRegisterHandlerTest}) because the + * handler unit test invokes the handler method directly and therefore bypasses the framework's + * envelope schema validation. The behaviour is additionally exercised end-to-end by the negative + * integration test in {@code InformantRegisterDocumentRequestIT}.

+ */ +class VerdictSchemaDependencyTest { + + private static final String VERDICT_SCHEMA = "/json/schema/informantRegisterDocument/verdict.json"; + + private Schema schema; + + @BeforeEach + void loadSchema() { + try (InputStream in = getClass().getResourceAsStream(VERDICT_SCHEMA)) { + schema = SchemaLoader.load(new JSONObject(new JSONTokener(in))); + } catch (final Exception e) { + throw new IllegalStateException("Unable to load verdict schema from " + VERDICT_SCHEMA, e); + } + } + + @Test + void verdictCodeWithVerdictDate_shouldValidate() { + assertDoesNotThrow(() -> schema.validate(new JSONObject() + .put("verdictCode", "G") + .put("verdictDate", "2026-04-13") + .put("verdictType", "FOUND_GUILTY"))); + } + + @Test + void verdictCodeWithoutVerdictDate_shouldValidate() { + assertDoesNotThrow(() -> schema.validate(new JSONObject().put("verdictCode", "G"))); + } + + @Test + void verdictDateWithoutVerdictCode_shouldFailValidation() { + final ValidationException exception = assertThrows(ValidationException.class, + () -> schema.validate(new JSONObject().put("verdictDate", "2026-04-13"))); + assertThat(exception.getMessage(), containsString("verdictCode")); + } + + @Test + void emptyVerdict_shouldValidate() { + assertDoesNotThrow(() -> schema.validate(new JSONObject())); + } +} diff --git a/results-domain/results-domain-event/pom.xml b/results-domain/results-domain-event/pom.xml index a2bc8acea..4f9494c4f 100644 --- a/results-domain/results-domain-event/pom.xml +++ b/results-domain/results-domain-event/pom.xml @@ -4,7 +4,7 @@ results-domain uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-domain-event diff --git a/results-event-sources/pom.xml b/results-event-sources/pom.xml index f46b1d457..1efe00bb1 100644 --- a/results-event-sources/pom.xml +++ b/results-event-sources/pom.xml @@ -3,7 +3,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT 4.0.0 diff --git a/results-event/pom.xml b/results-event/pom.xml index f3dc2c700..1af24aee2 100644 --- a/results-event/pom.xml +++ b/results-event/pom.xml @@ -5,7 +5,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-event pom diff --git a/results-event/results-event-listener/pom.xml b/results-event/results-event-listener/pom.xml index f29474de6..dfc38d981 100644 --- a/results-event/results-event-listener/pom.xml +++ b/results-event/results-event-listener/pom.xml @@ -4,7 +4,7 @@ results-event uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-event-listener diff --git a/results-event/results-event-listener/src/main/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListener.java b/results-event/results-event-listener/src/main/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListener.java index fd8780235..8e1fdaed8 100644 --- a/results-event/results-event-listener/src/main/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListener.java +++ b/results-event/results-event-listener/src/main/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListener.java @@ -2,6 +2,7 @@ import static java.time.ZonedDateTime.now; import static java.util.UUID.randomUUID; +import static java.util.stream.Collectors.toList; import static uk.gov.justice.services.core.annotation.Component.EVENT_LISTENER; import static uk.gov.moj.cpp.domains.constant.RegisterStatus.GENERATED; import static uk.gov.moj.cpp.domains.constant.RegisterStatus.NOTIFIED; @@ -11,6 +12,7 @@ import uk.gov.justice.results.courts.InformantRegisterGenerated; import uk.gov.justice.results.courts.InformantRegisterNotified; import uk.gov.justice.results.courts.InformantRegisterNotifiedV2; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.core.annotation.Handles; import uk.gov.justice.services.core.annotation.ServiceComponent; @@ -28,10 +30,14 @@ import javax.transaction.Transactional; import org.apache.commons.lang3.BooleanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ServiceComponent(EVENT_LISTENER) public class InformantRegisterEventListener { + private static final Logger LOGGER = LoggerFactory.getLogger(InformantRegisterEventListener.class); + private static final String INFORMANT_REGISTER_REQUEST_PARAM = "informantRegister"; @Inject @@ -43,19 +49,40 @@ public class InformantRegisterEventListener { @Transactional @Handles("results.event.informant-register-recorded") public void saveInformantRegister(final JsonEnvelope event) { - final JsonObject informantRegisterDocumentRequestJson = event.payloadAsJsonObject().getJsonObject(INFORMANT_REGISTER_REQUEST_PARAM); + final InformantRegisterDocumentRequest documentRequest = jsonObjectToObjectConverter.convert(informantRegisterDocumentRequestJson, InformantRegisterDocumentRequest.class); + + saveRecordedInformantRegister(informantRegisterDocumentRequestJson, documentRequest.getRegisterDate(), + documentRequest.getHearingId(), documentRequest.getProsecutionAuthorityId(), + documentRequest.getProsecutionAuthorityCode(), documentRequest.getProsecutionAuthorityOuCode()); + } - final InformantRegisterDocumentRequest informantRegisterDocumentRequest = jsonObjectToObjectConverter.convert(informantRegisterDocumentRequestJson, InformantRegisterDocumentRequest.class); + @Transactional + @Handles("results.event.informant-register-recorded-v2") + public void saveInformantRegisterV2(final JsonEnvelope event) { + final JsonObject informantRegisterDocumentRequestJsonV2 = event.payloadAsJsonObject().getJsonObject(INFORMANT_REGISTER_REQUEST_PARAM); + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest documentRequestV2 = + jsonObjectToObjectConverter.convert(informantRegisterDocumentRequestJsonV2, uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.class); + + saveRecordedInformantRegister(informantRegisterDocumentRequestJsonV2, documentRequestV2.getRegisterDate(), + documentRequestV2.getHearingId(), documentRequestV2.getProsecutionAuthorityId(), + documentRequestV2.getProsecutionAuthorityCode(), documentRequestV2.getProsecutionAuthorityOuCode()); + } + private void saveRecordedInformantRegister(final JsonObject informantRegisterDocumentRequestJson, + final ZonedDateTime registerDate, + final UUID hearingId, + final UUID prosecutionAuthorityId, + final String prosecutionAuthorityCode, + final String prosecutionAuthorityOuCode) { final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); informantRegisterEntity.setId(randomUUID()); - informantRegisterEntity.setRegisterDate(informantRegisterDocumentRequest.getRegisterDate().toLocalDate()); - informantRegisterEntity.setRegisterTime(informantRegisterDocumentRequest.getRegisterDate()); - informantRegisterEntity.setHearingId(informantRegisterDocumentRequest.getHearingId()); - informantRegisterEntity.setProsecutionAuthorityId(informantRegisterDocumentRequest.getProsecutionAuthorityId()); - informantRegisterEntity.setProsecutionAuthorityCode(informantRegisterDocumentRequest.getProsecutionAuthorityCode()); - informantRegisterEntity.setProsecutionAuthorityOuCode(informantRegisterDocumentRequest.getProsecutionAuthorityOuCode()); + informantRegisterEntity.setRegisterDate(registerDate.toLocalDate()); + informantRegisterEntity.setRegisterTime(registerDate); + informantRegisterEntity.setHearingId(hearingId); + informantRegisterEntity.setProsecutionAuthorityId(prosecutionAuthorityId); + informantRegisterEntity.setProsecutionAuthorityCode(prosecutionAuthorityCode); + informantRegisterEntity.setProsecutionAuthorityOuCode(prosecutionAuthorityOuCode); informantRegisterEntity.setPayload(informantRegisterDocumentRequestJson.toString()); informantRegisterEntity.setStatus(RECORDED); informantRegisterRepository.save(informantRegisterEntity); @@ -67,24 +94,61 @@ public void generateInformantRegister(final JsonEnvelope event) { final InformantRegisterGenerated informantRegisterGenerated = jsonObjectToObjectConverter.convert(payload, InformantRegisterGenerated.class); final ZonedDateTime currentDateTime = now(); - final UUID prosecutionAuthorityId = informantRegisterGenerated.getInformantRegisterDocumentRequests().get(0).getProsecutionAuthorityId(); - final LocalDate registerDate = informantRegisterGenerated.getInformantRegisterDocumentRequests().get(0).getRegisterDate().toLocalDate(); + final List documentRequests = informantRegisterGenerated.getInformantRegisterDocumentRequests(); + if (documentRequests == null || documentRequests.isEmpty()) { + LOGGER.warn("Skipping results.event.informant-register-generated: no informantRegisterDocumentRequests present"); + return; + } + + final UUID prosecutionAuthorityId = documentRequests.get(0).getProsecutionAuthorityId(); + final LocalDate registerDate = documentRequests.get(0).getRegisterDate().toLocalDate(); + final List hearingIds = documentRequests.stream().map(InformantRegisterDocumentRequest::getHearingId).collect(toList()); + + markInformantRegistersGenerated(prosecutionAuthorityId, registerDate, + BooleanUtils.isTrue(informantRegisterGenerated.getSystemGenerated()), hearingIds, currentDateTime); + } + + @Handles("results.event.informant-register-generated-v2") + public void generateInformantRegisterV2(final JsonEnvelope event) { + final JsonObject payload = event.payloadAsJsonObject(); + final InformantRegisterGeneratedV2 informantRegisterGenerated = jsonObjectToObjectConverter.convert(payload, InformantRegisterGeneratedV2.class); + final ZonedDateTime currentDateTime = now(); + + final List documentRequests = informantRegisterGenerated.getInformantRegisterDocumentRequests(); + if (documentRequests == null || documentRequests.isEmpty()) { + LOGGER.warn("Skipping results.event.informant-register-generated-v2: no informantRegisterDocumentRequests present"); + return; + } + + final UUID prosecutionAuthorityId = documentRequests.get(0).getProsecutionAuthorityId(); + final LocalDate registerDate = documentRequests.get(0).getRegisterDate().toLocalDate(); + final List hearingIds = documentRequests.stream() + .map(uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest::getHearingId) + .collect(toList()); + + markInformantRegistersGenerated(prosecutionAuthorityId, registerDate, + BooleanUtils.isTrue(informantRegisterGenerated.getSystemGenerated()), hearingIds, currentDateTime); + } + private void markInformantRegistersGenerated(final UUID prosecutionAuthorityId, + final LocalDate registerDate, + final boolean systemGenerated, + final List hearingIds, + final ZonedDateTime currentDateTime) { final List informantRegisters = informantRegisterRepository.findByProsecutionAuthorityIdAndRegisterDateForStatusRecorded(prosecutionAuthorityId, registerDate); informantRegisters.forEach(informantRegisterEntity -> { informantRegisterEntity.setStatus(GENERATED); informantRegisterEntity.setProcessedOn(currentDateTime); - if(BooleanUtils.isTrue(informantRegisterGenerated.getSystemGenerated())) { + if (systemGenerated) { informantRegisterEntity.setGeneratedDate(currentDateTime.toLocalDate()); informantRegisterEntity.setGeneratedTime(currentDateTime); } }); - informantRegisterGenerated.getInformantRegisterDocumentRequests().stream().map(InformantRegisterDocumentRequest::getHearingId).forEach(hearingId -> { + hearingIds.forEach(hearingId -> { final List informantRegistersList = informantRegisterRepository.findByHearingIdAndStatusRecorded(hearingId); informantRegistersList.forEach(informantRegisterEntity -> informantRegisterEntity.setProcessedOn(currentDateTime)); }); - } @Handles("results.event.informant-register-notified") @@ -92,12 +156,7 @@ public void notifyInformantRegister(final JsonEnvelope event) { final JsonObject payload = event.payloadAsJsonObject(); final InformantRegisterNotified informantRegisterNotified = jsonObjectToObjectConverter.convert(payload, InformantRegisterNotified.class); final List informantRegisters = informantRegisterRepository.findByProsecutionAuthorityIdAndStatusGenerated(informantRegisterNotified.getProsecutionAuthorityId()); - informantRegisters.forEach(informantRegisterEntity -> { - informantRegisterEntity.setStatus(NOTIFIED); - informantRegisterEntity.setFileId(informantRegisterNotified.getFileId()); - informantRegisterEntity.setProcessedOn(now()); - } - ); + markInformantRegistersNotified(informantRegisters, informantRegisterNotified.getFileId()); } @Handles("results.event.informant-register-notified-v2") @@ -105,9 +164,13 @@ public void notifyInformantRegisterV2(final JsonEnvelope event) { final JsonObject payload = event.payloadAsJsonObject(); final InformantRegisterNotifiedV2 informantRegisterNotified = jsonObjectToObjectConverter.convert(payload, InformantRegisterNotifiedV2.class); final List informantRegisters = informantRegisterRepository.findByProsecutionAuthorityIdAndRegisterDateAndStatusGenerated(informantRegisterNotified.getProsecutionAuthorityId(), informantRegisterNotified.getRegisterDate()); + markInformantRegistersNotified(informantRegisters, informantRegisterNotified.getFileId()); + } + + private void markInformantRegistersNotified(final List informantRegisters, final UUID fileId) { informantRegisters.forEach(informantRegisterEntity -> { informantRegisterEntity.setStatus(NOTIFIED); - informantRegisterEntity.setFileId(informantRegisterNotified.getFileId()); + informantRegisterEntity.setFileId(fileId); informantRegisterEntity.setProcessedOn(now()); } ); diff --git a/results-event/results-event-listener/src/test/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListenerTest.java b/results-event/results-event-listener/src/test/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListenerTest.java index 1363af68c..4d9ecf790 100644 --- a/results-event/results-event-listener/src/test/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListenerTest.java +++ b/results-event/results-event-listener/src/test/java/uk/gov/moj/cpp/results/event/InformantRegisterEventListenerTest.java @@ -2,17 +2,19 @@ import static java.util.Collections.singletonList; import static java.util.UUID.randomUUID; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterHearingVenue.informantRegisterHearingVenue; import static uk.gov.justice.results.courts.InformantRegisterNotified.informantRegisterNotified; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; +import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; +import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createReader; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataWithRandomUUID; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; @@ -29,6 +31,8 @@ import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.InformantRegisterRecordedV2; import uk.gov.moj.cpp.results.persist.InformantRegisterRepository; import uk.gov.moj.cpp.results.persist.entity.InformantRegisterEntity; @@ -40,6 +44,7 @@ import javax.json.JsonObject; import com.google.common.collect.Lists; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -74,7 +79,7 @@ public void setup() { @Test public void shouldSaveInformantRegisterRequested() { final UUID prosecutionAuthId = randomUUID(); - final String ouCode = randomAlphanumeric(10); + final String ouCode = RandomStringUtils.insecure().nextAlphanumeric(10); final InformantRegisterDocumentRequest informantRegisterDocumentRequest = informantRegisterDocumentRequest() .withProsecutionAuthorityId(prosecutionAuthId) .withProsecutionAuthorityOuCode(ouCode) @@ -101,14 +106,51 @@ public void shouldSaveInformantRegisterRequested() { assertThat(savedInformantRegisterEntity.getStatus(), is(RECORDED)); } + @Test + void saveInformantRegisterV2_shouldSaveEntityFromLocalDocumentRequest() { + final UUID prosecutionAuthId = randomUUID(); + final String ouCode = RandomStringUtils.insecure().nextAlphanumeric(10); + final ZonedDateTime registerDate = ZonedDateTime.parse("2026-04-13T09:00:00Z"); + + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest localDocumentRequest = + uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest() + .withProsecutionAuthorityId(prosecutionAuthId) + .withProsecutionAuthorityOuCode(ouCode) + .withRegisterDate(registerDate) + .build(); + + final InformantRegisterRecordedV2 event = InformantRegisterRecordedV2.informantRegisterRecordedV2() + .withProsecutionAuthorityId(prosecutionAuthId) + .withInformantRegister(localDocumentRequest) + .build(); + + informantRegisterEventListener.saveInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-recorded-v2"), + objectToJsonObjectConverter.convert(event))); + + final ArgumentCaptor captor = forClass(InformantRegisterEntity.class); + verify(informantRegisterRepository).save(captor.capture()); + final InformantRegisterEntity saved = captor.getValue(); + + assertThat(saved.getProsecutionAuthorityId(), is(prosecutionAuthId)); + assertThat(saved.getProsecutionAuthorityOuCode(), is(ouCode)); + assertThat(saved.getStatus(), is(RECORDED)); + assertThat(saved.getPayload(), notNullValue()); + } + @Test public void shouldSaveInformantRegisterGenerated() { final UUID prosecutionAuthId = randomUUID(); - final LocalDate registerDate = LocalDate.now(); + // Use a fixed instant (in UTC) so the expected registerDate matches how the listener + // derives it: the framework's JSON converter normalises ZonedDateTime to UTC, and the + // listener calls getRegisterDate().toLocalDate(). Using LocalDate.now()/ZonedDateTime.now() + // here makes the test flaky around the midnight boundary in non-UTC zones (e.g. BST). + final ZonedDateTime registerDateTime = ZonedDateTime.parse("2026-04-13T09:00:00Z"); + final LocalDate registerDate = registerDateTime.toLocalDate(); final UUID fileId = randomUUID(); final InformantRegisterDocumentRequest informantRegisterDocumentRequest = informantRegisterDocumentRequest() .withProsecutionAuthorityId(prosecutionAuthId) - .withRegisterDate(ZonedDateTime.now()) + .withRegisterDate(registerDateTime) .withHearingVenue(informantRegisterHearingVenue().build()) .build(); @@ -128,6 +170,190 @@ public void shouldSaveInformantRegisterGenerated() { assertThat(informantRegisterEntity.getStatus(), is(GENERATED)); } + @Test + void generateInformantRegisterV2_shouldSetEntityStatusToGenerated() { + final UUID prosecutionAuthId = randomUUID(); + final ZonedDateTime registerDate = ZonedDateTime.parse("2026-04-13T09:00:00Z"); + + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest localDocumentRequest = + uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest() + .withProsecutionAuthorityId(prosecutionAuthId) + .withRegisterDate(registerDate) + .build(); + + final InformantRegisterGeneratedV2 event = InformantRegisterGeneratedV2.informantRegisterGeneratedV2() + .withInformantRegisterDocumentRequests(singletonList(localDocumentRequest)) + .withSystemGenerated(false) + .build(); + + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + informantRegisterEntity.setProsecutionAuthorityId(prosecutionAuthId); + informantRegisterEntity.setStatus(RECORDED); + when(informantRegisterRepository.findByProsecutionAuthorityIdAndRegisterDateForStatusRecorded( + prosecutionAuthId, registerDate.toLocalDate())).thenReturn(singletonList(informantRegisterEntity)); + + informantRegisterEventListener.generateInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated-v2"), + objectToJsonObjectConverter.convert(event))); + + assertThat(informantRegisterEntity.getStatus(), is(GENERATED)); + assertThat(informantRegisterEntity.getProcessedOn(), notNullValue()); + } + + @Test + void saveInformantRegister_withPreMigrationFlatVerdictCode_shouldNotThrow() { + final UUID prosecutionAuthId = randomUUID(); + final JsonObject offenceJson = createObjectBuilder() + .add("offenceCode", "PS90010") + .add("orderIndex", 1) + .add("offenceTitle", "Theft") + .add("pleaValue", "NOT_GUILTY") + .add("verdictCode", "G") + .build(); + final JsonObject informantRegisterDocumentRequestJson = createObjectBuilder() + .add("prosecutionAuthorityId", prosecutionAuthId.toString()) + .add("registerDate", "2026-04-13T09:00:00Z") + .add("hearingDate", "2026-04-13T09:00:00Z") + .add("hearingId", randomUUID().toString()) + .add("prosecutionAuthorityCode", "TFL") + .add("fileName", "test.csv") + .add("hearingVenue", createObjectBuilder() + .add("courtHouse", "Crown Court") + .add("ljaName", "LJA") + .add("courtSessions", createArrayBuilder() + .add(createObjectBuilder() + .add("courtRoom", "Room 1") + .add("hearingStartTime", "2026-04-13T09:00:00Z") + .add("defendants", createArrayBuilder() + .add(createObjectBuilder() + .add("name", "John Smith") + .add("address1", "1 High St") + .add("firstName", "John") + .add("lastName", "Smith") + .add("prosecutionCasesOrApplications", createArrayBuilder() + .add(createObjectBuilder() + .add("caseOrApplicationReference", "TFL123") + .add("offences", createArrayBuilder().add(offenceJson))))))))) + .build(); + final JsonObject payload = createObjectBuilder() + .add("informantRegister", informantRegisterDocumentRequestJson) + .add("prosecutionAuthorityId", prosecutionAuthId.toString()) + .build(); + + final ArgumentCaptor captor = forClass(InformantRegisterEntity.class); + + informantRegisterEventListener.saveInformantRegister(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-recorded"), payload)); + + verify(informantRegisterRepository).save(captor.capture()); + assertThat(captor.getValue().getStatus(), is(RECORDED)); + assertThat(captor.getValue().getProsecutionAuthorityId(), is(prosecutionAuthId)); + } + + @Test + void generateInformantRegister_withPreMigrationFlatVerdictCode_shouldNotThrow() { + final UUID prosecutionAuthId = randomUUID(); + final LocalDate registerDate = LocalDate.parse("2026-04-13"); + final JsonObject offenceJson = createObjectBuilder() + .add("offenceCode", "PS90010") + .add("orderIndex", 1) + .add("offenceTitle", "Theft") + .add("pleaValue", "NOT_GUILTY") + .add("verdictCode", "G") + .build(); + final JsonObject documentRequestJson = createObjectBuilder() + .add("prosecutionAuthorityId", prosecutionAuthId.toString()) + .add("registerDate", "2026-04-13T09:00:00Z") + .add("hearingDate", "2026-04-13T09:00:00Z") + .add("hearingId", randomUUID().toString()) + .add("prosecutionAuthorityCode", "TFL") + .add("fileName", "test.csv") + .add("hearingVenue", createObjectBuilder() + .add("courtHouse", "Crown Court") + .add("ljaName", "LJA") + .add("courtSessions", createArrayBuilder() + .add(createObjectBuilder() + .add("courtRoom", "Room 1") + .add("hearingStartTime", "2026-04-13T09:00:00Z") + .add("defendants", createArrayBuilder() + .add(createObjectBuilder() + .add("name", "John Smith") + .add("address1", "1 High St") + .add("firstName", "John") + .add("lastName", "Smith") + .add("prosecutionCasesOrApplications", createArrayBuilder() + .add(createObjectBuilder() + .add("caseOrApplicationReference", "TFL123") + .add("offences", createArrayBuilder().add(offenceJson))))))))) + .build(); + final JsonObject payload = createObjectBuilder() + .add("informantRegisterDocumentRequests", createArrayBuilder().add(documentRequestJson)) + .add("fileId", randomUUID().toString()) + .add("systemGenerated", false) + .build(); + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + informantRegisterEntity.setProsecutionAuthorityId(prosecutionAuthId); + informantRegisterEntity.setStatus(RECORDED); + when(informantRegisterRepository.findByProsecutionAuthorityIdAndRegisterDateForStatusRecorded(prosecutionAuthId, registerDate)).thenReturn(singletonList(informantRegisterEntity)); + + informantRegisterEventListener.generateInformantRegister(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated"), payload)); + + assertThat(informantRegisterEntity.getStatus(), is(GENERATED)); + } + + @Test + void generateInformantRegister_withNullDocumentRequests_shouldSkipAndNotInteractWithRepository() { + final JsonObject payload = createObjectBuilder() + .add("fileId", randomUUID().toString()) + .add("systemGenerated", false) + .build(); + + informantRegisterEventListener.generateInformantRegister(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated"), payload)); + + verifyNoInteractions(informantRegisterRepository); + } + + @Test + void generateInformantRegister_withEmptyDocumentRequests_shouldSkipAndNotInteractWithRepository() { + final JsonObject payload = createObjectBuilder() + .add("informantRegisterDocumentRequests", createArrayBuilder()) + .add("fileId", randomUUID().toString()) + .add("systemGenerated", false) + .build(); + + informantRegisterEventListener.generateInformantRegister(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated"), payload)); + + verifyNoInteractions(informantRegisterRepository); + } + + @Test + void generateInformantRegisterV2_withNullDocumentRequests_shouldSkipAndNotInteractWithRepository() { + final JsonObject payload = createObjectBuilder() + .add("systemGenerated", false) + .build(); + + informantRegisterEventListener.generateInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated-v2"), payload)); + + verifyNoInteractions(informantRegisterRepository); + } + + @Test + void generateInformantRegisterV2_withEmptyDocumentRequests_shouldSkipAndNotInteractWithRepository() { + final JsonObject payload = createObjectBuilder() + .add("informantRegisterDocumentRequests", createArrayBuilder()) + .add("systemGenerated", false) + .build(); + + informantRegisterEventListener.generateInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated-v2"), payload)); + + verifyNoInteractions(informantRegisterRepository); + } + @Test public void shouldNotifyInformantRegister() { final UUID prosecutionAuthId = randomUUID(); diff --git a/results-event/results-event-listener/src/yaml/subscriptions-descriptor.yaml b/results-event/results-event-listener/src/yaml/subscriptions-descriptor.yaml index 522f0777f..b7b321076 100644 --- a/results-event/results-event-listener/src/yaml/subscriptions-descriptor.yaml +++ b/results-event/results-event-listener/src/yaml/subscriptions-descriptor.yaml @@ -23,6 +23,12 @@ subscriptions_descriptor: - name: results.event.informant-register-generated schema_uri: http://justice.gov.uk/results/courts/informant-register-generated.json + - name: results.event.informant-register-recorded-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-recorded-v2.json + + - name: results.event.informant-register-generated-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-generated-v2.json + - name: results.event.informant-register-notified schema_uri: http://justice.gov.uk/results/courts/informant-register-notified.json diff --git a/results-event/results-event-processor/pom.xml b/results-event/results-event-processor/pom.xml index 080f72ee3..522f9e551 100644 --- a/results-event/results-event-processor/pom.xml +++ b/results-event/results-event-processor/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-event - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-event-processor diff --git a/results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java b/results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java index 553d9c07e..45460faa3 100644 --- a/results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java +++ b/results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java @@ -118,6 +118,31 @@ public void generateInformantRegister(final JsonEnvelope envelope) throws IOExce processInformantRegisterNotificationRequest(envelope, prosecutionAuthorityId, registerDate, fileId, templateId); } + @SuppressWarnings({"squid:S1160"}) + @Handles("results.event.informant-register-generated-v2") + public void generateInformantRegisterV2(final JsonEnvelope envelope) throws IOException, FileServiceException { + final JsonObject payload = envelope.payloadAsJsonObject(); + final List informantRegisters = payload.getJsonArray(FIELD_INFORMANT_REGISTER_DOCUMENT_REQUESTS).getValuesAs(JsonObject.class); + final JsonObject informantRegister = informantRegisters.get(0); + final UUID prosecutionAuthorityId = fromString(informantRegister.getString(FIELD_PROSECUTION_AUTHORITY_ID)); + final LocalDate registerDate = ZonedDateTime.parse(informantRegister.getString(FIELD_REGISTER_DATE)).toLocalDate(); + final String fileName = informantRegister.getString(FILE_NAME); + + final byte[] informantRegisterInBytes = generateCsvDocumentV2(informantRegisters); + + final JsonObject metadata = createObjectBuilder() + .add(FILE_NAME, fileName) + .build(); + + UUID fileId = null; + + final String templateId = informantRegister.containsKey(FIELD_RECIPIENTS) ? getTemplateId(informantRegister.getJsonArray(FIELD_RECIPIENTS)) : ""; + if (StringUtils.isNotBlank(templateId)) { + fileId = fileStorer.store(metadata, new ByteArrayInputStream(informantRegisterInBytes)); + } + processInformantRegisterNotificationRequest(envelope, prosecutionAuthorityId, registerDate, fileId, templateId); + } + @Handles("results.event.informant-register-notified-v2") public void notifyProsecutionAuthorityV2(final JsonEnvelope envelope) { notifyProsecutionAuthority(envelope); @@ -253,4 +278,110 @@ private String getAddress(final InformantRegisterDefendant defendant) { .filter(StringUtils::isNotBlank) .collect(Collectors.joining(", ")); } + + private byte[] generateCsvDocumentV2(final List informantRegistersByRegisterDate) throws IOException { + final List informantRegisters = new ArrayList<>(); + informantRegistersByRegisterDate.forEach( + informantRegister -> mapToInformantRegisterDocumentsV2(informantRegisters, informantRegister)); + + final CsvMapper csvMapper = new CsvMapper(); + final CsvSchema schema = csvMapper.schemaFor(InformantRegisterDocument.class).withHeader(); + final ObjectWriter writer = csvMapper.writer(schema); + return writer.writeValueAsBytes(informantRegisters); + } + + private void mapToInformantRegisterDocumentsV2(final List informantRegisters, final JsonObject informantRegisterJson) { + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest documentRequest = + jsonObjectToObjectConverter.convert(informantRegisterJson, uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.class); + documentRequest.getHearingVenue().getCourtSessions().forEach(courtSession -> courtSession.getDefendants().forEach(defendant -> { + if (isNotEmpty(defendant.getResults())) { + defendant.getResults().forEach(result -> buildInformantRegisterV2(informantRegisters, documentRequest, courtSession, defendant, null, result, null, null)); + } + defendant.getProsecutionCasesOrApplications().forEach(caseOrApplication -> { + if (isNotEmpty(caseOrApplication.getResults())) { + caseOrApplication.getResults().forEach(caseResult -> buildInformantRegisterV2(informantRegisters, documentRequest, courtSession, defendant, caseOrApplication, caseResult, null, null)); + } + caseOrApplication.getOffences().forEach(offence -> { + if (isNotEmpty(offence.getOffenceResults())) { + offence.getOffenceResults().forEach(offenceResult -> buildInformantRegisterV2(informantRegisters, documentRequest, courtSession, defendant, caseOrApplication, null, offence, offenceResult)); + } else { + buildInformantRegisterV2(informantRegisters, documentRequest, courtSession, defendant, caseOrApplication, null, offence, null); + } + }); + }); + })); + } + + @SuppressWarnings({"squid:S00107"}) + private void buildInformantRegisterV2( + final List informantRegisters, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest documentRequest, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing courtSession, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant defendant, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication caseOrApplication, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult result, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence offence, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult offenceResult) { + + final InformantRegisterDocument.Builder informantRegisterDocumentBuilder = informantRegisterDocument() + .withInfDestID(EMPTY_STRING) + .withName(documentRequest.getProsecutionAuthorityName()) + .withHearingStartTime(documentRequest.getHearingDate().format(ISO_INSTANT)) + .withLjaName(documentRequest.getHearingVenue().getLjaName()) + .withCourtHouse(documentRequest.getHearingVenue().getCourtHouse()) + .withCourtRoom(courtSession.getCourtRoom()) + .withSessionType(EMPTY_STRING) + .withTitle(defendant.getTitle()) + .withForeNames(defendant.getFirstName()) + .withSurName(defendant.getLastName()) + .withAddress(getAddressV2(defendant)) + .withPostCode(defendant.getPostCode()) + .withDateOfBirth(defendant.getDateOfBirth()) + .withCaseNumber(EMPTY_STRING) + .withSeqNo(EMPTY_STRING); + + if (nonNull(caseOrApplication)) { + informantRegisterDocumentBuilder + .withCaseOrApplicationReference(caseOrApplication.getCaseOrApplicationReference()) + .withArrestSummonsNumber(caseOrApplication.getArrestSummonsNumber()); + } + + if (nonNull(offence)) { + buildOffenceDetailsV2(informantRegisterDocumentBuilder, offence, offenceResult); + } + + if (nonNull(result)) { + buildResultV2(informantRegisterDocumentBuilder, result); + } + + informantRegisters.add(informantRegisterDocumentBuilder.build()); + } + + private void buildOffenceDetailsV2( + final InformantRegisterDocument.Builder builder, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence offence, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult offenceResult) { + builder.withOffenceCode(offence.getOffenceCode()); + builder.withOffenceTitle(offence.getOffenceTitle()); + builder.withPleaValue(offence.getPleaValue()); + builder.withVerdictCode(offence.getVerdict() != null ? offence.getVerdict().getVerdictCode() : null); + if (nonNull(offenceResult)) { + buildResultV2(builder, offenceResult); + } + } + + private void buildResultV2( + final InformantRegisterDocument.Builder builder, + final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterResult offenceResult) { + builder.withCjsResultCode(offenceResult.getCjsResultCode()); + builder.withResultText(offenceResult.getResultText().trim()); + } + + private String getAddressV2(final uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant defendant) { + final List addressAsList = Arrays.asList(defendant.getAddress1(), defendant.getAddress2(), defendant.getAddress3(), defendant.getAddress4(), defendant.getAddress5()); + return addressAsList + .stream() + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(", ")); + } } diff --git a/results-event/results-event-processor/src/test/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessorTest.java b/results-event/results-event-processor/src/test/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessorTest.java index 61e77fd58..051e91168 100644 --- a/results-event/results-event-processor/src/test/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessorTest.java +++ b/results-event/results-event-processor/src/test/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessorTest.java @@ -2,6 +2,7 @@ import static com.google.common.io.Resources.getResource; import static java.nio.charset.Charset.defaultCharset; +import static java.util.Collections.singletonList; import static java.util.UUID.randomUUID; import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; @@ -20,6 +21,14 @@ import static uk.gov.justice.services.test.utils.core.enveloper.EnveloperFactory.createEnveloper; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataWithRandomUUID; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication.informantRegisterCaseOrApplication; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant.informantRegisterDefendant; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing.informantRegisterHearing; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearingVenue.informantRegisterHearingVenue; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence.informantRegisterOffence; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient.informantRegisterRecipient; +import static uk.gov.justice.results.courts.informantRegisterDocument.Verdict.verdict; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; @@ -31,10 +40,20 @@ import uk.gov.justice.services.messaging.Envelope; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.messaging.MetadataBuilder; +import uk.gov.justice.results.courts.InformantRegisterGeneratedV2; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearingVenue; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterRecipient; +import uk.gov.justice.results.courts.informantRegisterDocument.Verdict; import uk.gov.moj.cpp.results.event.service.ApplicationParameters; import uk.gov.moj.cpp.results.event.service.NotificationNotifyService; import java.io.ByteArrayInputStream; +import java.time.ZonedDateTime; import java.util.UUID; import javax.json.JsonArray; @@ -137,6 +156,86 @@ public void shouldGenerateInformantRegisterForNoRecipients() throws Exception { assertThat(captor.getValue().payload().toString(), is(containsString("31af405e-7b60-4dd8-a244-c24c2d3fa595"))); } + @Test + public void generateInformantRegisterV2_whenOffenceHasVerdictCode_shouldPopulateVerdictCodeInCsvRow() throws Exception { + final UUID prosecutionAuthorityId = UUID.fromString("31af405e-7b60-4dd8-a244-c24c2d3fa595"); + final Verdict verdictObj = verdict().withVerdictCode("G").build(); + final InformantRegisterOffence offence = informantRegisterOffence() + .withOrderIndex(1) + .withOffenceCode("PS90010") + .withOffenceTitle("Theft") + .withVerdict(verdictObj) + .build(); + final InformantRegisterCaseOrApplication caseOrApplication = informantRegisterCaseOrApplication() + .withCaseOrApplicationReference("TFL123") + .withOffences(singletonList(offence)) + .build(); + final InformantRegisterDefendant defendant = informantRegisterDefendant() + .withName("John Smith") + .withFirstName("John") + .withLastName("Smith") + .withAddress1("1 High St") + .withProsecutionCasesOrApplications(singletonList(caseOrApplication)) + .build(); + final InformantRegisterHearing hearing = informantRegisterHearing() + .withCourtRoom("Room 1") + .withHearingStartTime("2026-04-13") + .withDefendants(singletonList(defendant)) + .build(); + final InformantRegisterHearingVenue venue = informantRegisterHearingVenue() + .withCourtHouse("Crown Court") + .withLjaName("LJA") + .withCourtSessions(singletonList(hearing)) + .build(); + final InformantRegisterRecipient recipient = informantRegisterRecipient() + .withRecipientName("Prosecutor") + .withEmailAddress1("test@tfl.gov.uk") + .withEmailTemplateName("informantTemplate") + .build(); + final InformantRegisterDocumentRequest docRequest = informantRegisterDocumentRequest() + .withProsecutionAuthorityId(prosecutionAuthorityId) + .withProsecutionAuthorityName("TFL Authority") + .withProsecutionAuthorityCode("TFL") + .withFileName("test.csv") + .withRegisterDate(ZonedDateTime.parse("2026-04-13T09:00:00Z")) + .withHearingDate(ZonedDateTime.parse("2026-04-13T09:00:00Z")) + .withRecipients(singletonList(recipient)) + .withHearingVenue(venue) + .build(); + final InformantRegisterGeneratedV2 event = InformantRegisterGeneratedV2.informantRegisterGeneratedV2() + .withInformantRegisterDocumentRequests(singletonList(docRequest)) + .withSystemGenerated(false) + .build(); + final JsonObject eventPayload = objectToJsonObjectConverter.convert(event); + + when(applicationParameters.getEmailTemplateId(anyString())).thenReturn(TEMPLATE_ID); + + informantRegisterEventProcessor.generateInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated-v2"), + eventPayload)); + + verify(fileStorer).store(filestorerMetadata.capture(), byteInputStreamArgumentCaptor.capture()); + final String csvOutput = IOUtils.toString(byteInputStreamArgumentCaptor.getValue(), defaultCharset()); + assertThat(csvOutput, containsString(",G,")); + } + + @Test + public void generateInformantRegisterV2_shouldProduceSameCsvOutputAsV1Handler() throws Exception { + final JsonArray informantRegisters = getJsonPayload("informant-register-document-requests.json").getJsonArray("informantRegisterDocumentRequests"); + final JsonObject payload = createObjectBuilder() + .add("informantRegisterDocumentRequests", informantRegisters) + .build(); + + when(applicationParameters.getEmailTemplateId(anyString())).thenReturn(TEMPLATE_ID); + + informantRegisterEventProcessor.generateInformantRegisterV2(envelopeFrom( + metadataWithRandomUUID("results.event.informant-register-generated-v2"), payload)); + + verify(fileStorer).store(filestorerMetadata.capture(), byteInputStreamArgumentCaptor.capture()); + final String expectedCsvContent = getFileContent("informantRegister.csv"); + assertThat(IOUtils.toString(byteInputStreamArgumentCaptor.getValue(), defaultCharset()), is(expectedCsvContent)); + } + @Test public void notifyProsecutionAuthority() { final JsonArrayBuilder recipientJsonArray = createArrayBuilder(); @@ -185,7 +284,6 @@ public void notifyProsecutionAuthorityV2() { assertThat(notificationJson.getValue().getString("templateId"), is(TEMPLATE_ID)); assertThat(notificationJson.getValue().getString("sendToAddress"), is(emailAddress1)); assertThat(notificationJson.getValue().getString("fileId"), is(fileId.toString())); - } private static JsonObject getJsonPayload(final String fileName) { @@ -193,17 +291,14 @@ private static JsonObject getJsonPayload(final String fileName) { } private static String getFileContent(final String fileName) { - String response = null; try { - response = Resources.toString( + return Resources.toString( getResource(fileName), defaultCharset() ); } catch (final Exception e) { - e.printStackTrace(); + throw new IllegalStateException("Failed to read test resource: " + fileName, e); } - - return response; } } diff --git a/results-event/results-event-processor/src/yaml/subscriptions-descriptor.yaml b/results-event/results-event-processor/src/yaml/subscriptions-descriptor.yaml index 7d13da3f1..5409079ea 100644 --- a/results-event/results-event-processor/src/yaml/subscriptions-descriptor.yaml +++ b/results-event/results-event-processor/src/yaml/subscriptions-descriptor.yaml @@ -27,8 +27,12 @@ subscriptions_descriptor: schema_uri: http://justice.gov.uk/core/courts/results.event.nces-email-notification.json - name: results.event.informant-register-generated schema_uri: http://justice.gov.uk/results/courts/informant-register-generated.json + - name: results.event.informant-register-generated-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-generated-v2.json - name: results.event.informant-register-notified schema_uri: http://justice.gov.uk/results/courts/informant-register-notified.json + - name: results.event.informant-register-notified-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-notified-v2.json - name: results.event.appeal-update-notification-requested schema_uri: http://moj.gov.uk/cpp/results/domain/event/results.event.appeal-update-notification-requested.json - name: results.event.publish-to-dcs diff --git a/results-healthchecks/pom.xml b/results-healthchecks/pom.xml index 8f0c8747d..b908cb296 100644 --- a/results-healthchecks/pom.xml +++ b/results-healthchecks/pom.xml @@ -3,7 +3,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT 4.0.0 diff --git a/results-integration-test/pom.xml b/results-integration-test/pom.xml index 4c8911ef4..39d8505c5 100644 --- a/results-integration-test/pom.xml +++ b/results-integration-test/pom.xml @@ -4,7 +4,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-integration-test diff --git a/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterDocumentRequestIT.java b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterDocumentRequestIT.java index c705edbfc..83d601745 100644 --- a/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterDocumentRequestIT.java +++ b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterDocumentRequestIT.java @@ -60,6 +60,53 @@ public void shouldAddInformantRegisterRequest() throws IOException { helper.verifyInformantRegisterIsNotified(prosecutionAuthorityId); } + @Test + public void shouldAddInformantRegisterRequestWithVerdictAndExposeItInProsecutorResults() throws IOException { + final UUID prosecutionAuthorityId = randomUUID(); + final UUID hearingId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final ZonedDateTime hearingDate = now(UTC).minusHours(1); + final String prosecutionAuthorityCode = STRING.next(); + final String prosecutionAuthorityOuCode = randomAlphanumeric(7); + + final Response writeResponse = recordInformantRegister(prosecutionAuthorityId, prosecutionAuthorityCode, prosecutionAuthorityOuCode, registerDate, + hearingId, hearingDate, "json/informant-register/results.add-informant-register-document-request-with-verdict.json"); + assertThat(writeResponse.getStatusCode(), equalTo(SC_ACCEPTED)); + helper.verifyInformantRegisterRequestsExists(prosecutionAuthorityId); + + helper.verifyProsecutorResultsContainVerdict(prosecutionAuthorityOuCode, registerDate.toLocalDate(), "G"); + + generateInformantRegister(); + helper.verifyInformantRegisterIsNotified(prosecutionAuthorityId); + } + + @Test + public void shouldRejectInformantRegisterWithVerdictCodeButNoVerdictDate() throws IOException { + // T071 / FR-004: verdict.json `dependencies` makes verdictCode and verdictDate co-dependent. + // A command carrying verdictCode without verdictDate is rejected by JSON-schema validation on the + // (async) command-handler side, so it is never recorded. Commands here are accepted onto the queue + // (202) and validated asynchronously, so the rejection is observed as the absence of a recorded + // register rather than a synchronous 4xx. + final UUID rejectedProsecutionAuthorityId = randomUUID(); + final UUID hearingId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final ZonedDateTime hearingDate = now(UTC).minusHours(1); + final String prosecutionAuthorityCode = STRING.next(); + + final Response writeResponse = recordInformantRegister(rejectedProsecutionAuthorityId, prosecutionAuthorityCode, registerDate, + hearingId, hearingDate, "json/informant-register/results.add-informant-register-document-request-with-invalid-verdict.json"); + assertThat(writeResponse.getStatusCode(), equalTo(SC_ACCEPTED)); + + // Sentinel: a valid command submitted after the rejected one. Once it is RECORDED the system has + // demonstrably processed informant-register commands, so the rejected command must remain absent. + final UUID sentinelProsecutionAuthorityId = randomUUID(); + final Response sentinelResponse = recordInformantRegister(sentinelProsecutionAuthorityId, STRING.next(), now(UTC), + randomUUID(), now(UTC).minusHours(1), "json/informant-register/results.add-informant-register-document-request.json"); + assertThat(sentinelResponse.getStatusCode(), equalTo(SC_ACCEPTED)); + + helper.verifyInformantRegisterRejectedNotRecorded(sentinelProsecutionAuthorityId, rejectedProsecutionAuthorityId); + } + @Test public void shouldAddInformantRegisterRequestForGroupCases() throws IOException { final UUID prosecutionAuthorityId = randomUUID(); diff --git a/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterEventFlowsIT.java b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterEventFlowsIT.java new file mode 100644 index 000000000..d767ffb1d --- /dev/null +++ b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterEventFlowsIT.java @@ -0,0 +1,220 @@ +package uk.gov.moj.cpp.results.it; + +import static java.time.ZoneOffset.UTC; +import static java.time.ZonedDateTime.now; +import static java.util.UUID.randomUUID; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static uk.gov.justice.services.messaging.JsonEnvelope.metadataBuilder; +import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; +import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; +import static uk.gov.justice.services.messaging.JsonObjects.createReader; +import static uk.gov.moj.cpp.results.it.utils.FileUtil.getPayload; +import static uk.gov.moj.cpp.results.it.utils.QueueUtil.privateEvents; +import static uk.gov.moj.cpp.results.it.utils.QueueUtil.sendMessage; +import static uk.gov.moj.cpp.results.it.utils.WireMockStubUtils.setupUsersGroupQueryStub; + +import uk.gov.justice.services.messaging.Metadata; +import uk.gov.moj.cpp.results.it.helper.InformantRegisterDocumentRequestHelper; + +import java.io.StringReader; +import java.time.ZonedDateTime; +import java.util.Random; +import java.util.UUID; + +import javax.jms.JMSException; +import javax.jms.MessageProducer; +import javax.json.JsonObject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Exercises the four informant-register domain-event flows at the event-listener boundary by publishing each event + * directly onto the internal {@code results.event} topic and asserting the resulting view-store projection: + * + *
    + *
  • {@code results.event.informant-register-recorded} (V1, core namespace) -> RECORDED projection
  • + *
  • {@code results.event.informant-register-recorded-v2} (V2, results namespace) -> RECORDED projection
  • + *
  • {@code results.event.informant-register-generated} (V1) advances a RECORDED register past RECORDED
  • + *
  • {@code results.event.informant-register-generated-v2} (V2) advances a RECORDED register past RECORDED
  • + *
+ * + *

The current command path only ever emits the V2 events, so the V1 events are produced here synthetically to + * confirm their listener handlers still resolve and project correctly (this is the listener-resolution guarantee the + * V2/generated-class consolidation depends on). The {@code generated} events only transition existing RECORDED + * registers, so each generate test first injects the matching {@code recorded} event.

+ */ +public class InformantRegisterEventFlowsIT { + + private static final String EVENT_RECORDED = "results.event.informant-register-recorded"; + private static final String EVENT_RECORDED_V2 = "results.event.informant-register-recorded-v2"; + private static final String EVENT_GENERATED = "results.event.informant-register-generated"; + private static final String EVENT_GENERATED_V2 = "results.event.informant-register-generated-v2"; + + private static final String V2_DOCUMENT_REQUEST_RESOURCE = + "json/informant-register/results.add-informant-register-document-request.json"; + + private static final Random RANDOM = new Random(); + + private InformantRegisterDocumentRequestHelper helper; + private MessageProducer privateProducer; + + @BeforeAll + public static void setupStubs() { + setupUsersGroupQueryStub(); + } + + @BeforeEach + public void setup() { + helper = new InformantRegisterDocumentRequestHelper(); + privateProducer = privateEvents.createProducer(); + } + + @AfterEach + public void tearDown() throws JMSException { + privateProducer.close(); + } + + @Test + public void recordedV2EventShouldProjectRecordedRegister() { + final UUID prosecutionAuthorityId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final JsonObject documentRequest = v2DocumentRequest(prosecutionAuthorityId, registerDate, randomUUID()); + + publishRecorded(EVENT_RECORDED_V2, prosecutionAuthorityId, documentRequest); + + helper.verifyInformantRegisterRequestsExists(prosecutionAuthorityId); + } + + @Test + public void generatedV2EventShouldAdvanceRecordedRegister() { + final UUID prosecutionAuthorityId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final JsonObject documentRequest = v2DocumentRequest(prosecutionAuthorityId, registerDate, randomUUID()); + + publishRecorded(EVENT_RECORDED_V2, prosecutionAuthorityId, documentRequest); + helper.verifyInformantRegisterRequestsExists(prosecutionAuthorityId); + + publishGenerated(EVENT_GENERATED_V2, documentRequest); + helper.verifyInformantRegisterNoLongerRecorded(prosecutionAuthorityId); + } + + @Test + public void recordedEventShouldProjectRecordedRegister() { + final UUID prosecutionAuthorityId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final JsonObject documentRequest = v1DocumentRequest(prosecutionAuthorityId, registerDate, randomUUID()); + + publishRecorded(EVENT_RECORDED, prosecutionAuthorityId, documentRequest); + + helper.verifyInformantRegisterRequestsExists(prosecutionAuthorityId); + } + + @Test + public void generatedEventShouldAdvanceRecordedRegister() { + final UUID prosecutionAuthorityId = randomUUID(); + final ZonedDateTime registerDate = now(UTC); + final JsonObject documentRequest = v1DocumentRequest(prosecutionAuthorityId, registerDate, randomUUID()); + + publishRecorded(EVENT_RECORDED, prosecutionAuthorityId, documentRequest); + helper.verifyInformantRegisterRequestsExists(prosecutionAuthorityId); + + publishGenerated(EVENT_GENERATED, documentRequest); + helper.verifyInformantRegisterNoLongerRecorded(prosecutionAuthorityId); + } + + private void publishRecorded(final String eventName, final UUID prosecutionAuthorityId, final JsonObject documentRequest) { + final JsonObject payload = createObjectBuilder() + .add("prosecutionAuthorityId", prosecutionAuthorityId.toString()) + .add("informantRegister", documentRequest) + .build(); + sendMessage(privateProducer, eventName, payload, metadataFor(eventName)); + } + + private void publishGenerated(final String eventName, final JsonObject documentRequest) { + // Both generated schemas are additionalProperties:false and expose only + // informantRegisterDocumentRequests + systemGenerated (the V1 schema also allows materialId), + // so no extra fields may be added or the envelope is rejected at validation. + final JsonObject payload = createObjectBuilder() + .add("informantRegisterDocumentRequests", createArrayBuilder().add(documentRequest)) + .add("systemGenerated", true) + .build(); + sendMessage(privateProducer, eventName, payload, metadataFor(eventName)); + } + + private Metadata metadataFor(final String eventName) { + // Inject under a dedicated test source (not "results") with a high random event number so the + // event buffer does not treat these synthetic events as already-seen for the real results source. + // A fresh random stream id keeps each injected event at position 1 of its own stream. + return metadataBuilder() + .withId(randomUUID()) + .withStreamId(randomUUID()) + .withPosition(1) + .withPreviousEventNumber(123) + .withEventNumber(Math.abs(RANDOM.nextLong())) + .withSource("event-indexer-test") + .withName(eventName) + .withUserId(randomUUID().toString()) + .build(); + } + + /** + * A complete, schema-valid V2 (results-namespace) informant-register document request, built by reusing the + * command-side test fixture so it stays in lock-step with the production schema. + */ + private JsonObject v2DocumentRequest(final UUID prosecutionAuthorityId, final ZonedDateTime registerDate, final UUID hearingId) { + final String body = getPayload(V2_DOCUMENT_REQUEST_RESOURCE) + .replaceAll("%PROSECUTION_AUTHORITY_ID%", prosecutionAuthorityId.toString()) + .replaceAll("%PROSECUTION_AUTHORITY_CODE%", randomAlphanumeric(7)) + .replaceAll("%PROSECUTION_AUTHORITY_OU_CODE%", randomAlphanumeric(7)) + .replaceAll("%REGISTER_DATE%", registerDate.toString()) + .replaceAll("%HEARING_ID%", hearingId.toString()) + .replaceAll("%HEARING_DATE%", registerDate.minusHours(1).toString()); + return createReader(new StringReader(body)).readObject(); + } + + /** + * A V1 (core-namespace) informant-register document request. The pre-migration shape carries a flat + * {@code verdictCode} on the offence rather than a structured verdict object. + */ + private JsonObject v1DocumentRequest(final UUID prosecutionAuthorityId, final ZonedDateTime registerDate, final UUID hearingId) { + final JsonObject offence = createObjectBuilder() + .add("offenceCode", "PS90010") + .add("orderIndex", 1) + .add("offenceTitle", "Theft") + .add("pleaValue", "NOT_GUILTY") + .add("verdictCode", "G") + .build(); + + final JsonObject hearingVenue = createObjectBuilder() + .add("courtHouse", "Crown Court") + .add("ljaName", "LJA") + .add("courtSessions", createArrayBuilder() + .add(createObjectBuilder() + .add("courtRoom", "Room 1") + .add("hearingStartTime", registerDate.minusHours(1).toString()) + .add("defendants", createArrayBuilder() + .add(createObjectBuilder() + .add("name", "John Smith") + .add("address1", "1 High St") + .add("firstName", "John") + .add("lastName", "Smith") + .add("prosecutionCasesOrApplications", createArrayBuilder() + .add(createObjectBuilder() + .add("caseOrApplicationReference", "TFL123") + .add("offences", createArrayBuilder().add(offence)))))))) + .build(); + + return createObjectBuilder() + .add("prosecutionAuthorityId", prosecutionAuthorityId.toString()) + .add("registerDate", registerDate.toString()) + .add("hearingDate", registerDate.minusHours(1).toString()) + .add("hearingId", hearingId.toString()) + .add("prosecutionAuthorityCode", randomAlphanumeric(7)) + .add("fileName", "informant-register.csv") + .add("hearingVenue", hearingVenue) + .build(); + } +} diff --git a/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/helper/InformantRegisterDocumentRequestHelper.java b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/helper/InformantRegisterDocumentRequestHelper.java index b63688399..dec84eb1c 100644 --- a/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/helper/InformantRegisterDocumentRequestHelper.java +++ b/results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/helper/InformantRegisterDocumentRequestHelper.java @@ -10,6 +10,7 @@ import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; @@ -24,6 +25,7 @@ import uk.gov.justice.services.common.http.HeaderConstants; import java.io.IOException; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.UUID; @@ -88,6 +90,18 @@ private String getInformantRegisterDocumentRequests(final String status, final M .getPayload(); } + /** + * Verifies that a rejected informant-register command was never recorded. The valid + * {@code sentinelProsecutionAuthorityId} (submitted after the rejected command) becoming RECORDED + * proves the system has processed informant-register commands; the rejected command must remain absent. + */ + public void verifyInformantRegisterRejectedNotRecorded(final UUID sentinelProsecutionAuthorityId, final UUID rejectedProsecutionAuthorityId) { + getInformantRegisterDocumentRequests(RECORDED.name(), + withJsonPath("$.informantRegisterDocumentRequests[*].prosecutionAuthorityId", hasItem(sentinelProsecutionAuthorityId.toString())), + withJsonPath("$.informantRegisterDocumentRequests[*].prosecutionAuthorityId", not(hasItem(rejectedProsecutionAuthorityId.toString()))) + ); + } + public void verifyInformantRegisterIsNotified(final UUID prosecutionAuthorityId) { getInformantRegisterDocumentRequests(NOTIFIED.name(), allOf( withJsonPath("$.informantRegisterDocumentRequests[*].status", hasItem(NOTIFIED.name())), @@ -95,6 +109,31 @@ public void verifyInformantRegisterIsNotified(final UUID prosecutionAuthorityId) )); } + /** + * Verifies that a register for the given prosecuting authority has progressed out of RECORDED — i.e. an + * informant-register-generated(-v2) event was consumed by the listener and advanced the projection. This is + * asserted as absence from the RECORDED list (rather than presence in GENERATED) because the generate flow can + * progress further to NOTIFIED asynchronously: the processor reacts to the generated event by sending a + * notify-informant-register command. Absence-from-RECORDED is stable whether the register lands on GENERATED or + * NOTIFIED. + */ + public void verifyInformantRegisterNoLongerRecorded(final UUID prosecutionAuthorityId) { + getInformantRegisterDocumentRequests(RECORDED.name(), + withJsonPath("$.informantRegisterDocumentRequests[*].prosecutionAuthorityId", not(hasItem(prosecutionAuthorityId.toString()))) + ); + } + + public void verifyProsecutorResultsContainVerdict(final String ouCode, final LocalDate startDate, final String expectedVerdictCode) { + pollWithDefaults(requestParams(getReadUrl(StringUtils.join("/prosecutor/", ouCode, "?startDate=", startDate.toString())), + "application/vnd.results.prosecutor-results+json") + .withHeader(HeaderConstants.USER_ID, USER_ID).build()) + .until( + status().is(OK), + payload().isJson(allOf( + withJsonPath("$.hearingVenues[*].courtSessions[*].defendants[*].prosecutionCasesOrApplications[*].offences[*].verdict.verdictCode", hasItem(expectedVerdictCode)) + ))); + } + public static Response recordInformantRegister(final UUID prosecutionAuthorityId, final String prosecutionAuthorityCode, final ZonedDateTime registerDate, final UUID hearingId, final ZonedDateTime hearingDate, final String fileName) throws IOException { return recordInformantRegister(prosecutionAuthorityId, prosecutionAuthorityCode, randomAlphanumeric(7), registerDate, hearingId, hearingDate, fileName, null); } diff --git a/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-invalid-verdict.json b/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-invalid-verdict.json new file mode 100644 index 000000000..8f4a31100 --- /dev/null +++ b/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-invalid-verdict.json @@ -0,0 +1,100 @@ +{ + "fileName": "InformantRegister_TFL_2020-04-12.csv", + "hearingVenue": { + "courtHouse": "Lavender Hill Magistrates' Court", + "courtSessions": [ + { + "courtRoom": "Room name", + "hearingStartTime": "2020-03-12", + "defendants": [ + { + "name": "Fred Smith", + "dateOfBirth": "1999-12-27", + "address1": "Flat 1", + "address2": "1 Old Road", + "address3": "London", + "address4": "Merton", + "postCode": "SW99 1AA", + "title": "MR", + "firstName": "Fred", + "lastName": "Smith", + "prosecutionCasesOrApplications": [ + { + "caseOrApplicationReference": "TFL4359536", + "offences": [ + { + "offenceCode": "PS90010", + "orderIndex": 1, + "offenceTitle": "Public service vehicle - passenger use altered / defaced ticket", + "pleaValue": "NOT_GUILTY", + "verdict": { + "verdictCode": "G" + }, + "offenceResults": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ] + } + ], + "results": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ], + "arrestSummonsNumber": "ASN_1999999" + } + ] + }, + { + "name": "Fred Daligarce", + "dateOfBirth": "1965-12-27", + "address1": "Flat 1", + "address2": "1 Old Road", + "address3": "London", + "address4": "Merton", + "postCode": "SW99 1AA", + "title": "MR", + "firstName": "Fred", + "lastName": "Daligarce", + "prosecutionCasesOrApplications": [ + { + "caseOrApplicationReference": "TFL4359536", + "offences": [ + { + "offenceCode": "PS90010", + "orderIndex": 1, + "offenceTitle": "Public service vehicle - passenger use altered / defaced ticket", + "pleaValue": "GUILTY" + } + ], + "results": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ], + "arrestSummonsNumber": "ASN_200000001" + } + ] + } + ] + } + ] + }, + "prosecutionAuthorityOuCode": "%PROSECUTION_AUTHORITY_OU_CODE%", + "prosecutionAuthorityCode": "%PROSECUTION_AUTHORITY_CODE%", + "prosecutionAuthorityId": "%PROSECUTION_AUTHORITY_ID%", + "majorCreditorCode": "PF01", + "prosecutionAuthorityName": "London Police", + "recipients": [ + { + "emailAddress1": "info@attendancelegalpanel.cjsm.net", + "emailAddress2": "", + "emailTemplateName": "ir_standard", + "recipientName": "Cambridge County Council (Educational Welfare)" + } + ], + "registerDate": "%REGISTER_DATE%", + "hearingDate": "%HEARING_DATE%", + "hearingId": "%HEARING_ID%" +} diff --git a/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-verdict.json b/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-verdict.json new file mode 100644 index 000000000..036c006fb --- /dev/null +++ b/results-integration-test/src/test/resources/json/informant-register/results.add-informant-register-document-request-with-verdict.json @@ -0,0 +1,102 @@ +{ + "fileName": "InformantRegister_TFL_2020-04-12.csv", + "hearingVenue": { + "courtHouse": "Lavender Hill Magistrates' Court", + "courtSessions": [ + { + "courtRoom": "Room name", + "hearingStartTime": "2020-03-12", + "defendants": [ + { + "name": "Fred Smith", + "dateOfBirth": "1999-12-27", + "address1": "Flat 1", + "address2": "1 Old Road", + "address3": "London", + "address4": "Merton", + "postCode": "SW99 1AA", + "title": "MR", + "firstName": "Fred", + "lastName": "Smith", + "prosecutionCasesOrApplications": [ + { + "caseOrApplicationReference": "TFL4359536", + "offences": [ + { + "offenceCode": "PS90010", + "orderIndex": 1, + "offenceTitle": "Public service vehicle - passenger use altered / defaced ticket", + "pleaValue": "NOT_GUILTY", + "verdict": { + "verdictCode": "G", + "verdictDate": "2020-03-12", + "verdictType": "FOUND_GUILTY" + }, + "offenceResults": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ] + } + ], + "results": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ], + "arrestSummonsNumber": "ASN_1999999" + } + ] + }, + { + "name": "Fred Daligarce", + "dateOfBirth": "1965-12-27", + "address1": "Flat 1", + "address2": "1 Old Road", + "address3": "London", + "address4": "Merton", + "postCode": "SW99 1AA", + "title": "MR", + "firstName": "Fred", + "lastName": "Daligarce", + "prosecutionCasesOrApplications": [ + { + "caseOrApplicationReference": "TFL4359536", + "offences": [ + { + "offenceCode": "PS90010", + "orderIndex": 1, + "offenceTitle": "Public service vehicle - passenger use altered / defaced ticket", + "pleaValue": "GUILTY" + } + ], + "results": [ + { + "resultText": "Pay by date /n Reserve Terms Lump sum" + } + ], + "arrestSummonsNumber": "ASN_200000001" + } + ] + } + ] + } + ] + }, + "prosecutionAuthorityOuCode": "%PROSECUTION_AUTHORITY_OU_CODE%", + "prosecutionAuthorityCode": "%PROSECUTION_AUTHORITY_CODE%", + "prosecutionAuthorityId": "%PROSECUTION_AUTHORITY_ID%", + "majorCreditorCode": "PF01", + "prosecutionAuthorityName": "London Police", + "recipients": [ + { + "emailAddress1": "info@attendancelegalpanel.cjsm.net", + "emailAddress2": "", + "emailTemplateName": "ir_standard", + "recipientName": "Cambridge County Council (Educational Welfare)" + } + ], + "registerDate": "%REGISTER_DATE%", + "hearingDate": "%HEARING_DATE%", + "hearingId": "%HEARING_ID%" +} diff --git a/results-json/pom.xml b/results-json/pom.xml index 1f39b6528..abc1ee7c3 100644 --- a/results-json/pom.xml +++ b/results-json/pom.xml @@ -3,7 +3,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT 4.0.0 diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterCaseOrApplication.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterCaseOrApplication.json new file mode 100644 index 000000000..35b2ff085 --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterCaseOrApplication.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterCaseOrApplication.json", + "description": "The details of a defendants prosecution case or application rendered in a informant register document", + "type": "object", + "properties": { + "caseOrApplicationReference": { + "description": "The case or application reference", + "type": "string" + }, + "arrestSummonsNumber": { + "type": "string" + }, + "applicationParticulars": { + "type": "string" + }, + "offences": { + "description": "The details of the offences for the defendant", + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterOffence.json" + } + }, + "results": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json" + } + } + }, + "required": [ + "caseOrApplicationReference", + "offences" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDefendant.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDefendant.json new file mode 100644 index 000000000..cf7a4c9aa --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDefendant.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDefendant.json", + "description": "The details of a defendant rendered in a informant register document", + "type": "object", + "properties": { + "name": { + "description": "The defendant name", + "type": "string" + }, + "dateOfBirth": { + "description": "The defendant date of birth", + "type": "string", + "format": "date" + }, + "address1": { + "description": "The defendant address line 1", + "type": "string" + }, + "address2": { + "description": "The defendant address line 2", + "type": "string" + }, + "address3": { + "description": "The defendant address line 3", + "type": "string" + }, + "address4": { + "description": "The defendant address line 4", + "type": "string" + }, + "address5": { + "description": "The defendant address line 5", + "type": "string" + }, + "postCode": { + "description": "The defendant postCode", + "type": "string" + }, + "nationality": { + "description": "The defendant nationality", + "type": "string" + }, + "title": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "prosecutionCasesOrApplications": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterCaseOrApplication.json" + } + }, + "results": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json" + } + } + }, + "required": [ + "name", + "address1" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDocumentRequest.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDocumentRequest.json new file mode 100644 index 000000000..cf7340b13 --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDocumentRequest.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json", + "description": "The details required to batch and generate a informant register document. Batching is by date, prosecuting authority id, court Centre Id, hearing Id", + "type": "object", + "properties": { + "registerDate": { + "type": "string", + "format": "date-time" + }, + "hearingDate": { + "type": "string", + "format": "date-time" + }, + "hearingId": { + "description": "The hearing id of the hearing shared", + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + }, + "prosecutionAuthorityId": { + "description": "The prosecuting authority reference data identifier", + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + }, + "prosecutionAuthorityCode": { + "description": "The prosecuting authority reference data code", + "type": "string" + }, + "prosecutionAuthorityOuCode": { + "description": "The prosecuting authority reference data organisation unit code", + "type": "string" + }, + "majorCreditorCode": { + "type": "string" + }, + "prosecutionAuthorityName": { + "type": "string", + "description": "The name of the prosecution authority from reference data. Required for documentation purposes." + }, + "fileName": { + "type": "string" + }, + "recipients": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterRecipient.json" + } + }, + "hearingVenue": { + "description": "The venue for the hearings for the prosecuting authority in question", + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearingVenue.json" + }, + "groupId": { + "description": "The group id of the group cases", + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + } + }, + "required": [ + "registerDate", + "hearingId", + "hearingDate", + "prosecutionAuthorityId", + "prosecutionAuthorityCode", + "hearingVenue", + "fileName" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearing.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearing.json new file mode 100644 index 000000000..542369109 --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearing.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearing.json", + "description": "The hearing details for the informant register entry", + "type": "object", + "properties": { + "courtRoom": { + "description": "The court room where the hearing was proceeded", + "type": "string" + }, + "hearingStartTime": { + "description": "The start time of the hearing", + "type": "string", + "format": "time" + }, + "defendants": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDefendant.json" + } + } + }, + "required": [ + "courtRoom", + "hearingStartTime", + "defendants" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearingVenue.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearingVenue.json new file mode 100644 index 000000000..aa47bed4c --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearingVenue.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearingVenue.json", + "description": "The details of a hearing venue rendered in a informant register", + "type": "object", + "properties": { + "ljaName": { + "description": "The court venue local justice area Name", + "type": "string" + }, + "courtHouse": { + "description": "The court venue Name", + "type": "string" + }, + "courtSessions": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearing.json" + } + } + }, + "required": [ + "courtHouse", + "courtSessions" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterOffence.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterOffence.json new file mode 100644 index 000000000..87ffa5a1a --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterOffence.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterOffence.json", + "description": "The details of a defendant offence for a prosecution case rendered in a informant register document", + "type": "object", + "properties": { + "originatingCaseUrn": { + "description": "URN of the case containing the offence", + "type": "string" + }, + "offenceCode": { + "description": "The cjs code for the offence", + "type": "string" + }, + "orderIndex": { + "description": "The offence index", + "type": "integer", + "minimum": 0 + }, + "offenceTitle": { + "description": "The title taken from ref data", + "type": "string" + }, + "pleaValue": { + "description": "The defendants plea against the offence", + "type": "string" + }, + "verdict": { + "description": "The structured verdict recorded against the offence", + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json" + }, + "offenceResults": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json" + } + } + }, + "required": [ + "offenceCode", + "orderIndex", + "offenceTitle" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterRecipient.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterRecipient.json new file mode 100644 index 000000000..b1bc186eb --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterRecipient.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterRecipient.json", + "description": "The recipient details for the Informant Register document", + "type": "object", + "properties": { + "recipientName": { + "type": "string" + }, + "emailAddress1": { + "type": "string" + }, + "emailAddress2": { + "type": "string" + }, + "emailTemplateName": { + "type": "string" + } + }, + "required": [ + "recipientName", + "emailAddress1", + "emailTemplateName" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResult.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResult.json new file mode 100644 index 000000000..ea03220d8 --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResult.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json", + "description": "The details of a published result rendered in a informant register document", + "type": "object", + "properties": { + "resultText": { + "description": "The textual description of the result", + "type": "string" + }, + "cjsResultCode": { + "description": "The cjs code for the result", + "type": "string" + }, + "resultData": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResultData.json" + } + }, + "required": [ + "resultText" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResultData.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResultData.json new file mode 100644 index 000000000..e7062d4e3 --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResultData.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResultData.json", + "description": "The details of a published result rendered in a informant register document", + "type": "object", + "properties": { + "amount": { + "description": "", + "type": "string" + }, + "nextHearingDate": { + "type": "string" + }, + "nextCourtLocation": { + "type": "string" + }, + "durationValue": { + "type": "string" + }, + "durationUnit": { + "type": "string" + }, + "durationStartDate": { + "type": "string" + }, + "durationEndDate": { + "type": "string" + }, + "secondaryDurationValue": { + "type": "string" + }, + "secondaryDurationUnit": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/prosecutorResult.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/prosecutorResult.json new file mode 100644 index 000000000..41dc240bd --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/prosecutorResult.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/prosecutorResult.json", + "description": "Response structure for results returned to prosecutor", + "type": "object", + "properties": { + "startDate": { + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/datePattern" + }, + "endDate": { + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/datePattern" + }, + "prosecutionAuthorityId": { + "description": "The prosecuting authority reference data identifier", + "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" + }, + "prosecutionAuthorityCode": { + "description": "The prosecuting authority reference data code", + "type": "string" + }, + "prosecutionAuthorityName": { + "type": "string", + "description": "The name of the prosecution authority from reference data." + }, + "prosecutionAuthorityOuCode": { + "type": "string" + }, + "majorCreditorCode": { + "type": "string" + }, + "hearingVenues": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearingVenue.json" + } + } + }, + "required": [ + "startDate", + "prosecutionAuthorityId", + "prosecutionAuthorityCode" + ], + "additionalProperties": false +} diff --git a/results-json/src/main/resources/json/schema/informantRegisterDocument/verdict.json b/results-json/src/main/resources/json/schema/informantRegisterDocument/verdict.json new file mode 100644 index 000000000..9f6c452ab --- /dev/null +++ b/results-json/src/main/resources/json/schema/informantRegisterDocument/verdict.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json", + "description": "Verdict recorded against an SJP offence", + "type": "object", + "properties": { + "verdictCode": { + "description": "Verdict code: G (FOUND_GUILTY), N (FOUND_NOT_GUILTY), PSJ (PROVED_SJP)", + "type": "string" + }, + "verdictDate": { + "description": "Conviction date in yyyy-MM-dd format. Optional, but when present verdictCode is required.", + "type": "string" + }, + "verdictType": { + "description": "Verdict type resolved from reference data: FOUND_GUILTY, FOUND_NOT_GUILTY, PROVED_SJP", + "type": "string" + } + }, + "dependencies": { + "verdictDate": ["verdictCode"] + }, + "additionalProperties": false +} diff --git a/results-performance-test/pom.xml b/results-performance-test/pom.xml index a2c91650c..c4c280bdf 100644 --- a/results-performance-test/pom.xml +++ b/results-performance-test/pom.xml @@ -3,7 +3,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT 4.0.0 diff --git a/results-query/pom.xml b/results-query/pom.xml index f56fbce77..09b6f581c 100644 --- a/results-query/pom.xml +++ b/results-query/pom.xml @@ -4,7 +4,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-query diff --git a/results-query/results-query-api/pom.xml b/results-query/results-query-api/pom.xml index d932f50a6..f40d72a51 100644 --- a/results-query/results-query-api/pom.xml +++ b/results-query/results-query-api/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-query - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-query-api diff --git a/results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json b/results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json index dfe1e7600..6b6fdb4fe 100644 --- a/results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json +++ b/results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json @@ -3,5 +3,5 @@ "description": "Response schema for prosecutor results", "id": "http://justice.gov.uk/results/courts/informant-register/prosecutor-result.json", "type": "object", - "$ref": "http://justice.gov.uk/core/courts/informantRegisterDocument/prosecutorResult.json" + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/prosecutorResult.json" } \ No newline at end of file diff --git a/results-query/results-query-view/pom.xml b/results-query/results-query-view/pom.xml index 925c702ca..044525b7f 100644 --- a/results-query/results-query-view/pom.xml +++ b/results-query/results-query-view/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-query - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-query-view diff --git a/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryView.java b/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryView.java index af0d0145a..93e0be714 100644 --- a/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryView.java +++ b/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryView.java @@ -10,6 +10,7 @@ import static uk.gov.justice.services.messaging.JsonObjects.getString; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; +import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; import uk.gov.justice.services.core.annotation.Component; import uk.gov.justice.services.core.annotation.Handles; import uk.gov.justice.services.core.annotation.ServiceComponent; @@ -26,9 +27,11 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import javax.json.JsonArray; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; import org.apache.commons.lang3.StringUtils; @@ -39,6 +42,11 @@ public class InformantRegisterDocumentRequestQueryView { private static final String FIELD_FILE_ID = "fileId"; private static final String FIELD_PROSECUTION_AUTHORITY_CODE = "prosecutionAuthorityCode"; private static final String FIELD_REGISTER_DATE = "registerDate"; + private static final String FIELD_PAYLOAD = "payload"; + private static final String FIELD_VERDICT = "verdict"; + private static final String FIELD_VERDICT_CODE = "verdictCode"; + private static final String FIELD_VERDICT_TYPE = "verdictType"; + private static final String FIELD_VERDICT_DATE = "verdictDate"; @Inject private InformantRegisterRepository informantRegisterRepository; @@ -46,6 +54,9 @@ public class InformantRegisterDocumentRequestQueryView { @Inject private ObjectToJsonObjectConverter objectToJsonObjectConverter; + @Inject + private StringToJsonObjectConverter stringToJsonObjectConverter; + @Handles("results.query.informant-register-document-request") public JsonEnvelope getInformantRegisterRequests(final JsonEnvelope envelope) { final JsonObjectBuilder jsonObjectBuilder = createObjectBuilder(); @@ -54,16 +65,80 @@ public JsonEnvelope getInformantRegisterRequests(final JsonEnvelope envelope) { if (isNotBlank(requestStatus)) { if(RegisterStatus.RECORDED.toString().equalsIgnoreCase(requestStatus)) { final List informantRegisterEntities = informantRegisterRepository.findByStatusRecorded(); - informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(objectToJsonObjectConverter.convert(informantRegisterEntity))); + informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(convertWithNormalisedVerdict(informantRegisterEntity))); } else { final List informantRegisterEntities = informantRegisterRepository.findByStatus(RegisterStatus.valueOf(requestStatus)); - informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(objectToJsonObjectConverter.convert(informantRegisterEntity))); + informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(convertWithNormalisedVerdict(informantRegisterEntity))); } } return envelopeFrom(envelope.metadata(), jsonObjectBuilder.add(FIELD_INFORMANT_REGISTER_DOCUMENTS, jsonArrayBuilder.build()).build()); } + /** + * Converts the entity to JSON and normalises every offence in the stored payload so that the + * verdict is always represented as a structured {@code verdict} object. Legacy payloads that + * carry only a bare {@code verdictCode} string on the offence are rewritten to + * {@code {"verdictCode": , "verdictType": null, "verdictDate": null}}; payloads that + * already hold a {@code verdict} object are left untouched. + */ + private JsonObject convertWithNormalisedVerdict(final InformantRegisterEntity informantRegisterEntity) { + final JsonObject converted = objectToJsonObjectConverter.convert(informantRegisterEntity); + final String payload = converted.getString(FIELD_PAYLOAD, null); + if (StringUtils.isBlank(payload)) { + return converted; + } + final String normalisedPayload = deepTransformObject(stringToJsonObjectConverter.convert(payload)).toString(); + final JsonObjectBuilder builder = createObjectBuilder(); + converted.forEach((key, value) -> { + if (FIELD_PAYLOAD.equals(key)) { + builder.add(FIELD_PAYLOAD, normalisedPayload); + } else { + builder.add(key, value); + } + }); + return builder.build(); + } + + private JsonObject deepTransformObject(final JsonObject object) { + final boolean wrapVerdictCode = object.containsKey(FIELD_VERDICT_CODE) && !object.containsKey(FIELD_VERDICT); + final JsonObjectBuilder builder = createObjectBuilder(); + object.forEach((key, value) -> { + if (wrapVerdictCode && FIELD_VERDICT_CODE.equals(key)) { + return; + } + if (!FIELD_VERDICT.equals(key) && value.getValueType() == JsonValue.ValueType.OBJECT) { + builder.add(key, deepTransformObject(value.asJsonObject())); + } else if (value.getValueType() == JsonValue.ValueType.ARRAY) { + builder.add(key, deepTransformArray(value.asJsonArray())); + } else { + builder.add(key, value); + } + }); + if (wrapVerdictCode) { + builder.add(FIELD_VERDICT, createObjectBuilder() + .add(FIELD_VERDICT_CODE, object.getString(FIELD_VERDICT_CODE)) + .addNull(FIELD_VERDICT_TYPE) + .addNull(FIELD_VERDICT_DATE) + .build()); + } + return builder.build(); + } + + private JsonArray deepTransformArray(final JsonArray array) { + final JsonArrayBuilder builder = createArrayBuilder(); + array.forEach(item -> { + if (item.getValueType() == JsonValue.ValueType.OBJECT) { + builder.add(deepTransformObject(item.asJsonObject())); + } else if (item.getValueType() == JsonValue.ValueType.ARRAY) { + builder.add(deepTransformArray(item.asJsonArray())); + } else { + builder.add(item); + } + }); + return builder.build(); + } + @Handles("results.query.informant-register-document-by-material") public JsonEnvelope getInformantRegistersByMaterial(final JsonEnvelope envelope) { final UUID fileId = fromString(envelope.payloadAsJsonObject().getString(FIELD_FILE_ID)); @@ -72,7 +147,7 @@ public JsonEnvelope getInformantRegistersByMaterial(final JsonEnvelope envelope) final JsonArrayBuilder jsonArrayBuilder = createArrayBuilder(); final List informantRegisterEntities = informantRegisterRepository.findByFileId(fileId); - informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(objectToJsonObjectConverter.convert(informantRegisterEntity))); + informantRegisterEntities.forEach(informantRegisterEntity -> jsonArrayBuilder.add(convertWithNormalisedVerdict(informantRegisterEntity))); return envelopeFrom(envelope.metadata(), jsonObjectBuilder.add(FIELD_INFORMANT_REGISTER_DOCUMENTS, jsonArrayBuilder.build()).build()); @@ -102,7 +177,7 @@ public JsonEnvelope getInformantRegistersByRequestDate(final JsonEnvelope envelo .flatMap(List::stream) .collect(Collectors.toList()); } - informantRegisterEntities.forEach(i -> jsonArrayBuilder.add(objectToJsonObjectConverter.convert(i))); + informantRegisterEntities.forEach(i -> jsonArrayBuilder.add(convertWithNormalisedVerdict(i))); }); return envelopeFrom(envelope.metadata(), diff --git a/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryView.java b/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryView.java index def8c98d1..5b3909300 100644 --- a/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryView.java +++ b/results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryView.java @@ -2,16 +2,16 @@ import static java.util.stream.Collectors.toList; import static org.apache.commons.collections.CollectionUtils.isNotEmpty; -import static uk.gov.justice.core.courts.informantRegisterDocument.ProsecutorResult.prosecutorResult; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.messaging.JsonObjects.getString; +import static uk.gov.justice.results.courts.informantRegisterDocument.ProsecutorResult.prosecutorResult; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; -import uk.gov.justice.core.courts.informantRegisterDocument.ProsecutorResult; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.informantRegisterDocument.ProsecutorResult; import uk.gov.moj.cpp.results.persist.InformantRegisterRepository; import uk.gov.moj.cpp.results.persist.entity.InformantRegisterEntity; @@ -50,7 +50,6 @@ public JsonEnvelope getProsecutorResults(final JsonEnvelope envelope) { final Optional optionalOuCode = getString(payload, FIELD_OUCODE); if (!(optionalStartDate.isPresent() && optionalOuCode.isPresent())) { - // this should not happen as API level validation would have stopped this return null; } diff --git a/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryViewTest.java b/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryViewTest.java index 4c7bf3a72..a10a8a44c 100644 --- a/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryViewTest.java +++ b/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/InformantRegisterDocumentRequestQueryViewTest.java @@ -3,6 +3,7 @@ import static com.google.common.collect.Lists.newArrayList; import static java.time.LocalDate.now; import static java.util.UUID.randomUUID; +import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -12,6 +13,7 @@ import static uk.gov.moj.cpp.domains.constant.RegisterStatus.RECORDED; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; +import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.moj.cpp.results.persist.InformantRegisterRepository; import uk.gov.moj.cpp.results.persist.entity.InformantRegisterEntity; @@ -26,6 +28,7 @@ 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; @ExtendWith(MockitoExtension.class) @@ -37,6 +40,9 @@ public class InformantRegisterDocumentRequestQueryViewTest { @Mock private ObjectToJsonObjectConverter objectToJsonObjectConverter; + @Spy + private StringToJsonObjectConverter stringToJsonObjectConverter = new StringToJsonObjectConverter(); + @InjectMocks private InformantRegisterDocumentRequestQueryView informantRegisterDocumentRequestQueryView; @@ -154,4 +160,143 @@ public void shouldGetInformantRegisterByDateWhenProsecutionAuthorityIsEmpty() { assertThat(informantRegisterRequests.payloadAsJsonObject().getJsonArray("informantRegisterDocumentRequests") .getJsonObject(0).getString("registerDate"), is(registerDate.toString())); } + + @Test + public void shouldConvertOffenceVerdictCodeToVerdictObjectWhenOnlyVerdictCodeIsPresent() { + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + final JsonObject converted = createObjectBuilder() + .add("payload", payloadWithOffence(createObjectBuilder().add("verdictCode", "G"))) + .build(); + + final JsonEnvelope envelope = recordedRequestEnvelope(); + when(objectToJsonObjectConverter.convert(informantRegisterEntity)).thenReturn(converted); + when(informantRegisterRepository.findByStatusRecorded()).thenReturn(Collections.singletonList(informantRegisterEntity)); + + final JsonObject offence = firstOffence(informantRegisterDocumentRequestQueryView.getInformantRegisterRequests(envelope)); + + assertThat(offence.containsKey("verdictCode"), is(false)); + assertThat(offence.getJsonObject("verdict").getString("verdictCode"), is("G")); + assertThat(offence.getJsonObject("verdict").isNull("verdictType"), is(true)); + assertThat(offence.getJsonObject("verdict").isNull("verdictDate"), is(true)); + } + + @Test + public void shouldKeepVerdictObjectAsIsWhenAlreadyPresent() { + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + final JsonObject verdictObject = createObjectBuilder() + .add("verdictCode", "G") + .add("verdictType", "FOUND_GUILTY") + .add("verdictDate", "2026-04-13") + .build(); + final JsonObject converted = createObjectBuilder() + .add("payload", payloadWithOffence(createObjectBuilder().add("verdict", verdictObject))) + .build(); + + final JsonEnvelope envelope = recordedRequestEnvelope(); + when(objectToJsonObjectConverter.convert(informantRegisterEntity)).thenReturn(converted); + when(informantRegisterRepository.findByStatusRecorded()).thenReturn(Collections.singletonList(informantRegisterEntity)); + + final JsonObject offence = firstOffence(informantRegisterDocumentRequestQueryView.getInformantRegisterRequests(envelope)); + + assertThat(offence.containsKey("verdictCode"), is(false)); + assertThat(offence.getJsonObject("verdict").getString("verdictCode"), is("G")); + assertThat(offence.getJsonObject("verdict").getString("verdictType"), is("FOUND_GUILTY")); + assertThat(offence.getJsonObject("verdict").getString("verdictDate"), is("2026-04-13")); + } + + @Test + void shouldConvertOffenceVerdictCodeToVerdictObjectForByDateQueryWhenOnlyVerdictCodeIsPresent() { + final LocalDate registerDate = now(); + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + final JsonObject converted = createObjectBuilder() + .add("payload", payloadWithOffence(createObjectBuilder().add("verdictCode", "G"))) + .build(); + + final JsonObject payload = createObjectBuilder().add("registerDate", registerDate.toString()).build(); + final JsonEnvelope envelope = envelopeFrom(metadataBuilder().withId(randomUUID()) + .withName("results.query.informant-register-document-by-request-date").build(), payload); + + when(objectToJsonObjectConverter.convert(informantRegisterEntity)).thenReturn(converted); + when(informantRegisterRepository.findByRegisterDate(registerDate)).thenReturn(newArrayList(informantRegisterEntity)); + + final JsonObject offence = firstOffence(informantRegisterDocumentRequestQueryView.getInformantRegistersByRequestDate(envelope)); + + assertThat(offence.containsKey("verdictCode"), is(false)); + assertThat(offence.getJsonObject("verdict").getString("verdictCode"), is("G")); + assertThat(offence.getJsonObject("verdict").isNull("verdictType"), is(true)); + assertThat(offence.getJsonObject("verdict").isNull("verdictDate"), is(true)); + } + + @Test + void shouldConvertOffenceVerdictCodeToVerdictObjectForByMaterialQueryWhenOnlyVerdictCodeIsPresent() { + final UUID fileId = randomUUID(); + final InformantRegisterEntity informantRegisterEntity = new InformantRegisterEntity(); + final JsonObject converted = createObjectBuilder() + .add("payload", payloadWithOffence(createObjectBuilder().add("verdictCode", "G"))) + .build(); + + final JsonObject payload = createObjectBuilder().add("fileId", fileId.toString()).build(); + final JsonEnvelope envelope = envelopeFrom(metadataBuilder().withId(randomUUID()) + .withName("results.query.informant-register-document-by-material").build(), payload); + + when(objectToJsonObjectConverter.convert(informantRegisterEntity)).thenReturn(converted); + when(informantRegisterRepository.findByFileId(fileId)).thenReturn(newArrayList(informantRegisterEntity)); + + final JsonObject offence = firstOffence(informantRegisterDocumentRequestQueryView.getInformantRegistersByMaterial(envelope)); + + assertThat(offence.containsKey("verdictCode"), is(false)); + assertThat(offence.getJsonObject("verdict").getString("verdictCode"), is("G")); + assertThat(offence.getJsonObject("verdict").isNull("verdictType"), is(true)); + assertThat(offence.getJsonObject("verdict").isNull("verdictDate"), is(true)); + } + + private JsonEnvelope recordedRequestEnvelope() { + return envelopeFrom(metadataBuilder().withId(randomUUID()) + .withName("results.query.informant-register-document-request").build(), + createObjectBuilder().add("requestStatus", RECORDED.name()).build()); + } + + private String payloadWithOffence(final javax.json.JsonObjectBuilder offenceBuilder) { + final JsonObject offence = offenceBuilder + .add("offenceCode", "PS90010") + .add("orderIndex", 1) + .add("offenceTitle", "An offence") + .build(); + final JsonObject caseOrApplication = createObjectBuilder() + .add("caseOrApplicationReference", "TFL4359536") + .add("offences", createArrayBuilder().add(offence).build()) + .build(); + final JsonObject defendant = createObjectBuilder() + .add("name", "Fred Smith") + .add("address1", "Flat 1") + .add("prosecutionCasesOrApplications", createArrayBuilder().add(caseOrApplication).build()) + .build(); + final JsonObject hearing = createObjectBuilder() + .add("courtRoom", "Room name") + .add("hearingStartTime", "2020-03-12") + .add("defendants", createArrayBuilder().add(defendant).build()) + .build(); + final JsonObject hearingVenue = createObjectBuilder() + .add("courtHouse", "Lavender Hill Magistrates' Court") + .add("courtSessions", createArrayBuilder().add(hearing).build()) + .build(); + return createObjectBuilder() + .add("fileName", "InformantRegister_TFL_2020-04-12.csv") + .add("hearingVenue", hearingVenue) + .build() + .toString(); + } + + private JsonObject firstOffence(final JsonEnvelope informantRegisterRequests) { + final String payload = informantRegisterRequests.payloadAsJsonObject() + .getJsonArray("informantRegisterDocumentRequests") + .getJsonObject(0) + .getString("payload"); + return stringToJsonObjectConverter.convert(payload) + .getJsonObject("hearingVenue") + .getJsonArray("courtSessions").getJsonObject(0) + .getJsonArray("defendants").getJsonObject(0) + .getJsonArray("prosecutionCasesOrApplications").getJsonObject(0) + .getJsonArray("offences").getJsonObject(0); + } } diff --git a/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryViewTest.java b/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryViewTest.java index 787b73419..6ab067fe8 100644 --- a/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryViewTest.java +++ b/results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryViewTest.java @@ -13,18 +13,29 @@ import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; -import static uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterHearingVenue.informantRegisterHearingVenue; import static uk.gov.justice.services.messaging.Envelope.metadataBuilder; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication.informantRegisterCaseOrApplication; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant.informantRegisterDefendant; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest.informantRegisterDocumentRequest; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing.informantRegisterHearing; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearingVenue.informantRegisterHearingVenue; +import static uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence.informantRegisterOffence; +import static uk.gov.justice.results.courts.informantRegisterDocument.Verdict.verdict; -import uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.messaging.MetadataBuilder; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterCaseOrApplication; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDefendant; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterDocumentRequest; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearing; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterHearingVenue; +import uk.gov.justice.results.courts.informantRegisterDocument.InformantRegisterOffence; +import uk.gov.justice.results.courts.informantRegisterDocument.Verdict; import uk.gov.moj.cpp.results.persist.InformantRegisterRepository; import uk.gov.moj.cpp.results.persist.entity.InformantRegisterEntity; @@ -32,6 +43,7 @@ import java.util.List; import java.util.UUID; +import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import org.junit.jupiter.api.BeforeEach; @@ -134,15 +146,60 @@ public void getProsecutorResults_NoResultsAvailable() { assertThat(prosecutorResults.payloadAsJsonObject().getString("prosecutionAuthorityName", null), nullValue()); } + @Test + public void getProsecutorResults_whenRequiredParamsMissing_shouldReturnNull() { + final JsonEnvelope prosecutorResults = prosecutorResultsQueryView.getProsecutorResults(createPayload(null, null, null)); + assertThat(prosecutorResults, nullValue()); + } + + @Test + public void getProsecutorResults_whenOffenceHasVerdict_shouldIncludeVerdictInResponse() { + final Verdict verdictObj = verdict().withVerdictCode("G").withVerdictDate("2026-04-13").withVerdictType("FOUND_GUILTY").build(); + final List results = getResultsWithVerdict(verdictObj); + when(informantRegisterRepository.findByProsecutionAuthorityOuCodeAndRegisterDateRange(ouCode, now(), now())).thenReturn(results); + + final JsonEnvelope prosecutorResults = prosecutorResultsQueryView.getProsecutorResults(createPayload(ouCode, now().toString(), now().toString())); + + final JsonObject offence = getFirstOffence(prosecutorResults); + assertThat(offence.getJsonObject("verdict").getString("verdictCode"), is("G")); + assertThat(offence.getJsonObject("verdict").getString("verdictDate"), is("2026-04-13")); + assertThat(offence.getJsonObject("verdict").getString("verdictType"), is("FOUND_GUILTY")); + } + + @Test + public void getProsecutorResults_whenOffenceHasNoVerdict_shouldOmitVerdictField() { + final List results = getResultsWithVerdict(null); + when(informantRegisterRepository.findByProsecutionAuthorityOuCodeAndRegisterDateRange(ouCode, now(), now())).thenReturn(results); + + final JsonEnvelope prosecutorResults = prosecutorResultsQueryView.getProsecutorResults(createPayload(ouCode, now().toString(), now().toString())); + + final JsonObject offence = getFirstOffence(prosecutorResults); + assertThat(offence.containsKey("verdict"), is(false)); + } + + @Test + public void getProsecutorResults_whenOffenceVerdictIsNull_shouldOmitVerdictField() { + final List results = getResultsWithVerdict(null); + when(informantRegisterRepository.findByProsecutionAuthorityOuCodeAndRegisterDateRange(ouCode, now(), now())).thenReturn(results); + + final JsonEnvelope prosecutorResults = prosecutorResultsQueryView.getProsecutorResults(createPayload(ouCode, now().toString(), now().toString())); + + final JsonObject offence = getFirstOffence(prosecutorResults); + assertThat(offence.get("verdict"), nullValue()); + } + private JsonEnvelope createPayload(final String ouCode, final String startDate, final String endDate) { final MetadataBuilder metadataBuilder = metadataBuilder().withId(randomUUID()).withName("results.prosecutor-results"); - final JsonObjectBuilder payloadBuilder = createObjectBuilder() - .add("ouCode", ouCode) - .add("startDate", startDate); + final JsonObjectBuilder payloadBuilder = createObjectBuilder(); + if (nonNull(ouCode)) { + payloadBuilder.add("ouCode", ouCode); + } + if (nonNull(startDate)) { + payloadBuilder.add("startDate", startDate); + } if (nonNull(endDate)) { payloadBuilder.add("endDate", endDate); } - return envelopeFrom(metadataBuilder, payloadBuilder); } @@ -155,8 +212,53 @@ private List getResults() { .withProsecutionAuthorityName(prosecutionAuthorityName) .withProsecutionAuthorityCode(prosecutionAuthorityCode) .build(); - entity.setPayload(objectToJsonObjectConverter.convert(informantRegisterDocumentRequest).toString()); return singletonList(entity); } -} \ No newline at end of file + + private List getResultsWithVerdict(final Verdict verdictObj) { + final InformantRegisterEntity entity = new InformantRegisterEntity(); + final InformantRegisterOffence offence = informantRegisterOffence() + .withOrderIndex(1) + .withOffenceCode("OFF001") + .withOffenceTitle("Theft") + .withVerdict(verdictObj) + .build(); + final InformantRegisterCaseOrApplication caseOrApplication = informantRegisterCaseOrApplication() + .withCaseOrApplicationReference("CASE001") + .withOffences(singletonList(offence)) + .build(); + final InformantRegisterDefendant defendant = informantRegisterDefendant() + .withName("John Smith") + .withAddress1("1 Main St") + .withProsecutionCasesOrApplications(singletonList(caseOrApplication)) + .build(); + final InformantRegisterHearing hearing = informantRegisterHearing() + .withCourtRoom("Court 1") + .withHearingStartTime("2026-04-13") + .withDefendants(singletonList(defendant)) + .build(); + final InformantRegisterHearingVenue hearingVenue = informantRegisterHearingVenue() + .withCourtHouse("Crown Court") + .withCourtSessions(singletonList(hearing)) + .build(); + final InformantRegisterDocumentRequest documentRequest = informantRegisterDocumentRequest() + .withHearingVenue(hearingVenue) + .withProsecutionAuthorityOuCode(ouCode) + .withProsecutionAuthorityId(prosecutionAuthorityId) + .withProsecutionAuthorityName(prosecutionAuthorityName) + .withProsecutionAuthorityCode(prosecutionAuthorityCode) + .build(); + entity.setPayload(objectToJsonObjectConverter.convert(documentRequest).toString()); + return singletonList(entity); + } + + private JsonObject getFirstOffence(final JsonEnvelope prosecutorResults) { + return prosecutorResults.payloadAsJsonObject() + .getJsonArray("hearingVenues").get(0).asJsonObject() + .getJsonArray("courtSessions").get(0).asJsonObject() + .getJsonArray("defendants").get(0).asJsonObject() + .getJsonArray("prosecutionCasesOrApplications").get(0).asJsonObject() + .getJsonArray("offences").get(0).asJsonObject(); + } +} diff --git a/results-service/pom.xml b/results-service/pom.xml index d34fa13e4..34ea6782d 100644 --- a/results-service/pom.xml +++ b/results-service/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-service war diff --git a/results-viewstore/pom.xml b/results-viewstore/pom.xml index b5db634f5..7a33159ad 100644 --- a/results-viewstore/pom.xml +++ b/results-viewstore/pom.xml @@ -4,7 +4,7 @@ results-parent uk.gov.moj.cpp.results - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-viewstore diff --git a/results-viewstore/results-viewstore-liquibase/pom.xml b/results-viewstore/results-viewstore-liquibase/pom.xml index 5a25c006f..afeb6e0a1 100644 --- a/results-viewstore/results-viewstore-liquibase/pom.xml +++ b/results-viewstore/results-viewstore-liquibase/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-viewstore - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-viewstore-liquibase diff --git a/results-viewstore/results-viewstore-persistence/pom.xml b/results-viewstore/results-viewstore-persistence/pom.xml index f47264b10..69a29244f 100644 --- a/results-viewstore/results-viewstore-persistence/pom.xml +++ b/results-viewstore/results-viewstore-persistence/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.results results-viewstore - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT results-viewstore-persistence diff --git a/runIntegrationTests.sh b/runIntegrationTests.sh index c43f46ffb..7801e8ee6 100755 --- a/runIntegrationTests.sh +++ b/runIntegrationTests.sh @@ -3,7 +3,7 @@ # Script that runs, liquibase, deploys wars and runs integration tests CONTEXT_NAME=results - +# FRAMEWORK_LIBRARIES_VERSION=$(mvn help:evaluate -Dexpression=framework-libraries.version -q -DforceStdout) FRAMEWORK_VERSION=$(mvn help:evaluate -Dexpression=framework.version -q -DforceStdout) EVENT_STORE_VERSION=$(mvn help:evaluate -Dexpression=event-store.version -q -DforceStdout) diff --git a/specs/001-informant-register-local-schema/checklists/requirements.md b/specs/001-informant-register-local-schema/checklists/requirements.md new file mode 100644 index 000000000..8ac107f04 --- /dev/null +++ b/specs/001-informant-register-local-schema/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Include Verdict in SJP Results — results context + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-06-16 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded (results context only) +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover all primary flows within this context +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Scope trimmed to cpp-context-results only per team instruction (2026-06-16). +- All 8 ACs from CIMD-3915 are represented within the results context boundary. +- All items pass. Spec is ready for `/speckit-plan`. diff --git a/specs/001-informant-register-local-schema/contracts/event-contracts.md b/specs/001-informant-register-local-schema/contracts/event-contracts.md new file mode 100644 index 000000000..141829034 --- /dev/null +++ b/specs/001-informant-register-local-schema/contracts/event-contracts.md @@ -0,0 +1,228 @@ +# Event Contracts: Include Verdict in SJP Results + +**Date**: 2026-06-16 | **Branch**: `CIMD-3915-informant-register-local-schema` + +## New Domain Events + +### `results.event.informant-register-recorded-v2` + +Emitted by: `InformantRegisterHandler.handleAddInformantRegisterToEventStream` +Schema `$id`: `http://justice.gov.uk/results/courts/informant-register-recorded-v2.json` +Schema file: `results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-recorded-v2.json` +Subscription (listener): added to `results-event-listener/src/yaml/subscriptions-descriptor.yaml` +Subscription (processor): NOT subscribed (processor does not need to react to recorded V2) + +```json +{ + "prosecutionAuthorityId": "", + "informantRegister": { + "registerDate": "2026-04-13T10:00:00.000Z", + "hearingDate": "2026-04-13T09:00:00.000Z", + "hearingId": "", + "prosecutionAuthorityId": "", + "prosecutionAuthorityCode": "DVSA", + "fileName": "informant-register-dvsa.csv", + "hearingVenue": { + "courtHouse": "Westminster Magistrates Court", + "courtSessions": [ + { + "courtRoom": "1", + "hearingStartTime": "09:00", + "defendants": [ + { + "name": "John Smith", + "address1": "1 Main Street", + "prosecutionCasesOrApplications": [ + { + "caseOrApplicationReference": "EARL001390", + "offences": [ + { + "offenceCode": "SF75060", + "offenceTitle": "Speed in excess of limit", + "orderIndex": 1, + "pleaValue": "GUILTY", + "verdict": { + "verdictCode": "G", + "verdictDate": "2026-04-13", + "verdictType": "FOUND_GUILTY" + }, + "offenceResults": [ + { + "resultText": "Fine", + "cjsResultCode": "1015" + } + ] + } + ] + } + ] + } + ] + } + ] + } + } +} +``` + +--- + +### `results.event.informant-register-generated-v2` + +Emitted by: `InformantRegisterHandler.processRequests` (called from both generate handlers) +Schema `$id`: `http://justice.gov.uk/results/courts/informant-register-generated-v2.json` +Schema file: `results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-generated-v2.json` +Subscription (listener): added to `results-event-listener/src/yaml/subscriptions-descriptor.yaml` +Subscription (processor): added to `results-event-processor/src/yaml/subscriptions-descriptor.yaml` + +```json +{ + "informantRegisterDocumentRequests": [ + { + "registerDate": "2026-04-13T10:00:00.000Z", + "hearingDate": "2026-04-13T09:00:00.000Z", + "hearingId": "", + "prosecutionAuthorityId": "", + "prosecutionAuthorityCode": "DVSA", + "fileName": "informant-register-dvsa.csv", + "hearingVenue": { "...": "same structure as above" } + } + ], + "systemGenerated": false +} +``` + +--- + +## Modified Command Schema + +### `results.add-informant-register` (command) + +Schema file: `results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json` + +**Before** (delegates entirely to core-domain): +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://moj.gov.uk/cpp/results/command/add-informant-register-request.json", + "$ref": "http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" +} +``` + +**After** (references local schema): +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://moj.gov.uk/cpp/results/command/add-informant-register-request.json", + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json" +} +``` + +--- + +## Modified Query Schema + +### `results.prosecutor-results` (query response) + +Schema file: `results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json` + +**Before** (delegates entirely to core-domain): +```json +{ + "$ref": "http://justice.gov.uk/core/courts/informantRegisterDocument/prosecutorResult.json" +} +``` + +**After** (references local schema with verdict): +```json +{ + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/prosecutorResult.json" +} +``` + +The local `prosecutorResult.json` transitively includes the local `informantRegisterOffence.json` which has the `verdict` object. + +--- + +## Subscription Descriptor Changes + +### Listener (`results-event-listener/src/yaml/subscriptions-descriptor.yaml`) + +Add after existing `results.event.informant-register-generated` entry: +```yaml +- name: results.event.informant-register-recorded-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-recorded-v2.json + +- name: results.event.informant-register-generated-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-generated-v2.json +``` + +### Processor (`results-event-processor/src/yaml/subscriptions-descriptor.yaml`) + +Add after existing `results.event.informant-register-generated` entry: +```yaml +- name: results.event.informant-register-generated-v2 + schema_uri: http://justice.gov.uk/results/courts/informant-register-generated-v2.json +``` + +--- + +## Verdict Object Schema (`verdict.json`) + +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json", + "description": "Verdict recorded against an SJP offence", + "type": "object", + "properties": { + "verdictCode": { + "description": "Verdict code: G (FOUND_GUILTY), N (FOUND_NOT_GUILTY), PSJ (PROVED_SJP)", + "type": "string" + }, + "verdictDate": { + "description": "Conviction date in yyyy-MM-dd format. Mandatory when verdictCode is present.", + "type": "string" + }, + "verdictType": { + "description": "Verdict type resolved from reference data: FOUND_GUILTY, FOUND_NOT_GUILTY, PROVED_SJP", + "type": "string" + } + }, + "additionalProperties": false +} +``` + +--- + +## Offence Schema Change (`informantRegisterOffence.json` — local) + +**Key change**: `verdictCode: String` → `verdict: { verdictCode, verdictDate, verdictType }` + +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterOffence.json", + "description": "The details of a defendant offence for an informant register document", + "type": "object", + "properties": { + "originatingCaseUrn": { "type": "string" }, + "offenceCode": { "type": "string" }, + "orderIndex": { "type": "integer", "minimum": 0 }, + "offenceTitle": { "type": "string" }, + "pleaValue": { "type": "string" }, + "verdict": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json" + }, + "offenceResults": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json" + } + } + }, + "required": ["offenceCode", "orderIndex", "offenceTitle"], + "additionalProperties": false +} +``` diff --git a/specs/001-informant-register-local-schema/data-model.md b/specs/001-informant-register-local-schema/data-model.md new file mode 100644 index 000000000..186137dce --- /dev/null +++ b/specs/001-informant-register-local-schema/data-model.md @@ -0,0 +1,196 @@ +# Data Model: Include Verdict in SJP Results + +**Date**: 2026-06-16 | **Branch**: `CIMD-3915-informant-register-local-schema` + +## New Local Types (`uk.gov.moj.cpp.results.domain.informant.model`) + +All types are hand-written POJOs with explicit builder, constructors, and getters. No Lombok. No Jackson annotations beyond `@JsonCreator` / `@JsonProperty` where needed for deserialization. + +### `Verdict` +``` +verdictCode : String (optional — "G", "N", "PSJ") +verdictDate : String (optional — yyyy-MM-dd; co-present with verdictCode) +verdictType : String (optional — "FOUND_GUILTY", "FOUND_NOT_GUILTY", "PROVED_SJP") +``` +Constraints: `verdictCode` and `verdictDate` are co-dependent — both present or both absent. + +### `InformantRegisterOffence` (replaces core-domain) +``` +offenceCode : String (required) +offenceTitle : String (required) +orderIndex : Integer (required) +originatingCaseUrn : String (optional) +pleaValue : String (optional) +verdict : Verdict (optional — replaces flat verdictCode) +offenceResults : List (optional, min 1 if present) +``` + +### `InformantRegisterResult` (replaces core-domain) +``` +resultText : String (required) +cjsResultCode : String (optional) +resultData : InformantRegisterResultData (optional) +``` + +### `InformantRegisterResultData` (replaces core-domain) +``` +amount : String (optional) +nextHearingDate : String (optional — date-time) +nextCourtLocation : String (optional) +durationValue : String (optional) +durationUnit : String (optional) +durationStartDate : String (optional — date-time) +durationEndDate : String (optional — date-time) +secondaryDurationValue : String (optional) +secondaryDurationUnit : String (optional) +``` + +### `InformantRegisterCaseOrApplication` (replaces core-domain) +``` +caseOrApplicationReference : String (required) +arrestSummonsNumber : String (optional) +applicationParticulars : String (optional) +offences : List (required, min 1) +results : List (optional, min 1 if present) +``` + +### `InformantRegisterDefendant` (replaces core-domain) +``` +name : String (required) +address1 : String (required) +address2–5 : String (optional) +postCode : String (optional) +dateOfBirth : String (optional — date) +nationality : String (optional) +title : String (optional) +firstName : String (optional) +lastName : String (optional) +prosecutionCasesOrApplications : List (optional, min 1 if present) +results : List (optional, min 1 if present) +``` + +### `InformantRegisterHearing` (replaces core-domain) +``` +courtRoom : String (required) +hearingStartTime : String (required — time) +defendants : List (required, min 1) +``` + +### `InformantRegisterHearingVenue` (replaces core-domain) +``` +ljaName : String (optional) +courtHouse : String (required) +courtSessions : List (required, min 1) +``` + +### `InformantRegisterRecipient` (replaces core-domain) +``` +recipientName : String (required) +emailAddress1 : String (required) +emailAddress2 : String (optional) +emailTemplateName : String (required) +``` + +### `InformantRegisterDocumentRequest` (replaces core-domain) +``` +registerDate : ZonedDateTime (required) +hearingDate : ZonedDateTime (required) +hearingId : UUID (required) +prosecutionAuthorityId : UUID (required) +prosecutionAuthorityCode : String (required) +prosecutionAuthorityOuCode : String (optional) +majorCreditorCode : String (optional) +prosecutionAuthorityName : String (optional) +fileName : String (required) +recipients : List (optional, min 1 if present) +hearingVenue : InformantRegisterHearingVenue (required) +groupId : UUID (optional) +``` + +### `ProsecutorResult` (replaces core-domain) +``` +startDate : LocalDate (required) +endDate : LocalDate (optional) +prosecutionAuthorityId : UUID (required) +prosecutionAuthorityCode : String (required) +prosecutionAuthorityName : String (optional) +prosecutionAuthorityOuCode : String (optional) +majorCreditorCode : String (optional) +hearingVenues : List (optional) +``` + +--- + +## New Domain Event Types + +### `InformantRegisterRecordedV2` (new — in results-domain-common or domain-event) +``` +prosecutionAuthorityId : UUID (required) +informantRegister : InformantRegisterDocumentRequest (local, required) +``` +Event name: `results.event.informant-register-recorded-v2` + +### `InformantRegisterGeneratedV2` (new) +``` +informantRegisterDocumentRequests : List (local, required) +systemGenerated : Boolean (optional) +``` +Event name: `results.event.informant-register-generated-v2` + +--- + +## JSON Schema Files (new / modified) + +### New local sub-schemas under `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/` + +| File | `$id` namespace | +|------|-----------------| +| `verdict.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json` | +| `informantRegisterOffence.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterOffence.json` | +| `informantRegisterResult.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResult.json` | +| `informantRegisterResultData.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterResultData.json` | +| `informantRegisterCaseOrApplication.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterCaseOrApplication.json` | +| `informantRegisterDefendant.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDefendant.json` | +| `informantRegisterHearing.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearing.json` | +| `informantRegisterHearingVenue.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterHearingVenue.json` | +| `informantRegisterRecipient.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterRecipient.json` | +| `informantRegisterDocumentRequest.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json` | +| `prosecutorResult.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/prosecutorResult.json` | + +### New V2 event schemas under `results-domain/results-domain-common/src/main/resources/json/schema/` + +| File | `$id` namespace | +|------|-----------------| +| `results.event.informant-register-recorded-v2.json` | `http://justice.gov.uk/results/courts/informant-register-recorded-v2.json` | +| `results.event.informant-register-generated-v2.json` | `http://justice.gov.uk/results/courts/informant-register-generated-v2.json` | + +### Modified command schema under `results-command/results-command-api/src/raml/json/schema/` + +| File | Change | +|------|--------| +| `results.add-informant-register.json` | Replace `$ref` to core-domain with local `$ref` to `informantRegisterDocumentRequest.json` | + +### Modified query schema under `results-query/results-query-api/src/raml/json/schema/` + +| File | Change | +|------|--------| +| `results.prosecutor-results.json` | Replace `$ref` to core-domain `prosecutorResult.json` with local `$ref` including `verdict` object | + +--- + +## State Transitions (Informant Register lifecycle — unchanged) + +``` +RECORDED → GENERATED → NOTIFIED +``` +The lifecycle is unaffected by this change. The verdict enrichment only alters the payload shape within each state. + +--- + +## Backward Compatibility + +Pre-migration events in the event store carry `informantRegister.hearingVenue.courtSessions[*].defendants[*].prosecutionCasesOrApplications[*].offences[*].verdictCode` as a flat string. When these are replayed: + +- V1 listener handlers (`informant-register-recorded`, `informant-register-generated`) still deserialize correctly using the core-domain types — these handlers are not removed. +- The local `InformantRegisterOffence` POJO must tolerate a missing `verdict` field when deserialized from stored JSON — `verdict` is optional. +- The `InformantRegisterEntity.payload` (stored as raw JSON string) for pre-migration records will not have a `verdict` object — the query view returns those offences without a `verdict` field (correct per FR-003). diff --git a/specs/001-informant-register-local-schema/plan.md b/specs/001-informant-register-local-schema/plan.md new file mode 100644 index 000000000..e64d588c2 --- /dev/null +++ b/specs/001-informant-register-local-schema/plan.md @@ -0,0 +1,299 @@ +# Implementation Plan: Include Verdict in SJP Results + +**Branch**: `CIMD-3915-informant-register-local-schema` | **Date**: 2026-06-16 +**Spec**: [spec.md](spec.md) | **Research**: [research.md](research.md) | **Data model**: [data-model.md](data-model.md) | **Contracts**: [contracts/event-contracts.md](contracts/event-contracts.md) + +## Summary + +Decouple the informant-register command and event schemas from the shared core-domain by introducing local JSON schemas and hand-written POJO types. Replace the flat `verdictCode` string on the offence schema with a structured `verdict` object (`verdictCode`, `verdictDate`, `verdictType`). Introduce two new V2 domain events (`informant-register-recorded-v2`, `informant-register-generated-v2`) that carry the local schema. Update the `results.prosecutor-results` query response to include the structured verdict. All changes follow strict TDD (red→green→refactor) with contracts updated before Java (Constitution Principle I), and three-layer discipline across command / listener / processor (Principle II). + +--- + +## Technical Context + +**Language/Version**: Java 17 (`centos8-j17` CI demand; local `mvn17` alias) +**Primary Dependencies**: CPP `service-parent-pom:17.103.9`; JEE/CDI; JUnit 5 + Mockito; Jackson (ObjectMapper via framework producer) +**Storage**: `DS.eventstore` (event store); `DS.results` (viewstore — `InformantRegisterEntity` table, payload stored as JSON string) +**Testing**: JUnit 5 (`@ExtendWith(MockitoExtension.class)`); `mvn test` for unit; `./runIntegrationTests.sh` for ITs +**Target Platform**: WildFly WAR on Docker (Linux / `centos8-j17`) +**Project Type**: Event-sourced bounded context (CQRS) +**Performance Goals**: No new performance requirements — existing throughput targets unchanged +**Constraints**: No Lombok; no Spring; no wildcard imports; no `System.out`; hand-written POJOs only; contracts before Java + +--- + +## Constitution Check + +| Principle | Status | Notes | +|-----------|--------|-------| +| **I. RAML / JSON-Schema Contract First** | PASS | All schemas and subscription descriptors updated before any Java change (see implementation phases below) | +| **II. CQRS Three-Layer Discipline** | PASS | Command side + listener + processor all addressed; see three-layer impact table in research.md | +| **III. CPP Framework Idioms** | PASS | `@ServiceComponent`, `@Handles`, `Aggregate.apply`, hand-written POJOs — no Spring/Lombok/JMS/JDBC | +| **IV. Spec-Driven Build Loop** | PASS | Flowing through speckit; code-reviewer + qa + spec-validator agents will run after implementation | +| **V. HMCTS CPP Standards** | PASS | Java 17; Maven; WAR/WildFly; JUnit+Mockito; no wildcard imports | +| **VI. Schema-Subscription Symmetry** | PASS | Both new V2 events have matching schemas AND subscription entries before Java; namespace `http://justice.gov.uk/results/courts/...` confirmed | +| **VII. No System.out / System.err** | PASS | SLF4J only; enforced in all new production and test code | +| **VIII. TDD** | PASS | Explicit TDD sequence in every phase: failing test first, then production code | + +--- + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-informant-register-local-schema/ +├── spec.md +├── plan.md ← this file +├── research.md +├── data-model.md +├── contracts/ +│ └── event-contracts.md +├── checklists/ +│ └── requirements.md +└── tasks.md ← generated by /speckit-tasks +``` + +### Source Code — affected modules + +```text +results-domain/results-domain-common/ +├── src/main/java/uk/gov/moj/cpp/results/domain/informant/model/ +│ ├── Verdict.java [NEW] +│ ├── InformantRegisterOffence.java [NEW — replaces core-domain] +│ ├── InformantRegisterResult.java [NEW] +│ ├── InformantRegisterResultData.java [NEW] +│ ├── InformantRegisterCaseOrApplication.java [NEW] +│ ├── InformantRegisterDefendant.java [NEW] +│ ├── InformantRegisterHearing.java [NEW] +│ ├── InformantRegisterHearingVenue.java [NEW] +│ ├── InformantRegisterRecipient.java [NEW] +│ ├── InformantRegisterDocumentRequest.java [NEW] +│ └── ProsecutorResult.java [NEW] +├── src/main/java/uk/gov/moj/cpp/results/domain/event/ +│ ├── InformantRegisterRecordedV2.java [NEW] +│ └── InformantRegisterGeneratedV2.java [NEW] +└── src/main/resources/json/schema/ + ├── informantRegisterDocument/ [NEW directory] + │ ├── verdict.json + │ ├── informantRegisterOffence.json + │ ├── informantRegisterResult.json + │ ├── informantRegisterResultData.json + │ ├── informantRegisterCaseOrApplication.json + │ ├── informantRegisterDefendant.json + │ ├── informantRegisterHearing.json + │ ├── informantRegisterHearingVenue.json + │ ├── informantRegisterRecipient.json + │ ├── informantRegisterDocumentRequest.json + │ └── prosecutorResult.json + ├── results.event.informant-register-recorded-v2.json [NEW] + └── results.event.informant-register-generated-v2.json [NEW] + +results-command/results-command-api/ +└── src/raml/json/schema/ + └── results.add-informant-register.json [MODIFIED — local $ref] + +results-event-sources/ +└── src/yaml/event-sources.yaml [check — no new topics needed] + +results-event/results-event-listener/ +├── src/yaml/subscriptions-descriptor.yaml [MODIFIED — add V2 entries] +└── src/main/java/uk/gov/moj/cpp/results/event/ + └── InformantRegisterEventListener.java [MODIFIED — add V2 handlers] + +results-event/results-event-processor/ +├── src/yaml/subscriptions-descriptor.yaml [MODIFIED — add V2 entry] +├── src/main/java/uk/gov/moj/cpp/results/event/processor/ +│ └── InformantRegisterEventProcessor.java [MODIFIED — add V2 handler] +└── src/main/java/uk/gov/moj/cpp/results/event/processor/model/ + └── InformantRegisterDocument.java [MODIFIED — verdict extraction] + +results-domain/results-domain-aggregate/ +└── src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ + └── ProsecutionAuthorityAggregate.java [MODIFIED — local types] + +results-command/results-command-handler/ +└── src/main/java/uk/gov/moj/cpp/results/command/handler/ + └── InformantRegisterHandler.java [MODIFIED — emit V2 events] + +results-query/results-query-api/ +└── src/raml/json/schema/ + └── results.prosecutor-results.json [MODIFIED — local $ref] + +results-query/results-query-view/ +└── src/main/java/uk/gov/moj/cpp/results/query/view/ + └── ProsecutorResultsQueryView.java [MODIFIED — local types] +``` + +--- + +## Implementation Phases + +> **TDD Rule (Principle VIII)**: In every phase below, the failing test is written and confirmed to fail for the right reason (assertion failure, not compilation error) BEFORE the production code is written. + +--- + +### Phase 0 — Contracts and Schemas First (Principle I) + +No Java. No tests. Pure contract artefacts. + +**0.1** Create `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/` directory and write all 11 local sub-schemas (see data-model.md for full list). Start with `verdict.json` as it has no dependencies, then build up in dependency order. + +**0.2** Write `results.event.informant-register-recorded-v2.json` and `results.event.informant-register-generated-v2.json` under `results-domain-common/src/main/resources/json/schema/` referencing the local sub-schemas. + +**0.3** Update `results.add-informant-register.json` (command-api) to replace the core-domain `$ref` with the local `informantRegisterDocumentRequest.json` reference. + +**0.4** Write local `prosecutorResult.json` sub-schema and update `results.prosecutor-results.json` (query-api) to reference it. + +**0.5** Add V2 subscription entries to `results-event-listener/src/yaml/subscriptions-descriptor.yaml` (both `recorded-v2` and `generated-v2`). + +**0.6** Add V2 subscription entry to `results-event-processor/src/yaml/subscriptions-descriptor.yaml` (`generated-v2` only). + +**Gate**: `mvn clean install -DskipTests` on affected modules must succeed before proceeding to Phase 1. + +--- + +### Phase 1 — Local POJO Types (TDD) + +**Order**: write the deepest types first (no dependencies), build up. + +**1.1 `Verdict` POJO** +- Write failing test in `results-domain-common`: `VerdictTest` — assert builder creates instance with all three fields, assert null fields are tolerated. +- Confirm test fails (compilation — class missing). Write `Verdict.java`. Test goes green. + +**1.2 `InformantRegisterOffence` POJO** +- Write failing test: `InformantRegisterOffenceTest` — assert builder sets `verdict`, assert `verdictCode` field is absent (compilation forces migration). +- Write `InformantRegisterOffence.java`. Test goes green. + +**1.3 Remaining POJOs** (`InformantRegisterResult`, `InformantRegisterResultData`, `InformantRegisterCaseOrApplication`, `InformantRegisterDefendant`, `InformantRegisterHearing`, `InformantRegisterHearingVenue`, `InformantRegisterRecipient`, `InformantRegisterDocumentRequest`, `ProsecutorResult`) +- Each gets a minimal builder/getter test before the production POJO is written. + +**1.4 V2 event POJOs** (`InformantRegisterRecordedV2`, `InformantRegisterGeneratedV2`) +- Write failing tests asserting correct event name constants and builder fields. +- Write production POJOs. + +**Gate**: `mvn -pl results-domain/results-domain-common test` green. + +--- + +### Phase 2 — Command Handler (TDD) + +**2.1 Gap-fill existing tests** — Review `InformantRegisterHandlerTest`. Add any missing scenarios (e.g., no test for `handleAddInformantRegisterToEventStream` emitting event with offence verdict data). Add failing tests first. + +**2.2 V2 emit test** — Add failing test to `InformantRegisterHandlerTest`: +- `handleAddInformantRegisterToEventStream` with offence carrying a `verdict` object → assert emitted event name is `results.event.informant-register-recorded-v2` and payload carries `verdict.verdictCode`. +- Confirm test fails (V2 event not yet emitted). + +**2.3** Update `InformantRegisterHandler.handleAddInformantRegisterToEventStream` to accept local `InformantRegisterDocumentRequest` and emit `InformantRegisterRecordedV2`. Keep V1 emit for backward compat (dual-emit or replace — confirm with team; default to V2-only if this is a greenfield migration). + +**2.4 Generate V2 test** — Add failing test: `processRequests` emits `results.event.informant-register-generated-v2`. + +**2.5** Update `processRequests` to emit `InformantRegisterGeneratedV2`. + +**Gate**: `mvn -pl results-command/results-command-handler test` green. + +--- + +### Phase 3 — Aggregate (TDD) + +**3.1 Gap-fill** — Review `ProsecutionAuthorityAggregateTest`. Add missing scenarios for `apply(InformantRegisterGeneratedV2)`. + +**3.2 V2 apply test** — Add failing test: `ProsecutionAuthorityAggregateTest` — `apply(InformantRegisterGeneratedV2)` sets `informantRegisterRecipients` from local `InformantRegisterDocumentRequest`. + +**3.3** Update `ProsecutionAuthorityAggregate.apply` to handle `InformantRegisterGeneratedV2` using local types. + +**Gate**: `mvn -pl results-domain/results-domain-aggregate test` green. + +--- + +### Phase 4 — Event Listener (TDD) + +**4.1 Gap-fill** — Review `InformantRegisterEventListenerTest`. Add missing scenarios where applicable. + +**4.2 V2 recorded handler test** — Add failing test: `saveInformantRegisterV2` handles `results.event.informant-register-recorded-v2` envelope, deserializes local `InformantRegisterDocumentRequest`, saves entity with same fields as V1. + +**4.3** Add `saveInformantRegisterV2` handler to `InformantRegisterEventListener` with `@Handles("results.event.informant-register-recorded-v2")`. + +**4.4 V2 generated handler test** — Add failing test: `generateInformantRegisterV2` handles `results.event.informant-register-generated-v2`, updates entity status to GENERATED. + +**4.5** Add `generateInformantRegisterV2` handler. + +**Gate**: `mvn -pl results-event/results-event-listener test` green. + +--- + +### Phase 5 — Event Processor (TDD) + +**5.1 Gap-fill** — Review `InformantRegisterEventProcessorTest`. Identify missing coverage for `buildOffenceDetails` (currently no assertion on `verdictCode` in the CSV output). + +**5.2 Verdict extraction test** — Add failing test: `generateInformantRegister` with offence carrying `verdict.verdictCode: "G"` → assert CSV row contains `verdictCode = "G"`. + +**5.3** Update `buildOffenceDetails` in `InformantRegisterEventProcessor`: replace `offence.getVerdictCode()` with null-safe `offence.getVerdict() != null ? offence.getVerdict().getVerdictCode() : null`. + +**5.4 V2 handler test** — Add failing test: `generateInformantRegisterV2` handles `results.event.informant-register-generated-v2` envelope and produces the same CSV output. + +**5.5** Add `generateInformantRegisterV2` handler to `InformantRegisterEventProcessor` with `@Handles("results.event.informant-register-generated-v2")`. + +**Gate**: `mvn -pl results-event/results-event-processor test` green. + +--- + +### Phase 6 — Query View (TDD) + +**6.1 Gap-fill** — Review `ProsecutorResultsQueryViewTest`. Identify missing scenario: no test asserting verdict data in response. + +**6.2 Verdict in response test** — Add failing test to `ProsecutorResultsQueryViewTest`: +- Store `InformantRegisterEntity` with a payload JSON that includes `verdict.verdictCode: "G"` on an offence. +- Call `getProsecutorResults`. +- Assert response JSON contains `hearingVenues[0].courtSessions[0].defendants[0].prosecutionCasesOrApplications[0].offences[0].verdict.verdictCode = "G"`. +- Confirm test fails (current code uses core-domain `InformantRegisterDocumentRequest` which has no `verdict` field). + +**6.3** Update `ProsecutorResultsQueryView` to use local `InformantRegisterDocumentRequest` and local `ProsecutorResult`. + +**6.4 Absent verdict test** — Add failing test: offence with no `verdict` in stored JSON → response offence has no `verdict` field (not null, absent). + +**6.5 NO_VERDICT omit test** — Add failing test: stored offence with `"verdict": null` → response omits `verdict` field. + +**Gate**: `mvn -pl results-query/results-query-view test` green. + +--- + +### Phase 7 — Backward Compatibility (TDD) + +**7.1 Pre-migration recorded replay test** — Add failing test to `InformantRegisterEventListenerTest`: +- Build a V1 `informant-register-recorded` JSON envelope with a flat `verdictCode` string field on an offence (old schema). +- Send to the V1 `saveInformantRegister` handler. +- Assert: no exception thrown; entity saved with correct `prosecutionAuthorityId` and `registerDate`. + +**7.2** Confirm this test passes without additional production code changes (V1 handler still uses core-domain types — old schema still valid). + +**7.3 Pre-migration generated replay test** — Same pattern for `informant-register-generated` V1 with flat `verdictCode`. + +**Gate**: `mvn test` on all affected modules green. + +--- + +### Phase 8 — Full Build Verification + +```bash +mvn clean install +``` + +All unit tests green. Zero compilation errors. Zero schema-subscription drift. + +--- + +## Complexity Tracking + +No constitution violations. All principles satisfied. + +--- + +## Risks + +| Risk | Likelihood | Mitigation | +|------|-----------|------------| +| Jackson deserialization of local POJOs from stored JSON fails silently | Medium | Explicit unit tests for deserialization of payload JSON strings to local types (Phase 6.2) | +| Namespace mismatch between schema `$id` and subscription `schema_uri` | Low | Phase 0 contract review; spec-validator agent run after implementation | +| Missing `@JsonCreator` / `@JsonProperty` on local POJO constructors | Medium | Phase 1 POJO tests include deserialization roundtrip assertions | +| Dual-emit in handler causes out-of-order replay issues | Low | V1 and V2 events are independent; listeners handle each separately | diff --git a/specs/001-informant-register-local-schema/quickstart.md b/specs/001-informant-register-local-schema/quickstart.md new file mode 100644 index 000000000..a1a3c9122 --- /dev/null +++ b/specs/001-informant-register-local-schema/quickstart.md @@ -0,0 +1,71 @@ +# Quickstart: CIMD-3915 — Include Verdict in SJP Results + +**Branch**: `CIMD-3915-informant-register-local-schema` +**Plan**: [plan.md](plan.md) | **Spec**: [spec.md](spec.md) + +## What this change does + +Replaces the core-domain dependency for informant-register schemas with locally-owned schemas and POJOs. Adds a structured `verdict` object (`verdictCode`, `verdictDate`, `verdictType`) to offences, replacing the flat `verdictCode` string. Exposes verdict data through the `results.prosecutor-results` query API. + +## Key commands + +```bash +# Run unit tests on a single module +mvn -pl results-domain/results-domain-common test +mvn -pl results-domain/results-domain-aggregate test +mvn -pl results-command/results-command-handler test +mvn -pl results-event/results-event-listener test +mvn -pl results-event/results-event-processor test +mvn -pl results-query/results-query-view test + +# Full build with unit tests +mvn clean install + +# Full build, skip tests (for iterating on compile errors) +mvn clean install -DskipTests +``` + +## Implementation order (TDD) + +Follow the phases in [plan.md](plan.md) strictly: + +1. **Phase 0** — Write all JSON schemas and update subscription descriptors first (no Java) +2. **Phase 1** — Write local POJO tests, then POJOs (deepest types first: `Verdict` → `InformantRegisterOffence` → …) +3. **Phase 2** — Command handler: gap-fill tests, then V2 emit tests, then production handler changes +4. **Phase 3** — Aggregate: gap-fill tests, then V2 apply test, then production aggregate change +5. **Phase 4** — Event listener: gap-fill tests, then V2 handler tests, then production handlers +6. **Phase 5** — Event processor: gap-fill tests, then verdict extraction test, then V2 handler test, then production changes +7. **Phase 6** — Query view: gap-fill tests, then verdict-in-response test, then production view change +8. **Phase 7** — Backward compat tests (V1 replay) +9. **Phase 8** — `mvn clean install` (full build gate) + +## New local packages + +| Package | Purpose | +|---------|---------| +| `uk.gov.moj.cpp.results.domain.informant.model` | 11 hand-written POJO types (in `results-domain-common`) | +| `uk.gov.moj.cpp.results.domain.event` | `InformantRegisterRecordedV2`, `InformantRegisterGeneratedV2` | + +## New JSON schemas + +All under `results-domain/results-domain-common/src/main/resources/json/schema/`: + +- `informantRegisterDocument/verdict.json` — new +- `informantRegisterDocument/informantRegisterOffence.json` — local copy with `verdict` object +- 9 other sub-schemas — local copies of core-domain sub-schemas +- `results.event.informant-register-recorded-v2.json` — new V2 event schema +- `results.event.informant-register-generated-v2.json` — new V2 event schema + +## What is NOT changing + +- V1 event handlers (`informant-register-recorded`, `informant-register-generated`) — kept for backward-compat replay +- Viewstore schema — no DDL change; verdict lives in the existing `payload` JSON column +- `informant-register-notified` / `informant-register-notified-v2` — out of scope (carry no offence data) +- CSV format — `verdictCode` string is still present; now sourced from `verdict.verdictCode` + +## Constitution gates (must not skip) + +- Contracts before Java (Principle I) — Phase 0 must be complete before Phase 1 +- Three-layer discipline (Principle II) — all three layers must be tested and updated +- TDD (Principle VIII) — failing test before every production code change +- Schema-subscription symmetry (Principle VI) — every new event has both a schema file AND a subscription entry diff --git a/specs/001-informant-register-local-schema/research.md b/specs/001-informant-register-local-schema/research.md new file mode 100644 index 000000000..7385c8d17 --- /dev/null +++ b/specs/001-informant-register-local-schema/research.md @@ -0,0 +1,107 @@ +# Research: Include Verdict in SJP Results — results context + +**Date**: 2026-06-16 | **Branch**: `CIMD-3915-informant-register-local-schema` + +## Decision 1: Event Versioning Strategy + +**Decision**: Introduce new V2 domain events (`results.event.informant-register-recorded-v2`, `results.event.informant-register-generated-v2`) that carry the new local schema, while keeping existing V1 event handlers for backward-compatible replay of historical events. + +**Rationale**: The existing event store contains pre-migration events carrying the old schema (core-domain references, flat `verdictCode`). These must remain replayable without errors (FR-012, SC-005). The project already uses this pattern — `InformantRegisterNotifiedV2` was introduced alongside `InformantRegisterNotified` — confirming it is the team's established approach. + +**Alternatives considered**: +- In-place schema mutation (no versioning) — rejected: would break replay of historical events already in the event store. +- Migration job to rewrite historic events — rejected: out of scope; event sourcing requires immutable event history. + +--- + +## Decision 2: Local POJO Strategy + +**Decision**: Hand-write local POJO types in `results-domain/results-domain-common` under the package `uk.gov.moj.cpp.results.domain.informant.model` to replace the core-domain generated classes used in the informant-register flows. Each POJO uses explicit constructors, getters, and a nested `Builder` class (no Lombok, no Jackson codegen). + +**Rationale**: The core-domain generated classes are produced by a codegen step that cannot be controlled from this context. Owning local POJOs is the only way to evolve the schema (e.g., replace `verdictCode` with `verdict` object) independently. The `results-domain-common` module already hosts shared domain helpers (`InformantRegisterHelper`) making it the correct home. + +**Alternatives considered**: +- Keep using core-domain types and add a `verdict` enrichment adapter — rejected: still requires core-domain codegen to expose the new field, defeating the decoupling goal. +- Generate POJOs from local JSON schemas using the CPP codegen plugin — rejected: adds build complexity; constitution Principle III confirms hand-written POJOs are standard where codegen is not already wired. + +**Types needed** (all in `uk.gov.moj.cpp.results.domain.informant.model`): + +| Local type | Replaces core-domain type | Key change | +|------------|--------------------------|------------| +| `InformantRegisterDocumentRequest` | `uk.gov.justice.core.courts.informantRegisterDocument.InformantRegisterDocumentRequest` | Local refs | +| `InformantRegisterHearingVenue` | `...InformantRegisterHearingVenue` | Local refs | +| `InformantRegisterHearing` | `...InformantRegisterHearing` | Local refs | +| `InformantRegisterDefendant` | `...InformantRegisterDefendant` | Local refs | +| `InformantRegisterCaseOrApplication` | `...InformantRegisterCaseOrApplication` | Local refs | +| `InformantRegisterOffence` | `...InformantRegisterOffence` | `verdict: Verdict` replaces `verdictCode: String` | +| `Verdict` | *(new)* | `verdictCode`, `verdictDate`, `verdictType` | +| `InformantRegisterResult` | `...InformantRegisterResult` | Local refs | +| `InformantRegisterResultData` | `...InformantRegisterResultData` | No change | +| `InformantRegisterRecipient` | `...InformantRegisterRecipient` | No change | +| `ProsecutorResult` | `...ProsecutorResult` | Uses local `InformantRegisterHearingVenue` | + +--- + +## Decision 3: Local JSON Schema Layout + +**Decision**: Place all local informant-register sub-schemas under `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/` with namespace `http://justice.gov.uk/results/courts/informantRegisterDocument/...` (matching the sibling V2 event namespace pattern). Command schema and query schema keep their existing file paths but replace the `$ref` with a local namespace ref. + +**Rationale**: The existing V2 event schemas use the `http://justice.gov.uk/results/courts/...` namespace (confirmed from `informant-register-notified-v2.json`). New local sub-schemas should use `http://justice.gov.uk/results/courts/informantRegisterDocument/...` for consistency. + +**Namespace mapping**: + +| Schema file | Old `$id` / `$ref` | New `$id` | +|-------------|---------------------|-----------| +| `informantRegisterOffence.json` (local) | `http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterOffence.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterOffence.json` | +| `informantRegisterDocumentRequest.json` (local) | `http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterDocumentRequest.json` | `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json` | +| `informant-register-recorded-v2.json` | *(new)* | `http://justice.gov.uk/results/courts/informant-register-recorded-v2.json` | +| `informant-register-generated-v2.json` | *(new)* | `http://justice.gov.uk/results/courts/informant-register-generated-v2.json` | +| `results.add-informant-register.json` (command) | `$ref` to core-domain | Self-contained with local refs | +| `results.prosecutor-results.json` (query) | `$ref` to core-domain `prosecutorResult.json` | Local inline with `verdict` object | + +--- + +## Decision 4: Command Handler Emit Strategy + +**Decision**: `handleAddInformantRegisterToEventStream` will emit the existing `InformantRegisterRecorded` event (V1, kept for backward compat with any downstream that still replays V1 events) AND a new `InformantRegisterRecordedV2` event carrying the local type. The listener adds a V2 handler alongside the existing V1 handler. + +**Rationale**: Dual-emit ensures both old replays (via V1) and new downstream (via V2) work correctly. The `processRequests` method similarly emits `InformantRegisterGeneratedV2` for the generate commands. + +**Alternatives considered**: +- Emit only V2 — rejected: breaks replay of the listener against historical V1 events (listener subscribed to V1 would miss new events; V2-only listener would miss historical events). +- Emit only V1 with an updated payload schema — rejected: backward-incompatible schema change on an existing event is a runtime risk. + +--- + +## Decision 5: ProsecutorResultsQueryView Migration + +**Decision**: `ProsecutorResultsQueryView` will be updated to use local `InformantRegisterDocumentRequest` and local `ProsecutorResult` types. The payload stored in `InformantRegisterEntity.payload` (as a JSON string) will be deserialized to the local `InformantRegisterDocumentRequest`, which includes the `verdict` object on offences. No viewstore schema change is required. + +**Rationale**: The payload is stored as a raw JSON string in the `InformantRegisterEntity`. As long as newly ingested records contain the `verdict` object in the JSON, the query view will naturally return it. Pre-migration records (without `verdict`) will simply have no `verdict` field in the response — consistent with FR-003 (omit when absent). + +**Alternatives considered**: +- Add a separate `verdictCode` column to the viewstore entity — rejected: the payload is stored as a JSON blob; extracting a field to a column adds Liquibase migration work and is unnecessary for read-only query purposes. + +--- + +## Decision 6: InformantRegisterDocument CSV Model + +**Decision**: The CSV `InformantRegisterDocument` model's `verdictCode` field is retained but now populated from `offence.getVerdict().getVerdictCode()` (null-safe, returning empty string if no verdict). The CSV format does not change. + +**Rationale**: The CSV format is consumed by downstream file recipients and is out of scope for the verdict object change. The CSV only needs the `verdictCode` string, which is still available as `verdict.verdictCode`. + +--- + +## Scope Confirmation — Three-Layer Impact + +| Layer | Events affected | Action | +|-------|----------------|--------| +| Command side | `informant-register-recorded-v2` (new), `informant-register-generated-v2` (new) | Handler emits V2; aggregate uses local types | +| Event listener | `informant-register-recorded-v2` (new), `informant-register-generated-v2` (new); V1 handlers kept | Add V2 handler methods; keep V1 for backward compat | +| Event processor | `informant-register-generated-v2` (new); existing `informant-register-generated` V1 kept for CSV | Add V2 handler; update `buildOffenceDetails` to read `verdict` object | +| Query | `results.prosecutor-results` | Schema + view updated to include `verdict` object | + +**Not affected** (explicitly out of scope): +- `informant-register-notified`, `informant-register-notified-v2`, `informant-register-notification-ignored` — carry no offence data +- Financial results aggregates, results aggregate, defendant aggregate +- All non-informant-register event flows diff --git a/specs/001-informant-register-local-schema/spec.md b/specs/001-informant-register-local-schema/spec.md new file mode 100644 index 000000000..2acd61a37 --- /dev/null +++ b/specs/001-informant-register-local-schema/spec.md @@ -0,0 +1,156 @@ +# Feature Specification: Include Verdict in SJP Results — results context changes + +**Feature Branch**: `CIMD-3915-informant-register-local-schema` +**Created**: 2026-06-16 +**Status**: Draft +**Jira**: CIMD-3915 +**Sprint**: 62 +**Scope**: `cpp-context-results` only + +## Overview + +The results context must include structured verdict data in the `results.prosecutor-results` query response so that downstream consumers can accurately report the legal outcome for each SJP offence. Two enabling changes are required: + +1. Decouple the informant register offence schema from the shared core-domain — own it locally so it can evolve independently. +2. Replace the flat `verdictCode` string on the offence with a structured `verdict` object (`verdictCode`, `verdictDate`, `verdictType`). + +The delivery surface is the `results.prosecutor-results` query. The command side (`add-informant-register`) is the ingest path that must accept and store the enriched offence data. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Add Informant Register Accepts and Stores Enriched Verdict Data (Priority: P1) + +The `results.add-informant-register` command accepts an informant register document request containing offences with the structured `verdict` object and records it using a locally-owned schema. + +**Why this priority**: This is the ingest entry point. Downstream generation and query flows depend on correctly stored data. + +**Independent Test**: Submit `results.add-informant-register` with an offence carrying a `verdict` object; confirm the recorded domain event stores all three verdict fields. Confirm a request with the old flat `verdictCode` field fails validation against the new schema. + +**Acceptance Scenarios**: + +1. **Given** a valid `add-informant-register` command where an offence carries a `verdict` object with `verdictCode`, `verdictDate`, and `verdictType`, **When** the command is received, **Then** the domain event is stored with all three verdict fields present on the offence. + +2. **Given** a valid `add-informant-register` command where an offence carries no `verdict` object, **When** the command is received, **Then** the event is stored without verdict data (verdict is optional on an offence). + +3. **Given** an `add-informant-register` command where an offence carries a legacy flat `verdictCode` string (old schema), **When** the command is received, **Then** the request is rejected with a validation error. + +--- + +### User Story 2 - Generate Informant Register Produces Enriched Generation Event (Priority: P2) + +The `results.generate-informant-register` and `results.generate-informant-register-by-date` commands produce a generation event that carries informant register document requests using the locally-owned schema, including the structured verdict object on offences. + +**Why this priority**: Generation reads from stored records and publishes events consumed downstream; it must propagate the enriched data correctly. + +**Independent Test**: Trigger `results.generate-informant-register` against stored records with verdict data; confirm the generated domain event contains offences with the `verdict` object intact. + +**Acceptance Scenarios**: + +1. **Given** stored informant register records include offences with a `verdict` object, **When** `results.generate-informant-register` is triggered, **Then** the `informant-register-generated` domain event carries those offences with the structured verdict data. + +2. **Given** stored informant register records include offences with no `verdict` object, **When** `results.generate-informant-register-by-date` is triggered, **Then** the `informant-register-generated` domain event carries those offences without a verdict field. + +3. **Given** no informant register records exist in `RECORDED` status, **When** `results.generate-informant-register` is triggered, **Then** no events are emitted and the command completes silently. + +--- + +### User Story 3 - Prosecutor Results Query Returns Structured Verdict on Offences (Priority: P1) + +The `results.prosecutor-results` query response includes a `verdict` object on each offence where a verdict was recorded, using the verdict type mapping from CIMD-3915. + +**Why this priority**: This is the primary delivery surface for this ticket within the results context. + +**Independent Test**: Query `results.prosecutor-results` for a prosecution authority with stored offences covering FOUND_GUILTY, FOUND_NOT_GUILTY, PROVED_SJP, NO_VERDICT, and null verdict; confirm each offence is returned with the correct verdict shape per the mapping table. + +**Acceptance Scenarios**: + +1. **Given** an offence has verdict FOUND_GUILTY, **When** `results.prosecutor-results` is queried, **Then** the offence includes `verdict` with `verdictCode: "G"` (mandatory), `verdictDate` in `yyyy-MM-dd` (mandatory when verdictCode present, omitted if conviction date is null), and optionally `verdictType: "FOUND_GUILTY"`. + +2. **Given** an offence has verdict FOUND_NOT_GUILTY (decision type DISMISS), **When** `results.prosecutor-results` is queried, **Then** the offence includes `verdict` with `verdictCode: "N"`, `verdictDate`, and optionally `verdictType: "FOUND_NOT_GUILTY"`. + +3. **Given** an offence has verdict PROVED_SJP (proved in absence, no plea), **When** `results.prosecutor-results` is queried, **Then** the offence includes `verdict` with `verdictCode: "PSJ"`, `verdictDate`, and optionally `verdictType: "PROVED_SJP"`. + +4. **Given** an offence has verdict NO_VERDICT or null verdict, **When** `results.prosecutor-results` is queried, **Then** the offence does NOT contain a `verdict` field (omitted entirely — not null). + +5. **Given** a case has multiple offences where some have verdicts and some do not, **When** `results.prosecutor-results` is queried, **Then** each offence is assessed independently — no verdict is mixed or defaulted from one offence to another. + +--- + +### User Story 4 - Pre-migration Events Remain Replayable (Priority: P2) + +Existing domain events stored before this change (carrying the old flat `verdictCode` string) can still be replayed against the aggregate and processed by the event listener without causing runtime errors. + +**Why this priority**: Event sourcing requires backward-compatible replay. Failure here silently breaks historical reconstitution. + +**Independent Test**: Replay a pre-migration `informant-register-recorded` event (with flat `verdictCode`) through the aggregate and event listener; confirm no exception is thrown and the state is consistent. + +**Acceptance Scenarios**: + +1. **Given** a pre-migration `informant-register-recorded` event exists in the event store with a flat `verdictCode` field on an offence, **When** the event is replayed, **Then** no runtime exception is thrown and the aggregate state is consistent. + +2. **Given** a pre-migration `informant-register-generated` event exists with flat `verdictCode` offences, **When** the event is replayed by the event listener, **Then** no runtime exception is thrown. + +--- + +### Edge Cases + +- `verdictCode` and `verdictDate` are co-dependent: if one is present the other must be present; if one is absent both are absent. +- `verdictType` is optional — it is populated from reference data; if the lookup returns no match, `verdictType` is omitted but the rest of the verdict object (`verdictCode`, `verdictDate`) is still returned. +- An offence with an entirely absent `verdict` object is valid — verdict is optional on an offence. +- A `verdict` object with none of its fields set is equivalent to no verdict — the field should be omitted rather than serialised as `{}`. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The `results.add-informant-register` command payload schema MUST be owned locally within the results context with no runtime `$ref` to any core-domain schema. +- **FR-002**: The `results.generate-informant-register` and `results.generate-informant-register-by-date` command handling MUST use the locally-owned informant register document types. +- **FR-003**: The local informant register offence schema MUST replace the flat `verdictCode` string field with an optional `verdict` object containing: `verdictCode` (string), `verdictDate` (string, `yyyy-MM-dd`), and `verdictType` (string) — all fields optional within the object. +- **FR-004**: `verdictCode` and `verdictDate` MUST be co-reported: both present or both absent. +- **FR-005**: New versioned domain events MUST be introduced for the add-informant-register and generate-informant-register flows that carry offences in the new local schema shape, coexisting with existing events in the event store. +- **FR-006**: The `ProsecutionAuthorityAggregate` MUST be updated to use locally-owned informant register document types. +- **FR-007**: The `results.prosecutor-results` query response MUST include the `verdict` object on offences per the verdict type mapping (FR-003), and MUST omit the field entirely (not null) when verdict is NO_VERDICT or null. +- **FR-008**: When verdict reference data is unavailable for an offence, the system MUST omit `verdictType` but still include `verdictCode` and `verdictDate` where available; the offence MUST still be returned and a warning logged. +- **FR-009**: Each offence MUST be assessed independently; verdict from one offence MUST NOT influence another. +- **FR-010**: All new local schemas MUST use the correct results-context namespace matching sibling schemas in the same descriptor. +- **FR-011**: Pre-migration events already stored in the event store MUST remain replayable without runtime errors — verified by unit test. + +### Key Entities + +- **InformantRegisterOffence (local)**: `offenceCode` (required), `offenceTitle` (required), `orderIndex` (required), `originatingCaseUrn` (optional), `pleaValue` (optional), `verdict` object (optional), `offenceResults` (optional). +- **Verdict**: `verdictCode` (string — "G", "N", "PSJ"), `verdictDate` (string — `yyyy-MM-dd`), `verdictType` (string — "FOUND_GUILTY", "FOUND_NOT_GUILTY", "PROVED_SJP"). All optional; `verdictCode` and `verdictDate` are co-dependent. +- **InformantRegisterDocumentRequest (local)**: Full document — hearing venue, defendants, cases/applications, offences. No external schema references. + +### Verdict Type Mapping + +| Internal verdict type | Verdict code | `verdictType` in payload | Verdict field in payload | +|-----------------------|--------------|--------------------------|--------------------------| +| FOUND_GUILTY | G | FOUND_GUILTY (optional) | Included | +| FOUND_NOT_GUILTY | N | FOUND_NOT_GUILTY (opt.) | Included | +| PROVED_SJP | PSJ | PROVED_SJP (optional) | Included | +| NO_VERDICT | (absent) | (absent) | Omitted entirely | +| null (not set) | (absent) | (absent) | Omitted entirely | + +## Success Criteria *(mandatory)* + +- **SC-001**: All unit tests for the command handler, aggregate, event listener, event processor, and query view pass with no failures. +- **SC-002**: `mvn clean install` on all affected modules completes with zero compilation errors and zero test failures. +- **SC-003**: Zero `$ref` references to `http://justice.gov.uk/core/courts/informantRegisterDocument/` remain in any command API schema or domain event schema for the informant-register flows. +- **SC-004**: The `results.prosecutor-results` query response schema validates against a payload containing the structured `verdict` object on an offence. +- **SC-005**: A unit test confirms replaying a pre-migration event (with flat `verdictCode`) does not throw an exception. + +## Development Constraints + +- **TDD is mandatory (Constitution Principle VIII)**: Every production code change MUST be preceded by a failing unit test that fails for the correct reason (assertion failure, not a compilation error). The commit history must show failing tests authored at or before the corresponding production code. This applies to: new/updated command handler methods, aggregate command and apply methods, event listener converters, event processor converters, query view methods, and all new local POJO types. +- **Missing tests first**: Before any schema migration or production code change is made, any gap in existing test coverage for the affected classes must be filled with failing tests. Existing tests for `InformantRegisterHandlerTest`, `ProsecutionAuthorityAggregateTest`, `InformantRegisterEventListenerTest`, `InformantRegisterEventProcessorTest`, and `ProsecutorResultsQueryViewTest` must be reviewed and supplemented where coverage is absent. +- **Three-layer discipline**: Every change touching a domain event must be assessed across all three layers — command side, event listener, event processor — with tests at each layer before production code. + +## Assumptions + +- `results.generate-informant-register-by-date.json` command schema is already self-contained (no core-domain reference) — no schema migration needed for that file. +- `InformantRegisterNotified`, `InformantRegisterNotifiedV2`, and `InformantRegisterNotificationIgnored` events are out of scope — they carry no offence data. +- Local Java types are hand-written POJOs (builder pattern, no Lombok) replacing the core-domain generated classes for the informant-register flows. +- The integration test (`InformantRegisterDocumentRequestIT`) is deferred — it will need updating separately after the unit-test-driven implementation is complete. +- `verdictType` is populated via a reference data lookup; if unavailable, it is omitted from the verdict object without failing the response. +- The plea value reporting issue noted in the Jira comments is out of scope for this ticket. +- **Dual-emit decision**: The command handler emits **V2 only** for all new ingest (both `add-informant-register` and `processRequests`). V1 events already persisted in the event store are handled by the existing V1 listener/processor handlers and are not re-emitted. V1 handlers are retained for backward-compatible replay (US4) but receive no new writes after this change. diff --git a/specs/001-informant-register-local-schema/tasks.md b/specs/001-informant-register-local-schema/tasks.md new file mode 100644 index 000000000..a4b0bed9e --- /dev/null +++ b/specs/001-informant-register-local-schema/tasks.md @@ -0,0 +1,265 @@ +# Tasks: CIMD-3915 — Include Verdict in SJP Results + +**Input**: `specs/001-informant-register-local-schema/` +**Prerequisites**: plan.md ✓, spec.md ✓, research.md ✓, data-model.md ✓, contracts/ ✓ + +**TDD discipline (Constitution Principle VIII)**: Every production code task MUST be preceded by its paired failing-test task. Write the test, confirm it fails for the right reason (assertion, not compilation), then write the production code. + +> **Phase mapping — plan.md vs tasks.md**: +> | tasks.md phase | plan.md phase | Scope | +> |---------------|---------------|-------| +> | Phase 1 (Contracts) | Phase 0 | JSON schemas + subscription descriptors | +> | Phase 2 (POJOs) | Phase 1 | Local Java types | +> | Phase 3 (US1) | Phases 2 + 3 | Command handler + aggregate | +> | Phase 4 (US3) | Phase 6 | Query view | +> | Phase 5 (US2) | Phases 4 + 5 | Event listener + processor | +> | Phase 6 (US4) | Phase 7 | Pre-migration backward compat | +> | Phase 7 (Polish) | Phase 8 | Full build + hygiene | + +## Format: `[ID] [P?] [Story?] Description` + +- **[P]**: Can run in parallel (different files, no shared dependencies) +- **[Story]**: User story — US1/US2/US3/US4 (from spec.md) +- Exact file paths included in all descriptions + +--- + +## Phase 1: Foundational — Contracts & Schemas (No Java) + +**Purpose**: All JSON schemas and subscription descriptor entries MUST exist before any Java is written (Constitution Principle I: Contract First). + +**⚠️ CRITICAL**: No Java task (Phase 2+) may start until T018 (build gate) passes. + +- [X] T001 Create directory `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/` and write `verdict.json` with `$id: http://justice.gov.uk/results/courts/informantRegisterDocument/verdict.json` — properties: `verdictCode`, `verdictDate`, `verdictType` (all optional strings) +- [X] T002 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterOffence.json` — replaces flat `verdictCode` with `"verdict": { "$ref": "...verdict.json" }`; required: `offenceCode`, `orderIndex`, `offenceTitle` +- [X] T003 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResult.json` — properties: `resultText` (required), `cjsResultCode`, `resultData` ($ref to resultData) +- [X] T004 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterResultData.json` — properties: `amount`, `nextHearingDate`, `nextCourtLocation`, `durationValue`, `durationUnit`, `durationStartDate`, `durationEndDate`, `secondaryDurationValue`, `secondaryDurationUnit` (all optional strings) +- [X] T005 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterCaseOrApplication.json` — required: `caseOrApplicationReference`; offences array $ref to offence schema +- [X] T006 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDefendant.json` — required: `name`, `address1`; optional: address2-5, postCode, dateOfBirth, nationality, firstName, lastName; prosecutionCasesOrApplications array +- [X] T007 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearing.json` — required: `courtRoom`, `hearingStartTime`, `defendants` (array, minItems 1) +- [X] T008 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterHearingVenue.json` — required: `courtHouse`, `courtSessions` (array, minItems 1); optional: `ljaName` +- [X] T009 [P] Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterRecipient.json` — required: `recipientName`, `emailAddress1`, `emailTemplateName`; optional: `emailAddress2` +- [X] T010 Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/informantRegisterDocumentRequest.json` — required: `registerDate`, `hearingDate`, `hearingId`, `prosecutionAuthorityId`, `prosecutionAuthorityCode`, `fileName`, `hearingVenue`; $refs all local sub-schemas [after T001–T009] +- [X] T011 Write `results-domain/results-domain-common/src/main/resources/json/schema/informantRegisterDocument/prosecutorResult.json` — required: `startDate`, `prosecutionAuthorityId`, `prosecutionAuthorityCode`; `hearingVenues` array $ref to local hearingVenue schema [after T010] +- [X] T012 Write `results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-recorded-v2.json` — `$id: http://justice.gov.uk/results/courts/informant-register-recorded-v2.json`; required: `prosecutionAuthorityId`, `informantRegister` ($ref local documentRequest) [after T010] +- [X] T013 Write `results-domain/results-domain-common/src/main/resources/json/schema/results.event.informant-register-generated-v2.json` — `$id: http://justice.gov.uk/results/courts/informant-register-generated-v2.json`; required: `informantRegisterDocumentRequests` (array $ref local documentRequest); optional: `systemGenerated` (boolean) [after T010] +- [X] T014 Update `results-command/results-command-api/src/raml/json/schema/results.add-informant-register.json` — replace `$ref` to core-domain with `$ref: http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json` [after T010] +- [X] T015 Update `results-query/results-query-api/src/raml/json/schema/results.prosecutor-results.json` — replace `$ref` to core-domain `prosecutorResult.json` with `$ref: http://justice.gov.uk/results/courts/informantRegisterDocument/prosecutorResult.json` [after T011] +- [X] T016 Add two V2 subscription entries to `results-event/results-event-listener/src/yaml/subscriptions-descriptor.yaml`: `results.event.informant-register-recorded-v2` (schema_uri: `http://justice.gov.uk/results/courts/informant-register-recorded-v2.json`) and `results.event.informant-register-generated-v2` (schema_uri: `http://justice.gov.uk/results/courts/informant-register-generated-v2.json`) [after T012, T013] +- [X] T017 Add V2 subscription entry to `results-event/results-event-processor/src/yaml/subscriptions-descriptor.yaml`: `results.event.informant-register-generated-v2` (schema_uri: `http://justice.gov.uk/results/courts/informant-register-generated-v2.json`) [after T013] +- [X] T018 Build gate: `mvn clean install -DskipTests` on affected modules must succeed — confirm zero compilation errors before proceeding to any Java task + +**Checkpoint**: All contract artefacts exist and the project compiles. Phase 2 may now begin. + +--- + +## Phase 2: Foundational — Local POJO Types (TDD) + +**Purpose**: Hand-written local POJO types in `uk.gov.moj.cpp.results.domain.informant.model` (no Lombok, no Spring). These POJOs are required by US1, US2, US3, and US4 — they are blocking prerequisites for all user story phases. + +**⚠️ TDD**: Every test task must be written and confirmed to FAIL (assertion failure or compilation failure) BEFORE its paired production-code task begins. + +- [X] T019 [P] Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/VerdictTest.java` — assert builder creates instance with `verdictCode`, `verdictDate`, `verdictType`; assert null fields tolerated; assert Jackson deserialization roundtrip from `{"verdictCode":"G","verdictDate":"2026-04-13","verdictType":"FOUND_GUILTY"}` preserves all fields +- [X] T020 Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/Verdict.java` — explicit constructor, getters, nested `Builder`; `@JsonCreator`/`@JsonProperty`; makes T019 green +- [X] T021 [P] Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/InformantRegisterOffenceTest.java` — assert builder sets `verdict` field (type `Verdict`); assert no `getVerdictCode()` method exists on the class; assert JSON with `verdict` object deserializes correctly; assert JSON with no `verdict` produces null verdict +- [X] T022 Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/InformantRegisterOffence.java` — `verdict: Verdict` field; required fields `offenceCode`, `offenceTitle`, `orderIndex`; no `verdictCode` field; builder + getters; makes T021 green +- [X] T023 [P] Write failing tests `InformantRegisterResultTest.java` and `InformantRegisterResultDataTest.java` in `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/` — assert builder/getters and JSON deserialization roundtrips +- [X] T024 [P] Write `InformantRegisterResult.java` and `InformantRegisterResultData.java` in `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/` — builder pattern, Jackson annotations; makes T023 green +- [X] T025 [P] Write failing tests `InformantRegisterCaseOrApplicationTest.java`, `InformantRegisterDefendantTest.java`, `InformantRegisterHearingTest.java`, `InformantRegisterHearingVenueTest.java`, `InformantRegisterRecipientTest.java` in `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/` — assert builders, getters, JSON deserialization roundtrips +- [X] T026 [P] Write `InformantRegisterCaseOrApplication.java`, `InformantRegisterDefendant.java`, `InformantRegisterHearing.java`, `InformantRegisterHearingVenue.java`, `InformantRegisterRecipient.java` in `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/` — builder pattern, Jackson annotations; makes T025 green +- [X] T027 Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/InformantRegisterDocumentRequestTest.java` — assert builder sets all required fields; assert JSON deserialization of a nested document (hearingVenue → courtSessions → defendants → offences → verdict) produces correct object graph +- [X] T028 Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/InformantRegisterDocumentRequest.java` — required: `registerDate` (ZonedDateTime), `hearingDate`, `hearingId`, `prosecutionAuthorityId`, `prosecutionAuthorityCode`, `fileName`, `hearingVenue`; optional: `prosecutionAuthorityOuCode`, `majorCreditorCode`, `prosecutionAuthorityName`, `recipients`, `groupId`; builder + getters; makes T027 green +- [X] T029 [P] Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/ProsecutorResultTest.java` — assert builder sets `startDate`, `prosecutionAuthorityId`, `prosecutionAuthorityCode`; optional fields nullable +- [X] T030 [P] Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/informant/model/ProsecutorResult.java` — builder + getters; makes T029 green +- [X] T031 Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/event/InformantRegisterRecordedV2Test.java` — assert event name constant equals `"results.event.informant-register-recorded-v2"`; assert builder sets `prosecutionAuthorityId` (UUID) and `informantRegister` (local `InformantRegisterDocumentRequest`) +- [X] T032 Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/event/InformantRegisterRecordedV2.java` — public static final `NAME = "results.event.informant-register-recorded-v2"`; fields: `prosecutionAuthorityId`, `informantRegister`; builder + getters; makes T031 green +- [X] T033 Write failing test `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/event/InformantRegisterGeneratedV2Test.java` — assert event name constant equals `"results.event.informant-register-generated-v2"`; assert builder sets `informantRegisterDocumentRequests` list and optional `systemGenerated` +- [X] T034 Write `results-domain/results-domain-common/src/main/java/uk/gov/moj/cpp/results/domain/event/InformantRegisterGeneratedV2.java` — public static final `NAME = "results.event.informant-register-generated-v2"`; fields: `informantRegisterDocumentRequests` (List), `systemGenerated` (Boolean); builder + getters; makes T033 green +- [X] T035 Build gate: `mvn -pl results-domain/results-domain-common test` — all tests green + +**Checkpoint**: All local types exist and are tested. User story phases may now begin. + +--- + +## Phase 3: US1 — Add Informant Register Ingest (Priority: P1) 🎯 MVP + +**Goal**: The `results.add-informant-register` command handler accepts offences with a structured `verdict` object, emits `InformantRegisterRecordedV2`, and the `ProsecutionAuthorityAggregate` applies the V2 event using local types. + +**Independent Test**: Submit `results.add-informant-register` with an offence carrying a `verdict` object with `verdictCode="G"`, `verdictDate="2026-04-13"`, `verdictType="FOUND_GUILTY"` → confirm the emitted event is `results.event.informant-register-recorded-v2` and `payload.informantRegister.hearingVenue.courtSessions[0].defendants[0].prosecutionCasesOrApplications[0].offences[0].verdict.verdictCode` equals `"G"`. + +- [X] T036 [US1] Gap-fill `results-command/results-command-handler/src/test/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandlerTest.java` — add missing scenarios: offence with no verdict (allowed), offence with null defendants (edge case), verify existing V1 tests still pass +- [X] T037 [US1] Write failing test in `InformantRegisterHandlerTest.java` — `handleAddInformantRegisterToEventStream_withVerdictObject_shouldEmitV2RecordedEvent`: build a command envelope with local `InformantRegisterDocumentRequest` carrying an offence with `verdict.verdictCode="G"` → assert `eventRepository.create` called with event name `results.event.informant-register-recorded-v2` and payload contains `verdict.verdictCode = "G"`; confirm test fails (V2 event not yet emitted) +- [X] T071 [US1] FR-004 co-dependency verified at the correct layer. The originally-specced location (`InformantRegisterHandlerTest`) is wrong: the handler unit test invokes the handler directly and bypasses framework envelope validation, so it cannot exercise rejection. Instead: (a) `results-domain/results-domain-common/src/test/java/uk/gov/moj/cpp/results/domain/informant/model/VerdictSchemaDependencyTest.java` (4 tests, green) loads `verdict.json` via everit and proves `verdictCode`⇄`verdictDate` co-dependency (verdictCode-only and verdictDate-only both fail; both-present and empty both pass); (b) negative IT `shouldRejectInformantRegisterWithVerdictCodeButNoVerdictDate` + fixture `results.add-informant-register-document-request-with-invalid-verdict.json` asserts the command is rejected with a 4xx at the API boundary. **No production handler guard needed** — the schema `dependencies` block is the enforcement mechanism. (IT compiles; runs under Docker.) +- [X] T038 [US1] Update `results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java` — change `handleAddInformantRegisterToEventStream` to deserialize payload to local `InformantRegisterDocumentRequest` and emit `InformantRegisterRecordedV2`; emit V2 only (V1 retained in event store for historical replay per spec.md Assumptions); makes T037 green +- [X] T039 [US1] Gap-fill `results-domain/results-domain-aggregate/src/test/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregateTest.java` — review existing `shouldReturnInformantRegisterNotified` and `shouldReturnInformantRegisterIgnored`; add missing scenarios for V1 generated event apply if not already covered +- [X] T040 [US1] Write failing test in `ProsecutionAuthorityAggregateTest.java` — `apply_informantRegisterGeneratedV2_shouldSetRecipientsFromLocalTypes`: create `InformantRegisterGeneratedV2` event with local `InformantRegisterDocumentRequest` containing `InformantRegisterRecipient`; apply to aggregate; assert `informantRegisterRecipients` is populated correctly; confirm test fails +- [X] T041 [US1] Update `results-domain/results-domain-aggregate/src/main/java/uk/gov/moj/cpp/results/domain/aggregate/ProsecutionAuthorityAggregate.java` — add `apply(InformantRegisterGeneratedV2 event)` method reading local `InformantRegisterDocumentRequest.getRecipients()`; update imports from core-domain to local types; makes T040 green +- [X] T042 [US1] Build gate: `mvn -pl results-domain/results-domain-common,results-command/results-command-handler,results-domain/results-domain-aggregate -am test` — all tests green + +**Checkpoint**: US1 fully implemented and tested. The command ingest path accepts and stores verdict data. + +--- + +## Phase 4: US3 — Prosecutor Results Query (Priority: P1) + +**Goal**: The `results.prosecutor-results` query returns the structured `verdict` object on offences where a verdict was recorded; omits the field entirely when absent. + +**Independent Test**: Store an `InformantRegisterEntity` with a payload JSON containing an offence with `"verdict": {"verdictCode": "G", "verdictDate": "2026-04-13", "verdictType": "FOUND_GUILTY"}`; call `getProsecutorResults`; assert response JSON includes `hearingVenues[0].courtSessions[0].defendants[0].prosecutionCasesOrApplications[0].offences[0].verdict.verdictCode = "G"`. Store a second entity with an offence without a `verdict` key; assert response offence has no `verdict` field. + +- [X] T043 [US3] Gap-fill `results-query/results-query-view/src/test/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryViewTest.java` — add any missing scenarios for current behaviour (e.g., empty result set, multiple hearing venues, null defendants) +- [X] T044 [US3] Write failing test in `ProsecutorResultsQueryViewTest.java` — `getProsecutorResults_whenOffenceHasVerdict_shouldIncludeVerdictInResponse`: store `InformantRegisterEntity` with payload JSON containing offence with `verdict.verdictCode: "G"`; call query; assert returned `ProsecutorResult` includes `offence.verdict.verdictCode = "G"`, `offence.verdict.verdictDate` present; confirm test fails (current code uses core-domain type with no `verdict` field) +- [X] T045 [US3] Write failing test in `ProsecutorResultsQueryViewTest.java` — `getProsecutorResults_whenOffenceHasNoVerdict_shouldOmitVerdictField`: store entity with offence JSON that has no `verdict` key; assert response offence object serializes without `verdict` key (not `"verdict": null`) +- [X] T046 [US3] Write failing test in `ProsecutorResultsQueryViewTest.java` — `getProsecutorResults_whenOffenceVerdictIsNull_shouldOmitVerdictField`: store entity with `"verdict": null` in offence JSON; assert response offence has no `verdict` key +- [~] T072 [US3] **DESCOPED (draft — needs sign-off).** Original intent (FR-008): WARN when a verdict-type reference lookup returns nothing, while still returning the offence. **Reason for descope:** the implemented architecture has *no* verdict-type reference lookup in the query path — `verdictType` is persisted in the entity `payload` JSON at ingest (US1) and deserialized verbatim into the local `InformantRegisterOffence.verdict` by `ProsecutorResultsQueryView`. There is no lookup that can fail at query time, so there is nothing to WARN about and the task's premise does not hold. **Options for sign-off:** (a) drop T072 and amend FR-008 to state verdict-type is resolved once at ingest, not at query (recommended); or (b) if a query-time resolution is genuinely required, that is a new design item (add a reference-data call + WARN in the query view) and should be re-specced as such, not patched in. **No code written.** +- [~] T073 [US3] **DESCOPED (draft — needs sign-off).** Original intent: omit an empty `"verdict": {}` from the query response (not `{}`, not `null`). **Reason for descope:** the prescribed fix — `@JsonInclude(NON_EMPTY)` on `InformantRegisterOffence.verdict` — does not work. `NON_EMPTY` treats a non-null POJO bean as non-empty, so an all-null `Verdict` deserialized from `{}` still serialises as `{}`. Correctly omitting it requires either the converter/query view to null-out a verdict whose fields are all null before serialising, or a custom serializer — neither is in the current task's one-line scope. Note the practical risk is low: `{}` only arises from a malformed stored payload (T071's schema dependency prevents a partial verdict, and a fully-absent verdict already serialises correctly via the existing `NON_NULL`). **Options for sign-off:** (a) drop T073 as a non-occurring edge case (recommended given the schema guard); or (b) implement a null-empty-verdict guard in `ProsecutorResultsQueryView` and re-spec accordingly. **No code written.** +- [X] T047 [US3] Update `results-query/results-query-view/src/main/java/uk/gov/moj/cpp/results/query/view/ProsecutorResultsQueryView.java` — replace core-domain `InformantRegisterDocumentRequest` and `ProsecutorResult` with local types; add SLF4J WARN log when verdict-type lookup returns no result (FR-008); use `@JsonInclude(NON_EMPTY)` on `InformantRegisterOffence.verdict` to omit null and empty-object cases (T046 + T073); makes T044–T046, T072, T073 green +- [X] T048 [US3] Build gate: `mvn -pl results-query/results-query-view -am test` — all tests green + +**Checkpoint**: US3 fully implemented and tested. Query API returns structured verdict data. + +--- + +## Phase 5: US2 — Generate Informant Register (Priority: P2) + +**Goal**: `results.generate-informant-register` and `results.generate-informant-register-by-date` emit `InformantRegisterGeneratedV2`; the event listener and processor handle V2 generated events, propagating the structured verdict through to the CSV output. + +**Independent Test**: Trigger `results.generate-informant-register` → confirm emitted event is `results.event.informant-register-generated-v2`; send that event to the listener → confirm entity status set to GENERATED; send to processor → confirm CSV row `verdictCode` equals `"G"` when offence verdict is `FOUND_GUILTY`. + +- [X] T050 [US2] Gap-fill `InformantRegisterHandlerTest.java` then write failing test `processRequests_shouldEmitInformantRegisterGeneratedV2`: review existing `processRequests` test coverage; add missing scenarios (e.g., empty document-requests list, null recipients); then write the V2 failing test — mock dependencies, confirm `processRequests` emits event with name `results.event.informant-register-generated-v2` and payload `informantRegisterDocumentRequests` list contains local `InformantRegisterDocumentRequest` with verdict data; confirm test currently fails (V2 not yet emitted) +- [X] T051 [US2] Update `results-command/results-command-handler/src/main/java/uk/gov/moj/cpp/results/command/handler/InformantRegisterHandler.java` — change `processRequests` to emit `InformantRegisterGeneratedV2` using local types; makes T050 green +- [X] T052 [US2] Gap-fill `results-event/results-event-listener/src/test/java/uk/gov/moj/cpp/results/event/listener/InformantRegisterEventListenerTest.java` — review existing test coverage; add missing scenarios if any (e.g., missing test for status transition) +- [X] T053 [US2] Write failing test in `InformantRegisterEventListenerTest.java` — `saveInformantRegisterV2_shouldSaveEntityFromLocalDocumentRequest`: build `results.event.informant-register-recorded-v2` envelope with local `InformantRegisterDocumentRequest`; assert listener saves `InformantRegisterEntity` with correct `prosecutionAuthorityId`, `registerDate`, and raw `payload` JSON; confirm fails (no V2 handler yet) +- [X] T054 [US2] Write failing test in `InformantRegisterEventListenerTest.java` — `generateInformantRegisterV2_shouldSetEntityStatusToGenerated`: build `results.event.informant-register-generated-v2` envelope; assert entity `status` updated to GENERATED; confirm fails +- [X] T055 [US2] Add `@Handles("results.event.informant-register-recorded-v2")` method `saveInformantRegisterV2` to `results-event/results-event-listener/src/main/java/uk/gov/moj/cpp/results/event/listener/InformantRegisterEventListener.java`; deserializes local `InformantRegisterDocumentRequest`; reuses existing entity-save logic; makes T053 green +- [X] T056 [US2] Add `@Handles("results.event.informant-register-generated-v2")` method `generateInformantRegisterV2` to `InformantRegisterEventListener.java`; updates entity status to GENERATED; makes T054 green +- [X] T057 [US2] Gap-fill `results-event/results-event-processor/src/test/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessorTest.java` — add missing coverage: `buildOffenceDetails` with offence that has a `verdict` object but `verdictCode = null` (should produce empty string); `buildOffenceDetails` with no verdict object at all +- [X] T058 [US2] Write failing test in `InformantRegisterEventProcessorTest.java` — `generateInformantRegister_whenOffenceHasVerdictCode_shouldPopulateVerdictCodeInCsvRow`: build event with offence `verdict.verdictCode = "G"`; assert `InformantRegisterDocument.verdictCode = "G"` in the CSV model; confirm fails (current code calls `offence.getVerdictCode()` which does not exist on local type) +- [X] T059 [US2] Update `buildOffenceDetails` in `results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java` — replace `offence.getVerdictCode()` with null-safe `offence.getVerdict() != null ? offence.getVerdict().getVerdictCode() : null`; makes T058 green +- [X] T060 [US2] Write failing test in `InformantRegisterEventProcessorTest.java` — `generateInformantRegisterV2_shouldProduceSameCsvOutputAsV1Handler`: build `results.event.informant-register-generated-v2` envelope with the same data as a V1 test; assert the same CSV rows are produced; confirm fails (no V2 handler yet) +- [X] T061 [US2] Add `@Handles("results.event.informant-register-generated-v2")` method `generateInformantRegisterV2` to `results-event/results-event-processor/src/main/java/uk/gov/moj/cpp/results/event/processor/InformantRegisterEventProcessor.java`; delegates to same CSV-generation logic as V1 handler; makes T060 green +- [X] T062 [US2] Build gate: `mvn -pl results-command/results-command-handler,results-event/results-event-listener,results-event/results-event-processor -am test` — all tests green + +**Checkpoint**: US2 fully implemented and tested. Generate flow emits V2 events; listener and processor handle them correctly. + +--- + +## Phase 6: US4 — Pre-migration Event Replay (Priority: P2) + +**Goal**: V1 events already in the event store (`informant-register-recorded`, `informant-register-generated`) replay through the listener without runtime errors, even though their offences carry a flat `verdictCode` string (old schema). + +**Independent Test**: Replay a V1 `informant-register-recorded` event JSON (old schema with flat `verdictCode: "G"` on an offence) through the V1 `saveInformantRegister` listener handler → confirm no exception thrown and entity saved correctly. + +- [X] T063 [US4] Write passing regression test in `InformantRegisterEventListenerTest.java` — `saveInformantRegister_withPreMigrationFlatVerdictCode_shouldNotThrow`: build V1 `informant-register-recorded` envelope JSON where offence has `"verdictCode": "G"` (flat string, old core-domain schema); send to V1 listener handler; assert no exception; assert entity saved with correct `prosecutionAuthorityId`. This test is expected to PASS without new production code — it is a backward-compat regression guard, not a TDD failing test +- [X] T064 [US4] Confirm T063 passes without new production code — V1 handler uses core-domain types that already support flat `verdictCode`; document this confirmation explicitly (no production change needed) +- [X] T065 [US4] Write failing test in `InformantRegisterEventListenerTest.java` — `generateInformantRegister_withPreMigrationFlatVerdictCode_shouldNotThrow`: build V1 `informant-register-generated` envelope with offences carrying flat `verdictCode` string; send to V1 listener handler; assert no exception +- [X] T066 [US4] Confirm T065 passes without new production code — document confirmation; no production change needed +- [X] T067 [US4] Build gate: `mvn test` on all affected modules — full unit-test suite green + +**Checkpoint**: US4 verified. Pre-migration V1 events remain replayable with no production code changes. + +--- + +## Phase 7: Polish & Full Build + +**Purpose**: Final verification and code hygiene checks across all changes. + +- [X] T068 Full build gate: `mvn clean install` — zero compilation errors, zero test failures across entire reactor +- [X] T069 [P] Code hygiene review: swept all 40 feature Java files (commit `413e0c2a4`) — no wildcard imports, no `System.out`/`System.err`, no Spring, no Lombok. One finding fixed: removed `e.printStackTrace()` in `InformantRegisterEventProcessorTest.getFileContent` (now throws `IllegalStateException` with cause — Constitution VII). Processor module rebuilt green. +- [X] T070 [P] Schema-subscription symmetry check (Constitution Principle VI + SC-003): verify `results.event.informant-register-recorded-v2.json` exists AND appears in listener `subscriptions-descriptor.yaml`; verify `results.event.informant-register-generated-v2.json` exists AND appears in both listener AND processor `subscriptions-descriptor.yaml`; verify zero `$ref` to `http://justice.gov.uk/core/courts/informantRegisterDocument/` remains in any command or event schema for informant-register flows (SC-003); confirm `results.prosecutor-results.json` schema accepts a sample payload containing a `verdict` sub-object with `verdictCode`, `verdictDate`, `verdictType` (SC-004) +- [X] T074 End-to-end IT (US1 + US3): add `shouldAddInformantRegisterRequestWithVerdictAndExposeItInProsecutorResults` to `results-integration-test/src/test/java/uk/gov/moj/cpp/results/it/InformantRegisterDocumentRequestIT.java` plus fixture `results.add-informant-register-document-request-with-verdict.json` (offence carries a `verdict` object with `verdictCode="G"`, `verdictDate`, `verdictType`); POST `results.add-informant-register` → poll `GET /prosecutor/{ouCode}?startDate=...` and assert the offence `verdict.verdictCode = "G"` surfaces. Added `verifyProsecutorResultsContainVerdict` helper. NOTE: compiles (`mvn -pl results-integration-test test-compile`) but NOT executed here — IT run requires Docker + `CPP_DOCKER_DIR` via `./runIntegrationTests.sh` +- [X] T075 ✅ **FIXED** (SC-003 gap found by T070) — repointed the handler-side messaging schema `results-command/results-command-handler/src/raml/json/schema/results.command.add-informant-register.json` (wired in `results-command-handler.messaging.raml` for media type `application/vnd.results.command.add-informant-register+json`) still `$ref`s core-domain `http://justice.gov.uk/core/courts/informantRegisterDocument/informantRegisterDocumentRequest.json`. T014 only migrated the command-**api** schema. Consequence: a command carrying the structured `verdict` object is validated at the handler boundary against the **core-domain** schema (flat `verdictCode`), so the verdict may be rejected before the handler runs — which would make T074 fail at runtime. Fix: repoint this `$ref` (and its `id`) to the local `http://justice.gov.uk/results/courts/informantRegisterDocument/informantRegisterDocumentRequest.json`, then re-run T068. (Other lingering core-domain refs are intentional/out-of-scope: V1 `recorded`/`generated` event schemas retained for historical replay; `notified`/`notified-v2` reference only ids; `by-material`/`by-request-date`/`document-request` query schemas were never in migration scope per T014/T015.) + +**Checkpoint**: Full build green (T068). Schema-subscription symmetry mostly clean (T070) — one gap (T075) found. Outstanding before code-reviewer/qa: T069 (hygiene), T071–T073 (US3 edge cases, see notes), T075 (handler messaging schema). + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (Contracts)**: No dependencies — start immediately +- **Phase 2 (POJOs)**: Requires T018 (Phase 1 gate) — BLOCKS all user story phases +- **Phase 3 (US1)**: Requires T035 (Phase 2 gate) — first P1 story, no cross-story dependency +- **Phase 4 (US3)**: Requires T035 (Phase 2 gate) — can start in parallel with Phase 3 (different modules) +- **Phase 5 (US2)**: Requires T035 (Phase 2 gate) AND T042 (US1 gate, command handler V2 emit) — processor depends on V2 events being emitted +- **Phase 6 (US4)**: Requires T035 (Phase 2 gate) — tests V1 handlers (no new production code); can start after Phase 2 +- **Phase 7 (Polish)**: Requires T042, T048, T062, T067 — all user story gates passed + +### User Story Dependencies + +| Story | Priority | Depends on | Blocked by | +|-------|----------|-----------|------------| +| US1 — Add Ingest | P1 | Phase 2 complete | — | +| US3 — Query | P1 | Phase 2 complete | — | +| US2 — Generate | P2 | Phase 2 + US1 gate | T042 | +| US4 — Compat | P2 | Phase 2 complete | — | + +### Within Each Phase + +1. TDD rule: Test task written → confirmed failing → production code written → confirmed passing +2. In Phase 2: deepest types first (`Verdict` → `InformantRegisterOffence` → … → `InformantRegisterDocumentRequest` → V2 events) +3. In Phase 3: gap-fill → failing V2 test → production command handler → failing aggregate test → production aggregate +4. In Phase 4: gap-fill → failing verdict tests (T044–T046) → production view +5. In Phase 5: gap-fill → failing handler test → production handler → failing listener tests → production listener → failing processor tests → production processor + +--- + +## Parallel Opportunities + +### Phase 1 (Contracts) — run T002–T009 in parallel + +``` +T001 (verdict.json — no deps) +T002–T009 [all in parallel — different schema files, no cross-deps] +Then: T010, T011, T012, T013, T014, T015, T016, T017 (sequentially or grouped by dep) +Then: T018 (build gate) +``` + +### Phase 2 (POJOs) — run test+production pairs in parallel where types are independent + +``` +T019 + T020 (Verdict) in parallel with T021 + T022 (Offence — once Verdict exists) +T023 + T024 (Result + ResultData) in parallel with T025 + T026 (remaining mid-tier types) +T027 + T028 (DocumentRequest — depends on all above) +T029 + T030 (ProsecutorResult) in parallel with T031 + T032 (RecordedV2 event) +T033 + T034 (GeneratedV2 event) after T028 +T035 (gate) +``` + +### Phase 3 + Phase 4 — run in parallel (different modules) + +``` +After T035: + [Track A] Phase 3: T036–T041 (command handler + aggregate) + [Track B] Phase 4: T043–T047 (query view — different module, no dependency on Track A) + T042 (Track A gate) + T048 (Track B gate) — then Phase 5 can begin +``` + +--- + +## Implementation Strategy + +### MVP (US1 + US3 only — both P1) + +1. Phase 1: Contracts (T001–T018) +2. Phase 2: POJOs (T019–T035) +3. Phase 3: US1 — Add Ingest (T036–T042) +4. Phase 4: US3 — Query (T043–T048) +5. **STOP and VALIDATE**: `mvn test` on all P1 modules green; US1 and US3 independently testable +6. Deliver MVP — query returns verdict data for newly ingested records + +### Full Delivery (add P2 stories) + +7. Phase 5: US2 — Generate (T050–T062) in parallel with Phase 6: US4 — Compat (T063–T067) +8. Phase 7: Full build (T068–T070) + +--- + +## Notes + +- [P] tasks involve different files with no shared dependencies — safe to parallelize +- [US*] label maps task to user story for traceability +- TDD gate per story: each story's tests must be green before moving to the next +- Never skip T018 or T035 build gates — they are the Principle I and Phase 2 completeness checkpoints +- Commits: after each build gate (T018, T035, T042, T048, T062, T067, T068) at minimum +- Integration test (`InformantRegisterDocumentRequestIT`) deferred — not part of this task list (see spec.md Assumptions) diff --git a/test-utilities/pom.xml b/test-utilities/pom.xml index 7c38ac325..b8fac7a8f 100644 --- a/test-utilities/pom.xml +++ b/test-utilities/pom.xml @@ -5,7 +5,7 @@ uk.gov.moj.cpp.results results-parent - 17.104.111-SNAPSHOT + 17.103.109-CIMD3915-SNAPSHOT test-utilities