diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java index 8973b74be63..667c13ecd80 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java @@ -66,9 +66,10 @@ private RescheduleLoansApiConstants() { // approve action request parameters public static final String approvedOnDateParam = "approvedOnDate"; + public static final String waiveOverdueChargesParamName = "waiveOverdueCharges"; - public static final Set APPROVE_REQUEST_DATA_PARAMETERS = Collections - .unmodifiableSet(new HashSet<>(Arrays.asList(localeParamName, dateFormatParamName, approvedOnDateParam))); + public static final Set APPROVE_REQUEST_DATA_PARAMETERS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(localeParamName, dateFormatParamName, approvedOnDateParam, waiveOverdueChargesParamName))); public static final Set commandParams = Collections.unmodifiableSet( new HashSet<>(Arrays.asList(allCommandParamName, approveCommandParamName, pendingCommandParamName, rejectCommandParamName))); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResourceSwagger.java index cf704ba6998..4e2ecc67db2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResourceSwagger.java @@ -213,6 +213,8 @@ public static final class PostUpdateRescheduleLoansRequest { public String locale; @Schema(example = "dd MMMM yyyy") public String dateFormat; + @Schema(example = "true") + public Boolean waiveOverdueCharges; } @Schema(description = "PostCreateRescheduleLoansResponse ") diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java index 9613a8bf6df..d8a015a6f37 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java @@ -69,7 +69,7 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq RescheduleLoansApiConstants.rejectedOnDateParam)); public static final Set APPROVE_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName, - RescheduleLoansApiConstants.approvedOnDateParam)); + RescheduleLoansApiConstants.approvedOnDateParam, RescheduleLoansApiConstants.waiveOverdueChargesParamName)); private final FromJsonHelper fromJsonHelper; @Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl") private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate; @@ -227,11 +227,21 @@ public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBu public static void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan, final LoanRepaymentScheduleInstallment installment) { + validateForOverdueCharges(dataValidatorBuilder, loan, installment, false); + } + + public static void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan, + final LoanRepaymentScheduleInstallment installment, final boolean waiveOverdueCharges) { + if (waiveOverdueCharges) { + return; + } if (installment != null) { LocalDate rescheduleFromDate = installment.getFromDate(); Collection charges = loan.getLoanCharges(); for (LoanCharge loanCharge : charges) { - if (loanCharge.isOverdueInstallmentCharge() && DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate)) { + if (loanCharge.isActive() && loanCharge.isOverdueInstallmentCharge() + && DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate) + && loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { dataValidatorBuilder.failWithCodeNoParameterAddedToErrorCode("not.allowed.due.to.overdue.charges"); break; } @@ -278,7 +288,7 @@ public void validateForCreateAction(final JsonCommand jsonCommand, final Loan lo LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate); validateReschedulingInstallment(dataValidatorBuilder, installment); - validateForOverdueCharges(dataValidatorBuilder, loan, installment); + validateForOverdueCharges(dataValidatorBuilder, loan, installment, false); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); @@ -321,15 +331,18 @@ public void validateForApproveAction(final JsonCommand jsonCommand, LoanReschedu validateApprovalDate(fromJsonHelper, loanRescheduleRequest, jsonElement, dataValidatorBuilder); validateRescheduleRequestStatus(loanRescheduleRequest, dataValidatorBuilder); + final Boolean waiveOverdueCharges = fromJsonHelper.extractBooleanNamed(RescheduleLoansApiConstants.waiveOverdueChargesParamName, + jsonElement); + dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.waiveOverdueChargesParamName).value(waiveOverdueCharges) + .ignoreIfNull().validateForBooleanValue(); + LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate(); final Loan loan = loanRescheduleRequest.getLoan(); - LoanRepaymentScheduleInstallment installment; - validateLoanIsActive(loan, dataValidatorBuilder); - installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate); + LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate); validateReschedulingInstallment(dataValidatorBuilder, installment); - validateForOverdueCharges(dataValidatorBuilder, loan, installment); + validateForOverdueCharges(dataValidatorBuilder, loan, installment, Boolean.TRUE.equals(waiveOverdueCharges)); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index bff1713d735..f4a33760b00 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -50,6 +50,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; @@ -78,6 +79,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; @@ -112,6 +114,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche private final BusinessEventNotifierService businessEventNotifierService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanChargeService loanChargeService; + private final LoanChargeWritePlatformService loanChargeWritePlatformService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanTermVariationsMapper loanTermVariationsMapper; private final LoanScheduleComponent loanSchedule; @@ -420,6 +423,19 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { final MathContext mathContext = MoneyHelper.getMathContext(); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.loanRepaymentScheduleTransactionProcessorFactory .determineProcessor(loan.transactionProcessingStrategy()); + + final Boolean waiveOverdueCharges = jsonCommand + .booleanObjectValueOfParameterNamed(RescheduleLoansApiConstants.waiveOverdueChargesParamName); + + if (Boolean.TRUE.equals(waiveOverdueCharges)) { + for (final LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.isActive() && loanCharge.isOverdueInstallmentCharge() + && loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { + this.loanChargeWritePlatformService.waiveLoanCharge(loan.getId(), loanCharge.getId(), jsonCommand); + } + } + } + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), loanApplicationTerms.getInterestMethod()); final LoanScheduleDTO loanScheduleDTO = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java index 93da039e9ca..21a5979f7c6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java @@ -564,4 +564,63 @@ private Long applyForLoanApplicationWithInterest(final Long clientId, final Long Long loanId = loanTransactionHelper.applyLoan(loanRequest).getLoanId(); return loanId; } + + @Test + public void testApproveLoanRescheduleRequestWithWaiveOverdueCharges() { + approveLoanRescheduleRequestWithOverdueCharges(true); + } + + @Test + public void testApproveLoanRescheduleRequestWithoutWaiveOverdueCharges() { + approveLoanRescheduleRequestWithOverdueCharges(false); + } + + private void approveLoanRescheduleRequestWithOverdueCharges(final boolean waiveOverdueCharges) { + final AtomicReference loanIdRef = new AtomicReference<>(); + + runAt("01 January 2025", () -> { + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + final Long productId = createLoanProductPeriodicWithInterest(); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), productId.intValue(), + BigDecimal.valueOf(1000.0), 4, 1, 4, BigDecimal.valueOf(2.0), "01 January 2025", "01 January 2025"); + final Long loanId = loanResponse.getLoanId(); + loanIdRef.set(loanId); + + loanTransactionHelper.approveLoan("01 January 2025", loanId.intValue()); + loanTransactionHelper.disburseLoan("01 January 2025", loanId.intValue(), "1000", null); + }); + + runAt("15 February 2025", () -> { + final Long loanId = loanIdRef.get(); + final Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanOverdueFeeJSON()); + loanTransactionHelper.addLoanCharge(loanId, new PostLoansLoanIdChargesRequest().chargeId(chargeId.longValue()) + .dueDate("01 February 2025").amount(Double.valueOf("10.0")).dateFormat(DATETIME_PATTERN).locale("en")); + + final PostCreateRescheduleLoansResponse rescheduleResponse = loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanId).rescheduleFromDate("01 March 2025") + .rescheduleReasonId(1L).submittedOnDate("15 February 2025").locale("en").dateFormat(DATETIME_PATTERN)); + final Long rescheduleRequestId = rescheduleResponse.getResourceId(); + + final PostUpdateRescheduleLoansRequest approveRequest = new PostUpdateRescheduleLoansRequest() + .approvedOnDate("15 February 2025").locale("en").dateFormat(DATETIME_PATTERN).waiveOverdueCharges(waiveOverdueCharges); + + if (waiveOverdueCharges) { + loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleRequestId, approveRequest); + + final GetLoanRescheduleRequestResponse getRescheduleResponse = loanRescheduleRequestHelper + .readLoanRescheduleRequest(rescheduleRequestId, null); + assertTrue(getRescheduleResponse.getStatus().getApproved()); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); + boolean chargeWaived = loanDetails.getCharges().stream() + .filter(c -> c.getIsPenalty() && c.getAmountOutstanding().compareTo(BigDecimal.ZERO) == 0 && c.getWaived()) + .findAny().isPresent(); + assertTrue(chargeWaived); + } else { + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleRequestId, approveRequest)); + assertTrue(exception.getMessage().contains("not.allowed.due.to.overdue.charges")); + } + }); + } }