diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java index d6ad0e5fc..c09a71479 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java @@ -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 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; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java index f872d6d27..fa4e4f6c8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java @@ -47,9 +47,14 @@ public final class CollectionData { public Collection delinquencyPausePeriods; public Collection 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); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java index 42ebbd6be..1d3cf5a63 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java @@ -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 loanTransactions) { + final Collection 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 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 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 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 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; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index fb50ee5f8..fcf050ee7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -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); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java index 6844fe235..e74cbf3c8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java @@ -73,7 +73,8 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { if (data.getSummary() != null) { final Collection 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())); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 8fd7316cb..468086871 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 7808e0758..943303a86 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -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 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") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 1f13036e4..2195253dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -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); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index 0a9945675..7524a9248 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -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 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 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 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 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 installmentsCollection = new HashMap<>(); installmentsCollection.put(1L, installmentCollectionData_1); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 5d08ad104..aa23b5072 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -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 ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index 88c3b484c..c9cdd2456 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -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 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 loanProductMap = new LoanProductTestBuilder().withInArrearsTolerance(inArrearsTolerance).build(null,