FINERACT-2081: Loan account data additional fields for summary and delinquency

This commit is contained in:
Jose Alberto Hernandez
2024-07-11 17:41:57 +02:00
committed by Adam Saghy
parent 4ac102f4e0
commit 79f32d32a3
11 changed files with 371 additions and 34 deletions
@@ -64,6 +64,11 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
return CollectionData.template();
}
BigDecimal delinquentPrincipal = BigDecimal.ZERO;
BigDecimal delinquentInterest = BigDecimal.ZERO;
BigDecimal delinquentFee = BigDecimal.ZERO;
BigDecimal delinquentPenalty = BigDecimal.ZERO;
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) {
@@ -71,6 +76,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
delinquentPrincipal = delinquentPrincipal.add(installment.getPrincipalOutstanding(loanCurrency).getAmount());
delinquentInterest = delinquentInterest.add(installment.getInterestOutstanding(loanCurrency).getAmount());
delinquentFee = delinquentFee.add(installment.getFeeChargesOutstanding(loanCurrency).getAmount());
delinquentPenalty = delinquentPenalty.add(installment.getPenaltyChargesOutstanding(loanCurrency).getAmount());
if (!oldestOverdueInstallment) {
log.debug("Oldest installment {} {}", installment.getInstallmentNumber(), installment.getDueDate());
CollectionData overDueInstallmentDelinquentData = calculateDelinquencyDataForOverdueInstallment(loan, installment);
@@ -85,6 +94,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
CollectionData nonOverDueInstallmentDelinquentData = calculateDelinquencyDataForNonOverdueInstallment(loan,
installment);
outstandingAmount = outstandingAmount.add(nonOverDueInstallmentDelinquentData.getDelinquentAmount());
delinquentPrincipal = delinquentPrincipal.add(nonOverDueInstallmentDelinquentData.getDelinquentPrincipal());
delinquentInterest = delinquentInterest.add(nonOverDueInstallmentDelinquentData.getDelinquentInterest());
delinquentFee = delinquentFee.add(nonOverDueInstallmentDelinquentData.getDelinquentFee());
delinquentPenalty = delinquentPenalty.add(nonOverDueInstallmentDelinquentData.getDelinquentPenalty());
if (!overdueSinceDateWasSet) {
overdueSinceDate = nonOverDueInstallmentDelinquentData.getDelinquentDate();
overdueSinceDateWasSet = true;
@@ -110,6 +123,11 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
collectionData.setDelinquentDate(overdueSinceDate);
}
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentPrincipal(delinquentPrincipal);
collectionData.setDelinquentInterest(delinquentInterest);
collectionData.setDelinquentFee(delinquentFee);
collectionData.setDelinquentPenalty(delinquentPenalty);
collectionData.setDelinquentDays(0L);
Long delinquentDays = overdueDays - graceDays;
if (delinquentDays > 0) {
@@ -248,8 +266,17 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
BigDecimal delinquentPrincipal = BigDecimal.ZERO;
BigDecimal delinquentInterest = BigDecimal.ZERO;
BigDecimal delinquentFee = BigDecimal.ZERO;
BigDecimal delinquentPenalty = BigDecimal.ZERO;
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
delinquentPrincipal = delinquentPrincipal.add(installment.getPrincipalOutstanding(loanCurrency).getAmount());
delinquentInterest = delinquentInterest.add(installment.getInterestOutstanding(loanCurrency).getAmount());
delinquentFee = delinquentFee.add(installment.getFeeChargesOutstanding(loanCurrency).getAmount());
delinquentPenalty = delinquentPenalty.add(installment.getPenaltyChargesOutstanding(loanCurrency).getAmount());
overdueSinceDate = installment.getDueDate();
BigDecimal amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
boolean isLatestInstallment = Objects.equals(installment.getId(), latestInstallment.getId());
@@ -272,6 +299,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
}
collectionData.setDelinquentDate(overdueSinceDate);
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentPrincipal(delinquentPrincipal);
collectionData.setDelinquentInterest(delinquentInterest);
collectionData.setDelinquentFee(delinquentFee);
collectionData.setDelinquentPenalty(delinquentPenalty);
return collectionData;
}
@@ -283,6 +314,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
BigDecimal delinquentPrincipal = BigDecimal.ZERO;
BigDecimal delinquentInterest = BigDecimal.ZERO;
BigDecimal delinquentFee = BigDecimal.ZERO;
BigDecimal delinquentPenalty = BigDecimal.ZERO;
List<LoanTransaction> chargebackTransactions = loan.getLoanTransactions(LoanTransaction::isChargeback);
BigDecimal amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
@@ -306,6 +341,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
}
collectionData.setDelinquentDate(overdueSinceDate);
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentPrincipal(delinquentPrincipal);
collectionData.setDelinquentInterest(delinquentInterest);
collectionData.setDelinquentFee(delinquentFee);
collectionData.setDelinquentPenalty(delinquentPenalty);
return collectionData;
}
@@ -47,9 +47,14 @@ public final class CollectionData {
public Collection<DelinquencyPausePeriod> delinquencyPausePeriods;
public Collection<InstallmentLevelDelinquency> installmentLevelDelinquency;
private BigDecimal delinquentPrincipal;
private BigDecimal delinquentInterest;
private BigDecimal delinquentFee;
private BigDecimal delinquentPenalty;
public static CollectionData template() {
final BigDecimal zero = BigDecimal.ZERO;
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero, null, null);
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
}
}
@@ -24,8 +24,12 @@ import java.util.Collection;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.springframework.util.CollectionUtils;
/**
@@ -91,11 +95,15 @@ public class LoanSummaryData {
private BigDecimal totalCreditBalanceRefundReversed;
private BigDecimal totalRepaymentTransaction;
private BigDecimal totalRepaymentTransactionReversed;
private BigDecimal totalInterestPaymentWaiver;
private final Long chargeOffReasonId;
private final String chargeOffReason;
private BigDecimal totalUnpaidAccruedDueInterest;
private BigDecimal totalUnpaidAccruedNotDueInterest;
public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData,
final Collection<LoanTransactionData> loanTransactions) {
final Collection<LoanTransactionData> loanTransactions, final LoanScheduleData repaymentSchedule) {
BigDecimal totalMerchantRefund = BigDecimal.ZERO;
BigDecimal totalMerchantRefundReversed = BigDecimal.ZERO;
@@ -110,6 +118,9 @@ public class LoanSummaryData {
BigDecimal totalCreditBalanceRefundReversed = BigDecimal.ZERO;
BigDecimal totalRepaymentTransaction = BigDecimal.ZERO;
BigDecimal totalRepaymentTransactionReversed = BigDecimal.ZERO;
BigDecimal totalInterestPaymentWaiver = BigDecimal.ZERO;
BigDecimal totalUnpaidAccruedDueInterest = BigDecimal.ZERO;
BigDecimal totalUnpaidAccruedNotDueInterest = BigDecimal.ZERO;
if (!CollectionUtils.isEmpty(loanTransactions)) {
@@ -131,6 +142,24 @@ public class LoanSummaryData {
loanTransactions);
totalRepaymentTransaction = computeTotalRepaymentTransactionAmount(loanTransactions);
totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions);
totalInterestPaymentWaiver = computeTotalAmountForNonReversedTransactions(LoanTransactionType.INTEREST_PAYMENT_WAIVER,
loanTransactions);
}
if (repaymentSchedule != null) {
// Accrued Due Interest on Past due installments
totalUnpaidAccruedDueInterest = computeTotalAccruedDueInterestAmount(repaymentSchedule.getPeriods());
if (MathUtil.isGreaterThanZero(totalUnpaidAccruedDueInterest)) {
totalUnpaidAccruedDueInterest = totalUnpaidAccruedDueInterest
.subtract(computeTotalInterestPaidDueAmount(repaymentSchedule.getPeriods()));
}
// Accrued Due Interest on Actual Installment
totalUnpaidAccruedNotDueInterest = computeTotalAccruedNotDueInterestAmountOnActualPeriod(repaymentSchedule.getPeriods());
if (MathUtil.isGreaterThanZero(totalUnpaidAccruedNotDueInterest)) {
totalUnpaidAccruedNotDueInterest = totalUnpaidAccruedNotDueInterest
.subtract(computeTotalInterestPaidNotDueAmountOnActualPeriod(repaymentSchedule.getPeriods()));
}
}
return LoanSummaryData.builder().currency(defaultSummaryData.currency).principalDisbursed(defaultSummaryData.principalDisbursed)
@@ -163,7 +192,9 @@ public class LoanSummaryData {
.totalChargeAdjustment(totalChargeAdjustment).totalChargeAdjustmentReversed(totalChargeAdjustmentReversed)
.totalChargeback(totalChargeback).totalCreditBalanceRefund(totalCreditBalanceRefund)
.totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed).totalRepaymentTransaction(totalRepaymentTransaction)
.totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).build();
.totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).totalInterestPaymentWaiver(totalInterestPaymentWaiver)
.totalUnpaidAccruedDueInterest(totalUnpaidAccruedDueInterest)
.totalUnpaidAccruedNotDueInterest(totalUnpaidAccruedNotDueInterest).build();
}
public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) {
@@ -191,4 +222,45 @@ public class LoanSummaryData {
loanTransactions);
return totalRepaymentTransaction.add(totalDownPaymentTransaction);
}
private static BigDecimal computeTotalAccruedDueInterestAmount(Collection<LoanSchedulePeriodData> periods) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
return periods.stream().filter(period -> !period.getDownPaymentPeriod() && businessDate.isAfter(period.getDueDate()))
.map(period -> period.getTotalAccruedInterest()).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static BigDecimal computeTotalInterestPaidDueAmount(Collection<LoanSchedulePeriodData> periods) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
return periods.stream().filter(period -> !period.getDownPaymentPeriod() && businessDate.isAfter(period.getDueDate()))
.map(period -> period.getInterestPaid()).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static BigDecimal computeTotalAccruedNotDueInterestAmountOnActualPeriod(Collection<LoanSchedulePeriodData> periods) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
return periods.stream()
.filter(period -> !period.getDownPaymentPeriod() && isActualPeriod(period) && businessDate.isBefore(period.getDueDate()))
.map(period -> period.getTotalAccruedInterest()).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static BigDecimal computeTotalInterestPaidNotDueAmountOnActualPeriod(Collection<LoanSchedulePeriodData> periods) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
return periods.stream()
.filter(period -> !period.getDownPaymentPeriod() && isActualPeriod(period) && businessDate.isBefore(period.getDueDate()))
.map(period -> period.getInterestPaid()).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static boolean isActualPeriod(LoanSchedulePeriodData period) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
boolean actualPeriod = false;
if (period.getPeriod() != null) {
if (period.getPeriod() == 1) {
actualPeriod = ((businessDate.isEqual(period.getFromDate()) || businessDate.isAfter(period.getFromDate()))
&& businessDate.isBefore(period.getDueDate()));
} else {
actualPeriod = (businessDate.isAfter(period.getFromDate()) && businessDate.isBefore(period.getDueDate()));
}
}
return actualPeriod;
}
}
@@ -72,6 +72,7 @@ public final class LoanSchedulePeriodData {
private final BigDecimal totalActualCostOfLoanForPeriod;
private final BigDecimal totalInstallmentAmountForPeriod;
private final BigDecimal totalCredits;
private final BigDecimal totalAccruedInterest;
private final Boolean downPaymentPeriod;
public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed,
@@ -109,7 +110,8 @@ public final class LoanSchedulePeriodData {
final BigDecimal totalDueForPeriod, final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod,
final BigDecimal totalPaidLateForPeriod, final BigDecimal totalWaived, final BigDecimal totalWrittenOff,
final BigDecimal totalOutstanding, final BigDecimal totalActualCostOfLoanForPeriod,
final BigDecimal totalInstallmentAmountForPeriod, final BigDecimal totalCredits, final boolean isDownPayment) {
final BigDecimal totalInstallmentAmountForPeriod, final BigDecimal totalCredits, final boolean isDownPayment,
final BigDecimal totalAccruedInterest) {
return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, obligationsMetOnDate, complete, principalOriginalDue,
principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
@@ -117,7 +119,7 @@ public final class LoanSchedulePeriodData {
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid,
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding,
totalActualCostOfLoanForPeriod, totalInstallmentAmountForPeriod, totalCredits, isDownPayment);
totalActualCostOfLoanForPeriod, totalInstallmentAmountForPeriod, totalCredits, isDownPayment, totalAccruedInterest);
}
public static LoanSchedulePeriodData withPaidDetail(final LoanSchedulePeriodData loanSchedulePeriodData, final boolean complete,
@@ -138,7 +140,8 @@ public final class LoanSchedulePeriodData {
loanSchedulePeriodData.totalPaidLateForPeriod, loanSchedulePeriodData.totalWaivedForPeriod,
loanSchedulePeriodData.totalWrittenOffForPeriod, loanSchedulePeriodData.totalOutstandingForPeriod,
loanSchedulePeriodData.totalActualCostOfLoanForPeriod, loanSchedulePeriodData.totalInstallmentAmountForPeriod,
loanSchedulePeriodData.totalCredits, loanSchedulePeriodData.getDownPaymentPeriod());
loanSchedulePeriodData.totalCredits, loanSchedulePeriodData.getDownPaymentPeriod(),
loanSchedulePeriodData.totalAccruedInterest);
}
/*
@@ -202,6 +205,7 @@ public final class LoanSchedulePeriodData {
this.totalInstallmentAmountForPeriod = null;
this.totalOverdue = DateUtils.isBeforeBusinessDate(dueDate) ? this.totalOutstandingForPeriod : null;
this.totalCredits = BigDecimal.ZERO;
this.totalAccruedInterest = BigDecimal.ZERO;
this.downPaymentPeriod = false;
}
@@ -261,6 +265,7 @@ public final class LoanSchedulePeriodData {
this.totalInstallmentAmountForPeriod = totalInstallmentAmountForPeriod;
this.totalOverdue = DateUtils.isBeforeBusinessDate(dueDate) ? this.totalOutstandingForPeriod : null;
this.totalCredits = BigDecimal.ZERO;
this.totalAccruedInterest = BigDecimal.ZERO;
this.downPaymentPeriod = false;
}
@@ -316,6 +321,7 @@ public final class LoanSchedulePeriodData {
this.totalOverdue = DateUtils.isBeforeBusinessDate(dueDate) ? this.totalOutstandingForPeriod : null;
this.totalCredits = BigDecimal.ZERO;
this.downPaymentPeriod = true;
this.totalAccruedInterest = BigDecimal.ZERO;
}
/*
@@ -334,7 +340,7 @@ public final class LoanSchedulePeriodData {
final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod, final BigDecimal totalPaidLateForPeriod,
final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
final BigDecimal totalActualCostOfLoanForPeriod, final BigDecimal totalInstallmentAmountForPeriod,
final BigDecimal totalCredits, final boolean isDownPayment) {
final BigDecimal totalCredits, final boolean isDownPayment, final BigDecimal totalAccruedInterest) {
this.period = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -385,6 +391,7 @@ public final class LoanSchedulePeriodData {
this.totalOverdue = DateUtils.isBeforeBusinessDate(dueDate) ? this.totalOutstandingForPeriod : null;
this.totalCredits = totalCredits;
this.downPaymentPeriod = isDownPayment;
this.totalAccruedInterest = totalAccruedInterest;
}
private BigDecimal defaultToZeroIfNull(final BigDecimal possibleNullValue) {
@@ -482,4 +489,5 @@ public final class LoanSchedulePeriodData {
public BigDecimal totalOutstandingForPeriod() {
return defaultToZeroIfNull(this.totalOutstandingForPeriod);
}
}
@@ -73,7 +73,8 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer {
if (data.getSummary() != null) {
final Collection<LoanTransactionData> currentLoanTransactions = service.retrieveLoanTransactions(loanId);
data.setSummary(LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), currentLoanTransactions));
data.setSummary(
LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), currentLoanTransactions, data.getRepaymentSchedule()));
} else {
data.setSummary(LoanSummaryData.withOnlyCurrencyData(data.getCurrency()));
}
@@ -1114,8 +1114,8 @@ public class LoansApiResource {
// updating summary with transaction amounts summary
if (loanBasicDetails.getSummary() != null) {
loanBasicDetails
.setSummary(LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), currentLoanRepayments));
loanBasicDetails.setSummary(
LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), currentLoanRepayments, repaymentSchedule));
}
final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments,
@@ -636,7 +636,13 @@ final class LoansApiResourceSwagger {
@Schema(example = "0.000000")
public Double totalRepaymentTransaction;
@Schema(example = "0.000000")
public Double totalInterestPaymentWaiver;
@Schema(example = "0.000000")
public Double totalRepaymentTransactionReversed;
@Schema(example = "0.000000")
public Double totalUnpaidAccruedDueInterest;
@Schema(example = "0.000000")
public Double totalUnpaidAccruedNotDueInterest;
public Set<GetLoansLoanIdOverdueCharges> overdueCharges;
@Schema(example = "1")
public Long chargeOffReasonId;
@@ -1004,6 +1010,14 @@ final class LoansApiResourceSwagger {
public LocalDate delinquentDate;
@Schema(example = "100.000000")
public Double delinquentAmount;
@Schema(example = "80.000000")
public Double delinquentPrincipal;
@Schema(example = "10.000000")
public Double delinquentInterest;
@Schema(example = "6.000000")
public Double delinquentFee;
@Schema(example = "4.000000")
public Double delinquentPenalty;
@Schema(example = "[2022, 07, 01]")
public LocalDate lastPaymentDate;
@Schema(example = "100.000000")
@@ -1160,8 +1160,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, "
+ " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, "
+ " ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, "
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as principalCredits, ls.credited_fee as feeCredits, ls.credited_penalty as penaltyCredits, ls.is_down_payment isDownPayment "
+ " from m_loan_repayment_schedule ls ";
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as principalCredits, ls.credited_fee as feeCredits, ls.credited_penalty as penaltyCredits, ls.is_down_payment isDownPayment, "
+ " ls.accrual_interest_derived as accrualInterest " + " from m_loan_repayment_schedule ls ";
}
@Override
@@ -1260,6 +1260,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal interestWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWrittenOff");
final BigDecimal totalInstallmentAmount = totalPrincipalPaid.zero().plus(principalDue).plus(interestExpectedDue)
.getAmount();
final BigDecimal accrualInterest = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accrualInterest");
final BigDecimal interestActualDue = interestExpectedDue.subtract(interestWaived).subtract(interestWrittenOff);
final BigDecimal interestOutstanding = interestActualDue.subtract(interestPaid);
@@ -1331,7 +1332,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod,
totalPaidForPeriod, totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaivedForPeriod,
totalWrittenOffForPeriod, totalOutstandingForPeriod, totalActualCostOfLoanForPeriod, totalInstallmentAmount,
credits, isDownPayment);
credits, isDownPayment, accrualInterest);
periods.add(periodData);
}
@@ -2313,6 +2314,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal totalPaid = null;
final BigDecimal totalInstallmentAmount = null;
final BigDecimal totalCredits = null;
final BigDecimal totalAccruedInterest = null;
return LoanSchedulePeriodData.periodWithPayments(loanId, period, fromDate, dueDate, obligationsMetOnDate, complete,
principalOriginalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
@@ -2320,7 +2322,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid,
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding,
totalActualCostOfLoanForPeriod, totalInstallmentAmount, totalCredits, false);
totalActualCostOfLoanForPeriod, totalInstallmentAmount, totalCredits, false, totalAccruedInterest);
}
}
@@ -167,8 +167,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null, null, null);
final BigDecimal zero = BigDecimal.ZERO;
CollectionData collectionData = new CollectionData(zero, 2L, null, 2L, overDueSinceDate, zero, null, null, null, null, null, null,
zero, zero, zero, zero);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -221,11 +222,11 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null, null, null);
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
@@ -349,11 +350,12 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null, null, null);
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null);
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
@@ -426,11 +428,11 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null, null, null);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null,
null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
@@ -514,14 +516,14 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null, null, null);
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null, null,
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData_1 = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null);
CollectionData installmentCollectionData_1 = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData_2 = new CollectionData(BigDecimal.ZERO, 0L, null, 0L, null, installmentPrincipalAmount,
null, null, null, null, null, null);
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, 0L, null, installmentPrincipalAmount, null,
null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData_1);
@@ -70,6 +70,7 @@ import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.accounting.PeriodicAccrualAccountingHelper;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
@@ -4601,6 +4602,85 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC144: Advanced payment allocation with Interest,
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. and with 5% Interest, 360/30, 1 repayment per month
// 2. Submit Loan and approve
// 3. Disburse
// 4. Validate Repayment Schedule
@Test
public void uc144() {
final String operationDate = "1 January 2024";
AtomicLong createdLoanId = new AtomicLong();
runAt(operationDate, () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365.getValue())
.numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.allowPartialPeriodInterestCalcualtion(false)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 5);
applicationRequest = applicationRequest.numberOfRepayments(5).loanTermFrequency(5).loanTermFrequencyType(2)
.interestRatePerPeriod(BigDecimal.valueOf(12.3)).interestCalculationPeriodType(DAYS)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(1)
.repaymentFrequencyType(2);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
// After Disbursement we are expecting no Accrual transactions
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
assertEquals(0.0, loanDetails.getSummary().getTotalUnpaidAccruedDueInterest());
assertEquals(0.0, loanDetails.getSummary().getTotalUnpaidAccruedNotDueInterest());
});
// Not Due yet
runAt("30 January 2024", () -> {
// Generate the Accruals
final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec,
responseSpec);
periodicAccrualAccountingHelper.runPeriodicAccrualAccounting("30 January 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertEquals(0.0, loanDetails.getSummary().getTotalUnpaidAccruedDueInterest());
assertEquals(0.97, loanDetails.getSummary().getTotalUnpaidAccruedNotDueInterest());
// Partial interest repayment
addRepaymentForLoan(createdLoanId.get(), 20.50, "30 January 2024");
loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertEquals(0.0, loanDetails.getSummary().getTotalUnpaidAccruedDueInterest());
assertEquals(0.05, loanDetails.getSummary().getTotalUnpaidAccruedNotDueInterest());
});
// Not Due and Due Interest
runAt("20 February 2024", () -> {
final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec,
responseSpec);
periodicAccrualAccountingHelper.runPeriodicAccrualAccounting("20 February 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertEquals(0.12, loanDetails.getSummary().getTotalUnpaidAccruedDueInterest());
assertEquals(0.52, loanDetails.getSummary().getTotalUnpaidAccruedNotDueInterest());
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS;
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE;
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -55,7 +56,11 @@ import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
import org.apache.fineract.client.models.PostLoansDelinquencyActionResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PutDelinquencyBucketResponse;
import org.apache.fineract.client.models.PutDelinquencyRangeResponse;
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
@@ -1444,6 +1449,115 @@ public class DelinquencyBucketsIntegrationTest extends BaseLoanIntegrationTest {
}
}
@Test
public void testLoanDelinquencyDataWithAmountPerPortions() {
// Given
final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
ArrayList<Integer> rangeIds = new ArrayList<>();
// First Range
String jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30);
PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec,
jsonRange);
rangeIds.add(delinquencyRangeResponse.getResourceId());
GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec,
delinquencyRangeResponse.getResourceId());
String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds);
PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec,
responseSpec, jsonBucket);
assertNotNull(delinquencyBucketResponse);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketResponse.getResourceId());
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper,
Math.toIntExact(delinquencyBucket.getId()), null);
assertNotNull(getLoanProductsProductResponse);
log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName());
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
final LocalDate todaysDate = Utils.getLocalDateOfTenant();
log.info("Local date of Tenant: {}", todaysDate);
// Older date to have more than one overdue installment
final LocalDate transactionDate = todaysDate.minusDays(50);
final String operationDate = Utils.dateFormatter.format(transactionDate);
final Double amount = 2000.0;
PostLoansRequest applicationRequest = applyLoanRequest(clientId, getLoanProductsProductResponse.getId(), operationDate, amount, 4);
applicationRequest = applicationRequest.numberOfRepayments(5).loanTermFrequency(5).loanTermFrequencyType(2)
.interestRatePerPeriod(BigDecimal.valueOf(12.3)).interestCalculationPeriodType(DAYS).repaymentEvery(1)
.repaymentFrequencyType(2);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
final Long loanId = loanResponse.getResourceId();
loanTransactionHelper.approveLoan(loanId, new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount))
.dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(amount)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
log.info("Loan Delinquency Range after Disbursement {}", loanDetails.getDelinquencyRange().getClassification());
assertNotNull(loanDetails.getDelinquent());
log.info("Loan Delinquency Data {} {}", loanDetails.getDelinquent().getDelinquentPrincipal(),
loanDetails.getDelinquent().getDelinquentInterest());
assertNotNull(loanDetails.getDelinquent().getDelinquentPrincipal());
assertEquals(305.91, loanDetails.getDelinquent().getDelinquentPrincipal());
assertNotNull(loanDetails.getDelinquent().getDelinquentInterest());
assertEquals(250.72, loanDetails.getDelinquent().getDelinquentInterest());
// Apply a partial repayment to move only the interest
PostLoansLoanIdTransactionsResponse loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 120f,
loanId.intValue());
assertNotNull(loansLoanIdTransactions);
log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails.getDelinquent());
assertNotNull(loanDetails.getDelinquencyRange().getClassification());
assertEquals(305.91, loanDetails.getDelinquent().getDelinquentPrincipal());
assertEquals(130.72, loanDetails.getDelinquent().getDelinquentInterest());
// Apply a repayment to cover interest and part of the principal
loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 330.72f, loanId.intValue());
assertNotNull(loansLoanIdTransactions);
log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails.getDelinquent());
assertNotNull(loanDetails.getDelinquencyRange().getClassification());
assertEquals(105.91, loanDetails.getDelinquent().getDelinquentPrincipal());
assertEquals(0.0, loanDetails.getDelinquent().getDelinquentInterest());
// Apply a repayment to cover the remain principal
loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 105.91f, loanId.intValue());
assertNotNull(loansLoanIdTransactions);
log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId());
// Loan without Delinquency Classification
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails.getDelinquent());
assertNull(loanDetails.getDelinquencyRange());
assertEquals(0.0, loanDetails.getDelinquent().getDelinquentPrincipal());
assertEquals(0.0, loanDetails.getDelinquent().getDelinquentInterest());
// Undo the last repayment transaction we must to have pending the principal
PostLoansLoanIdTransactionsResponse reverseRepayment = loanTransactionHelper.reverseLoanTransaction(loanId,
loansLoanIdTransactions.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM yyyy")
.transactionDate(operationDate).transactionAmount(0.0).locale("en"));
assertNotNull(reverseRepayment);
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails.getDelinquent());
assertNotNull(loanDetails.getDelinquencyRange().getClassification());
assertEquals(105.91, loanDetails.getDelinquent().getDelinquentPrincipal());
assertEquals(0.0, loanDetails.getDelinquent().getDelinquentInterest());
}
private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,
final Integer delinquencyBucketId, final String inArrearsTolerance) {
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().withInArrearsTolerance(inArrearsTolerance).build(null,