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();
+    }
 }


Reply via email to