fineract-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From my...@apache.org
Subject [fineract-cn-customer] 01/28: Initial Commit
Date Mon, 22 Jan 2018 15:24:38 GMT
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 3358f5554c0b325d8804cff8097350848d88aeb6
Author: Markus Geiss <mgeiss@mifos.org>
AuthorDate: Thu Mar 16 12:56:11 2017 +0100

    Initial Commit
---
 .gitignore                                         |  16 +
 HEADER                                             |  13 +
 LICENSE                                            | 201 +++++++++
 README.md                                          |  25 ++
 api/build.gradle                                   |  34 ++
 api/settings.gradle                                |   1 +
 .../io/mifos/customer/PermittableGroupIds.java     |  23 +
 .../customer/api/v1/CustomerEventConstants.java    |  58 +++
 .../api/v1/client/AddressValidationException.java  |  19 +
 .../api/v1/client/CommandExecutionException.java   |  19 +
 .../client/ContactDetailValidationException.java   |  19 +
 .../v1/client/CustomerAlreadyExistsException.java  |  19 +
 .../customer/api/v1/client/CustomerClient.java     | 225 ++++++++++
 .../api/v1/client/CustomerNotFoundException.java   |  19 +
 .../api/v1/client/CustomerValidationException.java |  19 +
 .../api/v1/client/TaskAlreadyExistsException.java  |  19 +
 .../api/v1/client/TaskExecutionException.java      |  19 +
 .../api/v1/client/TaskNotFoundException.java       |  19 +
 .../api/v1/client/TaskValidationException.java     |  19 +
 .../api/v1/client/TasksStillPendingException.java  |  19 +
 .../io/mifos/customer/api/v1/domain/Address.java   |  84 ++++
 .../io/mifos/customer/api/v1/domain/Command.java   |  71 +++
 .../customer/api/v1/domain/ContactDetail.java      |  90 ++++
 .../io/mifos/customer/api/v1/domain/Customer.java  | 224 ++++++++++
 .../mifos/customer/api/v1/domain/CustomerPage.java |  53 +++
 .../customer/api/v1/domain/ExpirationDate.java     |  69 +++
 .../customer/api/v1/domain/IdentificationCard.java |  69 +++
 .../customer/api/v1/domain/TaskDefinition.java     | 108 +++++
 .../mifos/customer/api/v1/domain/TaskInstance.java |  63 +++
 .../catalog/api/v1/CatalogEventConstants.java      |  27 ++
 .../v1/client/CatalogAlreadyExistsException.java   |  19 +
 .../catalog/api/v1/client/CatalogClient.java       |  63 +++
 .../api/v1/client/CatalogValidationException.java  |  19 +
 .../customer/catalog/api/v1/domain/Catalog.java    | 104 +++++
 .../customer/catalog/api/v1/domain/Field.java      | 159 +++++++
 .../customer/catalog/api/v1/domain/Option.java     |  66 +++
 .../customer/catalog/api/v1/domain/Value.java      |  56 +++
 build.gradle                                       |  29 ++
 component-test/build.gradle                        |  38 ++
 component-test/settings.gradle                     |   1 +
 .../main/java/io/mifos/customer/TestCustomer.java  | 419 ++++++++++++++++++
 .../java/io/mifos/customer/TestInfrastructure.java | 107 +++++
 .../java/io/mifos/customer/TestTaskDefinition.java | 212 +++++++++
 .../java/io/mifos/customer/TestTaskInstance.java   | 206 +++++++++
 .../io/mifos/customer/catalog/TestCatalog.java     | 201 +++++++++
 .../catalog/listener/CatalogEventListener.java     |  45 ++
 .../customer/catalog/util/CatalogGenerator.java    |  64 +++
 .../customer/listener/CustomerEventListener.java   | 126 ++++++
 .../customer/listener/MigrationEventListener.java  |  46 ++
 .../mifos/customer/listener/TaskEventListener.java |  55 +++
 .../io/mifos/customer/util/AddressGenerator.java   |  38 ++
 .../io/mifos/customer/util/CommandGenerator.java   |  32 ++
 .../customer/util/ContactDetailGenerator.java      |  36 ++
 .../io/mifos/customer/util/CustomerGenerator.java  |  49 +++
 .../customer/util/IdentificationCardGenerator.java |  39 ++
 .../java/io/mifos/customer/util/TaskGenerator.java |  38 ++
 component-test/src/main/resources/logback.xml      |  40 ++
 docs/customer-state-machine.uxf                    | 172 ++++++++
 gradle/wrapper/gradle-wrapper.jar                  | Bin 0 -> 54212 bytes
 gradle/wrapper/gradle-wrapper.properties           |   6 +
 gradlew                                            | 172 ++++++++
 gradlew.bat                                        |  84 ++++
 service/build.gradle                               |  63 +++
 service/settings.gradle                            |   1 +
 .../internal/command/CreateCatalogCommand.java     |  31 ++
 .../internal/command/handler/CatalogAggregate.java |  64 +++
 .../config/CatalogServiceConfiguration.java        |  39 ++
 .../service/internal/mapper/CatalogMapper.java     |  55 +++
 .../service/internal/mapper/FieldMapper.java       |  87 ++++
 .../service/internal/mapper/OptionMapper.java      |  51 +++
 .../service/internal/repository/CatalogEntity.java | 133 ++++++
 .../internal/repository/CatalogRepository.java     |  27 ++
 .../service/internal/repository/FieldEntity.java   | 196 +++++++++
 .../internal/repository/FieldRepository.java       |  27 ++
 .../internal/repository/FieldValueEntity.java      |  81 ++++
 .../internal/repository/FieldValueRepository.java  |  30 ++
 .../service/internal/repository/OptionEntity.java  | 103 +++++
 .../service/internal/service/CatalogService.java   |  79 ++++
 .../internal/service/FieldValueValidator.java      | 159 +++++++
 .../rest/config/CatalogRestConfiguration.java      |  53 +++
 .../rest/controller/CatalogRestController.java     | 104 +++++
 .../customer/service/CustomerApplication.java      |  31 ++
 .../mifos/customer/service/ServiceConstants.java   |  21 +
 .../internal/command/ActivateCustomerCommand.java  |  36 ++
 .../AddTaskDefinitionToCustomerCommand.java        |  36 ++
 .../internal/command/CloseCustomerCommand.java     |  36 ++
 .../internal/command/CreateCustomerCommand.java    |  32 ++
 .../command/CreateTaskDefinitionCommand.java       |  32 ++
 .../command/ExecuteTaskForCustomerCommand.java     |  36 ++
 .../internal/command/InitializeServiceCommand.java |  23 +
 .../internal/command/LockCustomerCommand.java      |  36 ++
 .../internal/command/ReopenCustomerCommand.java    |  36 ++
 .../internal/command/UnlockCustomerCommand.java    |  36 ++
 .../internal/command/UpdateAddressCommand.java     |  38 ++
 .../command/UpdateContactDetailsCommand.java       |  40 ++
 .../internal/command/UpdateCustomerCommand.java    |  32 ++
 .../command/UpdateIdentificationCardCommand.java   |  38 ++
 .../command/UpdateTaskDefinitionCommand.java       |  38 ++
 .../command/handler/CustomerAggregate.java         | 387 ++++++++++++++++
 .../command/handler/MigrationAggregate.java        |  58 +++
 .../internal/command/handler/TaskAggregate.java    | 162 +++++++
 .../config/CustomerServiceConfiguration.java       |  40 ++
 .../service/internal/mapper/AddressMapper.java     |  48 ++
 .../service/internal/mapper/CommandMapper.java     |  51 +++
 .../internal/mapper/ContactDetailMapper.java       |  46 ++
 .../service/internal/mapper/CustomerMapper.java    |  75 ++++
 .../service/internal/mapper/FieldValueMapper.java  |  32 ++
 .../internal/mapper/IdentificationCardMapper.java  |  45 ++
 .../internal/mapper/TaskDefinitionMapper.java      |  51 +++
 .../internal/mapper/TaskInstanceMapper.java        |  47 ++
 .../service/internal/repository/AddressEntity.java | 105 +++++
 .../internal/repository/AddressRepository.java     |  23 +
 .../service/internal/repository/CommandEntity.java | 105 +++++
 .../internal/repository/CommandRepository.java     |  25 ++
 .../internal/repository/ContactDetailEntity.java   | 109 +++++
 .../repository/ContactDetailRepository.java        |  27 ++
 .../internal/repository/CustomerEntity.java        | 233 ++++++++++
 .../internal/repository/CustomerRepository.java    |  38 ++
 .../repository/IdentificationCardEntity.java       | 104 +++++
 .../repository/IdentificationCardRepository.java   |  25 ++
 .../internal/repository/TaskDefinitionEntity.java  | 131 ++++++
 .../repository/TaskDefinitionRepository.java       |  34 ++
 .../internal/repository/TaskInstanceEntity.java    | 105 +++++
 .../repository/TaskInstanceRepository.java         |  27 ++
 .../service/internal/service/CustomerService.java  | 162 +++++++
 .../service/internal/service/TaskService.java      |  77 ++++
 .../rest/config/CustomerRestConfiguration.java     |  65 +++
 .../rest/controller/CustomerRestController.java    | 489 +++++++++++++++++++++
 service/src/main/resources/application.yml         |  66 +++
 service/src/main/resources/bootstrap.yml           |  19 +
 .../db/migrations/mariadb/V1__initial_setup.sql    | 165 +++++++
 .../resources/diagrams/customer-state-machine.uxf  | 163 +++++++
 service/src/main/resources/logback.xml             |  40 ++
 settings.gradle                                    |   6 +
 shared.gradle                                      |  64 +++
 135 files changed, 9813 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..95274b9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+.gradle
+.idea
+**/build/
+**/target/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+*.iml
+
+*.log
+
+*.toDelete
\ No newline at end of file
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..76f9222
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,13 @@
+Copyright ${year} ${name}
+
+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.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..100d215
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source identifier, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object identifier, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source identifier control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f2b0706
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# Mifos I/O Customer Management
+
+[![Join the chat at https://gitter.im/mifos-initiative/mifos.io](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mifos-initiative/mifos.io?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+Ma'at covers simple functionality around CRM and KYC.
+
+## Versioning
+The version numbers follow the [Semantic Versioning](http://semver.org/) scheme.
+
+In addition to MAJOR.MINOR.PATCH the following postfixes are used to indicate the development state.
+
+* BUILD-SNAPSHOT - A release currently in development. 
+* M - A _milestone_ release include specific sets of functions and are released as soon as the functionality is complete.
+* RC - A _release candidate_ is a version with potential to be a final product, considered _code complete_.
+* RELEASE - _General availability_ indicates that this release is the best available version and is recommended for all usage.
+
+The versioning layout is {MAJOR}.{MINOR}.{PATCH}-{INDICATOR}[.{PATCH}]. Only milestones and release candidates can  have patch versions. Some examples:
+
+1.2.3.BUILD-SNAPSHOT  
+1.3.5.M.1  
+1.5.7.RC.2  
+2.0.0.RELEASE
+
+## License
+See [LICENSE](LICENSE) file.
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..b6a26a1
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,34 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE'
+    }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+    compile(
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
+            [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+    )
+}
+
+publishing {
+    publications {
+        api(MavenPublication) {
+            from components.java
+            groupId project.group
+            artifactId project.name
+            version project.version
+        }
+    }
+}
diff --git a/api/settings.gradle b/api/settings.gradle
new file mode 100644
index 0000000..7c8e3dc
--- /dev/null
+++ b/api/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'api'
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/customer/PermittableGroupIds.java b/api/src/main/java/io/mifos/customer/PermittableGroupIds.java
new file mode 100644
index 0000000..193feb0
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/PermittableGroupIds.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public interface PermittableGroupIds {
+
+  String CUSTOMER = "customer__v1__customer";
+  String TASK = "customer__v1__task";
+  String CATALOG = "catalog__v1__catalog";
+}
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
new file mode 100644
index 0000000..434f6f3
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+@SuppressWarnings("unused")
+public interface CustomerEventConstants {
+
+  String DESTINATION = "customer-v1";
+
+  String SELECTOR_NAME = "action";
+
+  String INITIALIZE = "initialize";
+
+  String POST_CUSTOMER = "post-customer";
+  String PUT_CUSTOMER = "put-customer";
+  String PUT_ADDRESS = "put-address";
+  String PUT_CONTACT_DETAILS = "put-contact-details";
+  String PUT_IDENTIFICATION_CARD = "put-identification-card";
+
+  String ACTIVATE_CUSTOMER = "activate-customer";
+  String LOCK_CUSTOMER = "lock-customer";
+  String UNLOCK_CUSTOMER = "unlock-customer";
+  String CLOSE_CUSTOMER = "close-customer";
+  String REOPEN_CUSTOMER = "reopen-customer";
+
+  String POST_TASK = "post-task";
+  String PUT_TASK = "put-task";
+
+  String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'";
+
+  String SELECTOR_POST_CUSTOMER = SELECTOR_NAME + " = '" + POST_CUSTOMER + "'";
+  String SELECTOR_PUT_CUSTOMER = SELECTOR_NAME + " = '" + PUT_CUSTOMER + "'";
+  String SELECTOR_PUT_ADDRESS = SELECTOR_NAME + " = '" + PUT_ADDRESS + "'";
+  String SELECTOR_PUT_CONTACT_DETAILS = SELECTOR_NAME + " = '" + PUT_CONTACT_DETAILS + "'";
+  String SELECTOR_PUT_IDENTIFICATION_CARD = SELECTOR_NAME + " = '" + PUT_IDENTIFICATION_CARD + "'";
+
+  String SELECTOR_ACTIVATE_CUSTOMER = SELECTOR_NAME + " = '" + ACTIVATE_CUSTOMER + "'";
+  String SELECTOR_LOCK_CUSTOMER = SELECTOR_NAME + " = '" + LOCK_CUSTOMER + "'";
+  String SELECTOR_UNLOCK_CUSTOMER = SELECTOR_NAME + " = '" + UNLOCK_CUSTOMER + "'";
+  String SELECTOR_CLOSE_CUSTOMER = SELECTOR_NAME + " = '" + CLOSE_CUSTOMER + "'";
+  String SELECTOR_REOPEN_CUSTOMER = SELECTOR_NAME + " = '" + REOPEN_CUSTOMER + "'";
+
+  String SELECTOR_POST_TASK = SELECTOR_NAME + " = '" + POST_TASK + "'";
+  String SELECTOR_PUT_TASK = SELECTOR_NAME + " = '" + PUT_TASK + "'";
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/AddressValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/AddressValidationException.java
new file mode 100644
index 0000000..edba50a
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/AddressValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class AddressValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CommandExecutionException.java b/api/src/main/java/io/mifos/customer/api/v1/client/CommandExecutionException.java
new file mode 100644
index 0000000..574fb0f
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CommandExecutionException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class CommandExecutionException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/ContactDetailValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/ContactDetailValidationException.java
new file mode 100644
index 0000000..20b3b86
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/ContactDetailValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class ContactDetailValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerAlreadyExistsException.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerAlreadyExistsException.java
new file mode 100644
index 0000000..e43bbbc
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerAlreadyExistsException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public final class CustomerAlreadyExistsException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerClient.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerClient.java
new file mode 100644
index 0000000..075890f
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerClient.java
@@ -0,0 +1,225 @@
+/*
+ * 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.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.customer.api.v1.domain.Address;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.api.v1.domain.ContactDetail;
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.api.v1.domain.CustomerPage;
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+@FeignClient(name="customer-v1", path="/customer/v1", configuration=CustomFeignClientsConfiguration.class)
+public interface CustomerClient {
+
+  @RequestMapping(
+      value = "/customers",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = CustomerAlreadyExistsException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = CustomerValidationException.class)
+  })
+  void createCustomer(@RequestBody final Customer customer);
+
+  @RequestMapping(
+      value = "/customers",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  CustomerPage fetchCustomers(@RequestParam(value = "term", required = false) final String term,
+                              @RequestParam(value = "includeClosed", required = false) final Boolean includeClosed,
+                              @RequestParam(value = "pageIndex", required = false) final Integer pageIndex,
+                              @RequestParam(value = "size", required = false) final Integer size,
+                              @RequestParam(value = "sortColumn", required = false) final String sortColumn,
+                              @RequestParam(value = "sortDirection", required = false) final String sortDirection);
+
+  @RequestMapping(
+      value = "/customers/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class)
+  Customer findCustomer(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/customers/{identifier}",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = CustomerValidationException.class)
+  })
+  void updateCustomer(@PathVariable("identifier") final String identifier, @RequestBody final Customer customer);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/commands",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = CommandExecutionException.class)
+  })
+  void customerCommand(@PathVariable("identifier") final String identifier, @RequestBody final Command command);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/commands",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class)
+  List<Command> fetchCustomerCommands(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/tasks/{taskIdentifier}",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = TaskAlreadyExistsException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = TaskValidationException.class)
+  })
+  void addTaskToCustomer(@PathVariable("identifier") final String identifier,
+                         @PathVariable("taskIdentifier") final String taskIdentifier);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/tasks/{taskIdentifier}",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = TaskExecutionException.class)
+  })
+  void taskForCustomerExecuted(@PathVariable("identifier") final String identifier,
+                               @PathVariable("taskIdentifier") final String taskIdentifier);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/tasks",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = TaskNotFoundException.class)
+  List<TaskDefinition> findTasksForCustomer(@PathVariable("identifier") final String identifier,
+                                          @RequestParam(value = "includeExecuted", required = false) final Boolean includeExecuted);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/address",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = AddressValidationException.class)
+  })
+  void putAddress(@PathVariable("identifier") final String identifier, @RequestBody final Address address);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/contact",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = ContactDetailValidationException.class)
+  })
+  void putContactDetails(@PathVariable("identifier") final String identifier,
+                         @RequestBody final List<ContactDetail> contactDetails);
+
+  @RequestMapping(
+      value = "/customers/{identifier}/identification",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = ContactDetailValidationException.class)
+  })
+  void putIdentificationCard(@PathVariable("identifier") final String identifier,
+                             @RequestBody final IdentificationCard identificationCard);
+
+  @RequestMapping(
+      value = "/tasks",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = TaskAlreadyExistsException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = TaskValidationException.class)
+  })
+  void createTask(@RequestBody final TaskDefinition taskDefinition);
+
+  @RequestMapping(
+      value = "/tasks",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  List<TaskDefinition> fetchAllTasks();
+
+  @RequestMapping(
+      value = "/tasks/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = TaskNotFoundException.class)
+  TaskDefinition findTask(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/tasks/{identifier}",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = TaskNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = TaskValidationException.class)
+  })
+  void updateTask(@PathVariable("identifier") final String identifier, @RequestBody final TaskDefinition taskDefinition);
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerNotFoundException.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerNotFoundException.java
new file mode 100644
index 0000000..e735ccd
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerNotFoundException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public final class CustomerNotFoundException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerValidationException.java
new file mode 100644
index 0000000..eefc2ed
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class CustomerValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/TaskAlreadyExistsException.java b/api/src/main/java/io/mifos/customer/api/v1/client/TaskAlreadyExistsException.java
new file mode 100644
index 0000000..def90d1
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/TaskAlreadyExistsException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public final class TaskAlreadyExistsException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/TaskExecutionException.java b/api/src/main/java/io/mifos/customer/api/v1/client/TaskExecutionException.java
new file mode 100644
index 0000000..8eeb682
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/TaskExecutionException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class TaskExecutionException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/TaskNotFoundException.java b/api/src/main/java/io/mifos/customer/api/v1/client/TaskNotFoundException.java
new file mode 100644
index 0000000..a95fbaa
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/TaskNotFoundException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public final class TaskNotFoundException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/TaskValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/TaskValidationException.java
new file mode 100644
index 0000000..a47f698
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/TaskValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public class TaskValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/TasksStillPendingException.java b/api/src/main/java/io/mifos/customer/api/v1/client/TasksStillPendingException.java
new file mode 100644
index 0000000..ff600e2
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/TasksStillPendingException.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+public final class TasksStillPendingException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/Address.java b/api/src/main/java/io/mifos/customer/api/v1/domain/Address.java
new file mode 100644
index 0000000..d1d4e1d
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/Address.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.hibernate.validator.constraints.NotBlank;
+
+public final class Address {
+
+  @NotBlank
+  private String street;
+  @NotBlank
+  private String city;
+  private String region;
+  private String postalCode;
+  @NotBlank
+  private String countryCode;
+  @NotBlank
+  private String country;
+
+  public Address() {
+    super();
+  }
+
+  public String getStreet() {
+    return this.street;
+  }
+
+  public void setStreet(final String street) {
+    this.street = street;
+  }
+
+  public String getCity() {
+    return this.city;
+  }
+
+  public void setCity(final String city) {
+    this.city = city;
+  }
+
+  public String getRegion() {
+    return this.region;
+  }
+
+  public void setRegion(final String region) {
+    this.region = region;
+  }
+
+  public String getPostalCode() {
+    return this.postalCode;
+  }
+
+  public void setPostalCode(final String postalCode) {
+    this.postalCode = postalCode;
+  }
+
+  public String getCountryCode() {
+    return this.countryCode;
+  }
+
+  public void setCountryCode(final String countryCode) {
+    this.countryCode = countryCode;
+  }
+
+  public String getCountry() {
+    return this.country;
+  }
+
+  public void setCountry(final String country) {
+    this.country = country;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/Command.java b/api/src/main/java/io/mifos/customer/api/v1/domain/Command.java
new file mode 100644
index 0000000..bba797e
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/Command.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.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+public final class Command {
+
+  public enum Action {
+    ACTIVATE,
+    LOCK,
+    UNLOCK,
+    CLOSE,
+    REOPEN
+  }
+
+  @NotBlank
+  private Action action;
+  private String comment;
+  private String createdOn;
+  private String createdBy;
+
+  public Command() {
+    super();
+  }
+
+  public String getAction() {
+    return this.action.name();
+  }
+
+  public void setAction(final String action) {
+    this.action = Action.valueOf(action.toUpperCase());
+  }
+
+  public String getComment() {
+    return this.comment;
+  }
+
+  public void setComment(final String comment) {
+    this.comment = comment;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/ContactDetail.java b/api/src/main/java/io/mifos/customer/api/v1/domain/ContactDetail.java
new file mode 100644
index 0000000..cd9fffb
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/ContactDetail.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+public final class ContactDetail {
+
+  public enum Type {
+    EMAIL,
+    PHONE,
+    MOBILE
+  }
+
+  public enum Group {
+    BUSINESS,
+    PRIVATE
+  }
+
+  @NotNull
+  private Type type;
+  @NotNull
+  private Group group;
+  @NotBlank
+  private String value;
+  @Min(1)
+  @Max(127)
+  private Integer preferenceLevel;  private Boolean validated;
+
+  public ContactDetail() {
+    super();
+  }
+
+  public String getType() {
+    return this.type.name();
+  }
+
+  public void setType(final String type) {
+    this.type = Type.valueOf(type.toUpperCase());
+  }
+
+  public String getValue() {
+    return this.value;
+  }
+
+  public String getGroup() {
+    return this.group.name();
+  }
+
+  public void setGroup(final String group) {
+    this.group = Group.valueOf(group);
+  }
+
+  public void setValue(final String value) {
+    this.value = value;
+  }
+
+  public Boolean getValidated() {
+    return this.validated;
+  }
+
+  public void setValidated(final Boolean validated) {
+    this.validated = validated;
+  }
+
+  public Integer getPreferenceLevel() {
+    return this.preferenceLevel;
+  }
+
+  public void setPreferenceLevel(final Integer preferenceLevel) {
+    this.preferenceLevel = preferenceLevel;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/Customer.java b/api/src/main/java/io/mifos/customer/api/v1/domain/Customer.java
new file mode 100644
index 0000000..50d9371
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/Customer.java
@@ -0,0 +1,224 @@
+/*
+ * 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.DateOfBirth;
+import io.mifos.customer.catalog.api.v1.domain.Value;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+public final class Customer {
+
+  public enum Type {
+    PERSON,
+    BUSINESS
+  }
+
+  public enum State {
+    PENDING,
+    ACTIVE,
+    LOCKED,
+    CLOSED
+  }
+
+  @NotBlank
+  private String identifier;
+  @NotNull
+  private Type type;
+  @NotBlank
+  private String givenName;
+  private String middleName;
+  @NotBlank
+  private String surname;
+  @NotNull
+  private DateOfBirth dateOfBirth;
+  @Valid
+  private IdentificationCard identificationCard;
+  private String accountBeneficiary;
+  private String referenceCustomer;
+  private String assignedOffice;
+  private String assignedEmployee;
+  @NotNull
+  @Valid
+  private Address address;
+  @Valid
+  private List<ContactDetail> contactDetails;
+  private State currentState;
+  private List<Value> customValues;
+  private String createdBy;
+  private String createdOn;
+  private String lastModifiedBy;
+  private String lastModifiedOn;
+
+  public Customer() {
+    super();
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getType() {
+    return this.type.name();
+  }
+
+  public void setType(final String type) {
+    this.type = Type.valueOf(type);
+  }
+
+  public String getGivenName() {
+    return this.givenName;
+  }
+
+  public void setGivenName(final String givenName) {
+    this.givenName = givenName;
+  }
+
+  public String getMiddleName() {
+    return this.middleName;
+  }
+
+  public void setMiddleName(final String middleName) {
+    this.middleName = middleName;
+  }
+
+  public String getSurname() {
+    return this.surname;
+  }
+
+  public void setSurname(final String surname) {
+    this.surname = surname;
+  }
+
+  public DateOfBirth getDateOfBirth() {
+    return this.dateOfBirth;
+  }
+
+  public void setDateOfBirth(final DateOfBirth dateOfBirth) {
+    this.dateOfBirth = dateOfBirth;
+  }
+
+  public IdentificationCard getIdentificationCard() {
+    return this.identificationCard;
+  }
+
+  public void setIdentificationCard(final IdentificationCard identificationCard) {
+    this.identificationCard = identificationCard;
+  }
+
+  public String getAccountBeneficiary() {
+    return this.accountBeneficiary;
+  }
+
+  public void setAccountBeneficiary(final String accountBeneficiary) {
+    this.accountBeneficiary = accountBeneficiary;
+  }
+
+  public String getReferenceCustomer() {
+    return this.referenceCustomer;
+  }
+
+  public void setReferenceCustomer(final String referenceCustomer) {
+    this.referenceCustomer = referenceCustomer;
+  }
+
+  public String getAssignedOffice() {
+    return this.assignedOffice;
+  }
+
+  public void setAssignedOffice(final String assignedOffice) {
+    this.assignedOffice = assignedOffice;
+  }
+
+  public String getAssignedEmployee() {
+    return this.assignedEmployee;
+  }
+
+  public void setAssignedEmployee(final String assignedEmployee) {
+    this.assignedEmployee = assignedEmployee;
+  }
+
+  public Address getAddress() {
+    return this.address;
+  }
+
+  public void setAddress(final Address address) {
+    this.address = address;
+  }
+
+  public List<ContactDetail> getContactDetails() {
+    return this.contactDetails;
+  }
+
+  public void setContactDetails(final List<ContactDetail> contactDetails) {
+    this.contactDetails = contactDetails;
+  }
+
+  public String getCurrentState() {
+    return this.currentState != null ? this.currentState.name() : null;
+  }
+
+  public void setCurrentState(final String currentState) {
+    this.currentState = State.valueOf(currentState);
+  }
+
+  public List<Value> getCustomValues() {
+    return this.customValues;
+  }
+
+  public void setCustomValues(final List<Value> customValues) {
+    this.customValues = customValues;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+
+  public String getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final String lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerPage.java b/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerPage.java
new file mode 100644
index 0000000..29a8c75
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/CustomerPage.java
@@ -0,0 +1,53 @@
+/*
+ * 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 java.util.List;
+
+public class CustomerPage {
+
+  private List<Customer> customers;
+  private Integer totalPages;
+  private Long totalElements;
+
+  public CustomerPage() {
+    super();
+  }
+
+  public List<Customer> getCustomers() {
+    return this.customers;
+  }
+
+  public void setCustomers(final List<Customer> customers) {
+    this.customers = customers;
+  }
+
+  public Integer getTotalPages() {
+    return this.totalPages;
+  }
+
+  public void setTotalPages(final Integer totalPages) {
+    this.totalPages = totalPages;
+  }
+
+  public Long getTotalElements() {
+    return this.totalElements;
+  }
+
+  public void setTotalElements(final Long totalElements) {
+    this.totalElements = totalElements;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/ExpirationDate.java b/api/src/main/java/io/mifos/customer/api/v1/domain/ExpirationDate.java
new file mode 100644
index 0000000..d87d330
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/ExpirationDate.java
@@ -0,0 +1,69 @@
+/*
+ * 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 javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+
+public class ExpirationDate {
+
+  @NotNull
+  private Integer year;
+  @NotNull
+  private Integer month;
+  @NotNull
+  private Integer day;
+
+  public ExpirationDate() {
+    super();
+  }
+
+  public Integer getYear() {
+    return this.year;
+  }
+
+  public void setYear(final Integer year) {
+    this.year = year;
+  }
+
+  public Integer getMonth() {
+    return this.month;
+  }
+
+  public void setMonth(final Integer month) {
+    this.month = month;
+  }
+
+  public Integer getDay() {
+    return this.day;
+  }
+
+  public void setDay(final Integer day) {
+    this.day = day;
+  }
+
+  public LocalDate toLocalDate() {
+    return LocalDate.of(this.year, this.month, this.day);
+  }
+
+  public static ExpirationDate fromLocalDate(LocalDate localDate) {
+    ExpirationDate dateOfBirth = new ExpirationDate();
+    dateOfBirth.setYear(localDate.getYear());
+    dateOfBirth.setMonth(localDate.getMonthValue());
+    dateOfBirth.setDay(localDate.getDayOfMonth());
+    return dateOfBirth;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/IdentificationCard.java b/api/src/main/java/io/mifos/customer/api/v1/domain/IdentificationCard.java
new file mode 100644
index 0000000..6f0667f
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/IdentificationCard.java
@@ -0,0 +1,69 @@
+/*
+ * 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 org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+public class IdentificationCard {
+
+  @NotBlank
+  private String type;
+  @NotBlank
+  private String number;
+  @NotNull
+  @Valid
+  private ExpirationDate expirationDate;
+  private String issuer;
+
+  public IdentificationCard() {
+    super();
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public String getNumber() {
+    return this.number;
+  }
+
+  public void setNumber(final String number) {
+    this.number = number;
+  }
+
+  public ExpirationDate getExpirationDate() {
+    return this.expirationDate;
+  }
+
+  public void setExpirationDate(final ExpirationDate expirationDate) {
+    this.expirationDate = expirationDate;
+  }
+
+  public String getIssuer() {
+    return this.issuer;
+  }
+
+  public void setIssuer(final String issuer) {
+    this.issuer = issuer;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/TaskDefinition.java b/api/src/main/java/io/mifos/customer/api/v1/domain/TaskDefinition.java
new file mode 100644
index 0000000..bd3dc48
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/TaskDefinition.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.NotNull;
+import java.util.HashSet;
+import java.util.Set;
+
+public final class TaskDefinition {
+
+  public enum Type {
+    ID_CARD,
+    FOUR_EYES,
+    CUSTOM
+  }
+
+  public enum Command {
+    ACTIVATE,
+    UNLOCK,
+    REOPEN
+  }
+
+  @NotBlank
+  private String identifier;
+  @NotNull
+  private Type type;
+  private Set<Command> commands;
+  @NotBlank
+  private String name;
+  @NotBlank
+  private String description;
+  private Boolean mandatory;
+  private Boolean predefined;
+
+  public String getIdentifier() {
+    return identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getType() {
+    return this.type.name();
+  }
+
+  public void setType(final String type) {
+    this.type = Type.valueOf(type);
+  }
+
+  public String[] getCommands() {
+    return this.commands.stream().map(Enum::name).toArray(size -> new String[size]);
+  }
+
+  public void setCommands(final String... commandNames) {
+    this.commands = new HashSet<>();
+    for (String command : commandNames) {
+      this.commands.add(Command.valueOf(command));
+    }
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public Boolean getMandatory() {
+    return this.mandatory;
+  }
+
+  public void setMandatory(final Boolean mandatory) {
+    this.mandatory = mandatory;
+  }
+
+  public Boolean getPredefined() {
+    return this.predefined;
+  }
+
+  public void setPredefined(final Boolean predefined) {
+    this.predefined = predefined;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/domain/TaskInstance.java b/api/src/main/java/io/mifos/customer/api/v1/domain/TaskInstance.java
new file mode 100644
index 0000000..dc6411a
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/domain/TaskInstance.java
@@ -0,0 +1,63 @@
+/*
+ * 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 org.hibernate.validator.constraints.NotBlank;
+
+public final class TaskInstance {
+
+  @NotBlank
+  private String taskIdentifier;
+  private String comment;
+  private String executedOn;
+  private String executedBy;
+
+  public TaskInstance() {
+    super();
+  }
+
+  public String getTaskIdentifier() {
+    return taskIdentifier;
+  }
+
+  public void setTaskIdentifier(final String taskIdentifier) {
+    this.taskIdentifier = taskIdentifier;
+  }
+
+  public String getComment() {
+    return this.comment;
+  }
+
+  public void setComment(final String comment) {
+    this.comment = comment;
+  }
+
+  public String getExecutedOn() {
+    return this.executedOn;
+  }
+
+  public void setExecutedOn(final String executedOn) {
+    this.executedOn = executedOn;
+  }
+
+  public String getExecutedBy() {
+    return this.executedBy;
+  }
+
+  public void setExecutedBy(final String executedBy) {
+    this.executedBy = executedBy;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/CatalogEventConstants.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/CatalogEventConstants.java
new file mode 100644
index 0000000..15dfa2a
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/CatalogEventConstants.java
@@ -0,0 +1,27 @@
+/*
+ * 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.catalog.api.v1;
+
+public interface CatalogEventConstants {
+
+  String DESTINATION = "nun-v1";
+
+  String SELECTOR_NAME = "action";
+
+  String POST_CATALOG = "post-catalog";
+
+  String SELECTOR_POST_CATALOG = SELECTOR_NAME + " = '" + POST_CATALOG + "'";
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogAlreadyExistsException.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogAlreadyExistsException.java
new file mode 100644
index 0000000..36e8428
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogAlreadyExistsException.java
@@ -0,0 +1,19 @@
+/*
+ * 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.catalog.api.v1.client;
+
+public class CatalogAlreadyExistsException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogClient.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogClient.java
new file mode 100644
index 0000000..e49c1f7
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogClient.java
@@ -0,0 +1,63 @@
+/*
+ * 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.catalog.api.v1.client;
+
+import io.mifos.core.api.annotation.ThrowsException;
+import io.mifos.core.api.annotation.ThrowsExceptions;
+import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+@FeignClient(name="customer-v1", path="/customer/v1", configuration=CustomFeignClientsConfiguration.class)
+public interface CatalogClient {
+
+  @RequestMapping(
+      path = "/catalogs",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = CatalogAlreadyExistsException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = CatalogValidationException.class)
+  })
+  void createCatalog(@RequestBody final Catalog catalog);
+
+  @RequestMapping(
+      path = "/catalogs",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  List<Catalog> fetchCatalogs();
+
+  @RequestMapping(
+      path = "/catalogs/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  Catalog findCatalog(@PathVariable("identifier") final String identifier);
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogValidationException.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogValidationException.java
new file mode 100644
index 0000000..f3bb2fc
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/client/CatalogValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * 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.catalog.api.v1.client;
+
+public class CatalogValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Catalog.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Catalog.java
new file mode 100644
index 0000000..37dd458
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Catalog.java
@@ -0,0 +1,104 @@
+/*
+ * 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.catalog.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import java.util.List;
+
+public class Catalog {
+
+  @NotEmpty
+  private String identifier;
+  @NotEmpty
+  private String name;
+  private String description;
+  @Valid
+  private List<Field> fields;
+  private String createdBy;
+  private String createdOn;
+  private String lastModifiedBy;
+  private String lastModifiedOn;
+
+  public Catalog() {
+    super();
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public List<Field> getFields() {
+    return this.fields;
+  }
+
+  public void setFields(final List<Field> fields) {
+    this.fields = fields;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+
+  public String getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final String lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Field.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Field.java
new file mode 100644
index 0000000..63192d2
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Field.java
@@ -0,0 +1,159 @@
+/*
+ * 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.catalog.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+public class Field {
+
+  public enum DataType {
+    TEXT,
+    NUMBER,
+    DATE,
+    SINGLE_SELECTION,
+    MULTI_SELECTION
+  }
+
+  @NotEmpty
+  private String identifier;
+  @NotNull
+  private DataType dataType;
+  @NotEmpty
+  private String label;
+  private String hint;
+  private String description;
+  @Valid
+  private List<Option> options;
+  private Boolean mandatory;
+  private Integer length;
+  private Integer precision;
+  private Double minValue;
+  private Double maxValue;
+  private String createdBy;
+  private String createdOn;
+
+  public Field() {
+    super();
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getDataType() {
+    return this.dataType.name();
+  }
+
+  public void setDataType(final String dataType) {
+    this.dataType = DataType.valueOf(dataType);
+  }
+
+  public String getLabel() {
+    return this.label;
+  }
+
+  public void setLabel(final String label) {
+    this.label = label;
+  }
+
+  public String getHint() {
+    return this.hint;
+  }
+
+  public void setHint(final String hint) {
+    this.hint = hint;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public List<Option> getOptions() {
+    return this.options;
+  }
+
+  public void setOptions(final List<Option> options) {
+    this.options = options;
+  }
+
+  public Boolean getMandatory() {
+    return this.mandatory;
+  }
+
+  public void setMandatory(final Boolean mandatory) {
+    this.mandatory = mandatory;
+  }
+
+  public Integer getLength() {
+    return this.length;
+  }
+
+  public void setLength(final Integer length) {
+    this.length = length;
+  }
+
+  public Integer getPrecision() {
+    return this.precision;
+  }
+
+  public void setPrecision(final Integer precision) {
+    this.precision = precision;
+  }
+
+  public Double getMinValue() {
+    return this.minValue;
+  }
+
+  public void setMinValue(final Double minValue) {
+    this.minValue = minValue;
+  }
+
+  public Double getMaxValue() {
+    return this.maxValue;
+  }
+
+  public void setMaxValue(final Double maxValue) {
+    this.maxValue = maxValue;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Option.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Option.java
new file mode 100644
index 0000000..9965cb7
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Option.java
@@ -0,0 +1,66 @@
+/*
+ * 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.catalog.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.constraints.NotNull;
+
+public class Option {
+
+  @NotEmpty
+  private String label;
+  @NotNull
+  private Integer value;
+  private String createdBy;
+  private String createdOn;
+
+  public Option() {
+    super();
+  }
+
+  public String getLabel() {
+    return this.label;
+  }
+
+  public void setLabel(final String label) {
+    this.label = label;
+  }
+
+  public Integer getValue() {
+    return this.value;
+  }
+
+  public void setValue(final Integer value) {
+    this.value = value;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Value.java b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Value.java
new file mode 100644
index 0000000..84e9299
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/catalog/api/v1/domain/Value.java
@@ -0,0 +1,56 @@
+/*
+ * 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.catalog.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotEmpty;
+
+public class Value {
+
+  @NotEmpty
+  private String catalogIdentifier;
+  @NotEmpty
+  private String fieldIdentifier;
+  @NotEmpty
+  private String value;
+
+  public Value() {
+    super();
+  }
+
+  public String getCatalogIdentifier() {
+    return this.catalogIdentifier;
+  }
+
+  public void setCatalogIdentifier(final String catalogIdentifier) {
+    this.catalogIdentifier = catalogIdentifier;
+  }
+
+  public String getFieldIdentifier() {
+    return this.fieldIdentifier;
+  }
+
+  public void setFieldIdentifier(final String fieldIdentifier) {
+    this.fieldIdentifier = fieldIdentifier;
+  }
+
+  public String getValue() {
+    return this.value;
+  }
+
+  public void setValue(final String value) {
+    this.value = value;
+  }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..b8c09e6
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,29 @@
+group 'io.mifos'
+
+task publishApiToMavenLocal {
+    dependsOn gradle.includedBuild('api').task(':publishToMavenLocal')
+}
+
+task publishServiceToMavenLocal {
+    mustRunAfter publishApiToMavenLocal
+    dependsOn gradle.includedBuild('service').task(':publishToMavenLocal')
+}
+
+task publishComponentTestToMavenLocal {
+    mustRunAfter publishApiToMavenLocal
+    mustRunAfter publishServiceToMavenLocal
+    dependsOn gradle.includedBuild('component-test').task(':publishToMavenLocal')
+}
+
+task publishToMavenLocal {
+    group 'all'
+    dependsOn publishApiToMavenLocal
+    dependsOn publishServiceToMavenLocal
+    dependsOn publishComponentTestToMavenLocal
+}
+
+task prepareForTest {
+    group 'all'
+    dependsOn publishToMavenLocal
+    dependsOn gradle.includedBuild('component-test').task(':build')
+}
diff --git a/component-test/build.gradle b/component-test/build.gradle
new file mode 100644
index 0000000..b35f0eb
--- /dev/null
+++ b/component-test/build.gradle
@@ -0,0 +1,38 @@
+buildscript {
+    ext {
+        springBootVersion = '1.4.1.RELEASE'
+    }
+
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+    }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+    compile(
+            [group: 'io.mifos.customer', name: 'api', version: project.version],
+            [group: 'io.mifos.customer', name: 'service', version: project.version],
+            [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+            [group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
+            [group: 'io.mifos.anubis', name: 'test', version: versions.frameworkanubis],
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+    )
+}
+
+publishing {
+    publications {
+        mavenJava(MavenPublication) {
+            from components.java
+        }
+    }
+}
diff --git a/component-test/settings.gradle b/component-test/settings.gradle
new file mode 100644
index 0000000..07867cb
--- /dev/null
+++ b/component-test/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'component-test'
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/customer/TestCustomer.java b/component-test/src/main/java/io/mifos/customer/TestCustomer.java
new file mode 100644
index 0000000..a4ae4d2
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/TestCustomer.java
@@ -0,0 +1,419 @@
+/*
+ * 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.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+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.CustomerAlreadyExistsException;
+import io.mifos.customer.api.v1.client.CustomerClient;
+import io.mifos.customer.api.v1.client.CustomerNotFoundException;
+import io.mifos.customer.api.v1.client.CustomerValidationException;
+import io.mifos.customer.api.v1.domain.Address;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.api.v1.domain.ContactDetail;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.api.v1.domain.CustomerPage;
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
+import io.mifos.customer.util.AddressGenerator;
+import io.mifos.customer.util.CommandGenerator;
+import io.mifos.customer.util.ContactDetailGenerator;
+import io.mifos.customer.util.CustomerGenerator;
+import io.mifos.customer.util.IdentificationCardGenerator;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestCustomer {
+
+  private static final String APP_NAME = "customer-v1";
+
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.customer.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @ComponentScan(
+      basePackages = {
+            "io.mifos.customer.listener"
+      }
+  )
+  @Import({CustomerRestConfiguration.class})
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+  private static final String TEST_USER = "maatkare";
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+  @Autowired
+  private CustomerClient customerClient;
+
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  public TestCustomer() {
+    super();
+  }
+
+  @Before
+  public void prepareTest() {
+    userContext = tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
+  }
+
+  @After
+  public void cleanupTest() {
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(CustomerEventConstants.INITIALIZE, CustomerEventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldCreateCustomer() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final Customer createdCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertNotNull(createdCustomer);
+  }
+
+  @Test
+  public void shouldNotCreateCustomerAlreadyExists() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    try {
+      this.customerClient.createCustomer(customer);
+      Assert.fail();
+    } catch (final CustomerAlreadyExistsException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldNotCreateCustomerValidationFailed() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    customer.getAddress().setStreet(null);
+    customer.setContactDetails(null);
+
+    try {
+      this.customerClient.createCustomer(customer);
+      Assert.fail();
+    } catch (final CustomerValidationException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldFindCustomer() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final Customer foundCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertNotNull(foundCustomer);
+    Assert.assertNotNull(foundCustomer.getIdentificationCard());
+    Assert.assertNotNull(foundCustomer.getAddress());
+    Assert.assertNotNull(foundCustomer.getContactDetails());
+    Assert.assertEquals(2, foundCustomer.getContactDetails().size());
+  }
+
+  @Test
+  public void shouldNotFindCustomerNotFound() throws Exception {
+    try {
+      this.customerClient.findCustomer(RandomStringUtils.randomAlphanumeric(8));
+      Assert.fail();
+    } catch (final CustomerNotFoundException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldFetchCustomers() throws Exception {
+    Stream.of(
+        CustomerGenerator.createRandomCustomer(),
+        CustomerGenerator.createRandomCustomer(),
+        CustomerGenerator.createRandomCustomer()
+    ).forEach(customer -> {
+      this.customerClient.createCustomer(customer);
+      try {
+        this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+      } catch (final InterruptedException ex) {
+        Assert.fail(ex.getMessage());
+      }
+    });
+
+    final CustomerPage customerPage = this.customerClient.fetchCustomers(null, null, 0, 20, null, null);
+    Assert.assertTrue(customerPage.getTotalElements() >= 3);
+  }
+
+
+  @Test
+  public void shouldFetchCustomersByTerm() throws Exception {
+    final Customer randomCustomer = CustomerGenerator.createRandomCustomer();
+    final String randomCustomerIdentifier = randomCustomer.getIdentifier();
+    this.customerClient.createCustomer(randomCustomer);
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, randomCustomerIdentifier);
+
+    final CustomerPage customerPage = this.customerClient.fetchCustomers(randomCustomerIdentifier, Boolean.FALSE, 0, 20, null, null);
+    Assert.assertTrue(customerPage.getTotalElements() == 1);
+  }
+
+  @Test
+  public void shouldUpdateCustomer() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    customer.setSurname(RandomStringUtils.randomAlphanumeric(256));
+
+    this.customerClient.updateCustomer(customer.getIdentifier(), customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_CUSTOMER, customer.getIdentifier());
+
+    final Customer updatedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(customer.getSurname(), updatedCustomer.getSurname());
+  }
+
+  @Test
+  public void shouldNotUpdateCustomerNotFound() throws Exception {
+    try {
+      this.customerClient.updateCustomer(RandomStringUtils.randomAlphanumeric(8), CustomerGenerator.createRandomCustomer());
+      Assert.fail();
+    } catch (final CustomerNotFoundException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldActivateClient() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    final Customer activatedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(Customer.State.ACTIVE.name(), activatedCustomer.getCurrentState());
+  }
+
+  @Test
+  public void shouldLockClient() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.LOCK, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.LOCK_CUSTOMER, customer.getIdentifier());
+
+    final Customer lockedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(Customer.State.LOCKED.name(), lockedCustomer.getCurrentState());
+  }
+
+  @Test
+  public void shouldUnlockClient() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.LOCK, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.LOCK_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.UNLOCK, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.UNLOCK_CUSTOMER, customer.getIdentifier());
+
+    final Customer unlockedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(Customer.State.ACTIVE.name(), unlockedCustomer.getCurrentState());
+  }
+
+  @Test
+  public void shouldCloseClient() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.CLOSE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.CLOSE_CUSTOMER, customer.getIdentifier());
+
+    final Customer closedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(Customer.State.CLOSED.name(), closedCustomer.getCurrentState());
+  }
+
+  @Test
+  public void shouldReopenClient() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.CLOSE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.CLOSE_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.REOPEN, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.REOPEN_CUSTOMER, customer.getIdentifier());
+
+    final Customer reopenedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    Assert.assertEquals(Customer.State.ACTIVE.name(), reopenedCustomer.getCurrentState());
+  }
+
+  @Test
+  public void shouldFetchCommands() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerClient.customerCommand(customer.getIdentifier(), CommandGenerator.create(Command.Action.ACTIVATE, "Test"));
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, customer.getIdentifier());
+
+    final List<Command> commands = this.customerClient.fetchCustomerCommands(customer.getIdentifier());
+    Assert.assertTrue(commands.size() == 1);
+  }
+
+  @Test
+  public void shouldUpdateAddress() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final Address address = AddressGenerator.createRandomAddress();
+    this.customerClient.putAddress(customer.getIdentifier(), address);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_ADDRESS, customer.getIdentifier());
+
+    final Customer changedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    final Address changedAddress = changedCustomer.getAddress();
+
+    Assert.assertEquals(address.getCity(), changedAddress.getCity());
+    Assert.assertEquals(address.getCountryCode(), changedAddress.getCountryCode());
+    Assert.assertEquals(address.getPostalCode(), changedAddress.getPostalCode());
+    Assert.assertEquals(address.getRegion(), changedAddress.getRegion());
+    Assert.assertEquals(address.getStreet(), changedAddress.getStreet());
+    Assert.assertEquals(address.getCountry(), changedAddress.getCountry());
+  }
+
+  @Test
+  public void shouldUpdateContactDetails() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final ContactDetail contactDetail = ContactDetailGenerator.createRandomContactDetail();
+    this.customerClient.putContactDetails(customer.getIdentifier(), Collections.singletonList(contactDetail));
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_CONTACT_DETAILS, customer.getIdentifier());
+
+    final Customer changedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    final List<ContactDetail> changedContactDetails = changedCustomer.getContactDetails();
+    Assert.assertEquals(1, changedContactDetails.size());
+    final ContactDetail changedContactDetail = changedContactDetails.get(0);
+    Assert.assertEquals(contactDetail.getType(), changedContactDetail.getType());
+    Assert.assertEquals(contactDetail.getValue(), changedContactDetail.getValue());
+    Assert.assertEquals(contactDetail.getValidated(), changedContactDetail.getValidated());
+    Assert.assertEquals(contactDetail.getGroup(), changedContactDetail.getGroup());
+    Assert.assertEquals(contactDetail.getPreferenceLevel(), changedContactDetail.getPreferenceLevel());
+  }
+
+  @Test
+  public void shouldUpdateIdentificationCard() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final IdentificationCard newIdentificationCard = IdentificationCardGenerator.createRandomIdentificationCard();
+    this.customerClient.putIdentificationCard(customer.getIdentifier(), newIdentificationCard);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_IDENTIFICATION_CARD, customer.getIdentifier());
+
+    final Customer changedCustomer = this.customerClient.findCustomer(customer.getIdentifier());
+    final IdentificationCard changedIdentificationCard = changedCustomer.getIdentificationCard();
+    Assert.assertNotNull(changedIdentificationCard);
+    Assert.assertEquals(newIdentificationCard.getType(), changedIdentificationCard.getType());
+    Assert.assertEquals(newIdentificationCard.getIssuer(), changedIdentificationCard.getIssuer());
+    Assert.assertEquals(newIdentificationCard.getNumber(), changedIdentificationCard.getNumber());
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/TestInfrastructure.java b/component-test/src/main/java/io/mifos/customer/TestInfrastructure.java
new file mode 100644
index 0000000..7a1d338
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/TestInfrastructure.java
@@ -0,0 +1,107 @@
+/*
+ * 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.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+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.service.rest.config.CustomerRestConfiguration;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestInfrastructure {
+  private static final String APP_NAME = "customer-v1";
+
+  @Configuration
+  @EnableEventRecording
+  @ComponentScan(
+      basePackages = {
+          "io.mifos.customer.listener"
+      }
+  )
+  @Import({CustomerRestConfiguration.class})
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  @Autowired
+  private DataSource dataSource;
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(CustomerEventConstants.INITIALIZE, CustomerEventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldInitializeCustomer() throws Exception {
+    try (final Connection connection = this.dataSource.getConnection()) {
+      final DatabaseMetaData databaseMetaData = connection.getMetaData();
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_customers", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_addresses", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_contact_details", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_identification_cards", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_commands", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_task_definitions", null).next());
+      Assert.assertTrue(databaseMetaData.getTables(null, null, "maat_task_instances", null).next());
+    }
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/TestTaskDefinition.java b/component-test/src/main/java/io/mifos/customer/TestTaskDefinition.java
new file mode 100644
index 0000000..25709cc
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/TestTaskDefinition.java
@@ -0,0 +1,212 @@
+/*
+ * 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.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+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.CustomerClient;
+import io.mifos.customer.api.v1.client.TaskAlreadyExistsException;
+import io.mifos.customer.api.v1.client.TaskNotFoundException;
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
+import io.mifos.customer.util.TaskGenerator;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestTaskDefinition {
+  private static final String APP_NAME = "customer-v1";
+
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.customer.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @ComponentScan(
+      basePackages = {
+          "io.mifos.customer.listener"
+      }
+  )
+  @Import({CustomerRestConfiguration.class})
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+  @Autowired
+  private CustomerClient customerClient;
+
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  @Before
+  public void prepareTest() {
+    String TEST_USER = "maatkare";
+    userContext = tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
+  }
+
+  @After
+  public void cleanupTest() {
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(CustomerEventConstants.INITIALIZE, CustomerEventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldCreateTask() throws Exception {
+    final TaskDefinition taskDefinition = TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE);
+    this.customerClient.createTask(taskDefinition);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    final TaskDefinition savedTaskDefinition = this.customerClient.findTask(taskDefinition.getIdentifier());
+    Assert.assertNotNull(savedTaskDefinition);
+    Assert.assertEquals(taskDefinition.getIdentifier(), savedTaskDefinition.getIdentifier());
+    Assert.assertEquals(taskDefinition.getType(), savedTaskDefinition.getType());
+    Assert.assertEquals(taskDefinition.getName(), savedTaskDefinition.getName());
+    Assert.assertEquals(taskDefinition.getDescription(), savedTaskDefinition.getDescription());
+    Assert.assertEquals(taskDefinition.getMandatory(), savedTaskDefinition.getMandatory());
+    Assert.assertEquals(taskDefinition.getPredefined(), savedTaskDefinition.getPredefined());
+    Assert.assertArrayEquals(taskDefinition.getCommands(), savedTaskDefinition.getCommands());
+  }
+
+  @Test
+  public void shouldNotCreateTaskAlreadyExists() throws Exception {
+    final TaskDefinition taskDefinition = TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE);
+    this.customerClient.createTask(taskDefinition);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    try {
+      this.customerClient.createTask(taskDefinition);
+      Assert.fail();
+    } catch (final TaskAlreadyExistsException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldFindTask() throws Exception {
+    final TaskDefinition taskDefinition = TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE);
+    this.customerClient.createTask(taskDefinition);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    final TaskDefinition savedTaskDefinition = this.customerClient.findTask(taskDefinition.getIdentifier());
+    Assert.assertNotNull(savedTaskDefinition);
+  }
+
+  @Test
+  public void shouldNotFindTaskNotFound() throws Exception {
+    try {
+      this.customerClient.findTask(RandomStringUtils.randomAlphanumeric(8));
+      Assert.fail();
+    } catch (TaskNotFoundException ex) {
+      // do nothing, expected
+    }
+  }
+
+  @Test
+  public void shouldFetchAllTasks() throws Exception {
+    Arrays.asList(
+        TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE),
+        TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE),
+        TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE)
+    ).forEach(taskDefinition -> {
+      this.customerClient.createTask(taskDefinition);
+      try {
+        this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+      } catch (final InterruptedException ex) {
+        Assert.fail();
+      }
+    });
+
+    final List<TaskDefinition> taskDefinitions = this.customerClient.fetchAllTasks();
+    Assert.assertTrue(taskDefinitions.size() >= 3);
+  }
+
+  @Test
+  public void shouldUpdateTask() throws Exception {
+    final TaskDefinition taskDefinition = TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.FALSE, Boolean.FALSE);
+    this.customerClient.createTask(taskDefinition);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    final TaskDefinition updatedTaskDefinition = TaskGenerator.createRandomTask(TaskDefinition.Type.CUSTOM, Boolean.TRUE, Boolean.TRUE);
+    updatedTaskDefinition.setIdentifier(taskDefinition.getIdentifier());
+    updatedTaskDefinition.setCommands(TaskDefinition.Command.REOPEN.name());
+
+    this.customerClient.updateTask(updatedTaskDefinition.getIdentifier(), updatedTaskDefinition);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_TASK, taskDefinition.getIdentifier());
+
+    final TaskDefinition fetchedTaskDefinition = this.customerClient.findTask(updatedTaskDefinition.getIdentifier());
+    Assert.assertNotNull(fetchedTaskDefinition);
+    Assert.assertEquals(updatedTaskDefinition.getIdentifier(), fetchedTaskDefinition.getIdentifier());
+    Assert.assertEquals(updatedTaskDefinition.getType(), fetchedTaskDefinition.getType());
+    Assert.assertEquals(updatedTaskDefinition.getName(), fetchedTaskDefinition.getName());
+    Assert.assertEquals(updatedTaskDefinition.getDescription(), fetchedTaskDefinition.getDescription());
+    Assert.assertEquals(updatedTaskDefinition.getMandatory(), fetchedTaskDefinition.getMandatory());
+    Assert.assertEquals(updatedTaskDefinition.getPredefined(), fetchedTaskDefinition.getPredefined());
+    Assert.assertArrayEquals(updatedTaskDefinition.getCommands(), fetchedTaskDefinition.getCommands());
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/TestTaskInstance.java b/component-test/src/main/java/io/mifos/customer/TestTaskInstance.java
new file mode 100644
index 0000000..e398735
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/TestTaskInstance.java
@@ -0,0 +1,206 @@
+/*
+ * 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.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+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.CustomerClient;
+import io.mifos.customer.api.v1.client.TaskExecutionException;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
+import io.mifos.customer.util.CustomerGenerator;
+import io.mifos.customer.util.IdentificationCardGenerator;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestTaskInstance {
+  private static final String APP_NAME = "customer-v1";
+
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.customer.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @ComponentScan(
+      basePackages = {
+          "io.mifos.customer.listener"
+      }
+  )
+  @Import({CustomerRestConfiguration.class})
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+  @Autowired
+  private CustomerClient customerClient;
+
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  @Before
+  public void prepareTest() {
+    String TEST_USER = "maatkare";
+    userContext = tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
+  }
+
+  @After
+  public void cleanupTest() {
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(CustomerEventConstants.INITIALIZE, CustomerEventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldProceedCustomerWorkFlowWithMandatoryIdTasks() throws Exception {
+    // create a predefined and mandatory task validating every state transition
+    // has a an ID card assigned
+    final TaskDefinition taskDefinition = new TaskDefinition();
+    taskDefinition.setIdentifier("nat-id");
+    taskDefinition.setType(TaskDefinition.Type.ID_CARD.name());
+    taskDefinition.setName("National ID is needed.");
+    taskDefinition.setCommands(
+        TaskDefinition.Command.ACTIVATE.name(),
+        TaskDefinition.Command.UNLOCK.name(),
+        TaskDefinition.Command.REOPEN.name()
+    );
+    taskDefinition.setPredefined(Boolean.TRUE);
+    taskDefinition.setMandatory(Boolean.TRUE);
+
+    this.customerClient.createTask(taskDefinition);
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    // create a random customer
+    final Customer randomCustomer = CustomerGenerator.createRandomCustomer();
+    randomCustomer.setIdentificationCard(null);
+    this.customerClient.createCustomer(randomCustomer);
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, randomCustomer.getIdentifier());
+
+    // try to activate the customer with missing ID card
+    final Command activateCustomer = new Command();
+    activateCustomer.setAction(Command.Action.ACTIVATE.name());
+    this.customerClient.customerCommand(randomCustomer.getIdentifier(), activateCustomer);
+    Assert.assertFalse(this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, randomCustomer.getIdentifier()));
+
+    // assert client is still in pending
+    final Customer stillPendingCustomer = this.customerClient.findCustomer(randomCustomer.getIdentifier());
+    Assert.assertEquals(Customer.State.PENDING.name(), stillPendingCustomer.getCurrentState());
+
+    try {
+      // try to close the task
+      this.customerClient.taskForCustomerExecuted(randomCustomer.getIdentifier(), taskDefinition.getIdentifier());
+      Assert.fail();
+    } catch (final TaskExecutionException ex) {
+      // do nothing, expected
+    }
+
+    // set the ID card for the customer
+    this.customerClient.putIdentificationCard(randomCustomer.getIdentifier(), IdentificationCardGenerator.createRandomIdentificationCard());
+    this.eventRecorder.wait(CustomerEventConstants.PUT_IDENTIFICATION_CARD, randomCustomer.getIdentifier());
+
+    // close the task
+    this.customerClient.taskForCustomerExecuted(randomCustomer.getIdentifier(), taskDefinition.getIdentifier());
+    this.eventRecorder.wait(CustomerEventConstants.PUT_CUSTOMER, randomCustomer.getIdentifier());
+
+    // try to activate customer
+    this.customerClient.customerCommand(randomCustomer.getIdentifier(), activateCustomer);
+    this.eventRecorder.wait(CustomerEventConstants.ACTIVATE_CUSTOMER, randomCustomer.getIdentifier());
+
+    // assert customer is now active
+    final Customer activatedCustomer = this.customerClient.findCustomer(randomCustomer.getIdentifier());
+    Assert.assertEquals(Customer.State.ACTIVE.name(), activatedCustomer.getCurrentState());
+
+    // set predefined to false so it does not have a side effect on other tests
+    taskDefinition.setPredefined(false);
+    this.customerClient.updateTask(taskDefinition.getIdentifier(), taskDefinition);
+    this.eventRecorder.wait(CustomerEventConstants.PUT_TASK, taskDefinition.getIdentifier());
+  }
+
+  @Test
+  public void shouldListNonMandatoryTasks() throws Exception{
+    final TaskDefinition taskDefinition = new TaskDefinition();
+    taskDefinition.setIdentifier("customid");
+    taskDefinition.setType(TaskDefinition.Type.CUSTOM.name());
+    taskDefinition.setName("Do the barrel roll");
+    taskDefinition.setCommands(
+            TaskDefinition.Command.ACTIVATE.name(),
+            TaskDefinition.Command.UNLOCK.name(),
+            TaskDefinition.Command.REOPEN.name()
+    );
+    taskDefinition.setPredefined(Boolean.TRUE);
+    taskDefinition.setMandatory(Boolean.FALSE);
+
+    this.customerClient.createTask(taskDefinition);
+    this.eventRecorder.wait(CustomerEventConstants.POST_TASK, taskDefinition.getIdentifier());
+
+    // create a random customer
+    final Customer randomCustomer = CustomerGenerator.createRandomCustomer();
+    this.customerClient.createCustomer(randomCustomer);
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, randomCustomer.getIdentifier());
+
+    final List<TaskDefinition> tasksForCustomer = this.customerClient.findTasksForCustomer(randomCustomer.getIdentifier(), false);
+
+    Assert.assertEquals(1, tasksForCustomer.size());
+  }
+
+}
diff --git a/component-test/src/main/java/io/mifos/customer/catalog/TestCatalog.java b/component-test/src/main/java/io/mifos/customer/catalog/TestCatalog.java
new file mode 100644
index 0000000..9f9f4ad
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/catalog/TestCatalog.java
@@ -0,0 +1,201 @@
+/*
+ * 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.catalog;
+
+import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+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.CustomerClient;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.catalog.api.v1.CatalogEventConstants;
+import io.mifos.customer.catalog.api.v1.client.CatalogClient;
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.api.v1.domain.Field;
+import io.mifos.customer.catalog.api.v1.domain.Value;
+import io.mifos.customer.catalog.util.CatalogGenerator;
+import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
+import io.mifos.customer.util.CustomerGenerator;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestCatalog {
+  private static final String APP_NAME = "customer-v1";
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.customer.api.v1.client", "io.mifos.customer.catalog.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @ComponentScan(
+      basePackages = {
+          "io.mifos.customer.listener",
+          "io.mifos.customer.catalog.listener"
+      }
+  )
+  @Import({CustomerRestConfiguration.class})
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+
+  @Autowired
+  private CatalogClient catalogClient;
+  @Autowired
+  private CustomerClient customerClient;
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  public TestCatalog() {
+    super();
+  }
+
+  @Before
+  public void prepareTest() {
+    final String TEST_USER = "nunkare";
+    userContext = tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
+  }
+
+  @After
+  public void cleanupTest() {
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(CustomerEventConstants.INITIALIZE, CustomerEventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldCreateCatalog() throws Exception {
+    final Catalog catalog = CatalogGenerator.createRandomCatalog();
+
+    this.catalogClient.createCatalog(catalog);
+    this.eventRecorder.wait(CatalogEventConstants.POST_CATALOG, catalog.getIdentifier());
+
+    final Catalog savedCatalog = this.catalogClient.findCatalog(catalog.getIdentifier());
+    Assert.assertEquals(catalog.getIdentifier(), savedCatalog.getIdentifier());
+    Assert.assertEquals(catalog.getName(), savedCatalog.getName());
+    Assert.assertEquals(catalog.getDescription(), savedCatalog.getDescription());
+    Assert.assertNotNull(savedCatalog.getCreatedBy());
+    Assert.assertNotNull(savedCatalog.getCreatedOn());
+    Assert.assertTrue(catalog.getFields().size() == savedCatalog.getFields().size());
+    savedCatalog.getFields().forEach(field -> {
+      if (field.getOptions() != null) {
+        Assert.assertTrue(field.getOptions().size() > 0);
+        Assert.assertEquals(Integer.valueOf(1), field.getOptions().get(0).getValue());
+      } else {
+        if (field.getDataType().equals(Field.DataType.SINGLE_SELECTION.name())) {
+          Assert.fail();
+        }
+      }
+    });
+  }
+
+  @Test
+  public void shouldFetchCatalogs() throws Exception {
+    final List<Catalog> catalogs = Arrays.asList(
+        CatalogGenerator.createRandomCatalog(),
+        CatalogGenerator.createRandomCatalog(),
+        CatalogGenerator.createRandomCatalog());
+
+    catalogs.forEach(catalog -> {
+      this.catalogClient.createCatalog(catalog);
+      try {
+        this.eventRecorder.wait(CatalogEventConstants.POST_CATALOG, catalog.getIdentifier());
+      } catch (InterruptedException e) {
+        Assert.fail();
+      }
+    });
+
+    final List<Catalog> fetchedCatalogs = this.catalogClient.fetchCatalogs();
+    Assert.assertTrue(fetchedCatalogs.size() >= 3);
+  }
+
+  @Test
+  public void shouldSaveCustomValues() throws Exception {
+    final Catalog randomCatalog = CatalogGenerator.createRandomCatalog();
+
+    this.catalogClient.createCatalog(randomCatalog);
+    this.eventRecorder.wait(CatalogEventConstants.POST_CATALOG, randomCatalog.getIdentifier());
+
+    final Customer randomCustomer = CustomerGenerator.createRandomCustomer();
+    randomCustomer.setCustomValues(randomCatalog.getFields()
+        .stream()
+        .map(field -> {
+          final Value value = new Value();
+          value.setCatalogIdentifier(randomCatalog.getIdentifier());
+          value.setFieldIdentifier(field.getIdentifier());
+          switch (Field.DataType.valueOf(field.getDataType())) {
+            case NUMBER:
+              value.setValue("123.45");
+              break;
+            case SINGLE_SELECTION:
+              value.setValue("1");
+          }
+          return value;
+        })
+        .collect(Collectors.toList())
+    );
+
+    this.customerClient.createCustomer(randomCustomer);
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, randomCustomer.getIdentifier());
+
+    final Customer savedCustomer = this.customerClient.findCustomer(randomCustomer.getIdentifier());
+    Assert.assertTrue(savedCustomer.getCustomValues().size() == 2);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/catalog/listener/CatalogEventListener.java b/component-test/src/main/java/io/mifos/customer/catalog/listener/CatalogEventListener.java
new file mode 100644
index 0000000..eecbe1f
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/catalog/listener/CatalogEventListener.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.catalog.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.catalog.api.v1.CatalogEventConstants;
+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;
+
+@SuppressWarnings("unused")
+@Component
+public class CatalogEventListener {
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public CatalogEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CatalogEventConstants.SELECTOR_POST_CATALOG
+  )
+  public void customerCreatedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                   final String payload) {
+    this.eventRecorder.event(tenant, CatalogEventConstants.POST_CATALOG, payload, String.class);
+  }}
diff --git a/component-test/src/main/java/io/mifos/customer/catalog/util/CatalogGenerator.java b/component-test/src/main/java/io/mifos/customer/catalog/util/CatalogGenerator.java
new file mode 100644
index 0000000..632e7eb
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/catalog/util/CatalogGenerator.java
@@ -0,0 +1,64 @@
+/*
+ * 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.catalog.util;
+
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.api.v1.domain.Field;
+import io.mifos.customer.catalog.api.v1.domain.Option;
+import org.apache.commons.lang3.RandomStringUtils;
+
+import java.util.Arrays;
+
+public class CatalogGenerator {
+
+  private CatalogGenerator() {
+    super();
+  }
+
+  public static Catalog createRandomCatalog() {
+    final Catalog catalog = new Catalog();
+    catalog.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    catalog.setName(RandomStringUtils.randomAlphanumeric(256));
+    catalog.setDescription(RandomStringUtils.randomAlphanumeric(4096));
+
+    final Field simpleField = new Field();
+    simpleField.setDataType(Field.DataType.NUMBER.name());
+    simpleField.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    simpleField.setLabel(RandomStringUtils.randomAlphanumeric(256));
+    simpleField.setHint(RandomStringUtils.randomAlphanumeric(512));
+    simpleField.setDescription(RandomStringUtils.randomAlphanumeric(4096));
+    simpleField.setMandatory(Boolean.FALSE);
+    simpleField.setLength(10);
+    simpleField.setPrecision(2);
+    simpleField.setMinValue(0.00D);
+    simpleField.setMaxValue(99999999.99D);
+
+    final Field optionField = new Field();
+    optionField.setDataType(Field.DataType.SINGLE_SELECTION.name());
+    optionField.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    optionField.setLabel(RandomStringUtils.randomAlphanumeric(256));
+    optionField.setHint(RandomStringUtils.randomAlphanumeric(512));
+    optionField.setDescription(RandomStringUtils.randomAlphanumeric(4096));
+    optionField.setMandatory(Boolean.FALSE);
+    final Option option = new Option();
+    option.setLabel(RandomStringUtils.randomAlphanumeric(256));
+    option.setValue(1);
+    optionField.setOptions(Arrays.asList(option));
+
+    catalog.setFields(Arrays.asList(simpleField, optionField));
+    return catalog;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java b/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java
new file mode 100644
index 0000000..51f711f
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java
@@ -0,0 +1,126 @@
+/*
+ * 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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CustomerEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public CustomerEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_POST_CUSTOMER
+  )
+  public void customerCreatedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                   final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.POST_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_PUT_CUSTOMER
+  )
+  public void customerUpdatedEvents(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                    final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_ACTIVATE_CUSTOMER
+  )
+  public void customerActivatedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                     final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.ACTIVATE_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_LOCK_CUSTOMER
+  )
+  public void customerLockedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                  final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.LOCK_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_UNLOCK_CUSTOMER
+  )
+  public void customerUnlockedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                    final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.UNLOCK_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_CLOSE_CUSTOMER
+  )
+  public void customerClosedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                  final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.CLOSE_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_REOPEN_CUSTOMER
+  )
+  public void customerReopenedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                    final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.REOPEN_CUSTOMER, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_PUT_ADDRESS
+  )
+  public void addressChangedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                  final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_ADDRESS, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_PUT_CONTACT_DETAILS
+  )
+  public void contactDetailsChangedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                  final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_CONTACT_DETAILS, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_PUT_IDENTIFICATION_CARD
+  )
+  public void identificationCardChangedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                             final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_IDENTIFICATION_CARD, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/listener/MigrationEventListener.java b/component-test/src/main/java/io/mifos/customer/listener/MigrationEventListener.java
new file mode 100644
index 0000000..61e7a70
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/listener/MigrationEventListener.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+@Component
+public class MigrationEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public MigrationEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_INITIALIZE
+  )
+  public void initialized(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                          final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.INITIALIZE, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/listener/TaskEventListener.java b/component-test/src/main/java/io/mifos/customer/listener/TaskEventListener.java
new file mode 100644
index 0000000..4d76476
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/listener/TaskEventListener.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.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.customer.api.v1.CustomerEventConstants;
+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;
+
+@SuppressWarnings("unused")
+@Component
+public class TaskEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public TaskEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_POST_TASK
+  )
+  public void taskCreatedEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                               final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.POST_TASK, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = CustomerEventConstants.DESTINATION,
+      selector = CustomerEventConstants.SELECTOR_PUT_TASK
+  )
+  public void taskUpdateEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                               final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_TASK, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/AddressGenerator.java b/component-test/src/main/java/io/mifos/customer/util/AddressGenerator.java
new file mode 100644
index 0000000..a21a469
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/AddressGenerator.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.util;
+
+import io.mifos.customer.api.v1.domain.Address;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public class AddressGenerator {
+
+  private AddressGenerator() {
+    super();
+  }
+
+  public static Address createRandomAddress() {
+    final Address address = new Address();
+    address.setStreet(RandomStringUtils.randomAlphanumeric(256));
+    address.setCity(RandomStringUtils.randomAlphanumeric(256));
+    address.setPostalCode(RandomStringUtils.randomAlphanumeric(32));
+    address.setRegion(RandomStringUtils.randomAlphanumeric(256));
+    address.setPostalCode(RandomStringUtils.randomAlphanumeric(32));
+    address.setCountryCode(RandomStringUtils.randomAlphanumeric(2));
+    address.setCountry(RandomStringUtils.randomAlphanumeric(256));
+    return address;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/CommandGenerator.java b/component-test/src/main/java/io/mifos/customer/util/CommandGenerator.java
new file mode 100644
index 0000000..933a913
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/CommandGenerator.java
@@ -0,0 +1,32 @@
+/*
+ * 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.util;
+
+import io.mifos.customer.api.v1.domain.Command;
+
+public final class CommandGenerator {
+
+  private CommandGenerator() {
+    super();
+  }
+
+  public static Command create(final Command.Action action, final String comment) {
+    final Command command = new Command();
+    command.setAction(action.name());
+    command.setComment(comment);
+    return command;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/ContactDetailGenerator.java b/component-test/src/main/java/io/mifos/customer/util/ContactDetailGenerator.java
new file mode 100644
index 0000000..f057ff1
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/ContactDetailGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * 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.util;
+
+import io.mifos.customer.api.v1.domain.ContactDetail;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public final class ContactDetailGenerator {
+
+  private ContactDetailGenerator() {
+    super();
+  }
+
+  public static ContactDetail createRandomContactDetail() {
+    final ContactDetail contactDetail = new ContactDetail();
+    contactDetail.setType(ContactDetail.Type.MOBILE.name());
+    contactDetail.setGroup(ContactDetail.Group.PRIVATE.name());
+    contactDetail.setValue(RandomStringUtils.randomAlphanumeric(32));
+    contactDetail.setPreferenceLevel(Integer.valueOf(1));
+    contactDetail.setValidated(Boolean.FALSE);
+    return contactDetail;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/CustomerGenerator.java b/component-test/src/main/java/io/mifos/customer/util/CustomerGenerator.java
new file mode 100644
index 0000000..88e8259
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/CustomerGenerator.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.util;
+
+import io.mifos.core.lang.DateOfBirth;
+import io.mifos.customer.api.v1.domain.Customer;
+import org.apache.commons.lang3.RandomStringUtils;
+
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.Arrays;
+
+public final class CustomerGenerator {
+
+  private CustomerGenerator() {
+    super();
+  }
+
+  public static Customer createRandomCustomer() {
+    final Customer customer = new Customer();
+    customer.setIdentifier(RandomStringUtils.randomAlphanumeric(8));
+    customer.setType(Customer.Type.PERSON.name());
+    customer.setGivenName(RandomStringUtils.randomAlphanumeric(256));
+    customer.setMiddleName(RandomStringUtils.randomAlphanumeric(256));
+    customer.setSurname(RandomStringUtils.randomAlphanumeric(256));
+    customer.setDateOfBirth(DateOfBirth.fromLocalDate(LocalDate.now(Clock.systemUTC())));
+    customer.setIdentificationCard(IdentificationCardGenerator.createRandomIdentificationCard());
+    customer.setAssignedOffice(RandomStringUtils.randomAlphanumeric(8));
+    customer.setAssignedEmployee(RandomStringUtils.randomAlphanumeric(8));
+    customer.setCurrentState(Customer.State.PENDING.name());
+    customer.setAddress(AddressGenerator.createRandomAddress());
+    customer.setContactDetails(Arrays.asList(ContactDetailGenerator.createRandomContactDetail(),
+        ContactDetailGenerator.createRandomContactDetail()));
+    return customer;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/IdentificationCardGenerator.java b/component-test/src/main/java/io/mifos/customer/util/IdentificationCardGenerator.java
new file mode 100644
index 0000000..d3c2b7d
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/IdentificationCardGenerator.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.util;
+
+import io.mifos.customer.api.v1.domain.ExpirationDate;
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+import org.apache.commons.lang3.RandomStringUtils;
+
+import java.time.Clock;
+import java.time.LocalDate;
+
+public class IdentificationCardGenerator {
+
+  private IdentificationCardGenerator() {
+    super();
+  }
+
+  public static IdentificationCard createRandomIdentificationCard() {
+    final IdentificationCard identificationCard = new IdentificationCard();
+    identificationCard.setType(RandomStringUtils.randomAlphanumeric(128));
+    identificationCard.setNumber(RandomStringUtils.randomAlphanumeric(32));
+    identificationCard.setExpirationDate(ExpirationDate.fromLocalDate(LocalDate.now(Clock.systemUTC()).plusYears(2L)));
+    identificationCard.setIssuer(RandomStringUtils.randomAlphanumeric(256));
+    return identificationCard;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/util/TaskGenerator.java b/component-test/src/main/java/io/mifos/customer/util/TaskGenerator.java
new file mode 100644
index 0000000..03dbf86
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/customer/util/TaskGenerator.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.util;
+
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public final class TaskGenerator {
+
+  private TaskGenerator() {
+    super();
+  }
+
+  public static TaskDefinition createRandomTask(final TaskDefinition.Type type, final Boolean mandatory, final Boolean predefined) {
+    final TaskDefinition taskDefinition = new TaskDefinition();
+    taskDefinition.setIdentifier(RandomStringUtils.randomAlphanumeric(8));
+    taskDefinition.setType(type.name());
+    taskDefinition.setName(RandomStringUtils.randomAlphanumeric(256));
+    taskDefinition.setDescription(RandomStringUtils.randomAlphanumeric(2048));
+    taskDefinition.setCommands(TaskDefinition.Command.ACTIVATE.name());
+    taskDefinition.setMandatory(mandatory);
+    taskDefinition.setPredefined(predefined);
+    return taskDefinition;
+  }
+}
diff --git a/component-test/src/main/resources/logback.xml b/component-test/src/main/resources/logback.xml
new file mode 100644
index 0000000..c29d3cd
--- /dev/null
+++ b/component-test/src/main/resources/logback.xml
@@ -0,0 +1,40 @@
+<!--
+
+    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.
+
+-->
+<configuration>
+    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>customer.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>customer.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>3GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="INFO">
+        <appender-ref ref="FILE"/>
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
\ No newline at end of file
diff --git a/docs/customer-state-machine.uxf b/docs/customer-state-machine.uxf
new file mode 100644
index 0000000..81f24e6
--- /dev/null
+++ b/docs/customer-state-machine.uxf
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<diagram program="umlet" version="14.1.1">
+  <help_text>fontsize=12
+fontfamily=Monospaced
+</help_text>
+  <zoom_level>10</zoom_level>
+  <element>
+    <id>UMLState</id>
+    <coordinates>
+      <x>440</x>
+      <y>90</y>
+      <w>90</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>PENDING</panel_attributes>
+    <additional_attributes/>
+  </element>
+  <element>
+    <id>UMLState</id>
+    <coordinates>
+      <x>440</x>
+      <y>180</y>
+      <w>90</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>ACTIVE</panel_attributes>
+    <additional_attributes/>
+  </element>
+  <element>
+    <id>UMLState</id>
+    <coordinates>
+      <x>260</x>
+      <y>180</y>
+      <w>90</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>LOCKED</panel_attributes>
+    <additional_attributes/>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>470</x>
+      <y>120</y>
+      <w>80</w>
+      <h>80</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+activate</panel_attributes>
+    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>340</x>
+      <y>170</y>
+      <w>120</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+lock</panel_attributes>
+    <additional_attributes>100.0;20.0;10.0;20.0</additional_attributes>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>340</x>
+      <y>190</y>
+      <w>120</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+unlock</panel_attributes>
+    <additional_attributes>10.0;20.0;100.0;20.0</additional_attributes>
+  </element>
+  <element>
+    <id>UMLState</id>
+    <coordinates>
+      <x>440</x>
+      <y>270</y>
+      <w>90</w>
+      <h>40</h>
+    </coordinates>
+    <panel_attributes>CLOSED</panel_attributes>
+    <additional_attributes/>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>470</x>
+      <y>210</y>
+      <w>70</w>
+      <h>80</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+delete</panel_attributes>
+    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>520</x>
+      <y>190</y>
+      <w>110</w>
+      <h>120</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+reopen</panel_attributes>
+    <additional_attributes>10.0;100.0;50.0;100.0;50.0;10.0;10.0;10.0</additional_attributes>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>290</x>
+      <y>210</y>
+      <w>170</w>
+      <h>100</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;
+delete</panel_attributes>
+    <additional_attributes>10.0;10.0;10.0;80.0;150.0;80.0</additional_attributes>
+  </element>
+  <element>
+    <id>UMLSpecialState</id>
+    <coordinates>
+      <x>470</x>
+      <y>20</y>
+      <w>20</w>
+      <h>20</h>
+    </coordinates>
+    <panel_attributes>type=initial</panel_attributes>
+    <additional_attributes/>
+  </element>
+  <element>
+    <id>Relation</id>
+    <coordinates>
+      <x>470</x>
+      <y>30</y>
+      <w>30</w>
+      <h>80</h>
+    </coordinates>
+    <panel_attributes>lt=-&gt;</panel_attributes>
+    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>
+  </element>
+  <element>
+    <id>UMLClass</id>
+    <coordinates>
+      <x>10</x>
+      <y>20</y>
+      <w>200</w>
+      <h>230</h>
+    </coordinates>
+    <panel_attributes>halign=left
+*Actions and Tasks*
+
+Every action can have a
+comment.
+
+The transition of one
+state to another needs
+to fulfill tasks appended
+to an action.
+
+All not closed tasks will
+be saved if a customer is
+put into DELETED state
+to recall them during
+reopen.
+</panel_attributes>
+    <additional_attributes/>
+  </element>
+</diagram>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e1411fb
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..6168b84
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Mar 16 12:44:36 CET 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4453cce
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save ( ) {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/service/build.gradle b/service/build.gradle
new file mode 100644
index 0000000..1de5c1d
--- /dev/null
+++ b/service/build.gradle
@@ -0,0 +1,63 @@
+buildscript {
+    ext {
+        springBootVersion = '1.4.1.RELEASE'
+    }
+
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+    }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+apply plugin: 'spring-boot'
+
+springBoot {
+    executable = true
+    classifier = 'boot'
+}
+
+dependencies {
+    compile(
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
+            [group: 'io.mifos.customer', name: 'api', version: project.version],
+            [group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+            [group: 'com.google.code.gson', name: 'gson'],
+            [group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
+            [group: 'io.mifos.core', name: 'async', version: versions.frameworkasync],
+            [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+            [group: 'io.mifos.core', name: 'mariadb', version: versions.frameworkmariadb],
+            [group: 'io.mifos.core', name: 'command', version: versions.frameworkcommand],
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+    )
+}
+
+publishToMavenLocal.dependsOn bootRepackage
+
+publishing {
+    publications {
+        service(MavenPublication) {
+            from components.java
+            groupId project.group
+            artifactId project.name
+            version project.version
+        }
+       bootService(MavenPublication) {
+            // "boot" jar
+            artifact ("$buildDir/libs/$project.name-$version-boot.jar")
+            groupId project.group
+            artifactId ("$project.name-boot")
+            version project.version
+        }
+    }
+}
diff --git a/service/settings.gradle b/service/settings.gradle
new file mode 100644
index 0000000..081feb4
--- /dev/null
+++ b/service/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'service'
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/command/CreateCatalogCommand.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/command/CreateCatalogCommand.java
new file mode 100644
index 0000000..09be13e
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/command/CreateCatalogCommand.java
@@ -0,0 +1,31 @@
+/*
+ * 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.catalog.service.internal.command;
+
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+
+public class CreateCatalogCommand {
+  private final Catalog catalog;
+
+  public CreateCatalogCommand(final Catalog catalog) {
+    super();
+    this.catalog = catalog;
+  }
+
+  public Catalog catalog() {
+    return this.catalog;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/command/handler/CatalogAggregate.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/command/handler/CatalogAggregate.java
new file mode 100644
index 0000000..74bf29d
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/command/handler/CatalogAggregate.java
@@ -0,0 +1,64 @@
+/*
+ * 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.catalog.service.internal.command.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.customer.catalog.api.v1.CatalogEventConstants;
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.service.internal.repository.CatalogEntity;
+import io.mifos.customer.catalog.service.internal.repository.CatalogRepository;
+import io.mifos.customer.catalog.service.internal.command.CreateCatalogCommand;
+import io.mifos.customer.catalog.service.internal.mapper.CatalogMapper;
+import io.mifos.customer.catalog.service.internal.mapper.FieldMapper;
+import io.mifos.customer.service.ServiceConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.stream.Collectors;
+
+@Aggregate
+public class CatalogAggregate {
+
+  private final Logger logger;
+  private final CatalogRepository catalogRepository;
+
+  @Autowired
+  public CatalogAggregate(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                          final CatalogRepository catalogRepository) {
+    super();
+    this.logger = logger;
+    this.catalogRepository = catalogRepository;
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CatalogEventConstants.SELECTOR_NAME, selectorValue = CatalogEventConstants.POST_CATALOG)
+  public String createCatalog(final CreateCatalogCommand createCatalogCommand) {
+    final Catalog catalog = createCatalogCommand.catalog();
+    final CatalogEntity catalogEntity = CatalogMapper.map(catalog);
+    catalogEntity.setFields(catalog.getFields()
+        .stream()
+        .map(field -> FieldMapper.map(catalogEntity, field))
+        .collect(Collectors.toList())
+    );
+    this.catalogRepository.save(catalogEntity);
+    return catalog.getIdentifier();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/config/CatalogServiceConfiguration.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/config/CatalogServiceConfiguration.java
new file mode 100644
index 0000000..eeb14db
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/config/CatalogServiceConfiguration.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.catalog.service.internal.config;
+
+import io.mifos.core.mariadb.config.EnableMariaDB;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@Configuration
+@EnableMariaDB
+@ComponentScan({
+    "io.mifos.customer.catalog.service.internal.service",
+    "io.mifos.customer.catalog.service.internal.repository",
+    "io.mifos.customer.catalog.service.internal.command.handler",
+    "io.mifos.customer.catalog.service.internal.event.handler"
+})
+@EnableJpaRepositories({
+    "io.mifos.customer.catalog.service.internal.repository"
+})
+public class CatalogServiceConfiguration {
+
+  public CatalogServiceConfiguration() {
+    super();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/CatalogMapper.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/CatalogMapper.java
new file mode 100644
index 0000000..da25b3d
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/CatalogMapper.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.catalog.service.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.service.internal.repository.CatalogEntity;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public class CatalogMapper {
+
+  private CatalogMapper() {
+    super();
+  }
+
+  public static CatalogEntity map(final Catalog catalog) {
+    final CatalogEntity catalogEntity = new CatalogEntity();
+    catalogEntity.setIdentifier(catalog.getIdentifier());
+    catalogEntity.setName(catalog.getName());
+    catalogEntity.setDescription(catalog.getDescription());
+    catalogEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    catalogEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    return catalogEntity;
+  }
+
+  public static Catalog map(final CatalogEntity catalogEntity) {
+    final Catalog catalog = new Catalog();
+    catalog.setIdentifier(catalogEntity.getIdentifier());
+    catalog.setName(catalogEntity.getName());
+    catalog.setDescription(catalogEntity.getDescription());
+    catalog.setCreatedBy(catalogEntity.getCreatedBy());
+    catalog.setCreatedOn(DateConverter.toIsoString(catalogEntity.getCreatedOn()));
+    if (catalogEntity.getLastModifiedBy() != null) {
+      catalog.setLastModifiedBy(catalogEntity.getLastModifiedBy());
+      catalog.setLastModifiedOn(DateConverter.toIsoString(catalogEntity.getLastModifiedOn()));
+    }
+    return catalog;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/FieldMapper.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/FieldMapper.java
new file mode 100644
index 0000000..cb4f52f
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/FieldMapper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.catalog.service.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.customer.catalog.api.v1.domain.Field;
+import io.mifos.customer.catalog.service.internal.repository.CatalogEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.stream.Collectors;
+
+public class FieldMapper {
+
+  private FieldMapper() {
+    super();
+  }
+
+  public static FieldEntity map(final CatalogEntity catalogEntity, final Field field) {
+    final FieldEntity fieldEntity = new FieldEntity();
+    fieldEntity.setCatalog(catalogEntity);
+    fieldEntity.setIdentifier(field.getIdentifier());
+    fieldEntity.setLabel(field.getLabel());
+    fieldEntity.setHint(field.getHint());
+    fieldEntity.setDescription(field.getDescription());
+    fieldEntity.setDataType(field.getDataType());
+    fieldEntity.setMandatory(field.getMandatory());
+    fieldEntity.setLength(field.getLength());
+    fieldEntity.setPrecision(field.getPrecision());
+    fieldEntity.setMinValue(field.getMinValue());
+    fieldEntity.setMaxValue(field.getMaxValue());
+    fieldEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    fieldEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    if (field.getOptions() != null && !field.getOptions().isEmpty()) {
+      fieldEntity.setOptions(field.getOptions()
+          .stream()
+          .map(option -> OptionMapper.map(fieldEntity, option))
+          .collect(Collectors.toList())
+      );
+    }
+
+    return fieldEntity;
+  }
+
+  public static Field map(final FieldEntity fieldEntity) {
+    final Field field = new Field();
+    field.setIdentifier(fieldEntity.getIdentifier());
+    field.setLabel(fieldEntity.getLabel());
+    field.setHint(fieldEntity.getHint());
+    field.setDescription(fieldEntity.getDescription());
+    field.setDataType(fieldEntity.getDataType());
+    field.setMandatory(fieldEntity.getMandatory());
+    field.setLength(fieldEntity.getLength());
+    field.setPrecision(fieldEntity.getPrecision());
+    field.setMinValue(fieldEntity.getMinValue());
+    field.setMaxValue(fieldEntity.getMaxValue());
+    field.setCreatedBy(fieldEntity.getCreatedBy());
+    field.setCreatedOn(DateConverter.toIsoString(fieldEntity.getCreatedOn()));
+
+    if (fieldEntity.getOptions() != null && !fieldEntity.getOptions().isEmpty()) {
+      field.setOptions(
+          fieldEntity.getOptions()
+              .stream()
+              .map(OptionMapper::map)
+              .collect(Collectors.toList())
+      );
+    }
+
+    return field;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/OptionMapper.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/OptionMapper.java
new file mode 100644
index 0000000..c2e18e7
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/mapper/OptionMapper.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.catalog.service.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.customer.catalog.api.v1.domain.Option;
+import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
+import io.mifos.customer.catalog.service.internal.repository.OptionEntity;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public class OptionMapper {
+
+  private OptionMapper() {
+    super();
+  }
+
+  public static OptionEntity map(final FieldEntity fieldEntity, final Option option) {
+    final OptionEntity optionEntity = new OptionEntity();
+    optionEntity.setField(fieldEntity);
+    optionEntity.setLabel(option.getLabel());
+    optionEntity.setValue(option.getValue());
+    optionEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    optionEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    return optionEntity;
+  }
+
+  public static Option map(final OptionEntity optionEntity) {
+    final Option option = new Option();
+    option.setLabel(optionEntity.getLabel());
+    option.setValue(optionEntity.getValue());
+    option.setCreatedBy(optionEntity.getCreatedBy());
+    option.setCreatedOn(DateConverter.toIsoString(optionEntity.getCreatedOn()));
+    return option;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogEntity.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogEntity.java
new file mode 100644
index 0000000..82e16e7
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogEntity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.catalog.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Entity
+@Table(name = "nun_catalogs")
+public class CatalogEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  private Long id;
+  @Column(name = "identifier", length = 32, nullable = false)
+  private String identifier;
+  @Column(name = "a_name", length = 256, nullable = false)
+  private String name;
+  @Column(name = "description", length = 4096)
+  private String description;
+  @OneToMany(mappedBy = "catalog", cascade = CascadeType.ALL)
+  private List<FieldEntity> fields;
+  @Column(name = "created_by")
+  private String createdBy;
+  @Column(name = "created_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "last_modified_by")
+  private String lastModifiedBy;
+  @Column(name = "last_modified_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime lastModifiedOn;
+
+  public CatalogEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public List<FieldEntity> getFields() {
+    return this.fields;
+  }
+
+  public void setFields(final List<FieldEntity> fields) {
+    this.fields = fields;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+
+  public LocalDateTime getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final LocalDateTime lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogRepository.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogRepository.java
new file mode 100644
index 0000000..702cff5
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/CatalogRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.catalog.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface CatalogRepository extends JpaRepository<CatalogEntity, Long> {
+
+  Optional<CatalogEntity> findByIdentifier(final String identifier);
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldEntity.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldEntity.java
new file mode 100644
index 0000000..ac83ceb
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldEntity.java
@@ -0,0 +1,196 @@
+/*
+ * 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.catalog.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Entity
+@Table(name = "nun_fields")
+public class FieldEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  private Long id;
+  @ManyToOne
+  @JoinColumn(name = "catalog_id")
+  private CatalogEntity catalog;
+  @Column(name = "identifier", length = 32, nullable = false)
+  private String identifier;
+  @Column(name = "data_type", length = 256, nullable = false)
+  private String dataType;
+  @Column(name = "a_label", length = 256, nullable = false)
+  private String label;
+  @Column(name = "a_hint", length = 512)
+  private String hint;
+  @Column(name = "description", length = 4096)
+  @OneToMany(mappedBy = "field", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
+  private List<OptionEntity> options;
+  private String description;
+  @Column(name = "mandatory")
+  private Boolean mandatory;
+  @Column(name = "a_length")
+  private Integer length;
+  @Column(name = "a_precision")
+  private Integer precision;
+  @Column(name = "min_value")
+  private Double minValue;
+  @Column(name = "max_value")
+  private Double maxValue;
+  @Column(name = "created_by")
+  private String createdBy;
+  @Column(name = "created_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+
+  public FieldEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public CatalogEntity getCatalog() {
+    return this.catalog;
+  }
+
+  public void setCatalog(final CatalogEntity catalog) {
+    this.catalog = catalog;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getDataType() {
+    return this.dataType;
+  }
+
+  public void setDataType(final String dataType) {
+    this.dataType = dataType;
+  }
+
+  public String getLabel() {
+    return this.label;
+  }
+
+  public void setLabel(final String label) {
+    this.label = label;
+  }
+
+  public String getHint() {
+    return this.hint;
+  }
+
+  public void setHint(final String hint) {
+    this.hint = hint;
+  }
+
+  public List<OptionEntity> getOptions() {
+    return this.options;
+  }
+
+  public void setOptions(final List<OptionEntity> options) {
+    this.options = options;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public Boolean getMandatory() {
+    return this.mandatory;
+  }
+
+  public void setMandatory(final Boolean mandatory) {
+    this.mandatory = mandatory;
+  }
+
+  public Integer getLength() {
+    return this.length;
+  }
+
+  public void setLength(final Integer length) {
+    this.length = length;
+  }
+
+  public Integer getPrecision() {
+    return this.precision;
+  }
+
+  public void setPrecision(final Integer precision) {
+    this.precision = precision;
+  }
+
+  public Double getMinValue() {
+    return this.minValue;
+  }
+
+  public void setMinValue(final Double minValue) {
+    this.minValue = minValue;
+  }
+
+  public Double getMaxValue() {
+    return this.maxValue;
+  }
+
+  public void setMaxValue(final Double maxValue) {
+    this.maxValue = maxValue;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldRepository.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldRepository.java
new file mode 100644
index 0000000..0733cbb
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.catalog.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface FieldRepository extends JpaRepository<FieldEntity, Long> {
+
+  Optional<FieldEntity> findByCatalogAndIdentifier(final CatalogEntity catalog, final String identifier);
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueEntity.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueEntity.java
new file mode 100644
index 0000000..53410a6
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueEntity.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.catalog.service.internal.repository;
+
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "nun_field_values")
+public class FieldValueEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @ManyToOne(optional = false)
+  @JoinColumn(name = "entity_id")
+  private CustomerEntity customer;
+  @ManyToOne(optional = false)
+  @JoinColumn(name = "field_id")
+  private FieldEntity field;
+  @Column(name = "a_value", length = 4096, nullable = false)
+  private String value;
+
+  public FieldValueEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public FieldEntity getField() {
+    return this.field;
+  }
+
+  public void setField(final FieldEntity field) {
+    this.field = field;
+  }
+
+  public String getValue() {
+    return this.value;
+  }
+
+  public void setValue(final String value) {
+    this.value = value;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueRepository.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueRepository.java
new file mode 100644
index 0000000..4058c35
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/FieldValueRepository.java
@@ -0,0 +1,30 @@
+/*
+ * 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.catalog.service.internal.repository;
+
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface FieldValueRepository extends JpaRepository<FieldValueEntity, Long> {
+
+  List<FieldValueEntity> findByCustomer(final CustomerEntity customer);
+
+  void deleteByCustomer(final CustomerEntity customer);
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/OptionEntity.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/OptionEntity.java
new file mode 100644
index 0000000..b37c8a9
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/repository/OptionEntity.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.catalog.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "nun_options")
+public class OptionEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @ManyToOne
+  @JoinColumn(name = "field_id")
+  private FieldEntity field;
+  @Column(name = "a_label", length = 256, nullable = false)
+  private String label;
+  @Column(name = "a_value", nullable = false)
+  private Integer value;
+  @Column(name = "created_by", length = 32)
+  private String createdBy;
+  @Column(name = "created_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+
+  public OptionEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public FieldEntity getField() {
+    return this.field;
+  }
+
+  public void setField(final FieldEntity field) {
+    this.field = field;
+  }
+
+  public String getLabel() {
+    return this.label;
+  }
+
+  public void setLabel(final String label) {
+    this.label = label;
+  }
+
+  public Integer getValue() {
+    return this.value;
+  }
+
+  public void setValue(final Integer value) {
+    this.value = value;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/service/CatalogService.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/service/CatalogService.java
new file mode 100644
index 0000000..fa1773e
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/service/CatalogService.java
@@ -0,0 +1,79 @@
+/*
+ * 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.catalog.service.internal.service;
+
+import io.mifos.customer.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.service.internal.mapper.CatalogMapper;
+import io.mifos.customer.catalog.service.internal.mapper.FieldMapper;
+import io.mifos.customer.catalog.service.internal.repository.CatalogRepository;
+import io.mifos.customer.service.ServiceConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class CatalogService {
+
+  private final Logger logger;
+  private final CatalogRepository catalogRepository;
+
+  @Autowired
+  public CatalogService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                        final CatalogRepository catalogRepository) {
+    super();
+    this.logger = logger;
+    this.catalogRepository = catalogRepository;
+  }
+
+  public Boolean catalogExists(final String identifier) {
+    return this.catalogRepository.findByIdentifier(identifier).isPresent();
+  }
+
+  public List<Catalog> fetchAllCatalogs() {
+    return this.catalogRepository.findAll()
+        .stream()
+        .map(catalogEntity -> {
+          final Catalog catalog = CatalogMapper.map(catalogEntity);
+          catalog.setFields(
+              catalogEntity.getFields()
+                  .stream()
+                  .map(FieldMapper::map)
+                  .collect(Collectors.toList())
+          );
+          return catalog;
+        })
+        .collect(Collectors.toList());
+  }
+
+  public Optional<Catalog> findCatalog(final String identifier) {
+    return this.catalogRepository.findByIdentifier(identifier)
+        .map(catalogEntity -> {
+          final Catalog catalog = CatalogMapper.map(catalogEntity);
+          catalog.setFields(
+              catalogEntity.getFields()
+                  .stream()
+                  .map(FieldMapper::map)
+                  .collect(Collectors.toList())
+          );
+          return catalog;
+        });
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/internal/service/FieldValueValidator.java b/service/src/main/java/io/mifos/customer/catalog/service/internal/service/FieldValueValidator.java
new file mode 100644
index 0000000..a41f0dc
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/internal/service/FieldValueValidator.java
@@ -0,0 +1,159 @@
+/*
+ * 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.catalog.service.internal.service;
+
+import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.customer.catalog.api.v1.domain.Field;
+import io.mifos.customer.catalog.api.v1.domain.Value;
+import io.mifos.customer.catalog.service.internal.repository.CatalogEntity;
+import io.mifos.customer.catalog.service.internal.repository.CatalogRepository;
+import io.mifos.customer.catalog.service.internal.repository.FieldRepository;
+import io.mifos.customer.catalog.service.internal.repository.OptionEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
+import io.mifos.customer.service.ServiceConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Component
+public class FieldValueValidator {
+
+  private final Logger logger;
+  private final CatalogRepository catalogRepository;
+  private final FieldRepository fieldRepository;
+
+  @Autowired
+  public FieldValueValidator(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                             final CatalogRepository catalogRepository,
+                             final FieldRepository fieldRepository) {
+    super();
+    this.logger = logger;
+    this.catalogRepository = catalogRepository;
+    this.fieldRepository = fieldRepository;
+  }
+
+  public void validateValues(final List<Value> values) {
+    values.forEach(value -> {
+      final CatalogEntity catalogEntity =
+          this.catalogRepository.findByIdentifier(value.getCatalogIdentifier())
+              .orElseThrow(() -> ServiceException.notFound("Catalog {0} not found.", value.getCatalogIdentifier()));
+
+      final FieldEntity fieldEntity =
+          this.fieldRepository.findByCatalogAndIdentifier(catalogEntity, value.getFieldIdentifier())
+              .orElseThrow(() -> ServiceException.notFound("Field {0} not found.", value.getFieldIdentifier()));
+
+      final Field.DataType dataType = Field.DataType.valueOf(fieldEntity.getDataType());
+
+      switch (dataType) {
+        case TEXT:
+          this.checkLength(value, fieldEntity);
+          break;
+        case NUMBER:
+          this.checkNumber(value, fieldEntity);
+          break;
+        case DATE:
+          this.checkDate(value, fieldEntity);
+          break;
+        case SINGLE_SELECTION:
+          this.checkOptions(value, fieldEntity, fieldEntity.getOptions(), true);
+          break;
+        case MULTI_SELECTION:
+          this.checkOptions(value, fieldEntity, fieldEntity.getOptions(), false);
+          break;
+        default:
+          throw ServiceException.badRequest("Unsupported data type {0} of field {1}.", dataType.name(), fieldEntity.getLabel());
+      }
+    });
+  }
+
+  private void checkLength(final Value value, final FieldEntity fieldEntity) {
+    if (fieldEntity.getLength() != null
+        && value.getValue().length() > fieldEntity.getLength()) {
+      throw ServiceException.badRequest("Value for field {0} must be smaller than or equals {1}.",
+          fieldEntity.getLabel(), fieldEntity.getLength());
+    }
+  }
+
+  private void checkNumber(final Value value, final FieldEntity fieldEntity) {
+    try {
+      final Double valueAsDouble = Double.valueOf(value.getValue());
+
+      if (fieldEntity.getMinValue() != null) {
+        if (valueAsDouble.compareTo(fieldEntity.getMinValue()) < 0) {
+          throw ServiceException.badRequest("Value for field {0} must be greater than or equals {1}.",
+              fieldEntity.getIdentifier(), fieldEntity.getMinValue());
+        }
+      }
+
+      if (fieldEntity.getMaxValue() != null) {
+        if (valueAsDouble.compareTo(fieldEntity.getMaxValue()) > 0) {
+          throw ServiceException.badRequest("Value for field {0} must be lesser than or equals {1}.",
+              fieldEntity.getIdentifier(), fieldEntity.getMaxValue());
+        }
+      }
+    } catch (final Throwable th) {
+      throw ServiceException.badRequest("Value for field {0} is not a number.", fieldEntity.getLabel());
+    }
+
+    final String[] split = StringUtils.split(value.getValue(), ".");
+    if (fieldEntity.getLength() != null)  {
+      if (split.length == 2) {
+        if ((split[0].length() + split[1].length()) > fieldEntity.getLength()) {
+          throw ServiceException.badRequest("Value for field {0} must be smaller than or equals {1}.",
+              fieldEntity.getLabel(), fieldEntity.getLength());
+        }
+
+        if (fieldEntity.getPrecision() != null)  {
+          if (split[1].length() > fieldEntity.getPrecision()) {
+            throw ServiceException.badRequest("Precision for field {0} must be smaller than or equals {1}.",
+                fieldEntity.getLabel(), fieldEntity.getPrecision());
+          }
+        }
+      } else {
+        this.checkLength(value, fieldEntity);
+      }
+    }
+  }
+
+  private void checkDate(final Value value, final FieldEntity fieldEntity) {
+    try {
+      DateConverter.fromIsoString(value.getValue());
+    } catch (final Throwable th) {
+      throw ServiceException.badRequest("Value for field {0} must be a valid ISO value.",fieldEntity.getLabel() );
+    }
+  }
+
+  private void checkOptions(final Value value, final FieldEntity fieldEntity, final List<OptionEntity> optionEntities, boolean singleSelection) {
+    final Set<String> valuesAsSet = StringUtils.commaDelimitedListToSet(value.getValue());
+
+    if (singleSelection && valuesAsSet.size() > 1) {
+      throw ServiceException.badRequest("Field {0} only supports single selection.", fieldEntity.getLabel());
+    }
+
+    final HashSet<String> optionsAsSet = new HashSet<>(optionEntities.size());
+    optionEntities.forEach(optionEntity -> optionsAsSet.add(optionEntity.getValue().toString()));
+    if (!optionsAsSet.containsAll(valuesAsSet)) {
+      throw ServiceException.badRequest("Unsupported option {0} for field {1}.", value.getValue(), fieldEntity.getLabel());
+    }
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/rest/config/CatalogRestConfiguration.java b/service/src/main/java/io/mifos/customer/catalog/service/rest/config/CatalogRestConfiguration.java
new file mode 100644
index 0000000..47a2e42
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/rest/config/CatalogRestConfiguration.java
@@ -0,0 +1,53 @@
+/*
+ * 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.catalog.service.rest.config;
+
+import io.mifos.anubis.config.EnableAnubis;
+import io.mifos.core.async.config.EnableAsync;
+import io.mifos.core.cassandra.config.EnableCassandra;
+import io.mifos.core.command.config.EnableCommandProcessing;
+import io.mifos.core.lang.config.EnableApplicationName;
+import io.mifos.core.lang.config.EnableServiceException;
+import io.mifos.core.lang.config.EnableTenantContext;
+import io.mifos.customer.catalog.service.internal.config.CatalogServiceConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@EnableAutoConfiguration
+@EnableDiscoveryClient
+@EnableAsync
+@EnableTenantContext
+@EnableCassandra
+@EnableCommandProcessing
+@EnableAnubis
+@EnableServiceException
+@EnableApplicationName
+@ComponentScan({
+    "io.mifos.customer.catalog.service.rest.controller"
+})
+@Import({
+    CatalogServiceConfiguration.class
+})
+public class CatalogRestConfiguration {
+
+  public CatalogRestConfiguration() {
+    super();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/catalog/service/rest/controller/CatalogRestController.java b/service/src/main/java/io/mifos/customer/catalog/service/rest/controller/CatalogRestController.java
new file mode 100644
index 0000000..0d4f583
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/catalog/service/rest/controller/CatalogRestController.java
@@ -0,0 +1,104 @@
+/*
+ * 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.catalog.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.catalog.api.v1.domain.Catalog;
+import io.mifos.customer.catalog.service.internal.service.CatalogService;
+import io.mifos.customer.catalog.service.internal.command.CreateCatalogCommand;
+import io.mifos.customer.service.ServiceConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/catalogs")
+public class CatalogRestController {
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+  private final CatalogService catalogService;
+
+  @Autowired
+  public CatalogRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                               final CommandGateway commandGateway,
+                               final CatalogService catalogService) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+    this.catalogService = catalogService;
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CATALOG)
+  @RequestMapping(
+      path = "",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> createCatalog(@RequestBody final Catalog catalog) {
+    if (this.catalogService.catalogExists(catalog.getIdentifier())) {
+      throw ServiceException.conflict("Catalog {0} already exists.", catalog.getIdentifier());
+    }
+    this.commandGateway.process(new CreateCatalogCommand(catalog));
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CATALOG)
+  @RequestMapping(
+      path = "",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<List<Catalog>> fetchCatalogs() {
+    return ResponseEntity.ok(this.catalogService.fetchAllCatalogs());
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CATALOG)
+  @RequestMapping(
+      path = "/{identifier}",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Catalog> findCatalog(@PathVariable("identifier") final String identifier) {
+    return ResponseEntity.ok(
+        this.catalogService.findCatalog(identifier)
+            .orElseThrow(() -> ServiceException.notFound("Catalog {0} not found.", identifier))
+    );
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/CustomerApplication.java b/service/src/main/java/io/mifos/customer/service/CustomerApplication.java
new file mode 100644
index 0000000..8096c60
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/CustomerApplication.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
+import org.springframework.boot.SpringApplication;
+
+public class CustomerApplication {
+
+  public CustomerApplication() {
+    super();
+  }
+
+  public static void main(String[] args) {
+
+    SpringApplication.run(CustomerRestConfiguration.class, args);
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/ServiceConstants.java b/service/src/main/java/io/mifos/customer/service/ServiceConstants.java
new file mode 100644
index 0000000..ae85302
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/ServiceConstants.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public interface ServiceConstants {
+
+  String LOGGER_NAME = "customer-logger";
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/ActivateCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/ActivateCustomerCommand.java
new file mode 100644
index 0000000..23be615
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/ActivateCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class ActivateCustomerCommand {
+
+  private final String identifier;
+  private final String comment;
+
+  public ActivateCustomerCommand(final String identifier, final String comment) {
+    super();
+    this.identifier = identifier;
+    this.comment = comment;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String comment() {
+    return this.comment;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/AddTaskDefinitionToCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/AddTaskDefinitionToCustomerCommand.java
new file mode 100644
index 0000000..364e09c
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/AddTaskDefinitionToCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class AddTaskDefinitionToCustomerCommand {
+
+  private final String customerIdentifier;
+  private final String taskIdentifier;
+
+  public AddTaskDefinitionToCustomerCommand(final String customerIdentifier, final String taskIdentifier) {
+    super();
+    this.customerIdentifier = customerIdentifier;
+    this.taskIdentifier = taskIdentifier;
+  }
+
+  public String customerIdentifier() {
+    return this.customerIdentifier;
+  }
+
+  public String taskIdentifier() {
+    return this.taskIdentifier;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CloseCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CloseCustomerCommand.java
new file mode 100644
index 0000000..1382582
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/CloseCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class CloseCustomerCommand {
+
+  private final String identifier;
+  private final String comment;
+
+  public CloseCustomerCommand(final String identifier, final String comment) {
+    super();
+    this.identifier = identifier;
+    this.comment = comment;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String comment() {
+    return this.comment;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CreateCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CreateCustomerCommand.java
new file mode 100644
index 0000000..4b84686
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/CreateCustomerCommand.java
@@ -0,0 +1,32 @@
+/*
+ * 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.Customer;
+
+public class CreateCustomerCommand {
+
+  private final Customer customer;
+
+  public CreateCustomerCommand(final Customer customer) {
+    super();
+    this.customer = customer;
+  }
+
+  public Customer customer() {
+    return this.customer;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CreateTaskDefinitionCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CreateTaskDefinitionCommand.java
new file mode 100644
index 0000000..3457d2c
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/CreateTaskDefinitionCommand.java
@@ -0,0 +1,32 @@
+/*
+ * 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.TaskDefinition;
+
+public class CreateTaskDefinitionCommand {
+
+  private final TaskDefinition taskDefinition;
+
+  public CreateTaskDefinitionCommand(final TaskDefinition taskDefinition) {
+    super();
+    this.taskDefinition = taskDefinition;
+  }
+
+  public TaskDefinition taskDefinition() {
+    return this.taskDefinition;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/ExecuteTaskForCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/ExecuteTaskForCustomerCommand.java
new file mode 100644
index 0000000..9ca0daf
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/ExecuteTaskForCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class ExecuteTaskForCustomerCommand {
+
+  private final String customerIdentifier;
+  private final String taskIdentifier;
+
+  public ExecuteTaskForCustomerCommand(final String customerIdentifier, final String taskIdentifier) {
+    super();
+    this.customerIdentifier = customerIdentifier;
+    this.taskIdentifier = taskIdentifier;
+  }
+
+  public String customerIdentifier() {
+    return this.customerIdentifier;
+  }
+
+  public String taskIdentifier() {
+    return this.taskIdentifier;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/InitializeServiceCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/InitializeServiceCommand.java
new file mode 100644
index 0000000..7863e9c
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/InitializeServiceCommand.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class InitializeServiceCommand {
+
+  public InitializeServiceCommand() {
+    super();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/LockCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/LockCustomerCommand.java
new file mode 100644
index 0000000..f285c33
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/LockCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class LockCustomerCommand {
+
+  private final String identifier;
+  private final String comment;
+
+  public LockCustomerCommand(final String identifier, final String comment) {
+    super();
+    this.identifier = identifier;
+    this.comment = comment;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String comment() {
+    return this.comment;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/ReopenCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/ReopenCustomerCommand.java
new file mode 100644
index 0000000..82574f0
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/ReopenCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class ReopenCustomerCommand {
+
+  private final String identifier;
+  private final String comment;
+
+  public ReopenCustomerCommand(final String identifier, final String comment) {
+    super();
+    this.identifier = identifier;
+    this.comment = comment;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String comment() {
+    return this.comment;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UnlockCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UnlockCustomerCommand.java
new file mode 100644
index 0000000..3ecec4d
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UnlockCustomerCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class UnlockCustomerCommand {
+
+  private final String identifier;
+  private final String comment;
+
+  public UnlockCustomerCommand(final String identifier, final String comment) {
+    super();
+    this.identifier = identifier;
+    this.comment = comment;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String comment() {
+    return this.comment;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UpdateAddressCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateAddressCommand.java
new file mode 100644
index 0000000..e6cfe90
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateAddressCommand.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.command;
+
+import io.mifos.customer.api.v1.domain.Address;
+
+public class UpdateAddressCommand {
+
+  private final String identifier;
+  private final Address address;
+
+  public UpdateAddressCommand(final String identifier, final Address address) {
+    super();
+    this.identifier = identifier;
+    this.address = address;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public Address address() {
+    return this.address;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UpdateContactDetailsCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateContactDetailsCommand.java
new file mode 100644
index 0000000..9299eca
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateContactDetailsCommand.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ContactDetail;
+
+import java.util.List;
+
+public class UpdateContactDetailsCommand {
+
+  private final String identifier;
+  private final List<ContactDetail> contactDetails;
+
+  public UpdateContactDetailsCommand(final String identifier, final List<ContactDetail> contactDetails) {
+    super();
+    this.identifier = identifier;
+    this.contactDetails = contactDetails;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public List<ContactDetail> contactDetails() {
+    return this.contactDetails;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UpdateCustomerCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateCustomerCommand.java
new file mode 100644
index 0000000..6cf69af
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateCustomerCommand.java
@@ -0,0 +1,32 @@
+/*
+ * 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.Customer;
+
+public class UpdateCustomerCommand {
+
+  private final Customer customer;
+
+  public UpdateCustomerCommand(final Customer customer) {
+    super();
+    this.customer = customer;
+  }
+
+  public Customer customer() {
+    return this.customer;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UpdateIdentificationCardCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateIdentificationCardCommand.java
new file mode 100644
index 0000000..2326afc
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateIdentificationCardCommand.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.command;
+
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+
+public class UpdateIdentificationCardCommand {
+
+  private final String identifier;
+  private final IdentificationCard identificationCard;
+
+  public UpdateIdentificationCardCommand(final String identifier, final IdentificationCard identificationCard) {
+    super();
+    this.identifier = identifier;
+    this.identificationCard = identificationCard;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public IdentificationCard identificationCard() {
+    return this.identificationCard;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/UpdateTaskDefinitionCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateTaskDefinitionCommand.java
new file mode 100644
index 0000000..e99ac92
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/UpdateTaskDefinitionCommand.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.command;
+
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+
+public class UpdateTaskDefinitionCommand {
+
+  private final String identifier;
+  private final TaskDefinition taskDefinition;
+
+  public UpdateTaskDefinitionCommand(final String identifier, final TaskDefinition taskDefinition) {
+    super();
+    this.identifier = identifier;
+    this.taskDefinition = taskDefinition;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public TaskDefinition taskDefinition() {
+    return this.taskDefinition;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java b/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java
new file mode 100644
index 0000000..661f34b
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java
@@ -0,0 +1,387 @@
+/*
+ * 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.domain.Command;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.catalog.service.internal.repository.CatalogEntity;
+import io.mifos.customer.catalog.service.internal.repository.CatalogRepository;
+import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldRepository;
+import io.mifos.customer.catalog.service.internal.repository.FieldValueEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldValueRepository;
+import io.mifos.customer.service.internal.command.ActivateCustomerCommand;
+import io.mifos.customer.service.internal.command.CloseCustomerCommand;
+import io.mifos.customer.service.internal.command.CreateCustomerCommand;
+import io.mifos.customer.service.internal.command.LockCustomerCommand;
+import io.mifos.customer.service.internal.command.ReopenCustomerCommand;
+import io.mifos.customer.service.internal.command.UnlockCustomerCommand;
+import io.mifos.customer.service.internal.command.UpdateAddressCommand;
+import io.mifos.customer.service.internal.command.UpdateContactDetailsCommand;
+import io.mifos.customer.service.internal.command.UpdateCustomerCommand;
+import io.mifos.customer.service.internal.command.UpdateIdentificationCardCommand;
+import io.mifos.customer.service.internal.mapper.AddressMapper;
+import io.mifos.customer.service.internal.mapper.CommandMapper;
+import io.mifos.customer.service.internal.mapper.ContactDetailMapper;
+import io.mifos.customer.service.internal.mapper.CustomerMapper;
+import io.mifos.customer.service.internal.mapper.FieldValueMapper;
+import io.mifos.customer.service.internal.mapper.IdentificationCardMapper;
+import io.mifos.customer.service.internal.repository.AddressEntity;
+import io.mifos.customer.service.internal.repository.AddressRepository;
+import io.mifos.customer.service.internal.repository.CommandRepository;
+import io.mifos.customer.service.internal.repository.ContactDetailEntity;
+import io.mifos.customer.service.internal.repository.ContactDetailRepository;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import io.mifos.customer.service.internal.repository.CustomerRepository;
+import io.mifos.customer.service.internal.repository.IdentificationCardEntity;
+import io.mifos.customer.service.internal.repository.IdentificationCardRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Date;
+import java.time.Clock;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+@Aggregate
+public class CustomerAggregate {
+
+  private final AddressRepository addressRepository;
+  private final CustomerRepository customerRepository;
+  private final IdentificationCardRepository identificationCardRepository;
+  private final ContactDetailRepository contactDetailRepository;
+  private final FieldValueRepository fieldValueRepository;
+  private final CatalogRepository catalogRepository;
+  private final FieldRepository fieldRepository;
+  private final CommandRepository commandRepository;
+  private final TaskAggregate taskAggregate;
+
+  @Autowired
+  public CustomerAggregate(final AddressRepository addressRepository,
+                           final CustomerRepository customerRepository,
+                           final IdentificationCardRepository identificationCardRepository,
+                           final ContactDetailRepository contactDetailRepository,
+                           final FieldValueRepository fieldValueRepository,
+                           final CatalogRepository catalogRepository,
+                           final FieldRepository fieldRepository,
+                           final CommandRepository commandRepository,
+                           final TaskAggregate taskAggregate) {
+    super();
+    this.addressRepository = addressRepository;
+    this.customerRepository = customerRepository;
+    this.identificationCardRepository = identificationCardRepository;
+    this.contactDetailRepository = contactDetailRepository;
+    this.fieldValueRepository = fieldValueRepository;
+    this.catalogRepository = catalogRepository;
+    this.fieldRepository = fieldRepository;
+    this.commandRepository = commandRepository;
+    this.taskAggregate = taskAggregate;
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.POST_CUSTOMER)
+  public String createCustomer(final CreateCustomerCommand createCustomerCommand) {
+    final Customer customer = createCustomerCommand.customer();
+
+    final AddressEntity savedAddress = this.addressRepository.save(AddressMapper.map(customer.getAddress()));
+
+    final CustomerEntity customerEntity = CustomerMapper.map(customer);
+    customerEntity.setCurrentState(Customer.State.PENDING.name());
+    customerEntity.setAddress(savedAddress);
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    if (customer.getIdentificationCard() != null) {
+      final IdentificationCardEntity identificationCardEntity = IdentificationCardMapper.map(customer.getIdentificationCard());
+      identificationCardEntity.setCustomer(savedCustomerEntity);
+      this.identificationCardRepository.save(identificationCardEntity);
+    }
+
+    if (customer.getContactDetails() != null) {
+      this.contactDetailRepository.save(
+          customer.getContactDetails()
+              .stream()
+              .map(contact -> {
+                final ContactDetailEntity contactDetailEntity = ContactDetailMapper.map(contact);
+                contactDetailEntity.setCustomer(savedCustomerEntity);
+                return contactDetailEntity;
+              })
+              .collect(Collectors.toList())
+      );
+    }
+
+    if (customer.getCustomValues() != null) {
+      this.setCustomValues(customer, savedCustomerEntity);
+    }
+
+    this.taskAggregate.onCustomerCommand(savedCustomerEntity, Command.Action.ACTIVATE);
+
+    return customer.getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_CUSTOMER)
+  public String updateCustomer(final UpdateCustomerCommand updateCustomerCommand) {
+    final Customer customer = updateCustomerCommand.customer();
+
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(customer.getIdentifier());
+
+    customerEntity.setGivenName(customer.getGivenName());
+    customerEntity.setMiddleName(customer.getMiddleName());
+    customerEntity.setSurname(customer.getSurname());
+    customerEntity.setAccountBeneficiary(customer.getAccountBeneficiary());
+    customerEntity.setReferenceCustomer(customer.getReferenceCustomer());
+    customerEntity.setAssignedOffice(customer.getAssignedOffice());
+    customerEntity.setAssignedEmployee(customer.getAssignedEmployee());
+
+    if (customer.getDateOfBirth() != null ) {
+      final LocalDate newDateOfBirth = customer.getDateOfBirth().toLocalDate();
+      if (customerEntity.getDateOfBirth() == null) {
+        customerEntity.setDateOfBirth(Date.valueOf(newDateOfBirth));
+      } else {
+        final LocalDate dateOfBirth = customerEntity.getDateOfBirth().toLocalDate();
+        if (!dateOfBirth.isEqual(newDateOfBirth)) {
+          customerEntity.setDateOfBirth(Date.valueOf(newDateOfBirth));
+        }
+      }
+    } else {
+      customerEntity.setDateOfBirth(null);
+    }
+
+    if (customer.getCustomValues() != null) {
+      this.fieldValueRepository.deleteByCustomer(customerEntity);
+      this.setCustomValues(customer, customerEntity);
+    }
+
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    this.customerRepository.save(customerEntity);
+
+    return customer.getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.ACTIVATE_CUSTOMER)
+  public String activateCustomer(final ActivateCustomerCommand activateCustomerCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(activateCustomerCommand.identifier());
+
+    if (this.taskAggregate.openTasksForCustomerExist(customerEntity)) {
+      throw ServiceException.conflict("Open Tasks for customer {0} exists.", activateCustomerCommand.identifier());
+    }
+
+    customerEntity.setCurrentState(Customer.State.ACTIVE.name());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    this.commandRepository.save(
+        CommandMapper.create(savedCustomerEntity, Command.Action.ACTIVATE.name(), activateCustomerCommand.comment())
+    );
+
+    return activateCustomerCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.LOCK_CUSTOMER)
+  public String lockCustomer(final LockCustomerCommand lockCustomerCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(lockCustomerCommand.identifier());
+    customerEntity.setCurrentState(Customer.State.LOCKED.name());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    this.commandRepository.save(
+        CommandMapper.create(savedCustomerEntity, Command.Action.LOCK.name(), lockCustomerCommand.comment())
+    );
+
+    this.taskAggregate.onCustomerCommand(savedCustomerEntity, Command.Action.UNLOCK);
+
+    return lockCustomerCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.UNLOCK_CUSTOMER)
+  public String unlockCustomer(final UnlockCustomerCommand unlockCustomerCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(unlockCustomerCommand.identifier());
+
+    if (this.taskAggregate.openTasksForCustomerExist(customerEntity)) {
+      throw ServiceException.conflict("Open Tasks for customer {0} exists.", unlockCustomerCommand.identifier());
+    }
+
+    customerEntity.setCurrentState(Customer.State.ACTIVE.name());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    this.commandRepository.save(
+        CommandMapper.create(savedCustomerEntity, Command.Action.UNLOCK.name(), unlockCustomerCommand.comment())
+    );
+
+    return unlockCustomerCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.CLOSE_CUSTOMER)
+  public String closeCustomer(final CloseCustomerCommand closeCustomerCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(closeCustomerCommand.identifier());
+    customerEntity.setCurrentState(Customer.State.CLOSED.name());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    this.commandRepository.save(
+        CommandMapper.create(savedCustomerEntity, Command.Action.CLOSE.name(), closeCustomerCommand.comment())
+    );
+
+    this.taskAggregate.onCustomerCommand(savedCustomerEntity, Command.Action.REOPEN);
+
+    return closeCustomerCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.REOPEN_CUSTOMER)
+  public String reopenCustomer(final ReopenCustomerCommand reopenCustomerCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(reopenCustomerCommand.identifier());
+
+    if (this.taskAggregate.openTasksForCustomerExist(customerEntity)) {
+      throw ServiceException.conflict("Open Tasks for customer {0} exists.", reopenCustomerCommand.identifier());
+    }
+
+    customerEntity.setCurrentState(Customer.State.ACTIVE.name());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final CustomerEntity savedCustomerEntity = this.customerRepository.save(customerEntity);
+
+    this.commandRepository.save(
+        CommandMapper.create(savedCustomerEntity, Command.Action.REOPEN.name(), reopenCustomerCommand.comment())
+    );
+
+    return reopenCustomerCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_ADDRESS)
+  public String updateAddress(final UpdateAddressCommand updateAddressCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(updateAddressCommand.identifier());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final AddressEntity oldAddressEntity = customerEntity.getAddress();
+
+    final AddressEntity newAddressEntity = this.addressRepository.save(AddressMapper.map(updateAddressCommand.address()));
+
+    customerEntity.setAddress(newAddressEntity);
+    this.customerRepository.save(customerEntity);
+
+    this.addressRepository.delete(oldAddressEntity);
+
+    return updateAddressCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_CONTACT_DETAILS)
+  public String updateContactDetails(final UpdateContactDetailsCommand updateContactDetailsCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(updateContactDetailsCommand.identifier());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final List<ContactDetailEntity> oldContactDetails = this.contactDetailRepository.findByCustomer(customerEntity);
+    this.contactDetailRepository.delete(oldContactDetails);
+
+    if (updateContactDetailsCommand.contactDetails() != null) {
+      this.contactDetailRepository.save(
+          updateContactDetailsCommand.contactDetails()
+              .stream()
+              .map(contact -> {
+                final ContactDetailEntity newContactDetail = ContactDetailMapper.map(contact);
+                newContactDetail.setCustomer(customerEntity);
+                return newContactDetail;
+              })
+              .collect(Collectors.toList())
+      );
+    }
+
+    return updateContactDetailsCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_IDENTIFICATION_CARD)
+  public String updateIdentificationCard(final UpdateIdentificationCardCommand updateIdentificationCardCommand) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(updateIdentificationCardCommand.identifier());
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    final IdentificationCardEntity oldIdentificationCard = this.identificationCardRepository.findByCustomer(customerEntity);
+    if (oldIdentificationCard != null) {
+      this.identificationCardRepository.delete(oldIdentificationCard);
+    }
+
+    if (updateIdentificationCardCommand.identificationCard() != null) {
+      final IdentificationCardEntity identificationCardEntity = IdentificationCardMapper.map(
+          updateIdentificationCardCommand.identificationCard());
+      identificationCardEntity.setCustomer(customerEntity);
+      this.identificationCardRepository.save(identificationCardEntity);
+    }
+
+    return updateIdentificationCardCommand.identifier();
+  }
+
+  private void setCustomValues(final Customer customer, final CustomerEntity savedCustomerEntity) {
+    this.fieldValueRepository.save(
+        customer.getCustomValues()
+            .stream()
+            .map(value -> {
+              final Optional<CatalogEntity> catalog =
+                  this.catalogRepository.findByIdentifier(value.getCatalogIdentifier());
+              final Optional<FieldEntity> field =
+                  this.fieldRepository.findByCatalogAndIdentifier(
+                      catalog.orElseThrow(() -> ServiceException.notFound("Catalog {0} not found.", value.getCatalogIdentifier())),
+                      value.getFieldIdentifier());
+              final FieldValueEntity fieldValueEntity = FieldValueMapper.map(value);
+              fieldValueEntity.setCustomer(savedCustomerEntity);
+              fieldValueEntity.setField(
+                  field.orElseThrow(() -> ServiceException.notFound("Field {0} not found.", value.getFieldIdentifier())));
+              return fieldValueEntity;
+            })
+            .collect(Collectors.toList())
+    );
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/handler/MigrationAggregate.java b/service/src/main/java/io/mifos/customer/service/internal/command/handler/MigrationAggregate.java
new file mode 100644
index 0000000..896c6ef
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/handler/MigrationAggregate.java
@@ -0,0 +1,58 @@
+/*
+ * 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.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.mariadb.domain.FlywayFactoryBean;
+import io.mifos.customer.api.v1.CustomerEventConstants;
+import io.mifos.customer.service.ServiceConstants;
+import io.mifos.customer.service.internal.command.InitializeServiceCommand;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+import javax.sql.DataSource;
+
+@SuppressWarnings({
+    "unused"
+})
+@Aggregate
+public class MigrationAggregate {
+
+  private final Logger logger;
+  private final DataSource dataSource;
+  private final FlywayFactoryBean flywayFactoryBean;
+
+  @Autowired
+  public MigrationAggregate(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                            final DataSource dataSource,
+                            final FlywayFactoryBean flywayFactoryBean) {
+    super();
+    this.logger = logger;
+    this.dataSource = dataSource;
+    this.flywayFactoryBean = flywayFactoryBean;
+  }
+
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.INITIALIZE)
+  public String initialize(final InitializeServiceCommand initializeServiceCommand) {
+    this.logger.debug("Start service migration.");
+    this.flywayFactoryBean.create(this.dataSource).migrate();
+    return CustomerEventConstants.INITIALIZE;
+  }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/handler/TaskAggregate.java b/service/src/main/java/io/mifos/customer/service/internal/command/handler/TaskAggregate.java
new file mode 100644
index 0000000..5933498
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/handler/TaskAggregate.java
@@ -0,0 +1,162 @@
+/*
+ * 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.customer.api.v1.CustomerEventConstants;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.service.internal.command.AddTaskDefinitionToCustomerCommand;
+import io.mifos.customer.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.customer.service.internal.command.ExecuteTaskForCustomerCommand;
+import io.mifos.customer.service.internal.command.UpdateTaskDefinitionCommand;
+import io.mifos.customer.service.internal.mapper.TaskDefinitionMapper;
+import io.mifos.customer.service.internal.mapper.TaskInstanceMapper;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import io.mifos.customer.service.internal.repository.CustomerRepository;
+import io.mifos.customer.service.internal.repository.TaskDefinitionEntity;
+import io.mifos.customer.service.internal.repository.TaskDefinitionRepository;
+import io.mifos.customer.service.internal.repository.TaskInstanceEntity;
+import io.mifos.customer.service.internal.repository.TaskInstanceRepository;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+@Aggregate
+public class TaskAggregate {
+
+  private final TaskDefinitionRepository taskDefinitionRepository;
+  private final TaskInstanceRepository taskInstanceRepository;
+  private final CustomerRepository customerRepository;
+
+  @Autowired
+  public TaskAggregate(final TaskDefinitionRepository taskDefinitionRepository,
+                       final TaskInstanceRepository taskInstanceRepository,
+                       final CustomerRepository customerRepository) {
+    super();
+    this.taskDefinitionRepository = taskDefinitionRepository;
+    this.taskInstanceRepository = taskInstanceRepository;
+    this.customerRepository = customerRepository;
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.POST_TASK)
+  public String createTaskDefinition(final CreateTaskDefinitionCommand createTaskDefinitionCommand) {
+    this.taskDefinitionRepository.save(TaskDefinitionMapper.map(createTaskDefinitionCommand.taskDefinition()));
+    return createTaskDefinitionCommand.taskDefinition().getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_TASK)
+  public String updateTaskDefinition(final UpdateTaskDefinitionCommand updateTaskDefinitionCommand) {
+    final TaskDefinitionEntity taskDefinitionEntity = this.taskDefinitionRepository.findByIdentifier(updateTaskDefinitionCommand.identifier());
+
+    final TaskDefinition updatedTaskDefinition = updateTaskDefinitionCommand.taskDefinition();
+    taskDefinitionEntity.setName(updatedTaskDefinition.getName());
+    taskDefinitionEntity.setDescription(updatedTaskDefinition.getDescription());
+    taskDefinitionEntity.setAssignedCommands(StringUtils.join(updatedTaskDefinition.getCommands(), ","));
+    taskDefinitionEntity.setMandatory(updatedTaskDefinition.getMandatory());
+    taskDefinitionEntity.setPredefined(updatedTaskDefinition.getPredefined());
+
+    this.taskDefinitionRepository.save(taskDefinitionEntity);
+
+    return updatedTaskDefinition.getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_CUSTOMER)
+  public String addTaskToCustomer(final AddTaskDefinitionToCustomerCommand addTaskDefinitionToCustomerCommand) {
+    final TaskDefinitionEntity taskDefinitionEntity =
+        this.taskDefinitionRepository.findByIdentifier(addTaskDefinitionToCustomerCommand.taskIdentifier());
+
+    final CustomerEntity customerEntity =
+        this.customerRepository.findByIdentifier(addTaskDefinitionToCustomerCommand.customerIdentifier());
+
+    this.taskInstanceRepository.save(TaskInstanceMapper.create(taskDefinitionEntity, customerEntity));
+
+    return addTaskDefinitionToCustomerCommand.customerIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_CUSTOMER)
+  public String executeTaskForCustomer(final ExecuteTaskForCustomerCommand executeTaskForCustomerCommand) {
+    final CustomerEntity customerEntity =
+        this.customerRepository.findByIdentifier(executeTaskForCustomerCommand.customerIdentifier());
+    final List<TaskInstanceEntity> taskInstanceEntities = this.taskInstanceRepository.findByCustomer(customerEntity);
+    if (taskInstanceEntities != null) {
+      final Optional<TaskInstanceEntity> taskInstanceEntityOptional = taskInstanceEntities
+          .stream()
+          .filter(
+              taskInstanceEntity -> taskInstanceEntity.getTaskDefinition().getIdentifier()
+                  .equals(executeTaskForCustomerCommand.taskIdentifier()))
+          .findAny();
+
+      if (taskInstanceEntityOptional.isPresent()) {
+        final TaskInstanceEntity taskInstanceEntity = taskInstanceEntityOptional.get();
+        taskInstanceEntity.setExecutedBy(UserContextHolder.checkedGetUser());
+        taskInstanceEntity.setExecutedOn(LocalDateTime.now(Clock.systemUTC()));
+        this.taskInstanceRepository.save(taskInstanceEntity);
+      }
+    }
+
+    return executeTaskForCustomerCommand.customerIdentifier();
+  }
+
+  @Transactional
+  public void onCustomerCommand(final CustomerEntity customerEntity, Command.Action action) {
+    final List<TaskDefinitionEntity> predefinedTasks =
+        this.taskDefinitionRepository.findByAssignedCommandsContaining(action.name());
+    if (predefinedTasks != null && predefinedTasks.size() > 0) {
+      this.taskInstanceRepository.save(
+          predefinedTasks
+              .stream()
+              .filter(TaskDefinitionEntity::isPredefined)
+              .map(taskDefinitionEntity -> TaskInstanceMapper.create(taskDefinitionEntity, customerEntity))
+              .collect(Collectors.toList())
+      );
+    }
+  }
+
+  @Transactional
+  public Boolean openTasksForCustomerExist(final CustomerEntity customerEntity) {
+    final List<TaskInstanceEntity> taskInstanceEntities = this.taskInstanceRepository.findByCustomer(customerEntity);
+
+    //noinspection SimplifiableIfStatement
+    if (taskInstanceEntities != null) {
+      return taskInstanceEntities
+          .stream()
+              .filter(taskInstanceEntity -> taskInstanceEntity.getTaskDefinition().isMandatory())
+              .filter(taskInstanceEntity -> taskInstanceEntity.getExecutedBy() == null)
+          .findAny().isPresent();
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/config/CustomerServiceConfiguration.java b/service/src/main/java/io/mifos/customer/service/internal/config/CustomerServiceConfiguration.java
new file mode 100644
index 0000000..6679ee6
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/config/CustomerServiceConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * 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.config;
+
+import io.mifos.core.mariadb.config.EnableMariaDB;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@Configuration
+@EnableMariaDB
+@ComponentScan({
+    "io.mifos.customer.service.internal.service",
+    "io.mifos.customer.service.internal.repository",
+    "io.mifos.customer.service.internal.command.handler",
+    "io.mifos.customer.service.internal.event.handler"
+})
+@EnableJpaRepositories({
+    "io.mifos.customer.service.internal.repository"
+})
+public class CustomerServiceConfiguration {
+
+  public CustomerServiceConfiguration() {
+    super();
+  }
+
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/AddressMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/AddressMapper.java
new file mode 100644
index 0000000..30a629f
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/AddressMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.customer.api.v1.domain.Address;
+import io.mifos.customer.service.internal.repository.AddressEntity;
+
+public final class AddressMapper {
+
+  private AddressMapper() {
+    super();
+  }
+
+  public static AddressEntity map(final Address address) {
+    final AddressEntity addressEntity = new AddressEntity();
+    addressEntity.setStreet(address.getStreet());
+    addressEntity.setCity(address.getCity());
+    addressEntity.setPostalCode(address.getPostalCode());
+    addressEntity.setRegion(address.getRegion());
+    addressEntity.setCountryCode(address.getCountryCode());
+    addressEntity.setCountry(address.getCountry());
+    return addressEntity;
+  }
+
+  public static Address map(final AddressEntity addressEntity) {
+    final Address address = new Address();
+    address.setStreet(addressEntity.getStreet());
+    address.setCity(addressEntity.getCity());
+    address.setPostalCode(addressEntity.getPostalCode());
+    address.setRegion(addressEntity.getRegion());
+    address.setCountryCode(addressEntity.getCountryCode());
+    address.setCountry(addressEntity.getCountry());
+    return address;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/CommandMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/CommandMapper.java
new file mode 100644
index 0000000..ceb5546
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/CommandMapper.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.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.service.internal.repository.CommandEntity;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public final class CommandMapper {
+
+  private CommandMapper() {
+    super();
+  }
+
+  public static CommandEntity create(final CustomerEntity customer, final String action, final String comment) {
+    final CommandEntity commandEntity = new CommandEntity();
+    commandEntity.setCustomer(customer);
+    commandEntity.setType(action);
+    commandEntity.setComment(comment);
+    commandEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    commandEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    return commandEntity;
+  }
+
+  public static Command map(final CommandEntity commandEntity) {
+    final Command command = new Command();
+    command.setAction(commandEntity.getType());
+    command.setComment(commandEntity.getComment());
+    command.setCreatedBy(commandEntity.getCreatedBy());
+    command.setCreatedOn(DateConverter.toIsoString(commandEntity.getCreatedOn()));
+    return command;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/ContactDetailMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/ContactDetailMapper.java
new file mode 100644
index 0000000..2440ccf
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/ContactDetailMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.customer.api.v1.domain.ContactDetail;
+import io.mifos.customer.service.internal.repository.ContactDetailEntity;
+
+public final class ContactDetailMapper {
+
+  private ContactDetailMapper() {
+    super();
+  }
+
+  public static ContactDetailEntity map(final ContactDetail contactDetail) {
+    final ContactDetailEntity contactDetailEntity = new ContactDetailEntity();
+    contactDetailEntity.setType(contactDetail.getType());
+    contactDetailEntity.setGroup(contactDetail.getGroup());
+    contactDetailEntity.setValue(contactDetail.getValue());
+    contactDetailEntity.setPreferenceLevel(contactDetail.getPreferenceLevel());
+    contactDetailEntity.setValid(contactDetail.getValidated());
+    return contactDetailEntity;
+  }
+
+  public static ContactDetail map(final ContactDetailEntity contactDetailEntity) {
+    final ContactDetail contactDetail = new ContactDetail();
+    contactDetail.setType(contactDetailEntity.getType());
+    contactDetail.setGroup(contactDetailEntity.getGroup());
+    contactDetail.setValue(contactDetailEntity.getValue());
+    contactDetail.setPreferenceLevel(contactDetailEntity.getPreferenceLevel());
+    contactDetail.setValidated(contactDetailEntity.getValid());
+    return contactDetail;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/CustomerMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/CustomerMapper.java
new file mode 100644
index 0000000..d4218e7
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/CustomerMapper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.core.lang.DateOfBirth;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+
+import java.sql.Date;
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public final class CustomerMapper {
+
+  private CustomerMapper() {
+    super();
+  }
+
+  public static CustomerEntity map(final Customer customer) {
+    final CustomerEntity customerEntity = new CustomerEntity();
+    customerEntity.setIdentifier(customer.getIdentifier());
+    customerEntity.setType(customer.getType());
+    customerEntity.setGivenName(customer.getGivenName());
+    customerEntity.setMiddleName(customer.getMiddleName());
+    customerEntity.setSurname(customer.getSurname());
+    customerEntity.setDateOfBirth(Date.valueOf(customer.getDateOfBirth().toLocalDate()));
+    customerEntity.setAccountBeneficiary(customer.getAccountBeneficiary());
+    customerEntity.setReferenceCustomer(customer.getReferenceCustomer());
+    customerEntity.setAssignedOffice(customer.getAssignedOffice());
+    customerEntity.setAssignedEmployee(customer.getAssignedEmployee());
+    customerEntity.setCurrentState(customer.getCurrentState());
+    customerEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    return customerEntity;
+  }
+
+  public static Customer map(final CustomerEntity customerEntity) {
+    final Customer customer = new Customer();
+    customer.setIdentifier(customerEntity.getIdentifier());
+    customer.setType(customerEntity.getType());
+    customer.setGivenName(customerEntity.getGivenName());
+    customer.setMiddleName(customerEntity.getMiddleName());
+    customer.setSurname(customerEntity.getSurname());
+    customer.setDateOfBirth(DateOfBirth.fromLocalDate(customerEntity.getDateOfBirth().toLocalDate()));
+    customer.setAccountBeneficiary(customerEntity.getAccountBeneficiary());
+    customer.setReferenceCustomer(customerEntity.getReferenceCustomer());
+    customer.setAssignedOffice(customerEntity.getAssignedOffice());
+    customer.setAssignedEmployee(customerEntity.getAssignedEmployee());
+    customer.setCurrentState(customerEntity.getCurrentState());
+    customer.setCreatedBy(customerEntity.getCreatedBy());
+    customer.setCreatedOn(DateConverter.toIsoString(customerEntity.getCreatedOn()));
+
+    if (customerEntity.getLastModifiedBy() != null) {
+      customer.setLastModifiedBy(customerEntity.getLastModifiedBy());
+      customer.setLastModifiedOn(DateConverter.toIsoString(customerEntity.getLastModifiedOn()));
+    }
+
+    return customer;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/FieldValueMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/FieldValueMapper.java
new file mode 100644
index 0000000..6845c93
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/FieldValueMapper.java
@@ -0,0 +1,32 @@
+/*
+ * 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.customer.catalog.api.v1.domain.Value;
+import io.mifos.customer.catalog.service.internal.repository.FieldValueEntity;
+
+public class FieldValueMapper {
+
+  private FieldValueMapper() {
+    super();
+  }
+
+  public static FieldValueEntity map(final Value value) {
+    final FieldValueEntity fieldValueEntity = new FieldValueEntity();
+    fieldValueEntity.setValue(value.getValue());
+    return fieldValueEntity;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/IdentificationCardMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/IdentificationCardMapper.java
new file mode 100644
index 0000000..adee8b2
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/IdentificationCardMapper.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.mapper;
+
+import io.mifos.customer.api.v1.domain.ExpirationDate;
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+import io.mifos.customer.service.internal.repository.IdentificationCardEntity;
+
+public final class IdentificationCardMapper {
+
+  private IdentificationCardMapper() {
+    super();
+  }
+
+  public static IdentificationCardEntity map(final IdentificationCard identificationCard) {
+    final IdentificationCardEntity identificationCardEntity = new IdentificationCardEntity();
+    identificationCardEntity.setType(identificationCard.getType());
+    identificationCardEntity.setNumber(identificationCard.getNumber());
+    identificationCardEntity.setExpirationDate(identificationCard.getExpirationDate().toLocalDate());
+    identificationCardEntity.setIssuer(identificationCard.getIssuer());
+    return identificationCardEntity;
+  }
+
+  public static IdentificationCard map(final IdentificationCardEntity identificationCardEntity) {
+    final IdentificationCard identificationCard = new IdentificationCard();
+    identificationCard.setType(identificationCardEntity.getType());
+    identificationCard.setNumber(identificationCardEntity.getNumber());
+    identificationCard.setExpirationDate(ExpirationDate.fromLocalDate(identificationCardEntity.getExpirationDate()));
+    identificationCard.setIssuer(identificationCardEntity.getIssuer());
+    return identificationCard;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskDefinitionMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskDefinitionMapper.java
new file mode 100644
index 0000000..33521ef
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskDefinitionMapper.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.internal.mapper;
+
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.service.internal.repository.TaskDefinitionEntity;
+import org.apache.commons.lang.StringUtils;
+
+public final class TaskDefinitionMapper {
+
+  private TaskDefinitionMapper() {
+    super();
+  }
+
+  public static TaskDefinitionEntity map(final TaskDefinition taskDefinition) {
+    final TaskDefinitionEntity taskDefinitionEntity = new TaskDefinitionEntity();
+    taskDefinitionEntity.setIdentifier(taskDefinition.getIdentifier());
+    taskDefinitionEntity.setType(taskDefinition.getType());
+    taskDefinitionEntity.setName(taskDefinition.getName());
+    taskDefinitionEntity.setDescription(taskDefinition.getDescription());
+    taskDefinitionEntity.setAssignedCommands(StringUtils.join(taskDefinition.getCommands(), ";"));
+    taskDefinitionEntity.setMandatory(taskDefinition.getMandatory());
+    taskDefinitionEntity.setPredefined(taskDefinition.getPredefined());
+    return taskDefinitionEntity;
+  }
+
+  public static TaskDefinition map(final TaskDefinitionEntity taskDefinitionEntity) {
+    final TaskDefinition taskDefinition = new TaskDefinition();
+    taskDefinition.setIdentifier(taskDefinitionEntity.getIdentifier());
+    taskDefinition.setType(taskDefinitionEntity.getType());
+    taskDefinition.setName(taskDefinitionEntity.getName());
+    taskDefinition.setDescription(taskDefinitionEntity.getDescription());
+    taskDefinition.setCommands(taskDefinitionEntity.getAssignedCommands().split(";"));
+    taskDefinition.setMandatory(taskDefinitionEntity.isMandatory());
+    taskDefinition.setPredefined(taskDefinitionEntity.isPredefined());
+    return taskDefinition;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskInstanceMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskInstanceMapper.java
new file mode 100644
index 0000000..b0b13fb
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/TaskInstanceMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.lang.DateConverter;
+import io.mifos.customer.api.v1.domain.TaskInstance;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import io.mifos.customer.service.internal.repository.TaskInstanceEntity;
+import io.mifos.customer.service.internal.repository.TaskDefinitionEntity;
+
+public class TaskInstanceMapper {
+
+  public TaskInstanceMapper() {
+    super();
+  }
+
+  public static TaskInstanceEntity create(final TaskDefinitionEntity taskDefinition, final CustomerEntity customer) {
+    final TaskInstanceEntity taskInstanceEntity = new TaskInstanceEntity();
+    taskInstanceEntity.setTaskDefinition(taskDefinition);
+    taskInstanceEntity.setCustomer(customer);
+    return taskInstanceEntity;
+  }
+
+  public static TaskInstance map(final TaskInstanceEntity taskInstanceEntity) {
+    final TaskInstance taskInstance = new TaskInstance();
+    taskInstance.setTaskIdentifier(taskInstanceEntity.getTaskDefinition().getIdentifier());
+    taskInstance.setComment(taskInstanceEntity.getComment());
+    if (taskInstanceEntity.getExecutedOn() != null) {
+      taskInstance.setExecutedOn(DateConverter.toIsoString(taskInstanceEntity.getExecutedOn()));
+    }
+    taskInstance.setExecutedBy(taskInstanceEntity.getExecutedBy());
+    return taskInstance;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/AddressEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/AddressEntity.java
new file mode 100644
index 0000000..bf950ce
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/AddressEntity.java
@@ -0,0 +1,105 @@
+/*
+ * 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.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "maat_addresses")
+public class AddressEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "street")
+  private String street;
+  @Column(name = "city")
+  private String city;
+  @Column(name = "postal_code")
+  private String postalCode;
+  @Column(name = "region")
+  private String region;
+  @Column(name = "country_code")
+  private String countryCode;
+  @Column(name = "country")
+  private String country;
+
+  public AddressEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getStreet() {
+    return this.street;
+  }
+
+  public void setStreet(final String street) {
+    this.street = street;
+  }
+
+  public String getCity() {
+    return this.city;
+  }
+
+  public void setCity(final String city) {
+    this.city = city;
+  }
+
+  public String getPostalCode() {
+    return this.postalCode;
+  }
+
+  public void setPostalCode(final String postalCode) {
+    this.postalCode = postalCode;
+  }
+
+  public String getRegion() {
+    return this.region;
+  }
+
+  public void setRegion(final String region) {
+    this.region = region;
+  }
+
+  public String getCountryCode() {
+    return this.countryCode;
+  }
+
+  public void setCountryCode(final String countryCode) {
+    this.countryCode = countryCode;
+  }
+
+  public String getCountry() {
+    return this.country;
+  }
+
+  public void setCountry(final String country) {
+    this.country = country;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/AddressRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/AddressRepository.java
new file mode 100644
index 0000000..6ee6439
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/AddressRepository.java
@@ -0,0 +1,23 @@
+/*
+ * 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.stereotype.Repository;
+
+@Repository
+public interface AddressRepository extends JpaRepository<AddressEntity, Long> {
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/CommandEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/CommandEntity.java
new file mode 100644
index 0000000..567c2e8
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/CommandEntity.java
@@ -0,0 +1,105 @@
+/*
+ * 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.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "maat_commands")
+public class CommandEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false)
+  @JoinColumn(name = "customer_id")
+  private CustomerEntity customer;
+  @Column(name = "a_type")
+  private String type;
+  @Column(name = "a_comment")
+  private String comment;
+  @Column(name = "created_by")
+  private String createdBy;
+  @Column(name = "created_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+
+  public CommandEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public String getComment() {
+    return this.comment;
+  }
+
+  public void setComment(final String comment) {
+    this.comment = comment;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/CommandRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/CommandRepository.java
new file mode 100644
index 0000000..40ec087
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/CommandRepository.java
@@ -0,0 +1,25 @@
+/*
+ * 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 java.util.List;
+
+public interface CommandRepository extends JpaRepository<CommandEntity, Long> {
+
+  List<CommandEntity> findByCustomer(final CustomerEntity customerEntity);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailEntity.java
new file mode 100644
index 0000000..82a87d6
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailEntity.java
@@ -0,0 +1,109 @@
+/*
+ * 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.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "maat_contact_details")
+public class ContactDetailEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @ManyToOne(fetch = FetchType.LAZY, optional = true)
+  @JoinColumn(name = "customer_id")
+  private CustomerEntity customer;
+  @Column(name = "a_type")
+  private String type;
+  @Column(name = "a_group")
+  private String group;
+  @Column(name = "a_value")
+  private String value;
+  @Column(name = "preference_level")
+  private Integer preferenceLevel;
+  @Column(name = "validated")
+  private Boolean valid;
+
+  public ContactDetailEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public String getGroup() {
+    return this.group;
+  }
+
+  public void setGroup(final String group) {
+    this.group = group;
+  }
+
+  public String getValue() {
+    return this.value;
+  }
+
+  public Integer getPreferenceLevel() {
+    return this.preferenceLevel;
+  }
+
+  public void setPreferenceLevel(final Integer preferenceLevel) {
+    this.preferenceLevel = preferenceLevel;
+  }
+
+  public void setValue(final String value) {
+    this.value = value;
+  }
+
+  public Boolean getValid() {
+    return this.valid;
+  }
+
+  public void setValid(final Boolean valid) {
+    this.valid = valid;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailRepository.java
new file mode 100644
index 0000000..888c4e1
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/ContactDetailRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ContactDetailRepository extends JpaRepository<ContactDetailEntity, Long> {
+
+  List<ContactDetailEntity> findByCustomer(final CustomerEntity customerEntity);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerEntity.java
new file mode 100644
index 0000000..d4bbf79
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerEntity.java
@@ -0,0 +1,233 @@
+/*
+ * 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.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import java.sql.Date;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "maat_customers")
+public class CustomerEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "a_type")
+  private String type;
+  @Column(name = "identifier")
+  private String identifier;
+  @Column(name = "given_name")
+  private String givenName;
+  @Column(name = "middle_name")
+  private String middleName;
+  @Column(name = "surname")
+  private String surname;
+  @Column(name = "date_of_birth")
+  private Date dateOfBirth;
+  @Column(name = "account_beneficiary")
+  private String accountBeneficiary;
+  @Column(name = "reference_customer")
+  private String referenceCustomer;
+  @Column(name = "assigned_office")
+  private String assignedOffice;
+  @Column(name = "assigned_employee")
+  private String assignedEmployee;
+  @Column(name = "current_state")
+  private String currentState;
+  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @JoinColumn(name = "address_id")
+  private AddressEntity address;
+  @Column(name = "created_by")
+  private String createdBy;
+  @Column(name = "created_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "last_modified_by")
+  private String lastModifiedBy;
+  @Column(name = "last_modified_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime lastModifiedOn;
+
+  public CustomerEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public String getGivenName() {
+    return this.givenName;
+  }
+
+  public void setGivenName(final String givenName) {
+    this.givenName = givenName;
+  }
+
+  public String getMiddleName() {
+    return this.middleName;
+  }
+
+  public void setMiddleName(final String middleName) {
+    this.middleName = middleName;
+  }
+
+  public String getSurname() {
+    return this.surname;
+  }
+
+  public void setSurname(final String surname) {
+    this.surname = surname;
+  }
+
+  public Date getDateOfBirth() {
+    return this.dateOfBirth;
+  }
+
+  public void setDateOfBirth(final Date dateOfBirth) {
+    this.dateOfBirth = dateOfBirth;
+  }
+
+  public String getAccountBeneficiary() {
+    return this.accountBeneficiary;
+  }
+
+  public void setAccountBeneficiary(final String accountBeneficiary) {
+    this.accountBeneficiary = accountBeneficiary;
+  }
+
+  public String getAssignedOffice() {
+    return this.assignedOffice;
+  }
+
+  public void setAssignedOffice(final String assignedOffice) {
+    this.assignedOffice = assignedOffice;
+  }
+
+  public String getReferenceCustomer() {
+    return this.referenceCustomer;
+  }
+
+  public void setReferenceCustomer(final String referenceCustomer) {
+    this.referenceCustomer = referenceCustomer;
+  }
+
+  public String getAssignedEmployee() {
+    return this.assignedEmployee;
+  }
+
+  public void setAssignedEmployee(final String assignedEmployee) {
+    this.assignedEmployee = assignedEmployee;
+  }
+
+  public String getCurrentState() {
+    return this.currentState;
+  }
+
+  public void setCurrentState(final String currentState) {
+    this.currentState = currentState;
+  }
+
+  public AddressEntity getAddress() {
+    return this.address;
+  }
+
+  public void setAddress(final AddressEntity address) {
+    this.address = address;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+
+  public LocalDateTime getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final LocalDateTime lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    CustomerEntity that = (CustomerEntity) o;
+
+    return identifier.equals(that.identifier);
+
+  }
+
+  @Override
+  public int hashCode() {
+    return identifier.hashCode();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerRepository.java
new file mode 100644
index 0000000..8d83fd8
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/CustomerRepository.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.domain.Page;
+import org.springframework.data.domain.Pageable;
+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;
+
+@Repository
+public interface CustomerRepository extends JpaRepository<CustomerEntity, Long> {
+
+  @Query("SELECT CASE WHEN COUNT(c) > 0 THEN 'true' ELSE 'false' END FROM CustomerEntity c WHERE c.identifier = :identifier")
+  Boolean existsByIdentifier(@Param("identifier") final String identifier);
+
+  Page<CustomerEntity> findByIdentifierContaining(final String identifier, final Pageable pageable);
+
+  CustomerEntity findByIdentifier(final String identifier);
+
+  Page<CustomerEntity> findByCurrentStateNot(final String state, final Pageable pageable);
+
+  Page<CustomerEntity> findByCurrentStateNotAndIdentifierContaining(final String state, final String identifier, final Pageable pageable);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardEntity.java
new file mode 100644
index 0000000..5dd9638
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardEntity.java
@@ -0,0 +1,104 @@
+/*
+ * 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.LocalDateConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.time.LocalDate;
+
+@Entity
+@Table(name = "maat_identification_cards")
+public class IdentificationCardEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "a_type")
+  private String type;
+  @ManyToOne(fetch = FetchType.LAZY, optional = true)
+  @JoinColumn(name = "customer_id")
+  private CustomerEntity customer;
+  @Column(name = "a_number")
+  private String number;
+  @Column(name = "expiration_date")
+  @Convert(converter = LocalDateConverter.class)
+  private LocalDate expirationDate;
+  @Column(name = "issuer")
+  private String issuer;
+
+  public IdentificationCardEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public String getNumber() {
+    return this.number;
+  }
+
+  public void setNumber(final String number) {
+    this.number = number;
+  }
+
+  public LocalDate getExpirationDate() {
+    return this.expirationDate;
+  }
+
+  public void setExpirationDate(final LocalDate expirationDate) {
+    this.expirationDate = expirationDate;
+  }
+
+  public String getIssuer() {
+    return this.issuer;
+  }
+
+  public void setIssuer(final String issuer) {
+    this.issuer = issuer;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardRepository.java
new file mode 100644
index 0000000..3ff632e
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/IdentificationCardRepository.java
@@ -0,0 +1,25 @@
+/*
+ * 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.stereotype.Repository;
+
+@Repository
+public interface IdentificationCardRepository extends JpaRepository<IdentificationCardEntity, Long> {
+
+  IdentificationCardEntity findByCustomer(final CustomerEntity customerEntity);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionEntity.java
new file mode 100644
index 0000000..d8a52c6
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionEntity.java
@@ -0,0 +1,131 @@
+/*
+ * 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.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "maat_task_definitions")
+public class TaskDefinitionEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "identifier")
+  private String identifier;
+  @Column(name = "a_type")
+  private String type;
+  @Column(name = "a_name")
+  private String name;
+  @Column(name = "description")
+  private String description;
+  @Column(name = "assigned_commands")
+  private String assignedCommands;
+  @Column(name = "mandatory")
+  private Boolean mandatory;
+  @Column(name = "predefined")
+  private Boolean predefined;
+
+  public TaskDefinitionEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getType() {
+    return this.type;
+  }
+
+  public void setType(final String type) {
+    this.type = type;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public String getAssignedCommands() {
+    return this.assignedCommands;
+  }
+
+  public void setAssignedCommands(final String assignedCommands) {
+    this.assignedCommands = assignedCommands;
+  }
+
+  public Boolean isMandatory() {
+    return this.mandatory;
+  }
+
+  public void setMandatory(final Boolean mandatory) {
+    this.mandatory = mandatory;
+  }
+
+  public Boolean isPredefined() {
+    return this.predefined;
+  }
+
+  public void setPredefined(final Boolean predefined) {
+    this.predefined = predefined;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    final TaskDefinitionEntity that = (TaskDefinitionEntity) o;
+
+    return id.equals(that.id);
+
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionRepository.java
new file mode 100644
index 0000000..8f00df0
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskDefinitionRepository.java
@@ -0,0 +1,34 @@
+/*
+ * 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.List;
+
+@Repository
+public interface TaskDefinitionRepository extends JpaRepository<TaskDefinitionEntity, Long> {
+
+  @Query("SELECT CASE WHEN COUNT(t) > 0 THEN 'true' ELSE 'false' END FROM TaskDefinitionEntity t WHERE t.identifier = :identifier")
+  Boolean existsByIdentifier(@Param("identifier") final String identifier);
+
+  TaskDefinitionEntity findByIdentifier(final String identifier);
+
+  List<TaskDefinitionEntity> findByAssignedCommandsContaining(final String command);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceEntity.java
new file mode 100644
index 0000000..761c549
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceEntity.java
@@ -0,0 +1,105 @@
+/*
+ * 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.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "maat_task_instances")
+public class TaskInstanceEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @ManyToOne(fetch = FetchType.EAGER, optional = false)
+  @JoinColumn(name = "task_definition_id")
+  private TaskDefinitionEntity taskDefinition;
+  @ManyToOne(fetch = FetchType.LAZY, optional = false)
+  @JoinColumn(name = "customer_id")
+  private CustomerEntity customer;
+  @Column(name = "a_comment")
+  private String comment;
+  @Column(name = "executed_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime executedOn;
+  @Column(name = "executed_by")
+  private String executedBy;
+
+  public TaskInstanceEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public TaskDefinitionEntity getTaskDefinition() {
+    return this.taskDefinition;
+  }
+
+  public void setTaskDefinition(final TaskDefinitionEntity taskDefinition) {
+    this.taskDefinition = taskDefinition;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public String getComment() {
+    return this.comment;
+  }
+
+  public void setComment(final String comment) {
+    this.comment = comment;
+  }
+
+  public LocalDateTime getExecutedOn() {
+    return this.executedOn;
+  }
+
+  public void setExecutedOn(final LocalDateTime executedOn) {
+    this.executedOn = executedOn;
+  }
+
+  public String getExecutedBy() {
+    return this.executedBy;
+  }
+
+  public void setExecutedBy(final String executedBy) {
+    this.executedBy = executedBy;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceRepository.java
new file mode 100644
index 0000000..0a7e2e8
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/TaskInstanceRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface TaskInstanceRepository extends JpaRepository<TaskInstanceEntity, Long> {
+
+  List<TaskInstanceEntity> findByCustomer(final CustomerEntity customer);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java b/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java
new file mode 100644
index 0000000..d383e04
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java
@@ -0,0 +1,162 @@
+/*
+ * 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.Command;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.api.v1.domain.CustomerPage;
+import io.mifos.customer.catalog.api.v1.domain.Value;
+import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldValueEntity;
+import io.mifos.customer.catalog.service.internal.repository.FieldValueRepository;
+import io.mifos.customer.service.ServiceConstants;
+import io.mifos.customer.service.internal.mapper.CommandMapper;
+import io.mifos.customer.service.internal.mapper.ContactDetailMapper;
+import io.mifos.customer.service.internal.mapper.CustomerMapper;
+import io.mifos.customer.service.internal.mapper.IdentificationCardMapper;
+import io.mifos.customer.service.internal.repository.CommandEntity;
+import io.mifos.customer.service.internal.repository.CommandRepository;
+import io.mifos.customer.service.internal.repository.ContactDetailEntity;
+import io.mifos.customer.service.internal.repository.ContactDetailRepository;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import io.mifos.customer.service.internal.repository.CustomerRepository;
+import io.mifos.customer.service.internal.repository.IdentificationCardEntity;
+import io.mifos.customer.service.internal.repository.IdentificationCardRepository;
+import io.mifos.customer.service.internal.mapper.AddressMapper;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class CustomerService {
+
+  private final Logger logger;
+  private final CustomerRepository customerRepository;
+  private final IdentificationCardRepository identificationCardRepository;
+  private final ContactDetailRepository contactDetailRepository;
+  private final FieldValueRepository fieldValueRepository;
+  private final CommandRepository commandRepository;
+
+  @Autowired
+  public CustomerService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                         final CustomerRepository customerRepository,
+                         final IdentificationCardRepository identificationCardRepository,
+                         final ContactDetailRepository contactDetailRepository,
+                         final FieldValueRepository fieldValueRepository,
+                         final CommandRepository commandRepository) {
+    super();
+    this.logger = logger;
+    this.customerRepository = customerRepository;
+    this.identificationCardRepository = identificationCardRepository;
+    this.contactDetailRepository = contactDetailRepository;
+    this.fieldValueRepository = fieldValueRepository;
+    this.commandRepository = commandRepository;
+  }
+
+  public Boolean customerExists(final String identifier) {
+    return this.customerRepository.existsByIdentifier(identifier);
+  }
+
+  public Optional<Customer> findCustomer(final String identifier) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(identifier);
+    if (customerEntity != null) {
+      final Customer customer = CustomerMapper.map(customerEntity);
+      customer.setAddress(AddressMapper.map(customerEntity.getAddress()));
+
+      final IdentificationCardEntity identificationCardEntity = this.identificationCardRepository.findByCustomer(customerEntity);
+      if (identificationCardEntity != null) {
+        customer.setIdentificationCard(IdentificationCardMapper.map(identificationCardEntity));
+      }
+
+      final List<ContactDetailEntity> contactDetailEntities = this.contactDetailRepository.findByCustomer(customerEntity);
+      if (contactDetailEntities != null) {
+        customer.setContactDetails(
+            contactDetailEntities
+                .stream()
+                  .map(ContactDetailMapper::map)
+                  .collect(Collectors.toList())
+        );
+      }
+
+      final List<FieldValueEntity> fieldValueEntities = this.fieldValueRepository.findByCustomer(customerEntity);
+      if (fieldValueEntities != null) {
+        customer.setCustomValues(
+            fieldValueEntities
+                .stream()
+                .map(fieldValueEntity -> {
+                  final Value value = new Value();
+                  value.setValue(fieldValueEntity.getValue());
+                  final FieldEntity fieldEntity = fieldValueEntity.getField();
+                  value.setCatalogIdentifier(fieldEntity.getCatalog().getIdentifier());
+                  value.setFieldIdentifier(fieldEntity.getIdentifier());
+                  return value;
+                }).collect(Collectors.toList())
+        );
+      }
+
+      return Optional.of(customer);
+    } else {
+      return Optional.empty();
+    }
+  }
+
+  public CustomerPage fetchCustomer(final String term, final Boolean includeClosed, final Pageable pageable) {
+    final Page<CustomerEntity> customerEntities;
+    if (includeClosed) {
+      if (term != null) {
+        customerEntities = this.customerRepository.findByIdentifierContaining(term, pageable);
+      } else {
+        customerEntities = this.customerRepository.findAll(pageable);
+      }
+    } else {
+      if (term != null) {
+        customerEntities = this.customerRepository.findByCurrentStateNotAndIdentifierContaining(Customer.State.CLOSED.name(), term, pageable);
+      } else {
+        customerEntities = this.customerRepository.findByCurrentStateNot(Customer.State.CLOSED.name(), pageable);
+      }
+    }
+
+    final CustomerPage customerPage = new CustomerPage();
+    customerPage.setTotalPages(customerEntities.getTotalPages());
+    customerPage.setTotalElements(customerEntities.getTotalElements());
+    if (customerEntities.getSize() > 0) {
+      final ArrayList<Customer> customers = new ArrayList<>(customerEntities.getSize());
+      customerPage.setCustomers(customers);
+      customerEntities.forEach(customerEntity -> customers.add(CustomerMapper.map(customerEntity)));
+    }
+
+    return customerPage;
+  }
+
+  public final List<Command> fetchCommandsByCustomer(final String identifier) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(identifier);
+    final List<CommandEntity> commands = this.commandRepository.findByCustomer(customerEntity);
+    if (commands != null) {
+      return commands.stream().map(CommandMapper::map).collect(Collectors.toList());
+    } else {
+      return Collections.emptyList();
+    }
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/service/TaskService.java b/service/src/main/java/io/mifos/customer/service/internal/service/TaskService.java
new file mode 100644
index 0000000..5f4c9de
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/service/TaskService.java
@@ -0,0 +1,77 @@
+/*
+ * 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.TaskDefinition;
+import io.mifos.customer.service.internal.mapper.TaskDefinitionMapper;
+import io.mifos.customer.service.internal.repository.CustomerEntity;
+import io.mifos.customer.service.internal.repository.CustomerRepository;
+import io.mifos.customer.service.internal.repository.TaskDefinitionRepository;
+import io.mifos.customer.service.internal.repository.TaskInstanceRepository;
+import io.mifos.customer.service.internal.repository.TaskDefinitionEntity;
+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;
+
+@Service
+public class TaskService {
+
+  private final TaskDefinitionRepository taskDefinitionRepository;
+  private final TaskInstanceRepository taskInstanceRepository;
+  private final CustomerRepository customerRepository;
+
+  @Autowired
+  public TaskService(final TaskDefinitionRepository taskDefinitionRepository,
+                     final TaskInstanceRepository taskInstanceRepository,
+                     final CustomerRepository customerRepository) {
+    super();
+    this.taskDefinitionRepository = taskDefinitionRepository;
+    this.taskInstanceRepository = taskInstanceRepository;
+    this.customerRepository = customerRepository;
+  }
+
+  public Boolean taskDefinitionExists(final String identifier) {
+    return this.taskDefinitionRepository.existsByIdentifier(identifier);
+  }
+
+  public Optional<TaskDefinition> findByIdentifier(final String identifier) {
+    final TaskDefinitionEntity taskDefinitionEntity = this.taskDefinitionRepository.findByIdentifier(identifier);
+    if (taskDefinitionEntity != null) {
+      return Optional.of(TaskDefinitionMapper.map(taskDefinitionEntity));
+    } else {
+      return Optional.empty();
+    }
+  }
+
+  public List<TaskDefinition> fetchAll() {
+    return this.taskDefinitionRepository.findAll()
+        .stream()
+        .map(TaskDefinitionMapper::map)
+        .collect(Collectors.toList());
+  }
+
+  public List<TaskDefinition> findTasksByCustomer(final String customerIdentifier, Boolean includeExecuted) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(customerIdentifier);
+    return this.taskInstanceRepository.findByCustomer(customerEntity)
+        .stream()
+        .filter(taskInstanceEntity -> (includeExecuted ? true : taskInstanceEntity.getExecutedBy() == null))
+        .map(taskInstanceEntity -> TaskDefinitionMapper.map(taskInstanceEntity.getTaskDefinition()))
+        .collect(Collectors.toList());
+  }
+}
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
new file mode 100644
index 0000000..528dea6
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/rest/config/CustomerRestConfiguration.java
@@ -0,0 +1,65 @@
+/*
+ * 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 io.mifos.anubis.config.EnableAnubis;
+import io.mifos.core.async.config.EnableAsync;
+import io.mifos.core.cassandra.config.EnableCassandra;
+import io.mifos.core.command.config.EnableCommandProcessing;
+import io.mifos.core.lang.ApplicationName;
+import io.mifos.core.lang.config.EnableApplicationName;
+import io.mifos.core.lang.config.EnableServiceException;
+import io.mifos.core.lang.config.EnableTenantContext;
+import io.mifos.customer.catalog.service.rest.config.CatalogRestConfiguration;
+import io.mifos.customer.service.ServiceConstants;
+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.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@EnableAutoConfiguration
+@EnableDiscoveryClient
+@EnableAsync
+@EnableTenantContext
+@EnableCassandra
+@EnableCommandProcessing
+@EnableAnubis
+@EnableServiceException
+@EnableApplicationName
+@ComponentScan({
+    "io.mifos.customer.service.rest.controller"
+})
+@Import({
+    CatalogRestConfiguration.class,
+    CustomerServiceConfiguration.class
+})
+public class CustomerRestConfiguration {
+
+  public CustomerRestConfiguration() {
+    super();
+  }
+
+  @Bean(name = ServiceConstants.LOGGER_NAME)
+  public Logger logger(final ApplicationName applicationName) {
+    return LoggerFactory.getLogger(applicationName.getServiceName());
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java b/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java
new file mode 100644
index 0000000..0062f22
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java
@@ -0,0 +1,489 @@
+/*
+ * 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.api.util.UserContextHolder;
+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.Address;
+import io.mifos.customer.api.v1.domain.Command;
+import io.mifos.customer.api.v1.domain.ContactDetail;
+import io.mifos.customer.api.v1.domain.Customer;
+import io.mifos.customer.api.v1.domain.CustomerPage;
+import io.mifos.customer.api.v1.domain.IdentificationCard;
+import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.catalog.service.internal.service.FieldValueValidator;
+import io.mifos.customer.service.internal.command.CreateCustomerCommand;
+import io.mifos.customer.service.internal.command.ExecuteTaskForCustomerCommand;
+import io.mifos.customer.service.internal.command.InitializeServiceCommand;
+import io.mifos.customer.service.internal.command.LockCustomerCommand;
+import io.mifos.customer.service.internal.command.UnlockCustomerCommand;
+import io.mifos.customer.service.internal.command.UpdateAddressCommand;
+import io.mifos.customer.service.internal.command.UpdateCustomerCommand;
+import io.mifos.customer.service.ServiceConstants;
+import io.mifos.customer.service.internal.command.ActivateCustomerCommand;
+import io.mifos.customer.service.internal.command.AddTaskDefinitionToCustomerCommand;
+import io.mifos.customer.service.internal.command.CloseCustomerCommand;
+import io.mifos.customer.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.customer.service.internal.command.ReopenCustomerCommand;
+import io.mifos.customer.service.internal.command.UpdateContactDetailsCommand;
+import io.mifos.customer.service.internal.command.UpdateIdentificationCardCommand;
+import io.mifos.customer.service.internal.command.UpdateTaskDefinitionCommand;
+import io.mifos.customer.service.internal.service.CustomerService;
+import io.mifos.customer.service.internal.service.TaskService;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/")
+public class CustomerRestController {
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+  private final CustomerService customerService;
+  private final FieldValueValidator fieldValueValidator;
+  private final TaskService taskService;
+
+  @Autowired
+  public CustomerRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                                final CommandGateway commandGateway,
+                                final CustomerService customerService,
+                                final FieldValueValidator fieldValueValidator,
+                                final TaskService taskService) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+    this.customerService = customerService;
+    this.fieldValueValidator = fieldValueValidator;
+    this.taskService = taskService;
+  }
+
+  @Permittable(value = AcceptedTokenType.SYSTEM)
+  @RequestMapping(
+      value = "/initialize",
+      method = RequestMethod.POST,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  ResponseEntity<Void>
+  initialize() throws InterruptedException {
+    this.commandGateway.process(new InitializeServiceCommand());
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> createCustomer(@RequestBody @Valid final Customer customer) throws InterruptedException {
+    if (this.customerService.customerExists(customer.getIdentifier())) {
+      throw ServiceException.conflict("Customer {0} already exists.", customer.getIdentifier());
+    }
+
+    if (customer.getCustomValues() != null) {
+      this.fieldValueValidator.validateValues(customer.getCustomValues());
+    }
+
+    this.commandGateway.process(new CreateCustomerCommand(customer));
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers",
+      method = RequestMethod.GET,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<CustomerPage> fetchCustomers(@RequestParam(value = "term", required = false) final String term,
+                                              @RequestParam(value = "includeClosed", required = false) final Boolean includeClosed,
+                                              @RequestParam(value = "pageIndex", required = false) final Integer pageIndex,
+                                              @RequestParam(value = "size", required = false) final Integer size,
+                                              @RequestParam(value = "sortColumn", required = false) final String sortColumn,
+                                              @RequestParam(value = "sortDirection", required = false) final String sortDirection) {
+    return ResponseEntity.ok(this.customerService.fetchCustomer(
+        term, (includeClosed != null ? includeClosed : Boolean.FALSE),
+        this.createPageRequest(pageIndex, size, sortColumn, sortDirection)));
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Customer> findCustomer(@PathVariable("identifier") final String identifier) {
+    final Optional<Customer> customer = this.customerService.findCustomer(identifier);
+    if (customer.isPresent()) {
+      return ResponseEntity.ok(customer.get());
+    } else {
+      throw ServiceException.notFound("Customer {0} not found.", identifier);
+    }
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> updateCustomer(@PathVariable("identifier") final String identifier,
+                                      @RequestBody final Customer customer) {
+    if (this.customerService.customerExists(identifier)) {
+      if (customer.getCustomValues() != null) {
+        this.fieldValueValidator.validateValues(customer.getCustomValues());
+      }
+      this.commandGateway.process(new UpdateCustomerCommand(customer));
+    } else {
+      throw ServiceException.notFound("Customer {0} not found.", identifier);
+    }
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/commands",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> customerCommand(@PathVariable("identifier") final String identifier,
+                                       @RequestBody final Command command) {
+    final Optional<Customer> customerOptional = this.customerService.findCustomer(identifier);
+    if (customerOptional.isPresent()) {
+      final Customer customer = customerOptional.get();
+      final Command.Action action = Command.Action.valueOf(command.getAction());
+      switch (action) {
+        case ACTIVATE:
+          if (!customer.getCurrentState().equals(Customer.State.PENDING.name())) {
+            throw ServiceException.badRequest(
+                "Customer {0} can not be activated, current state is {1}.",
+                identifier,
+                customer.getCurrentState());
+          }
+          this.commandGateway.process(new ActivateCustomerCommand(identifier, command.getComment()));
+          break;
+        case LOCK:
+          if (!customer.getCurrentState().equals(Customer.State.ACTIVE.name())) {
+            throw ServiceException.badRequest(
+                "Customer {0} can not be locked, current state is {1}.",
+                identifier,
+                customer.getCurrentState());
+          }
+          this.commandGateway.process(new LockCustomerCommand(identifier, command.getComment()));
+          break;
+        case UNLOCK:
+          if (!customer.getCurrentState().equals(Customer.State.LOCKED.name())) {
+            throw ServiceException.badRequest(
+                "Customer {0} can not be unlocked, current state is {1}.",
+                identifier,
+                customer.getCurrentState());
+          }
+          this.commandGateway.process(new UnlockCustomerCommand(identifier, command.getComment()));
+          break;
+        case CLOSE:
+          if (!customer.getCurrentState().equals(Customer.State.ACTIVE.name())
+              && !customer.getCurrentState().equals(Customer.State.LOCKED.name())) {
+            throw ServiceException.badRequest(
+                "Customer {0} can not be closed, current state is {1}.",
+                identifier,
+                customer.getCurrentState());
+          }
+          this.commandGateway.process(new CloseCustomerCommand(identifier, command.getComment()));
+          break;
+        case REOPEN:
+          if (!customer.getCurrentState().equals(Customer.State.CLOSED.name())) {
+            throw ServiceException.badRequest(
+                "Customer {0} can not be reopened, current state is {1}.",
+                identifier,
+                customer.getCurrentState());
+          }
+          this.commandGateway.process(new ReopenCustomerCommand(identifier, command.getComment()));
+          break;
+        default:
+          throw ServiceException.badRequest("Unsupported action {0}.", command.getAction());
+      }
+    } else {
+      throw ServiceException.notFound("Customer {0} not found.", identifier);
+    }
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/commands",
+      method = RequestMethod.GET,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<List<Command>> fetchCustomerCommands(@PathVariable("identifier") final String identifier) {
+    if (this.customerService.customerExists(identifier)) {
+      return ResponseEntity.ok(this.customerService.fetchCommandsByCustomer(identifier));
+    } else {
+      throw ServiceException.notFound("Customer {0} not found.", identifier);
+    }
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/tasks/{taskIdentifier}",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
... 772 lines suppressed ...

-- 
To stop receiving notification emails like this one, please contact
myrle@apache.org.

Mime
View raw message