diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 4a2837084..1ffc5265e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3614,4 +3614,12 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId + "/transactions?command=charge-off"; return this; } + + public CommandWrapperBuilder undoChargeOff(final Long loanId) { + this.actionName = "UNDOCHARGEOFF"; + this.entityName = "LOAN"; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions?command=undo-charge-off"; + return this; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanUndoChargeOffBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanUndoChargeOffBusinessEvent.java new file mode 100644 index 000000000..bd65fef31 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanUndoChargeOffBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.loan.transaction; + +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanUndoChargeOffBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanUndoChargeOffBusinessEvent"; + + public LoanUndoChargeOffBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index f7bdb4c8a..0aeaddc6e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -77,6 +77,7 @@ import org.springframework.stereotype.Component; public class LoanTransactionsApiResource { public static final String CHARGE_OFF_COMMAND_VALUE = "charge-off"; + public static final String UNDO_CHARGE_OFF_COMMAND_VALUE = "undo-charge-off"; private final Set responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId", LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME)); @@ -463,6 +464,8 @@ public class LoanTransactionsApiResource { commandRequest = builder.creditBalanceRefund(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, CHARGE_OFF_COMMAND_VALUE)) { commandRequest = builder.chargeOff(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, UNDO_CHARGE_OFF_COMMAND_VALUE)) { + commandRequest = builder.undoChargeOff(resolvedLoanId).build(); } if (commandRequest == null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 9e589df8b..2922a4f5b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -7027,10 +7027,29 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return getLoanTransactions().stream().filter(predicate).toList(); } + public LoanTransaction findChargedOffTransaction() { + return getLoanTransactions().stream() // + .filter(LoanTransaction::isNotReversed) // + .filter(LoanTransaction::isChargeOff) // + .findFirst() // + .orElse(null); + } + public void handleMaturityDateActivate() { if (this.expectedMaturityDate != null && this.actualMaturityDate == null) { this.actualMaturityDate = this.expectedMaturityDate; } } + public LoanTransaction getLastUserTransaction() { + return getLoanTransactions().stream() // + .filter(LoanTransaction::isNotReversed) // + .filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) // + .reduce((first, second) -> second) // + .orElse(null); + } + + public LocalDate getChargedOffOnDate() { + return chargedOffOnDate; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 7fadb8e01..c402fc1a9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -161,7 +161,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final boolean isRecoveryRepayment, final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { checkClientOrGroupActive(loan); - + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan); businessEventNotifierService.notifyPreBusinessEvent(repaymentEvent); @@ -357,6 +362,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final Integer transactionType, Integer installmentNumber) { boolean isAccountTransfer = true; checkClientOrGroupActive(loan); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanChargePaymentPreBusinessEvent(loan)); final List existingTransactionIds = new ArrayList<>(); @@ -436,6 +447,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { boolean isAccountTransfer = true; final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanRefundPreBusinessEvent(loan)); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -482,6 +499,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final PaymentDetail paymentDetail, final String noteText, final ExternalId txnExternalId, final boolean isLoanToLoanTransfer) { final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } boolean isAccountTransfer = true; final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -508,6 +531,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override public void reverseTransfer(final LoanTransaction loanTransaction) { + if (loanTransaction.getLoan().isChargedOff() + && loanTransaction.getTransactionDate().isBefore(loanTransaction.getLoan().getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", + "Loan transaction: " + loanTransaction.getId() + + " reversal is not allowed before or on the date when the loan got charged-off", + loanTransaction.getId()); + } loanTransaction.reverse(); saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); } @@ -633,6 +663,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate transactionDate, final BigDecimal transactionAmount, final String noteText, final ExternalId externalId, PaymentDetail paymentDetail) { + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanCreditBalanceRefundPreBusinessEvent(loan)); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -670,6 +707,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final List existingReversedTransactionIds = new ArrayList<>(); final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail, transactionDate, txnExternalId); final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); @@ -702,6 +745,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override public LoanTransaction foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId, Map changes) { + if (loan.isChargedOff() && foreClosureDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loan.getId() + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loan.getId()); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanForeClosurePreBusinessEvent(loan)); MonetaryCurrency currency = loan.getCurrency(); List newTransactions = new ArrayList<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoChargeOffLoanCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoChargeOffLoanCommandHandler.java new file mode 100644 index 000000000..90e324bc0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UndoChargeOffLoanCommandHandler.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "UNDOCHARGEOFF") +public class UndoChargeOffLoanCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return writePlatformService.undoChargeOff(command); + } +} 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 4da3fd478..4a870cea7 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 @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -131,6 +132,11 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche // use the loan id to get a Loan entity object final Loan loan = this.loanAssembler.assembleFrom(loanId); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Loan: " + loanId + " reschedule installment is not allowed. Loan Account is Charged-off", loanId); + } + // validate the request in the JsonCommand object passed as // parameter this.loanRescheduleRequestDataValidator.validateForCreateAction(jsonCommand, loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index bd4fbb614..738094a48 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -41,6 +42,7 @@ import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -131,6 +133,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatformService { @@ -173,7 +176,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); - + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Adding charge to Loan: " + loanId + " is not allowed. Loan Account is Charged-off", loanId); + } List loanDisburseDetails = loan.getDisbursementDetails(); final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); @@ -360,7 +366,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo throw new LoanChargeWaiveCannotBeReversedException( LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); } - + if (loan.isChargedOff() && (loanTransaction.getTransactionDate().isBefore(loan.getChargedOffOnDate()) + || loanTransaction.getTransactionDate().isEqual(loan.getChargedOffOnDate()))) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", + "Undo Loan transaction: " + loanTransaction.getId() + + " is not allowed before or on the date when the loan got charged-off", + loanTransaction.getId()); + } final Map changes = new LinkedHashMap<>(); businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); @@ -713,6 +725,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo public void applyOverdueChargesForLoan(final Long loanId, Collection overdueLoanScheduleDataList) { Loan loan = this.loanAssembler.assembleFrom(loanId); + if (loan.isChargedOff()) { + log.warn("Adding charge to Loan: {} is not allowed. Loan Account is Charged-off", loanId); + return; + } final List existingTransactionIds = loan.findExistingTransactionIds(); final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); boolean runInterestRecalculation = false; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index 41be2c0ac..4b09cd2b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -112,4 +112,7 @@ public interface LoanWritePlatformService { CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command); CommandProcessingResult chargeOff(JsonCommand command); + + @Transactional + CommandProcessingResult undoChargeOff(JsonCommand command); } 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 3879f0615..839b826b8 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 @@ -80,6 +80,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdra import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWrittenOffPostBusinessEvent; @@ -286,6 +287,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Loan loan = this.loanAssembler.assembleFrom(loanId); // Fail fast if client/group is not active or actual loan status disallows disbursal checkClientOrGroupActive(loan); + + final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + + if (loan.isChargedOff() && actualDisbursementDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loanId + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loanId); + } + if (loan.loanProduct().isDisallowExpectedDisbursements()) { List filteredList = loan.getDisbursementDetails().stream() .filter(disbursementDetails -> disbursementDetails.actualDisbursementDate() == null).toList(); @@ -326,8 +337,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } - final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); - // validate ActualDisbursement Date Against Expected Disbursement Date LoanProduct loanProduct = loan.loanProduct(); if (loanProduct.syncExpectedWithDisbursementDate()) { @@ -805,6 +814,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Undo Loan: " + loanId + " disbursement is not allowed. Loan Account is Charged-off", loanId); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan)); removeLoanCycle(loan); final List existingTransactionIds = new ArrayList<>(); @@ -1057,6 +1070,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new PlatformServiceUnavailableException("error.msg.loan.written.off.update.not.allowed", "Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); } + if (loan.isChargedOff() && (transactionToAdjust.getTransactionDate().isBefore(loan.getChargedOffOnDate()) + || transactionToAdjust.getTransactionDate().isEqual(loan.getChargedOffOnDate()))) { + throw new GeneralPlatformDomainRuleException("error.msg.adjusted.transaction.date.cannot.be.earlier.than.charge.off.date", + "Loan transaction: " + transactionId + " adjustment is not allowed before or on the date when the loan got charged-off", + transactionId); + } if (transactionToAdjust.hasChargebackLoanTransactionRelations()) { throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", @@ -1411,6 +1430,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); + LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final Loan loan = this.loanAssembler.assembleFrom(loanId); if (command.hasParameter("writeoffReasonId")) { Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId"); @@ -1421,6 +1441,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } checkClientOrGroupActive(loan); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loanId + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loanId); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanWrittenOffPreBusinessEvent(loan)); entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.WRITE_OFF.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); @@ -1479,6 +1505,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + if (loan.isChargedOff() && transactionDate.isBefore(loan.getChargedOffOnDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + + loanId + + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", + loanId); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseBusinessEvent(loan)); final Map changes = new LinkedHashMap<>(); @@ -1579,6 +1613,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Loan: " + loanId + " Close as rescheduled is not allowed. Loan Account is Charged-off", loanId); + } removeLoanCycle(loan); businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseAsRescheduleBusinessEvent(loan)); @@ -1929,6 +1967,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.getExpectedFirstRepaymentOnDate(), presentMeetingDate); } + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Loan: " + loan.getId() + " reschedule is not allowed. Loan Account is Charged-off", loan.getId()); + } + Boolean isSkipRepaymentOnFirstMonth = false; int numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); @@ -2219,6 +2262,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId); + } final Map actualChanges = new LinkedHashMap<>(); LocalDate expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); if (!loan.loanProduct().isMultiDisburseLoan()) { @@ -2314,6 +2361,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId); + } LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId); this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails); @@ -2565,6 +2616,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LocalDate recalculateFromDate = loan.getLastRepaymentDate(); validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan); checkClientOrGroupActive(loan); + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", + "Undo Loan: " + loanId + " last disbursement is not allowed. Loan Account is Charged-off", loanId); + } businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoLastDisbursalBusinessEvent(loan)); final MonetaryCurrency currency = loan.getCurrency(); @@ -2655,16 +2710,31 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName)); changes.put(LoanApiConstants.localeParameterName, command.locale()); changes.put(LoanApiConstants.dateFormatParameterName, command.dateFormat()); - - // TODO: add business logic validation (transaction date cannot be future, cannot be earlier than latest - // transactions, etc) - Loan loan = loanAssembler.assembleFrom(command.getLoanId()); - businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeOffPreBusinessEvent(loan)); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(LoanApiConstants.transactionDateParamName); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final AppUser currentUser = getAppUserIfPresent(); + Loan loan = loanAssembler.assembleFrom(command.getLoanId()); + final Long loanId = loan.getId(); + if (!loan.isOpen()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.active", + "Loan: " + loanId + " Charge-off is not allowed. Loan Account is not Active", loanId); + } + if (loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.already.charged.off", + "Loan: " + loanId + " is already charged-off", loanId); + } + if (transactionDate.isBefore(loan.getLastUserTransactionDate())) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.is.before.than.the.last.user.transaction", + "Loan: " + loanId + " charge-off cannot be executed. User transaction was found after the charge-off transaction date!", + loanId); + } + if (loan.isInterestBearing()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.interest.bearing", + "Loan: " + loanId + " Charge-off is not allowed. Loan Account is interest bearing", loanId); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeOffPreBusinessEvent(loan)); + if (command.hasParameter(LoanApiConstants.chargeOffReasonIdParamName)) { Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName); CodeValue chargeOffReason = this.codeValueRepository @@ -2703,6 +2773,49 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .with(changes).build(); } + @Override + @Transactional + public CommandProcessingResult undoChargeOff(JsonCommand command) { + final Long loanId = command.getLoanId(); + final Loan loan = this.loanAssembler.assembleFrom(loanId); + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + checkClientOrGroupActive(loan); + if (!loan.isOpen()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.active", + "Loan: " + loanId + " Undo Charge-off is not allowed. Loan Account is not Active", loanId); + } + if (!loan.isChargedOff()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.charged.off", "Loan: " + loanId + " is not charged-off", + loanId); + } + LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); + if (chargedOffTransaction == null) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.transaction.not.found", + "Loan: " + loanId + " charge-off transaction was not found", loanId); + } + if (!chargedOffTransaction.equals(loan.getLastUserTransaction())) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.is.not.the.last.user.transaction", + "Loan: " + loanId + " charge-off cannot be undone. User transaction was found after charge-off!", loanId); + } + businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction)); + + chargedOffTransaction.reverse(); + loan.liftChargeOff(); + loanTransactionRepository.saveAndFlush(chargedOffTransaction); + saveLoanWithDataIntegrityViolationChecks(loan); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction)); + return new CommandProcessingResultBuilder() // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .withEntityId(chargedOffTransaction.getId()) // + .withEntityExternalId(chargedOffTransaction.getExternalId()) // + .build(); + } + private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { if (!loan.isMultiDisburmentLoan()) { final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 077d0ff9a..7e1fd751b 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -113,4 +113,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0094_add_external_event_default_configuration.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0094_add_external_event_default_configuration.xml new file mode 100644 index 000000000..3a9db5f7c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0094_add_external_event_default_configuration.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 441ff0f29..439bd30f4 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -95,7 +95,7 @@ public class ExternalEventConfigurationValidationServiceTest { "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent", - "LoanChargeOffPostBusinessEvent"); + "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); @@ -171,7 +171,8 @@ public class ExternalEventConfigurationValidationServiceTest { "SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent", "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", - "LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent"); + "LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent", + "LoanUndoChargeOffBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java index 604620a91..6e744ce82 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java @@ -19,18 +19,23 @@ package org.apache.fineract.portfolio.loanaccount.domain; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; /** @@ -76,6 +81,95 @@ public class LoanTest { assertEquals(0, chargeIds.size()); } + /** + * Tests {@link Loan#findChargedOffTransaction()} with empty list + */ + @Test + public void testFindChargedOffTransactionEmptyList() { + final Loan loan = new Loan(); + final LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); + assertNull(chargedOffTransaction); + } + + /** + * Tests {@link Loan#findChargedOffTransaction()} where there is no charge-off transaction + */ + @Test + public void testFindChargedOffTransactionNoChargeOffTransaction() { + final Loan loan = new Loan(); + final LoanTransaction loanTransaction = new LoanTransaction(); + ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction)); + final LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); + assertNull(chargedOffTransaction); + } + + /** + * Tests {@link Loan#findChargedOffTransaction()} where there is charge-off transaction + */ + @Test + public void testFindChargedOffTransactionWithChargeOffTransaction() { + final Loan loan = new Loan(); + + final LoanTransaction loanTransaction = new LoanTransaction(); + final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class); + final LoanTransaction loanTransaction3 = new LoanTransaction(); + when(loanTransaction2.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction2.isChargeOff()).thenReturn(Boolean.TRUE); + ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3)); + final LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); + assertNotNull(chargedOffTransaction); + assertEquals(loanTransaction2, chargedOffTransaction); + } + + /** + * Tests {@link Loan#findChargedOffTransaction()} where there is charge-off transaction but reversed + */ + @Test + public void testFindChargedOffTransactionWithReversedChargeOffTransaction() { + final Loan loan = new Loan(); + + final LoanTransaction loanTransaction = new LoanTransaction(); + final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class); + final LoanTransaction loanTransaction3 = new LoanTransaction(); + when(loanTransaction2.isReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction2.isChargeOff()).thenReturn(Boolean.TRUE); + ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3)); + final LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); + assertNull(chargedOffTransaction); + } + + /** + * Tests {@link Loan#getLastUserTransaction()} with empty list + */ + @Test + public void testGetLastUserTransactionEmptyList() { + final Loan loan = new Loan(); + final LoanTransaction userTransaction = loan.getLastUserTransaction(); + assertNull(userTransaction); + } + + /** + * Tests {@link Loan#getLastUserTransaction()} where there are user transactions + */ + @Test + public void testGetLastUserTransaction() { + final Loan loan = new Loan(); + + final LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class); + when(loanTransaction.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction.isAccrualTransaction()).thenReturn(Boolean.FALSE); + final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class); + when(loanTransaction2.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction2.isAccrualTransaction()).thenReturn(Boolean.FALSE); + final LoanTransaction loanTransaction3 = Mockito.mock(LoanTransaction.class); + when(loanTransaction3.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction3.isAccrualTransaction()).thenReturn(Boolean.TRUE); + ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3)); + final LoanTransaction userTransaction = loan.getLastUserTransaction(); + assertNotNull(userTransaction); + assertEquals(loanTransaction2, userTransaction); + } + /** * Builds a new loan charge. * diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 1ea60fb92..ba102d3ee 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -69,9 +69,11 @@ import org.apache.fineract.client.models.PostGLAccountsRequest; import org.apache.fineract.client.models.PostGLAccountsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.client.models.PutChargeTransactionChangesRequest; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.integrationtests.common.BusinessDateHelper; @@ -6954,54 +6956,263 @@ public class ClientLoanIntegrationTest { @Test public void chargeOff() { - final Account assetAccount = this.accountHelper.createAssetAccount(); - final Account incomeAccount = this.accountHelper.createIncomeAccount(); - final Account expenseAccount = this.accountHelper.createExpenseAccount(); - final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); - String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); - Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); - final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, - overpaymentAccount); + try { + GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec, this.responseSpec, true); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec, this.responseSpec, true); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2022.09.30").dateFormat("yyyy.MM.dd").locale("en")); + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterestMultiDisbursement(assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); - final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID); + final Integer loanID = applyForLoanApplication(clientID, loanProductID); - HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); - LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); - loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); - LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); - LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + ResponseSpecification errorResponseSpec = new ResponseSpecBuilder().expectStatusCode(403).build(); + LoanTransactionHelper errorLoanTransactionHelper = new LoanTransactionHelper(this.requestSpec, errorResponseSpec); - loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); - LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(UUID.randomUUID().toString()).chargeOffReasonId((long) chargeOffReasonId)); + }); - GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); - assertTrue(loanDetails.getStatus().getActive()); - assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding()); - assertFalse(loanDetails.getChargedOff()); - assertNull(loanDetails.getSummary().getChargeOffReasonId()); - assertNull(loanDetails.getSummary().getChargeOffReason()); - assertNull(loanDetails.getTimeline().getChargedOffOnDate()); - assertNull(loanDetails.getTimeline().getChargedOffByUsername()); - assertNull(loanDetails.getTimeline().getChargedOffByFirstname()); - assertNull(loanDetails.getTimeline().getChargedOffByLastname()); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.not.active")); - String transactionExternalId = UUID.randomUUID().toString(); - this.loanTransactionHelper.chargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022") - .locale("en").dateFormat("dd MMMM yyyy").externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + exception = assertThrows(CallFailedRuntimeException.class, () -> { + this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.not.active")); - loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); - assertTrue(loanDetails.getStatus().getActive()); - assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding()); - assertTrue(loanDetails.getChargedOff()); - assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId()); - assertEquals(randomText, loanDetails.getSummary().getChargeOffReason()); - assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate()); - assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername()); - assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname()); - assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname()); + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("02 September 2022", loanID, "1000"); + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(2000.0, loanDetails.getSummary().getTotalOutstanding()); + assertFalse(loanDetails.getChargedOff()); + assertNull(loanDetails.getSummary().getChargeOffReasonId()); + assertNull(loanDetails.getSummary().getChargeOffReason()); + assertNull(loanDetails.getTimeline().getChargedOffOnDate()); + assertNull(loanDetails.getTimeline().getChargedOffByUsername()); + assertNull(loanDetails.getTimeline().getChargedOffByFirstname()); + assertNull(loanDetails.getTimeline().getChargedOffByLastname()); + + Integer flatPenaltySpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "3", true)); + this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "3")); + Integer chargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "5")); + + PostLoansLoanIdChargesChargeIdResponse waiveChargeResponse = this.loanTransactionHelper.waiveLoanCharge((long) loanID, + (long) chargeId, new PostLoansLoanIdChargesChargeIdRequest()); + + String transactionExternalId = UUID.randomUUID().toString(); + this.loanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(2003.0, loanDetails.getSummary().getTotalOutstanding()); + assertTrue(loanDetails.getChargedOff()); + assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId()); + assertEquals(randomText, loanDetails.getSummary().getChargeOffReason()); + assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate()); + assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername()); + assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname()); + assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname()); + + GetLoansLoanIdTransactions chargeOffTransaction = loanDetails.getTransactions().get(loanDetails.getTransactions().size() - 1); + + assertEquals(2003.0, chargeOffTransaction.getAmount()); + assertEquals(2000.0, chargeOffTransaction.getPrincipalPortion()); + assertEquals(3.0, chargeOffTransaction.getPenaltyChargesPortion()); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(UUID.randomUUID().toString()).chargeOffReasonId((long) chargeOffReasonId)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.already.charged.off")); + + HashMap chargeAddingError = errorLoanTransactionHelper.addChargesForLoanGetFullResponse(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "3")); + + assertEquals("error.msg.loan.is.charged.off", + ((Map) ((List) chargeAddingError.get("errors")).get(0)).get("userMessageGlobalisationCode")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.undoWaiveLoanCharge((long) loanID, waiveChargeResponse.getSubResourceId(), + new PutChargeTransactionChangesRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + assertFalse(loanDetails.getChargedOff()); + assertNull(loanDetails.getSummary().getChargeOffReasonId()); + assertNull(loanDetails.getSummary().getChargeOffReason()); + assertNull(loanDetails.getTimeline().getChargedOffOnDate()); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.not.charged.off")); + + PostLoansLoanIdTransactionsResponse loanRepaymentResponse = this.loanTransactionHelper.makeLoanRepayment((long) loanID, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en") + .transactionAmount(5.0)); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("04 September 2022").locale("en") + .dateFormat("dd MMMM yyyy").externalId(UUID.randomUUID().toString()) + .chargeOffReasonId((long) chargeOffReasonId)); + }); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.charge.off.is.before.than.the.last.user.transaction")); + + this.loanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("06 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(UUID.randomUUID().toString()).chargeOffReasonId((long) chargeOffReasonId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID); + chargeOffTransaction = loanDetails.getTransactions().get(loanDetails.getTransactions().size() - 1); + + assertEquals(1998.0, chargeOffTransaction.getAmount()); + assertEquals(1998.0, chargeOffTransaction.getPrincipalPortion()); + + this.loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("07 September 2022").locale("en").transactionAmount(5.0)); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.charge.off.is.not.the.last.user.transaction")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.adjustLoanTransaction((long) loanID, loanRepaymentResponse.getResourceId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate("06 September 2022").locale("en") + .dateFormat("dd MMMM yyyy").transactionAmount(0.0)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.adjusted.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.makeWriteoff((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("05 September 2022").locale("en")); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.closeLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("05 September 2022").locale("en")); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.forecloseLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("05 September 2022").locale("en")); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.closeRescheduledLoan((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en")); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off")); + + HashMap disbursementDetailREsponse = (HashMap) errorLoanTransactionHelper.addAndDeleteDisbursementDetail(loanID, "1000", + "03 September 2022", List.of(this.loanTransactionHelper.createTrancheDetail(null, "05 September 2022", "200")), ""); + + assertEquals("error.msg.loan.is.charged.off", + ((Map) ((List) disbursementDetailREsponse.get("errors")).get(0)).get("userMessageGlobalisationCode")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.undoLastDisbursalLoan((long) loanID, new PostLoansLoanIdRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.undoDisbursalLoan((long) loanID, new PostLoansLoanIdRequest()); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.disburseLoan((long) loanID, + new PostLoansLoanIdRequest().actualDisbursementDate("4 September 2022").transactionAmount(new BigDecimal("10")) + .locale("en").dateFormat("dd MMMM yyyy")); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + this.loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("07 September 2022").locale("en").transactionAmount(5000.0)); + + exception = assertThrows(CallFailedRuntimeException.class, () -> { + errorLoanTransactionHelper.makeRefundByCash((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0)); + }); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date")); + + this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("08 September 2022").locale("en").transactionAmount(3007.0)); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } } @Test @@ -7480,6 +7691,16 @@ public class ClientLoanIntegrationTest { return this.loanTransactionHelper.getLoanProductId(loanProductJSON); } + private Integer createLoanProductWithPeriodicAccrualAccountingNoInterestMultiDisbursement(final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance() + .withAccountingRulePeriodicAccrual(accounts).withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30") + .withDaysInYear("365").withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true).build(null); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) { String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID); String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java index df825c504..29fc0b725 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; @@ -246,6 +247,36 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { } + @Test + public void testCreateLoanRescheduleRequestFailIfLoanIsChargedOff() { + // create all required entities + this.createRequiredEntities(); + this.createLoanRescheduleRequestWhichFailsAsLoanIdChargedOff(); + + } + + /** + * create new loan reschedule request + **/ + private void createLoanRescheduleRequestWhichFailsAsLoanIdChargedOff() { + + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateGraceOnPrincipal(null).updateGraceOnInterest(null) + .updateExtraTerms(null).updateRescheduleFromDate("04 January 2015").updateAdjustedDueDate("04 October 2015") + .updateRecalculateInterest(true).build(this.loanId.toString()); + + this.loanTransactionHelper.chargeOffLoan((long) this.loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate("04 January 2015").locale("en").dateFormat("dd MMMM yyyy")); + + ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(403).build(); + LoanRescheduleRequestHelper errorLoanRescheduleRequestHelper = new LoanRescheduleRequestHelper(this.requestSpec, responseSpec); + HashMap response = errorLoanRescheduleRequestHelper.createLoanRescheduleRequestWithFullResponse(requestJSON); + assertEquals("error.msg.loan.is.charged.off", ((Map) ((List) response.get("errors")).get(0)).get("userMessageGlobalisationCode")); + + this.loanTransactionHelper.undoChargeOffLoan((long) this.loanId, new PostLoansLoanIdTransactionsRequest()); + this.loanTransactionHelper.closeRescheduledLoan((long) this.loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("04 January 2015").locale("en")); + } + /** * create new loan reschedule request **/ diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java index f8e89ec39..42785222d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java @@ -50,6 +50,7 @@ import java.util.TimeZone; import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PutJobsJobIDRequest; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.integrationtests.common.BusinessDateHelper; @@ -603,6 +604,51 @@ public class SchedulerJobsTestResults { LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); } + @Test + public void testApplyPenaltyForOverdueLoansJobOutcomeIfLoanChargedOff() throws InterruptedException { + this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); + this.loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanOverdueFeeJSON()); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + Assertions.assertNotNull(loanID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + this.loanTransactionHelper.chargeOffLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().transactionDate("03 March 2020").locale("en").dateFormat("dd MMMM yyyy")); + + String JobName = "Apply penalty to overdue loans"; + this.schedulerJobHelper.executeAndAwaitJob(JobName); + + ArrayList repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, + loanID); + + Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"), + "Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job"); + + this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()); + this.loanTransactionHelper.closeRescheduledLoan((long) loanID, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("03 March 2020").locale("en")); + } + @Test public void testLoanCOBJobOutcome() { this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index 036adfec2..130f8bd29 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -455,6 +455,11 @@ public class ExternalEventConfigurationHelper { loanChargeOffPostBusinessEvent.put("enabled", false); defaults.add(loanChargeOffPostBusinessEvent); + Map loanUndoChargeOffPostBusinessEvent = new HashMap<>(); + loanUndoChargeOffPostBusinessEvent.put("type", "LoanUndoChargeOffBusinessEvent"); + loanUndoChargeOffPostBusinessEvent.put("enabled", false); + defaults.add(loanUndoChargeOffPostBusinessEvent); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java index 7ba7227fe..4e3b827f4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; +import java.util.HashMap; public class LoanRescheduleRequestHelper { @@ -40,6 +41,11 @@ public class LoanRescheduleRequestHelper { return Utils.performServerPost(this.requestSpec, this.responseSpec, URL, requestJSON, "resourceId"); } + public HashMap createLoanRescheduleRequestWithFullResponse(final String requestJSON) { + final String URL = LOAN_RESCHEDULE_REQUEST_URL + "?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerPost(this.requestSpec, this.responseSpec, URL, requestJSON, ""); + } + public Integer rejectLoanRescheduleRequest(final Integer requestId, final String requestJSON) { final String URL = LOAN_RESCHEDULE_REQUEST_URL + "/" + requestId + "?" + Utils.TENANT_IDENTIFIER + "&command=reject"; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index d445462cb..4d9dc927e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -710,6 +710,11 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "adjust")); } + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final Long loanId, final Long transactionId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, transactionId, request, "adjust")); + } + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final Integer loanId, final Long transactionId, String date, ResponseSpecification responseSpec) { return postLoanTransaction(createLoanTransactionURL(null, loanId, transactionId.intValue()), @@ -757,6 +762,12 @@ public class LoanTransactionHelper extends IntegrationTest { return (Integer) response.get("resourceId"); } + public HashMap addChargesForLoanGetFullResponse(final Integer loanId, final String request) { + log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------"); + final String ADD_CHARGES_URL = LOAN_ACCOUNT_URL + "/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerPost(requestSpec, responseSpec, ADD_CHARGES_URL, request, ""); + } + public Integer addChargesForLoan(final Integer loanId, final String request, final ResponseSpecification responseSpecParam) { log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------"); final String ADD_CHARGES_URL = LOAN_ACCOUNT_URL + "/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; @@ -1738,6 +1749,10 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loans.stateTransitions1(loanExternalId, request, "disburse")); } + public PostLoansLoanIdResponse disburseLoan(Long loanId, PostLoansLoanIdRequest request) { + return ok(fineract().loans.stateTransitions(loanId, request, "disburse")); + } + public PostLoansLoanIdResponse disburseToSavingsLoan(String loanExternalId, PostLoansLoanIdRequest request) { return ok(fineract().loans.stateTransitions1(loanExternalId, request, "disburseToSavings")); } @@ -1750,10 +1765,18 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loans.stateTransitions1(loanExternalId, request, "undodisbursal")); } + public PostLoansLoanIdResponse undoDisbursalLoan(Long loanId, PostLoansLoanIdRequest request) { + return ok(fineract().loans.stateTransitions(loanId, request, "undodisbursal")); + } + public PostLoansLoanIdResponse undoLastDisbursalLoan(String loanExternalId, PostLoansLoanIdRequest request) { return ok(fineract().loans.stateTransitions1(loanExternalId, request, "undolastdisbursal")); } + public PostLoansLoanIdResponse undoLastDisbursalLoan(Long loanId, PostLoansLoanIdRequest request) { + return ok(fineract().loans.stateTransitions(loanId, request, "undolastdisbursal")); + } + public PostLoansLoanIdResponse assignLoanOfficerLoan(String loanExternalId, PostLoansLoanIdRequest request) { return ok(fineract().loans.stateTransitions1(loanExternalId, request, "assignloanofficer")); } @@ -1774,14 +1797,26 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "close-rescheduled")); } + public PostLoansLoanIdTransactionsResponse closeRescheduledLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "close-rescheduled")); + } + public PostLoansLoanIdTransactionsResponse closeLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "close")); } + public PostLoansLoanIdTransactionsResponse closeLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "close")); + } + public PostLoansLoanIdTransactionsResponse forecloseLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "foreclosure")); } + public PostLoansLoanIdTransactionsResponse forecloseLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "foreclosure")); + } + public PostLoansLoanIdTransactionsResponse chargeOffLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "charge-off")); } @@ -1789,4 +1824,12 @@ public class LoanTransactionHelper extends IntegrationTest { public PostLoansLoanIdTransactionsResponse chargeOffLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "charge-off")); } + + public PostLoansLoanIdTransactionsResponse undoChargeOffLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "undo-charge-off")); + } + + public PostLoansLoanIdTransactionsResponse undoChargeOffLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undo-charge-off")); + } }