FINERACT-2059: Re-aging repayment schedule handling
This commit is contained in:
+2
-1
@@ -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";
|
||||
}
|
||||
|
||||
+8
-5
@@ -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());
|
||||
}
|
||||
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+6
-5
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-2
@@ -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());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+30
@@ -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>
|
||||
+4
-2
@@ -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")
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
+6
@@ -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);
|
||||
|
||||
+1
-1
@@ -498,7 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
}
|
||||
if (!changes.isEmpty()) {
|
||||
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
|
||||
+53
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -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>
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+11
-2
@@ -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 {
|
||||
|
||||
-7
@@ -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,
|
||||
|
||||
+150
-72
@@ -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,102 +83,75 @@ 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 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 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user