diff --git a/sjp-command/sjp-command-api/src/raml/json/schema/sjp.add-case-document.json b/sjp-command/sjp-command-api/src/raml/json/schema/sjp.add-case-document.json index 32c7388d95..491d482258 100644 --- a/sjp-command/sjp-command-api/src/raml/json/schema/sjp.add-case-document.json +++ b/sjp-command/sjp-command-api/src/raml/json/schema/sjp.add-case-document.json @@ -10,6 +10,10 @@ }, "documentType": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "required": [ diff --git a/sjp-command/sjp-command-api/src/raml/json/sjp.add-case-document.json b/sjp-command/sjp-command-api/src/raml/json/sjp.add-case-document.json index 4283fd9478..3e4050471a 100644 --- a/sjp-command/sjp-command-api/src/raml/json/sjp.add-case-document.json +++ b/sjp-command/sjp-command-api/src/raml/json/sjp.add-case-document.json @@ -1,5 +1,6 @@ { "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", - "documentType": "SJPN" + "documentType": "SJPN", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf" } \ No newline at end of file diff --git a/sjp-command/sjp-command-controller/src/raml/json/schema/sjp.command.add-case-document.json b/sjp-command/sjp-command-controller/src/raml/json/schema/sjp.command.add-case-document.json index 4600251780..928e2edf0a 100644 --- a/sjp-command/sjp-command-controller/src/raml/json/schema/sjp.command.add-case-document.json +++ b/sjp-command/sjp-command-controller/src/raml/json/schema/sjp.command.add-case-document.json @@ -13,6 +13,10 @@ }, "documentType": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "required": [ diff --git a/sjp-command/sjp-command-controller/src/raml/json/sjp.command.add-case-document.json b/sjp-command/sjp-command-controller/src/raml/json/sjp.command.add-case-document.json index ceefaf2112..21e2ca8130 100644 --- a/sjp-command/sjp-command-controller/src/raml/json/sjp.command.add-case-document.json +++ b/sjp-command/sjp-command-controller/src/raml/json/sjp.command.add-case-document.json @@ -2,5 +2,6 @@ "caseId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd2", "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", - "documentType": "SJPN" + "documentType": "SJPN", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf" } \ No newline at end of file diff --git a/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandler.java b/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandler.java index a0a89de4a5..483b900f01 100644 --- a/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandler.java +++ b/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandler.java @@ -37,7 +37,8 @@ private CaseDocument caseDocumentFrom(final JsonObject payload) { UUID.fromString(payload.getString("id")), UUID.fromString(payload.getString("materialId")), payload.getString("documentType", null), - clock.now()); + clock.now(), + payload.getString("documentUri", null)); } } diff --git a/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandler.java b/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandler.java index 79f668b68c..e3359c0edd 100644 --- a/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandler.java +++ b/sjp-command/sjp-command-handler/src/main/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandler.java @@ -19,6 +19,9 @@ @ServiceComponent(Component.COMMAND_HANDLER) public class UploadCaseDocumentHandler extends CaseCommandHandler { + static final String CASE_DOCUMENT = "caseDocument"; + static final String CASE_DOCUMENT_URI = "caseDocumentUri"; + @Inject private Clock clock; @Handles("sjp.command.upload-case-document") @@ -26,13 +29,25 @@ public void handle(JsonEnvelope command) throws EventStreamException { JsonObject payload = command.payloadAsJsonObject(); final UUID caseId = getCaseId(payload); - final UUID caseDocumentReference = UUID.fromString(payload.getString("caseDocument")); final String caseDocumentType = payload.getString("caseDocumentType"); + + // Exactly one of a file service id (caseDocument) or a blob uri (caseDocumentUri) is + // present - the command schema's oneOf enforces that, and JsonSchemaValidationInterceptor + // applies it on the way in, so this only has to pick whichever arrived. + final String caseDocumentReference = valueOrNull(payload, CASE_DOCUMENT); + final String caseDocumentUri = valueOrNull(payload, CASE_DOCUMENT_URI); + + final UUID fileServiceId = caseDocumentReference == null ? null : UUID.fromString(caseDocumentReference); + if(isNull(getUserId(command))){ command = JsonEnvelope.envelopeFrom(metadataFrom(command.metadata()).withUserId(getUserIdFromCaseAggregate(caseId)),payload); } applyToCaseAggregate(command, - aCase -> aCase.uploadCaseDocument(caseId, caseDocumentReference, caseDocumentType) + aCase -> aCase.uploadCaseDocument(caseId, fileServiceId, caseDocumentUri, caseDocumentType) ); } + + private static String valueOrNull(final JsonObject payload, final String field) { + return payload.containsKey(field) && !payload.isNull(field) ? payload.getString(field) : null; + } } diff --git a/sjp-command/sjp-command-handler/src/raml/json/schema/sjp.command.add-case-document.json b/sjp-command/sjp-command-handler/src/raml/json/schema/sjp.command.add-case-document.json index 4600251780..928e2edf0a 100644 --- a/sjp-command/sjp-command-handler/src/raml/json/schema/sjp.command.add-case-document.json +++ b/sjp-command/sjp-command-handler/src/raml/json/schema/sjp.command.add-case-document.json @@ -13,6 +13,10 @@ }, "documentType": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "required": [ diff --git a/sjp-command/sjp-command-handler/src/raml/json/sjp.command.add-case-document.json b/sjp-command/sjp-command-handler/src/raml/json/sjp.command.add-case-document.json index ceefaf2112..21e2ca8130 100644 --- a/sjp-command/sjp-command-handler/src/raml/json/sjp.command.add-case-document.json +++ b/sjp-command/sjp-command-handler/src/raml/json/sjp.command.add-case-document.json @@ -2,5 +2,6 @@ "caseId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd2", "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", - "documentType": "SJPN" + "documentType": "SJPN", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf" } \ No newline at end of file diff --git a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandlerTest.java b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandlerTest.java index e734c4dd88..a9f44278d9 100644 --- a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandlerTest.java +++ b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/AddCaseDocumentHandlerTest.java @@ -78,6 +78,8 @@ public class AddCaseDocumentHandlerTest { @InjectMocks private AddCaseDocumentHandler addCaseDocumentHandler; + private static final String DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf"; + @Spy private Clock clock = new UtcClock(); @@ -124,9 +126,10 @@ public void testAddCaseDocument_triggersCaseDocumentAddedEvent() throws Exceptio public void testAddCaseDocument_whenCaseDocumentAlreadyExists_ReturnCaseDocumentAlreadyExistsEvent() throws Exception { JsonEnvelope addCaseDocumentCommand = anAddCaseDocumentCommand() + .withDocumentUri(DOCUMENT_URI) .build(); caseAggregate.receiveCase(CaseBuilder.aDefaultSjpCase().build(), clock.now()); - caseAggregate.addCaseDocument(CASE_ID, aCaseDocument().build()); + caseAggregate.addCaseDocument(CASE_ID, aCaseDocument().withDocumentUri(DOCUMENT_URI).build()); addCaseDocumentHandler.addCaseDocument(addCaseDocumentCommand); @@ -135,7 +138,47 @@ public void testAddCaseDocument_whenCaseDocumentAlreadyExists_ReturnCaseDocument jsonEnvelope( withMetadataEnvelopedFrom(addCaseDocumentCommand) .withName("sjp.events.case-document-addition-failed"), - payloadIsJson(withJsonPath("$.documentId", is(CASE_DOCUMENT_ID_STR))))))); + payloadIsJson(allOf( + withJsonPath("$.caseId", is(CASE_ID_STR)), + withJsonPath("$.documentId", is(CASE_DOCUMENT_ID_STR)), + withJsonPath("$.documentUri", is(DOCUMENT_URI)))))))); + } + + @Test + public void testAddCaseDocument_whenBlobAddressed_carriesTheUriOnTheAddedEvent() throws Exception { + final JsonEnvelope addCaseDocumentCommand = anAddCaseDocumentCommand() + .withDocumentUri(DOCUMENT_URI) + .build(); + caseAggregate.receiveCase(CaseBuilder.aDefaultSjpCase().build(), clock.now()); + + addCaseDocumentHandler.addCaseDocument(addCaseDocumentCommand); + + assertThat(eventStream, eventStreamAppendedWith( + streamContaining( + jsonEnvelope( + withMetadataEnvelopedFrom(addCaseDocumentCommand) + .withName("sjp.events.case-document-added"), + payloadIsJson(allOf( + withJsonPath("$.caseId", is(CASE_ID_STR)), + withJsonPath("$.caseDocument.id", is(CASE_DOCUMENT_ID_STR)), + withJsonPath("$.caseDocument.documentUri", is(DOCUMENT_URI)) + )))))); + } + + @Test + public void testAddCaseDocument_whenFileServiceAddressed_omitsTheUriEntirely() throws Exception { + final JsonEnvelope addCaseDocumentCommand = anAddCaseDocumentCommand().build(); + caseAggregate.receiveCase(CaseBuilder.aDefaultSjpCase().build(), clock.now()); + + addCaseDocumentHandler.addCaseDocument(addCaseDocumentCommand); + + // Absent rather than null: the framework serialises with NON_ABSENT. + assertThat(eventStream, eventStreamAppendedWith( + streamContaining( + jsonEnvelope( + withMetadataEnvelopedFrom(addCaseDocumentCommand) + .withName("sjp.events.case-document-added"), + payloadIsJson(withoutJsonPath("$.caseDocument.documentUri")))))); } @Test diff --git a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandlerTest.java b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandlerTest.java index 27764e1eec..36d016fcb7 100644 --- a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandlerTest.java +++ b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/UploadCaseDocumentHandlerTest.java @@ -31,6 +31,7 @@ import uk.gov.moj.cpp.sjp.event.CaseStarted; import java.util.UUID; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import javax.json.JsonObjectBuilder; @@ -48,6 +49,7 @@ public class UploadCaseDocumentHandlerTest { private static final String CASE_ID_PROPERTY = "caseId"; private static final String CASE_DOCUMENT_REFERENCE_PROPERTY = "caseDocument"; + private static final String CASE_DOCUMENT_URI_PROPERTY = "caseDocumentUri"; private static final String CASE_DOCUMENT_TYPE_PROPERTY = "caseDocumentType"; @Mock @@ -81,6 +83,7 @@ public class UploadCaseDocumentHandlerTest { private static final UUID CASE_ID = randomUUID(); private static final UUID DOCUMENT_REFERENCE = randomUUID(); private static final String DOCUMENT_TYPE = "PLEA"; + private static final String DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/doc.pdf"; @BeforeEach @@ -124,8 +127,8 @@ public void shouldRejectUploadOfCaseDocumentWhenCaseIsInReferredToCourtState() t when(caseAggregate1.getState()).thenReturn(state); when(state.getMetadataUserId()).thenReturn(randomUUID()); - final CaseDocumentUploadRejected caseDocumentUploadRejected = new CaseDocumentUploadRejected(DOCUMENT_REFERENCE, ""); - when(caseAggregate1.uploadCaseDocument(CASE_ID, DOCUMENT_REFERENCE, DOCUMENT_TYPE)).thenReturn(Stream.of(caseDocumentUploadRejected)); + final CaseDocumentUploadRejected caseDocumentUploadRejected = new CaseDocumentUploadRejected(DOCUMENT_REFERENCE, null, ""); + when(caseAggregate1.uploadCaseDocument(CASE_ID, DOCUMENT_REFERENCE, null, DOCUMENT_TYPE)).thenReturn(Stream.of(caseDocumentUploadRejected)); uploadCaseDocumentHandler.handle(command); @@ -143,6 +146,29 @@ public void shouldRejectUploadOfCaseDocumentWhenCaseIsInReferredToCourtState() t } + @Test + public void shouldRaiseUploadedEventCarryingTheUriWhenDocumentIsBlobAddressed() throws EventStreamException { + final JsonEnvelope command = createCommand(payload -> payload.add(CASE_DOCUMENT_URI_PROPERTY, DOCUMENT_URI)); + when(eventSource.getStreamById(CASE_ID)).thenReturn(eventStream); + when(aggregateService.get(eventStream, CaseAggregate.class)).thenReturn(caseAggregate); + + uploadCaseDocumentHandler.handle(command); + + assertThat(eventStream, eventStreamAppendedWith( + streamContaining( + jsonEnvelope( + withMetadataEnvelopedFrom(command) + .withName("sjp.events.case-document-uploaded"), + payloadIsJson(allOf( + withJsonPath("$.caseId", equalTo(CASE_ID.toString())), + withJsonPath("$.documentReferenceUri", equalTo(DOCUMENT_URI)), + withJsonPath("$.documentType", equalTo(DOCUMENT_TYPE)) + ))) + ))); + } + + + private JsonEnvelope createCaseDocumentUploadCommand(final UUID caseId, final UUID caseDocumentReference, final String documentType) { final JsonObjectBuilder payload = createObjectBuilder() .add(CASE_ID_PROPERTY, caseId.toString()) @@ -153,4 +179,14 @@ private JsonEnvelope createCaseDocumentUploadCommand(final UUID caseId, final UU metadataOf(randomUUID(), "sjp.command.upload-case-document"), payload.build()); } + + private JsonEnvelope createCommand(final UnaryOperator withReference) { + final JsonObjectBuilder payload = createObjectBuilder() + .add(CASE_ID_PROPERTY, CASE_ID.toString()) + .add(CASE_DOCUMENT_TYPE_PROPERTY, DOCUMENT_TYPE); + + return envelopeFrom( + metadataOf(randomUUID(), "sjp.command.upload-case-document"), + withReference.apply(payload).build()); + } } \ No newline at end of file diff --git a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/builder/AddCaseDocumentCommandBuilder.java b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/builder/AddCaseDocumentCommandBuilder.java index 84a718a290..c74b3d6e8b 100644 --- a/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/builder/AddCaseDocumentCommandBuilder.java +++ b/sjp-command/sjp-command-handler/src/test/java/uk/gov/moj/cpp/sjp/command/handler/builder/AddCaseDocumentCommandBuilder.java @@ -21,6 +21,7 @@ public class AddCaseDocumentCommandBuilder { private UUID id = CASE_DOCUMENT_ID; private UUID materialId = CASE_DOCUMENT_MATERIAL_ID; private String documentType; + private String documentUri; private AddCaseDocumentCommandBuilder() { } @@ -38,6 +39,11 @@ public AddCaseDocumentCommandBuilder withDocumentType(String documentType) { return this; } + public AddCaseDocumentCommandBuilder withDocumentUri(String documentUri) { + this.documentUri = documentUri; + return this; + } + public AddCaseDocumentCommandBuilder withId(UUID id) { this.id = id; return this; @@ -58,6 +64,10 @@ public JsonEnvelope build() { victim.add("documentType", documentType); } + if (this.documentUri != null) { + victim.add("documentUri", documentUri); + } + return envelopeFrom( metadataOf(UUID.randomUUID(), "sjp.command.add-case-document"), victim.build() diff --git a/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/CaseAggregate.java b/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/CaseAggregate.java index fe938520dc..7970fdf279 100644 --- a/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/CaseAggregate.java +++ b/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/CaseAggregate.java @@ -204,8 +204,11 @@ public Stream resolveCaseAOCPEligibility(final UUID caseId, final boolea surchargeAmountMin, surchargeAmountMax, surchargeFinePercentage, surchargeAmount)); } - public Stream uploadCaseDocument(final UUID caseId, final UUID documentReference, final String documentType) { - return apply(CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, documentType, state)); + public Stream uploadCaseDocument(final UUID caseId, + final UUID documentReference, + final String documentReferenceUri, + final String documentType) { + return apply(CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, documentReferenceUri, documentType, state)); } public Stream updateHearingRequirements(final UUID userId, diff --git a/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandler.java b/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandler.java index f622cd43ff..8a314b1928 100644 --- a/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandler.java +++ b/sjp-domain/sjp-domain-aggregate/src/main/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandler.java @@ -36,7 +36,7 @@ public Stream addCaseDocument(final UUID caseId, if (state.getCaseDocuments().containsKey(caseDocument.getId())) { LOGGER.warn("Case Document already exists with ID {}", caseDocument.getId()); return Stream.of( - new CaseDocumentAlreadyExists(caseDocument.getId(), "Add Case Document")); + new CaseDocumentAlreadyExists(caseId, caseDocument.getId(), caseDocument.getDocumentUri(), "Add Case Document")); } final int documentCount = state.getDocumentCountByDocumentType() @@ -45,25 +45,35 @@ public Stream addCaseDocument(final UUID caseId, return Stream.of(new CaseDocumentAdded(caseId, caseDocument, documentCount + 1)); } + /** + * @param documentReference file service id of the document, or null when it is addressed by + * {@code documentReferenceUri} + * @param documentReferenceUri blob uri of the document, or null when it is addressed by + * {@code documentReference}. Exactly one of the two is set; the + * caller validates that. + */ public Stream uploadCaseDocument(final UUID caseId, final UUID documentReference, + final String documentReferenceUri, final String documentType, final CaseAggregateState state) { if (!state.hasGrantedApplication()) { + final Object reference = nonNull(documentReference) ? documentReference : documentReferenceUri; + if (state.isCaseReferredForCourtHearing()) { - LOGGER.warn("Case Document Upload rejected as case is referred to court for hearing: {}", documentReference); - final String description = format("Case Document %s Upload rejected as case %s is referred to court for hearing", documentReference, caseId); - return Stream.of(new CaseDocumentUploadRejected(documentReference, description)); + LOGGER.warn("Case Document Upload rejected as case is referred to court for hearing: {}", reference); + final String description = format("Case Document %s Upload rejected as case %s is referred to court for hearing", reference, caseId); + return Stream.of(new CaseDocumentUploadRejected(documentReference, documentReferenceUri, description)); } if (!state.isManagedByAtcm()) { - LOGGER.warn("Case Document Upload rejected as case is no longer managed by ATCM: {}", documentReference); - final String description = format("Case Document %s Upload rejected as case %s is not managed by ATCM", documentReference, caseId); - return Stream.of(new CaseDocumentUploadRejected(documentReference, description)); + LOGGER.warn("Case Document Upload rejected as case is no longer managed by ATCM: {}", reference); + final String description = format("Case Document %s Upload rejected as case %s is not managed by ATCM", reference, caseId); + return Stream.of(new CaseDocumentUploadRejected(documentReference, documentReferenceUri, description)); } } - return Stream.of(new CaseDocumentUploaded(caseId, documentReference, documentType)); + return Stream.of(new CaseDocumentUploaded(caseId, documentReference, documentReferenceUri, documentType)); } public Stream deleteCaseDocument(final CaseAggregateState caseAggregateState, final UUID documentId) { diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/AddCaseDocumentTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/AddCaseDocumentTest.java index d17b413040..e9cfecbfa3 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/AddCaseDocumentTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/AddCaseDocumentTest.java @@ -7,6 +7,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -25,6 +26,7 @@ public class AddCaseDocumentTest extends CaseAggregateBaseTest { private static final CaseDocument caseDocument = CaseDocumentBuilder.defaultCaseDocument(); + private static final String DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf"; @Test public void uploadCaseDocument_caseDocumentUploadedKeepsTrackOfNthDocumentOfType() { @@ -45,7 +47,7 @@ public void testCaseInsensitivityWhilstUploadingCaseDocument() { private void assertUploadCaseDocument(String documentType, int expectedIndexWithinDocumentType) { //when - Stream eventStream = caseAggregate.addCaseDocument(UUID.randomUUID(), new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), documentType, null)); + Stream eventStream = caseAggregate.addCaseDocument(UUID.randomUUID(), new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), documentType, null, null)); List events = asList(eventStream.toArray()); //then @@ -57,7 +59,7 @@ private void assertUploadCaseDocument(String documentType, int expectedIndexWith public void caseDocumentAdded_apply() { //given final CaseDocument sjpn = - new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), "SJPN", null); + new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), "SJPN", null, null); final CaseDocumentAdded firstCaseDocumentAddedEvent = (CaseDocumentAdded) caseAggregate.apply(new CaseDocumentAdded(UUID.randomUUID(), sjpn, 1)); assertEquals(1, firstCaseDocumentAddedEvent.getIndexWithinDocumentType()); @@ -65,7 +67,7 @@ public void caseDocumentAdded_apply() { assertEquals(2, secondCaseDocumentAddedEvent.getIndexWithinDocumentType()); //when - Stream eventStream = caseAggregate.addCaseDocument(aCase.getId(), new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), "SJPN", null)); + Stream eventStream = caseAggregate.addCaseDocument(aCase.getId(), new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), "SJPN", null, null)); List events = asList(eventStream.toArray()); //then @@ -115,4 +117,36 @@ public void shouldReturnCaseDocumentAlreadyExistsWhenCaseDocumentAlreadyExists_s Object object = objects.get(0); assertThat(object.getClass(), is(CoreMatchers.equalTo(CaseDocumentAlreadyExists.class))); } + + @Test + public void shouldCarryTheUriOnTheRejectionWhenABlobAddressedDocumentIsFiledTwice() { + // A duplicate filing is the one outcome that leaves the blob unreferenced, so the rejection + // has to name the uri - otherwise the calling context cannot correlate it back and release + // the blob. + final CaseDocument blobAddressed = CaseDocumentBuilder.aCaseDocument() + .withDocumentUri(DOCUMENT_URI) + .build(); + + caseAggregate.addCaseDocument(aCase.getId(), blobAddressed); + + final List events = caseAggregate.addCaseDocument(aCase.getId(), blobAddressed).collect(toList()); + + assertThat(events.size(), is(1)); + + final CaseDocumentAlreadyExists rejection = (CaseDocumentAlreadyExists) events.get(0); + assertThat(rejection.getCaseId(), is(aCase.getId())); + assertThat(rejection.getDocumentUri(), is(DOCUMENT_URI)); + assertThat(rejection.getDocumentId(), is(blobAddressed.getId())); + } + + @Test + public void shouldLeaveTheUriNullOnTheRejectionForAFileServiceAddressedDocument() { + caseAggregate.addCaseDocument(aCase.getId(), caseDocument); + + final List events = caseAggregate.addCaseDocument(aCase.getId(), caseDocument).collect(toList()); + + final CaseDocumentAlreadyExists rejection = (CaseDocumentAlreadyExists) events.get(0); + assertThat(rejection.getCaseId(), is(aCase.getId())); + assertThat(rejection.getDocumentUri(), is(nullValue())); + } } diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UpdateFinancialMeansTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UpdateFinancialMeansTest.java index 94ecae53ae..ac72c06133 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UpdateFinancialMeansTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UpdateFinancialMeansTest.java @@ -95,7 +95,7 @@ public void shouldCreateFinancialMeansDeletedEventIfDefendantExists() { public void addCaseDocument(String documentType) { UUID documentId = UUID.randomUUID(); - CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now()); + CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now(), null); Stream eventStream = caseAggregate.addCaseDocument(UUID.randomUUID(), caseDocument); } diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UploadCaseDocumentTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UploadCaseDocumentTest.java index 90d8a0e456..f4b7c9a5d8 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UploadCaseDocumentTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/UploadCaseDocumentTest.java @@ -48,7 +48,7 @@ public class UploadCaseDocumentTest extends CaseAggregateBaseTest { @Test public void raisesCaseStartedEvent_whenCaseIsNotStarted() { - final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, documentType); + final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, null, documentType); final List events = eventsStream.collect(Collectors.toList()); assertThat(events, hasSize(1)); @@ -63,7 +63,7 @@ public void raisesCaseStartedEvent_whenCaseIsNotStarted() { @Test public void raisesOnlyCaseDocumentsUploaded_whenCaseIsCreated() { - final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, documentType); + final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, null, documentType); final List events = eventsStream.collect(Collectors.toList()); assertThat(events, hasSize(1)); @@ -111,7 +111,7 @@ private void thenCaseDocumentUploadRejectEventRaised(final UUID documentReferenc } private List whenUploadCaseDocumentInvoked(final UUID documentReference, final String documentType, final UUID caseId) { - final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, documentType); + final Stream eventsStream = caseAggregate.uploadCaseDocument(caseId, documentReference, null, documentType); return eventsStream.collect(Collectors.toList()); } diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandlerTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandlerTest.java index 81b011e7e3..f75516cf8a 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandlerTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/handler/CaseDocumentHandlerTest.java @@ -7,12 +7,16 @@ import uk.gov.moj.cpp.sjp.event.CaseDocumentUploadRejected; import uk.gov.moj.cpp.sjp.event.CaseDocumentUploaded; +import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,11 +34,32 @@ public void uploadCaseDocument_whenCaseReferredForCourtHearing_shouldReturnCaseD when(state.hasGrantedApplication()).thenReturn(false); when(state.isCaseReferredForCourtHearing()).thenReturn(true); - Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, documentType, state); + Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, null, documentType, state); assertThat(result.collect(Collectors.toList()), contains(instanceOf(CaseDocumentUploadRejected.class))); } + @Test + public void uploadCaseDocument_whenBlobAddressedAndRejected_shouldCarryTheUriOnTheRejection() { + final UUID caseId = UUID.randomUUID(); + final String documentReferenceUri = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/doc.pdf"; + final CaseAggregateState state = mock(CaseAggregateState.class); + + when(state.hasGrantedApplication()).thenReturn(false); + when(state.isCaseReferredForCourtHearing()).thenReturn(true); + + final List events = CaseDocumentHandler.INSTANCE + .uploadCaseDocument(caseId, null, documentReferenceUri, "type", state) + .collect(Collectors.toList()); + + assertThat(events, contains(instanceOf(CaseDocumentUploadRejected.class))); + + final CaseDocumentUploadRejected rejected = (CaseDocumentUploadRejected) events.get(0); + assertThat(rejected.getDocumentReferenceUri(), is(documentReferenceUri)); + assertThat(rejected.getDocumentId(), is(nullValue())); + assertThat(rejected.getDescription(), containsString(documentReferenceUri)); + } + @Test public void uploadCaseDocument_whenCaseNotManagedByAtcm_shouldReturnCaseDocumentUploadRejectedEvent() { UUID caseId = UUID.randomUUID(); @@ -46,7 +71,7 @@ public void uploadCaseDocument_whenCaseNotManagedByAtcm_shouldReturnCaseDocument when(state.isCaseReferredForCourtHearing()).thenReturn(false); when(state.isManagedByAtcm()).thenReturn(false); - Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, documentType, state); + Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, null, documentType, state); assertThat(result.collect(Collectors.toList()), contains(instanceOf(CaseDocumentUploadRejected.class))); } @@ -60,7 +85,7 @@ public void uploadCaseDocument_whenCaseManagedByAtcm_shouldReturnCaseDocumentUpl when(state.hasGrantedApplication()).thenReturn(true); - Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, documentType, state); + Stream result = CaseDocumentHandler.INSTANCE.uploadCaseDocument(caseId, documentReference, null, documentType, state); assertThat(result.collect(Collectors.toList()), contains(instanceOf(CaseDocumentUploaded.class))); } diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentAddedMutatorTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentAddedMutatorTest.java index 5e9052de03..9e2b97c08f 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentAddedMutatorTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentAddedMutatorTest.java @@ -17,7 +17,7 @@ public class CaseDocumentAddedMutatorTest { public void shouldAddCaseDocument() { UUID documentId = UUID.randomUUID(); String documentType = "pdf"; - CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now()); + CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now(), null); CaseDocumentAdded event = new CaseDocumentAdded(UUID.randomUUID(), caseDocument, 0); CaseAggregateState state = new CaseAggregateState(); diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentDeletedMutatorTest.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentDeletedMutatorTest.java index 390e909af5..c3a557af1c 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentDeletedMutatorTest.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/aggregate/mutator/CaseDocumentDeletedMutatorTest.java @@ -18,7 +18,7 @@ public class CaseDocumentDeletedMutatorTest { public void shouldDeleteCaseDocumentWhenNoDocumentsPreviousSaved() { UUID documentId = UUID.randomUUID(); String documentType = "pdf"; - CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now()); + CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now(), null); CaseDocumentDeleted caseDocumentDeletedEvent = new CaseDocumentDeleted(UUID.randomUUID(), caseDocument); CaseAggregateState state = new CaseAggregateState(); @@ -33,7 +33,7 @@ public void shouldDeleteCaseDocumentWhenNoDocumentsPreviousSaved() { public void shouldDeleteCaseDocument() { UUID documentId = UUID.randomUUID(); String documentType = "pdf"; - CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now()); + CaseDocument caseDocument = new CaseDocument(documentId, UUID.randomUUID(), documentType, ZonedDateTime.now(), null); CaseDocumentAdded caseDocumentAddedEvent = new CaseDocumentAdded(UUID.randomUUID(), caseDocument, 0); CaseDocumentDeleted caseDocumentDeletedEvent = new CaseDocumentDeleted(UUID.randomUUID(), caseDocument); diff --git a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/testutils/CaseDocumentBuilder.java b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/testutils/CaseDocumentBuilder.java index 4b47b6bc2d..6a567766ac 100644 --- a/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/testutils/CaseDocumentBuilder.java +++ b/sjp-domain/sjp-domain-aggregate/src/test/java/uk/gov/moj/cpp/sjp/domain/testutils/CaseDocumentBuilder.java @@ -13,13 +13,15 @@ public class CaseDocumentBuilder { private UUID id = DefaultTestData.CASE_DOCUMENT_ID; private UUID materialId = DefaultTestData.CASE_DOCUMENT_MATERIAL_ID; + private String documentUri; public static CaseDocument defaultCaseDocument() { return new CaseDocument( DefaultTestData.CASE_DOCUMENT_ID, DefaultTestData.CASE_DOCUMENT_MATERIAL_ID, DefaultTestData.CASE_DOCUMENT_TYPE_SJPN, - clock.now() + clock.now(), + null ); } @@ -30,7 +32,12 @@ public static CaseDocumentBuilder aCaseDocument() { return new CaseDocumentBuilder(); } + public CaseDocumentBuilder withDocumentUri(String documentUri) { + this.documentUri = documentUri; + return this; + } + public CaseDocument build() { - return new CaseDocument(id, materialId, null, clock.now()); + return new CaseDocument(id, materialId, null, clock.now(), documentUri); } } diff --git a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/domain/CaseDocument.java b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/domain/CaseDocument.java index 8808a2ea80..fe10a2875c 100644 --- a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/domain/CaseDocument.java +++ b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/domain/CaseDocument.java @@ -21,11 +21,21 @@ public class CaseDocument implements Serializable { private final ZonedDateTime addedAt; - public CaseDocument(UUID id, UUID materialId, String documentType, ZonedDateTime addedAt) { + /** + * Blob uri the document was filed from, or null when it was addressed by its file service + * {@link #id} instead. + * + *

Absent from events recorded before blob addressing existed; those replay with this field + * null, exactly as before. + */ + private final String documentUri; + + public CaseDocument(UUID id, UUID materialId, String documentType, ZonedDateTime addedAt, String documentUri) { this.id = id; this.materialId = materialId; this.documentType = documentType; this.addedAt = addedAt; + this.documentUri = documentUri; } public UUID getId() { @@ -52,4 +62,8 @@ public ZonedDateTime getAddedAt() { return addedAt; } + public String getDocumentUri() { + return documentUri; + } + } \ No newline at end of file diff --git a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentAlreadyExists.java b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentAlreadyExists.java index 650d32bf60..06e4735ca7 100644 --- a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentAlreadyExists.java +++ b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentAlreadyExists.java @@ -9,19 +9,47 @@ @Event("sjp.events.case-document-addition-failed") public class CaseDocumentAlreadyExists { + /** + * Case the document was being filed against. Always populated for events raised from this + * version onward; absent from events recorded before it, which replay with this field null. + */ + private final UUID caseId; + private final UUID documentId; + + /** + * Blob uri the rejected document was filed from, or null when it was addressed by its file + * service id alone. Unlike {@link CaseDocumentUploadRejected}, this is an optional extra rather + * than an alternative: {@code documentId} is always populated here, because a blob-addressed + * document's id is derived from its uri before the command is sent. + * + *

Absent from events recorded before blob addressing existed; those replay with this field + * null, exactly as before. + */ + private final String documentUri; + private final String description; @JsonCreator - public CaseDocumentAlreadyExists(UUID documentId, String description) { + public CaseDocumentAlreadyExists(UUID caseId, UUID documentId, String documentUri, String description) { + this.caseId = caseId; this.documentId = documentId; + this.documentUri = documentUri; this.description = description; } + public UUID getCaseId() { + return caseId; + } + public UUID getDocumentId() { return documentId; } + public String getDocumentUri() { + return documentUri; + } + public String getDescription() { return description; } diff --git a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploadRejected.java b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploadRejected.java index 230d95a381..e673032fdc 100644 --- a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploadRejected.java +++ b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploadRejected.java @@ -8,12 +8,27 @@ @Event("sjp.events.case-document-upload-rejected") public class CaseDocumentUploadRejected { + /** + * File service id of the rejected document. Null when it was addressed by + * {@link #documentReferenceUri} instead - exactly one of the two is set. + */ private final UUID documentId; + + /** + * Blob uri of the rejected document. Null when it was addressed by {@link #documentId} + * instead - exactly one of the two is set. + * + *

Absent from events recorded before blob addressing existed; those replay with this field + * null and {@code documentId} populated, exactly as before. + */ + private final String documentReferenceUri; + private final String description; @JsonCreator - public CaseDocumentUploadRejected(UUID documentId, String description) { + public CaseDocumentUploadRejected(UUID documentId, String documentReferenceUri, String description) { this.documentId = documentId; + this.documentReferenceUri = documentReferenceUri; this.description = description; } @@ -21,6 +36,10 @@ public UUID getDocumentId() { return documentId; } + public String getDocumentReferenceUri() { + return documentReferenceUri; + } + public String getDescription() { return description; } diff --git a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploaded.java b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploaded.java index 2695a05533..9d663cf7b9 100644 --- a/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploaded.java +++ b/sjp-domain/sjp-domain-event/src/main/java/uk/gov/moj/cpp/sjp/event/CaseDocumentUploaded.java @@ -11,14 +11,28 @@ public class CaseDocumentUploaded { private final UUID caseId; + /** + * File service id of the document. Null when the document is addressed by + * {@link #documentReferenceUri} instead - exactly one of the two is set. + */ private final UUID documentReference; + /** + * Blob uri of the document. Null when the document is addressed by + * {@link #documentReference} instead - exactly one of the two is set. + * + *

Absent from events recorded before blob addressing existed; those replay with this field + * null and {@code documentReference} populated, exactly as before. + */ + private final String documentReferenceUri; + private final String documentType; @JsonCreator - public CaseDocumentUploaded(UUID caseId, UUID documentReference, String documentType) { + public CaseDocumentUploaded(UUID caseId, UUID documentReference, String documentReferenceUri, String documentType) { this.caseId = caseId; this.documentReference = documentReference; + this.documentReferenceUri = documentReferenceUri; this.documentType = documentType; } @@ -30,6 +44,10 @@ public UUID getDocumentReference() { return documentReference; } + public String getDocumentReferenceUri() { + return documentReferenceUri; + } + public String getDocumentType() { return documentType; } diff --git a/sjp-event/sjp-event-listener/src/main/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocument.java b/sjp-event/sjp-event-listener/src/main/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocument.java index fb4ff78d5d..b1984adaab 100644 --- a/sjp-event/sjp-event-listener/src/main/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocument.java +++ b/sjp-event/sjp-event-listener/src/main/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocument.java @@ -13,6 +13,7 @@ public CaseDocument convert(CaseDocumentAdded source) { return new CaseDocument(caseDocument.getId(), caseDocument.getMaterialId(), caseDocument.getDocumentType(), - caseDocument.getAddedAt(), source.getCaseId(), source.getIndexWithinDocumentType()); + caseDocument.getAddedAt(), source.getCaseId(), source.getIndexWithinDocumentType(), + caseDocument.getDocumentUri()); } } diff --git a/sjp-event/sjp-event-listener/src/test/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocumentTest.java b/sjp-event/sjp-event-listener/src/test/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocumentTest.java index e72ec53bc4..b1469f21d3 100644 --- a/sjp-event/sjp-event-listener/src/test/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocumentTest.java +++ b/sjp-event/sjp-event-listener/src/test/java/uk/gov/moj/cpp/sjp/event/listener/converter/CaseDocumentAddedToCaseDocumentTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import uk.gov.justice.services.common.util.Clock; @@ -27,9 +28,11 @@ public class CaseDocumentAddedToCaseDocumentTest { private UUID caseDocId = UUID.randomUUID(); private UUID caseDocMaterialId = UUID.randomUUID(); + private static final String DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf"; + @BeforeEach public void setup() { - CaseDocument caseDocument = new CaseDocument(caseDocId, caseDocMaterialId, "SJPN", clock.now()); + CaseDocument caseDocument = new CaseDocument(caseDocId, caseDocMaterialId, "SJPN", clock.now(), DOCUMENT_URI); converter = new CaseDocumentAddedToCaseDocument(); event = new CaseDocumentAdded(caseId, caseDocument, 1); @@ -41,6 +44,17 @@ public void shouldConvertCaseDocumentEventToCaseDocument() { assertThat(caseDocument, is(notNullValue())); assertThat(caseDocument.getDocumentNumber(), is(1)); + assertThat(caseDocument.getDocumentUri(), is(DOCUMENT_URI)); + } + + @Test + public void shouldLeaveDocumentUriNullForAFileServiceAddressedDocument() { + final CaseDocument fileServiceAddressed = new CaseDocument(caseDocId, caseDocMaterialId, "SJPN", clock.now(), null); + + final uk.gov.moj.cpp.sjp.persistence.entity.CaseDocument caseDocument = + converter.convert(new CaseDocumentAdded(caseId, fileServiceAddressed, 1)); + + assertThat(caseDocument.getDocumentUri(), is(nullValue())); } } diff --git a/sjp-event/sjp-event-listener/src/yaml/json/schema/sjp.events.case-document-added.json b/sjp-event/sjp-event-listener/src/yaml/json/schema/sjp.events.case-document-added.json index c538e1e429..4726b14359 100644 --- a/sjp-event/sjp-event-listener/src/yaml/json/schema/sjp.events.case-document-added.json +++ b/sjp-event/sjp-event-listener/src/yaml/json/schema/sjp.events.case-document-added.json @@ -21,6 +21,10 @@ }, "addedAt": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "additionalProperties": true, diff --git a/sjp-event/sjp-event-listener/src/yaml/json/sjp.events.case-document-added.json b/sjp-event/sjp-event-listener/src/yaml/json/sjp.events.case-document-added.json index ce860096e4..8f8e7c71f9 100644 --- a/sjp-event/sjp-event-listener/src/yaml/json/sjp.events.case-document-added.json +++ b/sjp-event/sjp-event-listener/src/yaml/json/sjp.events.case-document-added.json @@ -4,6 +4,7 @@ "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", "addedAt": "2016-11-16T11:22:58.319Z", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf", "indexWithinDocumentType": 1 } } diff --git a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentProcessor.java b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentProcessor.java index 2f9df8917d..8347fcf999 100644 --- a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentProcessor.java +++ b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentProcessor.java @@ -1,12 +1,22 @@ package uk.gov.moj.cpp.sjp.event.processor; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +import static java.util.UUID.nameUUIDFromBytes; import static java.util.UUID.randomUUID; import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; import static uk.gov.justice.services.messaging.JsonEnvelope.metadataFrom; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.CASE_ID; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_ID; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_REFERENCE; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_REFERENCE_URI; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_TYPE; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_URI; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.FILE_SERVICE_ID; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.FILE_URI; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.ID; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.MATERIAL_ID; import uk.gov.justice.services.core.annotation.Component; @@ -23,6 +33,7 @@ import javax.inject.Inject; import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,32 +59,45 @@ public class CaseDocumentProcessor { public void handleCaseDocumentUploaded(final JsonEnvelope caseDocumentUploadedEvent) { final JsonObject payload = caseDocumentUploadedEvent.payloadAsJsonObject(); final UUID caseId = UUID.fromString(payload.getString(CASE_ID)); - final UUID documentReference = UUID.fromString(payload.getString(DOCUMENT_REFERENCE)); final String documentType = payload.getString(DOCUMENT_TYPE); - final JsonObject fileUploadedEventPayload = createObjectBuilder() - .add("documentId", documentReference.toString()) - .add("caseId", caseId.toString()) - .build(); + // Exactly one of these is set - the event schema's oneOf enforces that, and + // JsonSchemaValidationInterceptor applies it on the way in. A blob-addressed document is + // forwarded onward as a uri; SJP never reads the document itself either way. + final String documentReference = valueOrNull(payload, DOCUMENT_REFERENCE); + final String documentReferenceUri = valueOrNull(payload, DOCUMENT_REFERENCE_URI); + final boolean addressedByUri = isNull(documentReference); + + final JsonObjectBuilder fileUploadedEventPayload = createObjectBuilder() + .add(CASE_ID, caseId.toString()); + final JsonObjectBuilder uploadFilePayload = createObjectBuilder() + .add(MATERIAL_ID, randomUUID().toString()); + final JsonObjectBuilder sjpMetadata = createObjectBuilder() + .add(CASE_ID, caseId.toString()) + .add(DOCUMENT_TYPE, documentType); + + if (addressedByUri) { + fileUploadedEventPayload.add(DOCUMENT_URI, documentReferenceUri); + // Material rejects a command carrying more than one file reference, so send only this one. + uploadFilePayload.add(FILE_URI, documentReferenceUri); + sjpMetadata.add(DOCUMENT_URI, documentReferenceUri); + } else { + fileUploadedEventPayload.add(DOCUMENT_ID, documentReference); + uploadFilePayload.add(FILE_SERVICE_ID, documentReference); + sjpMetadata.add(DOCUMENT_ID, documentReference); + } sender.send(enveloper.withMetadataFrom(caseDocumentUploadedEvent, "public.sjp.case-document-uploaded") - .apply(fileUploadedEventPayload)); - - final JsonObject uploadFilePayload = createObjectBuilder() - .add("materialId", randomUUID().toString()) - .add("fileServiceId", documentReference.toString()) - .build(); - - final JsonObject sjpMetadata = createObjectBuilder() - .add("caseId", caseId.toString()) - .add("documentId", documentReference.toString()) - .add("documentType", documentType) - .build(); + .apply(fileUploadedEventPayload.build())); sender.send(metadataHelper.envelopeWithCustomMetadata( metadataFrom(caseDocumentUploadedEvent.metadata()).withName("material.command.upload-file").build(), - sjpMetadata, - uploadFilePayload)); + sjpMetadata.build(), + uploadFilePayload.build())); + } + + private static String valueOrNull(final JsonObject payload, final String field) { + return payload.containsKey(field) && !payload.isNull(field) ? payload.getString(field) : null; } @Handles("sjp.events.case-document-upload-rejected") @@ -81,8 +105,8 @@ public void handleCaseDocumentUploadRejected(final JsonEnvelope uploadRejectedEv sender.send(envelopeFrom( metadataFrom(uploadRejectedEvent.metadata()) .withName("public.sjp.events.case-document-upload-rejected"), - uploadRejectedEvent.payloadAsJsonObject() - )); + uploadRejectedEvent.payloadAsJsonObject() + )); } @Handles("material.material-added") @@ -92,20 +116,33 @@ public void handleMaterialAdded(final JsonEnvelope materialAddedEvent) { final Optional sjpProcessId = metadataHelper.getSjpProcessId(materialAddedEvent); if (sjpMetadata.isPresent()) { - final UUID caseId = UUID.fromString(sjpMetadata.get().getString("caseId")); - final UUID documentId = UUID.fromString(sjpMetadata.get().getString("documentId")); - final String documentType = sjpMetadata.get().getString("documentType"); + final UUID caseId = UUID.fromString(sjpMetadata.get().getString(CASE_ID)); + final String documentType = sjpMetadata.get().getString(DOCUMENT_TYPE); + final String documentId = valueOrNull(sjpMetadata.get(), DOCUMENT_ID); + final String documentUri = valueOrNull(sjpMetadata.get(), DOCUMENT_URI); LOGGER.info("Material {} is a {} for sjp case {}", materialId, documentType, caseId); - final JsonObject payload = createObjectBuilder() - .add("id", documentId.toString()) - .add("caseId", caseId.toString()) - .add("materialId", materialId.toString()) - .add("documentType", documentType) - .build(); - - sender.send(enveloper.withMetadataFrom(materialAddedEvent, "sjp.command.add-case-document").apply(payload)); + // A blob-addressed document has no file service id to become the case document's + // identity, and case_document.id is a uuid primary key. Derive a stable v3 uuid from + // the blob uri: the same uri always yields the same id, so a redelivered + // material.material-added is caught by the aggregate's duplicate check exactly as it is + // on the file-service path. + final String caseDocumentId = getCaseDocumentId(documentId, documentUri); + + final JsonObjectBuilder payload = createObjectBuilder() + .add(ID, caseDocumentId) + .add(CASE_ID, caseId.toString()) + .add(MATERIAL_ID, materialId.toString()) + .add(DOCUMENT_TYPE, documentType); + + // Carried onward so the case document records where it came from, and so the calling + // context can correlate the filing back to the blob it supplied. + if (nonNull(documentUri)) { + payload.add(DOCUMENT_URI, documentUri); + } + + sender.send(enveloper.withMetadataFrom(materialAddedEvent, "sjp.command.add-case-document").apply(payload.build())); } //TODO remove - ATCM-4293 else if (sjpProcessId.isPresent()) { @@ -115,4 +152,8 @@ else if (sjpProcessId.isPresent()) { LOGGER.info("Material {} is not for sjp", materialId); } } + + private static String getCaseDocumentId(final String documentId, final String documentUri) { + return nonNull(documentId) ? documentId : nameUUIDFromBytes(documentUri.getBytes(UTF_8)).toString(); + } } diff --git a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessor.java b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessor.java index de81dce659..44cf9f25fd 100644 --- a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessor.java +++ b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessor.java @@ -29,6 +29,7 @@ public class CaseDocumentUpdatedProcessor { private static final String PUBLIC_CASE_DOCUMENT_ALREADY_ADDED_PUBLIC_EVENT = "public.sjp.case-document-already-exists"; private static final String PUBLIC_CASE_DOCUMENT_ADDED_PUBLIC_EVENT = "public.sjp.case-document-added"; + private static final String PUBLIC_CASE_DOCUMENT_ADDITION_FAILED_PUBLIC_EVENT = "public.sjp.case-document-addition-failed"; @Handles(CaseDocumentAdded.EVENT_NAME) public void handleCaseDocumentAdded(final JsonEnvelope jsonEnvelope) { @@ -48,6 +49,17 @@ public void handleDuplicateCaseDocumentAddedEvent(final JsonEnvelope jsonEnvelop sender.send(enveloper.withMetadataFrom(jsonEnvelope, PUBLIC_CASE_DOCUMENT_ALREADY_ADDED_PUBLIC_EVENT).apply(publicEventPayload)); } + /** + * Promotes the duplicate-filing rejection so a calling context learns that its document was + * already on the case. The payload passes through unchanged - it is flat, so there is nothing + * to lift out of a nested object. + */ + @Handles("sjp.events.case-document-addition-failed") + public void handleCaseDocumentAdditionFailed(final JsonEnvelope jsonEnvelope) { + sender.send(enveloper.withMetadataFrom(jsonEnvelope, PUBLIC_CASE_DOCUMENT_ADDITION_FAILED_PUBLIC_EVENT) + .apply(jsonEnvelope.payloadAsJsonObject())); + } + private JsonObject getCaseDocumentPublicEventPayload(String caseId, JsonObject caseDocument) { final JsonObjectBuilder jsonObjectBuilder = createObjectBuilder() .add(EventProcessorConstants.CASE_ID, caseId) @@ -58,6 +70,12 @@ private JsonObject getCaseDocumentPublicEventPayload(String caseId, JsonObject c jsonObjectBuilder.add(EventProcessorConstants.DOCUMENT_TYPE, caseDocument.getString(EventProcessorConstants.DOCUMENT_TYPE)); } + // Present only for a blob-addressed document. The framework serialises with NON_ABSENT, so + // the key is absent rather than null on the file-service path. + if (caseDocument.containsKey(EventProcessorConstants.DOCUMENT_URI)) { + jsonObjectBuilder.add(EventProcessorConstants.DOCUMENT_URI, caseDocument.getString(EventProcessorConstants.DOCUMENT_URI)); + } + return jsonObjectBuilder.build(); } diff --git a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/EventProcessorConstants.java b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/EventProcessorConstants.java index 16be8e0035..4965412c42 100644 --- a/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/EventProcessorConstants.java +++ b/sjp-event/sjp-event-processor/src/main/java/uk/gov/moj/cpp/sjp/event/processor/EventProcessorConstants.java @@ -16,8 +16,14 @@ public class EventProcessorConstants { public static final String PLEAD_DATE = "pleadDate"; public static final String POSTING_DATE = "postingDate"; public static final String CASE_DOCUMENT = "caseDocument"; + public static final String CASE_DOCUMENT_URI = "caseDocumentUri"; public static final String MATERIAL_ID = "materialId"; public static final String DOCUMENT_REFERENCE = "documentReference"; + public static final String DOCUMENT_REFERENCE_URI = "documentReferenceUri"; + public static final String DOCUMENT_ID = "documentId"; + public static final String DOCUMENT_URI = "documentUri"; + public static final String FILE_SERVICE_ID = "fileServiceId"; + public static final String FILE_URI = "fileUri"; public static final String DOCUMENT_TYPE = "documentType"; public static final String REASON = "reason"; public static final String MARKED_AT = "markedAt"; diff --git a/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessorTest.java b/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessorTest.java index e24bcc0f3e..97a7fbefb8 100644 --- a/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessorTest.java +++ b/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUpdatedProcessorTest.java @@ -12,6 +12,7 @@ import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.CASE_DOCUMENT; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.CASE_ID; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_TYPE; +import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.DOCUMENT_URI; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.ID; import static uk.gov.moj.cpp.sjp.event.processor.EventProcessorConstants.MATERIAL_ID; @@ -39,12 +40,14 @@ public class CaseDocumentUpdatedProcessorTest { private static final String PUBLIC_CASE_DOCUMENT_ADDED_PUBLIC_EVENT = "public.sjp.case-document-added"; private static final String PUBLIC_CASE_DOCUMENT_ALREADY_EXISTS_PUBLIC_EVENT = "public.sjp.case-document-already-exists"; + private static final String PUBLIC_CASE_DOCUMENT_ADDITION_FAILED_PUBLIC_EVENT = "public.sjp.case-document-addition-failed"; private static final String VALUE_CASE_ID = UUID.randomUUID().toString(); private static final String VALUE_MATERIAL_ID = UUID.randomUUID().toString(); private static final String VALUE_CASE_DOCUMENT_ID = UUID.randomUUID().toString(); private static final String VALUE_USER_ID = UUID.randomUUID().toString(); private static final String VALUE_DOCUMENT_TYPE = "SJPN"; + private static final String VALUE_DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf"; @InjectMocks private CaseDocumentUpdatedProcessor caseDocumentUpdatedListener; @@ -113,7 +116,66 @@ public void shouldRaisePublicEventForCaseDocumentAlreadyAdded() { assertThat(payload.getString(MATERIAL_ID), is(VALUE_MATERIAL_ID)); } + @Test + public void shouldEchoDocumentUriOnThePublicEventWhenDocumentIsBlobAddressed() { + final JsonEnvelope privateEnvelope = buildCaseDocumentAddedEvent( + Optional.of(VALUE_DOCUMENT_TYPE), Optional.of(VALUE_DOCUMENT_URI), PUBLIC_CASE_DOCUMENT_ADDED_PUBLIC_EVENT); + + caseDocumentUpdatedListener.handleCaseDocumentAdded(privateEnvelope); + + verify(sender).send(envelopeCaptor.capture()); + + final JsonObject payload = envelopeCaptor.getValue().payloadAsJsonObject(); + assertThat(payload.getString(DOCUMENT_URI), is(VALUE_DOCUMENT_URI)); + assertThat(payload.getString(ID), is(VALUE_CASE_DOCUMENT_ID)); + } + + @Test + public void shouldOmitDocumentUriKeyEntirelyWhenDocumentIsFileServiceAddressed() { + final JsonEnvelope privateEnvelope = buildCaseDocumentAddedEvent( + Optional.of(VALUE_DOCUMENT_TYPE), PUBLIC_CASE_DOCUMENT_ADDED_PUBLIC_EVENT); + + caseDocumentUpdatedListener.handleCaseDocumentAdded(privateEnvelope); + + verify(sender).send(envelopeCaptor.capture()); + + // Absent, not null - the framework serialises with NON_ABSENT and the public schema types + // documentUri as a string, so a null would fail validation downstream. + assertFalse(envelopeCaptor.getValue().payloadAsJsonObject().containsKey(DOCUMENT_URI)); + } + + @Test + public void shouldPromoteCaseDocumentAdditionFailedToAPublicEvent() { + final JsonObject privatePayload = createObjectBuilder() + .add(CASE_ID, VALUE_CASE_ID) + .add("documentId", VALUE_CASE_DOCUMENT_ID) + .add(DOCUMENT_URI, VALUE_DOCUMENT_URI) + .add("description", "Add Case Document") + .build(); + + final JsonEnvelope privateEnvelope = envelopeFrom( + metadataWithRandomUUID("sjp.events.case-document-addition-failed").withUserId(VALUE_USER_ID), + privatePayload); + + caseDocumentUpdatedListener.handleCaseDocumentAdditionFailed(privateEnvelope); + + verify(sender).send(envelopeCaptor.capture()); + + final JsonEnvelope publicEvent = envelopeCaptor.getValue(); + assertThat(publicEvent.metadata(), + withMetadataEnvelopedFrom(privateEnvelope).withName(PUBLIC_CASE_DOCUMENT_ADDITION_FAILED_PUBLIC_EVENT)); + + // Republished verbatim - the payload is already flat, so nothing is lifted or dropped. + // caseId rides along on that pass-through; it is the key the calling context joins on. + assertThat(publicEvent.payloadAsJsonObject(), is(privatePayload)); + assertThat(publicEvent.payloadAsJsonObject().getString(CASE_ID), is(VALUE_CASE_ID)); + } + private JsonEnvelope buildCaseDocumentAddedEvent(final Optional documentType, String eventName) { + return buildCaseDocumentAddedEvent(documentType, Optional.empty(), eventName); + } + + private JsonEnvelope buildCaseDocumentAddedEvent(final Optional documentType, final Optional documentUri, String eventName) { final JsonObjectBuilder documentBuilder = createObjectBuilder() .add(ID, VALUE_CASE_DOCUMENT_ID) .add(MATERIAL_ID, VALUE_MATERIAL_ID); @@ -122,6 +184,10 @@ private JsonEnvelope buildCaseDocumentAddedEvent(final Optional document documentBuilder.add(DOCUMENT_TYPE, VALUE_DOCUMENT_TYPE); } + if (documentUri.isPresent()) { + documentBuilder.add(DOCUMENT_URI, documentUri.get()); + } + return envelopeFrom( metadataWithRandomUUID(eventName) .withUserId(VALUE_USER_ID), diff --git a/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUploadedProcessorTest.java b/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUploadedProcessorTest.java index 44e52e9496..5e4ea60607 100644 --- a/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUploadedProcessorTest.java +++ b/sjp-event/sjp-event-processor/src/test/java/uk/gov/moj/cpp/sjp/event/processor/CaseDocumentUploadedProcessorTest.java @@ -2,6 +2,8 @@ import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.UUID.nameUUIDFromBytes; import static java.util.UUID.randomUUID; import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.hamcrest.MatcherAssert.assertThat; @@ -52,6 +54,7 @@ public class CaseDocumentUploadedProcessorTest { private static final String DOCUMENT_TYPE = "PLEA"; + private static final String DOCUMENT_URI = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/doc.pdf"; private final UUID caseId = randomUUID(); private final UUID documentReference = randomUUID(); private final UUID materialId = randomUUID(); @@ -169,6 +172,126 @@ public void shouldIgnoreMaterialAddedEventNotInitiatedBySjp() { verify(sjpProcessManagerService, never()).signalUploadFileProcess(any(), any(), any()); } + @Test + public void shouldForwardTheUriToMaterialWhenDocumentIsBlobAddressed() { + final String documentUri = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/doc.pdf"; + final JsonObject payload = createObjectBuilder() + .add("caseId", caseId.toString()) + .add("documentReferenceUri", documentUri) + .add("documentType", DOCUMENT_TYPE).build(); + + caseDocumentProcessor.handleCaseDocumentUploaded(createEnvelope("sjp.events.case-document-uploaded", payload)); + + verify(sender, times(2)).send(envelopeCaptor.capture()); + final List sent = envelopeCaptor.getAllValues(); + + // public event carries the uri variant, not documentId + assertThat(sent.get(0).payloadAsJsonObject().toString(), isJson(allOf( + withJsonPath("$.caseId", equalTo(caseId.toString())), + withJsonPath("$.documentUri", equalTo(documentUri))))); + assertThat(sent.get(0).payloadAsJsonObject().containsKey("documentId"), is(false)); + + // Material gets fileUri and, crucially, NOT fileServiceId - it rejects a command + // carrying more than one file reference. + assertThat(sent.get(1).metadata().name(), is("material.command.upload-file")); + assertThat(sent.get(1).payloadAsJsonObject().toString(), isJson(allOf( + withJsonPath("$.materialId", notNullValue()), + withJsonPath("$.fileUri", equalTo(documentUri))))); + assertThat(sent.get(1).payloadAsJsonObject().containsKey("fileServiceId"), is(false)); + } + + @Test + public void shouldAddCaseDocumentForABlobAddressedDocumentWithAnIdDerivedFromTheUri() { + // A blob-addressed document has no file service id, and case_document.id is a uuid primary + // key, so the id is derived from the uri. The uri rides along so the case document records + // where it came from and the calling context can correlate the filing. + caseDocumentProcessor.handleMaterialAdded(blobAddressedMaterialAdded()); + + verify(sender).send(envelopeCaptor.capture()); + + final JsonEnvelope command = envelopeCaptor.getValue(); + assertThat(command.metadata().name(), is("sjp.command.add-case-document")); + assertThat(command.payloadAsJsonObject().toString(), isJson(allOf( + withJsonPath("$.id", equalTo(nameUUIDFromBytes(DOCUMENT_URI.getBytes(UTF_8)).toString())), + withJsonPath("$.caseId", equalTo(caseId.toString())), + withJsonPath("$.materialId", equalTo(materialId.toString())), + withJsonPath("$.documentUri", equalTo(DOCUMENT_URI))))); + } + + @Test + public void shouldDeriveTheSameIdEveryTimeSoARedeliveryIsCaughtAsADuplicate() { + caseDocumentProcessor.handleMaterialAdded(blobAddressedMaterialAdded()); + caseDocumentProcessor.handleMaterialAdded(blobAddressedMaterialAdded()); + + verify(sender, times(2)).send(envelopeCaptor.capture()); + final List sent = envelopeCaptor.getAllValues(); + + // Same uri, same id - so the aggregate's containsKey check rejects the second filing + // exactly as it does on the file-service path. + assertThat(sent.get(1).payloadAsJsonObject().getString("id"), + is(sent.get(0).payloadAsJsonObject().getString("id"))); + } + + @Test + public void shouldNotCarryDocumentUriForAFileServiceAddressedDocument() { + final Metadata enriched = metadataFrom( + JsonObjects.createObjectBuilder(materialAddedMetadata.asJsonObject()) + .add("sjpMetadata", createObjectBuilder() + .add("caseId", caseId.toString()) + .add("documentId", documentReference.toString()) + .add("documentType", DOCUMENT_TYPE) + .build()).build()) + .build(); + + caseDocumentProcessor.handleMaterialAdded(envelopeFrom(enriched, materialAddedPayload)); + + verify(sender).send(envelopeCaptor.capture()); + assertThat(envelopeCaptor.getValue().payloadAsJsonObject().containsKey("documentUri"), is(false)); + } + + private JsonEnvelope blobAddressedMaterialAdded() { + final Metadata enriched = metadataFrom( + JsonObjects.createObjectBuilder(materialAddedMetadata.asJsonObject()) + .add("sjpMetadata", createObjectBuilder() + .add("caseId", caseId.toString()) + .add("documentUri", DOCUMENT_URI) + .add("documentType", DOCUMENT_TYPE) + .build()).build()) + .build(); + + return envelopeFrom(enriched, materialAddedPayload); + } + + @Test + public void shouldPromoteCaseDocumentUploadRejectedToAPublicEvent() { + final String description = "Case Document Upload rejected as case is referred to court for hearing"; + final JsonEnvelope privateEvent = createEnvelope("sjp.events.case-document-upload-rejected", + createObjectBuilder() + .add("documentId", documentReference.toString()) + .add("description", description) + .build()); + + caseDocumentProcessor.handleCaseDocumentUploadRejected(privateEvent); + + verify(sender).send(envelopeCaptor.capture()); + final JsonEnvelope publicEvent = envelopeCaptor.getValue(); + + assertThat(publicEvent.metadata().name(), is("public.sjp.events.case-document-upload-rejected")); + + // Republished verbatim: the payload passes through untouched. + assertThat(publicEvent.payloadAsJsonObject().toString(), isJson(allOf( + withJsonPath("$.documentId", equalTo(documentReference.toString())), + withJsonPath("$.description", equalTo(description))))); + + // Characterising current behaviour, which differs from the sibling promotion in + // CaseDocumentUpdatedProcessor: this handler copies metadata with + // JsonEnvelope.metadataFrom rather than Enveloper.withMetadataFrom, so the public event + // reuses the private event's message id and carries no causation chain. If that is ever + // brought into line with the other processor, these two assertions are the ones to update. + assertThat(publicEvent.metadata().id(), is(privateEvent.metadata().id())); + assertThat(publicEvent.metadata().causation().isEmpty(), is(true)); + } + private JsonEnvelope prepareCaseDocumentUploadedEnvelope(final UUID caseId, final UUID documentReference, final String documentType) { JsonObject payload = createObjectBuilder() .add("caseId", caseId.toString()) diff --git a/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-added.json b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-added.json index ceefaf2112..21e2ca8130 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-added.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-added.json @@ -2,5 +2,6 @@ "caseId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd2", "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", - "documentType": "SJPN" + "documentType": "SJPN", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf" } \ No newline at end of file diff --git a/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-addition-failed.json b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-addition-failed.json new file mode 100644 index 0000000000..96acf74f9f --- /dev/null +++ b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.case-document-addition-failed.json @@ -0,0 +1,6 @@ +{ + "caseId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd2", + "documentId": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf", + "description": "Add Case Document" +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.events.case-document-upload-rejected.json b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.events.case-document-upload-rejected.json index 7ad43a6918..f9aae1f7ee 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.events.case-document-upload-rejected.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/public.sjp.events.case-document-upload-rejected.json @@ -1,4 +1,4 @@ { - "documentId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd1", - "description": "Case Document Upload rejected as case is referred to court for hearing: 6ac98e9c-66b2-4363-8a78-641dbec8bbd1" -} \ No newline at end of file + "documentReferenceUri": "https://sadevfilestore.blob.core.windows.net/steccm13-stagingdvla/generated/6ac98e9c-66b2-4363-8a78-641dbec8bbd1.pdf", + "description": "Case Document Upload rejected as case is referred to court for hearing" +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-added.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-added.json index a209b9bab4..b43e583002 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-added.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-added.json @@ -15,6 +15,10 @@ }, "documentType" : { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "required": [ diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-addition-failed.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-addition-failed.json new file mode 100644 index 0000000000..8835286949 --- /dev/null +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-addition-failed.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://cpp.moj.gov.uk/sjp/json/schema/event/public-case-document-addition-failed.json", + "type": "object", + "properties": { + "caseId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentUri": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "required": [ + "documentId", + "description" + ] +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-already-exists.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-already-exists.json index 40deef3321..32af67a05b 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-already-exists.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.case-document-already-exists.json @@ -15,6 +15,10 @@ }, "documentType" : { "enum": ["SJPN", "CITN", "PLEA", "FINANCIAL_MEANS", "WS", "RESULT_ORDER"] + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "required": [ diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.events.case-document-upload-rejected.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.events.case-document-upload-rejected.json index a6a4422cdb..b7b35fa10f 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.events.case-document-upload-rejected.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/public.sjp.events.case-document-upload-rejected.json @@ -3,15 +3,30 @@ "id": "http://justice.gov.uk/json/schemas/domains/sjp/event/public-case-document-upload-rejected.json", "type": "object", "properties": { - "documentId": { - "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" - }, - "description": { - "type": "string" - } + "documentId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentReferenceUri": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } }, "required": [ - "documentId", "description" + ], + "oneOf": [ + { + "required": [ + "documentId" + ] + }, + { + "required": [ + "documentReferenceUri" + ] + } ] -} \ No newline at end of file +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-added.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-added.json index c538e1e429..4726b14359 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-added.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-added.json @@ -21,6 +21,10 @@ }, "addedAt": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "additionalProperties": true, diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-addition-failed.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-addition-failed.json new file mode 100644 index 0000000000..9641b16d3c --- /dev/null +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-addition-failed.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/json/schemas/domains/sjp/event/case-document-addition-failed.json", + "type": "object", + "properties": { + "caseId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentUri": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "required": [ + "documentId", + "description" + ] +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-already-exists.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-already-exists.json index 7deef6171f..542fb1171c 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-already-exists.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-already-exists.json @@ -28,6 +28,10 @@ }, "addedAt": { "type": "string" + }, + "documentUri": { + "type": "string", + "format": "uri" } }, "additionalProperties": false, diff --git a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-upload-rejected.json b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-upload-rejected.json index 685d65246d..3b4da9e94a 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-upload-rejected.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/schema/sjp.events.case-document-upload-rejected.json @@ -3,15 +3,30 @@ "id": "http://justice.gov.uk/json/schemas/domains/sjp/event/case-document-upload-rejected.json", "type": "object", "properties": { - "documentId": { - "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" - }, - "description": { - "type": "string" - } + "documentId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "documentReferenceUri": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } }, "required": [ - "documentId", "description" + ], + "oneOf": [ + { + "required": [ + "documentId" + ] + }, + { + "required": [ + "documentReferenceUri" + ] + } ] -} \ No newline at end of file +} diff --git a/sjp-event/sjp-event-processor/src/yaml/json/sjp.events.case-document-added.json b/sjp-event/sjp-event-processor/src/yaml/json/sjp.events.case-document-added.json index 7fec8ad0e7..ab865783a0 100644 --- a/sjp-event/sjp-event-processor/src/yaml/json/sjp.events.case-document-added.json +++ b/sjp-event/sjp-event-processor/src/yaml/json/sjp.events.case-document-added.json @@ -2,7 +2,8 @@ "caseId": "6ac98e9c-66b2-4363-8a78-641dbec8bbd2", "caseDocument": { "id": "fc3f8d82-d200-4cab-a492-2796e6fdf42c", - "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be" + "materialId": "87a321ef-ea34-47d7-865d-7f430b4775be", + "documentUri": "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/sjpn.pdf" }, "indexWithinDocumentType": 1 } diff --git a/sjp-event/sjp-event-processor/src/yaml/public-publications-descriptor.yaml b/sjp-event/sjp-event-processor/src/yaml/public-publications-descriptor.yaml index fb12a32995..72d0b4c62c 100644 --- a/sjp-event/sjp-event-processor/src/yaml/public-publications-descriptor.yaml +++ b/sjp-event/sjp-event-processor/src/yaml/public-publications-descriptor.yaml @@ -29,6 +29,9 @@ subscriptions_descriptor: - name: public.sjp.case-document-already-exists schema_uri: http://cpp.moj.gov.uk/sjp/json/schema/event/public-case-document-already-exists.json + - name: public.sjp.case-document-addition-failed + schema_uri: http://cpp.moj.gov.uk/sjp/json/schema/event/public-case-document-addition-failed.json + - name: public.sjp.case-created schema_uri: http://cpp.moj.gov.uk/sjp/json/schema/event/public-case-created.json diff --git a/sjp-event/sjp-event-processor/src/yaml/subscriptions-descriptor.yaml b/sjp-event/sjp-event-processor/src/yaml/subscriptions-descriptor.yaml index 0cdee4dae1..42314794f6 100644 --- a/sjp-event/sjp-event-processor/src/yaml/subscriptions-descriptor.yaml +++ b/sjp-event/sjp-event-processor/src/yaml/subscriptions-descriptor.yaml @@ -26,6 +26,9 @@ subscriptions_descriptor: - name: sjp.events.case-document-already-exists schema_uri: http://justice.gov.uk/json/schemas/domains/sjp/event/case-document-already-exists.json + - name: sjp.events.case-document-addition-failed + schema_uri: http://justice.gov.uk/json/schemas/domains/sjp/event/case-document-addition-failed.json + - name: sjp.events.case-document-upload-rejected schema_uri: http://justice.gov.uk/json/schemas/domains/sjp/event/case-document-upload-rejected.json diff --git a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/helper/CaseDocumentHelper.java b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/helper/CaseDocumentHelper.java index 6e73ed2c59..70976ce36d 100644 --- a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/helper/CaseDocumentHelper.java +++ b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/helper/CaseDocumentHelper.java @@ -7,7 +7,10 @@ import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.lang.String.format; import static java.util.UUID.fromString; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.UUID.nameUUIDFromBytes; import static java.util.UUID.randomUUID; +import static javax.json.Json.createObjectBuilder; import static javax.ws.rs.core.Response.Status.OK; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.equalTo; @@ -65,6 +68,7 @@ public class CaseDocumentHelper implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(CaseDocumentHelper.class); private static final String WRITE_MEDIA_TYPE = "application/vnd.sjp.add-case-document+json"; + private static final String UPLOAD_CASE_DOCUMENT_JSON_MEDIA_TYPE = "application/vnd.sjp.upload-case-document+json"; public static final String GET_CASE_DOCUMENTS_MEDIA_TYPE = "application/vnd.sjp.query.case-documents+json"; private static final String TEMPLATE_ADD_CASE_DOCUMENT_PAYLOAD = "payload/sjp.command.add-case-document.json"; @@ -149,6 +153,51 @@ public void uploadCaseDocument(UUID userId, String documentType, String fileName makeMultipartFormPostCall(userId, writeUrl, "caseDocument", request); } + /** + * Uploads via the JSON branch of {@code sjp.upload-case-document}, supplying an existing + * document reference rather than a binary part. + * + *

This is the branch other contexts call - staging-dvla among them - and it behaves + * materially differently from the multipart branch: {@code SjpServiceFileInterceptor} is a + * no-op here, so the reference the caller supplies is the one that reaches the handler and is + * forwarded on to Material. The multipart branch stores the binary itself and substitutes its + * own reference, so it never exercises a caller-supplied one. + * + * @param documentReference the reference the caller is handing to SJP + * @return the same reference, for chaining into the Material stub + */ + public UUID uploadCaseDocumentByReference(final UUID userId, final String documentType, final UUID documentReference) { + final String writeUrl = format("/cases/%s/upload-case-document/%s", caseId, documentType); + final String payload = createObjectBuilder() + .add("caseDocument", documentReference.toString()) + .build() + .toString(); + + makePostCall(userId, writeUrl, UPLOAD_CASE_DOCUMENT_JSON_MEDIA_TYPE, payload, Response.Status.ACCEPTED); + + return documentReference; + } + + /** + * The blob-addressed sibling of {@link #uploadCaseDocumentByReference}: the caller supplies a + * container uri rather than a file service id. SJP never reads the blob - it forwards the uri + * to Material, which performs the only read. + * + * @param documentUri the blob uri the caller is handing to SJP + * @return the same uri, for chaining into the Material stub + */ + public String uploadCaseDocumentByUri(final UUID userId, final String documentType, final String documentUri) { + final String writeUrl = format("/cases/%s/upload-case-document/%s", caseId, documentType); + final String payload = createObjectBuilder() + .add("caseDocumentUri", documentUri) + .build() + .toString(); + + makePostCall(userId, writeUrl, UPLOAD_CASE_DOCUMENT_JSON_MEDIA_TYPE, payload, Response.Status.ACCEPTED); + + return documentUri; + } + public void verifyInPublicTopic() { final String caseDocumentAddedEvent = publicConsumer.retrieveMessage().orElse(null); @@ -160,6 +209,61 @@ public void verifyInPublicTopic() { .assertThat("$.materialId", is(materialId)); } + /** + * Asserts the public completion event for an upload-driven document, where the material id is + * minted by Material rather than seeded by this helper. + * + *

The no-arg {@link #verifyInPublicTopic()} asserts against the helper's own pre-seeded + * {@code materialId}, which only holds for the add-case-document path. + * + *

This is the event a calling context correlates on: {@code id} is the reference it + * supplied and {@code caseId} tells it which case confirmed. + */ + public void verifyInPublicTopic(final UUID expectedDocumentId, final UUID expectedMaterialId) { + final String caseDocumentAddedEvent = publicConsumer.retrieveMessage().orElse(null); + + assertThat(caseDocumentAddedEvent, notNullValue()); + + with(caseDocumentAddedEvent) + .assertThat("$.caseId", is(caseId.toString())) + .assertThat("$.id", is(expectedDocumentId.toString())) + .assertThat("$.materialId", is(expectedMaterialId.toString())); + } + + /** + * The blob-addressed sibling of {@link #verifyInPublicTopic(UUID, UUID)}. The join key here is + * {@code documentUri} - the uri the calling context supplied - because there is no file service + * id to correlate on. {@code id} is derived from that uri, so it is stable but not something + * the caller knew in advance. + */ + public void verifyInPublicTopicForBlobAddressedDocument(final String expectedDocumentUri, final UUID expectedMaterialId) { + final String caseDocumentAddedEvent = publicConsumer.retrieveMessage().orElse(null); + + assertThat(caseDocumentAddedEvent, notNullValue()); + + with(caseDocumentAddedEvent) + .assertThat("$.caseId", is(caseId.toString())) + .assertThat("$.documentUri", is(expectedDocumentUri)) + .assertThat("$.id", is(nameUUIDFromBytes(expectedDocumentUri.getBytes(UTF_8)).toString())) + .assertThat("$.materialId", is(expectedMaterialId.toString())); + } + + /** + * The blob-addressed sibling of {@link #verifyCaseDocumentUploadedEventRaised()}: the public + * upload event carries {@code documentUri} instead of {@code documentId}. + */ + public String verifyCaseDocumentUploadedEventRaisedForUri() { + final String caseDocumentUploadedEvent = publicCaseDocumentUploaded.retrieveMessage().orElse(null); + + assertThat(caseDocumentUploadedEvent, notNullValue()); + + with(caseDocumentUploadedEvent) + .assertThat("$.caseId", isAUuid()) + .assertThat("$.documentUri", notNullValue()); + + return new JsonPath(caseDocumentUploadedEvent).getString("documentUri"); + } + public void verifyUploadRejectedInPublicTopic() { final String caseDocumentUploadRejected = publicConsumerForRejected.retrieveMessage().orElse(null); @@ -286,6 +390,7 @@ public String getDocumentId() { @Override public void close() { publicConsumer.close(); + publicConsumerForRejected.close(); publicCaseDocumentAlreadyExistsConsumer.close(); publicCaseDocumentUploaded.close(); } diff --git a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/stub/MaterialStub.java b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/stub/MaterialStub.java index ad69bbcf78..57c7097cff 100644 --- a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/stub/MaterialStub.java +++ b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/stub/MaterialStub.java @@ -11,6 +11,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.lang.String.format; +import static java.util.Arrays.asList; import static java.util.Optional.empty; import static java.util.UUID.randomUUID; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; @@ -32,6 +33,7 @@ import uk.gov.justice.services.messaging.JsonEnvelope; import java.time.ZonedDateTime; +import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -57,14 +59,38 @@ public static void stubAddCaseMaterial() { .willReturn(aResponse().withStatus(SC_OK))); } + /** + * Fields on {@code material.command.upload-file} that can carry the source document reference. + * + *

Only {@code fileServiceId} is in use today. The list exists so that the BYO-FileStore + * change - which swaps the reference onto a URI-shaped field - fails with a readable assertion + * rather than silently never matching and hanging in the {@code await} below until it times + * out. Add the new field name here at the same time as the producer starts sending it. + */ + private static final List DOCUMENT_REFERENCE_FIELDS = asList("fileServiceId", "fileUri"); + + private static boolean matchesDocumentReference(final JsonEnvelope command, final String documentReference) { + final JsonObject payload = command.payloadAsJsonObject(); + return DOCUMENT_REFERENCE_FIELDS.stream() + .anyMatch(field -> documentReference.equals(payload.getString(field, null))); + } + public static UUID processMaterialAddedCommand(final UUID documentReference) { + return processMaterialAddedCommand(documentReference.toString()); + } + + /** + * Reference-shape-agnostic variant: the reference is a file service uuid on the legacy path and + * a blob uri on the BYO-FileStore path, and Material matches on whichever field carried it. + */ + public static UUID processMaterialAddedCommand(final String documentReference) { final DefaultJsonObjectEnvelopeConverter envelopeConverter = new DefaultJsonObjectEnvelopeConverter(); final JsonEnvelope addMaterialCommand = await().until(() -> findAll(postRequestedFor(urlPathEqualTo(COMMAND_URL)) .withHeader(CONTENT_TYPE, equalTo(COMMAND_MEDIA_TYPE))) .stream() .map(LoggedRequest::getBodyAsString) .map(envelopeConverter::asEnvelope) - .filter(command -> documentReference.toString().equals(command.payloadAsJsonObject().getString("fileServiceId", null))) + .filter(command -> matchesDocumentReference(command, documentReference)) .findFirst(), not(empty())) .get(); diff --git a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/test/AddCaseDocumentIT.java b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/test/AddCaseDocumentIT.java index a945dde980..1f39c58dda 100644 --- a/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/test/AddCaseDocumentIT.java +++ b/sjp-integration-test/src/test/java/uk/gov/moj/sjp/it/test/AddCaseDocumentIT.java @@ -1,9 +1,12 @@ package uk.gov.moj.sjp.it.test; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.UUID.nameUUIDFromBytes; import static java.util.UUID.randomUUID; import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.apache.commons.lang3.RandomUtils.nextInt; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static uk.gov.justice.json.schemas.domains.sjp.User.user; @@ -150,6 +153,64 @@ public void shouldUploadCaseDocument() { } } + /** + * The JSON branch of {@code sjp.upload-case-document}, where the caller supplies an existing + * document reference instead of a binary part. This is the branch other contexts drive, and + * until now it had no integration coverage at all - every other test here posts multipart, + * which never exercises a caller-supplied reference because the file interceptor stores the + * binary and substitutes its own. + * + *

The reference the caller hands in must be the one that reaches Material and the one that + * comes back as the case document's id, which is what makes the public completion event usable + * as a correlation signal by the calling context. + */ + @Test + public void shouldUploadCaseDocumentSuppliedByReferenceOnTheJsonBranch() { + final String documentType = "PLEA"; + final UUID documentReference = randomUUID(); + createCase(); + stubAddCaseMaterial(); + + try (final CaseDocumentHelper caseDocumentHelper = new CaseDocumentHelper(caseId)) { + caseDocumentHelper.uploadCaseDocumentByReference(USER_ID, documentType, documentReference); + + final UUID documentId = caseDocumentHelper.verifyCaseDocumentUploadedEventRaised(); + assertThat(documentId, is(documentReference)); + + final UUID materialId = MaterialStub.processMaterialAddedCommand(documentReference); + CaseDocumentHelper.assertDocumentAdded(USER_ID, caseId, materialId, documentReference, documentType); + + caseDocumentHelper.verifyInPublicTopic(documentReference, materialId); + } + } + + /** + * The blob-addressed end of the same JSON branch: the caller supplies a container uri instead + * of a file service id. The uri must reach Material as {@code fileUri}, and must come back on + * the public completion event - that echo is the only join key a calling context has for a + * blob-addressed filing, and it is what lets staging-dvla release the blob. + */ + @Test + public void shouldUploadCaseDocumentSuppliedByUriOnTheJsonBranch() { + final String documentType = "PLEA"; + final String documentUri = "https://sadevfilestore.blob.core.windows.net/stack-stagingdvla/generated/plea.pdf"; + createCase(); + stubAddCaseMaterial(); + + try (final CaseDocumentHelper caseDocumentHelper = new CaseDocumentHelper(caseId)) { + caseDocumentHelper.uploadCaseDocumentByUri(USER_ID, documentType, documentUri); + + assertThat(caseDocumentHelper.verifyCaseDocumentUploadedEventRaisedForUri(), is(documentUri)); + + final UUID materialId = MaterialStub.processMaterialAddedCommand(documentUri); + final UUID derivedDocumentId = nameUUIDFromBytes(documentUri.getBytes(UTF_8)); + + CaseDocumentHelper.assertDocumentAdded(USER_ID, caseId, materialId, derivedDocumentId, documentType); + + caseDocumentHelper.verifyInPublicTopicForBlobAddressedDocument(documentUri, materialId); + } + } + @Test public void shouldUploadCaseDocumentApplication() { final String documentType = "APPLICATION"; diff --git a/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/CaseDocumentViewTest.java b/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/CaseDocumentViewTest.java index 02db350b95..0285f9e071 100644 --- a/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/CaseDocumentViewTest.java +++ b/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/CaseDocumentViewTest.java @@ -16,11 +16,11 @@ public class CaseDocumentViewTest { public void testOrder() { //given CaseDocumentView caseDocumentViewA2 = new CaseDocumentView( - new CaseDocument(null, null, "A-documentType", null, null, 2)); + new CaseDocument(null, null, "A-documentType", null, null, 2, null)); CaseDocumentView caseDocumentViewB1 = new CaseDocumentView( - new CaseDocument(null, null, "B-documentType", null, null, 1)); + new CaseDocument(null, null, "B-documentType", null, null, 1, null)); CaseDocumentView caseDocumentViewB2 = new CaseDocumentView( - new CaseDocument(null, null, "B-documentType", null, null, 2)); + new CaseDocument(null, null, "B-documentType", null, null, 2, null)); List caseDocumentViews = new ArrayList<>(); caseDocumentViews.add(caseDocumentViewA2); diff --git a/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/service/CaseServiceTest.java b/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/service/CaseServiceTest.java index f4abb4b694..fc664585e9 100644 --- a/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/service/CaseServiceTest.java +++ b/sjp-query/sjp-query-view/src/test/java/uk/gov/moj/cpp/sjp/query/view/service/CaseServiceTest.java @@ -444,7 +444,7 @@ public void shouldReturnNullIfCaseNotFoundSearchCaseByMaterialId() { public void shouldFindCaseDocuments() { final List caseDocumentList = new ArrayList<>(); final UUID documentId = randomUUID(); - final CaseDocument caseDocument = new CaseDocument(documentId, randomUUID(), "SJPN", clock.now(), CASE_ID, 2); + final CaseDocument caseDocument = new CaseDocument(documentId, randomUUID(), "SJPN", clock.now(), CASE_ID, 2, null); caseDocumentList.add(caseDocument); when(caseRepository.findCaseDocuments(CASE_ID)).thenReturn(caseDocumentList); @@ -457,7 +457,7 @@ public void shouldFindCaseDocuments() { @Test public void shouldFindCaseDocument() { - final CaseDocument caseDocument = new CaseDocument(randomUUID(), randomUUID(), "SJPN", clock.now(), CASE_ID, 2); + final CaseDocument caseDocument = new CaseDocument(randomUUID(), randomUUID(), "SJPN", clock.now(), CASE_ID, 2, null); when(caseRepository.findCaseDocuments(CASE_ID)).thenReturn(singletonList(caseDocument)); @@ -673,7 +673,7 @@ public void shouldFindResultOrders() { final CaseDocument caseDocument = new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), CaseDocument.RESULT_ORDER_DOCUMENT_TYPE, - clock.now(), caseDetail.getId(), null); + clock.now(), caseDetail.getId(), null, null); final ZonedDateTime FROM_DATE_TIME = FROM_DATE.atStartOfDay(UTC); final ZonedDateTime TO_DATE_TIME = TO_DATE.atStartOfDay(UTC); @@ -703,7 +703,7 @@ public void shouldNotFindResultOrders() { null, new DefendantDetail(), null, null, null); final CaseDocument caseDocument = new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), CaseDocument.RESULT_ORDER_DOCUMENT_TYPE, - clock.now(), caseDetail.getId(), null); + clock.now(), caseDetail.getId(), null, null); final ZonedDateTime FROM_DATE_TIME = FROM_DATE.atStartOfDay(UTC); final ZonedDateTime TO_DATE_TIME = TO_DATE.atStartOfDay(UTC); diff --git a/sjp-viewstore/sjp-viewstore-liquibase/src/main/resources/liquibase/sjp-view-store-db-changesets/094-add-document-uri-to-case-document.xml b/sjp-viewstore/sjp-viewstore-liquibase/src/main/resources/liquibase/sjp-view-store-db-changesets/094-add-document-uri-to-case-document.xml new file mode 100644 index 0000000000..3114b6bf87 --- /dev/null +++ b/sjp-viewstore/sjp-viewstore-liquibase/src/main/resources/liquibase/sjp-view-store-db-changesets/094-add-document-uri-to-case-document.xml @@ -0,0 +1,20 @@ + + + + + + + Blob uri a case document was filed from. Null for documents addressed by their file + service id, which is every document predating BYO-FileStore. + + + + + + + + diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/main/java/uk/gov/moj/cpp/sjp/persistence/entity/CaseDocument.java b/sjp-viewstore/sjp-viewstore-persistence/src/main/java/uk/gov/moj/cpp/sjp/persistence/entity/CaseDocument.java index 065d410b5c..3c9f766365 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/main/java/uk/gov/moj/cpp/sjp/persistence/entity/CaseDocument.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/main/java/uk/gov/moj/cpp/sjp/persistence/entity/CaseDocument.java @@ -37,11 +37,17 @@ public class CaseDocument implements Serializable { @Column(name = "document_number") private Integer documentNumber; + /** + * Blob uri the document was filed from, or null when it was addressed by its file service id. + */ + @Column(name = "document_uri") + private String documentUri; + public CaseDocument() { super(); } - public CaseDocument(UUID id, UUID materialId, String documentType, ZonedDateTime addedAt, UUID caseId, Integer documentNumber) { + public CaseDocument(UUID id, UUID materialId, String documentType, ZonedDateTime addedAt, UUID caseId, Integer documentNumber, String documentUri) { super(); this.id = id; this.materialId = materialId; @@ -49,6 +55,7 @@ public CaseDocument(UUID id, UUID materialId, String documentType, ZonedDateTime this.addedAt = addedAt; this.caseId = caseId; this.documentNumber = documentNumber; + this.documentUri = documentUri; } public UUID getId() { @@ -99,4 +106,11 @@ public void setDocumentNumber(Integer documentNumber) { this.documentNumber = documentNumber; } + public String getDocumentUri() { + return documentUri; + } + public void setDocumentUri(String documentUri) { + this.documentUri = documentUri; + } + } diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/builder/CaseDocumentBuilder.java b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/builder/CaseDocumentBuilder.java index e7619efa82..6a2c31f2b9 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/builder/CaseDocumentBuilder.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/builder/CaseDocumentBuilder.java @@ -13,6 +13,7 @@ public class CaseDocumentBuilder { private UUID id; private UUID materialId; private String documentType; + private String documentUri; private CaseDocumentBuilder() { this.id = UUID.randomUUID(); @@ -34,8 +35,13 @@ public CaseDocumentBuilder withDocumentType(String documentType) { return this; } + public CaseDocumentBuilder withDocumentUri(String documentUri) { + this.documentUri = documentUri; + return this; + } + public CaseDocument build() { - return new CaseDocument(id, materialId, documentType, clock.now(), UUID.randomUUID(), 1); + return new CaseDocument(id, materialId, documentType, clock.now(), UUID.randomUUID(), 1, documentUri); } } diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseApplicationRepositoryTest.java b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseApplicationRepositoryTest.java index 255fbf1dac..e23edd2378 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseApplicationRepositoryTest.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseApplicationRepositoryTest.java @@ -109,6 +109,6 @@ private CaseDetail createCase(final UUID caseId, final String urn, final UUID de } private CaseDocument getCaseDocument(final UUID caseId, final UUID materialId) { - return new CaseDocument(randomUUID(), materialId, "SJPN", clock.now(), caseId, 1); + return new CaseDocument(randomUUID(), materialId, "SJPN", clock.now(), caseId, 1, null); } } diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseDocumentRepositoryTest.java b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseDocumentRepositoryTest.java index cf6e702860..d44952f0ec 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseDocumentRepositoryTest.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseDocumentRepositoryTest.java @@ -71,7 +71,7 @@ private void addCaseDocument(final ZonedDateTime addedAt) { caseDetail.setDefendant(defendantDetail); final CaseDocument caseDocument = new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), CaseDocument.RESULT_ORDER_DOCUMENT_TYPE, - addedAt, caseDetail.getId(), DOCUMENT_NUMBER); + addedAt, caseDetail.getId(), DOCUMENT_NUMBER, null); caseRepository.save(caseDetail); caseDocumentRepository.save(caseDocument); diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositorySjpnTest.java b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositorySjpnTest.java index f427c6e6d9..6e3b80e25b 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositorySjpnTest.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositorySjpnTest.java @@ -188,7 +188,7 @@ private void createOtherDocuments(final List cases) { private void createCaseDocuments(final List cases, final String documentType) { for (final CaseDetail caseDetail : cases) { - final CaseDocument sjpNotice = new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), documentType, clock.now(), caseDetail.getId(), 1); + final CaseDocument sjpNotice = new CaseDocument(UUID.randomUUID(), UUID.randomUUID(), documentType, clock.now(), caseDetail.getId(), 1, null); caseDetail.addCaseDocuments(sjpNotice); caseRepository.save(caseDetail); } diff --git a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositoryTest.java b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositoryTest.java index 772d0f3102..5ec450b86a 100644 --- a/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositoryTest.java +++ b/sjp-viewstore/sjp-viewstore-persistence/src/test/java/uk/gov/moj/cpp/sjp/persistence/repository/CaseRepositoryTest.java @@ -457,7 +457,7 @@ private CaseDetail getCase(final UUID caseId, final String urn, final UUID defen } private CaseDocument getCaseDocument(final UUID caseId, final UUID materialId) { - return new CaseDocument(randomUUID(), materialId, "SJPN", clock.now(), caseId, 1); + return new CaseDocument(randomUUID(), materialId, "SJPN", clock.now(), caseId, 1, null); } }