Implementing and testing CLOSE.
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 33d2978..785627a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -88,6 +88,7 @@
step5Disburse(BigDecimal.valueOf(2000L));
step6CalculateInterestAccrual();
step7PaybackPartialAmount(expectedCurrentBalance);
+ step8Close();
}
@@ -102,6 +103,7 @@
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.
@@ -308,4 +310,20 @@
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/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index b796cc4..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
@@ -219,8 +219,9 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
+ final BigDecimal disbursalAmount = Optional.ofNullable(command.getCommand().getPaymentSize()).orElse(BigDecimal.ZERO);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForDisburse(dataContextOfAction, command.getCommand().getPaymentSize());
+ costComponentService.getCostComponentsForDisburse(dataContextOfAction, disbursalAmount);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -369,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);
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 67bc6ce..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
@@ -190,7 +190,7 @@
if (dataContextOfAction.getCaseParameters().getMaximumBalance().compareTo(
currentBalance.add(requestedDisbursalSize)) > 0)
- throw ServiceException.badRequest("Cannot disburse over the maximum balance.");
+ throw ServiceException.conflict("Cannot disburse over the maximum balance.");
final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
customerLoanAccountIdentifier,
@@ -379,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;
}