FINERACT-2059: Re-aging repayment schedule handling
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 96411c2..56c18c3 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 @@
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 d9d9c93..e5266b1 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 @@
}
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 @@
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 @@
}
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 @@
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 72ffbd7..c1ff292 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 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 @@
@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 @@
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 @@
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 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 1b737fe..a37e32e 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 @@
|| 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 3e33114..4a02782 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 @@
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 78198ea..fcba7a0 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 @@
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 b05c2ae..ba9eccf 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 @@
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 @@
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 @@
// 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 @@
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 @@
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 d4a6a9f..30d876e 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.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.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.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.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 @@
}
}
}
+ // 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 @@
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 @@
.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 @@
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 5304770..9655538 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 0000000..df8158a
--- /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 e427143..445bfe8 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 @@
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 d2acafe..7a01ba3 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 @@
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 98d75ff..dd2db46 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 @@
// }
}
+ // 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 42d4ff6..b8dccde 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 @@
}
}
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 39bf599..8b27424 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.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.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 @@
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 @@
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 @@
.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 @@
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 @@
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 68b3962..905f4f4 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 @@
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 @@
@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 5539fee..381165a 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 e4ba465..24979b4 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.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 @@
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 @@
@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 926ce0c..ccf4d02 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 @@
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 @@
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 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 7dc67f4..656017a 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 @@
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 @@
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 c5a64bf..e3bebe2 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 @@
// 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,102 +83,75 @@
// 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 re-age transaction
- reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, "02 February 2023", 6);
+ // create charge
+ double chargeAmount = 10.0;
+ PostChargesResponse chargeResult = createCharge(chargeAmount);
+ Long chargeId = chargeResult.getResourceId();
- // verify transactions
- verifyTransactions(loanId, //
- transaction(1250.0, "Disbursement", "01 January 2023"), //
- transaction(1250.0, "Re-age", "02 February 2023") //
- );
-
- // TODO: verify installments when schedule generation is implemented
- });
- }
-
- @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);
-
- PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
-
- PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
- approveLoanRequest(amount, "01 January 2023"));
-
- Long loanId = approvedLoanResult.getLoanId();
-
- // disburse Loan
- disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
-
- // verify transactions
- verifyTransactions(loanId, //
- transaction(1250.0, "Disbursement", "01 January 2023") //
- );
+ // add charge after maturity
+ PostLoansLoanIdChargesResponse loanChargeResult = addLoanCharge(loanId, chargeId, "11 April 2023", chargeAmount);
// 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"), //
+ installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") //
);
-
- createdLoanId.set(loanId);
+ checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1));
});
- runAt("02 February 2023", () -> {
+ 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") //
);
+
+ 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));
});
- runAt("03 February 2023", () -> {
+ runAt("13 April 2023", () -> {
long loanId = createdLoanId.get();
// create re-age transaction
@@ -178,10 +160,106 @@
// 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") //
);
- // TODO: verify installments when schedule generation is implemented
+ // 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();
+
+ 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(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(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") //
+ );
+
+ // create re-age transaction
+ reAgeLoan(loanId, RepaymentFrequencyType.DAYS_STRING, 30, "13 April 2023", 3);
+
+ // 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"), //
+ 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("14 April 2023", () -> {
+ long loanId = createdLoanId.get();
+
+ // disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(100.0), "14 April 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"), //
+ 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") //
+ );
+
+ // 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));
});
}
+
}