From d955bc26cb5a476b6b3b14cad337c922f5dcab31 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 18 Mar 2024 14:28:22 +0100 Subject: [PATCH] FINERACT-2059: Re-aging repayment schedule handling --- .../api/LoanReAgingApiConstants.java | 3 +- .../portfolio/loanaccount/domain/Loan.java | 13 +- .../LoanRepaymentScheduleInstallment.java | 42 ++++ .../loanaccount/domain/LoanTransaction.java | 3 +- .../domain/LoanTransactionType.java | 4 +- .../domain/reaging/LoanReAgeParameter.java | 7 +- ...RepaymentScheduleTransactionProcessor.java | 11 +- ...edPaymentScheduleTransactionProcessor.java | 74 +++++- .../module/loan/module-changelog-master.xml | 1 + ...0_add_re_aged_flag_to_loan_installment.xml | 30 +++ .../LoanTransactionsApiResourceSwagger.java | 6 +- ...stractCumulativeLoanScheduleGenerator.java | 3 +- ...tractProgressiveLoanScheduleGenerator.java | 6 + ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- .../reaging/LoanReAgingServiceImpl.java | 62 ++++- .../starter/LoanAccountAutoStarter.java | 6 +- .../parts/0136_loan_reaging_parameters.xml | 8 + ...ymentScheduleTransactionProcessorTest.java | 4 +- .../BaseLoanIntegrationTest.java | 13 +- ...tailsNextPaymentDateConfigurationTest.java | 7 - .../reaging/LoanReAgingIntegrationTest.java | 222 ++++++++++++------ 21 files changed, 413 insertions(+), 114 deletions(-) create mode 100644 fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java index 96411c24b..56c18c33a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java @@ -24,7 +24,8 @@ public interface LoanReAgingApiConstants { String dateFormatParameterName = "dateFormat"; String externalIdParameterName = "externalId"; - String frequency = "frequency"; + String frequencyType = "frequencyType"; + String frequencyNumber = "frequencyNumber"; String startDate = "startDate"; String numberOfInstallments = "numberOfInstallments"; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index d9d9c931c..e5266b169 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -2166,8 +2166,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private LocalDate determineExpectedMaturityDate() { - final int numberOfInstallments = this.repaymentScheduleInstallments.size(); - List installments = getRepaymentScheduleInstallments(); + List installments = getRepaymentScheduleInstallments().stream() + .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); + final int numberOfInstallments = installments.size(); LocalDate maturityDate = installments.get(numberOfInstallments - 1).getDueDate(); ListIterator iterator = installments.listIterator(numberOfInstallments); while (iterator.hasPrevious()) { @@ -3432,7 +3433,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final List repaymentsOrWaivers = new ArrayList<>(); List 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 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 installments = getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1); + LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment + .getLastNonDownPaymentInstallment(installments); reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 72ffbd798..c1ff29222 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -29,6 +29,7 @@ import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDate; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -145,6 +146,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa @Column(name = "is_down_payment", nullable = false) private boolean isDownPayment; + @Column(name = "is_re_aged", nullable = false) + private boolean isReAged; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment") private Set 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 installments) { + return installments.stream().filter(i -> !i.isDownPayment()).reduce((first, second) -> second).orElseThrow(); + } + private BigDecimal defaultToNullIfZero(final BigDecimal value) { BigDecimal result = value; if (BigDecimal.ZERO.compareTo(value) == 0) { @@ -400,6 +434,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa return this.installmentNumber.compareTo(o.installmentNumber); } + public int compareToByDueDate(LoanRepaymentScheduleInstallment o) { + return this.dueDate.compareTo(o.dueDate); + } + public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) { return !isPrincipalCompleted(currency); } @@ -1022,4 +1060,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa public enum PaymentAction { PAY, UNPAY } + + public boolean isReAged() { + return isReAged; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 1b737fe28..a37e32e82 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -865,7 +865,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.CHARGE_OFF.equals(getTypeOf())); + || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) || LoanTransactionType.REAMORTIZE.equals(getTypeOf()) + || LoanTransactionType.REAGE.equals(getTypeOf())); } public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index 3e3311444..4a02782b6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -61,7 +61,9 @@ public enum LoanTransactionType { CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), // CHARGE_OFF(27, "loanTransactionType.chargeOff"), // DOWN_PAYMENT(28, "loanTransactionType.downPayment"), // - REAGE(29, "loanTransactionType.reAge"), REAMORTIZE(30, "loanTransactionType.reAmortize"); + REAGE(29, "loanTransactionType.reAge"), // + REAMORTIZE(30, "loanTransactionType.reAmortize"), // + ; private final Integer value; private final String code; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java index 78198ea71..fcba7a095 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java @@ -40,8 +40,11 @@ public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom { private Long loanTransactionId; @Enumerated(EnumType.STRING) - @Column(name = "frequency", nullable = false) - private PeriodFrequencyType frequency; + @Column(name = "frequency_type", nullable = false) + private PeriodFrequencyType frequencyType; + + @Column(name = "frequency_number", nullable = false) + private Integer frequencyNumber; @Column(name = "start_date", nullable = false) private LocalDate startDate; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index b05c2ae9e..ba9eccf37 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -479,7 +479,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen loanTransaction.resetDerivedComponents(); List transactionMappings = new ArrayList<>(); final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); - installments.sort(byDate); + List 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 charges, List installments) { if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) { - LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.get(installments.size() - 1); + LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.stream().filter(i -> !i.isDownPayment()) + .reduce((first, second) -> second).orElseThrow(); LocalDate installmentDueDate = null; LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges); @@ -867,7 +869,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); installment.markAsAdditional(); loan.addLoanRepaymentScheduleInstallment(installment); - } } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index d4a6a9f42..30d876ef7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -22,7 +22,6 @@ import static java.math.BigDecimal.ZERO; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toList; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK; -import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; @@ -44,11 +43,14 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; @@ -71,6 +73,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -84,12 +88,15 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Slf4j +@RequiredArgsConstructor public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + private final LoanReAgingParameterRepository reAgingParameterRepository; + @Override public String getCode() { return ADVANCED_PAYMENT_ALLOCATION_STRATEGY; @@ -144,6 +151,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } } + // Remove re-aged and additional (N+1) installments (if applicable), those will be recreated during the + // reprocessing + installments.removeIf(LoanRepaymentScheduleInstallment::isReAged); + installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional); addChargeOnlyRepaymentInstallmentIfRequired(charges, installments); @@ -185,6 +196,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep ctx.getOverpaymentHolder()); case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); + case REAGE -> handleReAge(loanTransaction, ctx); // TODO: Cover rest of the transaction types default -> { log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); @@ -201,7 +213,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .toList(); List 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 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 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 installments) { + AtomicInteger counter = new AtomicInteger(0); + installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate) + .forEachOrdered(i -> i.updateInstallmentNumber(counter.getAndIncrement())); + } + + private LocalDate calculateReAgedInstallmentDueDate(LoanReAgeParameter reAgeParameter, LocalDate dueDate) { + return switch (reAgeParameter.getFrequencyType()) { + case DAYS -> dueDate.plusDays(reAgeParameter.getFrequencyNumber()); + case WEEKS -> dueDate.plusWeeks(reAgeParameter.getFrequencyNumber()); + case MONTHS -> dueDate.plusMonths(reAgeParameter.getFrequencyNumber()); + case YEARS -> dueDate.plusYears(reAgeParameter.getFrequencyNumber()); + default -> throw new UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode()); + }; + } } diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index 530477091..9655538f0 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -42,4 +42,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml new file mode 100644 index 000000000..df8158a9c --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1020_add_re_aged_flag_to_loan_installment.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index e42714357..445bfe8d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -285,8 +285,10 @@ final class LoanTransactionsApiResourceSwagger { public Long writeoffReasonId; // command=reAge START - @Schema(example = "frequency") - public String frequency; + @Schema(example = "frequencyType") + public String frequencyType; + @Schema(example = "frequencyNumber") + public Integer frequencyNumber; @Schema(example = "startDate") public String startDate; @Schema(example = "numberOfInstallments") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index d2acafeee..7a01ba34c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -1854,7 +1854,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch holidayDetailDTO); updateMapWithAmount(principalPortionMap, unprocessed, applicableDate); installment.addPrincipalAmount(unprocessed); - LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1); + LoanRepaymentScheduleInstallment lastInstallment = installments.stream().filter(i -> !i.isDownPayment()) + .reduce((first, second) -> second).orElseThrow(); lastInstallment.updatePrincipal(lastInstallment.getPrincipal(unprocessed.getCurrency()).plus(unprocessed).getAmount()); lastInstallment.payPrincipalComponent(detail.getTransactionDate(), unprocessed); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java index 98d75ffc7..dd2db46ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java @@ -181,6 +181,12 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc // } } + // If the disbursement happened after maturity date + if (loanApplicationTerms.isMultiDisburseLoan()) { + processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, + DateUtils.getBusinessLocalDate().plusDays(1)); + } + // determine fees and penalties for charges which depends on total // loan interest updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 42d4ff675..b8dccde4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -498,7 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } if (!changes.isEmpty()) { - + loan.updateLoanScheduleDependentDerivedFields(); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java index 39bf599cd..8b27424af 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java @@ -24,6 +24,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -41,12 +42,18 @@ import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.note.domain.Note; +import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,6 +68,8 @@ public class LoanReAgingServiceImpl { private final BusinessEventNotifierService businessEventNotifierService; private final LoanTransactionRepository loanTransactionRepository; private final LoanReAgingParameterRepository reAgingParameterRepository; + private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final NoteRepository noteRepository; public CommandProcessingResult reAge(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -77,6 +86,15 @@ public class LoanReAgingServiceImpl { LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command); reAgingParameterRepository.saveAndFlush(reAgeParameter); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(loan.transactionProcessingStrategy()); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction, + new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()))); + + loan.updateLoanScheduleDependentDerivedFields(); + persistNote(loan, command, changes); + // delinquency recalculation will be triggered by the event in a decoupled way via a listener businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeTransactionBusinessEvent(reAgeTransaction)); @@ -91,15 +109,6 @@ public class LoanReAgingServiceImpl { .with(changes).build(); } - private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) { - // TODO: these parameters should be checked when the validations are implemented - PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequency, - PeriodFrequencyType.class); - LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate); - Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments); - return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, startDate, numberOfInstallments); - } - public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); reAgingValidator.validateUndoReAge(loan, command); @@ -115,6 +124,10 @@ public class LoanReAgingServiceImpl { reverseReAgeTransaction(reAgeTransaction, command); loanTransactionRepository.saveAndFlush(reAgeTransaction); + reProcessLoanTransactions(reAgeTransaction.getLoan()); + loan.updateLoanScheduleDependentDerivedFields(); + persistNote(loan, command, changes); + // delinquency recalculation will be triggered by the event in a decoupled way via a listener businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeTransactionBusinessEvent(reAgeTransaction)); @@ -156,4 +169,35 @@ public class LoanReAgingServiceImpl { return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE.getValue(), transactionDate, txPrincipalAmount, txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId); } + + private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) { + // TODO: these parameters should be checked when the validations are implemented + PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequencyType, + PeriodFrequencyType.class); + LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate); + Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments); + Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber); + return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, periodFrequencyNumber, startDate, + numberOfInstallments); + } + + private void reProcessLoanTransactions(Loan loan) { + final List 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 changes) { + if (command.hasParameter("note")) { + final String note = command.stringValueOfParameterNamed("note"); + final Note newNote = Note.loanNote(loan, note); + changes.put("note", note); + + this.noteRepository.saveAndFlush(newNote); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 68b39629b..905f4f4bd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter; import java.util.List; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; @@ -103,8 +104,9 @@ public class LoanAccountAutoStarter { @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) - public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() { - return new AdvancedPaymentScheduleTransactionProcessor(); + public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor( + LoanReAgingParameterRepository reAgingParameterRepository) { + return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml index 5539fee94..381165ab9 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0136_loan_reaging_parameters.xml @@ -71,4 +71,12 @@ + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index e4ba465b7..24979b4bc 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -89,6 +90,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1); private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class); private AdvancedPaymentScheduleTransactionProcessor underTest; + private LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class); @BeforeAll public static void init() { @@ -102,7 +104,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { @BeforeEach public void setUp() { - underTest = new AdvancedPaymentScheduleTransactionProcessor(); + underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 926ce0c98..ccf4d02a1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -378,11 +378,12 @@ public abstract class BaseLoanIntegrationTest { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); } - protected void reAgeLoan(Long loanId, String frequency, String startDate, Integer numberOfInstallments) { + protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments) { PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest(); request.setDateFormat(DATETIME_PATTERN); request.setLocale("en"); - request.setFrequency(frequency); + request.setFrequencyType(frequencyType); + request.setFrequencyNumber(frequencyNumber); request.setStartDate(startDate); request.setNumberOfInstallments(numberOfInstallments); loanTransactionHelper.reAge(loanId, request); @@ -798,6 +799,13 @@ public abstract class BaseLoanIntegrationTest { assertEquals(paidLate, period.getTotalPaidLateForPeriod()); } + protected void checkMaturityDates(long loanId, LocalDate expectedMaturityDate, LocalDate actualMaturityDate) { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + assertEquals(expectedMaturityDate, loanDetails.getTimeline().getExpectedMaturityDate()); + assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate()); + } + @RequiredArgsConstructor public static class BatchRequestBuilder { @@ -937,6 +945,7 @@ public abstract class BaseLoanIntegrationTest { public static final Integer MONTHS = 2; public static final String MONTHS_STRING = "MONTHS"; + public static final String DAYS_STRING = "DAYS"; } public static class InterestCalculationPeriodType { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java index 7dc67f4a7..656017a6f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDelinquencyDetailsNextPaymentDateConfigurationTest.java @@ -120,9 +120,6 @@ public class LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023") .dateFormat(DATETIME_PATTERN).locale("en")); - // delinquency null next payment date for date after maturity date - verifyLoanDelinquencyNextPaymentDate(loanId, "", true); - } finally { // reset global config GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec, @@ -238,10 +235,6 @@ public class LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023") .dateFormat(DATETIME_PATTERN).locale("en")); - - // delinquency null next payment date for date after maturity date - verifyLoanDelinquencyNextPaymentDate(loanId, "", true); - } finally { // reset global config GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java index c5a64bfc2..e3bebe228 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java @@ -19,10 +19,15 @@ package org.apache.fineract.integrationtests.loan.reaging; import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; +import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; @@ -40,13 +45,17 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); - int numberOfRepayments = 1; + int numberOfRepayments = 3; int repaymentEvery = 1; // Create Loan Product PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() // .numberOfRepayments(numberOfRepayments) // .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .enableDownPayment(true) // + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) // + .enableAutoRepaymentForDownPayment(true) // .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); // PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); @@ -74,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)); }); } + }