Accepting, persisting and checking case-level definition of interest.
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Case.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Case.java
index cebfaa2..542e7ca 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Case.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Case.java
@@ -15,10 +15,14 @@
  */
 package io.mifos.portfolio.api.v1.domain;
 
-import io.mifos.portfolio.api.v1.validation.ValidAccountAssignments;
 import io.mifos.core.lang.validation.constraints.ValidIdentifier;
+import io.mifos.portfolio.api.v1.validation.ValidAccountAssignments;
 import org.hibernate.validator.constraints.NotBlank;
 
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
 import java.util.Objects;
 import java.util.Set;
 
@@ -31,6 +35,12 @@
   private String identifier;
   @ValidIdentifier
   private String productIdentifier;
+
+  @DecimalMin(value = "0.00")
+  @DecimalMax(value = "999.99")
+  @NotNull
+  private BigDecimal interest;
+
   @NotBlank
   private String parameters;
   @ValidAccountAssignments
@@ -61,6 +71,14 @@
     this.productIdentifier = productIdentifier;
   }
 
+  public BigDecimal getInterest() {
+    return interest;
+  }
+
+  public void setInterest(BigDecimal interest) {
+    this.interest = interest;
+  }
+
   public String getParameters() {
     return parameters;
   }
@@ -123,30 +141,32 @@
     if (o == null || getClass() != o.getClass()) return false;
     Case aCase = (Case) o;
     return Objects.equals(identifier, aCase.identifier) &&
-            Objects.equals(productIdentifier, aCase.productIdentifier) &&
-            Objects.equals(parameters, aCase.parameters) &&
-            Objects.equals(accountAssignments, aCase.accountAssignments) &&
-            currentState == aCase.currentState;
+        Objects.equals(productIdentifier, aCase.productIdentifier) &&
+        Objects.equals(interest, aCase.interest) &&
+        Objects.equals(parameters, aCase.parameters) &&
+        Objects.equals(accountAssignments, aCase.accountAssignments) &&
+        currentState == aCase.currentState;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(identifier, productIdentifier, parameters, accountAssignments, currentState);
+    return Objects.hash(identifier, productIdentifier, interest, parameters, accountAssignments, currentState);
   }
 
   @Override
   public String toString() {
     return "Case{" +
-            "identifier='" + identifier + '\'' +
-            ", productIdentifier='" + productIdentifier + '\'' +
-            ", parameters='" + parameters + '\'' +
-            ", accountAssignments=" + accountAssignments +
-            ", currentState=" + currentState +
-            ", createdOn='" + createdOn + '\'' +
-            ", createdBy='" + createdBy + '\'' +
-            ", lastModifiedOn='" + lastModifiedOn + '\'' +
-            ", lastModifiedBy='" + lastModifiedBy + '\'' +
-            '}';
+        "identifier='" + identifier + '\'' +
+        ", productIdentifier='" + productIdentifier + '\'' +
+        ", interest=" + interest +
+        ", parameters='" + parameters + '\'' +
+        ", accountAssignments=" + accountAssignments +
+        ", currentState=" + currentState +
+        ", createdOn='" + createdOn + '\'' +
+        ", createdBy='" + createdBy + '\'' +
+        ", lastModifiedOn='" + lastModifiedOn + '\'' +
+        ", lastModifiedBy='" + lastModifiedBy + '\'' +
+        '}';
   }
 
   public enum State {
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index c253395..5643ecb 100644
--- a/api/src/test/java/io/mifos/Fixture.java
+++ b/api/src/test/java/io/mifos/Fixture.java
@@ -55,7 +55,7 @@
     product.setDescription("Loan for seeds or agricultural equipment");
     product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
     product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10000))));
-    product.setInterestRange(new InterestRange(BigDecimal.valueOf(3, 2), BigDecimal.valueOf(12, 2)));
+    product.setInterestRange(new InterestRange(BigDecimal.valueOf(3_00, 2), BigDecimal.valueOf(12_00, 2)));
     product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
 
     product.setCurrencyCode("XXX");
@@ -115,6 +115,7 @@
     accountAssignments.add(new AccountAssignment(ENTRY, "001-012"));
     ret.setAccountAssignments(accountAssignments);
     ret.setCurrentState(Case.State.CREATED.name());
+    ret.setInterest(BigDecimal.valueOf(10_0000, 4));
 
     final CaseParameters caseParameters = getTestCaseParameters();
     final Gson gson = new Gson();
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CaseTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CaseTest.java
index bc7ecd2..d9aeb46 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CaseTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CaseTest.java
@@ -21,6 +21,7 @@
 import org.apache.commons.lang.RandomStringUtils;
 import org.junit.runners.Parameterized;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -61,8 +62,14 @@
             .adjustment(x -> x.setProductIdentifier(null))
             .valid(false));
     ret.add(new ValidationTestCase<Case>("tooLongAccountIdentifier")
-            .adjustment(x -> x.getAccountAssignments().add(new AccountAssignment("x", "0123456789")))
-            .valid(false));
+        .adjustment(x -> x.getAccountAssignments().add(new AccountAssignment("x", "0123456789")))
+        .valid(false));
+    ret.add(new ValidationTestCase<Case>("out of range interest")
+        .adjustment(x -> x.setInterest(BigDecimal.TEN.negate()))
+        .valid(false));
+    ret.add(new ValidationTestCase<Case>("null interest")
+        .adjustment(x -> x.setInterest(BigDecimal.TEN.negate()))
+        .valid(false));
 
     return ret;
   }
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index 8c1a110..8ebb702 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -53,7 +53,7 @@
     product.setDescription("Loan for seeds or agricultural equipment");
     product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
     product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10000))));
-    product.setInterestRange(new InterestRange(BigDecimal.valueOf(3, 2), BigDecimal.valueOf(12, 2)));
+    product.setInterestRange(new InterestRange(BigDecimal.valueOf(3_00, 2), BigDecimal.valueOf(12_00, 2)));
     product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
 
     product.setCurrencyCode("XXX");
@@ -118,6 +118,7 @@
     final Set<AccountAssignment> accountAssignments = new HashSet<>();
     ret.setAccountAssignments(accountAssignments);
     ret.setCurrentState(Case.State.CREATED.name());
+    ret.setInterest(BigDecimal.valueOf(10_0000, 4));
 
     final CaseParameters caseParameters = getTestCaseParameters();
     final Gson gson = new Gson();
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCases.java b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
index c7f730e..b6afb42 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCases.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
@@ -63,6 +63,13 @@
     }
   }
 
+  @Test(expected = IllegalArgumentException.class)
+  public void shouldFailToCreateCaseWithInterestOutOfRange() throws InterruptedException {
+    final Product product = createAndEnableProduct();
+
+    createAdjustedCase(product.getIdentifier(), x -> x.setInterest(BigDecimal.valueOf(13_0000, 4)));
+  }
+
   @Test
   public void shouldCreateCase() throws InterruptedException {
     final Product product = createAndEnableProduct();
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java b/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
index ab8b20b..11b422f 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
@@ -17,6 +17,8 @@
 
 import io.mifos.core.lang.ServiceException;
 import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.InterestRange;
+import io.mifos.portfolio.api.v1.domain.Product;
 import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
 import io.mifos.portfolio.service.internal.repository.ProductRepository;
@@ -26,6 +28,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
 import java.util.Optional;
 
 /**
@@ -57,10 +60,19 @@
     if (!productEnabled.orElseThrow(() -> ServiceException.internalError("Product should exist, but doesn't"))) {
       throw ServiceException.badRequest("Product must be enabled before cases for it can be created: " + productIdentifier);}
 
-    getPatternFactory(productIdentifier).checkParameters(instance.getParameters());
+    checkForChange(productIdentifier, instance);
   }
 
   public void checkForChange(final String productIdentifier, final Case instance) {
+    final Product product = productService.findByIdentifier(productIdentifier)
+        .orElseThrow(() -> ServiceException.badRequest("Product must exist ''{0}''.", productIdentifier));
+    final InterestRange interestRange = product.getInterestRange();
+
+    final BigDecimal interest = instance.getInterest();
+    if (interest.compareTo(interestRange.getMinimum()) < 0 ||
+        interest.compareTo(interestRange.getMaximum()) > 0)
+      throw ServiceException.badRequest("Interest for the case ({0}) must be within the range defined by the product ({1}).", interest, interestRange.toString());
+
     getPatternFactory(productIdentifier).checkParameters(instance.getParameters());
   }
 
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
index e6ece90..2ffcbe8 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
@@ -37,6 +37,7 @@
 
     ret.setIdentifier(instance.getIdentifier());
     ret.setProductIdentifier(instance.getProductIdentifier());
+    ret.setInterest(instance.getInterest());
     ret.setParameters(parameters);
     ret.setAccountAssignments(instance.getAccountAssignments().stream().map(CaseMapper::mapAccountAssignmentEntity).collect(Collectors.toSet()));
     ret.setCurrentState(instance.getCurrentState());
@@ -62,6 +63,7 @@
 
     ret.setIdentifier(instance.getIdentifier());
     ret.setProductIdentifier(instance.getProductIdentifier());
+    ret.setInterest(instance.getInterest());
     ret.setAccountAssignments(instance.getAccountAssignments().stream()
         .map(x -> CaseMapper.map(x, ret))
         .collect(Collectors.toSet()));
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
index 83e023f..39a4ee7 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
@@ -19,6 +19,7 @@
 
 import javax.annotation.Nullable;
 import javax.persistence.*;
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.Objects;
 import java.util.Set;
@@ -41,6 +42,9 @@
   @Column(name = "product_identifier", nullable = false)
   private String productIdentifier;
 
+  @Column(name = "interest")
+  private BigDecimal interest;
+
   @OneToMany(targetEntity = CaseAccountAssignmentEntity.class, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "caseEntity")
   private Set<CaseAccountAssignmentEntity> accountAssignments;
 
@@ -95,6 +99,14 @@
     this.productIdentifier = productIdentifier;
   }
 
+  public BigDecimal getInterest() {
+    return interest;
+  }
+
+  public void setInterest(BigDecimal interest) {
+    this.interest = interest;
+  }
+
   public Set<CaseAccountAssignmentEntity> getAccountAssignments() {
     return accountAssignments;
   }
diff --git a/service/src/main/resources/db/migrations/mariadb/V6__interest_and_charges.sql b/service/src/main/resources/db/migrations/mariadb/V6__interest_and_charges.sql
new file mode 100644
index 0000000..a10b21c
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V6__interest_and_charges.sql
@@ -0,0 +1,17 @@
+--
+-- Copyright 2017 Kuelap, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--    http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+ALTER TABLE bastet_cases ADD COLUMN interest DECIMAL(7,4) NULL DEFAULT NULL;
\ No newline at end of file