FINERACT-1806: Charge-off validation
This commit is contained in:
+8
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
@@ -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) {
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+50
-1
@@ -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<>();
|
||||
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -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);
|
||||
|
||||
+18
-2
@@ -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;
|
||||
|
||||
+3
@@ -112,4 +112,7 @@ public interface LoanWritePlatformService {
|
||||
CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult chargeOff(JsonCommand command);
|
||||
|
||||
@Transactional
|
||||
CommandProcessingResult undoChargeOff(JsonCommand command);
|
||||
}
|
||||
|
||||
+121
-8
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
+40
@@ -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>
|
||||
+3
-2
@@ -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));
|
||||
|
||||
+94
@@ -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.
|
||||
*
|
||||
|
||||
+261
-40
@@ -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());
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.not.active"));
|
||||
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
this.loanTransactionHelper.chargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022")
|
||||
.locale("en").dateFormat("dd MMMM yyyy").externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.not.active"));
|
||||
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId());
|
||||
assertEquals(randomText, loanDetails.getSummary().getChargeOffReason());
|
||||
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate());
|
||||
assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername());
|
||||
assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname());
|
||||
assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname());
|
||||
loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID);
|
||||
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
|
||||
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
|
||||
|
||||
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("02 September 2022", loanID, "1000");
|
||||
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanID, "1000");
|
||||
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertEquals(2000.0, loanDetails.getSummary().getTotalOutstanding());
|
||||
assertFalse(loanDetails.getChargedOff());
|
||||
assertNull(loanDetails.getSummary().getChargeOffReasonId());
|
||||
assertNull(loanDetails.getSummary().getChargeOffReason());
|
||||
assertNull(loanDetails.getTimeline().getChargedOffOnDate());
|
||||
assertNull(loanDetails.getTimeline().getChargedOffByUsername());
|
||||
assertNull(loanDetails.getTimeline().getChargedOffByFirstname());
|
||||
assertNull(loanDetails.getTimeline().getChargedOffByLastname());
|
||||
|
||||
Integer flatPenaltySpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "3", true));
|
||||
this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper
|
||||
.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "3"));
|
||||
Integer chargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper
|
||||
.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "5"));
|
||||
|
||||
PostLoansLoanIdChargesChargeIdResponse waiveChargeResponse = this.loanTransactionHelper.waiveLoanCharge((long) loanID,
|
||||
(long) chargeId, new PostLoansLoanIdChargesChargeIdRequest());
|
||||
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
this.loanTransactionHelper.chargeOffLoan((long) loanID,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertEquals(2003.0, loanDetails.getSummary().getTotalOutstanding());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId());
|
||||
assertEquals(randomText, loanDetails.getSummary().getChargeOffReason());
|
||||
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate());
|
||||
assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername());
|
||||
assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname());
|
||||
assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname());
|
||||
|
||||
GetLoansLoanIdTransactions chargeOffTransaction = loanDetails.getTransactions().get(loanDetails.getTransactions().size() - 1);
|
||||
|
||||
assertEquals(2003.0, chargeOffTransaction.getAmount());
|
||||
assertEquals(2000.0, chargeOffTransaction.getPrincipalPortion());
|
||||
assertEquals(3.0, chargeOffTransaction.getPenaltyChargesPortion());
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.chargeOffLoan((long) loanID,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(UUID.randomUUID().toString()).chargeOffReasonId((long) chargeOffReasonId));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.already.charged.off"));
|
||||
|
||||
HashMap chargeAddingError = errorLoanTransactionHelper.addChargesForLoanGetFullResponse(loanID, LoanTransactionHelper
|
||||
.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatPenaltySpecifiedDueDate), "04 September 2022", "3"));
|
||||
|
||||
assertEquals("error.msg.loan.is.charged.off",
|
||||
((Map) ((List) chargeAddingError.get("errors")).get(0)).get("userMessageGlobalisationCode"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.undoWaiveLoanCharge((long) loanID, waiveChargeResponse.getSubResourceId(),
|
||||
new PutChargeTransactionChangesRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
this.loanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest());
|
||||
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
|
||||
assertFalse(loanDetails.getChargedOff());
|
||||
assertNull(loanDetails.getSummary().getChargeOffReasonId());
|
||||
assertNull(loanDetails.getSummary().getChargeOffReason());
|
||||
assertNull(loanDetails.getTimeline().getChargedOffOnDate());
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.not.charged.off"));
|
||||
|
||||
PostLoansLoanIdTransactionsResponse loanRepaymentResponse = this.loanTransactionHelper.makeLoanRepayment((long) loanID,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en")
|
||||
.transactionAmount(5.0));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.chargeOffLoan((long) loanID,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("04 September 2022").locale("en")
|
||||
.dateFormat("dd MMMM yyyy").externalId(UUID.randomUUID().toString())
|
||||
.chargeOffReasonId((long) chargeOffReasonId));
|
||||
});
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.charge.off.is.before.than.the.last.user.transaction"));
|
||||
|
||||
this.loanTransactionHelper.chargeOffLoan((long) loanID,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("06 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(UUID.randomUUID().toString()).chargeOffReasonId((long) chargeOffReasonId));
|
||||
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
|
||||
chargeOffTransaction = loanDetails.getTransactions().get(loanDetails.getTransactions().size() - 1);
|
||||
|
||||
assertEquals(1998.0, chargeOffTransaction.getAmount());
|
||||
assertEquals(1998.0, chargeOffTransaction.getPrincipalPortion());
|
||||
|
||||
this.loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
|
||||
.transactionDate("07 September 2022").locale("en").transactionAmount(5.0));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.undoChargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.charge.off.is.not.the.last.user.transaction"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.adjustLoanTransaction((long) loanID, loanRepaymentResponse.getResourceId(),
|
||||
new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate("06 September 2022").locale("en")
|
||||
.dateFormat("dd MMMM yyyy").transactionAmount(0.0));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.adjusted.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.makeWriteoff((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
|
||||
.transactionDate("05 September 2022").locale("en"));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.closeLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
|
||||
.transactionDate("05 September 2022").locale("en"));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.forecloseLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
|
||||
.transactionDate("05 September 2022").locale("en"));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.closeRescheduledLoan((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en"));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off"));
|
||||
|
||||
HashMap disbursementDetailREsponse = (HashMap) errorLoanTransactionHelper.addAndDeleteDisbursementDetail(loanID, "1000",
|
||||
"03 September 2022", List.of(this.loanTransactionHelper.createTrancheDetail(null, "05 September 2022", "200")), "");
|
||||
|
||||
assertEquals("error.msg.loan.is.charged.off",
|
||||
((Map) ((List) disbursementDetailREsponse.get("errors")).get(0)).get("userMessageGlobalisationCode"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.undoLastDisbursalLoan((long) loanID, new PostLoansLoanIdRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.undoDisbursalLoan((long) loanID, new PostLoansLoanIdRequest());
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.is.charged.off"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.disburseLoan((long) loanID,
|
||||
new PostLoansLoanIdRequest().actualDisbursementDate("4 September 2022").transactionAmount(new BigDecimal("10"))
|
||||
.locale("en").dateFormat("dd MMMM yyyy"));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
this.loanTransactionHelper.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
|
||||
.transactionDate("07 September 2022").locale("en").transactionAmount(5000.0));
|
||||
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
errorLoanTransactionHelper.makeRefundByCash((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("05 September 2022").locale("en").transactionAmount(5.0));
|
||||
});
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date"));
|
||||
|
||||
this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
|
||||
.dateFormat("dd MMMM yyyy").transactionDate("08 September 2022").locale("en").transactionAmount(3007.0));
|
||||
} finally {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -7480,6 +7691,16 @@ public class ClientLoanIntegrationTest {
|
||||
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||
}
|
||||
|
||||
private Integer createLoanProductWithPeriodicAccrualAccountingNoInterestMultiDisbursement(final Account... accounts) {
|
||||
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
|
||||
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth()
|
||||
.withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0")
|
||||
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
|
||||
.withAccountingRulePeriodicAccrual(accounts).withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30")
|
||||
.withDaysInYear("365").withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true).build(null);
|
||||
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||
}
|
||||
|
||||
private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) {
|
||||
String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID);
|
||||
String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
|
||||
|
||||
+31
@@ -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
|
||||
**/
|
||||
|
||||
+46
@@ -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);
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -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";
|
||||
|
||||
|
||||
+43
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user