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