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