From 0a2db35f8d791cf3d31b20a53862c99b82a4fbde Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 2 Feb 2023 20:34:03 +0100 Subject: [PATCH] FINERACT-1839: Reversal on the loan account after CBR / Chargeback --- .../LoanDelinquencyDomainServiceImpl.java | 68 +++-- .../portfolio/loanaccount/domain/Loan.java | 22 +- .../LoanRepaymentScheduleInstallment.java | 6 +- .../loanaccount/domain/LoanTransaction.java | 5 +- ...RepaymentScheduleTransactionProcessor.java | 267 +++++++++++++----- ...WritePlatformServiceJpaRepositoryImpl.java | 3 +- .../LoanDelinquencyDomainServiceTest.java | 4 + .../ClientLoanIntegrationTest.java | 229 +++++++++++++++ 8 files changed, 492 insertions(+), 112 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java index 1d569c90b..72086c72f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java @@ -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 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()); + } } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index e9ccab2fd..a1307afc2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -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 getLoanTransactions(Predicate predicate) { + return getLoanTransactions().stream().filter(predicate).toList(); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 9e864bb51..ab2b6326d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 9a0fbcc60..2ae376090 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index ecb992846..46d79637a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -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 transactionstoBeProcessed = new ArrayList<>(); + final List transactionsToBeProcessed = new ArrayList<>(); for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { if (loanTransaction.isChargePayment()) { List 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 byDate = new Comparator() { @@ -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 transactionsToBeProcessed) { + + List mergedTransactionList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); + for (Map.Entry 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 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 installments, + List transactionsToBeProcessed) { + // pass through for new transactions + if (loanTransaction.getId() == null) { + return; + } + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); + + List mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); + Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency); + processCreditTransaction(newLoanTransaction, overpaidAmount, currency, installments); + createNewTransactionIfNecessary(loanTransaction, newLoanTransaction, currency, changedTransactionDetail); + } + + private List getMergedTransactionList(List transactionList, + ChangedTransactionDetail changedTransactionDetail) { + List 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 transactions, + List 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 installments) { + loanTransaction.resetDerivedComponents(); + List transactionMappings = new ArrayList<>(); + final Comparator 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 installments) { - List transactionMappings = new ArrayList<>(); - final Comparator byDate = new Comparator() { - - @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 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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index afac84139..a6a358fdb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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()); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java index e462aeb67..ef48eb58a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java @@ -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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index c69acfd3f..79adfb007 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -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 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 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")