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