diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index b5cd234a344..ce987293654 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3377,3 +3377,21 @@ Feature: LoanRepayment | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 23 August 2024 | 23 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 5.0 | 116.92 | 116.92 | 114.72 | 0.0 | 0.0 | | 2 | 2 | 25 August 2024 | 23 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 7.8 | 7.8 | 7.8 | 7.8 | 0.0 | 0.0 | + + @AdvancedPaymentAllocation @ProgressiveLoanSchedule + Scenario: Progressive loan, undo earlier repayment + Given Global configuration "enable-business-date" is enabled + When Admin sets the business date to "23 June 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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE | 23 June 2024 | 400 | 7.0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "23 June 2024" with "400" amount and expected disbursement date on "23 June 2024" + When Admin successfully disburse the loan on "23 June 2024" with "400" EUR transaction amount + When Admin sets the business date to "24 June 2024" + And Customer makes "AUTOPAY" repayment on "24 June 2024" with 100 EUR transaction amount + When Admin sets the business date to "10 September 2024" + And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "10 September 2024" with 400 EUR transaction amount and self-generated Idempotency key + When Admin makes Credit Balance Refund transaction on "10 September 2024" with 91.21 EUR transaction amount + When Customer undo "1"th "Repayment" transaction made on "24 June 2024" + Then Loan status will be "ACTIVE" 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 a3c45d1b8c3..1ac524f9bfa 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 @@ -2665,12 +2665,6 @@ public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment, final List existingTransactionIds, final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) { - HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); - validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate()); - validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), - holidayDetailDTO.getHolidays()); - validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(), holidayDetailDTO.getWorkingDays(), - holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); ChangedTransactionDetail changedTransactionDetail = null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java index 8d0ae94beef..2b3d81e889b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java @@ -72,6 +72,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; @@ -776,15 +777,34 @@ public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, Lo } } - public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate) { + public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate, final LoanEvent event) { if (!(loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())) { return; } LocalDate lastTransactionDate = loan.getLastUserTransactionDate(); if (DateUtils.isAfter(lastTransactionDate, activityDate)) { - String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date"; - String action = "repayment.or.waiver"; - String postfix = "cannot.be.made.before.last.transaction.date"; + String errorMessage = null; + String action = null; + String postfix = null; + switch (event) { + case LOAN_REPAYMENT_OR_WAIVER -> { + errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date"; + action = "repayment.or.waiver"; + postfix = "cannot.be.made.before.last.transaction.date"; + } + case WRITE_OFF_OUTSTANDING -> { + errorMessage = "The date on which a write off is made cannot be earlier than last transaction date"; + action = "writeoff"; + postfix = "cannot.be.made.before.last.transaction.date"; + } + case LOAN_CHARGE_PAYMENT -> { + errorMessage = "The date on which a charge payment is made cannot be earlier than last transaction date"; + action = "charge.payment"; + postfix = "cannot.be.made.before.last.transaction.date"; + } + default -> { + } + } throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate); } } @@ -831,7 +851,7 @@ public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) { validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created"); validateClientOfficeJoiningDateIsBeforeTransactionDate(loan, transactionDate); - validateActivityNotBeforeLastTransactionDate(loan, transactionDate); + validateActivityNotBeforeLastTransactionDate(loan, transactionDate, LoanEvent.LOAN_REPAYMENT_OR_WAIVER); HolidayDetailDTO holidayDetailDTO = loanUtilService.constructHolidayDTO(loan.getOfficeId(), loan.getDisbursementDate()); validateRepaymentDateIsOnHoliday(transactionDate, holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); validateRepaymentDateIsOnNonWorkingDay(transactionDate, holidayDetailDTO.getWorkingDays(), 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 9a03fdfb514..34ca03940e9 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 @@ -191,6 +191,7 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; @@ -1565,6 +1566,18 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); + HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); + if (loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { + // validate cumulative + loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, transactionToAdjust.getTransactionDate(), + LoanEvent.LOAN_REPAYMENT_OR_WAIVER); + } + // common validations + loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(), + holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(), + holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); + final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId);