Merge pull request #2 from myrlen/develop
Checking for invalid customer. Changing input 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/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
index 2268871..b7170d0 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
@@ -39,7 +39,8 @@
@SuppressWarnings("WeakerAccess")
public enum ChargeMethod {
FIXED,
- PROPORTIONAL
+ PROPORTIONAL,
+ INTEREST
}
@ValidIdentifier
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java
new file mode 100644
index 0000000..d119bd7
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.domain;
+
+import org.hibernate.validator.constraints.ScriptAssert;
+
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import java.math.BigDecimal;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@ScriptAssert(lang = "javascript", script = "_this.maximum != null && _this.minimum != null && _this.maximum.compareTo(_this.minimum) >= 0 && _this.minimum.scale() <= 2 && _this.maximum.scale() <= 2")
+public class InterestRange {
+ @DecimalMin(value = "0.00")
+ @DecimalMax(value = "999.99")
+ private BigDecimal minimum;
+ @DecimalMin(value = "0.00")
+ @DecimalMax(value = "999.99")
+ private BigDecimal maximum;
+
+ public InterestRange() {
+ }
+
+ public InterestRange(BigDecimal minimum, BigDecimal maximum) {
+ this.minimum = minimum;
+ this.maximum = maximum;
+ }
+
+ public BigDecimal getMinimum() {
+ return minimum;
+ }
+
+ public void setMinimum(BigDecimal minimum) {
+ this.minimum = minimum;
+ }
+
+ public BigDecimal getMaximum() {
+ return maximum;
+ }
+
+ public void setMaximum(BigDecimal maximum) {
+ this.maximum = maximum;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InterestRange that = (InterestRange) o;
+
+ return minimum != null ? minimum.equals(that.minimum) : that.minimum == null && (maximum != null ? maximum.equals(that.maximum) : that.maximum == null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = minimum != null ? minimum.hashCode() : 0;
+ result = 31 * result + (maximum != null ? maximum.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "InterestRange{" +
+ "minimum=" + minimum +
+ ", maximum=" + maximum +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Product.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Product.java
index 6f34f9b..0e7274d 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Product.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Product.java
@@ -43,6 +43,9 @@
@Valid
private BalanceRange balanceRange;
@NotNull
+ @Valid
+ private InterestRange interestRange;
+ @NotNull
private InterestBasis interestBasis;
@NotNull
@ValidIdentifier(maxLength = 512)
@@ -101,6 +104,14 @@
this.balanceRange = balanceRange;
}
+ public InterestRange getInterestRange() {
+ return interestRange;
+ }
+
+ public void setInterestRange(InterestRange interestRange) {
+ this.interestRange = interestRange;
+ }
+
public InterestBasis getInterestBasis() {
return interestBasis;
}
@@ -199,6 +210,7 @@
Objects.equals(name, product.name) &&
Objects.equals(termRange, product.termRange) &&
Objects.equals(balanceRange, product.balanceRange) &&
+ Objects.equals(interestRange, product.interestRange) &&
interestBasis == product.interestBasis &&
Objects.equals(patternPackage, product.patternPackage) &&
Objects.equals(description, product.description) &&
@@ -209,7 +221,7 @@
@Override
public int hashCode() {
- return Objects.hash(identifier, name, termRange, balanceRange, interestBasis, patternPackage, description, currencyCode, minorCurrencyUnitDigits, accountAssignments, parameters);
+ return Objects.hash(identifier, name, termRange, balanceRange, interestRange, interestBasis, patternPackage, description, currencyCode, minorCurrencyUnitDigits, accountAssignments, parameters);
}
@Override
@@ -219,6 +231,7 @@
", name='" + name + '\'' +
", termRange=" + termRange +
", balanceRange=" + balanceRange +
+ ", interestRange=" + interestRange +
", interestBasis=" + interestBasis +
", patternPackage='" + patternPackage + '\'' +
", description='" + description + '\'' +
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index f45d457..5643ecb 100644
--- a/api/src/test/java/io/mifos/Fixture.java
+++ b/api/src/test/java/io/mifos/Fixture.java
@@ -55,6 +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_00, 2), BigDecimal.valueOf(12_00, 2)));
product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
product.setCurrencyCode("XXX");
@@ -114,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/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java
new file mode 100644
index 0000000..0f4a851
--- /dev/null
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.domain;
+
+import io.mifos.core.test.domain.ValidationTest;
+import io.mifos.core.test.domain.ValidationTestCase;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class InterestRangeTest extends ValidationTest<InterestRange> {
+ public InterestRangeTest(ValidationTestCase<InterestRange> testCase) {
+ super(testCase);
+ }
+
+ @Override
+ protected InterestRange createValidTestSubject() {
+ return new InterestRange(BigDecimal.valueOf(0.02d).setScale(2, BigDecimal.ROUND_UNNECESSARY),
+ BigDecimal.valueOf(0.03d).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ValidationTestCase> ret = new ArrayList<>();
+ ret.add(new ValidationTestCase<InterestRange>("basicCase")
+ .adjustment(x -> {})
+ .valid(true));
+ ret.add(new ValidationTestCase<InterestRange>("5 and 10")
+ .adjustment(x -> {
+ x.setMinimum(BigDecimal.valueOf(5L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ x.setMaximum(BigDecimal.valueOf(10L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ })
+ .valid(true));
+ ret.add(new ValidationTestCase<InterestRange>("5 and 5")
+ .adjustment(x -> {
+ x.setMinimum(BigDecimal.valueOf(5L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ x.setMaximum(BigDecimal.valueOf(5L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ })
+ .valid(true));
+ ret.add(new ValidationTestCase<InterestRange>("maxNull")
+ .adjustment((x) -> x.setMaximum(null))
+ .valid(false));
+ ret.add(new ValidationTestCase<InterestRange>("maximim smaller than minimum")
+ .adjustment(x -> {
+ x.setMinimum(BigDecimal.valueOf(10L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ x.setMaximum(BigDecimal.valueOf(5L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
+ })
+ .valid(false));
+ ret.add(new ValidationTestCase<InterestRange>("too large scale")
+ .adjustment(x ->
+ x.setMinimum(x.getMinimum().setScale(3, BigDecimal.ROUND_UNNECESSARY)))
+ .valid(false));
+ ret.add(new ValidationTestCase<InterestRange>("smaller scale")
+ .adjustment(x ->
+ x.setMinimum(x.getMinimum().setScale(1, BigDecimal.ROUND_HALF_EVEN)))
+ .valid(true));
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ProductTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ProductTest.java
index 9ab2448..3ab8947 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ProductTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ProductTest.java
@@ -80,6 +80,22 @@
ret.add(new ValidationTestCase<Product>("switchedBalanceRangeMinMax")
.adjustment(product -> product.setBalanceRange(new BalanceRange(Fixture.fixScale(BigDecimal.TEN), Fixture.fixScale(BigDecimal.ZERO))))
.valid(false));
+ ret.add(new ValidationTestCase<Product>("nullInterestRange")
+ .adjustment(product -> product.setInterestRange(null))
+ .valid(false));
+ //noinspection BigDecimalMethodWithoutRoundingCalled
+ ret.add(new ValidationTestCase<Product>("switchedInterestRangeMinMax")
+ .adjustment(product -> product.setInterestRange(new InterestRange(BigDecimal.valueOf(200, 2), BigDecimal.valueOf(0.9).setScale(2))))
+ .valid(false));
+ ret.add(new ValidationTestCase<Product>("tooBigMaximumInterestRange")
+ .adjustment(product -> product.setInterestRange(new InterestRange(new BigDecimal("999.99"), new BigDecimal("1000.00"))))
+ .valid(false));
+ ret.add(new ValidationTestCase<Product>("negativeMinimumInterestRange")
+ .adjustment(product -> product.setInterestRange(new InterestRange(BigDecimal.valueOf(-1, 2), BigDecimal.valueOf(1, 2))))
+ .valid(false));
+ ret.add(new ValidationTestCase<Product>("tooManyDigitsAfterTheDecimalInterestRange")
+ .adjustment(product -> product.setInterestRange(new InterestRange(BigDecimal.valueOf(1, 2), new BigDecimal("1.001"))))
+ .valid(false));
ret.add(new ValidationTestCase<Product>("nullInterestBasis")
.adjustment(product -> product.setInterestBasis(null))
.valid(false));
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index adadf68..4c7dfcd 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -21,6 +21,7 @@
import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
import io.mifos.core.test.listener.EnableEventRecording;
import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.api.v1.client.IndividualLending;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
@@ -32,6 +33,7 @@
import io.mifos.portfolio.service.internal.util.RhythmAdapter;
import org.junit.*;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -114,6 +116,9 @@
@MockBean
LedgerManager ledgerManager;
+ @MockBean
+ CustomerManager customerManager;
+
@SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@Autowired
@Qualifier(LOGGER_NAME)
@@ -123,6 +128,7 @@
public void prepTest() {
userContext = this.tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
AccountingFixture.mockAccountingPrereqs(ledgerManager);
+ Mockito.doReturn(true).when(customerManager).isCustomerInGoodStanding(Fixture.CUSTOMER_IDENTIFIER);
}
@After
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 5b3785e..f533a12 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -41,6 +41,7 @@
static final int MINOR_CURRENCY_UNIT_DIGITS = 2;
static final BigDecimal INTEREST_RATE = BigDecimal.valueOf(0.10).setScale(4, RoundingMode.HALF_EVEN);
static final BigDecimal ACCRUAL_PERIODS = BigDecimal.valueOf(365.2425);
+ public static final String CUSTOMER_IDENTIFIER = "alice";
private static int uniquenessSuffix = 0;
@@ -52,6 +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_00, 2), BigDecimal.valueOf(12_00, 2)));
product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
product.setCurrencyCode("XXX");
@@ -116,6 +118,7 @@
final Set<AccountAssignment> accountAssignments = new HashSet<>();
ret.setAccountAssignments(accountAssignments);
ret.setCurrentState(Case.State.CREATED.name());
+ ret.setInterest(BigDecimal.valueOf(10_00, 2));
final CaseParameters caseParameters = getTestCaseParameters();
final Gson gson = new Gson();
@@ -134,7 +137,7 @@
{
final CaseParameters ret = new CaseParameters(generateUniqueIdentifer("fred"));
- ret.setCustomerIdentifier("alice");
+ ret.setCustomerIdentifier(CUSTOMER_IDENTIFIER);
ret.setMaximumBalance(fixScale(BigDecimal.valueOf(2000L)));
ret.setTermRange(new TermRange(ChronoUnit.MONTHS, 18));
ret.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 1, null, null));
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index cc2c74c..4ab454a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -27,8 +27,10 @@
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
-import io.mifos.portfolio.api.v1.domain.*;
-import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
+import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.domain.TaskDefinition;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.rhythm.spi.v1.client.BeatListener;
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
@@ -126,13 +128,6 @@
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT);
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID, DISBURSEMENT_FEE_AMOUNT);
- final ChargeDefinition interestChargeDefinition = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.INTEREST_ID);
- interestChargeDefinition.setAmount(Fixture.INTEREST_RATE);
-
- portfolioManager.changeChargeDefinition(product.getIdentifier(), interestChargeDefinition.getIdentifier(), interestChargeDefinition);
- Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
- new ChargeDefinitionEvent(product.getIdentifier(), interestChargeDefinition.getIdentifier())));
-
taskDefinition = createTaskDefinition(product);
portfolioManager.enableProduct(product.getIdentifier(), true);
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 7493e81..b6afb42 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCases.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
@@ -29,6 +29,7 @@
import io.mifos.portfolio.api.v1.events.EventConstants;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Mockito;
import java.math.BigDecimal;
import java.util.*;
@@ -62,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();
@@ -199,6 +207,23 @@
}
@Test
+ public void shouldThrowWhenCustomerNotInGoodStanding() throws InterruptedException {
+ Mockito.doReturn(false).when(customerManager).isCustomerInGoodStanding("don");
+
+ final Product product = createProduct();
+
+ final CaseParameters newCaseParameters = Fixture.createAdjustedCaseParameters(x -> x.setCustomerIdentifier("don"));
+ final String originalParameters = new Gson().toJson(newCaseParameters);
+
+ try {
+ createAdjustedCase(product.getIdentifier(), x -> x.setParameters(originalParameters));
+ Assert.fail("This should cause an illegal argument exception because Don is not a customer in good standing.");
+ }
+ catch (final IllegalArgumentException ignored){
+ }
+ }
+
+ @Test
public void shouldThrowWhenProductNotActivated() throws InterruptedException {
final Product product = createProduct();
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
index 6673c26..685c183 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -62,11 +62,11 @@
ChargeIdentifiers.DISBURSE_PAYMENT_ID,
ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID,
ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID,
+ ChargeIdentifiers.INTEREST_ID,
ChargeIdentifiers.REPAYMENT_ID)
.collect(Collectors.toSet());
final Set<String> expectedChangeableChargeDefinitionIdentifiers = Stream.of(
ChargeIdentifiers.DISBURSEMENT_FEE_ID,
- ChargeIdentifiers.INTEREST_ID,
ChargeIdentifiers.LATE_FEE_ID,
ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID,
ChargeIdentifiers.PROCESSING_FEE_ID)
@@ -154,24 +154,24 @@
@Test
- public void shouldChangeInterestChargeDefinition() throws InterruptedException {
+ public void shouldChangeDisbursementFeeChargeDefinition() throws InterruptedException {
final Product product = createProduct();
- final ChargeDefinition interestChargeDefinition
- = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.INTEREST_ID);
- interestChargeDefinition.setAmount(Fixture.INTEREST_RATE);
+ final ChargeDefinition disbursementFeeDefinition
+ = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID);
+ disbursementFeeDefinition.setAmount(BigDecimal.valueOf(10_0000, 4));
portfolioManager.changeChargeDefinition(
product.getIdentifier(),
- interestChargeDefinition.getIdentifier(),
- interestChargeDefinition);
+ disbursementFeeDefinition.getIdentifier(),
+ disbursementFeeDefinition);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
- new ChargeDefinitionEvent(product.getIdentifier(), interestChargeDefinition.getIdentifier())));
+ new ChargeDefinitionEvent(product.getIdentifier(), disbursementFeeDefinition.getIdentifier())));
final ChargeDefinition chargeDefinitionAsChanged
- = portfolioManager.getChargeDefinition(product.getIdentifier(), interestChargeDefinition.getIdentifier());
+ = portfolioManager.getChargeDefinition(product.getIdentifier(), disbursementFeeDefinition.getIdentifier());
- Assert.assertEquals(interestChargeDefinition, chargeDefinitionAsChanged);
+ Assert.assertEquals(disbursementFeeDefinition, chargeDefinitionAsChanged);
}
@Test
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
index 62e6f39..0ef6c65 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
@@ -26,6 +26,7 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Mockito;
import java.util.HashSet;
import java.util.Set;
@@ -37,6 +38,9 @@
@Test
public void shouldReturnIndividualLoansCases() throws InterruptedException {
+ Mockito.doReturn(true).when(customerManager).isCustomerInGoodStanding("susi");
+ Mockito.doReturn(true).when(customerManager).isCustomerInGoodStanding("george");
+ Mockito.doReturn(true).when(customerManager).isCustomerInGoodStanding("harold");
final Product product = createAndEnableProduct();
final Set<String> susisCaseIdentifiers = new HashSet<>();
final Set<String> georgeCaseIdentifiers = new HashSet<>();
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
index f370f90..876d07a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
@@ -332,6 +332,7 @@
product.setDescription(StringUtils.repeat("x", 4096));
product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
product.setBalanceRange(new BalanceRange(BigDecimal.ZERO.setScale(4, BigDecimal.ROUND_UNNECESSARY), new BigDecimal(10000).setScale(4, BigDecimal.ROUND_UNNECESSARY)));
+ product.setInterestRange(new InterestRange(new BigDecimal("999.98"), new BigDecimal("999.99")));
product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
product.setCurrencyCode("XTS");
product.setMinorCurrencyUnitDigits(4);
diff --git a/service/build.gradle b/service/build.gradle
index f4f79d6..8194a22 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -36,6 +36,7 @@
[group: 'io.mifos.rhythm', name: 'api', version: versions.mifosrhythm],
[group: 'io.mifos.accounting', name: 'api', version: versions.frameworkaccounting],
[group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+ [group: 'io.mifos.customer', name: 'api', version: versions.mifoscustomer],
[group: 'com.google.code.gson', name: 'gson'],
[group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
[group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 07c8d76..9488c60 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -18,6 +18,7 @@
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import io.mifos.core.lang.ServiceException;
+import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
@@ -29,6 +30,7 @@
import io.mifos.individuallending.internal.repository.CreditWorthinessFactorType;
import io.mifos.individuallending.internal.service.CostComponentService;
import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DataContextService;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
@@ -57,19 +59,25 @@
public class IndividualLendingPatternFactory implements PatternFactory {
final static private String INDIVIDUAL_LENDING_PACKAGE = "io.mifos.individuallending.api.v1";
private final CaseParametersRepository caseParametersRepository;
+ private final DataContextService dataContextService;
private final CostComponentService costComponentService;
+ private final CustomerManager customerManager;
private final IndividualLendingCommandDispatcher individualLendingCommandDispatcher;
private final Gson gson;
@Autowired
IndividualLendingPatternFactory(
- final CaseParametersRepository caseParametersRepository,
- final CostComponentService costComponentService,
- final IndividualLendingCommandDispatcher individualLendingCommandDispatcher,
- @Qualifier(ServiceConstants.GSON_NAME) final Gson gson)
+ final CaseParametersRepository caseParametersRepository,
+ final DataContextService dataContextService,
+ final CostComponentService costComponentService,
+ final CustomerManager customerManager,
+ final IndividualLendingCommandDispatcher individualLendingCommandDispatcher,
+ @Qualifier(ServiceConstants.GSON_NAME) final Gson gson)
{
this.caseParametersRepository = caseParametersRepository;
+ this.dataContextService = dataContextService;
this.costComponentService = costComponentService;
+ this.customerManager = customerManager;
this.individualLendingCommandDispatcher = individualLendingCommandDispatcher;
this.gson = gson;
}
@@ -182,14 +190,15 @@
final ChargeDefinition interestCharge = charge(
INTEREST_NAME,
Action.ACCEPT_PAYMENT,
- BigDecimal.valueOf(0.05),
+ BigDecimal.ONE,
CUSTOMER_LOAN,
INTEREST_INCOME);
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
- interestCharge.setReadOnly(false);
+ interestCharge.setChargeMethod(ChargeDefinition.ChargeMethod.INTEREST);
+ interestCharge.setReadOnly(true);
final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
@@ -240,9 +249,18 @@
return ret;
}
+ @Override
+ public void checkParameters(final String parameters) {
+ final CaseParameters caseParameters = gson.fromJson(parameters, CaseParameters.class);
+ final String customerIdentifier = caseParameters.getCustomerIdentifier();
+ if (!customerManager.isCustomerInGoodStanding(customerIdentifier))
+ throw ServiceException.badRequest("Customer ''{0}'' is either not a customer or is not in good standing.");
+ }
+
@Transactional
@Override
public void persistParameters(final Long caseId, final String parameters) {
+ checkParameters(parameters);
final CaseParameters caseParameters = gson.fromJson(parameters, CaseParameters.class);
final CaseParametersEntity caseParametersEntity = CaseParametersMapper.map(caseId, caseParameters);
caseParametersRepository.save(caseParametersEntity);
@@ -277,7 +295,8 @@
@Transactional
@Override
- public void changeParameters(Long caseId, String parameters) {
+ public void changeParameters(final Long caseId, final String parameters) {
+ checkParameters(parameters);
final CaseParameters caseParameters = gson.fromJson(parameters, CaseParameters.class);
final CaseParametersEntity oldCaseParameters = caseParametersRepository.findByCaseId(caseId)
.orElseThrow(() -> new IllegalArgumentException("Case id does not represent an individual loan: " + caseId));
@@ -331,7 +350,7 @@
final Set<String> forAccountDesignators,
final BigDecimal forPaymentSize) {
final Action action = Action.valueOf(actionIdentifier);
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState());
checkActionCanBeExecuted(caseState, action);
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index 36bbe16..00740d5 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -61,6 +61,7 @@
@Aggregate
public class IndividualLoanCommandHandler {
private final CaseRepository caseRepository;
+ private final DataContextService dataContextService;
private final CostComponentService costComponentService;
private final AccountingAdapter accountingAdapter;
private final TaskInstanceRepository taskInstanceRepository;
@@ -68,10 +69,12 @@
@Autowired
public IndividualLoanCommandHandler(
final CaseRepository caseRepository,
+ final DataContextService dataContextService,
final CostComponentService costComponentService,
final AccountingAdapter accountingAdapter,
final TaskInstanceRepository taskInstanceRepository) {
this.caseRepository = caseRepository;
+ this.dataContextService = dataContextService;
this.costComponentService = costComponentService;
this.accountingAdapter = accountingAdapter;
this.taskInstanceRepository = taskInstanceRepository;
@@ -85,7 +88,7 @@
public IndividualLoanCommandEvent process(final OpenCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.OPEN);
@@ -126,7 +129,7 @@
public IndividualLoanCommandEvent process(final DenyCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DENY);
@@ -162,7 +165,7 @@
public IndividualLoanCommandEvent process(final ApproveCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
@@ -213,7 +216,7 @@
public IndividualLoanCommandEvent process(final DisburseCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DISBURSE);
@@ -262,7 +265,7 @@
public IndividualLoanCommandEvent process(final ApplyInterestCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, null);
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPLY_INTEREST);
@@ -301,7 +304,7 @@
public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
@@ -346,7 +349,7 @@
public IndividualLoanCommandEvent process(final WriteOffCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
@@ -364,7 +367,7 @@
public IndividualLoanCommandEvent process(final CloseCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
@@ -403,7 +406,7 @@
public IndividualLoanCommandEvent process(final RecoverCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index fb76cbc..67725d2 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -20,15 +20,8 @@
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
-import io.mifos.individuallending.internal.repository.CaseParametersRepository;
-import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.service.internal.repository.CaseEntity;
-import io.mifos.portfolio.service.internal.repository.CaseRepository;
-import io.mifos.portfolio.service.internal.repository.ProductEntity;
-import io.mifos.portfolio.service.internal.repository.ProductRepository;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import org.javamoney.calc.common.Rate;
import org.javamoney.moneta.Money;
@@ -54,48 +47,17 @@
private static final int EXTRA_PRECISION = 4;
private static final int RUNNING_CALCULATION_PRECISION = 8;
- private final ProductRepository productRepository;
- private final CaseRepository caseRepository;
- private final CaseParametersRepository caseParametersRepository;
private final IndividualLoanService individualLoanService;
private final AccountingAdapter accountingAdapter;
@Autowired
public CostComponentService(
- final ProductRepository productRepository,
- final CaseRepository caseRepository,
- final CaseParametersRepository caseParametersRepository,
final IndividualLoanService individualLoanService,
final AccountingAdapter accountingAdapter) {
- this.productRepository = productRepository;
- this.caseRepository = caseRepository;
- this.caseParametersRepository = caseParametersRepository;
this.individualLoanService = individualLoanService;
this.accountingAdapter = accountingAdapter;
}
- public DataContextOfAction checkedGetDataContext(
- final String productIdentifier,
- final String caseIdentifier,
- final @Nullable List<AccountAssignment> oneTimeAccountAssignments) {
-
- final ProductEntity product =
- productRepository.findByIdentifier(productIdentifier)
- .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
- final CaseEntity customerCase =
- caseRepository.findByProductIdentifierAndIdentifier(productIdentifier, caseIdentifier)
- .orElseThrow(() -> ServiceException.notFound("Case not found ''{0}.{1}''.", productIdentifier, caseIdentifier));
-
- final CaseParameters caseParameters =
- caseParametersRepository.findByCaseId(customerCase.getId())
- .map(x -> CaseParametersMapper.mapEntity(x, product.getMinorCurrencyUnitDigits()))
- .orElseThrow(() -> ServiceException.notFound(
- "Individual loan not found ''{0}.{1}''.",
- productIdentifier, caseIdentifier));
-
- return new DataContextOfAction(product, customerCase, caseParameters, oneTimeAccountAssignments);
- }
-
public CostComponentsForRepaymentPeriod getCostComponentsForAction(
final Action action,
final DataContextOfAction dataContextOfAction,
@@ -140,6 +102,7 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -158,6 +121,7 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -177,6 +141,7 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -233,6 +198,7 @@
caseParameters.getMaximumBalance(),
currentBalance,
disbursalSize,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -272,6 +238,7 @@
caseParameters.getMaximumBalance(),
currentBalance,
BigDecimal.ZERO,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -307,7 +274,11 @@
final List<ScheduledCharge> hypotheticalScheduledCharges = individualLoanService.getScheduledCharges(
productIdentifier,
hypotheticalScheduledActions);
- loanPaymentSize = getLoanPaymentSize(currentBalance, minorCurrencyUnitDigits, hypotheticalScheduledCharges);
+ loanPaymentSize = getLoanPaymentSize(
+ currentBalance,
+ dataContextOfAction.getInterestAsFraction(),
+ minorCurrencyUnitDigits,
+ hypotheticalScheduledCharges);
}
final List<ScheduledCharge> scheduledChargesForThisAction = individualLoanService.getScheduledCharges(
@@ -331,6 +302,7 @@
caseParameters.getMaximumBalance(),
currentBalance,
loanPaymentSize,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -414,6 +386,7 @@
caseParameters.getMaximumBalance(),
currentBalance,
BigDecimal.ZERO,
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
true);
}
@@ -436,6 +409,7 @@
final BigDecimal maximumBalance,
final BigDecimal runningBalance,
final BigDecimal entryAccountAdjustment, //disbursement or payment size.
+ final BigDecimal interest,
final int minorCurrencyUnitDigits,
final boolean accrualAccounting) {
final Map<String, BigDecimal> balanceAdjustments = new HashMap<>();
@@ -470,7 +444,7 @@
final CostComponent costComponent = costComponentMap
.computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
- final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge)
+ final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge, interest)
.apply(amountProportionalTo)
.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
adjustBalances(
@@ -526,34 +500,42 @@
}
private static Optional<ChargeProportionalDesignator> proportionalToDesignator(final ScheduledCharge scheduledCharge) {
- if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL))
+ if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL) &&
+ !scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.INTEREST))
return Optional.of(ChargeProportionalDesignator.NOT_PROPORTIONAL);
return ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
}
private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
- final ScheduledCharge scheduledCharge)
+ final ScheduledCharge scheduledCharge, final BigDecimal interest)
{
switch (scheduledCharge.getChargeDefinition().getChargeMethod())
{
- case FIXED:
+ case FIXED: {
return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount();
- case PROPORTIONAL:
- return (amountProportionalTo) ->
- PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
- .multiply(amountProportionalTo);
- default:
+ }
+ case PROPORTIONAL: {
+ final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, scheduledCharge.getChargeDefinition().getAmount(), RUNNING_CALCULATION_PRECISION);
+ return chargeAmountPerPeriod::multiply;
+ }
+ case INTEREST: {
+ final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, interest, RUNNING_CALCULATION_PRECISION);
+ return chargeAmountPerPeriod::multiply;
+ }
+ default: {
return (amountProportionalTo) -> BigDecimal.ZERO;
+ }
}
}
static BigDecimal getLoanPaymentSize(final BigDecimal startingBalance,
+ final BigDecimal interest,
final int minorCurrencyUnitDigits,
final List<ScheduledCharge> scheduledCharges) {
final int precision = startingBalance.precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
final Map<Period, BigDecimal> accrualRatesByPeriod
- = PeriodChargeCalculator.getPeriodAccrualInterestRate(scheduledCharges, precision);
+ = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, precision);
final int periodCount = accrualRatesByPeriod.size();
if (periodCount == 0)
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
index f7c2485..af636fc 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
@@ -23,6 +23,7 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
@@ -34,6 +35,7 @@
private final CaseEntity customerCase;
private final CaseParameters caseParameters;
private final List<AccountAssignment> oneTimeAccountAssignments;
+ private final BigDecimal interestAsFraction;
DataContextOfAction(final @Nonnull ProductEntity product,
final @Nonnull CaseEntity customerCase,
@@ -43,6 +45,7 @@
this.customerCase = customerCase;
this.caseParameters = caseParameters;
this.oneTimeAccountAssignments = oneTimeAccountAssignments == null ? Collections.emptyList() : oneTimeAccountAssignments;
+ interestAsFraction = customerCase.getInterest().divide(BigDecimal.valueOf(100), 4, BigDecimal.ROUND_HALF_EVEN);;
}
public @Nonnull ProductEntity getProduct() {
@@ -68,4 +71,8 @@
public String getMessageForCharge(final Action action) {
return getCompoundIdentifer() + "." + action.name();
}
+
+ BigDecimal getInterestAsFraction() {
+ return interestAsFraction;
+ }
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java
new file mode 100644
index 0000000..225d0a6
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
+import io.mifos.individuallending.internal.repository.CaseParametersRepository;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import io.mifos.portfolio.service.internal.repository.ProductRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class DataContextService {
+ private final ProductRepository productRepository;
+ private final CaseRepository caseRepository;
+ private final CaseParametersRepository caseParametersRepository;
+
+ @Autowired
+ public DataContextService(
+ final ProductRepository productRepository,
+ final CaseRepository caseRepository,
+ final CaseParametersRepository caseParametersRepository) {
+ this.productRepository = productRepository;
+ this.caseRepository = caseRepository;
+ this.caseParametersRepository = caseParametersRepository;
+ }
+
+ public DataContextOfAction checkedGetDataContext(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final @Nullable List<AccountAssignment> oneTimeAccountAssignments) {
+
+ final ProductEntity product =
+ productRepository.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
+ final CaseEntity customerCase =
+ caseRepository.findByProductIdentifierAndIdentifier(productIdentifier, caseIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Case not found ''{0}.{1}''.", productIdentifier, caseIdentifier));
+
+ final CaseParameters caseParameters =
+ caseParametersRepository.findByCaseId(customerCase.getId())
+ .map(x -> CaseParametersMapper.mapEntity(x, product.getMinorCurrencyUnitDigits()))
+ .orElseThrow(() -> ServiceException.notFound(
+ "Individual loan not found ''{0}.{1}''.",
+ productIdentifier, caseIdentifier));
+
+ return new DataContextOfAction(product, customerCase, caseParameters, oneTimeAccountAssignments);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
index e7423fb..0f77033 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
@@ -15,15 +15,12 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.Product;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
-import io.mifos.portfolio.service.internal.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -40,40 +37,36 @@
*/
@Service
public class IndividualLoanService {
- private final ProductService productService;
private final ChargeDefinitionService chargeDefinitionService;
@Autowired
- public IndividualLoanService(final ProductService productService,
- final ChargeDefinitionService chargeDefinitionService) {
- this.productService = productService;
+ public IndividualLoanService(final ChargeDefinitionService chargeDefinitionService) {
this.chargeDefinitionService = chargeDefinitionService;
}
public PlannedPaymentPage getPlannedPaymentsPage(
- final String productIdentifier,
- final CaseParameters caseParameters,
- final int pageIndex,
- final int size,
- final @Nonnull LocalDate initialDisbursalDate) {
- final Product product = productService.findByIdentifier(productIdentifier)
- .orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
- final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
+ final DataContextOfAction dataContextOfAction,
+ final int pageIndex,
+ final int size,
+ final @Nonnull LocalDate initialDisbursalDate) {
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, caseParameters);
+ final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, scheduledActions);
+ final List<ScheduledCharge> scheduledCharges = getScheduledCharges(dataContextOfAction.getProduct().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
- caseParameters.getMaximumBalance(),
+ dataContextOfAction.getCaseParameters().getMaximumBalance(),
+ dataContextOfAction.getInterestAsFraction(),
minorCurrencyUnitDigits,
scheduledCharges);
final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(
- caseParameters.getMaximumBalance(),
+ dataContextOfAction.getCaseParameters().getMaximumBalance(),
minorCurrencyUnitDigits,
scheduledCharges,
- loanPaymentSize);
+ loanPaymentSize,
+ dataContextOfAction.getInterestAsFraction());
final Set<ChargeName> chargeNames = scheduledCharges.stream()
.map(IndividualLoanService::chargeNameFromChargeDefinition)
@@ -124,7 +117,8 @@
final BigDecimal initialBalance,
final int minorCurrencyUnitDigits,
final List<ScheduledCharge> scheduledCharges,
- final BigDecimal loanPaymentSize) {
+ final BigDecimal loanPaymentSize,
+ final BigDecimal interest) {
final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
= scheduledCharges.stream()
.collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
@@ -164,6 +158,7 @@
initialBalance,
balance,
currentLoanPaymentSize,
+ interest,
minorCurrencyUnitDigits,
false);
@@ -185,7 +180,7 @@
return scheduledAction.repaymentPeriod;
}
- private List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
+ static List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
return scheduledActions.stream()
@@ -198,7 +193,7 @@
.collect(Collectors.toList());
}
- private Stream<ChargeDefinition> getChargeDefinitionStream(
+ private static Stream<ChargeDefinition> getChargeDefinitionStream(
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
final ScheduledAction scheduledAction) {
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index 58cac97..39bb325 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -38,12 +38,14 @@
{
}
- static Map<Period, BigDecimal> getPeriodAccrualInterestRate(final List<ScheduledCharge> scheduledCharges,
- final int precision) {
+ static Map<Period, BigDecimal> getPeriodAccrualInterestRate(
+ final BigDecimal interest,
+ final List<ScheduledCharge> scheduledCharges,
+ final int precision) {
return scheduledCharges.stream()
.filter(PeriodChargeCalculator::accruedInterestCharge)
.collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
- Collectors.mapping(x -> chargeAmountPerPeriod(x, precision), RateCollectors.compound(precision))));
+ Collectors.mapping(x -> chargeAmountPerPeriod(x, interest, precision), RateCollectors.compound(precision))));
}
private static boolean accruedInterestCharge(final ScheduledCharge scheduledCharge)
@@ -52,15 +54,16 @@
scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
scheduledCharge.getChargeDefinition().getAccrueAction().equals(Action.APPLY_INTEREST.name()) &&
scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT &&
- scheduledCharge.getScheduledAction().actionPeriod != null;
+ scheduledCharge.getScheduledAction().actionPeriod != null &&
+ scheduledCharge.getChargeDefinition().getChargeMethod() == ChargeDefinition.ChargeMethod.INTEREST;
}
- static BigDecimal chargeAmountPerPeriod(final ScheduledCharge scheduledCharge, final int precision)
+ static BigDecimal chargeAmountPerPeriod(final ScheduledCharge scheduledCharge, final BigDecimal amount, final int precision)
{
final ChargeDefinition chargeDefinition = scheduledCharge.getChargeDefinition();
final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
if (chargeDefinition.getForCycleSizeUnit() == null)
- return chargeDefinition.getAmount();
+ return amount;
final BigDecimal actionPeriodDuration
= BigDecimal.valueOf(
@@ -84,7 +87,7 @@
final int accrualPeriodsInActionPeriod = actionPeriodDuration.divide(
accrualPeriodDuration.orElse(actionPeriodDuration), precision, BigDecimal.ROUND_HALF_EVEN)
.intValueExact();
- final BigDecimal rateForAccrualPeriod = chargeDefinition.getAmount().divide(
+ final BigDecimal rateForAccrualPeriod = amount.divide(
accrualPeriodsInCycle, precision, BigDecimal.ROUND_HALF_EVEN);
return createCompoundedRate(rateForAccrualPeriod, accrualPeriodsInActionPeriod, precision);
}
diff --git a/service/src/main/java/io/mifos/individuallending/rest/PlannedPaymentsRestController.java b/service/src/main/java/io/mifos/individuallending/rest/PlannedPaymentsRestController.java
index 3df4fab..4d3dce0 100644
--- a/service/src/main/java/io/mifos/individuallending/rest/PlannedPaymentsRestController.java
+++ b/service/src/main/java/io/mifos/individuallending/rest/PlannedPaymentsRestController.java
@@ -18,12 +18,11 @@
import io.mifos.anubis.annotation.AcceptedTokenType;
import io.mifos.anubis.annotation.Permittable;
-import io.mifos.individuallending.internal.service.CaseParametersService;
-import io.mifos.individuallending.internal.service.IndividualLoanService;
import io.mifos.core.lang.DateConverter;
-import io.mifos.core.lang.ServiceException;
-import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DataContextService;
+import io.mifos.individuallending.internal.service.IndividualLoanService;
import io.mifos.portfolio.api.v1.PermittableGroupIds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@@ -31,6 +30,7 @@
import java.time.LocalDate;
import java.time.ZoneId;
+import java.util.Collections;
/**
* @author Myrle Krantz
@@ -39,14 +39,14 @@
@RestController
@RequestMapping("/individuallending/products/{productidentifier}/cases/{caseidentifier}/plannedpayments")
public class PlannedPaymentsRestController {
- private final CaseParametersService caseParametersService;
+ private final DataContextService dataContextService;
private final IndividualLoanService individualLoanService;
@Autowired
public PlannedPaymentsRestController(
- final CaseParametersService caseParametersService,
- final IndividualLoanService individualLoanService) {
- this.caseParametersService = caseParametersService;
+ final DataContextService dataContextService,
+ final IndividualLoanService individualLoanService) {
+ this.dataContextService = dataContextService;
this.individualLoanService = individualLoanService;
}
@@ -63,11 +63,7 @@
@RequestParam(value = "size", required = false) final Integer size,
@RequestParam(value = "initialDisbursalDate", required = false) final String initialDisbursalDate)
{
- final CaseParameters caseParameters = caseParametersService
- .findByIdentifier(productIdentifier, caseIdentifier)
- .orElseThrow(() -> ServiceException.notFound(
- "Instance with identifier " + productIdentifier + "." + caseIdentifier + " doesn't exist or it is not an individual loan."));
-
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
final LocalDate parsedInitialDisbursalDate = initialDisbursalDate == null
? LocalDate.now(ZoneId.of("UTC"))
@@ -75,6 +71,6 @@
final Integer pageIndexToUse = pageIndex != null ? pageIndex : 0;
final Integer sizeToUse = size != null ? size : 20;
- return individualLoanService.getPlannedPaymentsPage(productIdentifier, caseParameters, pageIndexToUse, sizeToUse, parsedInitialDisbursalDate);
+ return individualLoanService.getPlannedPaymentsPage(dataContextOfAction, pageIndexToUse, sizeToUse, parsedInitialDisbursalDate);
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/config/PortfolioServiceConfiguration.java b/service/src/main/java/io/mifos/portfolio/service/config/PortfolioServiceConfiguration.java
index adcb3b3..9668332 100644
--- a/service/src/main/java/io/mifos/portfolio/service/config/PortfolioServiceConfiguration.java
+++ b/service/src/main/java/io/mifos/portfolio/service/config/PortfolioServiceConfiguration.java
@@ -25,6 +25,7 @@
import io.mifos.core.lang.config.EnableServiceException;
import io.mifos.core.lang.config.EnableTenantContext;
import io.mifos.core.mariadb.config.EnableMariaDB;
+import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.IndividualLendingConfiguration;
import io.mifos.portfolio.service.ServiceConstants;
import io.mifos.rhythm.api.v1.client.RhythmManager;
@@ -60,7 +61,7 @@
})
@EnableJpaRepositories(basePackages = "io.mifos.portfolio.service.internal.repository")
@EntityScan(basePackages = "io.mifos.portfolio.service.internal.repository")
-@EnableFeignClients(clients = {LedgerManager.class, RhythmManager.class})
+@EnableFeignClients(clients = {LedgerManager.class, RhythmManager.class, CustomerManager.class})
@RibbonClient(name = "portfolio-v1")
@EnableApplicationName
@Import(IndividualLendingConfiguration.class)
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
new file mode 100644
index 0000000..11b422f
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package io.mifos.portfolio.service.internal.checker;
+
+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;
+import io.mifos.portfolio.service.internal.service.CaseService;
+import io.mifos.portfolio.service.internal.service.ProductService;
+import io.mifos.products.spi.PatternFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Component
+public class CaseChecker {
+ private final CaseService caseService;
+ private final ProductService productService;
+ private final ProductRepository productRepository;
+ private final PatternFactoryRegistry patternFactoryRegistry;
+
+ @Autowired
+ public CaseChecker(final CaseService caseService,
+ final ProductService productService,
+ final ProductRepository productRepository,
+ final PatternFactoryRegistry patternFactoryRegistry) {
+ this.caseService = caseService;
+ this.productService = productService;
+ this.productRepository = productRepository;
+ this.patternFactoryRegistry = patternFactoryRegistry;
+ }
+
+ public void checkForCreate(final String productIdentifier, final Case instance) {
+ caseService.findByIdentifier(productIdentifier, instance.getIdentifier())
+ .ifPresent(x -> {throw ServiceException.conflict("Duplicate identifier: " + productIdentifier + "." + x.getIdentifier());});
+
+ final Optional<Boolean> productEnabled = productService.findEnabledByIdentifier(productIdentifier);
+ 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);}
+
+ 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());
+ }
+
+ private PatternFactory getPatternFactory(final String productIdentifier) {
+ return productRepository.findByIdentifier(productIdentifier)
+ .map(ProductEntity::getPatternPackage)
+ .flatMap(patternFactoryRegistry::getPatternFactoryForPackage)
+ .orElseThrow(() -> new IllegalArgumentException("Case references unsupported product type."));
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/CaseCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/CaseCommandHandler.java
index a8700c0..aa91b80 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/CaseCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/CaseCommandHandler.java
@@ -80,7 +80,7 @@
return new CaseEvent(caseInstance.getProductIdentifier(), caseInstance.getIdentifier());
}
- private PatternFactory getPatternFactory(String productIdentifier) {
+ private PatternFactory getPatternFactory(final String productIdentifier) {
return productRepository.findByIdentifier(productIdentifier)
.map(ProductEntity::getPatternPackage)
.flatMap(patternFactoryRegistry::getPatternFactoryForPackage)
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/mapper/ProductMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
index 40d85d8..4e470b3 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
@@ -43,6 +43,8 @@
productEntity.getTermRangeMaximum()));
product.setBalanceRange(
new BalanceRange(productEntity.getBalanceRangeMinimum().setScale(productEntity.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_HALF_EVEN), productEntity.getBalanceRangeMaximum().setScale(productEntity.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_HALF_EVEN)));
+ product.setInterestRange(
+ new InterestRange(productEntity.getInterestRangeMinimum(), productEntity.getInterestRangeMaximum()));
product.setInterestBasis(productEntity.getInterestBasis());
product.setPatternPackage(productEntity.getPatternPackage());
product.setDescription(productEntity.getDescription());
@@ -70,8 +72,8 @@
ret.setTermRangeMaximum(product.getTermRange().getMaximum());
ret.setBalanceRangeMinimum(product.getBalanceRange().getMinimum());
ret.setBalanceRangeMaximum(product.getBalanceRange().getMaximum());
- ret.setInterestRangeMinimum(BigDecimal.ZERO);
- ret.setInterestRangeMaximum(BigDecimal.valueOf(999.99));
+ ret.setInterestRangeMinimum(product.getInterestRange().getMinimum());
+ ret.setInterestRangeMaximum(product.getInterestRange().getMaximum());
ret.setInterestBasis(product.getInterestBasis());
ret.setPatternPackage(product.getPatternPackage());
ret.setDescription(product.getDescription());
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/java/io/mifos/portfolio/service/rest/CaseRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
index f2a4ed5..3117f67 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
@@ -25,6 +25,7 @@
import io.mifos.portfolio.api.v1.domain.CasePage;
import io.mifos.portfolio.api.v1.domain.Command;
import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.service.internal.checker.CaseChecker;
import io.mifos.portfolio.service.internal.command.ChangeCaseCommand;
import io.mifos.portfolio.service.internal.command.CreateCaseCommand;
import io.mifos.portfolio.service.internal.service.CaseService;
@@ -40,7 +41,6 @@
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
/**
@@ -53,17 +53,21 @@
private final CommandGateway commandGateway;
private final CaseService caseService;
+ private final CaseChecker caseChecker;
private final ProductService productService;
private final TaskInstanceService taskInstanceService;
- @Autowired public CaseRestController(
+ @Autowired
+ public CaseRestController(
final CommandGateway commandGateway,
final CaseService caseService,
+ final CaseChecker caseChecker,
final ProductService productService,
final TaskInstanceService taskInstanceService) {
super();
this.commandGateway = commandGateway;
this.caseService = caseService;
+ this.caseChecker = caseChecker;
this.productService = productService;
this.taskInstanceService = taskInstanceService;
}
@@ -94,13 +98,6 @@
{
checkThatProductExists(productIdentifier);
- caseService.findByIdentifier(productIdentifier, instance.getIdentifier())
- .ifPresent(x -> {throw ServiceException.conflict("Duplicate identifier: " + productIdentifier + "." + x.getIdentifier());});
-
- final Optional<Boolean> productEnabled = productService.findEnabledByIdentifier(productIdentifier);
- 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);}
-
if (!instance.getProductIdentifier().equals(productIdentifier))
throw ServiceException.badRequest("Product identifier in request body must match product identifier in request path.");
@@ -120,6 +117,8 @@
if (instance.getLastModifiedOn() != null)
throw ServiceException.badRequest("LastModifiedOn must 'null' be upon initial creation.");
+ caseChecker.checkForCreate(productIdentifier, instance);
+
this.commandGateway.process(new CreateCaseCommand(instance));
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
@@ -159,6 +158,8 @@
if (!caseIdentifier.equals(instance.getIdentifier()))
throw ServiceException.badRequest("Instance identifier may not be changed.");
+ caseChecker.checkForChange(productIdentifier, instance);
+
this.commandGateway.process(new ChangeCaseCommand(instance));
return new ResponseEntity<>(HttpStatus.ACCEPTED);
//TODO: Make sure case can't be changed from certain states.
@@ -245,5 +246,5 @@
throw ServiceException.notFound("Product with identifier ''{0}'' doesn''t exist.", productIdentifier);
}
- //TODO: check that case parameters are within product parameters in put and post.
+ //TODO: createCheck that case parameters are within product parameters in put and post.
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/products/spi/PatternFactory.java b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
index 5357e39..4b9f3c2 100644
--- a/service/src/main/java/io/mifos/products/spi/PatternFactory.java
+++ b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
@@ -32,6 +32,7 @@
public interface PatternFactory {
Pattern pattern();
List<ChargeDefinition> charges();
+ void checkParameters(String parameters);
void persistParameters(Long caseId, String parameters);
void changeParameters(Long caseId, String parameters);
Optional<String> getParameters(Long caseId, int minorCurrencyUnitDigits);
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..78e109a
--- /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(5,2) NULL DEFAULT NULL;
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
index c649180..7f73106 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
@@ -86,7 +86,6 @@
}
static ScheduledCharge scheduledInterestBookingCharge(
- final double amount,
final LocalDate initialDate,
final int chargeDateDelta,
final int periodBeginDelta,
@@ -99,12 +98,12 @@
new Period(chargeDate, periodLength),
getPeriod(initialDate, periodBeginDelta, periodLength));
final ChargeDefinition chargeDefinition = new ChargeDefinition();
- chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.INTEREST);
chargeDefinition.setForCycleSizeUnit(ChronoUnit.YEARS);
chargeDefinition.setIdentifier("blah");
chargeDefinition.setAccrueAction(Action.APPLY_INTEREST.name());
chargeDefinition.setChargeAction(Action.ACCEPT_PAYMENT.name());
- chargeDefinition.setAmount(BigDecimal.valueOf(amount));
+ chargeDefinition.setAmount(BigDecimal.ONE);
chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
chargeDefinition.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
chargeDefinition.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index 0cbf933..1bb13b1 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
@@ -24,8 +24,9 @@
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
-import io.mifos.portfolio.service.internal.service.ProductService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -86,6 +87,7 @@
private CaseParameters caseParameters;
private LocalDate initialDisbursementDate;
private List<ChargeDefinition> chargeDefinitions;
+ private BigDecimal interest;
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(
PROCESSING_FEE_ID,
LOAN_FUNDS_ALLOCATION_ID,
@@ -127,6 +129,11 @@
return this;
}
+ TestCase interest(final BigDecimal newVal) {
+ this.interest = newVal;
+ return this;
+ }
+
TestCase expectChargeInstancesForActionDatePair(final Action action,
final LocalDate forDate,
final List<ChargeDefinition> chargeDefinitions) {
@@ -134,6 +141,17 @@
return this;
}
+ DataContextOfAction getDataContextOfAction() {
+
+ final ProductEntity product = new ProductEntity();
+ product.setMinorCurrencyUnitDigits(minorCurrencyUnitDigits);
+ product.setIdentifier(productIdentifier);
+ final CaseEntity customerCase = new CaseEntity();
+ customerCase.setInterest(interest);
+
+ return new DataContextOfAction(product, customerCase, caseParameters, Collections.emptyList());
+ }
+
@Override
public String toString() {
return "TestCase{" +
@@ -153,7 +171,6 @@
private final TestCase testCase;
private final IndividualLoanService testSubject;
- private final Product product;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction;
@@ -169,7 +186,7 @@
final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
final List<ChargeDefinition> defaultChargesWithFeesReplaced =
- chargesWithInterestRate(0.01).stream().map(x -> {
+ charges().stream().map(x -> {
switch (x.getIdentifier()) {
case PROCESSING_FEE_ID:
return processingFeeCharge;
@@ -185,6 +202,7 @@
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitions(defaultChargesWithFeesReplaced)
+ .interest(BigDecimal.valueOf(1))
.expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
.expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
Collections.singletonList(loanOriginationFeeCharge));
@@ -199,13 +217,14 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 0, null, null));
caseParameters.setMaximumBalance(BigDecimal.valueOf(200000));
- final List<ChargeDefinition> charges = chargesWithInterestRate(0.10);
+ final List<ChargeDefinition> charges = charges();
return new TestCase("yearLoanTestCase")
.minorCurrencyUnitDigits(3)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitions(charges);
+ .chargeDefinitions(charges)
+ .interest(BigDecimal.valueOf(10));
}
private static TestCase chargeDefaultsCase()
@@ -216,24 +235,18 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 1, 0, 0));
caseParameters.setMaximumBalance(BigDecimal.valueOf(2000));
- final List<ChargeDefinition> charges = chargesWithInterestRate(0.05);
+ final List<ChargeDefinition> charges = charges();
return new TestCase("chargeDefaultsCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitions(charges);
+ .chargeDefinitions(charges)
+ .interest(BigDecimal.valueOf(5));
}
- private static List<ChargeDefinition> chargesWithInterestRate(final double interestRate) {
- final List<ChargeDefinition> defaultLoanCharges = IndividualLendingPatternFactory.defaultIndividualLoanCharges();
-
- defaultLoanCharges.forEach(x -> {
- if (x.getIdentifier().equals(ChargeIdentifiers.INTEREST_ID))
- x.setAmount(BigDecimal.valueOf(interestRate));
- });
-
- return defaultLoanCharges;
+ private static List<ChargeDefinition> charges() {
+ return IndividualLendingPatternFactory.defaultIndividualLoanCharges();
}
private static ChargeDefinition getFixedSingleChargeDefinition(
@@ -258,11 +271,7 @@
{
this.testCase = testCase;
- final ProductService productServiceMock = Mockito.mock(ProductService.class);
final ChargeDefinitionService chargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
- product = new Product();
- product.setMinorCurrencyUnitDigits(testCase.minorCurrencyUnitDigits);
- Mockito.doReturn(Optional.of(product)).when(productServiceMock).findByIdentifier(testCase.productIdentifier);
chargeDefinitionsByChargeAction = testCase.chargeDefinitions.stream()
.collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
Collectors.mapping(x -> x, Collectors.toList())));
@@ -273,21 +282,24 @@
Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
- testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock);
+ testSubject = new IndividualLoanService(chargeDefinitionServiceMock);
}
@Test
public void getPlannedPayments() throws Exception {
- final PlannedPaymentPage firstPage = testSubject.getPlannedPaymentsPage(testCase.productIdentifier,
- testCase.caseParameters,
+ final PlannedPaymentPage firstPage = testSubject.getPlannedPaymentsPage(testCase.getDataContextOfAction(),
0,
20,
testCase.initialDisbursementDate);
+ Assert.assertFalse(firstPage.getElements().size() == 0);
+
final List<PlannedPayment> allPlannedPayments =
- Stream.iterate(0, x -> x + 1).limit(firstPage.getTotalPages())
- .map(x -> testSubject.getPlannedPaymentsPage(testCase.productIdentifier,
- testCase.caseParameters, x, 20, testCase.initialDisbursementDate))
+ Stream.iterate(0, x -> x + 1).limit(firstPage.getTotalPages())
+ .map(x -> testSubject.getPlannedPaymentsPage(testCase.getDataContextOfAction(),
+ x,
+ 20,
+ testCase.initialDisbursementDate))
.flatMap(x -> x.getElements().stream())
.collect(Collectors.toList());
@@ -314,8 +326,8 @@
//All entries should have the correct scale.
allPlannedPayments.forEach(x -> {
- x.getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
- Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getRemainingPrincipal().scale());
+ x.getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
+ Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getRemainingPrincipal().scale());
final int uniqueChargeIdentifierCount = x.getCostComponents().stream()
.map(CostComponent::getChargeIdentifier)
.collect(Collectors.toSet())
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
index 6b3c2ff..0961727 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
@@ -38,6 +38,7 @@
List<ScheduledCharge> scheduledCharges;
int precision;
Map<Period, BigDecimal> expectedPeriodRates;
+ private BigDecimal interest;
private TestCase(final String description) {
this.description = description;
@@ -64,6 +65,11 @@
"description='" + description + '\'' +
'}';
}
+
+ TestCase interest(BigDecimal newVal) {
+ this.interest = newVal;
+ return this;
+ }
}
@Parameterized.Parameters
@@ -79,8 +85,8 @@
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestBookingCharge(0.01, initialDate, 0, 0, 1));
- scheduledCharges.add(scheduledInterestBookingCharge(0.01, initialDate, 1, 1, 1));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 0, 0, 1));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 1, 1, 1));
final BigDecimal dailyInterestRate = BigDecimal.valueOf(0.01)
.divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
@@ -90,6 +96,7 @@
expectedPeriodRates.put(getPeriod(initialDate, 1, 1), dailyInterestRate);
return new TestCase("simpleCase")
+ .interest(BigDecimal.valueOf(0.01))
.scheduledCharges(scheduledCharges)
.precision(20)
.expectedPeriodRates(expectedPeriodRates);
@@ -99,8 +106,8 @@
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestBookingCharge(0.10, initialDate, 2, 0, 3));
- scheduledCharges.add(scheduledInterestBookingCharge(0.10, initialDate, 4, 2, 2));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 2, 0, 3));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 4, 2, 2));
final BigDecimal dailyInterestRate = BigDecimal.valueOf(0.10)
.divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
@@ -110,6 +117,7 @@
expectedPeriodRates.put(getPeriod(initialDate, 2, 2), PeriodChargeCalculator.createCompoundedRate(dailyInterestRate, 2, 20));
return new TestCase("bitOfCompoundingCase")
+ .interest(BigDecimal.valueOf(0.10))
.scheduledCharges(scheduledCharges)
.precision(20)
.expectedPeriodRates(expectedPeriodRates);
@@ -119,14 +127,15 @@
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestBookingCharge(0.00, initialDate, 2, 0, 3));
- scheduledCharges.add(scheduledInterestBookingCharge(0.00, initialDate, 4, 2, 2));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 2, 0, 3));
+ scheduledCharges.add(scheduledInterestBookingCharge(initialDate, 4, 2, 2));
final Map<Period, BigDecimal> expectedPeriodRates = new HashMap<>();
expectedPeriodRates.put(getPeriod(initialDate, 0, 3), BigDecimal.ZERO.setScale(20, BigDecimal.ROUND_UNNECESSARY));
expectedPeriodRates.put(getPeriod(initialDate, 2, 2), BigDecimal.ZERO.setScale(20, BigDecimal.ROUND_UNNECESSARY));
return new TestCase("zeroInterestPerPeriod")
+ .interest(BigDecimal.valueOf(0.00))
.scheduledCharges(scheduledCharges)
.precision(20)
.expectedPeriodRates(expectedPeriodRates);
@@ -141,7 +150,7 @@
@Test
public void getPeriodAccrualRatesTest()
{
- final Map<Period, BigDecimal> periodRates = PeriodChargeCalculator.getPeriodAccrualInterestRate(testCase.scheduledCharges, testCase.precision);
+ final Map<Period, BigDecimal> periodRates = PeriodChargeCalculator.getPeriodAccrualInterestRate(testCase.interest, testCase.scheduledCharges, testCase.precision);
Assert.assertEquals(testCase.expectedPeriodRates, periodRates);
}
}
diff --git a/shared.gradle b/shared.gradle
index 88fd123..410d57d 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -12,6 +12,7 @@
frameworkasync : '0.1.0-BUILD-SNAPSHOT',
frameworkaccounting : '0.1.0-BUILD-SNAPSHOT',
mifosrhythm : '0.1.0-BUILD-SNAPSHOT',
+ mifoscustomer : '0.1.0-BUILD-SNAPSHOT',
validator : '5.3.0.Final',
javamoneylib : '0.9-SNAPSHOT'
]
@@ -70,5 +71,6 @@
uxf = 'XML_STYLE'
}
ext.year = Calendar.getInstance().get(Calendar.YEAR)
- ext.name = 'The Mifos Initiative'
+ ext.name = 'Kuelap, Inc'
+ skipExistingHeaders true
}