Routing the size of the payment through with the command so that the
user can repay more or less than the pre-calculated amount.
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index 76eb969..cc28b11 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -17,6 +17,7 @@
import javax.annotation.Nullable;
import javax.validation.Valid;
+import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
@@ -30,7 +31,7 @@
@Valid
@Nullable
- private List<CostComponent> costComponents;
+ private BigDecimal paymentSize;
private String note;
private String createdOn;
@@ -47,12 +48,13 @@
this.oneTimeAccountAssignments = oneTimeAccountAssignments;
}
- public List<CostComponent> getCostComponents() {
- return costComponents;
+ @Nullable
+ public BigDecimal getPaymentSize() {
+ return paymentSize;
}
- public void setCostComponents(List<CostComponent> costComponents) {
- this.costComponents = costComponents;
+ public void setPaymentSize(@Nullable BigDecimal paymentSize) {
+ this.paymentSize = paymentSize;
}
public String getNote() {
@@ -85,20 +87,20 @@
if (o == null || getClass() != o.getClass()) return false;
Command command = (Command) o;
return Objects.equals(oneTimeAccountAssignments, command.oneTimeAccountAssignments) &&
- Objects.equals(costComponents, command.costComponents) &&
+ Objects.equals(paymentSize, command.paymentSize) &&
Objects.equals(note, command.note);
}
@Override
public int hashCode() {
- return Objects.hash(oneTimeAccountAssignments, costComponents, note);
+ return Objects.hash(oneTimeAccountAssignments, paymentSize, note);
}
@Override
public String toString() {
return "Command{" +
"oneTimeAccountAssignments=" + oneTimeAccountAssignments +
- ", costComponents=" + costComponents +
+ ", paymentSize=" + paymentSize +
", note='" + note + '\'' +
", createdOn='" + createdOn + '\'' +
", createdBy='" + createdBy + '\'' +
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 437276d..2fd1429 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -195,8 +195,19 @@
final List<AccountAssignment> oneTimeAccountAssignments,
final String event,
final Case.State nextState) throws InterruptedException {
+ checkStateTransfer(productIdentifier, caseIdentifier, action, oneTimeAccountAssignments, BigDecimal.ZERO, event, nextState);
+ }
+
+ void checkStateTransfer(final String productIdentifier,
+ final String caseIdentifier,
+ final Action action,
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final BigDecimal paymentSize,
+ final String event,
+ final Case.State nextState) throws InterruptedException {
final Command command = new Command();
command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
+ command.setPaymentSize(paymentSize);
portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier)));
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 2ad7ab3..472fc5d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -268,6 +268,7 @@
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
Collections.singletonList(assignEntryToTeller()),
+ expectedCurrentBalance,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
Case.State.ACTIVE); //Close has to be done explicitly.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
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 41c3227..14ef0f9 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
@@ -102,7 +102,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.OPEN,
entry,
- getRequestedChargeAmounts(command.getCommand().getCostComponents()),
designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
@@ -142,7 +141,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.DENY,
entry,
- getRequestedChargeAmounts(command.getCommand().getCostComponents()),
designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
@@ -188,7 +186,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.APPROVE,
entry,
- getRequestedChargeAmounts(command.getCommand().getCostComponents()),
designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
@@ -229,7 +226,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.DISBURSE,
entry,
- getRequestedChargeAmounts(command.getCommand().getCostComponents()),
designatorToAccountIdentifierMapper)),
Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
.collect(Collectors.toList());
@@ -278,7 +274,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.APPLY_INTEREST,
entry,
- Collections.emptyMap(),
designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
@@ -309,16 +304,11 @@
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction);
-
- final Map<String, BigDecimal> requestedChargeAmounts
- = getRequestedChargeAmounts(command.getCommand().getCostComponents());
+ costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction, command.getCommand().getPaymentSize());
final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
.filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
- .map(entry -> getChargeAmount(
- requestedChargeAmounts.get(entry.getKey().getIdentifier()),
- entry.getValue().getAmount()))
+ .map(entry -> entry.getValue().getAmount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
@@ -328,7 +318,6 @@
.map(entry -> mapCostComponentEntryToChargeInstance(
Action.ACCEPT_PAYMENT,
entry,
- requestedChargeAmounts,
designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
@@ -398,38 +387,27 @@
private static ChargeInstance mapCostComponentEntryToChargeInstance(
final Action action,
final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
- final Map<String, BigDecimal> requestedChargeAmounts,
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
-
- final BigDecimal requestedChargeAmount = requestedChargeAmounts.get(chargeDefinition.getIdentifier());
- final BigDecimal configuredChargeAmount = costComponentEntry.getValue().getAmount();
-
- final BigDecimal finalChargeAmount = getChargeAmount(requestedChargeAmount, configuredChargeAmount);
+ final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
if (chargeDefinition.getAccrualAccountDesignator() != null) {
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- finalChargeAmount);
+ chargeAmount);
else
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- finalChargeAmount);
+ chargeAmount);
}
else
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
- finalChargeAmount);
- }
-
- private static BigDecimal getChargeAmount(
- final BigDecimal requestedChargeAmount,
- final BigDecimal configuredChargeAmount) {
- return requestedChargeAmount != null ? requestedChargeAmount : configuredChargeAmount;
+ chargeAmount);
}
private static ChargeInstance getDisbursalChargeInstance(
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 48c4a82..5c871ba 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
@@ -30,10 +30,13 @@
import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.repository.ProductRepository;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import org.javamoney.calc.common.Rate;
+import org.javamoney.moneta.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
+import javax.money.MonetaryAmount;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDate;
@@ -47,6 +50,7 @@
*/
@Service
public class CostComponentService {
+ private static final int EXTRA_PRECISION = 4;
private static final int RUNNING_CALCULATION_PRECISION = 8;
private final ProductRepository productRepository;
@@ -106,7 +110,7 @@
case APPLY_INTEREST:
return getCostComponentsForApplyInterest(dataContextOfAction);
case ACCEPT_PAYMENT:
- return getCostComponentsForAcceptPayment(dataContextOfAction);
+ return getCostComponentsForAcceptPayment(dataContextOfAction, null);
case CLOSE:
return getCostComponentsForClose(dataContextOfAction);
case MARK_LATE:
@@ -253,7 +257,8 @@
}
public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
- final DataContextOfAction dataContextOfAction)
+ final DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal requestedLoanPaymentSize)
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -272,11 +277,24 @@
caseParameters
);
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final BigDecimal loanPaymentSize;
+ if (requestedLoanPaymentSize != null)
+ loanPaymentSize = requestedLoanPaymentSize;
+ else {
+ final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
+ today(),
+ caseParameters);
+ final List<ScheduledCharge> hypotheticalScheduledCharges = individualLoanService.getScheduledCharges(
+ productIdentifier,
+ hypotheticalScheduledActions);
+ loanPaymentSize = getLoanPaymentSize(currentBalance, minorCurrencyUnitDigits, hypotheticalScheduledCharges);
+ }
+
+ final List<ScheduledCharge> scheduledChargesForThisAction = individualLoanService.getScheduledCharges(
productIdentifier,
Collections.singletonList(scheduledAction));
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
.collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.ACCEPT_PAYMENT)));
final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
@@ -285,12 +303,14 @@
.collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
+
+
return getCostComponentsForScheduledCharges(
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getMaximumBalance(),
currentBalance,
- BigDecimal.ZERO,//TODO: This needs to be provided by the user, or calculated. ZERO is wrong.
+ loanPaymentSize,
minorCurrencyUnitDigits);
}
@@ -467,6 +487,27 @@
(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);
+
+ final int periodCount = accrualRatesByPeriod.size();
+ if (periodCount == 0)
+ return startingBalance;
+
+ final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream()
+ .collect(RateCollectors.geometricMean(precision));
+
+ final MonetaryAmount presentValue = AnnuityPayment.calculate(
+ Money.of(startingBalance, "XXX"),
+ Rate.of(geometricMeanAccrualRate),
+ periodCount);
+ return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact());
+ }
+
private static LocalDate today() {
return LocalDate.now(Clock.systemUTC());
}
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 23eb766..ebdf4cf 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
@@ -23,13 +23,10 @@
import io.mifos.portfolio.api.v1.domain.Product;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import io.mifos.portfolio.service.internal.service.ProductService;
-import org.javamoney.calc.common.Rate;
-import org.javamoney.moneta.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
-import javax.money.MonetaryAmount;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
@@ -42,18 +39,14 @@
*/
@Service
public class IndividualLoanService {
- private static final int EXTRA_PRECISION = 4;
private final ProductService productService;
private final ChargeDefinitionService chargeDefinitionService;
- private final PeriodChargeCalculator periodChargeCalculator;
@Autowired
public IndividualLoanService(final ProductService productService,
- final ChargeDefinitionService chargeDefinitionService,
- final PeriodChargeCalculator periodChargeCalculator) {
+ final ChargeDefinitionService chargeDefinitionService) {
this.productService = productService;
this.chargeDefinitionService = chargeDefinitionService;
- this.periodChargeCalculator = periodChargeCalculator;
}
public PlannedPaymentPage getPlannedPaymentsPage(
@@ -70,16 +63,10 @@
final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, scheduledActions);
- final int precision = caseParameters.getMaximumBalance().precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
- final Map<Period, BigDecimal> accrualRatesByPeriod
- = periodChargeCalculator.getPeriodAccrualRates(scheduledCharges,
- precision);
-
- final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(precision));
- final BigDecimal loanPaymentSize = loanPaymentInContextOfAccruedInterest(
+ final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
caseParameters.getMaximumBalance(),
- accrualRatesByPeriod.size(),
- geometricMeanAccrualRate);
+ minorCurrencyUnitDigits,
+ scheduledCharges);
final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(
caseParameters.getMaximumBalance(),
@@ -189,17 +176,6 @@
return scheduledAction.repaymentPeriod;
}
- private BigDecimal loanPaymentInContextOfAccruedInterest(
- final BigDecimal initialBalance,
- final int periodCount,
- final BigDecimal geometricMeanOfInterest) {
- if (periodCount == 0)
- throw new IllegalStateException("To calculate a loan payment there must be at least one payment period.");
-
- final MonetaryAmount presentValue = AnnuityPayment.calculate(Money.of(initialBalance, "XXX"), Rate.of(geometricMeanOfInterest), periodCount);
- return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact());
- }
-
private List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
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 d6186a7..f43aa99 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
@@ -34,7 +34,7 @@
{
}
- Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
+ static Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
return scheduledCharges.stream()
.filter(PeriodChargeCalculator::accruedCharge)
.collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
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 c5859c5..209dafd 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
@@ -273,7 +273,7 @@
Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
- testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new PeriodChargeCalculator());
+ testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock);
}
@Test