Merge pull request #41 from myrle-krantz/develop
multiple disbursals, multiple repayments, and close, plus straightening up in planned payments.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
index c0e0c56..9518a1b 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
@@ -46,9 +46,6 @@
String REPAYMENT_ID = "repayment";
String TRACK_RETURN_PRINCIPAL_NAME = "Track return principal";
String TRACK_RETURN_PRINCIPAL_ID = "track-return-principal";
- String MAXIMUM_BALANCE_DESIGNATOR = "{maximumbalance}";
- String RUNNING_BALANCE_DESIGNATOR = "{runningbalance}";
- String PRINCIPAL_ADJUSTMENT_DESIGNATOR = "{principaladjustment}";
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java
new file mode 100644
index 0000000..6f60a57
--- /dev/null
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.api.v1.domain.product;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public enum ChargeProportionalDesignator {
+ NOT_PROPORTIONAL("{notproportional}", 0),
+ MAXIMUM_BALANCE_DESIGNATOR("{maximumbalance}", 1),
+ RUNNING_BALANCE_DESIGNATOR("{runningbalance}", 2),
+ PRINCIPAL_ADJUSTMENT_DESIGNATOR("{principaladjustment}", 3),
+ REPAYMENT_DESIGNATOR("{repayment}", 4),
+ ;
+
+ private final String value;
+ private final int orderOfApplication;
+
+ ChargeProportionalDesignator(final String value, final int orderOfApplication) {
+ this.value = value;
+ this.orderOfApplication = orderOfApplication;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public int getOrderOfApplication() {
+ return orderOfApplication;
+ }
+
+ public static Optional<ChargeProportionalDesignator> fromString(final String value) {
+ if (value == null)
+ return Optional.of(NOT_PROPORTIONAL);
+ return Arrays.stream(ChargeProportionalDesignator.values())
+ .filter(x -> x.getValue().equals(value))
+ .findFirst();
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java
index ab16e8b..8363d88 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java
@@ -16,7 +16,7 @@
package io.mifos.portfolio.api.v1.validation;
import io.mifos.core.lang.validation.CheckIdentifier;
-import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@@ -36,8 +36,7 @@
if (value == null)
return true;
- if (value.equals(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR) ||
- value.equals(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR))
+ if (ChargeProportionalDesignator.fromString(value).isPresent())
return true;
final CheckIdentifier identifierChecker = new CheckIdentifier();
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 472fc5d..785627a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -85,9 +85,25 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5DisburseFullAmount();
+ step5Disburse(BigDecimal.valueOf(2000L));
step6CalculateInterestAccrual();
- step7PaybackFullAmount();
+ step7PaybackPartialAmount(expectedCurrentBalance);
+ step8Close();
+ }
+
+
+ @Test
+ public void workflowWithTwoNearlyEqualRepayments() throws InterruptedException {
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase();
+ step4ApproveCase();
+ step5Disburse(BigDecimal.valueOf(2000L));
+ step6CalculateInterestAccrual();
+ final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
+ step7PaybackPartialAmount(repayment1);
+ step7PaybackPartialAmount(expectedCurrentBalance);
+ step8Close();
}
//Create product and set charges to fixed fees.
@@ -193,13 +209,14 @@
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
- private void step5DisburseFullAmount() throws InterruptedException {
- logger.info("step5DisburseFullAmount");
+ private void step5Disburse(final BigDecimal amount) throws InterruptedException {
+ logger.info("step5Disburse");
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
Collections.singletonList(assignEntryToTeller()),
+ amount,
IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE,
Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
@@ -246,20 +263,20 @@
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(
- AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
+ customerLoanAccountIdentifier,
calculatedInterest.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(
- customerLoanAccountIdentifier,
+ AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
calculatedInterest.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
}
- private void step7PaybackFullAmount() throws InterruptedException {
- logger.info("step7PaybackFullAmount");
+ private void step7PaybackPartialAmount(final BigDecimal amount) throws InterruptedException {
+ logger.info("step7PaybackPartialAmount");
AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
@@ -268,24 +285,45 @@
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
Collections.singletonList(assignEntryToTeller()),
- expectedCurrentBalance,
+ amount,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
Case.State.ACTIVE); //Close has to be done explicitly.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
+ final BigDecimal principal = amount.subtract(interestAccrued);
+
final Set<Debtor> debtors = new HashSet<>();
- debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
- debtors.add(new Debtor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.subtract(interestAccrued).toPlainString()));
- debtors.add(new Debtor(customerLoanAccountIdentifier, expectedCurrentBalance.toPlainString()));
+ debtors.add(new Debtor(customerLoanAccountIdentifier, amount.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, principal.toPlainString()));
+ if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
+ debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
- creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
- creditors.add(new Creditor(AccountingFixture.LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.subtract(interestAccrued).toPlainString()));
- creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, principal.toPlainString()));
+ if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
+ creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
- expectedCurrentBalance = expectedCurrentBalance.subtract(expectedCurrentBalance);
+ expectedCurrentBalance = expectedCurrentBalance.subtract(amount);
+ interestAccrued = BigDecimal.ZERO;
+ }
+
+ private void step8Close() throws InterruptedException {
+ logger.info("step8Close");
+
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+
+ checkStateTransfer(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.CLOSE,
+ Collections.singletonList(assignEntryToTeller()),
+ IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE,
+ Case.State.CLOSED); //Close has to be done explicitly.
+
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 6a30102..ed4c8a0 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -19,6 +19,7 @@
import io.mifos.core.lang.ServiceException;
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;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
import io.mifos.individuallending.internal.repository.CaseCreditWorthinessFactorEntity;
@@ -134,7 +135,7 @@
disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
disbursePayment.setFromAccountDesignator(LOANS_PAYABLE);
disbursePayment.setToAccountDesignator(ENTRY);
- disbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ disbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
disbursePayment.setAmount(BigDecimal.ONE);
@@ -145,7 +146,7 @@
trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
- trackPrincipalDisbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
trackPrincipalDisbursePayment.setAmount(BigDecimal.ONE);
@@ -167,18 +168,18 @@
BigDecimal.valueOf(0.30),
PENDING_DISBURSAL,
ARREARS_ALLOWANCE);
- writeOffAllowanceCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
+ writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
final ChargeDefinition interestCharge = charge(
INTEREST_NAME,
Action.ACCEPT_PAYMENT,
BigDecimal.valueOf(0.05),
- INTEREST_ACCRUAL,
+ CUSTOMER_LOAN,
INTEREST_INCOME);
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
- interestCharge.setAccrualAccountDesignator(CUSTOMER_LOAN);
- interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
+ interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
+ interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
@@ -187,20 +188,20 @@
customerRepaymentCharge.setDescription(REPAYMENT_NAME);
customerRepaymentCharge.setFromAccountDesignator(CUSTOMER_LOAN);
customerRepaymentCharge.setToAccountDesignator(ENTRY);
- customerRepaymentCharge.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
customerRepaymentCharge.setAmount(BigDecimal.ONE);
- final ChargeDefinition trackPrincipalRepaymentCharge = new ChargeDefinition();
- trackPrincipalRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
- trackPrincipalRepaymentCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
- trackPrincipalRepaymentCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
- trackPrincipalRepaymentCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
- trackPrincipalRepaymentCharge.setFromAccountDesignator(LOANS_PAYABLE);
- trackPrincipalRepaymentCharge.setToAccountDesignator(LOAN_FUNDS_SOURCE);
- trackPrincipalRepaymentCharge.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
- trackPrincipalRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- trackPrincipalRepaymentCharge.setAmount(BigDecimal.ONE);
+ final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
+ trackReturnPrincipalCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ trackReturnPrincipalCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
+ trackReturnPrincipalCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
+ trackReturnPrincipalCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
+ trackReturnPrincipalCharge.setFromAccountDesignator(LOAN_FUNDS_SOURCE);
+ trackReturnPrincipalCharge.setToAccountDesignator(LOANS_PAYABLE);
+ trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ trackReturnPrincipalCharge.setAmount(BigDecimal.ONE);
final ChargeDefinition disbursementReturnCharge = charge(
RETURN_DISBURSEMENT_NAME,
@@ -208,7 +209,7 @@
BigDecimal.valueOf(1.0),
PENDING_DISBURSAL,
LOAN_FUNDS_SOURCE);
- interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR); //TODO: Balance in which account?
+ interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
ret.add(processingFee);
ret.add(loanOriginationFee);
@@ -220,7 +221,7 @@
ret.add(writeOffAllowanceCharge);
ret.add(interestCharge);
ret.add(customerRepaymentCharge);
- ret.add(trackPrincipalRepaymentCharge);
+ ret.add(trackReturnPrincipalCharge);
ret.add(disbursementReturnCharge);
return ret;
@@ -371,7 +372,7 @@
ret.setChargeAction(action.name());
ret.setAmount(defaultAmount);
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- ret.setProportionalTo(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR);
+ ret.setProportionalTo(ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue());
ret.setFromAccountDesignator(fromAccount);
ret.setToAccountDesignator(toAccount);
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 14ef0f9..36bbe16 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
@@ -22,7 +22,6 @@
import io.mifos.core.command.annotation.EventEmitter;
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.IndividualLendingPatternFactory;
-import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
@@ -52,8 +51,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -103,6 +102,8 @@
Action.OPEN,
entry,
designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
@@ -142,6 +143,8 @@
Action.DENY,
entry,
designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
@@ -187,6 +190,8 @@
Action.APPROVE,
entry,
designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
@@ -214,20 +219,21 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
+ final BigDecimal disbursalAmount = Optional.ofNullable(command.getCommand().getPaymentSize()).orElse(BigDecimal.ZERO);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForDisburse(dataContextOfAction);
+ costComponentService.getCostComponentsForDisburse(dataContextOfAction, disbursalAmount);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final BigDecimal disbursalAmount = dataContextOfAction.getCaseParameters().getMaximumBalance();
- final List<ChargeInstance> charges = Stream.concat(
- costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DISBURSE,
- entry,
- designatorToAccountIdentifierMapper)),
- Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
+ final List<ChargeInstance> charges =
+ costComponentsForRepaymentPeriod.stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.DISBURSE,
+ entry,
+ designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
@@ -275,6 +281,8 @@
Action.APPLY_INTEREST,
entry,
designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
@@ -319,6 +327,8 @@
Action.ACCEPT_PAYMENT,
entry,
designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.collect(Collectors.toList());
@@ -360,6 +370,27 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ costComponentService.getCostComponentsForClose(dataContextOfAction);
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+ final List<ChargeInstance> charges =
+ costComponentsForRepaymentPeriod.stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.DISBURSE,
+ entry,
+ designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ accountingAdapter.bookCharges(charges,
+ command.getCommand().getNote(),
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE),
+ Action.DISBURSE.getTransactionType());
+
final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
@@ -384,39 +415,34 @@
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
}
- private static ChargeInstance mapCostComponentEntryToChargeInstance(
+ private static Optional<ChargeInstance> mapCostComponentEntryToChargeInstance(
final Action action,
final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
- if (chargeDefinition.getAccrualAccountDesignator() != null) {
+ if (CostComponentService.chargeIsAccrued(chargeDefinition)) {
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
- return new ChargeInstance(
+ return Optional.of(new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- chargeAmount);
- else
- return new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+ chargeAmount));
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
+ return Optional.of(new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- chargeAmount);
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+ chargeAmount));
+ else
+ return Optional.empty();
}
- else
- return new ChargeInstance(
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
+ return Optional.of(new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
- chargeAmount);
- }
-
- private static ChargeInstance getDisbursalChargeInstance(
- final BigDecimal amount,
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
- return new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.PENDING_DISBURSAL),
- designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
- amount);
+ chargeAmount));
+ else
+ return Optional.empty();
}
private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
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 5c871ba..51898aa 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
@@ -18,7 +18,7 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
-import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+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;
@@ -35,6 +35,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.money.MonetaryAmount;
import java.math.BigDecimal;
@@ -42,7 +43,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
-import java.util.function.BiFunction;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -106,7 +107,7 @@
case DENY:
return getCostComponentsForDeny(dataContextOfAction);
case DISBURSE:
- return getCostComponentsForDisburse(dataContextOfAction);
+ return getCostComponentsForDisburse(dataContextOfAction, dataContextOfAction.getCaseParameters().getMaximumBalance());
case APPLY_INTEREST:
return getCostComponentsForApplyInterest(dataContextOfAction);
case ACCEPT_PAYMENT:
@@ -138,7 +139,8 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
@@ -155,7 +157,8 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
@@ -173,15 +176,21 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForDisburse(final DataContextOfAction dataContextOfAction) {
+ public CostComponentsForRepaymentPeriod getCostComponentsForDisburse(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nonnull BigDecimal requestedDisbursalSize) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ if (dataContextOfAction.getCaseParameters().getMaximumBalance().compareTo(
+ currentBalance.add(requestedDisbursalSize)) > 0)
+ throw ServiceException.conflict("Cannot disburse over the maximum balance.");
final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
customerLoanAccountIdentifier,
@@ -190,12 +199,15 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DISBURSE, today()));
+
+ final BigDecimal disbursalSize = requestedDisbursalSize.negate();
+
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
productIdentifier, scheduledActions);
final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.DISBURSE)));
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.DISBURSE)));
final Map<ChargeDefinition, CostComponent> accruedCostComponents =
optionalStartOfTerm.map(startOfTerm ->
@@ -214,8 +226,9 @@
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getMaximumBalance(),
currentBalance,
- caseParameters.getMaximumBalance(),//TODO: This needs to be provided by the user.
- minorCurrencyUnitDigits);
+ disbursalSize,
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(
@@ -239,7 +252,7 @@
Collections.singletonList(interestAction));
final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.APPLY_INTEREST)));
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.APPLY_INTEREST)));
final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
.stream()
@@ -253,7 +266,8 @@
caseParameters.getMaximumBalance(),
currentBalance,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
@@ -295,7 +309,7 @@
Collections.singletonList(scheduledAction));
final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.ACCEPT_PAYMENT)));
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.ACCEPT_PAYMENT)));
final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
.stream()
@@ -311,12 +325,18 @@
caseParameters.getMaximumBalance(),
currentBalance,
loanPaymentSize,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
- private static boolean isAccruedChargeForAction(final ScheduledCharge scheduledCharge, final Action action) {
- return scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
- scheduledCharge.getChargeDefinition().getChargeAction().equals(action.name());
+ private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getChargeAction().equals(action.name());
+ }
+
+ private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getAccrueAction().equals(action.name());
}
private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
@@ -359,9 +379,46 @@
private CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
return null;
}
- private CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
- return null;
+
+ public CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ if (currentBalance.compareTo(BigDecimal.ZERO) != 0)
+ throw ServiceException.conflict("Cannot close loan until the balance is zero.");
+
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+
+ final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+ final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+ final LocalDate today = today();
+ final ScheduledAction closeAction = new ScheduledAction(Action.CLOSE, today, new Period(1, today));
+
+ final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(closeAction));
+
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.CLOSE)));
+
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
+ .stream()
+ .map(ScheduledCharge::getChargeDefinition)
+ .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
+ chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
+
+ return getCostComponentsForScheduledCharges(
+ accruedCostComponents,
+ chargesSplitIntoScheduledAndAccrued.get(false),
+ caseParameters.getMaximumBalance(),
+ currentBalance,
+ BigDecimal.ZERO,
+ minorCurrencyUnitDigits,
+ true);
}
+
private CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
return null;
}
@@ -371,61 +428,87 @@
final Collection<ScheduledCharge> scheduledCharges,
final BigDecimal maximumBalance,
final BigDecimal runningBalance,
- final BigDecimal loanPaymentSize,
- final int minorCurrencyUnitDigits) {
- BigDecimal balanceAdjustment = BigDecimal.ZERO;
- BigDecimal currentRunningBalance = runningBalance;
+ final BigDecimal entryAccountAdjustment, //disbursement or payment size.
+ final int minorCurrencyUnitDigits,
+ final boolean accrualAccounting) {
+ final Map<String, BigDecimal> balanceAdjustments = new HashMap<>();
+ balanceAdjustments.put(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO);
final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
for (Map.Entry<ChargeDefinition, CostComponent> entry : accruedCostComponents.entrySet()) {
- costComponentMap.put(entry.getKey(), entry.getValue());
+ final ChargeDefinition chargeDefinition = entry.getKey();
+ final BigDecimal chargeAmount = entry.getValue().getAmount();
+ costComponentMap.put(
+ chargeDefinition,
+ entry.getValue());
- if (chargeDefinitionTouchesAccount(entry.getKey(), AccountDesignators.CUSTOMER_LOAN))
- balanceAdjustment = balanceAdjustment.add(entry.getValue().getAmount());
+ //TODO: This should adjust differently depending on accrual accounting.
+ // It can't be fixed until getAmountProportionalTo is fixed.
+ adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
+ adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
}
- final Map<Boolean, List<ScheduledCharge>> partitionedCharges = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(CostComponentService::proportionalToPrincipalAdjustment));
- for (final ScheduledCharge scheduledCharge : partitionedCharges.get(false))
- {
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
+ for (final ScheduledCharge scheduledCharge : scheduledCharges) {
+ if (accrualAccounting || !isAccrualChargeForAction(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().action)) {
+ final BigDecimal amountProportionalTo = getAmountProportionalTo(
+ scheduledCharge,
+ maximumBalance,
+ runningBalance,
+ entryAccountAdjustment,
+ balanceAdjustments);
+ //TODO: getAmountProportionalTo is programmed under the assumption of non-accrual accounting.
- final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge)
- .apply(maximumBalance, currentRunningBalance)
- .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
- if (chargeDefinitionTouchesAccount(scheduledCharge.getChargeDefinition(), AccountDesignators.CUSTOMER_LOAN))
- balanceAdjustment = balanceAdjustment.add(chargeAmount);
- costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
- currentRunningBalance = currentRunningBalance.add(chargeAmount);
- }
+ final CostComponent costComponent = costComponentMap
+ .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
- final BigDecimal principalAdjustment = loanPaymentSize.subtract(balanceAdjustment);
- for (final ScheduledCharge scheduledCharge : partitionedCharges.get(true))
- {
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
-
- final BigDecimal chargeAmount = applyPrincipalAdjustmentCharge(scheduledCharge, principalAdjustment)
- .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
- if (chargeDefinitionTouchesAccount(scheduledCharge.getChargeDefinition(), AccountDesignators.CUSTOMER_LOAN))
- balanceAdjustment = balanceAdjustment.add(chargeAmount);
- costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
- currentRunningBalance = currentRunningBalance.add(chargeAmount);
+ final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge)
+ .apply(amountProportionalTo)
+ .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ adjustBalances(
+ scheduledCharge.getScheduledAction().action,
+ scheduledCharge.getChargeDefinition(),
+ chargeAmount,
+ balanceAdjustments,
+ false); //TODO: once you've fixed getAmountProportionalTo, use the passed in variable.
+ costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+ }
}
return new CostComponentsForRepaymentPeriod(
- runningBalance,
costComponentMap,
- balanceAdjustment.negate());
+ balanceAdjustments.getOrDefault(AccountDesignators.LOANS_PAYABLE, BigDecimal.ZERO).negate());
}
- private static BigDecimal applyPrincipalAdjustmentCharge(
+ private static BigDecimal getAmountProportionalTo(
final ScheduledCharge scheduledCharge,
- final BigDecimal principalAdjustment) {
- return scheduledCharge.getChargeDefinition().getAmount().multiply(principalAdjustment);
+ final BigDecimal maximumBalance,
+ final BigDecimal runningBalance,
+ final BigDecimal loanPaymentSize,
+ final Map<String, BigDecimal> balanceAdjustments) {
+ final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo = proportionalToDesignator(scheduledCharge);
+ return optionalChargeProportionalTo.map(chargeProportionalTo -> {
+ switch (chargeProportionalTo) {
+ case NOT_PROPORTIONAL:
+ return BigDecimal.ZERO;
+ case MAXIMUM_BALANCE_DESIGNATOR:
+ return maximumBalance;
+ case RUNNING_BALANCE_DESIGNATOR:
+ return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ case REPAYMENT_DESIGNATOR:
+ return loanPaymentSize;
+ case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
+ final BigDecimal newRunningBalance
+ = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
+ return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
+ }
+ default:
+ return BigDecimal.ZERO;
+ }
+ }).orElse(BigDecimal.ZERO);
+//TODO: correctly implement charges which are proportional to other charges.
}
private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
@@ -435,64 +518,35 @@
return ret;
}
- private static boolean proportionalToPrincipalAdjustment(final ScheduledCharge scheduledCharge) {
+ private static Optional<ChargeProportionalDesignator> proportionalToDesignator(final ScheduledCharge scheduledCharge) {
if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL))
- return false;
- final String proportionalTo = scheduledCharge.getChargeDefinition().getProportionalTo();
- return proportionalTo != null && proportionalTo.equals(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ return Optional.of(ChargeProportionalDesignator.NOT_PROPORTIONAL);
+
+ return ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
}
- private static BiFunction<BigDecimal, BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
+ private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
final ScheduledCharge scheduledCharge)
{
-
switch (scheduledCharge.getChargeDefinition().getChargeMethod())
{
case FIXED:
- return (maximumBalance, runningBalance) -> scheduledCharge.getChargeDefinition().getAmount();
- case PROPORTIONAL: {
- switch (scheduledCharge.getChargeDefinition().getProportionalTo()) {
- case ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR:
- return (maximumBalance, runningBalance) ->
- PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
- .multiply(runningBalance);
- case ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR:
- return (maximumBalance, runningBalance) ->
- PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
- .multiply(maximumBalance);
- case ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR: //This is handled elsewhere.
- throw new IllegalStateException("A principal adjustment charge should not be passed to the same application function as the other charges.");
- default:
-//TODO: correctly implement charges which are proportionate to other charges.
- return (maximumBalance, runningBalance) ->
- PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
- .multiply(maximumBalance);
- }
- }
+ return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount();
+ case PROPORTIONAL:
+ return (amountProportionalTo) ->
+ PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
+ .multiply(amountProportionalTo);
default:
- return (maximumBalance, runningBalance) -> BigDecimal.ZERO;
+ return (amountProportionalTo) -> BigDecimal.ZERO;
}
}
- private static boolean chargeDefinitionTouchesCustomerVisibleAccount(final ChargeDefinition chargeDefinition)
- {
- return chargeDefinitionTouchesAccount(chargeDefinition, AccountDesignators.CUSTOMER_LOAN) ||
- chargeDefinitionTouchesAccount(chargeDefinition, AccountDesignators.ENTRY);
- }
-
- private static boolean chargeDefinitionTouchesAccount(final ChargeDefinition chargeDefinition, final String accountDesignator)
- {
- return chargeDefinition.getToAccountDesignator().equals(accountDesignator) ||
- chargeDefinition.getFromAccountDesignator().equals(accountDesignator) ||
- (chargeDefinition.getAccrualAccountDesignator() != null && chargeDefinition.getAccrualAccountDesignator().equals(accountDesignator));
- }
-
static BigDecimal getLoanPaymentSize(final BigDecimal startingBalance,
final int minorCurrencyUnitDigits,
final List<ScheduledCharge> scheduledCharges) {
final int precision = startingBalance.precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
final Map<Period, BigDecimal> accrualRatesByPeriod
- = PeriodChargeCalculator.getPeriodAccrualRates(scheduledCharges, precision);
+ = PeriodChargeCalculator.getPeriodAccrualInterestRate(scheduledCharges, precision);
final int periodCount = accrualRatesByPeriod.size();
if (periodCount == 0)
@@ -505,7 +559,46 @@
Money.of(startingBalance, "XXX"),
Rate.of(geometricMeanAccrualRate),
periodCount);
- return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact());
+ return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ }
+
+ private static void adjustBalances(
+ final Action action,
+ final ChargeDefinition chargeDefinition,
+ final BigDecimal chargeAmount,
+ final Map<String, BigDecimal> balanceAdjustments,
+ boolean accrualAccounting) {
+ if (accrualAccounting) {
+ if (chargeIsAccrued(chargeDefinition)) {
+ if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
+ adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
+ adjustBalance(chargeDefinition.getAccrualAccountDesignator(), chargeAmount, balanceAdjustments);
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ adjustBalance(chargeDefinition.getAccrualAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
+ adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
+ }
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
+ adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
+ }
+ }
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
+ adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
+ }
+ }
+
+ private static void adjustBalance(
+ final String designator,
+ final BigDecimal chargeAmount,
+ final Map<String, BigDecimal> balanceAdjustments) {
+ final BigDecimal balance = balanceAdjustments.computeIfAbsent(designator, (x) -> BigDecimal.ZERO);
+ final BigDecimal newBalance = balance.add(chargeAmount);
+ balanceAdjustments.put(designator, newBalance);
+ }
+
+ public static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
+ return chargeDefinition.getAccrualAccountDesignator() != null;
}
private static LocalDate today() {
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
index defa570..cb7938a 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
@@ -26,23 +26,16 @@
* @author Myrle Krantz
*/
public class CostComponentsForRepaymentPeriod {
- final private BigDecimal runningBalance;
final private Map<ChargeDefinition, CostComponent> costComponents;
final private BigDecimal balanceAdjustment;
CostComponentsForRepaymentPeriod(
- final BigDecimal runningBalance,
final Map<ChargeDefinition, CostComponent> costComponents,
final BigDecimal balanceAdjustment) {
- this.runningBalance = runningBalance;
this.costComponents = costComponents;
this.balanceAdjustment = balanceAdjustment;
}
- public BigDecimal getRunningBalance() {
- return runningBalance;
- }
-
Map<ChargeDefinition, CostComponent> getCostComponents() {
return costComponents;
}
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 ebdf4cf..e7423fb 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
@@ -19,6 +19,7 @@
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;
@@ -124,10 +125,14 @@
final int minorCurrencyUnitDigits,
final List<ScheduledCharge> scheduledCharges,
final BigDecimal loanPaymentSize) {
- final Map<Period, Set<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
- = scheduledCharges.stream()
- .collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
- Collectors.mapping(x -> x, Collectors.toSet())));
+ final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
+ = scheduledCharges.stream()
+ .collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
+ Collectors.mapping(x -> x,
+ Collector.of(
+ () -> new TreeSet<>(new ScheduledChargeComparator()),
+ SortedSet::add,
+ (left, right) -> { left.addAll(right); return left; }))));
final List<Period> sortedRepaymentPeriods
= orderedScheduledChargesGroupedByPeriod.keySet().stream()
@@ -136,27 +141,31 @@
BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
final List<PlannedPayment> plannedPayments = new ArrayList<>();
- for (final Period repaymentPeriod : sortedRepaymentPeriods)
+ for (int i = 0; i < sortedRepaymentPeriods.size(); i++)
{
+ final Period repaymentPeriod = sortedRepaymentPeriods.get(i);
final BigDecimal currentLoanPaymentSize;
if (repaymentPeriod.isDefined()) {
- if (balance.compareTo(loanPaymentSize) < 0)
- currentLoanPaymentSize = balance;
+ // last repayment period: Force the proposed payment to "overhang". Cost component calculation
+ // corrects last loan payment downwards but not upwards.
+ if (i == sortedRepaymentPeriods.size() - 1)
+ currentLoanPaymentSize = loanPaymentSize.add(BigDecimal.valueOf(sortedRepaymentPeriods.size()));
else
currentLoanPaymentSize = loanPaymentSize;
}
else
currentLoanPaymentSize = BigDecimal.ZERO;
- final Set<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
+ final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
CostComponentService.getCostComponentsForScheduledCharges(
Collections.emptyMap(),
scheduledChargesInPeriod,
- balance,
+ initialBalance,
balance,
currentLoanPaymentSize,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ false);
final PlannedPayment plannedPayment = new PlannedPayment();
plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
@@ -205,7 +214,42 @@
if (accrueMapping == null)
accrueMapping = Stream.empty();
+ return Stream.concat(
+ accrueMapping.sorted(IndividualLoanService::proportionalityApplicationOrder),
+ chargeMapping.sorted(IndividualLoanService::proportionalityApplicationOrder));
+ }
- return Stream.concat(accrueMapping, chargeMapping);
+ private static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
+ {
+ @Override
+ public int compare(ScheduledCharge o1, ScheduledCharge o2) {
+ int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
+ if (ret == 0)
+ ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
+ if (ret == 0)
+ ret = proportionalityApplicationOrder(o1.getChargeDefinition(), o2.getChargeDefinition());
+ if (ret == 0)
+ return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
+ else
+ return ret;
+ }
+ }
+
+ private static int proportionalityApplicationOrder(final ChargeDefinition o1, final ChargeDefinition o2) {
+ final Optional<ChargeProportionalDesignator> aProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o1.getProportionalTo());
+ final Optional<ChargeProportionalDesignator> bProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o2.getProportionalTo());
+
+ if (aProportionalToDesignator.isPresent() && bProportionalToDesignator.isPresent())
+ return Integer.compare(
+ aProportionalToDesignator.get().getOrderOfApplication(),
+ bProportionalToDesignator.get().getOrderOfApplication());
+ else if (aProportionalToDesignator.isPresent())
+ return 1;
+ else if (bProportionalToDesignator.isPresent())
+ return -1;
+ else
+ return 0;
}
}
\ No newline at end of file
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 f43aa99..58cac97 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
@@ -15,14 +15,18 @@
*/
package io.mifos.individuallending.internal.service;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
+import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -34,38 +38,59 @@
{
}
- static Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
+ static Map<Period, BigDecimal> getPeriodAccrualInterestRate(final List<ScheduledCharge> scheduledCharges,
+ final int precision) {
return scheduledCharges.stream()
- .filter(PeriodChargeCalculator::accruedCharge)
+ .filter(PeriodChargeCalculator::accruedInterestCharge)
.collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
Collectors.mapping(x -> chargeAmountPerPeriod(x, precision), RateCollectors.compound(precision))));
}
- private static boolean accruedCharge(final ScheduledCharge scheduledCharge)
+ private static boolean accruedInterestCharge(final ScheduledCharge scheduledCharge)
{
return scheduledCharge.getChargeDefinition().getAccrualAccountDesignator() != null &&
scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
- scheduledCharge.getScheduledAction().repaymentPeriod != null;
+ scheduledCharge.getChargeDefinition().getAccrueAction().equals(Action.APPLY_INTEREST.name()) &&
+ scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT &&
+ scheduledCharge.getScheduledAction().actionPeriod != null;
}
static BigDecimal chargeAmountPerPeriod(final ScheduledCharge scheduledCharge, final int precision)
{
- if (scheduledCharge.getChargeDefinition().getForCycleSizeUnit() == null)
- return scheduledCharge.getChargeDefinition().getAmount();
+ final ChargeDefinition chargeDefinition = scheduledCharge.getChargeDefinition();
+ final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
+ if (chargeDefinition.getForCycleSizeUnit() == null)
+ return chargeDefinition.getAmount();
final BigDecimal actionPeriodDuration
- = BigDecimal.valueOf(
- scheduledCharge.getScheduledAction().actionPeriod
- .getDuration()
- .getSeconds());
+ = BigDecimal.valueOf(
+ scheduledAction.actionPeriod
+ .getDuration()
+ .getSeconds());
+ final Optional<BigDecimal> accrualPeriodDuration = Optional.ofNullable(chargeDefinition.getAccrueAction())
+ .flatMap(action -> ScheduledActionHelpers.getAccrualPeriodDurationForAction(Action.valueOf(action)))
+ .map(Duration::getSeconds)
+ .map(BigDecimal::valueOf);
+
final BigDecimal chargeDefinitionCycleSizeUnitDuration
= BigDecimal.valueOf(
- Optional.ofNullable(scheduledCharge.getChargeDefinition().getForCycleSizeUnit())
+ Optional.ofNullable(chargeDefinition.getForCycleSizeUnit())
.orElse(ChronoUnit.YEARS)
.getDuration()
.getSeconds());
- final BigDecimal periodsInCycle = chargeDefinitionCycleSizeUnitDuration.divide(actionPeriodDuration, precision, BigDecimal.ROUND_HALF_EVEN);
- return scheduledCharge.getChargeDefinition().getAmount().divide(periodsInCycle, precision, BigDecimal.ROUND_HALF_EVEN);
+ final BigDecimal accrualPeriodsInCycle = chargeDefinitionCycleSizeUnitDuration.divide(
+ accrualPeriodDuration.orElse(actionPeriodDuration), precision, BigDecimal.ROUND_HALF_EVEN);
+ final int accrualPeriodsInActionPeriod = actionPeriodDuration.divide(
+ accrualPeriodDuration.orElse(actionPeriodDuration), precision, BigDecimal.ROUND_HALF_EVEN)
+ .intValueExact();
+ final BigDecimal rateForAccrualPeriod = chargeDefinition.getAmount().divide(
+ accrualPeriodsInCycle, precision, BigDecimal.ROUND_HALF_EVEN);
+ return createCompoundedRate(rateForAccrualPeriod, accrualPeriodsInActionPeriod, precision);
+ }
+
+ static BigDecimal createCompoundedRate(final BigDecimal interestRate, final int periodCount, final int precision)
+ {
+ return Stream.generate(() -> interestRate).limit(periodCount).collect(RateCollectors.compound(precision));
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
index 434414b..b9c5eb6 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
@@ -262,4 +262,11 @@
final int maxDay = YearMonth.of(paymentDate.getYear(), paymentDate.getMonth()).lengthOfMonth()-1;
return paymentDate.plusDays(Math.min(maxDay, alignmentDay));
}
+
+ public static Optional<Duration> getAccrualPeriodDurationForAction(final Action action) {
+ if (action == Action.APPLY_INTEREST)
+ return Optional.of(ChronoUnit.DAYS.getDuration());
+ else
+ return Optional.empty();
+ }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index adffc64..4718e1b 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
@@ -15,6 +15,7 @@
*/
package io.mifos.portfolio.service.internal.mapper;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.internal.repository.ChargeDefinitionEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
@@ -71,15 +72,17 @@
if ((chargeMethod == ChargeDefinition.ChargeMethod.FIXED) || (from.getProportionalTo() != null))
return from.getProportionalTo();
- if (identifier.equals(LOAN_FUNDS_ALLOCATION_ID))
- return MAXIMUM_BALANCE_DESIGNATOR;
- else if (identifier.equals(LOAN_ORIGINATION_FEE_ID))
- return MAXIMUM_BALANCE_DESIGNATOR;
- else if (identifier.equals(PROCESSING_FEE_ID))
- return MAXIMUM_BALANCE_DESIGNATOR;
- else if (identifier.equals(LATE_FEE_ID))
- return REPAYMENT_ID;
- else
- return RUNNING_BALANCE_DESIGNATOR;
+ switch (identifier) {
+ case LOAN_FUNDS_ALLOCATION_ID:
+ return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
+ case LOAN_ORIGINATION_FEE_ID:
+ return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
+ case PROCESSING_FEE_ID:
+ return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
+ case LATE_FEE_ID:
+ return ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue();
+ default:
+ return ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue();
+ }
}
}
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 2dc3d1c..c649180 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
@@ -85,19 +85,19 @@
return new ScheduledAction(Action.ACCEPT_PAYMENT, to, repaymentPeriod, repaymentPeriod);
}
- static ScheduledCharge scheduledInterestCharge(
- final double amount,
- final LocalDate initialDate,
- final int chargeDateDelta,
- final int periodBeginDelta,
- final int periodLength)
+ static ScheduledCharge scheduledInterestBookingCharge(
+ final double amount,
+ final LocalDate initialDate,
+ final int chargeDateDelta,
+ final int periodBeginDelta,
+ final int periodLength)
{
final LocalDate chargeDate = initialDate.plusDays(chargeDateDelta);
final ScheduledAction scheduledAction = new ScheduledAction(
- Action.APPLY_INTEREST,
- chargeDate,
- new Period(chargeDate, 1),
- getPeriod(initialDate, periodBeginDelta, periodLength));
+ Action.ACCEPT_PAYMENT,
+ chargeDate,
+ new Period(chargeDate, periodLength),
+ getPeriod(initialDate, periodBeginDelta, periodLength));
final ChargeDefinition chargeDefinition = new ChargeDefinition();
chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
chargeDefinition.setForCycleSizeUnit(ChronoUnit.YEARS);
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 209dafd..0cbf933 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
@@ -299,8 +299,13 @@
.map(CostComponent::getAmount)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
+ final BigDecimal valueOfPrincipleTrackingCostComponent = allPlannedPayments.get(x).getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID))
+ .map(CostComponent::getAmount)
+ .reduce(BigDecimal::add)
+ .orElse(BigDecimal.ZERO);
final BigDecimal principalDifference = allPlannedPayments.get(x-1).getRemainingPrincipal().subtract(allPlannedPayments.get(x).getRemainingPrincipal());
- Assert.assertEquals(costComponentSum, principalDifference);
+ Assert.assertEquals(valueOfPrincipleTrackingCostComponent, principalDifference);
Assert.assertNotEquals("Remaining principle should always be positive or zero.",
allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
return costComponentSum;
@@ -325,7 +330,8 @@
Assert.assertTrue(maxPayment.isPresent());
Assert.assertTrue(minPayment.isPresent());
final double percentDifference = percentDifference(maxPayment.get(), minPayment.get());
- Assert.assertTrue("Percent difference = " + percentDifference, percentDifference < 0.01);
+ Assert.assertTrue("Percent difference = " + percentDifference + ", max = " + maxPayment.get() + ", min = " + minPayment.get(),
+ percentDifference < 0.01);
//Final balance should be zero.
Assert.assertEquals(BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
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 8571638..6b3c2ff 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
@@ -23,10 +23,9 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
-import java.util.stream.Stream;
import static io.mifos.individuallending.internal.service.Fixture.getPeriod;
-import static io.mifos.individuallending.internal.service.Fixture.scheduledInterestCharge;
+import static io.mifos.individuallending.internal.service.Fixture.scheduledInterestBookingCharge;
/**
* @author Myrle Krantz
@@ -80,68 +79,57 @@
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 0, 0, 1));
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 1, 1, 1));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.01, initialDate, 0, 0, 1));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.01, initialDate, 1, 1, 1));
final BigDecimal dailyInterestRate = BigDecimal.valueOf(0.01)
- .divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
+ .divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
final Map<Period, BigDecimal> expectedPeriodRates = new HashMap<>();
expectedPeriodRates.put(getPeriod(initialDate, 0, 1), dailyInterestRate);
expectedPeriodRates.put(getPeriod(initialDate, 1, 1), dailyInterestRate);
return new TestCase("simpleCase")
- .scheduledCharges(scheduledCharges)
- .precision(20)
- .expectedPeriodRates(expectedPeriodRates);
+ .scheduledCharges(scheduledCharges)
+ .precision(20)
+ .expectedPeriodRates(expectedPeriodRates);
}
private static TestCase bitOfCompoundingCase()
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 0, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 1, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 2, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 3, 2, 2));
- scheduledCharges.add(scheduledInterestCharge(0.01, initialDate, 4, 2, 2));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.10, initialDate, 2, 0, 3));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.10, initialDate, 4, 2, 2));
- final BigDecimal dailyInterestRate = BigDecimal.valueOf(0.01)
- .divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
+ final BigDecimal dailyInterestRate = BigDecimal.valueOf(0.10)
+ .divide(BigDecimal.valueOf(365.2425), 20, BigDecimal.ROUND_HALF_EVEN);
final Map<Period, BigDecimal> expectedPeriodRates = new HashMap<>();
- expectedPeriodRates.put(getPeriod(initialDate, 0, 3), createCompoundedInterestRate(dailyInterestRate, 3, 20));
- expectedPeriodRates.put(getPeriod(initialDate, 2, 2), createCompoundedInterestRate(dailyInterestRate, 2, 20));
+ expectedPeriodRates.put(getPeriod(initialDate, 0, 3), PeriodChargeCalculator.createCompoundedRate(dailyInterestRate, 3, 20));
+ expectedPeriodRates.put(getPeriod(initialDate, 2, 2), PeriodChargeCalculator.createCompoundedRate(dailyInterestRate, 2, 20));
return new TestCase("bitOfCompoundingCase")
- .scheduledCharges(scheduledCharges)
- .precision(20)
- .expectedPeriodRates(expectedPeriodRates);
+ .scheduledCharges(scheduledCharges)
+ .precision(20)
+ .expectedPeriodRates(expectedPeriodRates);
}
private static TestCase zeroInterestPerPeriod()
{
final LocalDate initialDate = LocalDate.now();
final List<ScheduledCharge> scheduledCharges = new ArrayList<>();
- scheduledCharges.add(scheduledInterestCharge(0.00, initialDate, 0, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.00, initialDate, 1, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.00, initialDate, 2, 0, 3));
- scheduledCharges.add(scheduledInterestCharge(0.00, initialDate, 3, 2, 2));
- scheduledCharges.add(scheduledInterestCharge(0.00, initialDate, 4, 2, 2));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.00, initialDate, 2, 0, 3));
+ scheduledCharges.add(scheduledInterestBookingCharge(0.00, 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")
- .scheduledCharges(scheduledCharges)
- .precision(20)
- .expectedPeriodRates(expectedPeriodRates);
- }
-
- private static BigDecimal createCompoundedInterestRate(BigDecimal interestRate, int periodCount, int precision)
- {
- return Stream.generate(() -> interestRate).limit(periodCount).collect(RateCollectors.compound(precision));
+ .scheduledCharges(scheduledCharges)
+ .precision(20)
+ .expectedPeriodRates(expectedPeriodRates);
}
private final TestCase testCase;
@@ -151,10 +139,9 @@
}
@Test
- public void test()
+ public void getPeriodAccrualRatesTest()
{
- final PeriodChargeCalculator testSubject = new PeriodChargeCalculator();
- final Map<Period, BigDecimal> periodRates = testSubject.getPeriodAccrualRates(testCase.scheduledCharges, testCase.precision);
+ final Map<Period, BigDecimal> periodRates = PeriodChargeCalculator.getPeriodAccrualInterestRate(testCase.scheduledCharges, testCase.precision);
Assert.assertEquals(testCase.expectedPeriodRates, periodRates);
}
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
index ff22aea..a883ee5 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.individuallending.internal.service;
import io.mifos.individuallending.api.v1.domain.workflow.Action;