Merge branch 'develop' into FINCN-124
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..a3b6dc5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+LICENSE
+README.md
+docs/
+HEADER
+NOTICE.txt
+.git/
+.gitignore
diff --git a/.gitignore b/.gitignore
index 480c856..5b0038d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,10 @@
 .idea
 build/
 target/
-**/out
+
+api/out/
+component-test/out/
+service/out
 
 # Ignore Gradle GUI config
 gradle-app.setting
@@ -15,3 +18,5 @@
 *.log
 
 *.toDelete
+
+*.jar
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2f484c8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+language: java
+sudo: false
+jdk:
+- openjdk8
+install: true
+script: "./travis.sh"
+env:
+  global:
+    - BUILD_SNAPSHOTS_BRANCH=develop
+    - ARTIFACTORY_URL=https://mifos.jfrog.io/mifos
+    - ARTIFACTORY_USER=travis-ci
+    - secure: "Q+gO7X7kludIHuAI/VIiI60olEHEkUVVPT0atuf9GceT6IaKjWkdO2w3uwltNpedGcNX07bA2P/5ELxCkiL+maCbxXED2u/+zZfvKenUzRsDXkaZ4PjX9T0poX+cai4yO4uEdUSq5qmVun7JN4KB5hQq96yMditRtHb4LWM28r6owRykQ1aRNGsWlYhFTni63HSICyZFpuWc1/7N6ClEJYiJRwkNG2NFHpqi9CkpX9YgIoJgx7226RBMm6azRek5BAMNWsRYn7iKDcL6XakWUrXGPTBM37Cg75ClCrweXwi9J1d7O7gEYrqhPgzzKWpg4vrnaZwRhGV1esAhXKUuHyMyfM1UTez3zRancNJj8SmdXPHCU45vc6r5qdJTzt8GCyJGxIe0Wh/X/8j/4oMC7vBOELqIINU0V/aopFAeutrUxacdl91qF7iV6xZBxLmrSJeRMLL/d4LENFysSozfH1E50rWxnou/fm2acjl/6uyCoe4IcA7hUBgnJhd6GRwkg/Uu/Y7HmFBcvMUveziFG/Dy8zlNZBuXRbdbwkGpnFtxjRADw97VkiRpI+yn+uisXbUN//t7yWgxEO3MovUylNdRjsNHE/VzVz5Q3UZihL9NUqY8pmWbV2zZ85lSgTVlp0gY6M+94y4WO8La2yzW8h9rbsCzdGlX5FVtsW0jKr4="
+after_failure:
+  - ./gradlew rat
+  - cat api/build/reports/rat/rat-report.txt
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3ee37c8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+FROM openjdk:8-jdk-alpine AS builder
+RUN mkdir builddir
+COPY . builddir
+WORKDIR builddir
+RUN ./gradlew publishToMavenLocal
+
+FROM openjdk:8-jdk-alpine AS runner
+
+ARG portfolio_port=2026
+
+ENV server.max-http-header-size=16384 \
+    cassandra.clusterName="Test Cluster" \
+    portfolio.bookLateFeesAndInterestAsUser=imhotep \
+    server.port=$portfolio_port
+
+WORKDIR /tmp
+COPY --from=builder /builddir/service/build/libs/service-0.1.0-BUILD-SNAPSHOT-boot.jar ./portfolio-service-boot.jar
+
+CMD ["java", "-jar", "portfolio-service-boot.jar"]
diff --git a/README.md b/README.md
index 6d41c15..78c9c6a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Apache Fineract CN Portfolio
+# Apache Fineract CN Portfolio [![Build Status](https://api.travis-ci.com/apache/fineract-cn-portfolio.svg?branch=develop)](https://travis-ci.com/apache/fineract-cn-portfolio)
 
 This project provides product and case management for Fineract CN.  Products are described and "instantiated"
 for customers as cases.
diff --git a/api/build.gradle b/api/build.gradle
index 421c480..5e0f91d 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -30,6 +30,7 @@
 plugins {
     id 'com.github.hierynomus.license' version '0.13.1'
     id("org.nosphere.apache.rat") version "0.3.1"
+    id "com.jfrog.artifactory" version "4.9.5"
 }
 
 apply from: '../shared.gradle'
@@ -55,7 +56,7 @@
             from components.java
             groupId project.group
             artifactId project.name
-            version project.version
+            version project.findProperty('externalVersion') ?: project.version
         }
     }
 }
diff --git a/build.gradle b/build.gradle
index 18918cd..eeb68fb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,6 +39,14 @@
     dependsOn publishComponentTestToMavenLocal
 }
 
+task artifactoryPublish {
+    group 'all'
+    dependsOn publishToMavenLocal
+    dependsOn gradle.includedBuild('api').task(':artifactoryPublish')
+    dependsOn gradle.includedBuild('service').task(':artifactoryPublish')
+    dependsOn gradle.includedBuild('component-test').task(':artifactoryPublish')
+}
+
 task prepareForTest {
     group 'all'
     dependsOn publishToMavenLocal
@@ -57,4 +65,4 @@
     dependsOn gradle.includedBuild('api').task(':rat')
     dependsOn gradle.includedBuild('service').task(':rat')
     dependsOn gradle.includedBuild('component-test').task(':rat')
-}
\ No newline at end of file
+}
diff --git a/component-test/build.gradle b/component-test/build.gradle
index b0dd2d7..266d54f 100644
--- a/component-test/build.gradle
+++ b/component-test/build.gradle
@@ -26,16 +26,22 @@
 
     dependencies {
         classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+        classpath  'org.asciidoctor:asciidoctor-gradle-plugin:1.5.2'
     }
 }
 
 plugins {
     id 'com.github.hierynomus.license' version '0.13.1'
     id("org.nosphere.apache.rat") version "0.3.1"
+    id "com.jfrog.artifactory" version "4.9.5"
 }
 
 apply from: '../shared.gradle'
 
+apply plugin: 'spring-boot'
+apply plugin: 'org.asciidoctor.convert'
+
+
 dependencies {
     compile(
             [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka-server'],
@@ -46,14 +52,26 @@
             [group: 'org.apache.fineract.cn', name: 'api', version: versions.frameworkapi],
             [group: 'org.apache.fineract.cn', name: 'test', version: versions.frameworktest],
             [group: 'org.apache.fineract.cn', name: 'lang', version: versions.frameworklang],
-            [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test'],
+            [group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc'],
+            [group: 'junit', name: 'junit', version: '4.12']
     )
+    
+}
+
+asciidoctor {
+    sourceDir 'build/doc/asciidoc/'
+    outputDir 'build/doc/html5'
+    options backend: "html", doctype: "book"
+    attributes "source-highlighter": "highlightjs", \
+                'snippets': file('build/doc/generated-snippets/')
 }
 
 publishing {
     publications {
         mavenJava(MavenPublication) {
             from components.java
+            version project.findProperty('externalVersion') ?: project.version
         }
     }
 }
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/BalanceSegmentSetApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/BalanceSegmentSetApiDocumentation.java
new file mode 100644
index 0000000..310e638
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/BalanceSegmentSetApiDocumentation.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.portfolio.api.v1.domain.BalanceSegmentSet;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.apache.fineract.cn.portfolio.api.v1.events.BalanceSegmentSetEvent;
+import org.apache.fineract.cn.portfolio.api.v1.events.EventConstants;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class BalanceSegmentSetApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-balancesegmentsets");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentCreateBalanceSegmentSet() throws Exception {
+
+    final Product product = createProduct();
+
+    final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+    balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifier("bss"));
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(5, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.TEN.setScale(5, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(10_000_0000, 5)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("abc", "def", "ghi"));
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/products/" + product.getIdentifier() + "/balancesegmentsets/")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(balanceSegmentSet)))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-create-balance-segment-set", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Balance segment set's identifier"),
+                            fieldWithPath("segments").description("Balance segment set's given name"),
+                            fieldWithPath("segmentIdentifiers").description("Balance segment sets's segment identfiers"))));
+
+  }
+
+  @Test
+  public void documentGetAllBalanceSegmentSets() throws Exception {
+    final Product product = createProduct();
+
+    final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+    balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifier("bss"));
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.TEN.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(10_000_0000, 3)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("how", "are", "you"));
+
+    portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+
+    this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/balancesegmentsets/")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+            .andExpect(status().isOk())
+            .andDo(document(
+                    "document-get-all-balance-segment-sets", preprocessRequest(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("[]").description("An array of balance segment sets"),
+                            fieldWithPath("[].identifier").description("Balance segment set's identifier "),
+                            fieldWithPath("[].segments").description("Balance segment set's segments "),
+                            fieldWithPath("[].segmentIdentifiers").description("Balance segment set's segment identifier"))));
+
+  }
+
+  @Test
+  public void documentGetBalanceSegmentSet() throws Exception {
+    final Product product = createProduct();
+
+    final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+    balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifier("bss"));
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.TEN.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(10_000_0000, 3)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("how", "are", "you"));
+
+    portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+    this.eventRecorder.wait(EventConstants.POST_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier()));
+
+
+    this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/balancesegmentsets/" + balanceSegmentSet.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+            .andExpect(status().isOk())
+            .andDo(document(
+                    "document-get-balance-segment-set", preprocessRequest(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("identifier").description("Balance segment set's identifier"),
+                            fieldWithPath("segments").description("Balance segment set's given name"),
+                            fieldWithPath("segmentIdentifiers").description("Balance segment sets's segment identfiers"))));
+
+  }
+
+  @Test
+  public void documentChangeBalanceSegmentSet() throws Exception {
+    final Product product = createProduct();
+
+    final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+    balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifier("bss"));
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.TEN.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(10_000_0000, 3)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("how", "are", "you"));
+
+    portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+    this.eventRecorder.wait(EventConstants.POST_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier()));
+
+
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(6, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(100_0000, 6),
+            BigDecimal.valueOf(10_000_0000, 6)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("abc", "def", "ghi"));
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/products/" + product.getIdentifier() + "/balancesegmentsets/" + balanceSegmentSet.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(balanceSegmentSet)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-balance-segment-set", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Balance segment set's identifier"),
+                            fieldWithPath("segments").description("Balance segment set's given name"),
+                            fieldWithPath("segmentIdentifiers").description("Balance segment sets's segment identfiers"))));
+
+  }
+
+  @Test
+  public void documentDeleteBalanceSegmentSet() throws Exception {
+    final Product product = createProduct();
+
+    final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+    balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifier("bss"));
+    balanceSegmentSet.setSegments(Arrays.asList(
+            BigDecimal.ZERO.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.TEN.setScale(3, BigDecimal.ROUND_HALF_EVEN),
+            BigDecimal.valueOf(10_000_0000, 3)));
+    balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("am", "fine", "thanks"));
+
+    portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+    this.mockMvc.perform(delete("/products/" + product.getIdentifier() + "/balancesegmentsets/" + balanceSegmentSet.getIdentifier())
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-delete-balance-segment-set", preprocessResponse(prettyPrint())));
+
+  }
+
+
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/CaseDocumentsApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/CaseDocumentsApiDocumentation.java
new file mode 100644
index 0000000..856998a
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/CaseDocumentsApiDocumentation.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.individuallending.api.v1.domain.caseinstance.CaseCustomerDocuments;
+import org.apache.fineract.cn.portfolio.api.v1.domain.*;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.Arrays;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class CaseDocumentsApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-casedocuments");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentGetCaseDocuments() throws Exception {
+
+    final Product product = createAndEnableProduct();
+
+    final Case customerCase = createCase(product.getIdentifier());
+
+
+    final CaseCustomerDocuments caseDocuments = caseDocumentsManager.getCaseDocuments(
+            product.getIdentifier(), customerCase.getIdentifier());
+
+    final CaseCustomerDocuments.Document studentLoanDocument
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "student_loan_documents");
+    final CaseCustomerDocuments.Document houseTitle
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "house_title");
+    final CaseCustomerDocuments.Document workContract
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "work_contract");
+
+    caseDocuments.setDocuments(Arrays.asList(studentLoanDocument, houseTitle, workContract));
+
+    this.mockMvc.perform(get("/individuallending/products/" + product.getIdentifier() + "/cases/" + customerCase.getIdentifier() + "/documents")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isOk())
+            .andDo(document("document-get-case-documents", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("documents").type("List<Document>").description("The case document +\n" +
+                                    " +\n" +
+                                    "_Document_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     customerId, +\n" +
+                                    "     documentId, +\n" +
+                                    "  } +"))));
+
+  }
+
+
+  @Test
+  public void documentChangeCaseDocuments() throws Exception {
+
+    final Product product = createAndEnableProduct();
+
+    final Case customerCase = createCase(product.getIdentifier());
+
+    final CaseCustomerDocuments caseDocuments = caseDocumentsManager.getCaseDocuments(
+            product.getIdentifier(), customerCase.getIdentifier());
+
+    final CaseCustomerDocuments.Document houseLoanDocument
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "house_loan_documents");
+    final CaseCustomerDocuments.Document houseTitle
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "house_title");
+    final CaseCustomerDocuments.Document workContract
+            = new CaseCustomerDocuments.Document(Fixture.CUSTOMER_IDENTIFIER, "work_contract");
+
+    caseDocuments.setDocuments(Arrays.asList(houseLoanDocument, houseTitle, workContract));
+
+    caseDocumentsManager.changeCaseDocuments(product.getIdentifier(), customerCase.getIdentifier(), caseDocuments);
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/individuallending/products/" + product.getIdentifier() + "/cases/" + customerCase.getIdentifier() + "/documents")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(caseDocuments)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-case-documents", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("documents").type("List<Document>").description("The case document +\n" +
+                                    " +\n" +
+                                    "_Document_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     customerId, +\n" +
+                                    "     documentId, +\n" +
+                                    "  } +"))));
+  }
+
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/CasesApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/CasesApiDocumentation.java
new file mode 100644
index 0000000..2470942
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/CasesApiDocumentation.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import org.apache.fineract.cn.individuallending.api.v1.domain.product.AccountDesignators;
+import org.apache.fineract.cn.portfolio.api.v1.domain.AccountAssignment;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Case;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class CasesApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-cases");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentCreateCase() throws Exception {
+
+    final Product product = createAndEnableProduct();
+
+    final Case caseInstance = createCase(product.getIdentifier());
+    caseInstance.setIdentifier("case-v1");
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/products/" + product.getIdentifier() + "/cases/")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(caseInstance)))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-create-case", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Cases's identifier"),
+                            fieldWithPath("productIdentifier").description("Products's identifier"),
+                            fieldWithPath("interest").description("Cases's interest"),
+                            fieldWithPath("parameters").description("cases's parameters"),
+                            fieldWithPath("accountAssignments").description("Cases's account assignment"),
+                            fieldWithPath("currentState").description("Cases's current state"))));
+
+  }
+
+  @Test
+  public void documentChangeCase() throws Exception {
+
+    final Product product = createAndEnableProduct();
+
+
+    final CaseParameters newCaseParameters = Fixture.createAdjustedCaseParameters(x -> {
+    });
+    final String originalParameters = new Gson().toJson(newCaseParameters);
+    final Case caseInstance = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(originalParameters));
+
+    final Set<AccountAssignment> accountAssignments = new HashSet<>();
+    accountAssignments.add(new AccountAssignment(AccountDesignators.CUSTOMER_LOAN_GROUP, "002-011"));
+    accountAssignments.add(new AccountAssignment(AccountDesignators.ENTRY, "002-012"));
+    caseInstance.setAccountAssignments(accountAssignments);
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/products/" + product.getIdentifier() + "/cases/" + caseInstance.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(caseInstance)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-case", preprocessRequest(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Cases's identifier"),
+                            fieldWithPath("productIdentifier").description("Products's identifier"),
+                            fieldWithPath("interest").description("Cases's interest"),
+                            fieldWithPath("parameters").description("cases's parameters"),
+                            fieldWithPath("accountAssignments").description("Cases's account assignment"),
+                            fieldWithPath("currentState").description("Cases's current state"))));
+
+  }
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/ChargeDefinitionApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/ChargeDefinitionApiDocumentation.java
new file mode 100644
index 0000000..281d25d
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/ChargeDefinitionApiDocumentation.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.individuallending.api.v1.domain.product.AccountDesignators;
+import org.apache.fineract.cn.individuallending.api.v1.domain.workflow.Action;
+import org.apache.fineract.cn.portfolio.api.v1.domain.ChargeDefinition;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.apache.fineract.cn.portfolio.api.v1.events.ChargeDefinitionEvent;
+import org.apache.fineract.cn.portfolio.api.v1.events.EventConstants;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.math.BigDecimal;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class ChargeDefinitionApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-chargedefinitions");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentGetAllChargeDefinitions() throws Exception {
+    final Product product = createProduct();
+
+    try {
+      this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/charges/")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-all-charge-definitions", preprocessRequest(prettyPrint()),
+                      responseFields(
+
+                              fieldWithPath("identifier").description("Charge definition's identifier"),
+                              fieldWithPath("name").description("Charge definitions given name"),
+                              fieldWithPath("description").description("Charge definitions description"),
+                              fieldWithPath("accrueAction").description("Charge definitions accrue action"),
+                              fieldWithPath("proportionalTo").description("Charge definitions proportional to"),
+                              fieldWithPath("accrualAccountDesignator").description("Charge definitions accrual acion generatort"),
+                              fieldWithPath("forCycleSizeUnit").description("Charge definitions cycle size unit"),
+                              fieldWithPath("forSegmentSet").description("Charge definitions segment set"),
+                              fieldWithPath("fromSegment").description("Charge definitions from segment"),
+                              fieldWithPath("toSegment").description("Charge definitions to segment"),
+                              fieldWithPath("chargeOnTop").description("Charge definitions charge on top"),
+                              fieldWithPath("fromAccountDesignator").description("From account designator"),
+                              fieldWithPath("toAccountDesignator").description("To account designator"),
+                              fieldWithPath("amount").description("Charge definition's amount"),
+                              fieldWithPath("chargeMethod").description("Charge definitions charge method"),
+                              fieldWithPath("chargeAction").description("Charge definition's charge action"),
+                              fieldWithPath("description").description("Employee's middle name"),
+                              fieldWithPath("readOnly").description("Readability"))));
+
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+  }
+
+
+  @Test
+  public void documentCreateChargeDefinition() throws Exception {
+
+    final Product product = createProduct();
+
+    final ChargeDefinition chargeDefinition = new ChargeDefinition();
+    chargeDefinition.setIdentifier("charge123");
+    chargeDefinition.setName("core123");
+    chargeDefinition.setFromAccountDesignator("Pembe");
+    chargeDefinition.setToAccountDesignator("Miriam");
+    chargeDefinition.setAmount(BigDecimal.ONE.setScale(3, BigDecimal.ROUND_UNNECESSARY));
+    chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+    chargeDefinition.setChargeAction(Action.OPEN.name());
+    chargeDefinition.setDescription("describe charge");
+
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/products/" + product.getIdentifier() + "/charges/")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(chargeDefinition)))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-create-charge-definition", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Charge definition's identifier"),
+                            fieldWithPath("name").description("Charge definitions given name"),
+                            fieldWithPath("fromAccountDesignator").description("From account designator"),
+                            fieldWithPath("toAccountDesignator").description("To account designator"),
+                            fieldWithPath("amount").description("Charge definition's amount"),
+                            fieldWithPath("chargeMethod").description("Charge definitions charge method"),
+                            fieldWithPath("chargeAction").description("Charge definition's charge action"),
+                            fieldWithPath("description").description("Employee's middle name"),
+                            fieldWithPath("readOnly").description("Readability"))));
+
+  }
+
+  @Test
+  public void documentChangeChargeDefinition() throws Exception {
+
+    final Product product = createProduct();
+
+    final ChargeDefinition chargeDefinition = new ChargeDefinition();
+    chargeDefinition.setIdentifier("charge124");
+    chargeDefinition.setName("core123");
+    chargeDefinition.setFromAccountDesignator("Pembe");
+    chargeDefinition.setToAccountDesignator("Miriam");
+    chargeDefinition.setAmount(BigDecimal.ONE.setScale(3, BigDecimal.ROUND_UNNECESSARY));
+    chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+    chargeDefinition.setChargeAction(Action.OPEN.name());
+    chargeDefinition.setDescription("describe charge");
+
+    portfolioManager.createChargeDefinition(product.getIdentifier(), chargeDefinition);
+
+    chargeDefinition.setName("charge12345");
+    chargeDefinition.setFromAccountDesignator("Paul");
+    chargeDefinition.setToAccountDesignator("Motia");
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/products/" + product.getIdentifier() + "/charges/" + chargeDefinition.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(chargeDefinition)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-charge-definition", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Charge definition's identifier"),
+                            fieldWithPath("name").description("Charge definitions given name"),
+                            fieldWithPath("fromAccountDesignator").description("From account designator"),
+                            fieldWithPath("toAccountDesignator").description("To account designator"),
+                            fieldWithPath("amount").description("Charge definition's amount"),
+                            fieldWithPath("chargeMethod").description("Charge definitions charge method"),
+                            fieldWithPath("chargeAction").description("Charge definition's charge action"),
+                            fieldWithPath("description").description("Employee's middle name"),
+                            fieldWithPath("readOnly").description("Readability"))));
+  }
+
+  @Test
+  public void documentGetChargeDefinition() throws Exception {
+
+    final Product product = createProduct();
+
+    final ChargeDefinition chargeDefinition = new ChargeDefinition();
+    chargeDefinition.setIdentifier("charge10");
+    chargeDefinition.setName("core123");
+    chargeDefinition.setFromAccountDesignator("pembe");
+    chargeDefinition.setToAccountDesignator("miriam");
+    chargeDefinition.setAmount(BigDecimal.ONE.setScale(3, BigDecimal.ROUND_UNNECESSARY));
+    chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+    chargeDefinition.setChargeAction(Action.OPEN.name());
+    chargeDefinition.setDescription("describe charge");
+
+    portfolioManager.createChargeDefinition(product.getIdentifier(), chargeDefinition);
+
+
+    this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/charges/" + chargeDefinition.getIdentifier())
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isOk())
+            .andDo(document("document-get-case-document", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("identifier").description("Charge definition's identifier"),
+                            fieldWithPath("name").description("Charge definitions given name"),
+                            fieldWithPath("description").description("Charge definitions description"),
+                            fieldWithPath("accrueAction").description("Charge definitions accrue action"),
+                            fieldWithPath("proportionalTo").description("Charge definitions proportional to"),
+                            fieldWithPath("accrualAccountDesignator").description("Charge definitions accrual acion generatort"),
+                            fieldWithPath("forCycleSizeUnit").description("Charge definitions cycle size unit"),
+                            fieldWithPath("forSegmentSet").description("Charge definitions segment set"),
+                            fieldWithPath("fromSegment").description("Charge definitions from segment"),
+                            fieldWithPath("toSegment").description("Charge definitions to segment"),
+                            fieldWithPath("chargeOnTop").description("Charge definitions charge on top"),
+                            fieldWithPath("fromAccountDesignator").description("From account designator"),
+                            fieldWithPath("toAccountDesignator").description("To account designator"),
+                            fieldWithPath("amount").description("Charge definition's amount"),
+                            fieldWithPath("chargeMethod").description("Charge definitions charge method"),
+                            fieldWithPath("chargeAction").description("Charge definition's charge action"),
+                            fieldWithPath("description").description("Employee's middle name"),
+                            fieldWithPath("readOnly").description("Readability"))));
+  }
+
+  @Test
+  public void documentDeleteChargeDefinition() throws Exception {
+
+    final Product product = createProduct();
+
+    final ChargeDefinition chargeDefinitionToDelete = new ChargeDefinition();
+    chargeDefinitionToDelete.setAmount(BigDecimal.TEN);
+    chargeDefinitionToDelete.setIdentifier("random123");
+    chargeDefinitionToDelete.setName("account");
+    chargeDefinitionToDelete.setDescription("account charge definition");
+    chargeDefinitionToDelete.setChargeAction(Action.APPROVE.name());
+    chargeDefinitionToDelete.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+    chargeDefinitionToDelete.setToAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
+    chargeDefinitionToDelete.setFromAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
+    portfolioManager.createChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete);
+    this.eventRecorder.wait(EventConstants.POST_CHARGE_DEFINITION,
+            new ChargeDefinitionEvent(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier()));
+
+    this.mockMvc.perform(delete("/products/" + product.getIdentifier() + "/charges/" + chargeDefinitionToDelete.getIdentifier())
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-delete-charge-definition", preprocessResponse(prettyPrint())));
+  }
+
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/LossProvisionApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/LossProvisionApiDocumentation.java
new file mode 100644
index 0000000..4673268
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/LossProvisionApiDocumentation.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.individuallending.api.v1.domain.product.LossProvisionConfiguration;
+import org.apache.fineract.cn.individuallending.api.v1.domain.product.LossProvisionStep;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+public class LossProvisionApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-lossprovision");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentChangeLossProvisionConfiguration() throws Exception {
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+
+    final List<LossProvisionStep> lossProvisionSteps = new ArrayList<>();
+    lossProvisionSteps.add(new LossProvisionStep(1, BigDecimal.valueOf(5_00, 2)));
+    final LossProvisionConfiguration lossProvisionConfiguration = new LossProvisionConfiguration(lossProvisionSteps);
+
+    individualLending.changeLossProvisionConfiguration(product.getIdentifier(), lossProvisionConfiguration);
+
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/individuallending/products/" + product.getIdentifier() + "/lossprovisionconfiguration")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(lossProvisionConfiguration)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-loss-provision-configuration", preprocessRequest(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("lossProvisionSteps").type("List<LossProvisionSteps>").description("The loss provision configurations +\n" +
+                                    " +\n" +
+                                    "_LossProvisionSteps_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     daysLate, +\n" +
+                                    "     percentProvision, +\n" +
+                                    "  } +")
+                    )
+            ));
+  }
+
+  @Test
+  public void documentGetLossProvisionConfiguration() throws Exception {
+
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+    final List<LossProvisionStep> lossProvisionSteps1 = new ArrayList<>();
+    lossProvisionSteps1.add(new LossProvisionStep(0, BigDecimal.valueOf(1_00, 2)));
+    lossProvisionSteps1.add(new LossProvisionStep(1, BigDecimal.valueOf(9_00, 2)));
+    lossProvisionSteps1.add(new LossProvisionStep(30, BigDecimal.valueOf(35_00, 2)));
+    lossProvisionSteps1.add(new LossProvisionStep(60, BigDecimal.valueOf(55_00, 2)));
+    final LossProvisionConfiguration lossProvisionConfiguration = new LossProvisionConfiguration(lossProvisionSteps1);
+
+    individualLending.changeLossProvisionConfiguration(product.getIdentifier(), lossProvisionConfiguration);
+
+    try {
+      this.mockMvc.perform(get("/individuallending/products/" + product.getIdentifier() + "/lossprovisionconfiguration")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-loss-provision-configuration", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("lossProvisionSteps").type("List<LossProvisionSteps>").description("The loss provision configurations +\n" +
+                                      " +\n" +
+                                      "_LossProvisionSteps_ { +\n" +
+                                      "  *enum* _Type_ { +\n" +
+                                      "     daysLate, +\n" +
+                                      "     percentProvision, +\n" +
+                                      "  } +")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+  }
+
+
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/PatternApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/PatternApiDocumentation.java
new file mode 100644
index 0000000..2d9c6a9
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/PatternApiDocumentation.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class PatternApiDocumentation extends AbstractPortfolioTest {
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-pattern");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentGetPatterns() throws Exception {
+
+    try {
+      this.mockMvc.perform(get("/patterns/")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-patterns", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("[].parameterPackage").description("Pattern's parameter package"),
+                              fieldWithPath("[].accountAssignmentGroups").description("Pattern's groups"),
+                              fieldWithPath("[].accountAssignmentsRequired").description("List of Pattern's account assignments")
+                      )
+
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/PlannedPaymentsApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/PlannedPaymentsApiDocumentation.java
new file mode 100644
index 0000000..dee2c3a
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/PlannedPaymentsApiDocumentation.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Case;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class PlannedPaymentsApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-plannedpayments");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentGetPaymentScheduledForCase() throws Exception {
+    final Product product = createAndEnableProduct();
+    final Case caseInstance = createCase(product.getIdentifier());
+
+    this.mockMvc.perform(get("/individuallending/products/" + product.getIdentifier() + "/cases/" + caseInstance.getIdentifier() + "/plannedpayments")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+            .andExpect(status().isOk())
+            .andDo(document(
+                    "document-get-payment-scheduled-for-case", preprocessRequest(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("chargeNames").description("Charge names"),
+                            fieldWithPath("elements").description("Payments"),
+                            fieldWithPath("totalPages").description("Total number of pages "),
+                            fieldWithPath("totalElements").description("Total elements found"))));
+
+  }
+
+  @Test
+  public void documentGetPaymentScheduledForParameters() throws Exception {
+    final Product product = createAndEnableProduct();
+    final Case caseInstance = createCase(product.getIdentifier());
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/individuallending/products/" + product.getIdentifier() + "/plannedpayments")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(caseInstance)))
+            .andExpect(status().isOk())
+            .andDo(document("document-get-payment-scheduled-for-parameters", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Cases's identifier"),
+                            fieldWithPath("productIdentifier").description("Products's identifier"),
+                            fieldWithPath("interest").description("Cases's interest"),
+                            fieldWithPath("parameters").description("cases's parameters"),
+                            fieldWithPath("accountAssignments").description("Cases's account assignment"),
+                            fieldWithPath("currentState").description("Cases's current state"))));
+
+  }
+
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/ProductsApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/ProductsApiDocumentation.java
new file mode 100644
index 0000000..e37237e
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/ProductsApiDocumentation.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.apache.fineract.cn.portfolio.api.v1.events.EventConstants;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class ProductsApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-product");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentCreateProduct() throws Exception {
+    final Product product = createAdjustedProduct(x -> {
+    });
+    product.setIdentifier("agro11");
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/products/")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(product)))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-create-product", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Product's identifier"),
+                            fieldWithPath("name").description("Product's given name"),
+                            fieldWithPath("termRange").type("List<TermRange>").description("The term range +\n" +
+                                    " +\n" +
+                                    "_TermRange_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     temporalUnit, +\n" +
+                                    "     maximum, +\n" +
+                                    "  } +"),
+                            fieldWithPath("balanceRange").description("Product's balance range"),
+                            fieldWithPath("interestRange").description("Products interest Range"),
+                            fieldWithPath("interestBasis").description("Products's interest basis"),
+                            fieldWithPath("patternPackage").description("Product's pattern package"),
+                            fieldWithPath("description").description("product description"),
+                            fieldWithPath("currencyCode").description("Country currency code"),
+                            fieldWithPath("minorCurrencyUnitDigits").description("Country minor currency unit"),
+                            fieldWithPath("accountAssignments").description("Account Assignments"),
+                            fieldWithPath("parameters").description("Product's parameters"),
+                            fieldWithPath("enabled").description("Readability"))));
+
+  }
+
+
+  @Test
+  public void documentGetProducts() throws Exception {
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+    this.eventRecorder.wait(EventConstants.POST_PRODUCT, product.getIdentifier());
+
+    try {
+      this.mockMvc.perform(get("/products?pageIndex=0&size=200")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-products", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("identifier").description("Product's identifier"),
+                              fieldWithPath("name").description("Product's given name"),
+                              fieldWithPath("termRange").type("List<TermRange>").description("The term range +\n" +
+                                      " +\n" +
+                                      "_TermRange_ { +\n" +
+                                      "  *enum* _Type_ { +\n" +
+                                      "     temporalUnit, +\n" +
+                                      "     maximum, +\n" +
+                                      "  } +"),
+                              fieldWithPath("balanceRange").description("Product's balance range"),
+                              fieldWithPath("interestRange").description("Products interest Range"),
+                              fieldWithPath("interestBasis").description("Products's interest basis"),
+                              fieldWithPath("patternPackage").description("Product's pattern package"),
+                              fieldWithPath("description").description("product description"),
+                              fieldWithPath("currencyCode").description("Country currency code"),
+                              fieldWithPath("minorCurrencyUnitDigits").description("Country minor currency unit"),
+                              fieldWithPath("accountAssignments").description("Account Assignments"),
+                              fieldWithPath("parameters").description("Product's parameters"),
+                              fieldWithPath("enabled").description("Readability"))));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+  }
+
+  @Test
+  public void documentDeleteProducts() throws Exception {
+
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+
+    this.mockMvc.perform(delete("/products/" + product.getIdentifier())
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-delete-products", preprocessResponse(prettyPrint())));
+
+  }
+
+  @Test
+  public void documentGetProduct() throws Exception {
+
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+    this.mockMvc.perform(get("/products/" + product.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+            .andExpect(status().isOk())
+            .andDo(document(
+                    "document-get-product", preprocessRequest(prettyPrint()),
+                    responseFields(
+                            fieldWithPath("identifier").description("Charge definition's identifier"),
+                            fieldWithPath("name").description("Charge definitions given name"),
+                            fieldWithPath("termRange").type("List<TermRange>").description("The term range +\n" +
+                                    " +\n" +
+                                    "_TermRange_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     temporalUnit, +\n" +
+                                    "     maximum, +\n" +
+                                    "  } +"),
+                            fieldWithPath("balanceRange").description("From account designator"),
+                            fieldWithPath("interestRange").description("To account designator"),
+                            fieldWithPath("interestBasis").description("Charge definition's amount"),
+                            fieldWithPath("patternPackage").description("Charge definitions charge method"),
+                            fieldWithPath("description").description("Charge definitions charge method"),
+                            fieldWithPath("currencyCode").description("Charge definition's charge action"),
+                            fieldWithPath("minorCurrencyUnitDigits").description("Employee's middle name"),
+                            fieldWithPath("accountAssignments").description("Readability"),
+                            fieldWithPath("parameters").description("Readability"),
+                            fieldWithPath("enabled").description("Readability"),
+                            fieldWithPath("createdOn").description("Readability"),
+                            fieldWithPath("createdBy").description("Readability"),
+                            fieldWithPath("lastModifiedOn").description("Readability"),
+                            fieldWithPath("lastModifiedBy").description("Readability"))));
+
+  }
+
+  @Test
+  public void documentChangeProduct() throws Exception {
+
+    final Product product = createAdjustedProduct(x -> {
+    });
+    product.setName("akawo");
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/products/" + product.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(product)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-product", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Charge definition's identifier"),
+                            fieldWithPath("name").description("Charge definitions given name"),
+                            fieldWithPath("termRange").type("List<TermRange>").description("The term range +\n" +
+                                    " +\n" +
+                                    "_TermRange_ { +\n" +
+                                    "  *enum* _Type_ { +\n" +
+                                    "     temporalUnit, +\n" +
+                                    "     maximum, +\n" +
+                                    "  } +"),
+                            fieldWithPath("balanceRange").description("From account designator"),
+                            fieldWithPath("interestRange").description("To account designator"),
+                            fieldWithPath("interestBasis").description("Charge definition's amount"),
+                            fieldWithPath("patternPackage").description("Charge definitions charge method"),
+                            fieldWithPath("description").description("Charge definitions charge method"),
+                            fieldWithPath("currencyCode").description("Charge definition's charge action"),
+                            fieldWithPath("minorCurrencyUnitDigits").description("Employee's middle name"),
+                            fieldWithPath("accountAssignments").description("Readability"),
+                            fieldWithPath("parameters").description("Readability"),
+                            fieldWithPath("enabled").description("Readability"))));
+
+  }
+
+  @Test
+  public void documentGetIncompleteAccountAssignments() throws Exception {
+
+    final Product product = createAdjustedProduct(x -> {
+    });
+
+    this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/incompleteaccountassignments")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+            .andExpect(status().isOk())
+            .andDo(document(
+                    "document-get-incomplete-account-assignments", preprocessRequest(prettyPrint()),
+                    responseFields(
+                    )));
+
+  }
+
+
+}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/portfolio/TaskDefinitionApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/portfolio/TaskDefinitionApiDocumentation.java
new file mode 100644
index 0000000..a3d62ed
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/portfolio/TaskDefinitionApiDocumentation.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.fineract.cn.portfolio;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
+import org.apache.fineract.cn.portfolio.api.v1.domain.TaskDefinition;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class TaskDefinitionApiDocumentation extends AbstractPortfolioTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-taskdefinition");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp() {
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void documentListTaskDefinitions() throws Exception {
+    final Product product = createProduct();
+
+    try {
+      this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/tasks/")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-list-task-definitions", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("identifier").type("String").description("task identifier's identifier"),
+                              fieldWithPath("name").type("String").description("task identifier's name"),
+                              fieldWithPath("description").type("String").description("task identifier's description"),
+                              fieldWithPath("actions").description("The task definition action"),
+                              fieldWithPath("fourEyes").type("String").description("task identifier's identifier"),
+                              fieldWithPath("mandatory").type("String").description("task identifier's identifier")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+  }
+
+
+  @Test
+  public void documentGetTaskDefinition() throws Exception {
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+    try {
+      this.mockMvc.perform(get("/products/" + product.getIdentifier() + "/tasks/" + taskDefinition.getIdentifier())
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-task-definition", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("identifier").type("String").description("task identifier's identifier"),
+                              fieldWithPath("name").type("String").description("task identifier's name"),
+                              fieldWithPath("description").type("String").description("task identifier's description"),
+                              fieldWithPath("actions").description("The task definition action"),
+                              fieldWithPath("fourEyes").type("String").description("task identifier's identifier"),
+                              fieldWithPath("mandatory").type("String").description("task identifier's identifier")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+  }
+
+  @Test
+  public void documentChangeTaskDefinition() throws Exception {
+
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+    taskDefinition.setDescription("Describe task definition");
+    taskDefinition.setFourEyes(false);
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(put("/products/" + product.getIdentifier() + "/tasks/" + taskDefinition.getIdentifier())
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(taskDefinition)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-change-task-definition", preprocessRequest(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").type("String").description("task identifier's identifier"),
+                            fieldWithPath("name").type("String").description("task identifier's name"),
+                            fieldWithPath("description").type("String").description("task identifier's description"),
+                            fieldWithPath("actions").description("The task definition action"),
+                            fieldWithPath("fourEyes").type("String").description("task identifier's identifier"),
+                            fieldWithPath("mandatory").type("String").description("task identifier's identifier")
+                    )
+
+            ));
+
+  }
+
+  @Test
+  public void documentAddTaskDefinition() throws Exception {
+
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+    taskDefinition.setIdentifier("ask");
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/products/" + product.getIdentifier() + "/tasks/")
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .content(gson.toJson(taskDefinition)))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-add-task-definition", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").type("String").description("task identifier's identifier"),
+                            fieldWithPath("name").type("String").description("task identifier's name"),
+                            fieldWithPath("description").type("String").description("task identifier's description"),
+                            fieldWithPath("actions").description("The task definition action"),
+                            fieldWithPath("fourEyes").type("String").description("task identifier's identifier"),
+                            fieldWithPath("mandatory").type("String").description("task identifier's identifier"))));
+  }
+
+  @Test
+  public void documentDeleteTaskDefinition() throws Exception {
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+    this.mockMvc.perform(delete("/products/" + product.getIdentifier() + "/tasks/" + taskDefinition.getIdentifier())
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .accept(MediaType.ALL_VALUE))
+            .andExpect(status().isAccepted())
+            .andDo(document("document-delete-task-definition", preprocessResponse(prettyPrint())));
+  }
+}
diff --git a/service/build.gradle b/service/build.gradle
index 2d55f52..de6e948 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -32,6 +32,7 @@
 plugins {
     id 'com.github.hierynomus.license' version '0.13.1'
     id("org.nosphere.apache.rat") version "0.3.1"
+    id "com.jfrog.artifactory" version "4.9.5"
 }
 
 apply from: '../shared.gradle'
@@ -76,14 +77,14 @@
             from components.java
             groupId project.group
             artifactId project.name
-            version project.version
+            version project.findProperty('externalVersion') ?: project.version
         }
         bootService(MavenPublication) {
             // "boot" jar
             artifact ("$buildDir/libs/$project.name-$version-boot.jar")
             groupId project.group
             artifactId ("$project.name-boot")
-            version project.version
+            version project.findProperty('externalVersion') ?: project.version
         }
     }
 }
diff --git a/shared.gradle b/shared.gradle
index ceb0f70..68dde5b 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -48,6 +48,7 @@
 repositories {
     jcenter()
     mavenLocal()
+    maven { url 'https://mifos.jfrog.io/mifos/libs-snapshot/' }
 }
 
 dependencyManagement {
@@ -79,6 +80,22 @@
     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
 }
 
+artifactory {
+    contextUrl = System.getenv("ARTIFACTORY_URL")
+    publish {
+        repository {
+            repoKey = project.findProperty('artifactoryRepoKey')
+            username = System.getenv("ARTIFACTORY_USER")
+            password = System.getenv("ARTIFACTORY_PASSWORD")
+        }
+
+        defaults {
+            publications ('api', 'componentTest', 'service', 'bootService')
+        }
+    }
+}
+artifactoryPublish.dependsOn('clean','publishToMavenLocal')
+
 license {
     header rootProject.file('../HEADER')
     strictCheck true
@@ -94,6 +111,7 @@
 rat {
     // List of exclude directives, defaults to ['**/.gradle/**']
     excludes = [
+            "**/.dockerignore/**",
             "**/.idea/**",
             "**/.gradle/**",
             "**/gradle/**",
@@ -102,4 +120,5 @@
             "gradlew.bat",
             "README.md"
     ]
+    plainOutput = true
 }
diff --git a/travis.sh b/travis.sh
new file mode 100755
index 0000000..03a75ee
--- /dev/null
+++ b/travis.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+
+# Documentation: https://cwiki.apache.org/confluence/display/FINERACT/Fineract-CN+Artifactory
+
+#Exit immediately if a command exits with a non-zero status.
+set -e
+EXIT_STATUS=0
+
+# Builds and Publishes a SNAPSHOT
+function build_snapshot() {
+  echo -e "Building and publishing a snapshot out of branch [$TRAVIS_BRANCH]"
+  ./gradlew -PartifactoryRepoKey=libs-snapshot-local -DbuildInfo.build.number=${TRAVIS_COMMIT::7} artifactoryPublish --stacktrace || EXIT_STATUS=$?
+}
+
+# Builds a Pull Request
+function build_pullrequest() {
+  echo -e "Building pull request #$TRAVIS_PULL_REQUEST of branch [$TRAVIS_BRANCH]. Won't publish anything to Artifactory."
+  ./gradlew publishToMavenLocal rat || EXIT_STATUS=$?
+}
+
+# For other branches we need to add branch name as prefix
+function build_otherbranch() {
+  echo -e "Building a snapshot out of branch [$TRAVIS_BRANCH] and publishing it with prefix '${TRAVIS_BRANCH}-SNAPSHOT'"
+  ./gradlew -PartifactoryRepoKey=libs-snapshot-local -DbuildInfo.build.number=${TRAVIS_COMMIT::7} -PexternalVersion=${TRAVIS_BRANCH}-SNAPSHOT artifactoryPublish --stacktrace || EXIT_STATUS=$?
+}
+
+# Builds and Publishes a Tag
+function build_tag() {
+  echo -e "Building tag [$TRAVIS_TAG] and publishing it as a release"
+  ./gradlew -PartifactoryRepoKey=libs-release-local -PexternalVersion=$TRAVIS_TAG artifactoryPublish --stacktrace || EXIT_STATUS=$?
+
+}
+
+echo -e "TRAVIS_BRANCH=$TRAVIS_BRANCH"
+echo -e "TRAVIS_TAG=$TRAVIS_TAG"
+echo -e "TRAVIS_COMMIT=${TRAVIS_COMMIT::7}"
+echo -e "TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST"
+
+# Build Logic
+if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+  build_pullrequest
+elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" != "$BUILD_SNAPSHOTS_BRANCH" ] && [ "$TRAVIS_TAG" == "" ]  ; then
+  build_otherbranch
+elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "$BUILD_SNAPSHOTS_BRANCH" ] && [ "$TRAVIS_TAG" == "" ] ; then
+  build_snapshot
+elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then
+  build_tag
+else
+  echo -e "WARN: Unexpected env variable values => Branch [$TRAVIS_BRANCH], Tag [$TRAVIS_TAG], Pull Request [#$TRAVIS_PULL_REQUEST]"
+  ./gradlew clean build
+fi
+
+exit ${EXIT_STATUS}