You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2023/02/23 09:22:17 UTC
[fineract] branch develop updated: FINERACT-1806: Charge-off validation
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 77da04fa7 FINERACT-1806: Charge-off validation
77da04fa7 is described below
commit 77da04fa7db4beaefbf1ea2f9d2b2841a815c1c2
Author: Adam Saghy <ad...@gmail.com>
AuthorDate: Wed Feb 22 14:52:45 2023 +0100
FINERACT-1806: Charge-off validation
---
.../commands/service/CommandWrapperBuilder.java | 8 +
.../LoanUndoChargeOffBusinessEvent.java | 35 +++
.../api/LoanTransactionsApiResource.java | 3 +
.../portfolio/loanaccount/domain/Loan.java | 19 ++
.../domain/LoanAccountDomainServiceJpa.java | 51 +++-
.../handler/UndoChargeOffLoanCommandHandler.java | 43 +++
...nRescheduleRequestWritePlatformServiceImpl.java | 6 +
.../LoanChargeWritePlatformServiceImpl.java | 20 +-
.../service/LoanWritePlatformService.java | 3 +
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 129 ++++++++-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
...94_add_external_event_default_configuration.xml | 40 +++
...nalEventConfigurationValidationServiceTest.java | 5 +-
.../portfolio/loanaccount/domain/LoanTest.java | 94 +++++++
.../ClientLoanIntegrationTest.java | 303 ++++++++++++++++++---
.../LoanRescheduleOnDecliningBalanceLoanTest.java | 31 +++
.../integrationtests/SchedulerJobsTestResults.java | 46 ++++
.../common/ExternalEventConfigurationHelper.java | 5 +
.../common/LoanRescheduleRequestHelper.java | 6 +
.../common/loans/LoanTransactionHelper.java | 43 +++
20 files changed, 837 insertions(+), 54 deletions(-)
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<String> 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<Long> 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<Long> existingTransactionIds = new ArrayList<>();
final List<Long> 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<Long> existingTransactionIds = new ArrayList<>();
final List<Long> 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<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -670,6 +707,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<Long> 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<String, Object> 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<LoanTransaction> 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<LoanDisbursementDetails> 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<String, Object> changes = new LinkedHashMap<>();
businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge));
@@ -713,6 +725,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> 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<Long> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> 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<LoanDisbursementDetails> 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<Long> 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<String, Object> 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<String, Object> 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<Long> existingTransactionIds = loan.findExistingTransactionIds();
+ final List<Long> 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 @@
<include file="parts/0091_modify_parameter_value_type_in_job_parameters_table.xml" relativeToChangelogFile="true" />
<include file="parts/0092_add_periodic_accrual_entries_business_step.xml" relativeToChangelogFile="true" />
<include file="parts/0093_update_general_accounting_table_reports.xml" relativeToChangelogFile="true" />
+ <include file="parts/0094_add_external_event_default_configuration.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <changeSet author="fineract" id="1">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanUndoChargeOffBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+ <changeSet id="2" author="fineract">
+ <insert tableName="m_permission">
+ <column name="grouping" value="transaction_loan"/>
+ <column name="code" value="UNDOCHARGEOFF_LOAN"/>
+ <column name="entity_name" value="LOAN"/>
+ <column name="action_name" value="UNDOCHARGEOFF"/>
+ <column name="can_maker_checker" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
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<FineractPlatformTenant> 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<FineractPlatformTenant> 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<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
- LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+ HashMap<String, Object> 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());
-
- 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));
+ 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());
+ 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"));
+
+ 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<HashMap> 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<String, Object> 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"));
+ }
}