FINERACT-1971: Wrong GL entries on refund with reverse replay and charge off
This commit is contained in:
committed by
Adam Saghy
parent
f0deb111cb
commit
369b809700
+26
-4
@@ -60,6 +60,7 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -4329,18 +4330,39 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
LoanTransaction chargeOffTransaction = this.loanTransactions.stream().filter(LoanTransaction::isChargeOff)
|
LoanTransaction chargeOffTransaction = this.loanTransactions.stream().filter(LoanTransaction::isChargeOff)
|
||||||
.filter(LoanTransaction::isNotReversed).findFirst().get();
|
.filter(LoanTransaction::isNotReversed).findFirst().get();
|
||||||
|
|
||||||
|
LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction);
|
||||||
|
|
||||||
this.loanTransactions.stream().filter(chargeOffDateCriteria).forEach(transaction -> {
|
this.loanTransactions.stream().filter(chargeOffDateCriteria).forEach(transaction -> {
|
||||||
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
|
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
|
||||||
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
|
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
|
||||||
List<Map<String, Object>> targetList = (transaction.getId().compareTo(chargeOffTransaction.getId()) < 0)
|
List<Map<String, Object>> targetList = null;
|
||||||
? newLoanTransactionsBeforeChargeOff
|
if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction)) {
|
||||||
: newLoanTransactionsAfterChargeOff;
|
// reversed transactions
|
||||||
if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) || !isExistingTransaction) {
|
LoanTransaction originalTransaction = getOriginalTransactionIfReverseReplayed(transaction);
|
||||||
|
targetList = originalTransaction.happenedBefore(originalChargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
|
||||||
|
: newLoanTransactionsAfterChargeOff;
|
||||||
|
|
||||||
|
} else if (!isExistingTransaction) {
|
||||||
|
// new and replayed transactions
|
||||||
|
targetList = transaction.happenedBefore(chargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
|
||||||
|
: newLoanTransactionsAfterChargeOff;
|
||||||
|
}
|
||||||
|
if (targetList != null) {
|
||||||
targetList.add(transaction.toMapData(currencyCode));
|
targetList.add(transaction.toMapData(currencyCode));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoanTransaction getOriginalTransactionIfReverseReplayed(LoanTransaction loanTransaction) {
|
||||||
|
if (!loanTransaction.getLoanTransactionRelations().isEmpty()) {
|
||||||
|
return loanTransaction.getLoanTransactionRelations().stream()
|
||||||
|
.filter(tr -> LoanTransactionRelationTypeEnum.REPLAYED.equals(tr.getRelationType())).map(tr -> tr.getToTransaction())
|
||||||
|
.collect(Collectors.toList()).stream().sorted(Comparator.comparingLong(LoanTransaction::getId)).findFirst()
|
||||||
|
.orElse(loanTransaction);
|
||||||
|
}
|
||||||
|
return loanTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
|
public Map<String, Object> deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
|
||||||
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
||||||
|
|
||||||
|
|||||||
+19
@@ -1016,6 +1016,25 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
this.loanReAgeParameter = loanReAgeParameter;
|
this.loanReAgeParameter = loanReAgeParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean happenedBefore(LoanTransaction loanTransaction) {
|
||||||
|
// compare transaction date, creation date and then transaction id
|
||||||
|
if (DateUtils.isBefore(getTransactionDate(), loanTransaction.getTransactionDate())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (DateUtils.isEqual(getTransactionDate(), loanTransaction.getTransactionDate())) {
|
||||||
|
if (DateUtils.isBefore(getCreatedDateTime(), loanTransaction.getCreatedDateTime())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (DateUtils.isEqual(getCreatedDateTime(), loanTransaction.getCreatedDateTime())) {
|
||||||
|
if (getId().compareTo(loanTransaction.getId()) < 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
||||||
// this is never stored in a Collection.
|
// this is never stored in a Collection.
|
||||||
}
|
}
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class LoanChargeOffAccountingEntriesForReverseReplayedTransactionsTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJournalEntriesForChargeOffLoanWithMultipleReverseReplay() {
|
||||||
|
runAt("24 May 2024", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
// Create DelinquencyBuckets
|
||||||
|
Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
|
||||||
|
Pair.of(1, 10), //
|
||||||
|
Pair.of(11, 30), //
|
||||||
|
Pair.of(31, 60), //
|
||||||
|
Pair.of(61, null)//
|
||||||
|
));
|
||||||
|
|
||||||
|
// create loan product
|
||||||
|
PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(3)//
|
||||||
|
.repaymentEvery(15)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue())//
|
||||||
|
.disallowExpectedDisbursements(true)//
|
||||||
|
.multiDisburseLoan(true)//
|
||||||
|
.enableDownPayment(true)//
|
||||||
|
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25.0))//
|
||||||
|
.enableAutoRepaymentForDownPayment(true);
|
||||||
|
|
||||||
|
loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue());
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "24 May 2024", 1000.0, 3);
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(200), "24 May 2024");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024"), //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// set business date 25 May
|
||||||
|
updateBusinessDate("25 May 2024");
|
||||||
|
|
||||||
|
// reverse downpayment transaction
|
||||||
|
Long downPaymentTransactionId = getTransactionId(loanId, "Down Payment", "24 May 2024");
|
||||||
|
loanTransactionHelper.reverseLoanTransaction(loanId, downPaymentTransactionId,
|
||||||
|
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("25 May 2024")
|
||||||
|
.transactionAmount(0.0).locale("en"));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// set business date 26 May
|
||||||
|
updateBusinessDate("26 May 2024");
|
||||||
|
// charge-off loan
|
||||||
|
Long chargeOffTransactionId = chargeOffLoan(loanId, "26 May 2024");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(200.0, "Charge-off", "26 May 2024", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, false) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// make backdated repayment on 25 May
|
||||||
|
Long repaymentTransactionId = addRepaymentForLoan(loanId, 10.0, "25 May 2024");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(10.0, "Repayment", "25 May 2024", 190.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(190.0, "Charge-off", "26 May 2024", 0.0, 190.0, 0.0, 0.0, 0.0, 0.0, 0.0, false));
|
||||||
|
|
||||||
|
// make refund on 26 May equal to disbursal amount
|
||||||
|
String merchantIssuedRefundExternalId = UUID.randomUUID().toString();
|
||||||
|
Long merchantIssuedRefundId = loanTransactionHelper.makeMerchantIssuedRefund(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("26 May 2024").locale("en")
|
||||||
|
.transactionAmount(200.0).externalId(merchantIssuedRefundExternalId))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(10.0, "Repayment", "25 May 2024", 190.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(190.0, "Charge-off", "26 May 2024", 0.0, 190.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(200.0, "Merchant Issued Refund", "26 May 2024", 0.0, 190.0, 0.0, 0.0, 0.0, 0.0, 10.0, false));
|
||||||
|
|
||||||
|
// verify loan status is overpaid
|
||||||
|
verifyLoanStatus(loanId, LoanStatus.OVERPAID);
|
||||||
|
// verify journal entries
|
||||||
|
|
||||||
|
// repayment
|
||||||
|
verifyTRJournalEntries(repaymentTransactionId, //
|
||||||
|
credit(loansReceivableAccount, 10), //
|
||||||
|
debit(fundSource, 10) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// charge off
|
||||||
|
verifyTRJournalEntries(chargeOffTransactionId, //
|
||||||
|
credit(loansReceivableAccount, 200), //
|
||||||
|
debit(chargeOffExpenseAccount, 200), //
|
||||||
|
debit(loansReceivableAccount, 200), //
|
||||||
|
credit(chargeOffExpenseAccount, 200) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// refund
|
||||||
|
verifyTRJournalEntries(merchantIssuedRefundId, //
|
||||||
|
credit(chargeOffExpenseAccount, 190), //
|
||||||
|
credit(overpaymentAccount, 10), //
|
||||||
|
debit(fundSource, 200)
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
// set business date 27 May
|
||||||
|
updateBusinessDate("27 May 2024");
|
||||||
|
|
||||||
|
// CBR
|
||||||
|
loanTransactionHelper.makeCreditBalanceRefund(loanId, new PostLoansLoanIdTransactionsRequest().transactionDate("27 May 2024")
|
||||||
|
.dateFormat(DATETIME_PATTERN).transactionAmount(10.0).locale("en"));
|
||||||
|
|
||||||
|
// verify loan status is closed
|
||||||
|
verifyLoanStatus(loanId, LoanStatus.CLOSED_OBLIGATIONS_MET);
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(10.0, "Repayment", "25 May 2024", 190.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(190.0, "Charge-off", "26 May 2024", 0.0, 190.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(200.0, "Merchant Issued Refund", "26 May 2024", 0.0, 190.0, 0.0, 0.0, 0.0, 0.0, 10.0, false), //
|
||||||
|
transaction(10.0, "Credit Balance Refund", "27 May 2024", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, false));
|
||||||
|
|
||||||
|
// reverse backdated repayment
|
||||||
|
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentTransactionId,
|
||||||
|
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("27 May 2024")
|
||||||
|
.transactionAmount(0.0).locale("en"));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(200.0, "Disbursement", "24 May 2024", 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(50.0, "Down Payment", "24 May 2024", 150.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(10.0, "Repayment", "25 May 2024", 190.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), //
|
||||||
|
transaction(200.0, "Charge-off", "26 May 2024", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(200.0, "Merchant Issued Refund", "26 May 2024", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||||
|
transaction(10.0, "Credit Balance Refund", "27 May 2024", 10.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, false));
|
||||||
|
|
||||||
|
// verify journal entries
|
||||||
|
|
||||||
|
// repayment
|
||||||
|
verifyTRJournalEntries(repaymentTransactionId, //
|
||||||
|
credit(loansReceivableAccount, 10), //
|
||||||
|
debit(fundSource, 10), //
|
||||||
|
debit(loansReceivableAccount, 10), //
|
||||||
|
credit(fundSource, 10) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// replayed merchant issued refund
|
||||||
|
Long replayedMerchantIssuedRefundId = loanTransactionHelper.getLoanTransactionDetails(loanId, merchantIssuedRefundExternalId)
|
||||||
|
.getId();
|
||||||
|
|
||||||
|
verifyTRJournalEntries(replayedMerchantIssuedRefundId, credit(chargeOffExpenseAccount, 200), //
|
||||||
|
debit(fundSource, 200) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify journal entries for reversed refund
|
||||||
|
verifyTRJournalEntries(merchantIssuedRefundId, //
|
||||||
|
credit(chargeOffExpenseAccount, 190), //
|
||||||
|
credit(overpaymentAccount, 10), //
|
||||||
|
debit(fundSource, 200), //
|
||||||
|
debit(chargeOffExpenseAccount, 190), //
|
||||||
|
debit(overpaymentAccount, 10), //
|
||||||
|
credit(fundSource, 200));
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user