FINERACT-1806: Charge-off validation

This commit is contained in:
Adam Saghy
2023-02-23 10:22:10 +01:00
parent f000de5364
commit 77da04fa7d
20 changed files with 836 additions and 53 deletions
@@ -3614,4 +3614,12 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/transactions?command=charge-off"; this.href = "/loans/" + loanId + "/transactions?command=charge-off";
return this; 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;
}
} }
@@ -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;
}
}
@@ -77,6 +77,7 @@ import org.springframework.stereotype.Component;
public class LoanTransactionsApiResource { public class LoanTransactionsApiResource {
public static final String CHARGE_OFF_COMMAND_VALUE = "charge-off"; 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", 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)); LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
@@ -463,6 +464,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.creditBalanceRefund(resolvedLoanId).build(); commandRequest = builder.creditBalanceRefund(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, CHARGE_OFF_COMMAND_VALUE)) { } else if (CommandParameterUtil.is(commandParam, CHARGE_OFF_COMMAND_VALUE)) {
commandRequest = builder.chargeOff(resolvedLoanId).build(); commandRequest = builder.chargeOff(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, UNDO_CHARGE_OFF_COMMAND_VALUE)) {
commandRequest = builder.undoChargeOff(resolvedLoanId).build();
} }
if (commandRequest == null) { if (commandRequest == null) {
@@ -7027,10 +7027,29 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return getLoanTransactions().stream().filter(predicate).toList(); return getLoanTransactions().stream().filter(predicate).toList();
} }
public LoanTransaction findChargedOffTransaction() {
return getLoanTransactions().stream() //
.filter(LoanTransaction::isNotReversed) //
.filter(LoanTransaction::isChargeOff) //
.findFirst() //
.orElse(null);
}
public void handleMaturityDateActivate() { public void handleMaturityDateActivate() {
if (this.expectedMaturityDate != null && this.actualMaturityDate == null) { if (this.expectedMaturityDate != null && this.actualMaturityDate == null) {
this.actualMaturityDate = this.expectedMaturityDate; 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;
}
} }
@@ -161,7 +161,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final boolean isRecoveryRepayment, final String chargeRefundChargeType, boolean isAccountTransfer, final boolean isRecoveryRepayment, final String chargeRefundChargeType, boolean isAccountTransfer,
HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) {
checkClientOrGroupActive(loan); 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); LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan);
businessEventNotifierService.notifyPreBusinessEvent(repaymentEvent); businessEventNotifierService.notifyPreBusinessEvent(repaymentEvent);
@@ -357,6 +362,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final Integer transactionType, Integer installmentNumber) { final Integer transactionType, Integer installmentNumber) {
boolean isAccountTransfer = true; boolean isAccountTransfer = true;
checkClientOrGroupActive(loan); 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanChargePaymentPreBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
@@ -436,6 +447,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
boolean isAccountTransfer = true; boolean isAccountTransfer = true;
final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); final Loan loan = this.loanAccountAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan); 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanRefundPreBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = 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 PaymentDetail paymentDetail, final String noteText, final ExternalId txnExternalId, final boolean isLoanToLoanTransfer) {
final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); final Loan loan = this.loanAccountAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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; boolean isAccountTransfer = true;
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -508,6 +531,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override @Override
public void reverseTransfer(final LoanTransaction loanTransaction) { 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(); loanTransaction.reverse();
saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction);
} }
@@ -633,6 +663,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override @Override
public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate transactionDate, final BigDecimal transactionAmount, public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate transactionDate, final BigDecimal transactionAmount,
final String noteText, final ExternalId externalId, PaymentDetail paymentDetail) { 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanCreditBalanceRefundPreBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -670,6 +707,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount); 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, final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail,
transactionDate, txnExternalId); transactionDate, txnExternalId);
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
@@ -702,6 +745,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override @Override
public LoanTransaction foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText, public LoanTransaction foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText,
final ExternalId externalId, Map<String, Object> changes) { 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanForeClosurePreBusinessEvent(loan));
MonetaryCurrency currency = loan.getCurrency(); MonetaryCurrency currency = loan.getCurrency();
List<LoanTransaction> newTransactions = new ArrayList<>(); List<LoanTransaction> newTransactions = new ArrayList<>();
@@ -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);
}
}
@@ -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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; 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.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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 // use the loan id to get a Loan entity object
final Loan loan = this.loanAssembler.assembleFrom(loanId); 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 // validate the request in the JsonCommand object passed as
// parameter // parameter
this.loanRescheduleRequestDataValidator.validateForCreateAction(jsonCommand, loan); this.loanRescheduleRequestDataValidator.validateForCreateAction(jsonCommand, loan);
@@ -33,6 +33,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; 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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.domain.ExternalId; 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.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatformService { public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatformService {
@@ -173,7 +176,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
Loan loan = this.loanAssembler.assembleFrom(loanId); Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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(); List<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId");
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
@@ -360,7 +366,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
throw new LoanChargeWaiveCannotBeReversedException( throw new LoanChargeWaiveCannotBeReversedException(
LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); 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<>(); final Map<String, Object> changes = new LinkedHashMap<>();
businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge));
@@ -713,6 +725,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) { public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
Loan loan = this.loanAssembler.assembleFrom(loanId); 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> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
boolean runInterestRecalculation = false; boolean runInterestRecalculation = false;
@@ -112,4 +112,7 @@ public interface LoanWritePlatformService {
CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command); CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command);
CommandProcessingResult chargeOff(JsonCommand command); CommandProcessingResult chargeOff(JsonCommand command);
@Transactional
CommandProcessingResult undoChargeOff(JsonCommand command);
} }
@@ -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.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent; 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.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.LoanUndoWrittenOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWrittenOffPostBusinessEvent; 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); Loan loan = this.loanAssembler.assembleFrom(loanId);
// Fail fast if client/group is not active or actual loan status disallows disbursal // Fail fast if client/group is not active or actual loan status disallows disbursal
checkClientOrGroupActive(loan); 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()) { if (loan.loanProduct().isDisallowExpectedDisbursements()) {
List<LoanDisbursementDetails> filteredList = loan.getDisbursementDetails().stream() List<LoanDisbursementDetails> filteredList = loan.getDisbursementDetails().stream()
.filter(disbursementDetails -> disbursementDetails.actualDisbursementDate() == null).toList(); .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 // validate ActualDisbursement Date Against Expected Disbursement Date
LoanProduct loanProduct = loan.loanProduct(); LoanProduct loanProduct = loan.loanProduct();
if (loanProduct.syncExpectedWithDisbursementDate()) { if (loanProduct.syncExpectedWithDisbursementDate()) {
@@ -805,6 +814,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Loan loan = this.loanAssembler.assembleFrom(loanId); Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan));
removeLoanCycle(loan); removeLoanCycle(loan);
final List<Long> existingTransactionIds = new ArrayList<>(); 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", throw new PlatformServiceUnavailableException("error.msg.loan.written.off.update.not.allowed",
"Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); "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()) { if (transactionToAdjust.hasChargebackLoanTransactionRelations()) {
throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", 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("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale()); changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat()); changes.put("dateFormat", command.dateFormat());
LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final Loan loan = this.loanAssembler.assembleFrom(loanId); final Loan loan = this.loanAssembler.assembleFrom(loanId);
if (command.hasParameter("writeoffReasonId")) { if (command.hasParameter("writeoffReasonId")) {
Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId"); Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId");
@@ -1421,6 +1441,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
checkClientOrGroupActive(loan); 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanWrittenOffPreBusinessEvent(loan));
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.WRITE_OFF.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); 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); final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseBusinessEvent(loan));
final Map<String, Object> changes = new LinkedHashMap<>(); final Map<String, Object> changes = new LinkedHashMap<>();
@@ -1579,6 +1613,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId); final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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); removeLoanCycle(loan);
businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseAsRescheduleBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseAsRescheduleBusinessEvent(loan));
@@ -1929,6 +1967,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getExpectedFirstRepaymentOnDate(), presentMeetingDate); 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; Boolean isSkipRepaymentOnFirstMonth = false;
int numberOfDays = 0; int numberOfDays = 0;
boolean isSkipRepaymentOnFirstMonthEnabled = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); boolean isSkipRepaymentOnFirstMonthEnabled = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled();
@@ -2219,6 +2262,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId); final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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<>(); final Map<String, Object> actualChanges = new LinkedHashMap<>();
LocalDate expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); LocalDate expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate();
if (!loan.loanProduct().isMultiDisburseLoan()) { if (!loan.loanProduct().isMultiDisburseLoan()) {
@@ -2314,6 +2361,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId); final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan); 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); LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails); this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
@@ -2565,6 +2616,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LocalDate recalculateFromDate = loan.getLastRepaymentDate(); final LocalDate recalculateFromDate = loan.getLastRepaymentDate();
validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan); validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan);
checkClientOrGroupActive(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)); businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoLastDisbursalBusinessEvent(loan));
final MonetaryCurrency currency = loan.getCurrency(); final MonetaryCurrency currency = loan.getCurrency();
@@ -2655,16 +2710,31 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName)); command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName));
changes.put(LoanApiConstants.localeParameterName, command.locale()); changes.put(LoanApiConstants.localeParameterName, command.locale());
changes.put(LoanApiConstants.dateFormatParameterName, command.dateFormat()); 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 LocalDate transactionDate = command.localDateValueOfParameterNamed(LoanApiConstants.transactionDateParamName);
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
final AppUser currentUser = getAppUserIfPresent(); 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)) { if (command.hasParameter(LoanApiConstants.chargeOffReasonIdParamName)) {
Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName); Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName);
CodeValue chargeOffReason = this.codeValueRepository CodeValue chargeOffReason = this.codeValueRepository
@@ -2703,6 +2773,49 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.with(changes).build(); .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) { private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
if (!loan.isMultiDisburmentLoan()) { if (!loan.isMultiDisburmentLoan()) {
final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
@@ -113,4 +113,5 @@
<include file="parts/0091_modify_parameter_value_type_in_job_parameters_table.xml" relativeToChangelogFile="true" /> <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/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/0093_update_general_accounting_table_reports.xml" relativeToChangelogFile="true" />
<include file="parts/0094_add_external_event_default_configuration.xml" relativeToChangelogFile="true" />
</databaseChangeLog> </databaseChangeLog>
@@ -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>
@@ -95,7 +95,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent",
"LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent",
"LoanChargeOffPostBusinessEvent"); "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -171,7 +171,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent", "SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent",
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent", "ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent",
"LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent"); "LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent",
"LoanUndoChargeOffBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -19,18 +19,23 @@
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.mock;
import static org.mockito.Mockito.when;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
/** /**
@@ -76,6 +81,95 @@ public class LoanTest {
assertEquals(0, chargeIds.size()); 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. * Builds a new loan charge.
* *
@@ -69,9 +69,11 @@ import org.apache.fineract.client.models.PostGLAccountsRequest;
import org.apache.fineract.client.models.PostGLAccountsResponse; import org.apache.fineract.client.models.PostGLAccountsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; 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.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; 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.client.util.CallFailedRuntimeException;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper;
@@ -6954,54 +6956,263 @@ public class ClientLoanIntegrationTest {
@Test @Test
public void chargeOff() { public void chargeOff() {
final Account assetAccount = this.accountHelper.createAssetAccount(); try {
final Account incomeAccount = this.accountHelper.createIncomeAccount(); GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec, this.responseSpec, true);
final Account expenseAccount = this.accountHelper.createExpenseAccount(); GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec, this.responseSpec, true);
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); .date("2022.09.30").dateFormat("yyyy.MM.dd").locale("en"));
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); final Account assetAccount = this.accountHelper.createAssetAccount();
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, final Account incomeAccount = this.accountHelper.createIncomeAccount();
overpaymentAccount); 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); HashMap<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); ResponseSpecification errorResponseSpec = new ResponseSpecBuilder().expectStatusCode(403).build();
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); LoanTransactionHelper errorLoanTransactionHelper = new LoanTransactionHelper(this.requestSpec, errorResponseSpec);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> {
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); 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); assertEquals(403, exception.getResponse().code());
assertTrue(loanDetails.getStatus().getActive()); assertTrue(exception.getMessage().contains("error.msg.loan.is.not.active"));
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(); exception = assertThrows(CallFailedRuntimeException.class, () -> {
this.loanTransactionHelper.chargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022") this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest());
.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); loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID);
assertTrue(loanDetails.getStatus().getActive()); LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding()); LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
assertTrue(loanDetails.getChargedOff());
assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId()); loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("02 September 2022", loanID, "1000");
assertEquals(randomText, loanDetails.getSummary().getChargeOffReason()); loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanID, "1000");
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate()); LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername());
assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname()); GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname()); 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 @Test
@@ -7480,6 +7691,16 @@ public class ClientLoanIntegrationTest {
return this.loanTransactionHelper.getLoanProductId(loanProductJSON); 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) { private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) {
String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID); String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID);
String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID); String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
@@ -32,6 +32,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; 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 * create new loan reschedule request
**/ **/
@@ -50,6 +50,7 @@ import java.util.TimeZone;
import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostClientsResponse; 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.client.models.PutJobsJobIDRequest;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper;
@@ -603,6 +604,51 @@ public class SchedulerJobsTestResults {
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); 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 @Test
public void testLoanCOBJobOutcome() { public void testLoanCOBJobOutcome() {
this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec);
@@ -455,6 +455,11 @@ public class ExternalEventConfigurationHelper {
loanChargeOffPostBusinessEvent.put("enabled", false); loanChargeOffPostBusinessEvent.put("enabled", false);
defaults.add(loanChargeOffPostBusinessEvent); defaults.add(loanChargeOffPostBusinessEvent);
Map<String, Object> loanUndoChargeOffPostBusinessEvent = new HashMap<>();
loanUndoChargeOffPostBusinessEvent.put("type", "LoanUndoChargeOffBusinessEvent");
loanUndoChargeOffPostBusinessEvent.put("enabled", false);
defaults.add(loanUndoChargeOffPostBusinessEvent);
return defaults; return defaults;
} }
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.util.HashMap;
public class LoanRescheduleRequestHelper { public class LoanRescheduleRequestHelper {
@@ -40,6 +41,11 @@ public class LoanRescheduleRequestHelper {
return Utils.performServerPost(this.requestSpec, this.responseSpec, URL, requestJSON, "resourceId"); 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) { public Integer rejectLoanRescheduleRequest(final Integer requestId, final String requestJSON) {
final String URL = LOAN_RESCHEDULE_REQUEST_URL + "/" + requestId + "?" + Utils.TENANT_IDENTIFIER + "&command=reject"; final String URL = LOAN_RESCHEDULE_REQUEST_URL + "/" + requestId + "?" + Utils.TENANT_IDENTIFIER + "&command=reject";
@@ -710,6 +710,11 @@ public class LoanTransactionHelper extends IntegrationTest {
return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "adjust")); 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, public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final Integer loanId, final Long transactionId, String date,
ResponseSpecification responseSpec) { ResponseSpecification responseSpec) {
return postLoanTransaction(createLoanTransactionURL(null, loanId, transactionId.intValue()), return postLoanTransaction(createLoanTransactionURL(null, loanId, transactionId.intValue()),
@@ -757,6 +762,12 @@ public class LoanTransactionHelper extends IntegrationTest {
return (Integer) response.get("resourceId"); 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) { public Integer addChargesForLoan(final Integer loanId, final String request, final ResponseSpecification responseSpecParam) {
log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------"); log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------");
final String ADD_CHARGES_URL = LOAN_ACCOUNT_URL + "/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; 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")); 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) { public PostLoansLoanIdResponse disburseToSavingsLoan(String loanExternalId, PostLoansLoanIdRequest request) {
return ok(fineract().loans.stateTransitions1(loanExternalId, request, "disburseToSavings")); 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")); 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) { public PostLoansLoanIdResponse undoLastDisbursalLoan(String loanExternalId, PostLoansLoanIdRequest request) {
return ok(fineract().loans.stateTransitions1(loanExternalId, request, "undolastdisbursal")); 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) { public PostLoansLoanIdResponse assignLoanOfficerLoan(String loanExternalId, PostLoansLoanIdRequest request) {
return ok(fineract().loans.stateTransitions1(loanExternalId, request, "assignloanofficer")); 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")); 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) { public PostLoansLoanIdTransactionsResponse closeLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "close")); 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) { public PostLoansLoanIdTransactionsResponse forecloseLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "foreclosure")); 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) { public PostLoansLoanIdTransactionsResponse chargeOffLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "charge-off")); 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) { public PostLoansLoanIdTransactionsResponse chargeOffLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "charge-off")); 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"));
}
} }