This is an automated email from the ASF dual-hosted git repository.

adamsaghy 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 d955bc26c FINERACT-2059: Re-aging repayment schedule handling
d955bc26c is described below

commit d955bc26cb5a476b6b3b14cad337c922f5dcab31
Author: Adam Saghy <[email protected]>
AuthorDate: Mon Mar 18 14:28:22 2024 +0100

    FINERACT-2059: Re-aging repayment schedule handling
---
 .../loanaccount/api/LoanReAgingApiConstants.java   |   3 +-
 .../portfolio/loanaccount/domain/Loan.java         |  13 +-
 .../domain/LoanRepaymentScheduleInstallment.java   |  42 +++++
 .../loanaccount/domain/LoanTransaction.java        |   3 +-
 .../loanaccount/domain/LoanTransactionType.java    |   4 +-
 .../domain/reaging/LoanReAgeParameter.java         |   7 +-
 ...tLoanRepaymentScheduleTransactionProcessor.java |  11 +-
 ...dvancedPaymentScheduleTransactionProcessor.java |  74 +++++++-
 .../tenant/module/loan/module-changelog-master.xml |   1 +
 .../1020_add_re_aged_flag_to_loan_installment.xml  |  30 ++++
 .../api/LoanTransactionsApiResourceSwagger.java    |   6 +-
 .../AbstractCumulativeLoanScheduleGenerator.java   |   3 +-
 .../AbstractProgressiveLoanScheduleGenerator.java  |   6 +
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |   2 +-
 .../service/reaging/LoanReAgingServiceImpl.java    |  62 ++++++-
 .../starter/LoanAccountAutoStarter.java            |   6 +-
 .../tenant/parts/0136_loan_reaging_parameters.xml  |   8 +
 ...cedPaymentScheduleTransactionProcessorTest.java |   4 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |  13 +-
 ...ncyDetailsNextPaymentDateConfigurationTest.java |   7 -
 .../loan/reaging/LoanReAgingIntegrationTest.java   | 200 ++++++++++++++-------
 21 files changed, 402 insertions(+), 103 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
index 96411c24b..56c18c33a 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
@@ -24,7 +24,8 @@ public interface LoanReAgingApiConstants {
     String dateFormatParameterName = "dateFormat";
     String externalIdParameterName = "externalId";
 
-    String frequency = "frequency";
+    String frequencyType = "frequencyType";
+    String frequencyNumber = "frequencyNumber";
     String startDate = "startDate";
     String numberOfInstallments = "numberOfInstallments";
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index d9d9c931c..e5266b169 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -2166,8 +2166,9 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
     }
 
     private LocalDate determineExpectedMaturityDate() {
-        final int numberOfInstallments = 
this.repaymentScheduleInstallments.size();
-        List<LoanRepaymentScheduleInstallment> installments = 
getRepaymentScheduleInstallments();
+        List<LoanRepaymentScheduleInstallment> installments = 
getRepaymentScheduleInstallments().stream()
+                .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
+        final int numberOfInstallments = installments.size();
         LocalDate maturityDate = installments.get(numberOfInstallments - 
1).getDueDate();
         ListIterator<LoanRepaymentScheduleInstallment> iterator = 
installments.listIterator(numberOfInstallments);
         while (iterator.hasPrevious()) {
@@ -3432,7 +3433,8 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
         final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
         List<LoanTransaction> trans = getLoanTransactions();
         for (final LoanTransaction transaction : trans) {
-            if (transaction.isNotReversed() && (transaction.isChargeOff() || 
!transaction.isNonMonetaryTransaction())) {
+            if (transaction.isNotReversed() && (transaction.isChargeOff() || 
transaction.isReAge() || transaction.isReAmortize()
+                    || !transaction.isNonMonetaryTransaction())) {
                 repaymentsOrWaivers.add(transaction);
             }
         }
@@ -3670,10 +3672,10 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
     }
 
     private LocalDate getNextUnpaidInstallmentDueDate() {
-        LocalDate nextUnpaidInstallmentDate = null;
         List<LoanRepaymentScheduleInstallment> installments = 
getRepaymentScheduleInstallments();
         LocalDate currentBusinessDate = DateUtils.getBusinessLocalDate();
         LocalDate expectedMaturityDate = determineExpectedMaturityDate();
+        LocalDate nextUnpaidInstallmentDate = expectedMaturityDate;
 
         for (final LoanRepaymentScheduleInstallment installment : 
installments) {
             boolean isCurrentDateBeforeInstallmentAndLoanPeriod = 
DateUtils.isBefore(currentBusinessDate, installment.getDueDate())
@@ -5664,7 +5666,8 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
                 lastCompoundingDate = compoundingDetail.getEffectiveDate();
             }
             List<LoanRepaymentScheduleInstallment> installments = 
getRepaymentScheduleInstallments();
-            LoanRepaymentScheduleInstallment lastInstallment = 
installments.get(installments.size() - 1);
+            LoanRepaymentScheduleInstallment lastInstallment = 
LoanRepaymentScheduleInstallment
+                    .getLastNonDownPaymentInstallment(installments);
             reverseTransactionsPostEffectiveDate(incomeTransactions, 
lastInstallment.getDueDate());
             reverseTransactionsPostEffectiveDate(accrualTransactions, 
lastInstallment.getDueDate());
         }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
index 72ffbd798..c1ff29222 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
@@ -29,6 +29,7 @@ import jakarta.persistence.Table;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -145,6 +146,9 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
     @Column(name = "is_down_payment", nullable = false)
     private boolean isDownPayment;
 
+    @Column(name = "is_re_aged", nullable = false)
+    private boolean isReAged;
+
     @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = 
FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment")
     private Set<LoanInterestRecalcualtionAdditionalDetails> 
loanCompoundingDetails = new HashSet<>();
 
@@ -223,6 +227,36 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
         this.obligationsMet = false;
     }
 
+    public LoanRepaymentScheduleInstallment(Loan loan, Integer 
installmentNumber, LocalDate fromDate, LocalDate dueDate,
+            BigDecimal principal, BigDecimal interestCharged, BigDecimal 
feeChargesCharged, BigDecimal penaltyCharges,
+            BigDecimal creditedPrincipal, BigDecimal creditedFee, BigDecimal 
creditedPenalty, boolean additional, boolean isDownPayment,
+            boolean isReAged) {
+        this.loan = loan;
+        this.installmentNumber = installmentNumber;
+        this.fromDate = fromDate;
+        this.dueDate = dueDate;
+        this.principal = principal;
+        this.interestCharged = interestCharged;
+        this.feeChargesCharged = feeChargesCharged;
+        this.penaltyCharges = penaltyCharges;
+        this.creditedPrincipal = creditedPrincipal;
+        this.creditedFee = creditedFee;
+        this.creditedPenalty = creditedPenalty;
+        this.additional = additional;
+        this.isDownPayment = isDownPayment;
+        this.isReAged = isReAged;
+    }
+
+    public static LoanRepaymentScheduleInstallment newReAgedInstallment(final 
Loan loan, final Integer installmentNumber,
+            final LocalDate fromDate, final LocalDate dueDate, final 
BigDecimal principal) {
+        return new LoanRepaymentScheduleInstallment(loan, installmentNumber, 
fromDate, dueDate, principal, null, null, null, null, null,
+                null, false, false, true);
+    }
+
+    public static LoanRepaymentScheduleInstallment 
getLastNonDownPaymentInstallment(List<LoanRepaymentScheduleInstallment> 
installments) {
+        return installments.stream().filter(i -> 
!i.isDownPayment()).reduce((first, second) -> second).orElseThrow();
+    }
+
     private BigDecimal defaultToNullIfZero(final BigDecimal value) {
         BigDecimal result = value;
         if (BigDecimal.ZERO.compareTo(value) == 0) {
@@ -400,6 +434,10 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
         return this.installmentNumber.compareTo(o.installmentNumber);
     }
 
+    public int compareToByDueDate(LoanRepaymentScheduleInstallment o) {
+        return this.dueDate.compareTo(o.dueDate);
+    }
+
     public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
         return !isPrincipalCompleted(currency);
     }
@@ -1022,4 +1060,8 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
     public enum PaymentAction {
         PAY, UNPAY
     }
+
+    public boolean isReAged() {
+        return isReAged;
+    }
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 1b737fe28..a37e32e82 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -865,7 +865,8 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
                 || 
LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
                 || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || 
LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
                 || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || 
LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())
-                || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()));
+                || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) || 
LoanTransactionType.REAMORTIZE.equals(getTypeOf())
+                || LoanTransactionType.REAGE.equals(getTypeOf()));
     }
 
     public void updateOutstandingLoanBalance(BigDecimal 
outstandingLoanBalance) {
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index 3e3311444..4a02782b6 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -61,7 +61,9 @@ public enum LoanTransactionType {
     CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
     CHARGE_OFF(27, "loanTransactionType.chargeOff"), //
     DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
-    REAGE(29, "loanTransactionType.reAge"), REAMORTIZE(30, 
"loanTransactionType.reAmortize");
+    REAGE(29, "loanTransactionType.reAge"), //
+    REAMORTIZE(30, "loanTransactionType.reAmortize"), //
+    ;
 
     private final Integer value;
     private final String code;
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
index 78198ea71..fcba7a095 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
@@ -40,8 +40,11 @@ public class LoanReAgeParameter extends 
AbstractAuditableWithUTCDateTimeCustom {
     private Long loanTransactionId;
 
     @Enumerated(EnumType.STRING)
-    @Column(name = "frequency", nullable = false)
-    private PeriodFrequencyType frequency;
+    @Column(name = "frequency_type", nullable = false)
+    private PeriodFrequencyType frequencyType;
+
+    @Column(name = "frequency_number", nullable = false)
+    private Integer frequencyNumber;
 
     @Column(name = "start_date", nullable = false)
     private LocalDate startDate;
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index b05c2ae9e..ba9eccf37 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -479,7 +479,8 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
         loanTransaction.resetDerivedComponents();
         List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = 
new ArrayList<>();
         final Comparator<LoanRepaymentScheduleInstallment> byDate = 
Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
-        installments.sort(byDate);
+        List<LoanRepaymentScheduleInstallment> installmentToBeProcessed = 
installments.stream().filter(i -> !i.isDownPayment())
+                .sorted(byDate).toList();
         final Money zeroMoney = Money.zero(currency);
         Money transactionAmount = loanTransaction.getAmount(currency);
         Money principalPortion = 
MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject()));
@@ -492,7 +493,7 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
             final LocalDate transactionDate = 
loanTransaction.getTransactionDate();
             boolean loanTransactionMapped = false;
             LocalDate pastDueDate = null;
-            for (final LoanRepaymentScheduleInstallment currentInstallment : 
installments) {
+            for (final LoanRepaymentScheduleInstallment currentInstallment : 
installmentToBeProcessed) {
                 pastDueDate = currentInstallment.getDueDate();
                 if (!currentInstallment.isAdditional() && 
DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
                     
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
@@ -526,7 +527,7 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
             // New installment will be added (N+1 scenario)
             if (!loanTransactionMapped) {
                 if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
-                    LoanRepaymentScheduleInstallment currentInstallment = 
installments.get(installments.size() - 1);
+                    LoanRepaymentScheduleInstallment currentInstallment = 
installmentToBeProcessed.get(installmentToBeProcessed.size() - 1);
                     
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
                     currentInstallment.addToPrincipal(transactionDate, 
transactionAmount);
                     if (repaidAmount.isGreaterThanZero()) {
@@ -848,7 +849,8 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
     protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> 
charges,
             List<LoanRepaymentScheduleInstallment> installments) {
         if (!CollectionUtils.isEmpty(charges) && 
!CollectionUtils.isEmpty(installments)) {
-            LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment 
= installments.get(installments.size() - 1);
+            LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment 
= installments.stream().filter(i -> !i.isDownPayment())
+                    .reduce((first, second) -> second).orElseThrow();
             LocalDate installmentDueDate = null;
 
             LoanCharge latestCharge = 
getLatestLoanChargeWithSpecificDueDate(charges);
@@ -867,7 +869,6 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
                             BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, 
false, null);
                     installment.markAsAdditional();
                     loan.addLoanRepaymentScheduleInstallment(installment);
-
                 }
             }
         }
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 d4a6a9f42..30d876ef7 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
@@ -22,7 +22,6 @@ 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;
@@ -44,11 +43,14 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.RequiredArgsConstructor;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.NotImplementedException;
@@ -71,6 +73,8 @@ import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
 import 
org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
+import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
+import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
@@ -84,12 +88,15 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 @Slf4j
+@RequiredArgsConstructor
 public class AdvancedPaymentScheduleTransactionProcessor extends 
AbstractLoanRepaymentScheduleTransactionProcessor {
 
     public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = 
"advanced-payment-allocation-strategy";
 
     public final SingleLoanChargeRepaymentScheduleProcessingWrapper 
loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
 
+    private final LoanReAgingParameterRepository reAgingParameterRepository;
+
     @Override
     public String getCode() {
         return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
@@ -144,6 +151,10 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
                 }
             }
         }
+        // Remove re-aged and additional (N+1) installments (if applicable), 
those will be recreated during the
+        // reprocessing
+        installments.removeIf(LoanRepaymentScheduleInstallment::isReAged);
+        installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional);
 
         addChargeOnlyRepaymentInstallmentIfRequired(charges, installments);
 
@@ -185,6 +196,7 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
                     ctx.getOverpaymentHolder());
             case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will 
not be processed.");
             case REAMORTIZE -> handleReAmortization(loanTransaction, 
ctx.getCurrency(), ctx.getInstallments());
+            case REAGE -> handleReAge(loanTransaction, ctx);
             // TODO: Cover rest of the transaction types
             default -> {
                 log.warn("Unhandled transaction processing for transaction 
type: {}", loanTransaction.getTypeOf());
@@ -201,7 +213,7 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
                 .toList();
         List<LoanRepaymentScheduleInstallment> futureInstallments = 
installments.stream() //
                 .filter(installment -> 
installment.getDueDate().isAfter(transactionDate)) //
-                .filter(installment -> !installment.isAdditional() && 
!installment.isDownPayment()) //
+                .filter(installment -> !installment.isAdditional() && 
!installment.isDownPayment() && !installment.isReAged()) //
                 .toList();
 
         BigDecimal overallOverDuePrincipal = ZERO;
@@ -1279,4 +1291,62 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         private Money aggregatedInterestPortion;
         private Money aggregatedPenaltyChargesPortion;
     }
+
+    private void handleReAge(LoanTransaction loanTransaction, TransactionCtx 
ctx) {
+        MonetaryCurrency currency = ctx.getCurrency();
+        List<LoanRepaymentScheduleInstallment> installments = 
ctx.getInstallments();
+        // Either we have the transaction id or we need to fetch it from 
context
+        Long loanTransactionId = loanTransaction.getId() != null ? 
loanTransaction.getId()
+                : 
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
+        LoanReAgeParameter reAgeParameter = 
reAgingParameterRepository.findByLoanTransactionId(loanTransactionId).orElseThrow();
+        AtomicReference<Money> outstandingPrincipalBalance = new 
AtomicReference<>(Money.zero(currency));
+        installments.forEach(i -> {
+            Money principalOutstanding = i.getPrincipalOutstanding(currency);
+            if (principalOutstanding.isGreaterThanZero()) {
+                
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
+                i.addToPrincipal(loanTransaction.getTransactionDate(), 
principalOutstanding.negated());
+            }
+        });
+
+        Money calculatedPrincipal = 
outstandingPrincipalBalance.get().dividedBy(reAgeParameter.getNumberOfInstallments(),
+                MoneyHelper.getRoundingMode());
+        Integer installmentAmountInMultiplesOf = 
loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
+        if (installmentAmountInMultiplesOf != null) {
+            calculatedPrincipal = 
Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
+        }
+        Money adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
+                
.minus(calculatedPrincipal.multipliedBy(reAgeParameter.getNumberOfInstallments()));
+        LoanRepaymentScheduleInstallment lastNormalInstallment = 
installments.stream().filter(i -> !i.isDownPayment())
+                .reduce((first, second) -> second).orElseThrow();
+        LoanRepaymentScheduleInstallment reAgedInstallment = 
LoanRepaymentScheduleInstallment.newReAgedInstallment(
+                lastNormalInstallment.getLoan(), 
lastNormalInstallment.getInstallmentNumber() + 1, 
lastNormalInstallment.getDueDate(),
+                reAgeParameter.getStartDate(), 
calculatedPrincipal.getAmount());
+        installments.add(reAgedInstallment);
+        for (int i = 1; i < reAgeParameter.getNumberOfInstallments(); i++) {
+            LocalDate calculatedDueDate = 
calculateReAgedInstallmentDueDate(reAgeParameter, 
reAgedInstallment.getDueDate());
+            reAgedInstallment = 
LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
+                    reAgedInstallment.getInstallmentNumber() + 1, 
reAgedInstallment.getDueDate(), calculatedDueDate,
+                    calculatedPrincipal.getAmount());
+            installments.add(reAgedInstallment);
+        }
+        reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), 
adjustCalculatedPrincipal);
+
+        reprocessInstallmentsOrder(installments);
+    }
+
+    private void 
reprocessInstallmentsOrder(List<LoanRepaymentScheduleInstallment> installments) 
{
+        AtomicInteger counter = new AtomicInteger(0);
+        
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate)
+                .forEachOrdered(i -> 
i.updateInstallmentNumber(counter.getAndIncrement()));
+    }
+
+    private LocalDate calculateReAgedInstallmentDueDate(LoanReAgeParameter 
reAgeParameter, LocalDate dueDate) {
+        return switch (reAgeParameter.getFrequencyType()) {
+            case DAYS -> dueDate.plusDays(reAgeParameter.getFrequencyNumber());
+            case WEEKS -> 
dueDate.plusWeeks(reAgeParameter.getFrequencyNumber());
+            case MONTHS -> 
dueDate.plusMonths(reAgeParameter.getFrequencyNumber());
+            case YEARS -> 
dueDate.plusYears(reAgeParameter.getFrequencyNumber());
+            default -> throw new 
UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode());
+        };
+    }
 }
diff --git 
a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
 
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
index 530477091..9655538f0 100644
--- 
a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
+++ 
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
@@ -42,4 +42,5 @@
   <include relativeToChangelogFile="true" 
file="parts/1017_add_fee_and_penalty_adjustments_to_loan.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/1018_rename_credited_principal_back_to_credits_amount.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/1019_add_fixed_length.xml"/>
+  <include relativeToChangelogFile="true" 
file="parts/1020_add_re_aged_flag_to_loan_installment.xml"/>
 </databaseChangeLog>
diff --git 
a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml
 
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml
new file mode 100644
index 000000000..df8158a9c
--- /dev/null
+++ 
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+    <changeSet author="fineract" id="1">
+        <addColumn tableName="m_loan_repayment_schedule">
+            <column name="is_re_aged" type="boolean" 
defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
index e42714357..445bfe8d5 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
@@ -285,8 +285,10 @@ final class LoanTransactionsApiResourceSwagger {
         public Long writeoffReasonId;
 
         // command=reAge START
-        @Schema(example = "frequency")
-        public String frequency;
+        @Schema(example = "frequencyType")
+        public String frequencyType;
+        @Schema(example = "frequencyNumber")
+        public Integer frequencyNumber;
         @Schema(example = "startDate")
         public String startDate;
         @Schema(example = "numberOfInstallments")
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
index d2acafeee..7a01ba34c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
@@ -1854,7 +1854,8 @@ public abstract class 
AbstractCumulativeLoanScheduleGenerator implements LoanSch
                 holidayDetailDTO);
         updateMapWithAmount(principalPortionMap, unprocessed, applicableDate);
         installment.addPrincipalAmount(unprocessed);
-        LoanRepaymentScheduleInstallment lastInstallment = 
installments.get(installments.size() - 1);
+        LoanRepaymentScheduleInstallment lastInstallment = 
installments.stream().filter(i -> !i.isDownPayment())
+                .reduce((first, second) -> second).orElseThrow();
         
lastInstallment.updatePrincipal(lastInstallment.getPrincipal(unprocessed.getCurrency()).plus(unprocessed).getAmount());
         lastInstallment.payPrincipalComponent(detail.getTransactionDate(), 
unprocessed);
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
index 98d75ffc7..dd2db46ff 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
@@ -181,6 +181,12 @@ public abstract class 
AbstractProgressiveLoanScheduleGenerator implements LoanSc
             // }
         }
 
+        // If the disbursement happened after maturity date
+        if (loanApplicationTerms.isMultiDisburseLoan()) {
+            processDisbursements(loanApplicationTerms, 
chargesDueAtTimeOfDisbursement, scheduleParams, periods,
+                    DateUtils.getBusinessLocalDate().plusDays(1));
+        }
+
         // determine fees and penalties for charges which depends on total
         // loan interest
         updatePeriodsWithCharges(currency, scheduleParams, periods, 
nonCompoundingCharges, mc);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 42d4ff675..b8dccde4f 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -498,7 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
             }
         }
         if (!changes.isEmpty()) {
-
+            loan.updateLoanScheduleDependentDerivedFields();
             loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
 
             final String noteText = 
command.stringValueOfParameterNamed("note");
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
index 39bf599cd..8b27424af 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
@@ -24,6 +24,7 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 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;
@@ -41,12 +42,18 @@ import 
org.apache.fineract.organisation.monetary.domain.Money;
 import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
 import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
 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.reaging.LoanReAgeParameter;
 import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
+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.apache.fineract.portfolio.note.domain.Note;
+import org.apache.fineract.portfolio.note.domain.NoteRepository;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -61,6 +68,8 @@ public class LoanReAgingServiceImpl {
     private final BusinessEventNotifierService businessEventNotifierService;
     private final LoanTransactionRepository loanTransactionRepository;
     private final LoanReAgingParameterRepository reAgingParameterRepository;
+    private final LoanRepaymentScheduleTransactionProcessorFactory 
loanRepaymentScheduleTransactionProcessorFactory;
+    private final NoteRepository noteRepository;
 
     public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
         Loan loan = loanAssembler.assembleFrom(loanId);
@@ -77,6 +86,15 @@ public class LoanReAgingServiceImpl {
         LoanReAgeParameter reAgeParameter = 
createReAgeParameter(reAgeTransaction, command);
         reAgingParameterRepository.saveAndFlush(reAgeParameter);
 
+        final LoanRepaymentScheduleTransactionProcessor 
loanRepaymentScheduleTransactionProcessor = 
loanRepaymentScheduleTransactionProcessorFactory
+                .determineProcessor(loan.transactionProcessingStrategy());
+        
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
+                new 
LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), 
loan.getRepaymentScheduleInstallments(),
+                        loan.getActiveCharges(), new 
MoneyHolder(loan.getTotalOverpaidAsMoney())));
+
+        loan.updateLoanScheduleDependentDerivedFields();
+        persistNote(loan, command, changes);
+
         // delinquency recalculation will be triggered by the event in a 
decoupled way via a listener
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanReAgeBusinessEvent(loan));
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanReAgeTransactionBusinessEvent(reAgeTransaction));
@@ -91,15 +109,6 @@ public class LoanReAgingServiceImpl {
                 .with(changes).build();
     }
 
-    private LoanReAgeParameter createReAgeParameter(LoanTransaction 
reAgeTransaction, JsonCommand command) {
-        // TODO: these parameters should be checked when the validations are 
implemented
-        PeriodFrequencyType periodFrequencyType = 
command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequency,
-                PeriodFrequencyType.class);
-        LocalDate startDate = 
command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
-        Integer numberOfInstallments = 
command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
-        return new LoanReAgeParameter(reAgeTransaction.getId(), 
periodFrequencyType, startDate, numberOfInstallments);
-    }
-
     public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) 
{
         Loan loan = loanAssembler.assembleFrom(loanId);
         reAgingValidator.validateUndoReAge(loan, command);
@@ -115,6 +124,10 @@ public class LoanReAgingServiceImpl {
         reverseReAgeTransaction(reAgeTransaction, command);
         loanTransactionRepository.saveAndFlush(reAgeTransaction);
 
+        reProcessLoanTransactions(reAgeTransaction.getLoan());
+        loan.updateLoanScheduleDependentDerivedFields();
+        persistNote(loan, command, changes);
+
         // delinquency recalculation will be triggered by the event in a 
decoupled way via a listener
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanUndoReAgeBusinessEvent(loan));
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanUndoReAgeTransactionBusinessEvent(reAgeTransaction));
@@ -156,4 +169,35 @@ public class LoanReAgingServiceImpl {
         return new LoanTransaction(loan, loan.getOffice(), 
LoanTransactionType.REAGE.getValue(), transactionDate, txPrincipalAmount,
                 txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, 
txExternalId);
     }
+
+    private LoanReAgeParameter createReAgeParameter(LoanTransaction 
reAgeTransaction, JsonCommand command) {
+        // TODO: these parameters should be checked when the validations are 
implemented
+        PeriodFrequencyType periodFrequencyType = 
command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequencyType,
+                PeriodFrequencyType.class);
+        LocalDate startDate = 
command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
+        Integer numberOfInstallments = 
command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
+        Integer periodFrequencyNumber = 
command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
+        return new LoanReAgeParameter(reAgeTransaction.getId(), 
periodFrequencyType, periodFrequencyNumber, startDate,
+                numberOfInstallments);
+    }
+
+    private void reProcessLoanTransactions(Loan loan) {
+        final List<LoanTransaction> filteredTransactions = 
loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
+                .filter(t -> t.isChargeOff() || 
!t.isNonMonetaryTransaction()).sorted(LoanTransactionComparator.INSTANCE).toList();
+
+        final LoanRepaymentScheduleTransactionProcessor 
loanRepaymentScheduleTransactionProcessor = 
loanRepaymentScheduleTransactionProcessorFactory
+                .determineProcessor(loan.transactionProcessingStrategy());
+        
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
 filteredTransactions,
+                loan.getCurrency(), loan.getRepaymentScheduleInstallments(), 
loan.getActiveCharges());
+    }
+
+    private void persistNote(Loan loan, JsonCommand command, Map<String, 
Object> changes) {
+        if (command.hasParameter("note")) {
+            final String note = command.stringValueOfParameterNamed("note");
+            final Note newNote = Note.loanNote(loan, note);
+            changes.put("note", note);
+
+            this.noteRepository.saveAndFlush(newNote);
+        }
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
index 68b39629b..905f4f4bd 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
 
 import java.util.List;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
+import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
@@ -103,8 +104,9 @@ public class LoanAccountAutoStarter {
 
     @Bean
     @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
-    public AdvancedPaymentScheduleTransactionProcessor 
advancedPaymentScheduleTransactionProcessor() {
-        return new AdvancedPaymentScheduleTransactionProcessor();
+    public AdvancedPaymentScheduleTransactionProcessor 
advancedPaymentScheduleTransactionProcessor(
+            LoanReAgingParameterRepository reAgingParameterRepository) {
+        return new 
AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
     }
 
 }
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml
index 5539fee94..381165ab9 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml
@@ -71,4 +71,12 @@
             </column>
         </addColumn>
     </changeSet>
+    <changeSet id="4" author="fineract">
+        <addColumn tableName="m_loan_reage_parameter">
+            <column name="frequency_number" type="SMALLINT" 
defaultValueNumeric="1">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+        <renameColumn tableName="m_loan_reage_parameter" 
oldColumnName="frequency" newColumnName="frequency_type" 
columnDataType="VARCHAR(100)"/>
+    </changeSet>
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
index e4ba465b7..24979b4bc 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
@@ -61,6 +61,7 @@ import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
 import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
@@ -89,6 +90,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
     private static final MonetaryCurrency MONETARY_CURRENCY = new 
MonetaryCurrency("USD", 2, 1);
     private static final MockedStatic<MoneyHelper> MONEY_HELPER = 
mockStatic(MoneyHelper.class);
     private AdvancedPaymentScheduleTransactionProcessor underTest;
+    private LoanReAgingParameterRepository reAgingParameterRepository = 
Mockito.mock(LoanReAgingParameterRepository.class);
 
     @BeforeAll
     public static void init() {
@@ -102,7 +104,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
 
     @BeforeEach
     public void setUp() {
-        underTest = new AdvancedPaymentScheduleTransactionProcessor();
+        underTest = new 
AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
 
         ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
         ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 926ce0c98..ccf4d02a1 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -378,11 +378,12 @@ public abstract class BaseLoanIntegrationTest {
         inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
     }
 
-    protected void reAgeLoan(Long loanId, String frequency, String startDate, 
Integer numberOfInstallments) {
+    protected void reAgeLoan(Long loanId, String frequencyType, int 
frequencyNumber, String startDate, Integer numberOfInstallments) {
         PostLoansLoanIdTransactionsRequest request = new 
PostLoansLoanIdTransactionsRequest();
         request.setDateFormat(DATETIME_PATTERN);
         request.setLocale("en");
-        request.setFrequency(frequency);
+        request.setFrequencyType(frequencyType);
+        request.setFrequencyNumber(frequencyNumber);
         request.setStartDate(startDate);
         request.setNumberOfInstallments(numberOfInstallments);
         loanTransactionHelper.reAge(loanId, request);
@@ -798,6 +799,13 @@ public abstract class BaseLoanIntegrationTest {
         assertEquals(paidLate, period.getTotalPaidLateForPeriod());
     }
 
+    protected void checkMaturityDates(long loanId, LocalDate 
expectedMaturityDate, LocalDate actualMaturityDate) {
+        GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+
+        assertEquals(expectedMaturityDate, 
loanDetails.getTimeline().getExpectedMaturityDate());
+        assertEquals(actualMaturityDate, 
loanDetails.getTimeline().getActualMaturityDate());
+    }
+
     @RequiredArgsConstructor
     public static class BatchRequestBuilder {
 
@@ -937,6 +945,7 @@ public abstract class BaseLoanIntegrationTest {
 
         public static final Integer MONTHS = 2;
         public static final String MONTHS_STRING = "MONTHS";
+        public static final String DAYS_STRING = "DAYS";
     }
 
     public static class InterestCalculationPeriodType {
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java
index 7dc67f4a7..656017a6f 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java
@@ -120,9 +120,6 @@ public class 
LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base
                 businessDateHelper.updateBusinessDate(new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
                         .dateFormat(DATETIME_PATTERN).locale("en"));
 
-                // delinquency null next payment date for date after maturity 
date
-                verifyLoanDelinquencyNextPaymentDate(loanId, "", true);
-
             } finally {
                 // reset global config
                 
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec,
 this.responseSpec,
@@ -238,10 +235,6 @@ public class 
LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base
 
                 businessDateHelper.updateBusinessDate(new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
                         .dateFormat(DATETIME_PATTERN).locale("en"));
-
-                // delinquency null next payment date for date after maturity 
date
-                verifyLoanDelinquencyNextPaymentDate(loanId, "", true);
-
             } finally {
                 // reset global config
                 
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec,
 this.responseSpec,
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
index c5a64bfc2..e3bebe228 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
@@ -19,10 +19,15 @@
 package org.apache.fineract.integrationtests.loan.reaging;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.fineract.client.models.PostChargesResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
 import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
@@ -40,13 +45,17 @@ public class LoanReAgingIntegrationTest extends 
BaseLoanIntegrationTest {
             // Create Client
             Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
 
-            int numberOfRepayments = 1;
+            int numberOfRepayments = 3;
             int repaymentEvery = 1;
 
             // Create Loan Product
             PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
 //
                     .numberOfRepayments(numberOfRepayments) //
                     .repaymentEvery(repaymentEvery) //
+                    .installmentAmountInMultiplesOf(null) //
+                    .enableDownPayment(true) //
+                    
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
+                    .enableAutoRepaymentForDownPayment(true) //
                     
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
 
             PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
@@ -74,114 +83,183 @@ public class LoanReAgingIntegrationTest extends 
BaseLoanIntegrationTest {
 
             // verify transactions
             verifyTransactions(loanId, //
-                    transaction(1250.0, "Disbursement", "01 January 2023") //
+                    transaction(1250.0, "Disbursement", "01 January 2023"), //
+                    transaction(312.5, "Down Payment", "01 January 2023") //
             );
 
             // verify schedule
             verifyRepaymentSchedule(loanId, //
-                    installment(0, null, "01 January 2023"), //
-                    installment(1250.0, false, "01 February 2023") //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, true, "01 January 2023"), //
+                    installment(312.5, false, "01 February 2023"), //
+                    installment(312.5, false, "01 March 2023"), //
+                    installment(312.5, false, "01 April 2023") //
             );
-
+            checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), 
LocalDate.of(2023, 4, 1));
             createdLoanId.set(loanId);
         });
 
-        runAt("02 February 2023", () -> {
+        runAt("11 April 2023", () -> {
+
+            long loanId = createdLoanId.get();
+
+            // create charge
+            double chargeAmount = 10.0;
+            PostChargesResponse chargeResult = createCharge(chargeAmount);
+            Long chargeId = chargeResult.getResourceId();
+
+            // add charge after maturity
+            PostLoansLoanIdChargesResponse loanChargeResult = 
addLoanCharge(loanId, chargeId, "11 April 2023", chargeAmount);
+
+            // verify schedule
+            verifyRepaymentSchedule(loanId, //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, true, "01 January 2023"), //
+                    installment(312.5, false, "01 February 2023"), //
+                    installment(312.5, false, "01 March 2023"), //
+                    installment(312.5, false, "01 April 2023"), //
+                    installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") 
//
+            );
+            checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), 
LocalDate.of(2023, 4, 1));
+        });
+
+        runAt("12 April 2023", () -> {
             long loanId = createdLoanId.get();
 
             // create re-age transaction
-            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, "02 
February 2023", 6);
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 
April 2023", 4);
 
             // verify transactions
             verifyTransactions(loanId, //
                     transaction(1250.0, "Disbursement", "01 January 2023"), //
-                    transaction(1250.0, "Re-age", "02 February 2023") //
+                    transaction(312.5, "Down Payment", "01 January 2023"), //
+                    transaction(937.5, "Re-age", "12 April 2023") //
             );
 
-            // TODO: verify installments when schedule generation is 
implemented
+            verifyRepaymentSchedule(loanId, //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, true, "01 January 2023"), //
+                    installment(0, true, "01 February 2023"), //
+                    installment(0, true, "01 March 2023"), //
+                    installment(0, true, "01 April 2023"), //
+                    installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023"), 
//
+                    installment(234.38, false, "12 April 2023"), //
+                    installment(234.38, false, "12 May 2023"), //
+                    installment(234.38, false, "12 June 2023"), //
+                    installment(234.36, false, "12 July 2023") //
+            );
+            checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), 
LocalDate.of(2023, 7, 12));
         });
-    }
 
-    @Test
-    public void test_LoanUndoReAgeTransaction_Works() {
-        AtomicLong createdLoanId = new AtomicLong();
-
-        runAt("01 January 2023", () -> {
-            // Create Client
-            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
-
-            int numberOfRepayments = 1;
-            int repaymentEvery = 1;
-
-            // Create Loan Product
-            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
 //
-                    .numberOfRepayments(numberOfRepayments) //
-                    .repaymentEvery(repaymentEvery) //
-                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
-
-            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
-            Long loanProductId = loanProductResponse.getResourceId();
-
-            // Apply and Approve Loan
-            double amount = 1250.0;
-
-            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)//
-                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
-                    .repaymentEvery(repaymentEvery)//
-                    .loanTermFrequency(numberOfRepayments)//
-                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
-                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+        runAt("13 April 2023", () -> {
+            long loanId = createdLoanId.get();
 
-            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            // create re-age transaction
+            undoReAgeLoan(loanId);
 
-            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
-                    approveLoanRequest(amount, "01 January 2023"));
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(1250.0, "Disbursement", "01 January 2023"), //
+                    transaction(312.5, "Down Payment", "01 January 2023"), //
+                    reversedTransaction(937.5, "Re-age", "12 April 2023") //
+            );
 
-            Long loanId = approvedLoanResult.getLoanId();
+            // verify schedule
+            verifyRepaymentSchedule(loanId, //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, true, "01 January 2023"), //
+                    installment(312.5, false, "01 February 2023"), //
+                    installment(312.5, false, "01 March 2023"), //
+                    installment(312.5, false, "01 April 2023"), //
+                    installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") 
//
+            );
+            checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), 
LocalDate.of(2023, 4, 1));
+        });
+        String repaymentExternalId = UUID.randomUUID().toString();
+        runAt("13 April 2023", () -> {
+            long loanId = createdLoanId.get();
 
-            // disburse Loan
-            disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 
2023");
+            loanTransactionHelper.makeLoanRepayment(loanId, new 
PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
+                    .transactionDate("13 April 
2023").locale("en").transactionAmount(100.0).externalId(repaymentExternalId));
 
             // verify transactions
             verifyTransactions(loanId, //
-                    transaction(1250.0, "Disbursement", "01 January 2023") //
+                    transaction(1250.0, "Disbursement", "01 January 2023"), //
+                    transaction(312.5, "Down Payment", "01 January 2023"), //
+                    reversedTransaction(937.5, "Re-age", "12 April 2023"), //
+                    transaction(100.0, "Repayment", "13 April 2023") //
             );
 
             // verify schedule
             verifyRepaymentSchedule(loanId, //
-                    installment(0, null, "01 January 2023"), //
-                    installment(1250.0, false, "01 February 2023") //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), 
//
+                    installment(312.5, 0, 0, 0, 212.5, false, "01 February 
2023"), //
+                    installment(312.5, 0, 0, 0, 312.5, false, "01 March 
2023"), //
+                    installment(312.5, 0, 0, 0, 312.5, false, "01 April 
2023"), //
+                    installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") 
//
             );
 
-            createdLoanId.set(loanId);
-        });
-
-        runAt("02 February 2023", () -> {
-            long loanId = createdLoanId.get();
-
             // create re-age transaction
-            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, "02 
February 2023", 6);
+            reAgeLoan(loanId, RepaymentFrequencyType.DAYS_STRING, 30, "13 
April 2023", 3);
 
             // verify transactions
             verifyTransactions(loanId, //
                     transaction(1250.0, "Disbursement", "01 January 2023"), //
-                    transaction(1250.0, "Re-age", "02 February 2023") //
+                    transaction(312.5, "Down Payment", "01 January 2023"), //
+                    reversedTransaction(937.5, "Re-age", "12 April 2023"), //
+                    transaction(100.0, "Repayment", "13 April 2023"), //
+                    transaction(837.5, "Re-age", "13 April 2023") //
             );
+
+            // verify schedule
+            verifyRepaymentSchedule(loanId, //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), 
//
+                    installment(100.0, 0, 0, 0, 0.0, true, "01 February 
2023"), //
+                    installment(0, 0, 0, 0, 0.0, true, "01 March 2023"), //
+                    installment(0, 0, 0, 0, 0.0, true, "01 April 2023"), //
+                    installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023"), 
//
+                    installment(279.17, 0, 0, 0, 279.17, false, "13 April 
2023"), //
+                    installment(279.17, 0, 0, 0, 279.17, false, "13 May 
2023"), //
+                    installment(279.16, 0, 0, 0, 279.16, false, "12 June 
2023") //
+            );
+            checkMaturityDates(loanId, LocalDate.of(2023, 6, 12), 
LocalDate.of(2023, 6, 12));
         });
 
-        runAt("03 February 2023", () -> {
+        runAt("14 April 2023", () -> {
             long loanId = createdLoanId.get();
 
-            // create re-age transaction
-            undoReAgeLoan(loanId);
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(100.0), "14 April 2023");
 
             // verify transactions
             verifyTransactions(loanId, //
                     transaction(1250.0, "Disbursement", "01 January 2023"), //
-                    reversedTransaction(1250.0, "Re-age", "02 February 2023") 
//
+                    transaction(312.5, "Down Payment", "01 January 2023"), //
+                    reversedTransaction(937.5, "Re-age", "12 April 2023"), //
+                    transaction(100.0, "Repayment", "13 April 2023"), //
+                    transaction(837.5, "Re-age", "13 April 2023"), //
+                    transaction(100.0, "Disbursement", "14 April 2023"), //
+                    transaction(25.0, "Down Payment", "14 April 2023") //
             );
 
-            // TODO: verify installments when schedule generation is 
implemented
+            // verify schedule
+            verifyRepaymentSchedule(loanId, //
+                    installment(1250, null, "01 January 2023"), //
+                    installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), 
//
+                    installment(100.0, 0, 0, 0, 0.0, true, "01 February 
2023"), //
+                    installment(0, 0, 0, 0, 0.0, true, "01 March 2023"), //
+                    installment(0, 0, 0, 0, 0.0, true, "01 April 2023"), //
+                    installment(0.0, 0.0, 10.0, 0.0, true, "11 April 2023"), //
+                    installment(279.17, 0, 0, 0, 264.17, false, "13 April 
2023"), //
+                    installment(100, null, "14 April 2023"), //
+                    installment(25.0, 0, 0, 0, 25.0, false, "14 April 2023"), 
//
+                    installment(316.67, 0, 0, 0, 316.67, false, "13 May 
2023"), //
+                    installment(316.66, 0, 0, 0, 316.66, false, "12 June 
2023") //
+            );
+            checkMaturityDates(loanId, LocalDate.of(2023, 6, 12), 
LocalDate.of(2023, 6, 12));
         });
     }
+
 }

Reply via email to