From d50906bef28e29e3dd31c307fdcda8ff99247031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Thu, 11 Sep 2025 14:25:24 +0200 Subject: [PATCH] FINERACT-2384: Incorrect reage transaction date and repayment schedule --- .../core/data/DataValidatorBuilder.java | 19 +++ .../resources/features/LoanReAging.feature | 56 ++++++ ...edPaymentScheduleTransactionProcessor.java | 159 ++++++++++-------- .../reaging/LoanReAgingServiceImpl.java | 22 ++- .../service/reaging/LoanReAgingValidator.java | 14 +- 5 files changed, 186 insertions(+), 84 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java index 253b4a857..c8745d7a3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java @@ -1017,6 +1017,25 @@ public class DataValidatorBuilder { return this; } + public DataValidatorBuilder validateDateAfterOrEqual(final LocalDate date) { + if (this.value == null && this.ignoreNullValue) { + return this; + } + + if (this.value != null && date != null) { + final LocalDate dateVal = (LocalDate) this.value; + if (DateUtils.isBefore(dateVal, date)) { + String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".is.before.than.date"; + String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than or equal to the provided date: " + + date; + final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter, + dateVal, date); + this.dataValidationErrors.add(error); + } + } + return this; + } + public DataValidatorBuilder validateDateBeforeOrEqual(final LocalDate date) { if (this.value == null && this.ignoreNullValue) { return this; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 045565774..70f8ae51d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -2139,3 +2139,59 @@ Feature: LoanReAging Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 03 May 2025 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | + Scenario: Verify that Loan re-aging transaction - can be performed before maturity date and merges the corresponding normal installments + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + When Admin sets the business date to "09 March 2024" + When Admin creates a Loan re-aging transaction by Loan external ID with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 15 March 2024 | 6 | + + Scenario: Verify that Loan re-aging transaction - can be performed before maturity date and removes additional normal installments + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + When Admin sets the business date to "09 March 2024" + When Admin creates a Loan re-aging transaction by Loan external ID with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 15 March 2024 | 1 | + + Scenario: Verify that Loan re-aging transaction - can be performed before maturity date and removes n+1 + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + When Admin adds "LOAN_NSF_FEE" due date charge with "1 October 2024" due date and 20 EUR transaction amount + + When Admin sets the business date to "09 March 2024" + When Admin creates a Loan re-aging transaction by Loan external ID with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 15 March 2024 | 10 | + + Scenario: Verify that Loan re-aging transaction - can be performed before maturity date and removes additional normal installments and not modifies n+1 + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + When Admin adds "LOAN_NSF_FEE" due date charge with "1 October 2024" due date and 20 EUR transaction amount + When Admin adds "LOAN_NSF_FEE" due date charge with "1 November 2024" due date and 30 EUR transaction amount + + When Admin sets the business date to "09 March 2024" + When Admin creates a Loan re-aging transaction by Loan external ID with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 15 March 2024 | 2 | diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index a9a9bd1ca..4afa5e881 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -2842,23 +2842,58 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private Money aggregatedPenaltyChargesPortion; } + private void mergeReAgedInstallment(final LoanRepaymentScheduleInstallment target, + final LoanRepaymentScheduleInstallment reAgedInstallment, MonetaryCurrency currency, LocalDate transactionDate) { + target.setAdditional(false); + target.setReAged(true); + target.setFromDate(reAgedInstallment.getFromDate()); + target.setDueDate(reAgedInstallment.getDueDate()); + target.setPrincipal(reAgedInstallment.getPrincipal().add(MathUtil.nullToZero(target.getPrincipalCompleted()))); + target.setInterestCharged(MathUtil.add(reAgedInstallment.getInterestCharged(), target.getInterestPaid())); + target.updateObligationsMet(currency, transactionDate); + } + + private void insertOrReplaceRelatedInstallment(List installments, + final LoanRepaymentScheduleInstallment reAgedInstallment, final MonetaryCurrency currency, final LocalDate transactionDate) { + Optional first = installments.stream() + .filter(installment -> Objects.equals(installment.getInstallmentNumber(), reAgedInstallment.getInstallmentNumber())) + .findFirst(); + + if (first.isPresent()) { + int indexOfReplaceInstallment = installments.indexOf(first.get()); + LoanRepaymentScheduleInstallment target = installments.get(indexOfReplaceInstallment); + + if (target.isAdditional()) { + // additional ( N+1 ) installment due date cannot be earlier than its original due date + if (!target.getDueDate().isAfter(reAgedInstallment.getDueDate())) { + mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate); + } else { + installments.add(reAgedInstallment); + reAgedInstallment.updateObligationsMet(currency, transactionDate); + installments.stream() + .filter(i -> i.getInstallmentNumber() != null && i.getInstallmentNumber() >= target.getInstallmentNumber()) + .forEach(i -> i.setInstallmentNumber(i.getInstallmentNumber() + 1)); + } + } else { + mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate); + } + } else { + installments.add(reAgedInstallment); + reAgedInstallment.updateObligationsMet(currency, transactionDate); + } + } + private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) { loanTransaction.resetDerivedComponents(); MonetaryCurrency currency = ctx.getCurrency(); List installments = ctx.getInstallments(); - final LocalDate startDate = loanTransaction.getLoanReAgeParameter().getStartDate(); - final LocalDate endDate = calculateReAgedInstallmentEndDate(loanTransaction.getLoanReAgeParameter()); - final AtomicReference outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); + AtomicReference outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); installments.forEach(i -> { - final boolean shouldInclude = !i.isAdditional() || i.getDueDate().isBefore(startDate) - || (!i.getDueDate().isBefore(startDate) && i.getDueDate().isBefore(endDate)); - if (shouldInclude) { - Money principalOutstanding = i.getPrincipalOutstanding(currency); - if (principalOutstanding.isGreaterThanZero()) { - outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); - i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); - } + Money principalOutstanding = i.getPrincipalOutstanding(currency); + if (principalOutstanding.isGreaterThanZero()) { + outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); + i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); } }); @@ -2878,27 +2913,45 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep adjustCalculatedPrincipal = outstandingPrincipalBalance.get() .minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())); } - final LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream() // - .filter(i -> !i.isDownPayment() && i.getDueDate().isBefore(loanTransaction.getTransactionDate())) // - .reduce((first, second) -> second) // - .orElseThrow(); - LoanRepaymentScheduleInstallment reAgedInstallment = createOrConvertReAgedInstallment(installments, startDate, - loanTransaction.getLoanReAgeParameter(), lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, - lastNormalInstallment.getDueDate(), calculatedPrincipal.getAmount()); - reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate()); + Optional lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment()) + .filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second); + + int reAgedInstallmentNumber; + LocalDate fromDate; + Loan loan; + if (lastNormalInstallmentOptional.isEmpty()) { + LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment()) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow(); + reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber(); + fromDate = firstNormalInstallment.getFromDate(); + loan = firstNormalInstallment.getLoan(); + } else { + LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get(); + reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1; + fromDate = lastNormalInstallment.getDueDate(); + loan = lastNormalInstallment.getLoan(); + } + + LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, + reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount()); + insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { - final LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), + LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), reAgedInstallment.getDueDate()); - - reAgedInstallment = createOrConvertReAgedInstallment(installments, calculatedDueDate, loanTransaction.getLoanReAgeParameter(), - reAgedInstallment.getLoan(), reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), - calculatedPrincipal.getAmount()); - reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate()); + int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1; + reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), + nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount()); + if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) { + reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); + } + insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); } - reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); - + int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber(); + List toRemove = installments.stream().filter(i -> i != null && !i.isAdditional() + && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList(); + toRemove.forEach(installments::remove); reprocessInstallments(installments); } @@ -2915,57 +2968,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep }); } - private LoanRepaymentScheduleInstallment convertAdditionalToReAged(final LoanRepaymentScheduleInstallment installment, - final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principalAmount) { - installment.setAdditional(false); - installment.setReAged(true); - installment.updateFromDate(fromDate); - installment.updateDueDate(dueDate); - installment.updatePrincipal(principalAmount); - return installment; - } - - private Optional findAdditionalForFrequency(final List installments, - final LocalDate targetDueDate, final LoanReAgeParameter reAgeParameter) { - final LocalDate nextReAgedDate = calculateReAgedInstallmentDueDate(reAgeParameter, targetDueDate); - - return installments.stream() // - .filter(LoanRepaymentScheduleInstallment::isAdditional) // - .filter(ai -> !ai.getDueDate().isBefore(targetDueDate) && ai.getDueDate().isBefore(nextReAgedDate)).findAny(); - } - - private LoanRepaymentScheduleInstallment createOrConvertReAgedInstallment(final List installments, - final LocalDate targetDueDate, final LoanReAgeParameter reAgeParameter, final Loan loan, final int installmentNumber, - final LocalDate fromDate, final BigDecimal principalAmount) { - final Optional additionalInstallment = findAdditionalForFrequency(installments, targetDueDate, - reAgeParameter); - - if (additionalInstallment.isPresent()) { - return convertAdditionalToReAged(additionalInstallment.get(), fromDate, targetDueDate, principalAmount); - } else { - final LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, - installmentNumber, fromDate, targetDueDate, principalAmount); - installments.add(reAgedInstallment); - return reAgedInstallment; - } - } - - private LocalDate calculateReAgedInstallmentEndDate(final LoanReAgeParameter reAgeParameter) { - return calculateReAgedNextDate(reAgeParameter.getFrequencyType(), reAgeParameter.getStartDate(), - reAgeParameter.getFrequencyNumber(), reAgeParameter.getNumberOfInstallments()); - } - private LocalDate calculateReAgedInstallmentDueDate(final LoanReAgeParameter reAgeParameter, final LocalDate dueDate) { - return calculateReAgedNextDate(reAgeParameter.getFrequencyType(), dueDate, reAgeParameter.getFrequencyNumber(), 1); + return calculateReAgedNextDate(reAgeParameter.getFrequencyType(), dueDate, reAgeParameter.getFrequencyNumber()); } private LocalDate calculateReAgedNextDate(final PeriodFrequencyType frequencyType, final LocalDate dueDate, - final Integer frequencyNumber, final Integer numberOfInstallments) { + final Integer frequencyNumber) { return switch (frequencyType) { - case DAYS -> dueDate.plusDays((long) frequencyNumber * numberOfInstallments); - case WEEKS -> dueDate.plusWeeks((long) frequencyNumber * numberOfInstallments); - case MONTHS -> dueDate.plusMonths((long) frequencyNumber * numberOfInstallments); - case YEARS -> dueDate.plusYears((long) frequencyNumber * numberOfInstallments); + case DAYS -> dueDate.plusDays((long) frequencyNumber); + case WEEKS -> dueDate.plusWeeks((long) frequencyNumber); + case MONTHS -> dueDate.plusMonths((long) frequencyNumber); + case YEARS -> dueDate.plusYears((long) frequencyNumber); default -> throw new UnsupportedOperationException(); }; } 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 5cba79573..ab5b9f0e7 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 @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.Loa import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.event.business.service.TransactionHelper; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants; @@ -77,6 +78,7 @@ public class LoanReAgingServiceImpl { private final LoanUtilService loanUtilService; private final LoanScheduleService loanScheduleService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; + private final TransactionHelper transactionHelper; public CommandProcessingResult reAge(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -89,14 +91,18 @@ public class LoanReAgingServiceImpl { LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command); LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command); reAgeTransaction.setLoanReAgeParameter(reAgeParameter); - loanTransactionRepository.saveAndFlush(reAgeTransaction); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.transactionProcessingStrategy()); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction, new TransactionCtx(loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())) { + reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan, reAgeTransaction.getTransactionDate()); + } else { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(loan.transactionProcessingStrategy()); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + } loan.updateLoanScheduleDependentDerivedFields(); persistNote(loan, command, changes); @@ -172,7 +178,10 @@ public class LoanReAgingServiceImpl { // reaging transaction date is always the current business date LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - + LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate); + if (transactionDate.isAfter(startDate)) { + transactionDate = startDate; + } // in case of a reaging transaction, only the outstanding principal amount until the business date is considered Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate); BigDecimal txPrincipalAmount = txPrincipal.getAmount(); @@ -182,7 +191,6 @@ public class LoanReAgingServiceImpl { } 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java index 74fa9ac4a..dfefabf28 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java @@ -62,9 +62,13 @@ public class LoanReAgingValidator { .notExceedingLengthOf(100); LocalDate startDate = command.localDateValueOfParameterNamed(LoanReAgingApiConstants.startDate); - baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull() - .validateDateAfter(loan.getMaturityDate()); - + if (loan.isProgressiveSchedule() && !loan.isInterestBearing()) { + baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull() + .validateDateAfterOrEqual(loan.getDisbursementDate()); + } else { + baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull() + .validateDateAfter(loan.getMaturityDate()); + } String frequencyType = command.stringValueOfParameterNamedAllowingNull(LoanReAgingApiConstants.frequencyType); baseDataValidator.reset().parameter(LoanReAgingApiConstants.frequencyType).value(frequencyType).notNull(); @@ -81,7 +85,9 @@ public class LoanReAgingValidator { private void validateReAgeBusinessRules(Loan loan) { // validate reaging shouldn't happen before maturity - if (DateUtils.isBefore(getBusinessLocalDate(), loan.getMaturityDate())) { + // on progressive loans it can + if (!(loan.isProgressiveSchedule() && !loan.isInterestBearing()) + && DateUtils.isBefore(getBusinessLocalDate(), loan.getMaturityDate())) { throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.cannot.be.submitted.before.maturity", "Loan cannot be re-aged before maturity", loan.getId()); }