FINERACT-1839: Reversal on the loan account after CBR / Chargeback

This commit is contained in:
Adam Saghy
2023-02-03 13:26:13 +01:00
committed by Arnold Gálovics
parent d7acfc1e5d
commit 0a2db35f8d
8 changed files with 492 additions and 112 deletions
@@ -20,6 +20,8 @@ package org.apache.fineract.portfolio.delinquency.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -27,7 +29,6 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,17 +44,19 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal amountAvailable = BigDecimal.ZERO;
BigDecimal amountAvailable;
BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
LoanRepaymentScheduleInstallment latestInstallment = loan.getLastLoanRepaymentScheduleInstallment();
List<LoanTransaction> chargebackTransactions = loan.getLoanTransactions(LoanTransaction::isChargeback);
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) {
if (installment.getDueDate().isBefore(businessDate)) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
@@ -66,10 +69,17 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
for (LoanTransactionToRepaymentScheduleMapping mappingInstallment : installment
.getLoanTransactionToRepaymentScheduleMappings()) {
final LoanTransaction loanTransaction = mappingInstallment.getLoanTransaction();
if (loanTransaction.isChargeback()) {
boolean isLatestInstallment = Objects.equals(installment.getId(), latestInstallment.getId());
for (LoanTransaction loanTransaction : chargebackTransactions) {
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = loanTransaction.getTransactionDate().isEqual(
installment.getFromDate()) || loanTransaction.getTransactionDate().isAfter(installment.getFromDate());
boolean isLoanTransactionIsBeforeNotLastInstallmentDueDate = !isLatestInstallment
&& loanTransaction.getTransactionDate().isBefore(installment.getDueDate());
boolean isLoanTransactionIsOnOrBeforeLastInstallmentDueDate = isLatestInstallment
&& (loanTransaction.getTransactionDate().isEqual(installment.getDueDate())
|| loanTransaction.getTransactionDate().isBefore(installment.getDueDate()));
if (isLoanTransactionIsOnOrAfterInstallmentFromDate && (isLoanTransactionIsBeforeNotLastInstallmentDueDate
|| isLoanTransactionIsOnOrBeforeLastInstallmentDueDate)) {
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
overdueSinceDate = loanTransaction.getTransactionDate();
@@ -78,29 +88,33 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
}
}
}
}
} else if (!firstNotYetDueInstallment) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
firstNotYetDueInstallment = true;
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
log.debug("Amount available {}", amountAvailable);
for (LoanTransactionToRepaymentScheduleMapping mappingInstallment : installment
.getLoanTransactionToRepaymentScheduleMappings()) {
final LoanTransaction loanTransaction = mappingInstallment.getLoanTransaction();
if (loanTransaction.isChargeback() && loanTransaction.getTransactionDate().isBefore(businessDate)) {
log.debug("Loan CB Transaction: {} {} {}", loanTransaction.getId(), loanTransaction.getTransactionDate(),
loanTransaction.getAmount());
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 && !overdueSinceDateWasSet) {
overdueSinceDate = loanTransaction.getTransactionDate();
overdueSinceDateWasSet = true;
} else if (!firstNotYetDueInstallment) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
firstNotYetDueInstallment = true;
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
log.debug("Amount available {}", amountAvailable);
for (LoanTransaction loanTransaction : chargebackTransactions) {
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = loanTransaction.getTransactionDate().isEqual(
installment.getFromDate()) || loanTransaction.getTransactionDate().isAfter(installment.getFromDate());
boolean isLoanTransactionIsBeforeInstallmentDueDate = loanTransaction.getTransactionDate()
.isBefore(installment.getDueDate());
boolean isLoanTransactionIsBeforeBusinessDate = loanTransaction.getTransactionDate().isBefore(businessDate);
if (isLoanTransactionIsOnOrAfterInstallmentFromDate && isLoanTransactionIsBeforeInstallmentDueDate
&& isLoanTransactionIsBeforeBusinessDate) {
log.debug("Loan CB Transaction: {} {} {}", loanTransaction.getId(), loanTransaction.getTransactionDate(),
loanTransaction.getAmount());
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 && !overdueSinceDateWasSet) {
overdueSinceDate = loanTransaction.getTransactionDate();
overdueSinceDateWasSet = true;
}
}
}
}
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
outstandingAmount = outstandingAmount.add(amountAvailable.abs());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
outstandingAmount = outstandingAmount.add(amountAvailable.abs());
}
}
}
}
@@ -44,6 +44,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
@@ -1346,8 +1347,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
installment.updateAccrualPortion(interest, fee, penality);
}
LoanRepaymentScheduleInstallment lastInstallment = getRepaymentScheduleInstallments()
.get(getRepaymentScheduleInstallments().size() - 1);
LoanRepaymentScheduleInstallment lastInstallment = getLastLoanRepaymentScheduleInstallment();
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getTransactionDate().isAfter(lastInstallment.getDueDate()) && !loanTransaction.isReversed()) {
loanTransaction.reverse();
@@ -3798,9 +3798,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isReversed()) {
continue;
}
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund()
|| loanTransaction.isChargeback())) {
if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
} else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
}
}
@@ -5804,7 +5805,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback()) {
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency());
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding = loanTransaction.getAmount(getCurrency())
@@ -6277,9 +6278,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final Money overpaidAmount = calculateTotalOverpayment(); // Before Transaction
if (overpaidAmount.isGreaterThanZero()) {
chargebackTransaction.setOverPayments(overpaidAmount);
}
if (chargebackTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(chargebackTransaction);
@@ -7018,4 +7016,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return this.chargedOff;
}
public LoanRepaymentScheduleInstallment getLastLoanRepaymentScheduleInstallment() {
return getRepaymentScheduleInstallments().get(getRepaymentScheduleInstallments().size() - 1);
}
public List<LoanTransaction> getLoanTransactions(Predicate<LoanTransaction> predicate) {
return getLoanTransactions().stream().filter(predicate).toList();
}
}
@@ -393,6 +393,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.obligationsMet = false;
this.obligationsMetOnDate = null;
if (this.credits != null) {
this.principal = this.principal.subtract(this.credits);
this.credits = null;
}
}
public void resetAccrualComponents() {
@@ -836,7 +840,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
}
}
public void updateDueChargeback(final LocalDate transactionDate, final Money transactionAmount) {
public void updateDueAndCredits(final LocalDate transactionDate, final Money transactionAmount) {
updateDueDate(transactionDate);
addToCredits(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount);
@@ -291,9 +291,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
final ExternalId externalId) {
final PaymentDetail paymentDetail = null;
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(), paymentDate,
externalId);
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(),
null, null, null, null, amount.getAmount(), false, null, externalId);
}
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
@@ -25,7 +25,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
@@ -34,7 +36,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -93,7 +94,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
wrapper.reprocess(currency, disbursementDate, installments, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
final List<LoanTransaction> transactionstoBeProcessed = new ArrayList<>();
final List<LoanTransaction> transactionsToBeProcessed = new ArrayList<>();
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.isChargePayment()) {
List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>();
@@ -147,11 +148,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
} else {
transactionstoBeProcessed.add(loanTransaction);
transactionsToBeProcessed.add(loanTransaction);
}
}
for (final LoanTransaction loanTransaction : transactionstoBeProcessed) {
for (final LoanTransaction loanTransaction : transactionsToBeProcessed) {
// TODO: analyze and remove this
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@@ -188,13 +189,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
// Adding Replayed relation from newly created transaction to reversed transaction
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
createNewTransactionIfNecessary(loanTransaction, newLoanTransaction, currency, changedTransactionDetail);
}
}
@@ -203,13 +198,200 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
handleWriteOff(loanTransaction, currency, installments);
} else if (loanTransaction.isRefundForActiveLoan()) {
loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments, charges);
} else if (loanTransaction.isCreditBalanceRefund()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed);
} else if (loanTransaction.isChargeback()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed);
reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed);
}
}
reprocessInstallments(installments, currency);
return changedTransactionDetail;
}
private void reprocessChargebackTransactionRelation(ChangedTransactionDetail changedTransactionDetail,
List<LoanTransaction> transactionsToBeProcessed) {
List<LoanTransaction> mergedTransactionList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
for (Map.Entry<Long, LoanTransaction> entry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
if (entry.getValue().isChargeback()) {
for (LoanTransaction loanTransaction : mergedTransactionList) {
if (loanTransaction.isReversed()) {
continue;
}
LoanTransactionRelation newLoanTransactionRelation = null;
LoanTransactionRelation oldLoanTransactionRelation = null;
for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) {
if (entry.getKey().equals(transactionRelation.getToTransaction().getId())
&& LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType())) {
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, entry.getValue(),
LoanTransactionRelationTypeEnum.CHARGEBACK);
oldLoanTransactionRelation = transactionRelation;
break;
}
}
if (newLoanTransactionRelation != null) {
loanTransaction.getLoanTransactionRelations().add(newLoanTransactionRelation);
loanTransaction.getLoanTransactionRelations().remove(oldLoanTransactionRelation);
}
}
}
}
}
private void reprocessInstallments(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) {
installments.remove(lastInstallment);
}
}
private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
List<LoanTransaction> transactionsToBeProcessed) {
// pass through for new transactions
if (loanTransaction.getId() == null) {
return;
}
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
List<LoanTransaction> mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency);
processCreditTransaction(newLoanTransaction, overpaidAmount, currency, installments);
createNewTransactionIfNecessary(loanTransaction, newLoanTransaction, currency, changedTransactionDetail);
}
private List<LoanTransaction> getMergedTransactionList(List<LoanTransaction> transactionList,
ChangedTransactionDetail changedTransactionDetail) {
List<LoanTransaction> mergedList = new ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values());
mergedList.addAll(new ArrayList<>(transactionList));
return mergedList;
}
private void createNewTransactionIfNecessary(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
MonetaryCurrency currency, ChangedTransactionDetail changedTransactionDetail) {
if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
// Adding Replayed relation from newly created transaction to reversed transaction
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
}
}
private Money calculateOverpaidAmount(LoanTransaction loanTransaction, List<LoanTransaction> transactions,
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
Money totalPaidInRepayments = Money.zero(currency);
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency));
}
for (final LoanTransaction transaction : transactions) {
if (transaction.isReversed()) {
continue;
}
if (transaction.equals(loanTransaction)) {
// We want to process only the transactions prior to the actual one
break;
}
if (transaction.isRefund() || transaction.isRefundForActiveLoan()) {
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency));
} else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) {
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency));
} else if (transaction.isRepaymentType()) {
totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency));
}
}
// if total paid in transactions higher than repayment schedule then
// theres an overpayment.
return MathUtil.negativeToZero(totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments));
}
private void processCreditTransaction(LoanTransaction loanTransaction, Money overpaidAmount, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) {
loanTransaction.resetDerivedComponents();
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
installments.sort(byDate);
final Money zeroMoney = Money.zero(currency);
Money transactionAmount = loanTransaction.getAmount(currency);
Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaidAmount));
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(principalPortion));
loanTransaction.updateOverPayments(repaidAmount);
loanTransaction.updateComponents(principalPortion, zeroMoney, zeroMoney, zeroMoney);
if (principalPortion.isGreaterThanZero()) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && currentInstallment.getDueDate().isAfter(transactionDate)) {
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
loanTransactionMapped = true;
break;
// If already exists an additional installment just update the due date and
// principal from the Loan charge back transaction
} else if (currentInstallment.isAdditional()) {
currentInstallment.updateDueAndCredits(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
loanTransactionMapped = true;
break;
}
}
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
} else {
Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), false, null);
installment.markAsAdditional();
installment.addToCredits(transactionAmount.getAmount());
loan.addLoanRepaymentScheduleInstallment(installment);
if (repaidAmount.isGreaterThanZero()) {
installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
}
}
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}
}
/**
* Provides support for processing the latest transaction (which should be latest transaction) against the loan
* schedule.
@@ -524,66 +706,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
@Override
public void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments, byDate);
final Money zeroMoney = Money.zero(currency);
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
if (overpaidAmount.isGreaterThanZero()) {
transactionAmountUnprocessed = loanTransaction.getAmount(currency).minus(overpaidAmount);
if (transactionAmountUnprocessed.isLessThanZero()) {
transactionAmountUnprocessed = zeroMoney;
}
}
if (transactionAmountUnprocessed.isGreaterThanZero()) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && currentInstallment.getDueDate().isAfter(transactionDate)) {
currentInstallment.addToCredits(transactionAmountUnprocessed.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmountUnprocessed);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
transactionAmountUnprocessed, zeroMoney, zeroMoney, zeroMoney));
loanTransactionMapped = true;
break;
// If already exists an additional installment just update the due date and
// principal from the Loan charge back transaction
} else if (currentInstallment.isAdditional()) {
currentInstallment.updateDueChargeback(transactionDate, transactionAmountUnprocessed);
loanTransactionMapped = true;
break;
}
}
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
Loan loan = loanTransaction.getLoan();
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
pastDueDate, transactionDate, transactionAmountUnprocessed.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), false, compoundingDetails);
installment.markAsAdditional();
installment.addToCredits(transactionAmountUnprocessed.getAmount());
loan.addLoanRepaymentScheduleInstallment(installment);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
transactionAmountUnprocessed, zeroMoney, zeroMoney, zeroMoney));
}
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}
processCreditTransaction(loanTransaction, overpaidAmount, currency, installments);
}
@Override
@@ -1302,7 +1302,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private void validateLoanTransactionAmountChargeBack(LoanTransaction loanTransaction, LoanTransaction chargebackTransaction) {
BigDecimal actualAmount = BigDecimal.ZERO;
for (LoanTransactionRelation loanTransactionRelation : loanTransaction.getLoanTransactionRelations()) {
if (loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK)) {
if (loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK)
&& loanTransactionRelation.getToTransaction().isNotReversed()) {
actualAmount = actualAmount.add(loanTransactionRelation.getToTransaction().getPrincipalPortion());
}
}
@@ -26,10 +26,12 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -136,6 +138,8 @@ public class LoanDelinquencyDomainServiceTest {
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Collections.emptyList());
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
when(loan.getCurrency()).thenReturn(currency);
CollectionData collectionData = underTest.getOverdueCollectionData(loan);
@@ -7137,6 +7137,235 @@ public class ClientLoanIntegrationTest {
}
}
@Test
public void testCreditBalanceRefundAfterMaturityWithReverseReplayOfRepayments() {
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.10.10").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();
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount,
expenseAccount, overpaymentAccount);
final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011");
final Integer loanID = applyForLoanApplication(clientID, loanProductID);
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);
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000");
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
this.loanTransactionHelper.makeRepayment("04 September 2022", Float.parseFloat("100"), loanID);
this.loanTransactionHelper.makeRepayment("05 September 2022", Float.parseFloat("1100"), loanID);
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(200.0, loanDetails.getTotalOverpaid());
assertTrue(loanDetails.getStatus().getOverpaid());
this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
.transactionAmount(200.0).transactionDate("10 October 2022").dateFormat("dd MMMM yyyy").locale("en"));
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size());
assertEquals(1000, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertEquals(100.0, loanDetails.getTransactions().get(1).getAmount());
assertEquals(100.0, loanDetails.getTransactions().get(1).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTransactions().get(1).getDate());
assertEquals(900.0, loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
assertEquals(1100.0, loanDetails.getTransactions().get(2).getAmount());
assertEquals(900.0, loanDetails.getTransactions().get(2).getPrincipalPortion());
assertEquals(200.0, loanDetails.getTransactions().get(2).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 5), loanDetails.getTransactions().get(2).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
assertEquals(200.0, loanDetails.getTransactions().get(3).getAmount());
assertEquals(200.0, loanDetails.getTransactions().get(3).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 10, 10), loanDetails.getTransactions().get(3).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
this.loanTransactionHelper.reverseLoanTransaction(loanDetails.getId(), loanDetails.getTransactions().get(1).getId(),
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy").transactionAmount(0.0)
.transactionDate("10 October 2022").locale("en"));
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(100.0, loanDetails.getTransactions().get(1).getAmount());
assertEquals(100.0, loanDetails.getTransactions().get(1).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTransactions().get(1).getDate());
assertTrue(loanDetails.getTransactions().get(1).getManuallyReversed());
assertEquals(1100.0, loanDetails.getTransactions().get(2).getAmount());
assertEquals(1000.0, loanDetails.getTransactions().get(2).getPrincipalPortion());
assertEquals(100.0, loanDetails.getTransactions().get(2).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 5), loanDetails.getTransactions().get(2).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
assertEquals(1, loanDetails.getTransactions().get(2).getTransactionRelations().size());
assertEquals(200.0, loanDetails.getTransactions().get(3).getAmount());
assertEquals(100.0, loanDetails.getTransactions().get(3).getPrincipalPortion());
assertEquals(100.0, loanDetails.getTransactions().get(3).getOverpaymentPortion());
assertEquals(100.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(LocalDate.of(2022, 10, 10), loanDetails.getTransactions().get(3).getDate());
assertEquals(1, loanDetails.getTransactions().get(3).getTransactionRelations().size());
assertTrue(loanDetails.getStatus().getActive());
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().size());
assertEquals(1000, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertTrue(loanDetails.getRepaymentSchedule().getPeriods().get(1).getComplete());
assertEquals(200, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalDue());
assertFalse(loanDetails.getRepaymentSchedule().getPeriods().get(2).getComplete());
assertEquals(100.0, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalPaid());
assertEquals(100.0, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalOutstanding());
} finally {
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec, this.responseSpec, false);
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec, this.responseSpec, false);
}
}
@Test
public void testCreditBalanceRefundBeforeMaturityWithReverseReplayOfRepaymentsAndRefund() {
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.10.10").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();
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount,
expenseAccount, overpaymentAccount);
final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011");
final Integer loanID = applyForLoanApplication(clientID, loanProductID);
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);
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000");
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
this.loanTransactionHelper.makeRepayment("04 September 2022", Float.parseFloat("500"), loanID);
this.loanTransactionHelper.makeRepayment("05 September 2022", Float.parseFloat("700"), loanID);
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(200.0, loanDetails.getTotalOverpaid());
assertTrue(loanDetails.getStatus().getOverpaid());
this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
.transactionAmount(200.0).transactionDate("06 September 2022").dateFormat("dd MMMM yyyy").locale("en"));
this.loanTransactionHelper.makeMerchantIssuedRefund((long) loanID, new PostLoansLoanIdTransactionsRequest().locale("en")
.dateFormat("dd MMMM yyyy").transactionDate("07 September 2022").transactionAmount(500.0));
this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
.transactionAmount(500.0).transactionDate("08 September 2022").dateFormat("dd MMMM yyyy").locale("en"));
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size());
assertEquals(1000, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertEquals(500.0, loanDetails.getTransactions().get(1).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(1).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTransactions().get(1).getDate());
assertEquals(500.0, loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
assertEquals(700.0, loanDetails.getTransactions().get(2).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(2).getPrincipalPortion());
assertEquals(200.0, loanDetails.getTransactions().get(2).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 5), loanDetails.getTransactions().get(2).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
assertEquals(200.0, loanDetails.getTransactions().get(3).getAmount());
assertEquals(200.0, loanDetails.getTransactions().get(3).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 6), loanDetails.getTransactions().get(3).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(500.0, loanDetails.getTransactions().get(4).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(4).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 7), loanDetails.getTransactions().get(4).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(4).getOutstandingLoanBalance());
assertEquals(500.0, loanDetails.getTransactions().get(5).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(5).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 8), loanDetails.getTransactions().get(5).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(5).getOutstandingLoanBalance());
this.loanTransactionHelper.reverseLoanTransaction(loanDetails.getId(), loanDetails.getTransactions().get(2).getId(),
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy").transactionAmount(0.0)
.transactionDate("07 September 2022").locale("en"));
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(500.0, loanDetails.getTransactions().get(1).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(1).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTransactions().get(1).getDate());
assertEquals(500.0, loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
assertEquals(700.0, loanDetails.getTransactions().get(2).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(2).getPrincipalPortion());
assertEquals(200.0, loanDetails.getTransactions().get(2).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 9, 5), loanDetails.getTransactions().get(2).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
assertTrue(loanDetails.getTransactions().get(2).getManuallyReversed());
assertEquals(200.0, loanDetails.getTransactions().get(3).getAmount());
assertEquals(200.0, loanDetails.getTransactions().get(3).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 6), loanDetails.getTransactions().get(3).getDate());
assertEquals(700.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(1, loanDetails.getTransactions().get(3).getTransactionRelations().size());
assertEquals(500.0, loanDetails.getTransactions().get(4).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(4).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 7), loanDetails.getTransactions().get(4).getDate());
assertEquals(200.0, loanDetails.getTransactions().get(4).getOutstandingLoanBalance());
assertEquals(1, loanDetails.getTransactions().get(4).getTransactionRelations().size());
assertEquals(500.0, loanDetails.getTransactions().get(5).getAmount());
assertEquals(500.0, loanDetails.getTransactions().get(5).getPrincipalPortion());
assertEquals(LocalDate.of(2022, 9, 8), loanDetails.getTransactions().get(5).getDate());
assertEquals(700.0, loanDetails.getTransactions().get(5).getOutstandingLoanBalance());
assertEquals(1, loanDetails.getTransactions().get(5).getTransactionRelations().size());
assertTrue(loanDetails.getStatus().getActive());
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size());
assertEquals(1700, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertFalse(loanDetails.getRepaymentSchedule().getPeriods().get(1).getComplete());
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid());
assertEquals(700.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
} finally {
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec, this.responseSpec, false);
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec, this.responseSpec, false);
}
}
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")