Various improvements and fixes to planned payment calculation,
plus implementation of multi-tranche loan disbursal.
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 9270c92..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,10 +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 REPAYMENT_DESIGNATOR = "{repayment}";
- 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 bc61fda..33d2978 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -85,7 +85,7 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5DisburseFullAmount();
+ step5Disburse(BigDecimal.valueOf(2000L));
step6CalculateInterestAccrual();
step7PaybackPartialAmount(expectedCurrentBalance);
}
@@ -97,7 +97,7 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5DisburseFullAmount();
+ step5Disburse(BigDecimal.valueOf(2000L));
step6CalculateInterestAccrual();
final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
step7PaybackPartialAmount(repayment1);
@@ -207,13 +207,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,
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 2bbea7c..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,7 +168,7 @@
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,
@@ -178,7 +179,7 @@
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
- interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
+ interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
@@ -187,7 +188,7 @@
customerRepaymentCharge.setDescription(REPAYMENT_NAME);
customerRepaymentCharge.setFromAccountDesignator(CUSTOMER_LOAN);
customerRepaymentCharge.setToAccountDesignator(ENTRY);
- customerRepaymentCharge.setProportionalTo(ChargeIdentifiers.REPAYMENT_DESIGNATOR);
+ customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
customerRepaymentCharge.setAmount(BigDecimal.ONE);
@@ -198,7 +199,7 @@
trackReturnPrincipalCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
trackReturnPrincipalCharge.setFromAccountDesignator(LOAN_FUNDS_SOURCE);
trackReturnPrincipalCharge.setToAccountDesignator(LOANS_PAYABLE);
- trackReturnPrincipalCharge.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
trackReturnPrincipalCharge.setAmount(BigDecimal.ONE);
@@ -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);
@@ -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 e62327e..b796cc4 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;
@@ -54,7 +53,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -222,21 +220,19 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForDisburse(dataContextOfAction);
+ costComponentService.getCostComponentsForDisburse(dataContextOfAction, command.getCommand().getPaymentSize());
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))
- .filter(Optional::isPresent)
- .map(Optional::get),
- 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,
@@ -404,7 +400,7 @@
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
- if (chargeIsAccrued(chargeDefinition)) {
+ if (CostComponentService.chargeIsAccrued(chargeDefinition)) {
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
return Optional.of(new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
@@ -427,19 +423,6 @@
return Optional.empty();
}
- private static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
- return chargeDefinition.getAccrualAccountDesignator() != null;
- }
-
- private static ChargeInstance getDisbursalChargeInstance(
- final BigDecimal amount,
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
- return new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.PENDING_DISBURSAL),
- designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
- amount);
- }
-
private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
if (costComponents == null)
return Collections.emptyMap();
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 7a1a6e5..2da9a4a 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;
@@ -52,7 +53,6 @@
public class CostComponentService {
private static final int EXTRA_PRECISION = 4;
private static final int RUNNING_CALCULATION_PRECISION = 8;
- private static final String NOT_PROPORTIONAL = "null";
private final ProductRepository productRepository;
private final CaseRepository caseRepository;
@@ -107,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:
@@ -139,7 +139,8 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
@@ -156,7 +157,8 @@
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
@@ -174,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.badRequest("Cannot disburse over the maximum balance.");
final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
customerLoanAccountIdentifier,
@@ -191,6 +199,9 @@
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);
@@ -215,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(
@@ -254,7 +266,8 @@
caseParameters.getMaximumBalance(),
currentBalance,
BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
@@ -312,7 +325,8 @@
caseParameters.getMaximumBalance(),
currentBalance,
loanPaymentSize,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ true);
}
private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
@@ -325,11 +339,6 @@
chargeDefinition.getAccrueAction().equals(action.name());
}
- private static boolean isOneOffChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
- return chargeDefinition.getChargeAction() != null &&
- chargeDefinition.getChargeAction().equals(action.name());
- }
-
private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
final LocalDate startOfTerm,
@@ -383,105 +392,86 @@
final BigDecimal maximumBalance,
final BigDecimal runningBalance,
final BigDecimal loanPaymentSize,
- final int minorCurrencyUnitDigits) {
- BigDecimal balanceAdjustment = BigDecimal.ZERO;
- BigDecimal currentRunningBalance = runningBalance;
+ 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<String, List<ScheduledCharge>> partitionedCharges = scheduledCharges.stream()
- .collect(Collectors.groupingBy(CostComponentService::proportionalToDesignator));
- final List<String> orderOfChargesByDesignatorFirstSet = Arrays.asList(
- NOT_PROPORTIONAL,
- ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR,
- ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
+ for (final ScheduledCharge scheduledCharge : scheduledCharges) {
+ if (accrualAccounting || !isAccrualChargeForAction(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().action)) {
+ final BigDecimal amountProportionalTo = getAmountProportionalTo(
+ scheduledCharge,
+ maximumBalance,
+ runningBalance,
+ loanPaymentSize,
+ balanceAdjustments);
+ //TODO: getAmountProportionalTo is programmed under the assumption of non-accrual accounting.
- for (final String chargeProportionalTo : orderOfChargesByDesignatorFirstSet)
- {
- final BigDecimal amountProportionalTo;
- switch (chargeProportionalTo) {
- case NOT_PROPORTIONAL:
- amountProportionalTo = BigDecimal.ZERO;
- break;
- case ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR:
- amountProportionalTo = maximumBalance;
- break;
- case ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR:
- amountProportionalTo = runningBalance;
- break;
- default:
- amountProportionalTo = BigDecimal.ZERO;
- break;
- }
-//TODO: correctly implement charges which are proportionate to other charges.
+ final CostComponent costComponent = costComponentMap
+ .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
- final List<ScheduledCharge> partition = partitionedCharges.get(chargeProportionalTo);
- if (partition != null) {
- for (final ScheduledCharge scheduledCharge : partition) {
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
-
- final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge)
- .apply(amountProportionalTo)
- .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 List<String> orderOfChargesByDesignatorSecondSet = Arrays.asList(
- ChargeIdentifiers.REPAYMENT_DESIGNATOR,
- ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
-
-
- final BigDecimal principalAdjustment = loanPaymentSize.subtract(balanceAdjustment);
- for (final String chargeProportionalTo : orderOfChargesByDesignatorSecondSet)
- {
- final BigDecimal amountProportionalTo;
- switch (chargeProportionalTo) {
- case ChargeIdentifiers.REPAYMENT_DESIGNATOR:
- amountProportionalTo = loanPaymentSize;
- break;
- case ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR:
- amountProportionalTo = principalAdjustment;
- break;
- default:
- amountProportionalTo = BigDecimal.ZERO;
- break;
- }
-
- final List<ScheduledCharge> partition = partitionedCharges.get(chargeProportionalTo);
- if (partition != null) {
- for (final ScheduledCharge scheduledCharge : partition) {
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
-
- final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge)
- .apply(amountProportionalTo)
- .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 getAmountProportionalTo(
+ final ScheduledCharge scheduledCharge,
+ 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) {
@@ -491,55 +481,35 @@
return ret;
}
- private static String proportionalToDesignator(final ScheduledCharge scheduledCharge) {
+ private static Optional<ChargeProportionalDesignator> proportionalToDesignator(final ScheduledCharge scheduledCharge) {
if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL))
- return NOT_PROPORTIONAL;
+ return Optional.of(ChargeProportionalDesignator.NOT_PROPORTIONAL);
- return scheduledCharge.getChargeDefinition().getProportionalTo();
+ return ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
}
private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
final ScheduledCharge scheduledCharge)
{
- final ChargeDefinition chargeDefinition = scheduledCharge.getChargeDefinition();
- final Action action = scheduledCharge.getScheduledAction().action;
switch (scheduledCharge.getChargeDefinition().getChargeMethod())
{
case FIXED:
return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount();
case PROPORTIONAL:
- if (isAccrualChargeForAction(chargeDefinition, action))
- return (amountProportionalTo) ->
- PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
- .multiply(amountProportionalTo);
- else if (isOneOffChargeForAction(chargeDefinition, action))
- return (amountProportionalTo) ->
- scheduledCharge.getChargeDefinition().getAmount()
- .multiply(amountProportionalTo);
+ return (amountProportionalTo) ->
+ PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
+ .multiply(amountProportionalTo);
default:
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)
@@ -552,7 +522,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..e2d25f6 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()
@@ -140,15 +145,12 @@
{
final BigDecimal currentLoanPaymentSize;
if (repaymentPeriod.isDefined()) {
- if (balance.compareTo(loanPaymentSize) < 0)
- currentLoanPaymentSize = balance;
- else
- currentLoanPaymentSize = loanPaymentSize;
+ 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(),
@@ -156,7 +158,8 @@
balance,
balance,
currentLoanPaymentSize,
- minorCurrencyUnitDigits);
+ minorCurrencyUnitDigits,
+ false);
final PlannedPayment plannedPayment = new PlannedPayment();
plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
@@ -205,7 +208,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 104946b..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;
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);
}
}