This is an automated email from the ASF dual-hosted git repository.
taskain pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new dce264c16 FINERACT-2060: re-amortization
dce264c16 is described below
commit dce264c1696bb53215bdbfd81b78476557bbc8a9
Author: taskain7 <[email protected]>
AuthorDate: Tue Mar 12 02:18:49 2024 +0100
FINERACT-2060: re-amortization
---
...dvancedPaymentScheduleTransactionProcessor.java | 49 ++
.../LoanReAmortizationServiceImpl.java | 31 ++
.../LoanReAmortizationIntegrationTest.java | 507 ++++++++++++++++++++-
3 files changed, 585 insertions(+), 2 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 60fb6caee..d4a6a9f42 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -18,9 +18,11 @@
*/
package
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
+import static java.math.BigDecimal.ZERO;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
+import static
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE;
import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
@@ -182,6 +184,7 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction,
ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
ctx.getOverpaymentHolder());
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will
not be processed.");
+ case REAMORTIZE -> handleReAmortization(loanTransaction,
ctx.getCurrency(), ctx.getInstallments());
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction
type: {}", loanTransaction.getTypeOf());
@@ -189,6 +192,52 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
}
}
+ private void handleReAmortization(LoanTransaction loanTransaction,
MonetaryCurrency currency,
+ List<LoanRepaymentScheduleInstallment> installments) {
+ BigDecimal remainingAmount = loanTransaction.getAmount();
+ LocalDate transactionDate = loanTransaction.getTransactionDate();
+ List<LoanRepaymentScheduleInstallment> previousInstallments =
installments.stream() //
+ .filter(installment ->
!installment.getDueDate().isAfter(transactionDate)) //
+ .toList();
+ List<LoanRepaymentScheduleInstallment> futureInstallments =
installments.stream() //
+ .filter(installment ->
installment.getDueDate().isAfter(transactionDate)) //
+ .filter(installment -> !installment.isAdditional() &&
!installment.isDownPayment()) //
+ .toList();
+
+ BigDecimal overallOverDuePrincipal = ZERO;
+ for (LoanRepaymentScheduleInstallment installment :
previousInstallments) {
+ Money principalCompleted =
installment.getPrincipalCompleted(currency);
+ overallOverDuePrincipal =
overallOverDuePrincipal.add(installment.getPrincipal(currency).minus(principalCompleted).getAmount());
+
installment.updatePrincipal(installment.getPrincipalCompleted(currency).getAmount());
+ installment.updateDerivedFields(currency, transactionDate);
+ }
+
+ if (overallOverDuePrincipal.compareTo(remainingAmount) != 0) {
+ remainingAmount = overallOverDuePrincipal;
+ loanTransaction.updateComponentsAndTotal(Money.of(currency,
remainingAmount), Money.zero(currency), Money.zero(currency),
+ Money.zero(currency));
+ }
+
+ LoanRepaymentScheduleInstallment lastFutureInstallment =
futureInstallments.stream()
+
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).get();
+ BigDecimal reAmortizationAmountPerInstallment =
remainingAmount.divide(BigDecimal.valueOf(futureInstallments.size()),
+ MoneyHelper.getRoundingMode());
+ Integer installmentAmountInMultiplesOf =
loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
+
+ for (LoanRepaymentScheduleInstallment installment :
futureInstallments) {
+ if (lastFutureInstallment.equals(installment)) {
+ installment.addToPrincipal(transactionDate, Money.of(currency,
remainingAmount));
+ } else {
+ if (installmentAmountInMultiplesOf != null) {
+ reAmortizationAmountPerInstallment =
Money.roundToMultiplesOf(reAmortizationAmountPerInstallment,
+ installmentAmountInMultiplesOf);
+ }
+ installment.addToPrincipal(transactionDate, Money.of(currency,
reAmortizationAmountPerInstallment));
+ remainingAmount =
remainingAmount.subtract(reAmortizationAmountPerInstallment);
+ }
+ }
+ }
+
@Override
protected void handleChargeback(LoanTransaction loanTransaction,
TransactionCtx ctx) {
processCreditTransaction(loanTransaction, ctx);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
index c3ff06eb6..b12af758f 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
@@ -22,8 +22,10 @@ import static java.math.BigDecimal.ZERO;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -40,9 +42,13 @@ import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.organisation.monetary.domain.Money;
import
org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -57,6 +63,7 @@ public class LoanReAmortizationServiceImpl {
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionRepository loanTransactionRepository;
+ private final LoanRepaymentScheduleTransactionProcessorFactory
loanRepaymentScheduleTransactionProcessorFactory;
public CommandProcessingResult reAmortize(Long loanId, JsonCommand
command) {
Loan loan = loanAssembler.assembleFrom(loanId);
@@ -67,6 +74,13 @@ public class LoanReAmortizationServiceImpl {
changes.put(LoanReAmortizationApiConstants.dateFormatParameterName,
command.dateFormat());
LoanTransaction reAmortizeTransaction =
createReAmortizeTransaction(loan, command);
+
+ final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor =
loanRepaymentScheduleTransactionProcessorFactory
+ .determineProcessor(loan.transactionProcessingStrategy());
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction,
+ new
LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(),
+ loan.getActiveCharges(), new
MoneyHolder(loan.getTotalOverpaidAsMoney())));
+
loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
// delinquency recalculation will be triggered by the event in a
decoupled way via a listener
@@ -117,6 +131,23 @@ public class LoanReAmortizationServiceImpl {
LoanReAmortizationApiConstants.externalIdParameterName);
reAmortizeTransaction.reverse(reversalExternalId);
reAmortizeTransaction.manuallyAdjustedOrReversed();
+ reProcessLoanTransactions(reAmortizeTransaction.getLoan());
+ }
+
+ private void reProcessLoanTransactions(Loan loan) {
+ final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
+ List<LoanTransaction> trans = loan.getLoanTransactions();
+ for (final LoanTransaction transaction : trans) {
+ if (transaction.isNotReversed() && (transaction.isChargeOff() ||
!transaction.isNonMonetaryTransaction())) {
+ repaymentsOrWaivers.add(transaction);
+ }
+ }
+ repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE);
+
+ final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor =
loanRepaymentScheduleTransactionProcessorFactory
+ .determineProcessor(loan.transactionProcessingStrategy());
+
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
repaymentsOrWaivers,
+ loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
}
private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan
loan) {
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
index d902baf85..eae605740 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
@@ -18,8 +18,11 @@
*/
package org.apache.fineract.integrationtests.loan.reamortization;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;
+import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
@@ -28,10 +31,17 @@ import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
import org.apache.fineract.integrationtests.common.ClientHelper;
import
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+@ExtendWith(LoanTestLifecycleExtension.class)
public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest
{
+ public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new
BigDecimal(25);
+
@Test
public void test_LoanReAmortizeTransaction_Works() {
AtomicLong createdLoanId = new AtomicLong();
@@ -99,7 +109,11 @@ public class LoanReAmortizationIntegrationTest extends
BaseLoanIntegrationTest {
transaction(625.0, "Re-amortize", "02 February 2023") //
);
- // TODO: verify installments when schedule generation is
implemented
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(0.0, true, "01 February 2023"), //
+ installment(1250.0, false, "01 March 2023") //
+ );
});
}
@@ -183,7 +197,496 @@ public class LoanReAmortizationIntegrationTest extends
BaseLoanIntegrationTest {
reversedTransaction(625.0, "Re-amortize", "02 February
2023") //
);
- // TODO: verify installments when schedule generation is
implemented
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(625.0, false, "01 February 2023"), //
+ installment(625.0, false, "01 March 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void reAmortizeLoanRepaymentScheduleTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("05 January 2023");
+ addCharge(loanId, false, 10.0, "05 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, 0.0, 10.0, 135.0, false, "16 January
2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("25 January 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, 0.0, 10.0, 10.0, false, "16 January
2023"), //
+ installment(187.5, false, "31 January 2023"), //
+ installment(187.5, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void completePastDueReAmortizationTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("01 February 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void partiallyPaidReAmortizationTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("17 January 2023");
+ addRepaymentForLoan(loanId, 50.0, "17 January 2023");
+
+ updateBusinessDate("30 January 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(50.0, true, "16 January 2023"), //
+ installment(162.5, false, "31 January 2023"), //
+ installment(162.5, false, "15 February 2023")//
+ );
});
}
+
+ @Test
+ public void reAmortizationOnSameDayOfInstallmentTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("05 January 2023");
+ addCharge(loanId, false, 10.0, "05 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, 0.0, 10.0, 135.0, false, "16 January
2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("31 January 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, 0.0, 10.0, 10.0, false, "16 January
2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void reAmortizationNPlusOneInstallmentTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("01 February 2023");
+ addCharge(loanId, false, 10.0, "27 February 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023"), //
+ installment(0.0, 0.0, 10.0, false, "27 February 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void reAmortizationBackdatedRepaymentAndReplayTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("01 February 2023");
+
+ reAmortizeLoan(loanId);
+
+ updateBusinessDate("01 February 2023");
+ addRepaymentForLoan(loanId, 125.0, "15 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(250.0, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void reAmortizationUndoRepaymentAndReplayTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("15 January 2023");
+ Long repaymentTransactionId = addRepaymentForLoan(loanId, 125.0,
"15 January 2023");
+
+ updateBusinessDate("01 February 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(250.0, false, "15 February 2023")//
+ );
+
+ loanTransactionHelper.reverseRepayment(loanId.intValue(),
repaymentTransactionId.intValue(), "01 February 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void reverseReAmortizationTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("01 February 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, true, "16 January 2023"), //
+ installment(0.0, true, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023")//
+ );
+
+ updateBusinessDate("02 February 2023");
+
+ undoReAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void reAmortizationDivisionTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(4, 15,
BigDecimal.valueOf(20));
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 500.0, 4, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(60);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(100.0, true, "01 January 2023"), //
+ installment(100.0, false, "16 January 2023"), //
+ installment(100.0, false, "31 January 2023"), //
+ installment(100.0, false, "15 February 2023"), //
+ installment(100.0, false, "02 March 2023")//
+ );
+
+ updateBusinessDate("17 January 2023");
+
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(100.0, true, "01 January 2023"), //
+ installment(0.0, true, "16 January 2023"), //
+ installment(133.33, false, "31 January 2023"), //
+ installment(133.33, false, "15 February 2023"), //
+ installment(133.34, false, "02 March 2023")//
+ );
+ });
+ }
+
+ @Test
+ public void secondDisbursementAfterReAmortizationTest() {
+ runAt("01 January 2023", () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ Long loanProductId =
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
+
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0, 3, req -> {
+ req.setRepaymentEvery(15);
+ req.setLoanTermFrequency(45);
+
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
+
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
+ });
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(125.0, false, "16 January 2023"), //
+ installment(125.0, false, "31 January 2023"), //
+ installment(125.0, false, "15 February 2023") //
+ );
+
+ updateBusinessDate("16 January 2023");
+ addCharge(loanId, false, 10.0, "16 January 2023");
+
+ updateBusinessDate("25 January 2023");
+ reAmortizeLoan(loanId);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, 0.0, 10.0, 10.0, false, "16 January
2023"), //
+ installment(187.5, false, "31 January 2023"), //
+ installment(187.5, false, "15 February 2023") //
+ );
+
+ updateBusinessDate("26 January 2023");
+
+ disburseLoan(loanId, BigDecimal.valueOf(500.00), "26 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(125.0, true, "01 January 2023"), //
+ installment(0.0, 0.0, 10.0, 0.0, true, "16 January 2023"),
//
+ installment(500.0, null, "26 January 2023"), //
+ installment(125.0, false, "26 January 2023"), //
+ installment(375.0, false, "31 January 2023"), //
+ installment(375.0, false, "15 February 2023") //
+ );
+ });
+ }
+
+ private Long
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int
numberOfInstallments, int repaymentEvery) {
+ return
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(numberOfInstallments,
repaymentEvery,
+ DOWN_PAYMENT_PERCENTAGE);
+ }
+
+ private Long
createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int
numberOfInstallments, int repaymentEvery,
+ BigDecimal downPaymentPercentage) {
+ boolean multiDisburseEnabled = true;
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation();
+ product.setMultiDisburseLoan(multiDisburseEnabled);
+ product.setNumberOfRepayments(numberOfInstallments);
+ product.setRepaymentEvery(repaymentEvery);
+
+ if (!multiDisburseEnabled) {
+ product.disallowExpectedDisbursements(null);
+ product.setAllowApprovedDisbursedAmountsOverApplied(null);
+ product.overAppliedCalculationType(null);
+ product.overAppliedNumber(null);
+ }
+
+ product.setEnableDownPayment(true);
+
product.setDisbursedAmountPercentageForDownPayment(downPaymentPercentage);
+ product.setEnableAutoRepaymentForDownPayment(true);
+ product.setInstallmentAmountInMultiplesOf(null);
+
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ GetLoanProductsProductIdResponse getLoanProductsProductIdResponse =
loanProductHelper
+ .retrieveLoanProductById(loanProductResponse.getResourceId());
+ assertNotNull(getLoanProductsProductIdResponse);
+ return loanProductResponse.getResourceId();
+ }
}