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.

Reply via email to