This is an automated email from the ASF dual-hosted git repository. myrle pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract-cn-customer.git
commit d8b5bf794ab4f1fedfd62f55c5f87df59b36f1b5 Author: Myrle Krantz <my...@apache.org> AuthorDate: Wed Oct 18 12:32:57 2017 +0200 Implementing documents for customers. --- .../io/mifos/customer/PermittableGroupIds.java | 1 + .../customer/api/v1/CustomerEventConstants.java | 10 + ...CompletedDocumentCannotBeChangedException.java} | 5 +- .../api/v1/client/CustomerDocumentsManager.java | 152 ++++++++++++ .../customer/api/v1/client/CustomerManager.java | 2 +- ...ption.java => DocumentValidationException.java} | 2 +- .../customer/api/v1/domain/CustomerDocument.java | 91 ++++++++ .../customer/api/v1/events/DocumentEvent.java | 71 ++++++ .../customer/api/v1/events/DocumentPageEvent.java | 85 +++++++ .../io/mifos/customer/AbstractCustomerTest.java | 4 + .../main/java/io/mifos/customer/TestCustomer.java | 4 +- .../main/java/io/mifos/customer/TestDocuments.java | 168 +++++++++++++ .../src/main/java/io/mifos/customer/TestSuite.java | 1 + .../customer/listener/DocumentEventListener.java | 81 +++++++ .../customer/util/CustomerDocumentGenerator.java | 21 +- .../internal/command/CompleteDocumentCommand.java | 45 ++++ .../internal/command/CreateDocumentCommand.java | 49 ++++ .../command/CreateDocumentPageCommand.java | 55 +++++ .../command/DeleteDocumentPageCommand.java | 52 +++++ .../command/handler/DocumentCommandHandler.java | 118 ++++++++++ .../service/internal/mapper/DocumentMapper.java | 70 ++++++ .../internal/repository/DocumentEntity.java | 117 ++++++++++ .../internal/repository/DocumentPageEntity.java | 128 ++++++++++ .../repository/DocumentPageRepository.java | 38 +++ .../internal/repository/DocumentRepository.java | 39 ++++ .../service/internal/service/DocumentService.java | 103 ++++++++ .../rest/config/CustomerRestConfiguration.java | 2 + .../service/rest/config/UploadProperties.java | 51 ++++ .../rest/controller/DocumentsRestController.java | 260 +++++++++++++++++++++ .../db/migrations/mariadb/V7__documents.sql | 39 ++++ 30 files changed, 1852 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/io/mifos/customer/PermittableGroupIds.java b/api/src/main/java/io/mifos/customer/PermittableGroupIds.java index f4f4dec..4d1ed80 100644 --- a/api/src/main/java/io/mifos/customer/PermittableGroupIds.java +++ b/api/src/main/java/io/mifos/customer/PermittableGroupIds.java @@ -17,6 +17,7 @@ package io.mifos.customer; public interface PermittableGroupIds { + String DOCUMENTS = "customer__v1__documents"; String CUSTOMER = "customer__v1__customer"; String PORTRAIT = "customer__v1__portrait"; String IDENTIFICATIONS = "customer__v1__identifications"; diff --git a/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java b/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java index ab5a9a3..03854ad 100644 --- a/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java +++ b/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java @@ -47,6 +47,11 @@ public interface CustomerEventConstants { String POST_PORTRAIT = "post-portrait"; String DELETE_PORTRAIT = "delete-portrait"; + String POST_DOCUMENT = "post-document"; + String POST_DOCUMENT_PAGE = "post-document-page"; + String DELETE_DOCUMENT_PAGE = "delete-document-page"; + String POST_DOCUMENT_COMPLETE = "post-document-complete"; + String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'"; String SELECTOR_POST_CUSTOMER = SELECTOR_NAME + " = '" + POST_CUSTOMER + "'"; @@ -72,4 +77,9 @@ public interface CustomerEventConstants { String SELECTOR_PUT_PORTRAIT = SELECTOR_NAME + " = '" + POST_PORTRAIT + "'"; String SELECTOR_DELETE_PORTRAIT = SELECTOR_NAME + " = '" + DELETE_PORTRAIT + "'"; + + String SELECTOR_POST_DOCUMENT = SELECTOR_NAME + " = '" + POST_DOCUMENT + "'"; + String SELECTOR_POST_DOCUMENT_PAGE = SELECTOR_NAME + " = '" + POST_DOCUMENT_PAGE + "'"; + String SELECTOR_DELETE_DOCUMENT_PAGE = SELECTOR_NAME + " = '" + DELETE_DOCUMENT_PAGE + "'"; + String SELECTOR_POST_DOCUMENT_COMPLETE = SELECTOR_NAME + " = '" + POST_DOCUMENT_COMPLETE + "'"; } diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/CompletedDocumentCannotBeChangedException.java similarity index 85% copy from api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java copy to api/src/main/java/io/mifos/customer/api/v1/client/CompletedDocumentCannotBeChangedException.java index cbe3a33..4182f46 100644 --- a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java +++ b/api/src/main/java/io/mifos/customer/api/v1/client/CompletedDocumentCannotBeChangedException.java @@ -15,5 +15,8 @@ */ package io.mifos.customer.api.v1.client; -public class PortraitValidationException extends RuntimeException { +/** + * @author Myrle Krantz + */ +public class CompletedDocumentCannotBeChangedException extends RuntimeException { } diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerDocumentsManager.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerDocumentsManager.java new file mode 100644 index 0000000..35b9be9 --- /dev/null +++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerDocumentsManager.java @@ -0,0 +1,152 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.api.v1.client; + +import io.mifos.core.api.annotation.ThrowsException; +import io.mifos.core.api.annotation.ThrowsExceptions; +import io.mifos.customer.api.v1.config.CustomerFeignClientConfig; +import io.mifos.customer.api.v1.domain.CustomerDocument; +import org.hibernate.validator.constraints.Range; +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * @author Myrle Krantz + */ +@FeignClient(name="customer-v1", path="/customer/v1", configuration= CustomerFeignClientConfig.class) +public interface CustomerDocumentsManager { + + @RequestMapping( + value = "/customers/{customeridentifier}/documents", + method = RequestMethod.GET, + produces = MediaType.ALL_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + List<CustomerDocument> getDocuments( + @PathVariable("customeridentifier") final String customerIdentifier); + + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}", + method = RequestMethod.GET, + produces = MediaType.ALL_VALUE + ) + CustomerDocument getDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier); + + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ThrowsExceptions({ + @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = DocumentValidationException.class) + }) + void createDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @RequestBody final CustomerDocument customerDocument); + + + /** + * Once a document is "completed" its name and images cannot be changed again. Only completed + * documents should be referenced by other services. + * + * @param completed once this is set to true it cannot be changed back again. + */ + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}/completed", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ThrowsExceptions({ + @ThrowsException(status = HttpStatus.CONFLICT, exception = CompletedDocumentCannotBeChangedException.class), + @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = DocumentValidationException.class), + }) + void completeDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @RequestBody final Boolean completed); + + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}/pages", + method = RequestMethod.GET, + produces = MediaType.ALL_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + List<Integer> getDocumentPageNumbers( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier); + + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.GET, + produces = MediaType.ALL_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + byte[] getDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") final Integer pageNumber); + + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.POST, + produces = MediaType.ALL_VALUE, + consumes = MediaType.MULTIPART_FORM_DATA_VALUE + ) + @ThrowsExceptions({ + @ThrowsException(status = HttpStatus.CONFLICT, exception = CompletedDocumentCannotBeChangedException.class), + @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = DocumentValidationException.class), + }) + void createDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") @Range(min=0) final Integer pageNumber, + @RequestBody final MultipartFile page); + + + @RequestMapping( + value = "/customers/{customeridentifier}/documents/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.DELETE, + produces = MediaType.ALL_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ThrowsExceptions({ + @ThrowsException(status = HttpStatus.CONFLICT, exception = CompletedDocumentCannotBeChangedException.class) + }) + void deleteDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") @Range(min=0) final Integer pageNumber); +} \ No newline at end of file diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java index 1d56aea..9b91502 100644 --- a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java +++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java @@ -329,7 +329,7 @@ public interface CustomerManager { ) @ThrowsExceptions({ @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class), - @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = PortraitValidationException.class), + @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = DocumentValidationException.class), }) void postPortrait(@PathVariable("identifier") final String identifier, @RequestBody final MultipartFile portrait); diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/DocumentValidationException.java similarity index 91% rename from api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java rename to api/src/main/java/io/mifos/customer/api/v1/client/DocumentValidationException.java index cbe3a33..6c0c60a 100644 --- a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java +++ b/api/src/main/java/io/mifos/customer/api/v1/client/DocumentValidationException.java @@ -15,5 +15,5 @@ */ package io.mifos.customer.api.v1.client; -public class PortraitValidationException extends RuntimeException { +public class DocumentValidationException extends RuntimeException { } diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerDocument.java b/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerDocument.java new file mode 100644 index 0000000..e6d5857 --- /dev/null +++ b/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerDocument.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.api.v1.domain; + +import io.mifos.core.lang.validation.constraints.ValidIdentifier; + +import java.util.Objects; + +/** + * @author Myrle Krantz + */ +public class CustomerDocument { + @ValidIdentifier + private String identifier; + + private boolean completed; + private String createdBy; + private String createdOn; + + public CustomerDocument() { + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public boolean isCompleted() { + return completed; + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(String createdOn) { + this.createdOn = createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CustomerDocument that = (CustomerDocument) o; + return completed == that.completed && + Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(identifier, completed); + } + + @Override + public String toString() { + return "CustomerDocument{" + + "identifier='" + identifier + '\'' + + ", completed=" + completed + + ", createdBy='" + createdBy + '\'' + + ", createdOn='" + createdOn + '\'' + + '}'; + } +} diff --git a/api/src/main/java/io/mifos/customer/api/v1/events/DocumentEvent.java b/api/src/main/java/io/mifos/customer/api/v1/events/DocumentEvent.java new file mode 100644 index 0000000..8e178fc --- /dev/null +++ b/api/src/main/java/io/mifos/customer/api/v1/events/DocumentEvent.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.api.v1.events; + +import java.util.Objects; + +/** + * @author Myrle Krantz + */ +public class DocumentEvent { + + private String customerIdentifier; + + private String documentIdentifier; + + public DocumentEvent(String customerIdentifier, String documentIdentifier) { + this.customerIdentifier = customerIdentifier; + this.documentIdentifier = documentIdentifier; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public void setCustomerIdentifier(String customerIdentifier) { + this.customerIdentifier = customerIdentifier; + } + + public String getDocumentIdentifier() { + return documentIdentifier; + } + + public void setDocumentIdentifier(String documentIdentifier) { + this.documentIdentifier = documentIdentifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DocumentEvent that = (DocumentEvent) o; + return Objects.equals(customerIdentifier, that.customerIdentifier) && + Objects.equals(documentIdentifier, that.documentIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(customerIdentifier, documentIdentifier); + } + + @Override + public String toString() { + return "DocumentEvent{" + + "customerIdentifier='" + customerIdentifier + '\'' + + ", documentIdentifier='" + documentIdentifier + '\'' + + '}'; + } +} diff --git a/api/src/main/java/io/mifos/customer/api/v1/events/DocumentPageEvent.java b/api/src/main/java/io/mifos/customer/api/v1/events/DocumentPageEvent.java new file mode 100644 index 0000000..9a747ef --- /dev/null +++ b/api/src/main/java/io/mifos/customer/api/v1/events/DocumentPageEvent.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.api.v1.events; + +import java.util.Objects; + +/** + * @author Myrle Krantz + */ +@SuppressWarnings("unused") +public class DocumentPageEvent { + + private String customerIdentifier; + + private String documentIdentifier; + + private int pageNumber; + + public DocumentPageEvent(String customerIdentifier, String documentIdentifier, int pageNumber) { + this.customerIdentifier = customerIdentifier; + this.documentIdentifier = documentIdentifier; + this.pageNumber = pageNumber; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public void setCustomerIdentifier(String customerIdentifier) { + this.customerIdentifier = customerIdentifier; + } + + public String getDocumentIdentifier() { + return documentIdentifier; + } + + public void setDocumentIdentifier(String documentIdentifier) { + this.documentIdentifier = documentIdentifier; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DocumentPageEvent that = (DocumentPageEvent) o; + return pageNumber == that.pageNumber && + Objects.equals(customerIdentifier, that.customerIdentifier) && + Objects.equals(documentIdentifier, that.documentIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(customerIdentifier, documentIdentifier, pageNumber); + } + + @Override + public String toString() { + return "DocumentPageEvent{" + + "customerIdentifier='" + customerIdentifier + '\'' + + ", documentIdentifier='" + documentIdentifier + '\'' + + ", pageNumber=" + pageNumber + + '}'; + } +} diff --git a/component-test/src/main/java/io/mifos/customer/AbstractCustomerTest.java b/component-test/src/main/java/io/mifos/customer/AbstractCustomerTest.java index 58cbad4..2649c13 100644 --- a/component-test/src/main/java/io/mifos/customer/AbstractCustomerTest.java +++ b/component-test/src/main/java/io/mifos/customer/AbstractCustomerTest.java @@ -21,6 +21,7 @@ import io.mifos.core.test.fixture.TenantDataStoreContextTestRule; import io.mifos.core.test.listener.EnableEventRecording; import io.mifos.core.test.listener.EventRecorder; import io.mifos.customer.api.v1.CustomerEventConstants; +import io.mifos.customer.api.v1.client.CustomerDocumentsManager; import io.mifos.customer.api.v1.client.CustomerManager; import io.mifos.customer.service.rest.config.CustomerRestConfiguration; import org.junit.After; @@ -73,6 +74,9 @@ public class AbstractCustomerTest extends SuiteTestEnvironment { CustomerManager customerManager; @Autowired + CustomerDocumentsManager customerDocumentsManager; + + @Autowired EventRecorder eventRecorder; private AutoUserContext userContext; diff --git a/component-test/src/main/java/io/mifos/customer/TestCustomer.java b/component-test/src/main/java/io/mifos/customer/TestCustomer.java index 36bf013..d11d5a8 100644 --- a/component-test/src/main/java/io/mifos/customer/TestCustomer.java +++ b/component-test/src/main/java/io/mifos/customer/TestCustomer.java @@ -20,7 +20,7 @@ import io.mifos.customer.api.v1.client.CustomerAlreadyExistsException; import io.mifos.customer.api.v1.client.CustomerNotFoundException; import io.mifos.customer.api.v1.client.CustomerValidationException; import io.mifos.customer.api.v1.client.PortraitNotFoundException; -import io.mifos.customer.api.v1.client.PortraitValidationException; +import io.mifos.customer.api.v1.client.DocumentValidationException; import io.mifos.customer.api.v1.domain.Address; import io.mifos.customer.api.v1.domain.Command; import io.mifos.customer.api.v1.domain.ContactDetail; @@ -392,7 +392,7 @@ public class TestCustomer extends AbstractCustomerTest { Assert.assertArrayEquals(secondFile.getBytes(), portrait); } - @Test(expected = PortraitValidationException.class) + @Test(expected = DocumentValidationException.class) public void shouldThrowIfPortraitExceedsMaxSize() throws Exception { final Customer customer = CustomerGenerator.createRandomCustomer(); diff --git a/component-test/src/main/java/io/mifos/customer/TestDocuments.java b/component-test/src/main/java/io/mifos/customer/TestDocuments.java new file mode 100644 index 0000000..7c5f4d2 --- /dev/null +++ b/component-test/src/main/java/io/mifos/customer/TestDocuments.java @@ -0,0 +1,168 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer; + +import io.mifos.core.api.util.NotFoundException; +import io.mifos.core.test.domain.TimeStampChecker; +import io.mifos.customer.api.v1.CustomerEventConstants; +import io.mifos.customer.api.v1.client.CompletedDocumentCannotBeChangedException; +import io.mifos.customer.api.v1.client.DocumentValidationException; +import io.mifos.customer.api.v1.domain.Customer; +import io.mifos.customer.api.v1.domain.CustomerDocument; +import io.mifos.customer.api.v1.events.DocumentEvent; +import io.mifos.customer.api.v1.events.DocumentPageEvent; +import io.mifos.customer.util.CustomerDocumentGenerator; +import io.mifos.customer.util.CustomerGenerator; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Myrle Krantz + */ +public class TestDocuments extends AbstractCustomerTest { + + @Test + public void shouldUploadEditThenCompleteDocument() throws InterruptedException, IOException { + //Prepare test. + final Customer customer = CustomerGenerator.createRandomCustomer(); + customerManager.createCustomer(customer); + eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier()); + + + //Check that document "stub" can be created. + final CustomerDocument customerDocument = CustomerDocumentGenerator.createRandomCustomerDocument(); + customerDocumentsManager.createDocument(customer.getIdentifier(), customerDocument.getIdentifier(), customerDocument); + eventRecorder.wait(CustomerEventConstants.POST_DOCUMENT, + new DocumentEvent(customer.getIdentifier(), customerDocument.getIdentifier())); + + final CustomerDocument createdCustomerDocument = customerDocumentsManager.getDocument( + customer.getIdentifier(), customerDocument.getIdentifier()); + Assert.assertEquals(customerDocument, createdCustomerDocument); + + + //Check that pages can be created. + for (int i = 0; i < 10; i++) { + createDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), i); + } + + List<Integer> pageNumbers = customerDocumentsManager.getDocumentPageNumbers( + customer.getIdentifier(), customerDocument.getIdentifier()); + final List<Integer> expectedPageNumbers = IntStream.range(0, 10).boxed().collect(Collectors.toList()); + Assert.assertEquals(expectedPageNumbers, pageNumbers); + + + //Check that a page can be deleted. + customerDocumentsManager.deleteDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 9); + eventRecorder.wait(CustomerEventConstants.DELETE_DOCUMENT_PAGE, + new DocumentPageEvent(customer.getIdentifier(), customerDocument.getIdentifier(), 9)); + + final List<Integer> changedPageNumbers = customerDocumentsManager.getDocumentPageNumbers( + customer.getIdentifier(), customerDocument.getIdentifier()); + final List<Integer> changedExpectedPageNumbers = IntStream.range(0, 9).boxed().collect(Collectors.toList()); + Assert.assertEquals(changedExpectedPageNumbers, changedPageNumbers); + + try { + customerDocumentsManager.getDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 9); + Assert.fail("Getting the 9th document page should throw a NotFoundException after the 9th page was removed."); + } + catch (final NotFoundException ignored) {} + + + //Check that a document which is missing pages cannot be completed + customerDocumentsManager.deleteDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 2); + eventRecorder.wait(CustomerEventConstants.DELETE_DOCUMENT_PAGE, + new DocumentPageEvent(customer.getIdentifier(), customerDocument.getIdentifier(), 2)); + + try { + customerDocumentsManager.completeDocument(customer.getIdentifier(), customerDocument.getIdentifier(), true); + Assert.fail("It shouldn't be possible to complete a document with missing pages."); + } + catch (final DocumentValidationException ignored) {} + + createDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 2); + + + //Check that a valid document can be completed. + final TimeStampChecker timeStampChecker = TimeStampChecker.roughlyNow(); + customerDocumentsManager.completeDocument(customer.getIdentifier(), customerDocument.getIdentifier(), true); + eventRecorder.wait(CustomerEventConstants.POST_DOCUMENT_COMPLETE, + new DocumentPageEvent(customer.getIdentifier(), customerDocument.getIdentifier(), 9)); + + final CustomerDocument completedDocument = customerDocumentsManager.getDocument( + customer.getIdentifier(), customerDocument.getIdentifier()); + Assert.assertEquals(true, completedDocument.isCompleted()); + timeStampChecker.assertCorrect(completedDocument.getCreatedOn()); + + + //Check that document can't be changed after completion + try { + createDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 9); + Assert.fail("Adding another page after the document is completed shouldn't be possible."); + } + catch (final CompletedDocumentCannotBeChangedException ignored) {} + try { + customerDocumentsManager.deleteDocumentPage(customer.getIdentifier(), customerDocument.getIdentifier(), 8); + Assert.fail("Deleting a page after the document is completed shouldn't be possible."); + } + catch (final CompletedDocumentCannotBeChangedException ignored) {} + + + //Check that document can't be uncompleted. + try { + customerDocumentsManager.completeDocument(customer.getIdentifier(), customerDocument.getIdentifier(), false); + Assert.fail("It shouldn't be possible to change a document from completed to uncompleted."); + } + catch (final CompletedDocumentCannotBeChangedException ignored) {} + + + //Check that document is in the list. + final List<CustomerDocument> documents = customerDocumentsManager.getDocuments(customer.getIdentifier()); + final boolean documentIsInList = documents.stream().anyMatch(x -> + (x.getIdentifier().equals(customerDocument.getIdentifier())) && + (x.isCompleted())); + Assert.assertTrue("The document we just completed should be in the list", documentIsInList); + } + + + private void createDocumentPage( + final String customerIdentifier, + final String documentIdentifier, + final int pageNumber) throws InterruptedException, IOException { + final MockMultipartFile page = new MockMultipartFile( + "page", + "test.png", + MediaType.IMAGE_PNG_VALUE, + RandomStringUtils.randomAlphanumeric(20).getBytes()); + + customerDocumentsManager.createDocumentPage(customerIdentifier, documentIdentifier, pageNumber, page); + eventRecorder.wait(CustomerEventConstants.POST_DOCUMENT_PAGE, + new DocumentPageEvent(customerIdentifier, documentIdentifier, pageNumber)); + + Thread.sleep(100); + + final byte[] uploadedPage = customerDocumentsManager.getDocumentPage(customerIdentifier, documentIdentifier, pageNumber); + Assert.assertTrue("Page " + pageNumber, Arrays.equals(page.getBytes(), uploadedPage)); + } +} diff --git a/component-test/src/main/java/io/mifos/customer/TestSuite.java b/component-test/src/main/java/io/mifos/customer/TestSuite.java index 4ed78bb..391fbbf 100644 --- a/component-test/src/main/java/io/mifos/customer/TestSuite.java +++ b/component-test/src/main/java/io/mifos/customer/TestSuite.java @@ -28,6 +28,7 @@ import org.junit.runners.Suite; TestInfrastructure.class, TestTaskDefinition.class, TestTaskInstance.class, + TestDocuments.class }) public class TestSuite extends SuiteTestEnvironment { } diff --git a/component-test/src/main/java/io/mifos/customer/listener/DocumentEventListener.java b/component-test/src/main/java/io/mifos/customer/listener/DocumentEventListener.java new file mode 100644 index 0000000..6875afd --- /dev/null +++ b/component-test/src/main/java/io/mifos/customer/listener/DocumentEventListener.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.listener; + +import io.mifos.core.lang.config.TenantHeaderFilter; +import io.mifos.core.test.listener.EventRecorder; +import io.mifos.customer.api.v1.CustomerEventConstants; +import io.mifos.customer.api.v1.events.DocumentEvent; +import io.mifos.customer.api.v1.events.DocumentPageEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +/** + * @author Myrle Krantz + */ +@Component +public class DocumentEventListener { + + private final EventRecorder eventRecorder; + + @Autowired + public DocumentEventListener(final EventRecorder eventRecorder) { + super(); + this.eventRecorder = eventRecorder; + } + + @JmsListener( + destination = CustomerEventConstants.DESTINATION, + selector = CustomerEventConstants.SELECTOR_POST_DOCUMENT + ) + public void postDocumentEvent( + @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant, + final String payload) { + this.eventRecorder.event(tenant, CustomerEventConstants.POST_DOCUMENT, payload, DocumentEvent.class); + } + + @JmsListener( + destination = CustomerEventConstants.DESTINATION, + selector = CustomerEventConstants.SELECTOR_POST_DOCUMENT_PAGE + ) + public void postDocumentPageEvent( + @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant, + final String payload) { + this.eventRecorder.event(tenant, CustomerEventConstants.POST_DOCUMENT_PAGE, payload, DocumentPageEvent.class); + } + + @JmsListener( + destination = CustomerEventConstants.DESTINATION, + selector = CustomerEventConstants.SELECTOR_DELETE_DOCUMENT_PAGE + ) + public void deleteDocumentPageEvent( + @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant, + final String payload) { + this.eventRecorder.event(tenant, CustomerEventConstants.DELETE_DOCUMENT_PAGE, payload, DocumentPageEvent.class); + } + + @JmsListener( + destination = CustomerEventConstants.DESTINATION, + selector = CustomerEventConstants.SELECTOR_POST_DOCUMENT_COMPLETE + ) + public void postDocumentComplete( + @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant, + final String payload) { + this.eventRecorder.event(tenant, CustomerEventConstants.POST_DOCUMENT_COMPLETE, payload, DocumentEvent.class); + } +} diff --git a/api/src/main/java/io/mifos/customer/PermittableGroupIds.java b/component-test/src/main/java/io/mifos/customer/util/CustomerDocumentGenerator.java similarity index 57% copy from api/src/main/java/io/mifos/customer/PermittableGroupIds.java copy to component-test/src/main/java/io/mifos/customer/util/CustomerDocumentGenerator.java index f4f4dec..4a7ee13 100644 --- a/api/src/main/java/io/mifos/customer/PermittableGroupIds.java +++ b/component-test/src/main/java/io/mifos/customer/util/CustomerDocumentGenerator.java @@ -13,13 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.mifos.customer; +package io.mifos.customer.util; -public interface PermittableGroupIds { +import io.mifos.customer.api.v1.domain.CustomerDocument; +import org.apache.commons.lang3.RandomStringUtils; - String CUSTOMER = "customer__v1__customer"; - String PORTRAIT = "customer__v1__portrait"; - String IDENTIFICATIONS = "customer__v1__identifications"; - String TASK = "customer__v1__task"; - String CATALOG = "catalog__v1__catalog"; +public final class CustomerDocumentGenerator { + + private CustomerDocumentGenerator() { + super(); + } + + public static CustomerDocument createRandomCustomerDocument() { + final CustomerDocument ret = new CustomerDocument(); + ret.setIdentifier(RandomStringUtils.randomAlphanumeric(8)); + return ret; + } } diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CompleteDocumentCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CompleteDocumentCommand.java new file mode 100644 index 0000000..1f60a9f --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/command/CompleteDocumentCommand.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.command; + +/** + * @author Myrle Krantz + */ +public class CompleteDocumentCommand { + private final String customerIdentifier; + private final String documentIdentifier; + + public CompleteDocumentCommand(String customerIdentifier, String documentIdentifier) { + this.customerIdentifier = customerIdentifier; + this.documentIdentifier = documentIdentifier; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public String getDocumentIdentifier() { + return documentIdentifier; + } + + @Override + public String toString() { + return "CompleteDocumentCommand{" + + "customerIdentifier='" + customerIdentifier + '\'' + + ", documentIdentifier='" + documentIdentifier + '\'' + + '}'; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentCommand.java new file mode 100644 index 0000000..3311eee --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.command; + +import io.mifos.customer.api.v1.domain.CustomerDocument; + +/** + * @author Myrle Krantz + */ +public class CreateDocumentCommand { + private final String customerIdentifier; + private final CustomerDocument customerDocument; + + public CreateDocumentCommand( + final String customerIdentifier, + final CustomerDocument customerDocument) { + this.customerIdentifier = customerIdentifier; + this.customerDocument = customerDocument; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public CustomerDocument getCustomerDocument() { + return customerDocument; + } + + @Override + public String toString() { + return "CreateDocumentCommand{" + + "customerIdentifier='" + customerIdentifier + '\'' + + ", customerDocument=" + customerDocument.getIdentifier() + + '}'; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentPageCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentPageCommand.java new file mode 100644 index 0000000..75ed8af --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/command/CreateDocumentPageCommand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.command; + +import org.springframework.web.multipart.MultipartFile; + +/** + * @author Myrle Krantz + */ +public class CreateDocumentPageCommand { + private final String customerIdentifier; + private final String documentIdentifier; + private final Integer pageNumber; + private final MultipartFile document; + + public CreateDocumentPageCommand( + final String customerIdentifier, + final String documentIdentifier, + final int pageNumber, + final MultipartFile document) { + this.customerIdentifier = customerIdentifier; + this.documentIdentifier = documentIdentifier; + this.pageNumber = pageNumber; + this.document = document; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public String getDocumentIdentifier() { + return documentIdentifier; + } + + public Integer getPageNumber() { + return pageNumber; + } + + public MultipartFile getDocument() { + return document; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/DeleteDocumentPageCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/DeleteDocumentPageCommand.java new file mode 100644 index 0000000..e7dd435 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/command/DeleteDocumentPageCommand.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.command; + +/** + * @author Myrle Krantz + */ +public class DeleteDocumentPageCommand { + private final String customerIdentifier; + private final String documentIdentifier; + private final Integer pageNumber; + + public DeleteDocumentPageCommand(String customerIdentifier, String documentIdentifier, Integer pageNumber) { + this.customerIdentifier = customerIdentifier; + this.documentIdentifier = documentIdentifier; + this.pageNumber = pageNumber; + } + + public String getCustomerIdentifier() { + return customerIdentifier; + } + + public String getDocumentIdentifier() { + return documentIdentifier; + } + + public Integer getPageNumber() { + return pageNumber; + } + + @Override + public String toString() { + return "DeleteDocumentPageCommand{" + + "customerIdentifier='" + customerIdentifier + '\'' + + ", documentIdentifier='" + documentIdentifier + '\'' + + ", pageNumber=" + pageNumber + + '}'; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/handler/DocumentCommandHandler.java b/service/src/main/java/io/mifos/customer/service/internal/command/handler/DocumentCommandHandler.java new file mode 100644 index 0000000..2ee4498 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/command/handler/DocumentCommandHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.command.handler; + +import io.mifos.core.api.util.UserContextHolder; +import io.mifos.core.command.annotation.Aggregate; +import io.mifos.core.command.annotation.CommandHandler; +import io.mifos.core.command.annotation.EventEmitter; +import io.mifos.core.lang.ServiceException; +import io.mifos.customer.api.v1.CustomerEventConstants; +import io.mifos.customer.api.v1.events.DocumentEvent; +import io.mifos.customer.api.v1.events.DocumentPageEvent; +import io.mifos.customer.service.internal.command.CompleteDocumentCommand; +import io.mifos.customer.service.internal.command.CreateDocumentCommand; +import io.mifos.customer.service.internal.command.CreateDocumentPageCommand; +import io.mifos.customer.service.internal.command.DeleteDocumentPageCommand; +import io.mifos.customer.service.internal.mapper.DocumentMapper; +import io.mifos.customer.service.internal.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.Clock; +import java.time.LocalDateTime; + +/** + * @author Myrle Krantz + */ +@Aggregate +public class DocumentCommandHandler { + private final DocumentRepository documentRepository; + private final DocumentPageRepository documentPageRepository; + private final CustomerRepository customerRepository; + + @Autowired + public DocumentCommandHandler( + final DocumentRepository documentRepository, + final DocumentPageRepository documentPageRepository, + final CustomerRepository customerRepository) { + this.documentRepository = documentRepository; + this.documentPageRepository = documentPageRepository; + this.customerRepository = customerRepository; + } + + @Transactional + @CommandHandler + @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.POST_DOCUMENT_PAGE) + public DocumentPageEvent process(final CreateDocumentPageCommand command) throws IOException { + final DocumentEntity documentEntity = documentRepository.findByCustomerIdAndDocumentIdentifier( + command.getCustomerIdentifier(), + command.getDocumentIdentifier()) + .orElseThrow(() -> ServiceException.badRequest("Document not found")); + + final DocumentPageEntity documentPageEntity = DocumentMapper.map(command.getDocument(), command.getPageNumber(), documentEntity); + documentPageRepository.save(documentPageEntity); + + return new DocumentPageEvent(command.getCustomerIdentifier(), command.getDocumentIdentifier(), command.getPageNumber()); + } + + @Transactional + @CommandHandler + @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.POST_DOCUMENT) + public DocumentEvent process(final CreateDocumentCommand command) throws IOException { + + + final CustomerEntity customerEntity = customerRepository.findByIdentifier(command.getCustomerIdentifier()); + final DocumentEntity documentEntity = DocumentMapper.map(command.getCustomerDocument(), customerEntity); + documentRepository.save(documentEntity); + + return new DocumentEvent(command.getCustomerIdentifier(), command.getCustomerDocument().getIdentifier()); + } + + @Transactional + @CommandHandler + @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.POST_DOCUMENT_COMPLETE) + public DocumentEvent process(final CompleteDocumentCommand command) throws IOException { + final DocumentEntity documentEntity = documentRepository.findByCustomerIdAndDocumentIdentifier( + command.getCustomerIdentifier(), + command.getDocumentIdentifier()) + .orElseThrow(() -> ServiceException.badRequest("Document not found")); + + documentEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC())); + documentEntity.setCreatedBy(UserContextHolder.checkedGetUser()); + documentEntity.setCompleted(true); + documentRepository.save(documentEntity); + + + return new DocumentEvent(command.getCustomerIdentifier(), command.getDocumentIdentifier()); + } + + @Transactional + @CommandHandler + @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.DELETE_DOCUMENT_PAGE) + public DocumentPageEvent process(final DeleteDocumentPageCommand command) throws IOException { + documentPageRepository.findByCustomerIdAndDocumentIdentifierAndPageNumber( + command.getCustomerIdentifier(), + command.getDocumentIdentifier(), + command.getPageNumber()) + .ifPresent(documentPageRepository::delete); + + //No exception if it's not present, because why bother. It's not present. That was the goal. + + return new DocumentPageEvent(command.getCustomerIdentifier(), command.getDocumentIdentifier(), command.getPageNumber()); + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/DocumentMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/DocumentMapper.java new file mode 100644 index 0000000..6ea88e9 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/DocumentMapper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.mapper; + +import io.mifos.core.api.util.UserContextHolder; +import io.mifos.core.lang.DateConverter; +import io.mifos.customer.api.v1.domain.CustomerDocument; +import io.mifos.customer.service.internal.repository.CustomerEntity; +import io.mifos.customer.service.internal.repository.DocumentEntity; +import io.mifos.customer.service.internal.repository.DocumentPageEntity; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.Clock; +import java.time.LocalDateTime; + +/** + * @author Myrle Krantz + */ +public class DocumentMapper { + private DocumentMapper() { + super(); + } + + + public static DocumentPageEntity map( + final MultipartFile multipartFile, + final int pageNumber, + final DocumentEntity documentEntity) throws IOException { + final DocumentPageEntity ret = new DocumentPageEntity(); + ret.setDocument(documentEntity); + ret.setPageNumber(pageNumber); + ret.setImage(multipartFile.getBytes()); + ret.setSize(multipartFile.getSize()); + ret.setContentType(multipartFile.getContentType()); + return ret; + } + + public static CustomerDocument map(final DocumentEntity documentEntity) { + final CustomerDocument ret = new CustomerDocument(); + ret.setCompleted(documentEntity.getCompleted()); + ret.setCreatedBy(documentEntity.getCreatedBy()); + ret.setCreatedOn(DateConverter.toIsoString(documentEntity.getCreatedOn())); + ret.setIdentifier(documentEntity.getIdentifier()); + return ret; + } + + public static DocumentEntity map(final CustomerDocument customerDocument, final CustomerEntity customerEntity) { + final DocumentEntity ret = new DocumentEntity(); + ret.setCustomer(customerEntity); + ret.setCompleted(false); + ret.setCreatedBy(UserContextHolder.checkedGetUser()); + ret.setCreatedOn(LocalDateTime.now(Clock.systemUTC())); + ret.setIdentifier(customerDocument.getIdentifier()); + return ret; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentEntity.java new file mode 100644 index 0000000..ae2513f --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentEntity.java @@ -0,0 +1,117 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.repository; + +import io.mifos.core.mariadb.util.LocalDateTimeConverter; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * @author Myrle Krantz + */ +@Entity +@Table(name = "maat_documents") +public class DocumentEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "customer_id") + private CustomerEntity customer; + + @Column(name = "identifier") + private String identifier; + + @Column(name = "is_completed", nullable = false) + private Boolean completed; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "created_on") + @Convert(converter = LocalDateTimeConverter.class) + private LocalDateTime createdOn; + + public DocumentEntity() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public CustomerEntity getCustomer() { + return customer; + } + + public void setCustomer(CustomerEntity customer) { + this.customer = customer; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public Boolean getCompleted() { + return completed; + } + + public void setCompleted(Boolean completed) { + this.completed = completed; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DocumentEntity that = (DocumentEntity) o; + return Objects.equals(customer, that.customer) && + Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(customer, identifier); + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageEntity.java new file mode 100644 index 0000000..32e5731 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageEntity.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.repository; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.Objects; + +/** + * @author Myrle Krantz + */ +@Entity +@Table(name = "maat_document_pages") +public class DocumentPageEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "document_id") + private DocumentEntity document; + + @Column(name = "page_number") + private Integer pageNumber; + + @Column(name = "content_type") + private String contentType; + + + @Column(name = "size") + private Long size; + + @Lob + @Column(name = "image") + private byte[] image; + + public DocumentPageEntity() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @SuppressWarnings("unused") + public DocumentEntity getDocument() { + return document; + } + + public void setDocument(DocumentEntity document) { + this.document = document; + } + + public Integer getPageNumber() { + return pageNumber; + } + + public void setPageNumber(Integer pageNumber) { + this.pageNumber = pageNumber; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public byte[] getImage() { + return image; + } + + public void setImage(byte[] image) { + this.image = image; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DocumentPageEntity that = (DocumentPageEntity) o; + return Objects.equals(document, that.document) && + Objects.equals(pageNumber, that.pageNumber); + } + + @Override + public int hashCode() { + return Objects.hash(document, pageNumber); + } + + @Override + public String toString() { + return "DocumentPageEntity{" + + "id=" + id + + ", document=" + document + + ", pageNumber=" + pageNumber + + ", contentType='" + contentType + '\'' + + ", size=" + size + + ", image=" + Arrays.toString(image) + + '}'; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageRepository.java new file mode 100644 index 0000000..bb35d09 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentPageRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * @author Myrle Krantz + */ +@Repository +public interface DocumentPageRepository extends JpaRepository<DocumentPageEntity, Long> { + @Query("SELECT d FROM DocumentPageEntity d WHERE d.document.customer.identifier = :customerIdentifier AND d.document.identifier = :documentIdentifier AND d.pageNumber = :pageNumber") + Optional<DocumentPageEntity> findByCustomerIdAndDocumentIdentifierAndPageNumber( + @Param("customerIdentifier") String customerIdentifier, @Param("documentIdentifier") String documentIdentifier, @Param("pageNumber") Integer pageNumber); + + @Query("SELECT d FROM DocumentPageEntity d WHERE d.document.customer.identifier = :customerIdentifier AND d.document.identifier = :documentIdentifier") + Stream<DocumentPageEntity> findByCustomerIdAndDocumentIdentifier( + @Param("customerIdentifier") String customerIdentifier, @Param("documentIdentifier") String documentIdentifier); +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentRepository.java new file mode 100644 index 0000000..70ea703 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/repository/DocumentRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * @author Myrle Krantz + */ +@Repository +public interface DocumentRepository extends JpaRepository<DocumentEntity, Long> { + + @Query("SELECT d FROM DocumentEntity d WHERE d.customer.identifier = :customerIdentifier AND d.identifier = :documentIdentifier") + Optional<DocumentEntity> findByCustomerIdAndDocumentIdentifier( + @Param("customerIdentifier") String customerIdentifier, @Param("documentIdentifier") String documentIdentifier); + + @Query("SELECT d FROM DocumentEntity d WHERE d.customer.identifier = :customerIdentifier") + Stream<DocumentEntity> findByCustomerId( + @Param("customerIdentifier") String customerIdentifier); +} diff --git a/service/src/main/java/io/mifos/customer/service/internal/service/DocumentService.java b/service/src/main/java/io/mifos/customer/service/internal/service/DocumentService.java new file mode 100644 index 0000000..88b6fcd --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/internal/service/DocumentService.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.internal.service; + +import io.mifos.customer.api.v1.domain.CustomerDocument; +import io.mifos.customer.service.internal.mapper.DocumentMapper; +import io.mifos.customer.service.internal.repository.DocumentEntity; +import io.mifos.customer.service.internal.repository.DocumentPageEntity; +import io.mifos.customer.service.internal.repository.DocumentPageRepository; +import io.mifos.customer.service.internal.repository.DocumentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Myrle Krantz + */ +@Service +public class DocumentService { + private final DocumentRepository documentRepository; + private final DocumentPageRepository documentPageRepository; + + @Autowired + public DocumentService( + final DocumentRepository documentRepository, + final DocumentPageRepository documentPageRepository) { + this.documentRepository = documentRepository; + this.documentPageRepository = documentPageRepository; + } + + public Optional<DocumentPageEntity> findPage( + final String customerIdentifier, + final String documentIdentifier, + final Integer pageNumber) { + return this.documentPageRepository.findByCustomerIdAndDocumentIdentifierAndPageNumber( + customerIdentifier, + documentIdentifier, + pageNumber); + } + + public Stream<CustomerDocument> find(final String customerIdentifier) { + final Stream<DocumentEntity> preMappedRet = this.documentRepository.findByCustomerId(customerIdentifier); + return preMappedRet.map(DocumentMapper::map); + } + + public Optional<CustomerDocument> findDocument( + final String customerIdentifier, + final String documentIdentifier) { + return this.documentRepository.findByCustomerIdAndDocumentIdentifier(customerIdentifier, documentIdentifier) + .map(DocumentMapper::map); + } + + public boolean documentExists( + final String customerIdentifier, + final String documentIdentifier) { + return findDocument(customerIdentifier, documentIdentifier).isPresent(); + } + + public Stream<Integer> findPageNumbers( + final String customerIdentifier, + final String documentIdentifier) { + return documentPageRepository.findByCustomerIdAndDocumentIdentifier(customerIdentifier, documentIdentifier) + .map(DocumentPageEntity::getPageNumber); + } + + public boolean isDocumentCompleted( + final String customerIdentifier, + final String documentIdentifier) { + return documentRepository.findByCustomerIdAndDocumentIdentifier(customerIdentifier, documentIdentifier) + .map(DocumentEntity::getCompleted).orElse(true); + } + + public boolean isDocumentMissingPages( + final String customerIdentifier, + final String documentIdentifier) { + final List<Integer> pageNumbers = findPageNumbers(customerIdentifier, documentIdentifier) + .sorted(Integer::compareTo) + .collect(Collectors.toList()); + for (int i = 0; i < pageNumbers.size(); i++) { + if (i != pageNumbers.get(i)) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/service/src/main/java/io/mifos/customer/service/rest/config/CustomerRestConfiguration.java b/service/src/main/java/io/mifos/customer/service/rest/config/CustomerRestConfiguration.java index e426f15..51b2494 100644 --- a/service/src/main/java/io/mifos/customer/service/rest/config/CustomerRestConfiguration.java +++ b/service/src/main/java/io/mifos/customer/service/rest/config/CustomerRestConfiguration.java @@ -29,6 +29,7 @@ import io.mifos.customer.service.internal.config.CustomerServiceConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -54,6 +55,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter CatalogRestConfiguration.class, CustomerServiceConfiguration.class }) +@EnableConfigurationProperties({UploadProperties.class}) public class CustomerRestConfiguration extends WebMvcConfigurerAdapter { public CustomerRestConfiguration() { diff --git a/service/src/main/java/io/mifos/customer/service/rest/config/UploadProperties.java b/service/src/main/java/io/mifos/customer/service/rest/config/UploadProperties.java new file mode 100644 index 0000000..0503cf0 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/rest/config/UploadProperties.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.rest.config; + +import org.hibernate.validator.constraints.Range; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; + +/** + * @author Myrle Krantz + */ +@Component +@ConfigurationProperties(prefix="upload") +@Validated +public class UploadProperties { + @Valid + private final Image image = new Image(); + + public static class Image { + @Range(min = 0L) + private long maxSize; + + public long getMaxSize() { + return maxSize; + } + + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + } + } + + public Image getImage() { + return image; + } +} diff --git a/service/src/main/java/io/mifos/customer/service/rest/controller/DocumentsRestController.java b/service/src/main/java/io/mifos/customer/service/rest/controller/DocumentsRestController.java new file mode 100644 index 0000000..8cd97d8 --- /dev/null +++ b/service/src/main/java/io/mifos/customer/service/rest/controller/DocumentsRestController.java @@ -0,0 +1,260 @@ +/* + * Copyright 2017 The Mifos Initiative. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.mifos.customer.service.rest.controller; + +import io.mifos.anubis.annotation.AcceptedTokenType; +import io.mifos.anubis.annotation.Permittable; +import io.mifos.core.command.gateway.CommandGateway; +import io.mifos.core.lang.ServiceException; +import io.mifos.customer.PermittableGroupIds; +import io.mifos.customer.api.v1.domain.CustomerDocument; +import io.mifos.customer.service.internal.command.CompleteDocumentCommand; +import io.mifos.customer.service.internal.command.CreateDocumentCommand; +import io.mifos.customer.service.internal.command.CreateDocumentPageCommand; +import io.mifos.customer.service.internal.command.DeleteDocumentPageCommand; +import io.mifos.customer.service.internal.repository.DocumentPageEntity; +import io.mifos.customer.service.internal.service.CustomerService; +import io.mifos.customer.service.internal.service.DocumentService; +import org.hibernate.validator.constraints.Range; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Myrle Krantz + */ +@RestController +@RequestMapping("/customers/{customeridentifier}/documents") +public class DocumentsRestController { + private final CommandGateway commandGateway; + private final CustomerService customerService; + private final DocumentService documentService; + + @Autowired + public DocumentsRestController( + final CommandGateway commandGateway, + final CustomerService customerService, + final DocumentService documentService) { + this.commandGateway = commandGateway; + this.customerService = customerService; + this.documentService = documentService; + } + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.ALL_VALUE + ) + public ResponseEntity<List<CustomerDocument>> getDocuments( + @PathVariable("customeridentifier") final String customerIdentifier) { + throwIfCustomerNotExists(customerIdentifier); + + return ResponseEntity.ok(documentService.find(customerIdentifier).collect(Collectors.toList())); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.ALL_VALUE + ) + public ResponseEntity<CustomerDocument> getDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier) { + return ResponseEntity + .ok(documentService.findDocument(customerIdentifier, documentIdentifier) + .orElseThrow(() -> ServiceException.notFound("Document ''{0}'' for customer ''{1}'' not found.", + documentIdentifier, customerIdentifier))); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public @ResponseBody + ResponseEntity<Void> createDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @RequestBody final @Valid CustomerDocument instance) { + throwIfCustomerNotExists(customerIdentifier); + + if (!instance.getIdentifier().equals(documentIdentifier)) + throw ServiceException.badRequest("Document identifier in request body must match document identifier in request path."); + + commandGateway.process(new CreateDocumentCommand(customerIdentifier, instance)); + + return ResponseEntity.accepted().build(); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}/completed", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public @ResponseBody + ResponseEntity<Void> completeDocument( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @RequestBody final @Valid Boolean completed) { + throwIfCustomerDocumentNotExists(customerIdentifier, documentIdentifier); + + if (!completed) + throwIfDocumentCompleted(customerIdentifier, documentIdentifier); + + throwIfPagesMissing(customerIdentifier, documentIdentifier); + + if (completed) + commandGateway.process(new CompleteDocumentCommand(customerIdentifier, documentIdentifier)); + + return ResponseEntity.accepted().build(); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}/pages", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.ALL_VALUE + ) + public @ResponseBody + ResponseEntity<List<Integer>> getDocumentPageNumbers( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier) { + throwIfCustomerDocumentNotExists(customerIdentifier, documentIdentifier); + + return ResponseEntity.ok(documentService.findPageNumbers(customerIdentifier, documentIdentifier).collect(Collectors.toList())); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.ALL_VALUE + ) + public ResponseEntity<byte[]> getDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") final Integer pageNumber) { + final DocumentPageEntity documentPageEntity = documentService.findPage(customerIdentifier, documentIdentifier, pageNumber) + .orElseThrow(() -> ServiceException.notFound("Page ''{0}'' of document ''{1}'' for customer ''{2}'' not found.", + pageNumber, documentIdentifier, customerIdentifier)); + + return ResponseEntity + .ok() + .contentType(MediaType.parseMediaType(documentPageEntity.getContentType())) + .contentLength(documentPageEntity.getImage().length) + .body(documentPageEntity.getImage()); + } + + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.MULTIPART_FORM_DATA_VALUE + ) + public @ResponseBody + ResponseEntity<Void> createDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") @Range(min=0) final Integer pageNumber, + @RequestBody final MultipartFile page) { + if(page == null) { + throw ServiceException.badRequest("Document not found"); + } + + throwIfCustomerNotExists(customerIdentifier); + throwIfDocumentCompleted(customerIdentifier, documentIdentifier); + throwIfInvalidContentType(page.getContentType()); + + commandGateway.process(new CreateDocumentPageCommand(customerIdentifier, documentIdentifier, pageNumber, page)); + + return ResponseEntity.accepted().build(); + } + + @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DOCUMENTS) + @RequestMapping( + value = "/{documentidentifier}/pages/{pagenumber}", + method = RequestMethod.DELETE, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.ALL_VALUE + ) + public @ResponseBody + ResponseEntity<Void> deleteDocumentPage( + @PathVariable("customeridentifier") final String customerIdentifier, + @PathVariable("documentidentifier") final String documentIdentifier, + @PathVariable("pagenumber") final Integer pageNumber) { + throwIfCustomerDocumentNotExists(customerIdentifier, documentIdentifier); + + throwIfDocumentCompleted(customerIdentifier, documentIdentifier); + + commandGateway.process(new DeleteDocumentPageCommand(customerIdentifier, documentIdentifier, pageNumber)); + + return ResponseEntity.accepted().build(); + } + + private void throwIfCustomerNotExists(final String customerIdentifier) { + if (!this.customerService.customerExists(customerIdentifier)) { + throw ServiceException.notFound("Customer ''{0}'' not found.", customerIdentifier); + } + } + + private void throwIfCustomerDocumentNotExists(final String customerIdentifier, final String documentIdentifier) { + if (!this.documentService.documentExists(customerIdentifier, documentIdentifier)) { + throw ServiceException.notFound("Customer ''{0}'' not found.", customerIdentifier); + } + } + + private void throwIfInvalidContentType(final String contentType) { + if(!contentType.contains(MediaType.IMAGE_JPEG_VALUE) + && !contentType.contains(MediaType.IMAGE_PNG_VALUE)) { + throw ServiceException.badRequest("Image has contentType ''{0}'', but only content types ''{1}'' and ''{2}'' allowed.", + contentType, MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE); + } + } + + private void throwIfDocumentCompleted(final String customerIdentifier, final String documentIdentifier) { + if (documentService.isDocumentCompleted(customerIdentifier, documentIdentifier)) + throw ServiceException.conflict("The document ''{0}'' for customer ''{1}'' is completed and cannot be uncompleted.", + documentIdentifier, customerIdentifier); + } + + private void throwIfPagesMissing(final String customerIdentifier, final String documentIdentifier) { + if (documentService.isDocumentMissingPages(customerIdentifier, documentIdentifier)) + throw ServiceException.badRequest("The document ''{0}'' for customer ''{1}'' is missing pages.", + documentIdentifier, customerIdentifier); + } +} diff --git a/service/src/main/resources/db/migrations/mariadb/V7__documents.sql b/service/src/main/resources/db/migrations/mariadb/V7__documents.sql new file mode 100644 index 0000000..fecaa31 --- /dev/null +++ b/service/src/main/resources/db/migrations/mariadb/V7__documents.sql @@ -0,0 +1,39 @@ +-- +-- Copyright 2017 The Mifos Initiative. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE maat_documents ( + id BIGINT NOT NULL AUTO_INCREMENT, + customer_id BIGINT NOT NULL, + identifier VARCHAR(32) NOT NULL, + is_completed BOOLEAN NOT NULL, + created_on TIMESTAMP(3) NOT NULL, + created_by VARCHAR(32) NOT NULL, + CONSTRAINT maat_documents_pk PRIMARY KEY (id), + CONSTRAINT maat_documents_uq UNIQUE (customer_id, identifier), + CONSTRAINT maat_documents_fk FOREIGN KEY (customer_id) REFERENCES maat_customers (id) ON UPDATE RESTRICT +); + +CREATE TABLE maat_document_pages ( + id BIGINT NOT NULL AUTO_INCREMENT, + document_id BIGINT NOT NULL, + page_number INT NOT NULL, + content_type VARCHAR(256) NOT NULL, + size BIGINT NOT NULL, + image MEDIUMBLOB NOT NULL, + CONSTRAINT maat_document_pages_pk PRIMARY KEY (id), + CONSTRAINT maat_document_pages_uq UNIQUE (document_id, page_number), + CONSTRAINT maat_document_pages_fk FOREIGN KEY (document_id) REFERENCES maat_documents (id) +); -- To stop receiving notification emails like this one, please contact my...@apache.org.