FINERACT-1806-Post-ChargeOff-Scenarios-1
This commit is contained in:
committed by
Arnold Gálovics
parent
5d011077e7
commit
6f677a3dd1
+177
@@ -59,7 +59,9 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
@@ -4480,6 +4482,181 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
return this.proposedPrincipal;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> deriveAccountingBridgeDataForChargeOff(final String currencyCode,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
||||
|
||||
final List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
|
||||
|
||||
// get map before charge-off
|
||||
final List<Map<String, Object>> newLoanTransactionsBeforeChargeOff = new ArrayList<>();
|
||||
final Map<String, Object> accountingBridgeDataBeforeChargeOff = getAccountingMapForChargeOffDateCriteria(currencyCode,
|
||||
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, newLoanTransactionsBeforeChargeOff, true);
|
||||
|
||||
// get map after charge-off
|
||||
final List<Map<String, Object>> newLoanTransactionsAfterChargeOff = new ArrayList<>();
|
||||
final Map<String, Object> accountingBridgeDataAfterChargeOff = getAccountingMapForChargeOffDateCriteria(currencyCode,
|
||||
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, newLoanTransactionsAfterChargeOff, false);
|
||||
|
||||
// get map onCharge off date
|
||||
getAccountingMapDataOnChargeOffDate(currencyCode, existingTransactionIds, existingReversedTransactionIds,
|
||||
newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff);
|
||||
|
||||
accountingBridgeDataBeforeChargeOff.put("newLoanTransactions", newLoanTransactionsBeforeChargeOff);
|
||||
accountingBridgeData.add(accountingBridgeDataBeforeChargeOff);
|
||||
|
||||
accountingBridgeDataAfterChargeOff.put("newLoanTransactions", newLoanTransactionsAfterChargeOff);
|
||||
accountingBridgeData.add(accountingBridgeDataAfterChargeOff);
|
||||
|
||||
return accountingBridgeData;
|
||||
}
|
||||
|
||||
private Map<String, Object> getAccountingBridgeDataGenericAttributes(final String currencyCode, boolean isAccountTransfer) {
|
||||
final Map<String, Object> accountingBridgeDataGenericAttributes = new LinkedHashMap<>();
|
||||
accountingBridgeDataGenericAttributes.put("loanId", getId());
|
||||
accountingBridgeDataGenericAttributes.put("loanProductId", productId());
|
||||
accountingBridgeDataGenericAttributes.put("officeId", getOfficeId());
|
||||
accountingBridgeDataGenericAttributes.put("currencyCode", currencyCode);
|
||||
accountingBridgeDataGenericAttributes.put("calculatedInterest", this.summary.getTotalInterestCharged());
|
||||
accountingBridgeDataGenericAttributes.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnLoanProduct());
|
||||
accountingBridgeDataGenericAttributes.put("upfrontAccrualBasedAccountingEnabled", isUpfrontAccrualAccountingEnabledOnLoanProduct());
|
||||
accountingBridgeDataGenericAttributes.put("periodicAccrualBasedAccountingEnabled",
|
||||
isPeriodicAccrualAccountingEnabledOnLoanProduct());
|
||||
accountingBridgeDataGenericAttributes.put("isAccountTransfer", isAccountTransfer);
|
||||
return accountingBridgeDataGenericAttributes;
|
||||
}
|
||||
|
||||
private Map<String, Object> getAccountingMapForChargeOffDateCriteria(final String currencyCode, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer, List<Map<String, Object>> newLoanTransactions,
|
||||
boolean isBeforeChargeOffDate) {
|
||||
final Map<String, Object> accountingBridgeDataChargeOff = new LinkedHashMap<>(
|
||||
getAccountingBridgeDataGenericAttributes(currencyCode, isAccountTransfer));
|
||||
if (isBeforeChargeOffDate) {
|
||||
accountingBridgeDataChargeOff.put("isChargeOff", false);
|
||||
accountingBridgeDataChargeOff.put("isFraud", false);
|
||||
} else {
|
||||
accountingBridgeDataChargeOff.put("isChargeOff", isChargedOff());
|
||||
accountingBridgeDataChargeOff.put("isFraud", isFraud());
|
||||
}
|
||||
Predicate<LoanTransaction> chargeOffDateCriteria = isBeforeChargeOffDate
|
||||
? transaction -> transaction.getTransactionDate().isBefore(getChargedOffOnDate())
|
||||
: transaction -> transaction.getTransactionDate().isAfter(getChargedOffOnDate());
|
||||
getTransactionsForAccountingBridgeData(currencyCode, existingTransactionIds, existingReversedTransactionIds, newLoanTransactions,
|
||||
chargeOffDateCriteria);
|
||||
return accountingBridgeDataChargeOff;
|
||||
}
|
||||
|
||||
private void getAccountingMapDataOnChargeOffDate(String currencyCode, List<Long> existingTransactionIds,
|
||||
List<Long> existingReversedTransactionIds, List<Map<String, Object>> newLoanTransactionsBeforeChargeOff,
|
||||
List<Map<String, Object>> newLoanTransactionsAfterChargeOff) {
|
||||
Predicate<LoanTransaction> isOnChargeOff = transaction -> transaction.getTransactionDate().isEqual(getChargedOffOnDate());
|
||||
List<LoanTransaction> transactionsOnChargeOffDate = this.loanTransactions.stream().filter(isOnChargeOff)
|
||||
.collect(Collectors.toList());
|
||||
/**
|
||||
*
|
||||
* TODO: Modify logic to retrieve correct charge-off transaction once reverse replay of charge-off is
|
||||
* implemented
|
||||
*/
|
||||
LoanTransaction chargeOffTransaction = this.loanTransactions.stream().filter(LoanTransaction::isChargeOff).findFirst().get();
|
||||
for (final LoanTransaction transaction : transactionsOnChargeOffDate) {
|
||||
checkAndAddReversedTransactionOnChargeOffDate(currencyCode, transaction, existingTransactionIds, existingReversedTransactionIds,
|
||||
newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff, chargeOffTransaction);
|
||||
checkAndAddNewTransactionOnChargeOffDate(currencyCode, transaction, existingTransactionIds, newLoanTransactionsBeforeChargeOff,
|
||||
newLoanTransactionsAfterChargeOff, chargeOffTransaction);
|
||||
checkAndAddChargeOffTransaction(currencyCode, transaction, existingTransactionIds, newLoanTransactionsAfterChargeOff);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndAddReversedTransactionOnChargeOffDate(String currencyCode, LoanTransaction transaction,
|
||||
List<Long> existingTransactionIds, List<Long> existingReversedTransactionIds,
|
||||
List<Map<String, Object>> newLoanTransactionsBeforeChargeOff, List<Map<String, Object>> newLoanTransactionsAfterChargeOff,
|
||||
LoanTransaction chargeOffTransaction) {
|
||||
if (!transaction.isChargeOff()) {
|
||||
if (transaction.isReversed() && existingTransactionIds.contains(transaction.getId())
|
||||
&& !existingReversedTransactionIds.contains(transaction.getId())) {
|
||||
compareWithChargeOffIdAndAddTransactionForAccountingData(currencyCode, newLoanTransactionsBeforeChargeOff,
|
||||
newLoanTransactionsAfterChargeOff, transaction, chargeOffTransaction.getId());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndAddReverseReplayedTransactionOnChargeOffDate(String currencyCode, LoanTransaction transaction,
|
||||
List<Map<String, Object>> newLoanTransactionsBeforeChargeOff, List<Map<String, Object>> newLoanTransactionsAfterChargeOff,
|
||||
LoanTransaction chargeOffTransaction) {
|
||||
Predicate<LoanTransactionRelation> isReplayed = transactionRelation -> LoanTransactionRelationTypeEnum.REPLAYED
|
||||
.equals(transactionRelation.getRelationType());
|
||||
List<LoanTransactionRelation> replayedTransactionRelations = transaction.getLoanTransactionRelations().stream().filter(isReplayed)
|
||||
.sorted(Comparator.comparing(LoanTransactionRelation::getToTransaction, Comparator.comparingLong(LoanTransaction::getId)))
|
||||
.toList();
|
||||
if (!replayedTransactionRelations.isEmpty()) {
|
||||
// Transaction Id for first reversed transaction
|
||||
Long transactionIdForReversedTransaction = replayedTransactionRelations.get(0).getToTransaction().getId();
|
||||
if (transactionIdForReversedTransaction > chargeOffTransaction.getId()) {
|
||||
newLoanTransactionsAfterChargeOff.add(transaction.toMapData(currencyCode));
|
||||
} else {
|
||||
newLoanTransactionsBeforeChargeOff.add(transaction.toMapData(currencyCode));
|
||||
}
|
||||
} else {
|
||||
// new transaction
|
||||
compareWithChargeOffIdAndAddTransactionForAccountingData(currencyCode, newLoanTransactionsBeforeChargeOff,
|
||||
newLoanTransactionsAfterChargeOff, transaction, chargeOffTransaction.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndAddNewTransactionOnChargeOffDate(String currencyCode, LoanTransaction transaction,
|
||||
List<Long> existingTransactionIds, List<Map<String, Object>> newLoanTransactionsBeforeChargeOff,
|
||||
List<Map<String, Object>> newLoanTransactionsAfterChargeOff, LoanTransaction chargeOffTransaction) {
|
||||
if (!transaction.isChargeOff()) {
|
||||
if (!existingTransactionIds.contains(transaction.getId())) {
|
||||
if (!transaction.getLoanTransactionRelations().isEmpty()) {
|
||||
checkAndAddReverseReplayedTransactionOnChargeOffDate(currencyCode, transaction, newLoanTransactionsBeforeChargeOff,
|
||||
newLoanTransactionsAfterChargeOff, chargeOffTransaction);
|
||||
} else {
|
||||
// new transaction
|
||||
compareWithChargeOffIdAndAddTransactionForAccountingData(currencyCode, newLoanTransactionsBeforeChargeOff,
|
||||
newLoanTransactionsAfterChargeOff, transaction, chargeOffTransaction.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndAddChargeOffTransaction(String currencyCode, LoanTransaction transaction, List<Long> existingTransactionIds,
|
||||
List<Map<String, Object>> newLoanTransactionsAfterChargeOff) {
|
||||
if (transaction.isChargeOff()) {
|
||||
/**
|
||||
*
|
||||
* TODO: Modify logic for reverse replay of charge-off
|
||||
*/
|
||||
if (!existingTransactionIds.contains(transaction.getId())) {
|
||||
newLoanTransactionsAfterChargeOff.add(transaction.toMapData(currencyCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void compareWithChargeOffIdAndAddTransactionForAccountingData(final String currencyCode,
|
||||
List<Map<String, Object>> newLoanTransactionsBeforeChargeOff, List<Map<String, Object>> newLoanTransactionsAfterChargeOff,
|
||||
LoanTransaction transaction, Long chargeOffTransactionId) {
|
||||
if (transaction.getId() > chargeOffTransactionId) {
|
||||
newLoanTransactionsAfterChargeOff.add(transaction.toMapData(currencyCode));
|
||||
} else {
|
||||
newLoanTransactionsBeforeChargeOff.add(transaction.toMapData(currencyCode));
|
||||
}
|
||||
}
|
||||
|
||||
private void getTransactionsForAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final List<Map<String, Object>> newLoanTransactions,
|
||||
Predicate<LoanTransaction> chargeOffDateCriteria) {
|
||||
Consumer<LoanTransaction> addTransactionForAccounting = transaction -> {
|
||||
if (transaction.isReversed() && existingTransactionIds.contains(transaction.getId())
|
||||
&& !existingReversedTransactionIds.contains(transaction.getId())) {
|
||||
newLoanTransactions.add(transaction.toMapData(currencyCode));
|
||||
} else if (!existingTransactionIds.contains(transaction.getId())) {
|
||||
newLoanTransactions.add(transaction.toMapData(currencyCode));
|
||||
}
|
||||
};
|
||||
this.loanTransactions.stream().filter(chargeOffDateCriteria).forEach(addTransactionForAccounting);
|
||||
}
|
||||
|
||||
public Map<String, Object> deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
||||
|
||||
|
||||
+14
-10
@@ -166,12 +166,7 @@ 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);
|
||||
|
||||
@@ -427,10 +422,19 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
|
||||
final MonetaryCurrency currency = loanAccount.getCurrency();
|
||||
|
||||
final Map<String, Object> accountingBridgeData = loanAccount.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
accountingBridgeData.put("isLoanToLoanTransfer", isLoanToLoanTransfer);
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
|
||||
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
|
||||
if (loanAccount.isChargedOff()) {
|
||||
accountingBridgeData = loanAccount.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
} else {
|
||||
accountingBridgeData.add(loanAccount.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer));
|
||||
}
|
||||
for (Map<String, Object> accountingData : accountingBridgeData) {
|
||||
accountingData.put("isLoanToLoanTransfer", isLoanToLoanTransfer);
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkClientOrGroupActive(final Loan loan) {
|
||||
|
||||
+12
-9
@@ -1080,12 +1080,6 @@ 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",
|
||||
@@ -1935,9 +1929,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
boolean isAccountTransfer = false;
|
||||
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
|
||||
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
|
||||
if (loan.isChargedOff()) {
|
||||
accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
} else {
|
||||
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer));
|
||||
}
|
||||
for (Map<String, Object> accountingData : accountingBridgeData) {
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
-22
@@ -7208,14 +7208,6 @@ public class ClientLoanIntegrationTest {
|
||||
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"));
|
||||
@@ -7262,20 +7254,6 @@ public class ClientLoanIntegrationTest {
|
||||
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));
|
||||
|
||||
+1263
File diff suppressed because it is too large
Load Diff
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.integrationtests.common.loans;
|
||||
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
||||
|
||||
public class LoanProductHelper extends IntegrationTest {
|
||||
|
||||
public LoanProductHelper() {}
|
||||
|
||||
public PostLoanProductsResponse createLoanProduct(PostLoanProductsRequest request) {
|
||||
return ok(fineract().loanProducts.createLoanProduct(request));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user