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.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; 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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -43,17 +44,19 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
final MonetaryCurrency loanCurrency = loan.getCurrency(); final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null; LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template(); CollectionData collectionData = CollectionData.template();
BigDecimal amountAvailable = BigDecimal.ZERO; BigDecimal amountAvailable;
BigDecimal outstandingAmount = BigDecimal.ZERO; BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false; boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false; boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = 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()); log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// Get the oldest overdue installment if exists one // Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) { if (!installment.isObligationsMet()) {
if (installment.getDueDate().isBefore(businessDate)) { if (installment.getDueDate().isBefore(businessDate)) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(), log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate()); installment.getDueDate());
@@ -66,10 +69,17 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount(); amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
for (LoanTransactionToRepaymentScheduleMapping mappingInstallment : installment boolean isLatestInstallment = Objects.equals(installment.getId(), latestInstallment.getId());
.getLoanTransactionToRepaymentScheduleMappings()) { for (LoanTransaction loanTransaction : chargebackTransactions) {
final LoanTransaction loanTransaction = mappingInstallment.getLoanTransaction(); boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = loanTransaction.getTransactionDate().isEqual(
if (loanTransaction.isChargeback()) { 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()); amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) { if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
overdueSinceDate = loanTransaction.getTransactionDate(); overdueSinceDate = loanTransaction.getTransactionDate();
@@ -78,29 +88,33 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
} }
} }
} }
} } else if (!firstNotYetDueInstallment) {
} else if (!firstNotYetDueInstallment) { log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(), installment.getDueDate());
installment.getDueDate()); firstNotYetDueInstallment = true;
firstNotYetDueInstallment = true; amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount(); log.debug("Amount available {}", amountAvailable);
log.debug("Amount available {}", amountAvailable); for (LoanTransaction loanTransaction : chargebackTransactions) {
for (LoanTransactionToRepaymentScheduleMapping mappingInstallment : installment boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = loanTransaction.getTransactionDate().isEqual(
.getLoanTransactionToRepaymentScheduleMappings()) { installment.getFromDate()) || loanTransaction.getTransactionDate().isAfter(installment.getFromDate());
final LoanTransaction loanTransaction = mappingInstallment.getLoanTransaction(); boolean isLoanTransactionIsBeforeInstallmentDueDate = loanTransaction.getTransactionDate()
if (loanTransaction.isChargeback() && loanTransaction.getTransactionDate().isBefore(businessDate)) { .isBefore(installment.getDueDate());
log.debug("Loan CB Transaction: {} {} {}", loanTransaction.getId(), loanTransaction.getTransactionDate(), boolean isLoanTransactionIsBeforeBusinessDate = loanTransaction.getTransactionDate().isBefore(businessDate);
loanTransaction.getAmount()); if (isLoanTransactionIsOnOrAfterInstallmentFromDate && isLoanTransactionIsBeforeInstallmentDueDate
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount()); && isLoanTransactionIsBeforeBusinessDate) {
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 && !overdueSinceDateWasSet) { log.debug("Loan CB Transaction: {} {} {}", loanTransaction.getId(), loanTransaction.getTransactionDate(),
overdueSinceDate = loanTransaction.getTransactionDate(); loanTransaction.getAmount());
overdueSinceDateWasSet = true; amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 && !overdueSinceDateWasSet) {
overdueSinceDate = loanTransaction.getTransactionDate();
overdueSinceDateWasSet = true;
}
} }
} }
}
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) { if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
outstandingAmount = outstandingAmount.add(amountAvailable.abs()); outstandingAmount = outstandingAmount.add(amountAvailable.abs());
}
} }
} }
} }
@@ -44,6 +44,7 @@ import java.util.Map;
import java.util.Objects; 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 javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Embedded; import javax.persistence.Embedded;
@@ -1346,8 +1347,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
installment.updateAccrualPortion(interest, fee, penality); installment.updateAccrualPortion(interest, fee, penality);
} }
LoanRepaymentScheduleInstallment lastInstallment = getRepaymentScheduleInstallments() LoanRepaymentScheduleInstallment lastInstallment = getLastLoanRepaymentScheduleInstallment();
.get(getRepaymentScheduleInstallments().size() - 1);
for (LoanTransaction loanTransaction : accruals) { for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getTransactionDate().isAfter(lastInstallment.getDueDate()) && !loanTransaction.isReversed()) { if (loanTransaction.getTransactionDate().isAfter(lastInstallment.getDueDate()) && !loanTransaction.isReversed()) {
loanTransaction.reverse(); loanTransaction.reverse();
@@ -3798,9 +3798,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isReversed()) { if (loanTransaction.isReversed()) {
continue; continue;
} }
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund() if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) {
|| loanTransaction.isChargeback())) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); 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()) { if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())); outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback()) { } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency()); Money transactionOutstanding = loanTransaction.getAmount(getCurrency());
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) { if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding = loanTransaction.getAmount(getCurrency()) transactionOutstanding = loanTransaction.getAmount(getCurrency())
@@ -6277,9 +6278,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode); .determineProcessor(this.transactionProcessingStrategyCode);
final Money overpaidAmount = calculateTotalOverpayment(); // Before Transaction final Money overpaidAmount = calculateTotalOverpayment(); // Before Transaction
if (overpaidAmount.isGreaterThanZero()) {
chargebackTransaction.setOverPayments(overpaidAmount);
}
if (chargebackTransaction.isNotZero(loanCurrency())) { if (chargebackTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(chargebackTransaction); addLoanTransaction(chargebackTransaction);
@@ -7018,4 +7016,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return this.chargedOff; 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.obligationsMet = false;
this.obligationsMetOnDate = null; this.obligationsMetOnDate = null;
if (this.credits != null) {
this.principal = this.principal.subtract(this.credits);
this.credits = null;
}
} }
public void resetAccrualComponents() { 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); updateDueDate(transactionDate);
addToCredits(transactionAmount.getAmount()); addToCredits(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount); 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, public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
final ExternalId externalId) { final ExternalId externalId) {
final PaymentDetail paymentDetail = null; return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(),
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(), paymentDate, null, null, null, null, amount.getAmount(), false, null, externalId);
externalId);
} }
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail, 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.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; 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.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; 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.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; 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.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -93,7 +94,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
wrapper.reprocess(currency, disbursementDate, installments, charges); wrapper.reprocess(currency, disbursementDate, installments, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
final List<LoanTransaction> transactionstoBeProcessed = new ArrayList<>(); final List<LoanTransaction> transactionsToBeProcessed = new ArrayList<>();
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.isChargePayment()) { if (loanTransaction.isChargePayment()) {
List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>(); List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>();
@@ -147,11 +148,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
} else { } else {
transactionstoBeProcessed.add(loanTransaction); transactionsToBeProcessed.add(loanTransaction);
} }
} }
for (final LoanTransaction loanTransaction : transactionstoBeProcessed) { for (final LoanTransaction loanTransaction : transactionsToBeProcessed) {
// TODO: analyze and remove this // TODO: analyze and remove this
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) { if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() { final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@@ -188,13 +189,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else { } else {
loanTransaction.reverse(); createNewTransactionIfNecessary(loanTransaction, newLoanTransaction, currency, changedTransactionDetail);
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);
} }
} }
@@ -203,13 +198,200 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
handleWriteOff(loanTransaction, currency, installments); handleWriteOff(loanTransaction, currency, installments);
} else if (loanTransaction.isRefundForActiveLoan()) { } else if (loanTransaction.isRefundForActiveLoan()) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments, charges); 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; 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 * Provides support for processing the latest transaction (which should be latest transaction) against the loan
* schedule. * schedule.
@@ -524,66 +706,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
@Override @Override
public void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, public void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>(); processCreditTransaction(loanTransaction, overpaidAmount, currency, installments);
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);
}
} }
@Override @Override
@@ -1302,7 +1302,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private void validateLoanTransactionAmountChargeBack(LoanTransaction loanTransaction, LoanTransaction chargebackTransaction) { private void validateLoanTransactionAmountChargeBack(LoanTransaction loanTransaction, LoanTransaction chargebackTransaction) {
BigDecimal actualAmount = BigDecimal.ZERO; BigDecimal actualAmount = BigDecimal.ZERO;
for (LoanTransactionRelation loanTransactionRelation : loanTransaction.getLoanTransactionRelations()) { 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()); actualAmount = actualAmount.add(loanTransactionRelation.getToTransaction().getPrincipalPortion());
} }
} }
@@ -26,10 +26,12 @@ import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -136,6 +138,8 @@ public class LoanDelinquencyDomainServiceTest {
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getLoanProduct()).thenReturn(loanProduct); when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); 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); when(loan.getCurrency()).thenReturn(currency);
CollectionData collectionData = underTest.getOverdueCollectionData(loan); 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) { private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")