FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans

This commit is contained in:
mark.vituska
2025-05-27 10:02:25 +02:00
committed by Adam Saghy
parent 07839951b9
commit e5329f6fe3
53 changed files with 1007 additions and 516 deletions
@@ -43,8 +43,8 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
}
@Override
public void createLoanNote(Long loanId, String note) {
throw new UnsupportedOperationException("createLoanNote() is not yet implemented.");
public void createLoanTransactionNote(Long loanTransactionId, String note) {
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
}
@Override
@@ -310,6 +310,12 @@ public final class DateUtils {
return isBeforeBusinessDate(localDate);
}
public static boolean isDateInRangeFromInclusiveToExclusive(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
final LocalDate target) {
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
&& DateUtils.isBefore(target, upToNotInclusive);
}
public static int compare(LocalDate first, LocalDate second) {
return compare(first, second, true);
}
@@ -26,7 +26,7 @@ public interface NoteWritePlatformService {
CommandProcessingResult createNote(JsonCommand command);
void createLoanNote(Long loanId, String note);
void createLoanTransactionNote(Long loanTransactionId, String note);
CommandProcessingResult updateNote(JsonCommand command);
@@ -148,7 +148,7 @@ public class EventCheckHelper {
.extractingData(loanAccountDataV1 -> loanAccountDataV1.getStatus().getId()).isEqualTo(body.getStatus().getId())//
.extractingData(loanAccountDataV1 -> loanAccountDataV1.getStatus().getCode()).isEqualTo(body.getStatus().getCode())//
.extractingData(LoanAccountDataV1::getClientId).isEqualTo(Long.valueOf(body.getClientId()))//
.extractingBigDecimal(LoanAccountDataV1::getApprovedPrincipal).isEqualTo(BigDecimal.valueOf(body.getApprovedPrincipal()))//
.extractingBigDecimal(LoanAccountDataV1::getApprovedPrincipal).isEqualTo(body.getApprovedPrincipal())//
.extractingData(loanAccountDataV1 -> loanAccountDataV1.getTimeline().getApprovedOnDate())//
.isEqualTo(FORMATTER_EVENTS.format(body.getTimeline().getApprovedOnDate()))//
.extractingData(loanAccountDataV1 -> loanAccountDataV1.getSummary().getCurrency().getCode())
@@ -198,7 +198,7 @@ public class EventCheckHelper {
Long clientIdActual = loanAccountDataV1.getClientId();
Long clientIdExpected = body.getClientId();
BigDecimal principalDisbursedActual = loanAccountDataV1.getSummary().getPrincipalDisbursed();
Double principalDisbursedExpectedDouble = body.getSummary().getPrincipalDisbursed();
Double principalDisbursedExpectedDouble = body.getSummary().getPrincipalDisbursed().doubleValue();
BigDecimal principalDisbursedExpected = BigDecimal.valueOf(principalDisbursedExpectedDouble);
String actualDisbursementDateActual = loanAccountDataV1.getTimeline().getActualDisbursementDate();
String actualDisbursementDateExpected = FORMATTER_EVENTS.format(body.getTimeline().getActualDisbursementDate());
@@ -210,7 +210,7 @@ public class EventCheckHelper {
.getTotalUnpaidPayableNotDueInterest();
BigDecimal totalUnpaidPayableNotDueInterestExpected = body.getSummary().getTotalUnpaidPayableNotDueInterest();
BigDecimal totalInterestPaymentWaiverActual = loanAccountDataV1.getSummary().getTotalInterestPaymentWaiver();
Double totalInterestPaymentWaiverExpectedDouble = body.getSummary().getTotalInterestPaymentWaiver();
Double totalInterestPaymentWaiverExpectedDouble = body.getSummary().getTotalInterestPaymentWaiver().doubleValue();
BigDecimal totalInterestPaymentWaiverExpected = new BigDecimal(totalInterestPaymentWaiverExpectedDouble,
MathContext.DECIMAL64);
BigDecimal delinquentInterestActual = loanAccountDataV1.getDelinquent().getDelinquentInterest();
@@ -296,7 +296,7 @@ public class EventCheckHelper {
eventAssertion.assertEvent(LoanDisbursalTransactionEvent.class, disbursementTransaction.getId())//
.extractingData(LoanTransactionDataV1::getLoanId).isEqualTo(body.getId())//
.extractingData(LoanTransactionDataV1::getDate).isEqualTo(FORMATTER_EVENTS.format(disbursementTransaction.getDate()))//
.extractingBigDecimal(LoanTransactionDataV1::getAmount).isEqualTo(BigDecimal.valueOf(disbursementTransaction.getAmount()));//
.extractingBigDecimal(LoanTransactionDataV1::getAmount).isEqualTo(disbursementTransaction.getAmount());//
}
public EventAssertion.EventAssertionBuilder<LoanTransactionDataV1> transactionEventCheck(
@@ -325,7 +325,7 @@ public class EventCheckHelper {
EventAssertion.EventAssertionBuilder<LoanTransactionDataV1> eventBuilder = eventAssertion.assertEvent(eventClass, transactionId);
eventBuilder.extractingData(LoanTransactionDataV1::getLoanId).isEqualTo(loanDetailsResponse.body().getId())//
.extractingData(LoanTransactionDataV1::getDate).isEqualTo(FORMATTER_EVENTS.format(transactionFound.getDate()))//
.extractingBigDecimal(LoanTransactionDataV1::getAmount).isEqualTo(BigDecimal.valueOf(transactionFound.getAmount()))//
.extractingBigDecimal(LoanTransactionDataV1::getAmount).isEqualTo(transactionFound.getAmount())//
.extractingData(LoanTransactionDataV1::getExternalOwnerId).isEqualTo(externalOwnerId);//
return eventBuilder;
}
@@ -420,8 +420,8 @@ public class EventCheckHelper {
.getGlobalConfiguration("outstanding-interest-calculation-strategy-for-external-asset-transfer");
if ("PAYABLE_OUTSTANDING_INTEREST".equals(outstandingInterestStrategy.getStringValue())) {
Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "all", null, null).execute();
totalOutstandingBalanceAmountExpected = BigDecimal.valueOf(loanDetails.body().getSummary().getTotalOutstanding());
outstandingInterestPortionExpected = BigDecimal.valueOf(loanDetails.body().getSummary().getInterestOutstanding());
totalOutstandingBalanceAmountExpected = loanDetails.body().getSummary().getTotalOutstanding();
outstandingInterestPortionExpected = loanDetails.body().getSummary().getInterestOutstanding();
}
String ownerExternalIdExpected = filtered.getStatus().getValue().equals("BUYBACK") ? null : filtered.getOwner().getExternalId();
@@ -161,7 +161,7 @@ public class LoanChargeAdjustmentStepDef extends AbstractStepDef {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
String dateActual = formatter.format(date);
Double amountActual = transactions.get(i).getAmount();
Double amountActual = transactions.get(i).getAmount().doubleValue();
if (dateActual.equals(transactionDate) && amountActual.equals(transactionAmount)) {
transactionMetConditions = transactions.get(i);
@@ -654,7 +654,7 @@ public class LoanDelinquencyStepDef extends AbstractStepDef {
Long loanLevelDelinquencyRangeIdExpected = delinquencyRange.getId();
String loanLevelDelinquencyRangeExpected = delinquencyRange.getClassification();
String loanLevelDelinquentDateExpected = FORMATTER.format(delinquent.getDelinquentDate());
BigDecimal loanLevelTotalAmountExpected = BigDecimal.valueOf(delinquent.getDelinquentAmount());
BigDecimal loanLevelTotalAmountExpected = delinquent.getDelinquentAmount();
assertThat(loanLevelDelinquencyRangeId)//
.as(ErrorMessageHelper.wrongValueInLoanDelinquencyRangeChangeBusinessEvent4(loanLevelDelinquencyRangeId,
@@ -715,7 +715,7 @@ public class LoanDelinquencyStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetails);
Double expectedLastRepaymentAmount1 = Double.valueOf(expectedLastRepaymentAmount);
Double actualLastRepaymentAmount = loanDetails.body().getDelinquent().getLastRepaymentAmount();
Double actualLastRepaymentAmount = loanDetails.body().getDelinquent().getLastRepaymentAmount().doubleValue();
String actualLastRepaymentDate = FORMATTER.format(loanDetails.body().getDelinquent().getLastRepaymentDate());
assertThat(actualLastRepaymentAmount)//
@@ -550,7 +550,7 @@ public class LoanRepaymentStepDef extends AbstractStepDef {
BigDecimal lastExpectedAmount = new BigDecimal(totalAmount).setScale(0, RoundingMode.HALF_DOWN);
for (int i = 1; i < periods.size(); i++) {
BigDecimal actualAmount = new BigDecimal(periods.get(i).getPrincipalOriginalDue()).setScale(0, RoundingMode.HALF_DOWN);
BigDecimal actualAmount = periods.get(i).getPrincipalOriginalDue().setScale(0, RoundingMode.HALF_DOWN);
if (i == periods.size() - 1) {
assertThat(actualAmount.compareTo(lastExpectedAmount))
@@ -613,7 +613,7 @@ public class LoanRepaymentStepDef extends AbstractStepDef {
eventAssertionBuilder
.extractingBigDecimal(
loanTransactionAdjustmentDataV1 -> loanTransactionAdjustmentDataV1.getTransactionToAdjust().getAmount())
.isEqualTo(BigDecimal.valueOf(targetTransaction.getAmount()));
.isEqualTo(targetTransaction.getAmount());
eventAssertionBuilder
.extractingData(
loanTransactionAdjustmentDataV1 -> loanTransactionAdjustmentDataV1.getTransactionToAdjust().getManuallyReversed())
@@ -1121,9 +1121,9 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
GetLoansLoanIdDelinquencySummary delinquent = loanDetailsResponse.body().getDelinquent();
String lastPaymentAmountActual = String.valueOf(delinquent.getLastPaymentAmount());
String lastPaymentAmountActual = new Utils.DoubleFormatter(delinquent.getLastPaymentAmount().doubleValue()).format();
String lastPaymentDateActual = FORMATTER.format(delinquent.getLastPaymentDate());
String lastRepaymentAmountActual = String.valueOf(delinquent.getLastRepaymentAmount());
String lastRepaymentAmountActual = new Utils.DoubleFormatter(delinquent.getLastRepaymentAmount().doubleValue()).format();
String lastRepaymentDateActual = FORMATTER.format(delinquent.getLastRepaymentDate());
assertThat(lastPaymentAmountActual)
@@ -1832,7 +1832,7 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
testContext().set(TestContextKey.LOAN_RESPONSE, loanDetailsResponse);
Double totalOutstandingActual = loanDetailsResponse.body().getSummary().getTotalOutstanding();
Double totalOutstandingActual = loanDetailsResponse.body().getSummary().getTotalOutstanding().doubleValue();
assertThat(totalOutstandingActual)
.as(ErrorMessageHelper.wrongAmountInTotalOutstanding(totalOutstandingActual, totalOutstandingExpected))
.isEqualTo(totalOutstandingExpected);
@@ -1847,8 +1847,8 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
testContext().set(TestContextKey.LOAN_RESPONSE, loanDetailsResponse);
Double totalOverpaidActual = loanDetailsResponse.body().getTotalOverpaid();
Double totalOutstandingActual = loanDetailsResponse.body().getSummary().getTotalOutstanding();
Double totalOverpaidActual = loanDetailsResponse.body().getTotalOverpaid().doubleValue();
Double totalOutstandingActual = loanDetailsResponse.body().getSummary().getTotalOutstanding().doubleValue();
double totalOutstandingExpected = 0.0;
assertThat(totalOutstandingActual)
.as(ErrorMessageHelper.wrongAmountInTotalOutstanding(totalOutstandingActual, totalOutstandingExpected))
@@ -1867,7 +1867,7 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
testContext().set(TestContextKey.LOAN_RESPONSE, loanDetailsResponse);
Double totalOverdueActual = loanDetailsResponse.body().getSummary().getTotalOverdue();
Double totalOverdueActual = loanDetailsResponse.body().getSummary().getTotalOverdue().doubleValue();
assertThat(totalOverdueActual).as(ErrorMessageHelper.wrongAmountInTotalOverdue(totalOverdueActual, totalOverdueExpected))
.isEqualTo(totalOverdueExpected);
}
@@ -1881,7 +1881,7 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
testContext().set(TestContextKey.LOAN_RESPONSE, loanDetailsResponse);
Double lastPaymentAmountActual = loanDetailsResponse.body().getDelinquent().getLastPaymentAmount();
Double lastPaymentAmountActual = loanDetailsResponse.body().getDelinquent().getLastPaymentAmount().doubleValue();
assertThat(lastPaymentAmountActual)
.as(ErrorMessageHelper.wrongLastPaymentAmount(lastPaymentAmountActual, lastPaymentAmountExpected))
.isEqualTo(lastPaymentAmountExpected);
@@ -2173,12 +2173,13 @@ public class LoanStepDef extends AbstractStepDef {
actualValues.add(t.getDueDate() == null ? null : FORMATTER.format(t.getDueDate()));
actualValues.add(t.getChargeCalculationType().getValue() == null ? null : t.getChargeCalculationType().getValue());
actualValues.add(t.getAmount() == null ? null : new Utils.DoubleFormatter(t.getAmount()).format());
actualValues.add(t.getAmount() == null ? null : new Utils.DoubleFormatter(t.getAmount().doubleValue()).format());
actualValues.add(t.getAmountPaid() == null ? null : new Utils.DoubleFormatter(t.getAmountPaid()).format());
actualValues.add(t.getAmountWaived() == null ? null : new Utils.DoubleFormatter(t.getAmountWaived()).format());
actualValues.add(t.getAmountPaid() == null ? null : new Utils.DoubleFormatter(t.getAmountPaid().doubleValue()).format());
actualValues.add(t.getAmountWaived() == null ? null : new Utils.DoubleFormatter(t.getAmountWaived().doubleValue()).format());
actualValues.add(t.getAmountOutstanding() == null ? null : new Utils.DoubleFormatter(t.getAmountOutstanding()).format());
actualValues.add(
t.getAmountOutstanding() == null ? null : new Utils.DoubleFormatter(t.getAmountOutstanding().doubleValue()).format());
return actualValues;
}).collect(Collectors.toList());
}
@@ -2681,9 +2682,9 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
GetLoansLoanIdDelinquencySummary delinquent = loanDetailsResponse.body().getDelinquent();
String lastPaymentAmountActual = String.valueOf(delinquent.getLastPaymentAmount());
String lastPaymentAmountActual = new Utils.DoubleFormatter(delinquent.getLastPaymentAmount().doubleValue()).format();
String lastPaymentDateActual = FORMATTER.format(delinquent.getLastPaymentDate());
String lastRepaymentAmountActual = String.valueOf(delinquent.getLastRepaymentAmount());
String lastRepaymentAmountActual = new Utils.DoubleFormatter(delinquent.getLastRepaymentAmount().doubleValue()).format();
String lastRepaymentDateActual = FORMATTER.format(delinquent.getLastRepaymentDate());
assertThat(lastPaymentAmountActual)
@@ -2923,7 +2924,7 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(loanDetails);
Double expectedAmountParsed = Double.parseDouble(expectedAmount);
Double totalRepaymentTransaction = loanDetails.body().getSummary().getTotalRepaymentTransaction();
Double totalRepaymentTransaction = loanDetails.body().getSummary().getTotalRepaymentTransaction().doubleValue();
assertThat(totalRepaymentTransaction)
.as(ErrorMessageHelper.wrongAmountInTotalRepaymentTransaction(totalRepaymentTransaction, expectedAmountParsed))
@@ -3310,8 +3311,8 @@ public class LoanStepDef extends AbstractStepDef {
final Double disbursementPrincipalAmount = Double.valueOf(loanData.get(19));
List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
disbursementDetail.add(
new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDate).principal(disbursementPrincipalAmount));
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDate)
.principal(BigDecimal.valueOf(disbursementPrincipalAmount)));
createFullyCustomizedLoanWithChargesExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
}
@@ -3325,9 +3326,9 @@ public class LoanStepDef extends AbstractStepDef {
List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateFirstDisbursal)
.principal(disbursementPrincipalAmountFirstDisbursal));
.principal(BigDecimal.valueOf(disbursementPrincipalAmountFirstDisbursal)));
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateSecondDisbursal)
.principal(disbursementPrincipalAmountSecondDisbursal));
.principal(BigDecimal.valueOf(disbursementPrincipalAmountSecondDisbursal)));
createFullyCustomizedLoanWithChargesExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
}
@@ -3418,8 +3419,8 @@ public class LoanStepDef extends AbstractStepDef {
final Double disbursementPrincipalAmount = Double.valueOf(loanData.get(17));
List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
disbursementDetail.add(
new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDate).principal(disbursementPrincipalAmount));
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDate)
.principal(BigDecimal.valueOf(disbursementPrincipalAmount)));
createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
}
@@ -3433,9 +3434,9 @@ public class LoanStepDef extends AbstractStepDef {
List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateFirstDisbursal)
.principal(disbursementPrincipalAmountFirstDisbursal));
.principal(BigDecimal.valueOf(disbursementPrincipalAmountFirstDisbursal)));
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateSecondDisbursal)
.principal(disbursementPrincipalAmountSecondDisbursal));
.principal(BigDecimal.valueOf(disbursementPrincipalAmountSecondDisbursal)));
createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
}
@@ -3698,16 +3699,20 @@ public class LoanStepDef extends AbstractStepDef {
switch (headerName) {
case "Transaction date" -> actualValues.add(t.getDate() == null ? null : FORMATTER.format(t.getDate()));
case "Transaction Type" -> actualValues.add(t.getType().getValue() == null ? null : t.getType().getValue());
case "Amount" -> actualValues.add(t.getAmount() == null ? null : String.valueOf(t.getAmount()));
case "Principal" -> actualValues.add(t.getPrincipalPortion() == null ? null : String.valueOf(t.getPrincipalPortion()));
case "Interest" -> actualValues.add(t.getInterestPortion() == null ? null : String.valueOf(t.getInterestPortion()));
case "Fees" -> actualValues.add(t.getFeeChargesPortion() == null ? null : String.valueOf(t.getFeeChargesPortion()));
case "Penalties" ->
actualValues.add(t.getPenaltyChargesPortion() == null ? null : String.valueOf(t.getPenaltyChargesPortion()));
case "Loan Balance" ->
actualValues.add(t.getOutstandingLoanBalance() == null ? null : String.valueOf(t.getOutstandingLoanBalance()));
case "Overpayment" ->
actualValues.add(t.getOverpaymentPortion() == null ? null : String.valueOf(t.getOverpaymentPortion()));
case "Amount" ->
actualValues.add(t.getAmount() == null ? null : new Utils.DoubleFormatter(t.getAmount().doubleValue()).format());
case "Principal" -> actualValues.add(
t.getPrincipalPortion() == null ? null : new Utils.DoubleFormatter(t.getPrincipalPortion().doubleValue()).format());
case "Interest" -> actualValues.add(
t.getInterestPortion() == null ? null : new Utils.DoubleFormatter(t.getInterestPortion().doubleValue()).format());
case "Fees" -> actualValues.add(t.getFeeChargesPortion() == null ? null
: new Utils.DoubleFormatter(t.getFeeChargesPortion().doubleValue()).format());
case "Penalties" -> actualValues.add(t.getPenaltyChargesPortion() == null ? null
: new Utils.DoubleFormatter(t.getPenaltyChargesPortion().doubleValue()).format());
case "Loan Balance" -> actualValues.add(t.getOutstandingLoanBalance() == null ? null
: new Utils.DoubleFormatter(t.getOutstandingLoanBalance().doubleValue()).format());
case "Overpayment" -> actualValues.add(t.getOverpaymentPortion() == null ? null
: new Utils.DoubleFormatter(t.getOverpaymentPortion().doubleValue()).format());
case "Reverted" -> actualValues.add(t.getManuallyReversed() == null ? null : String.valueOf(t.getManuallyReversed()));
case "Replayed" -> {
boolean hasReplayed = t.getTransactionRelations().stream().anyMatch(e -> "REPLAYED".equals(e.getRelationType()));
@@ -3748,27 +3753,27 @@ public class LoanStepDef extends AbstractStepDef {
case "Paid date" -> actualValues.add(repaymentPeriod.getObligationsMetOnDate() == null ? null
: FORMATTER.format(repaymentPeriod.getObligationsMetOnDate()));
case "Balance of loan" -> actualValues.add(repaymentPeriod.getPrincipalLoanBalanceOutstanding() == null ? null
: String.valueOf(repaymentPeriod.getPrincipalLoanBalanceOutstanding()));
case "Principal due" ->
actualValues.add(repaymentPeriod.getPrincipalDue() == null ? null : String.valueOf(repaymentPeriod.getPrincipalDue()));
case "Interest" ->
actualValues.add(repaymentPeriod.getInterestDue() == null ? null : String.valueOf(repaymentPeriod.getInterestDue()));
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalLoanBalanceOutstanding().doubleValue()).format());
case "Principal due" -> actualValues.add(repaymentPeriod.getPrincipalDue() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalDue().doubleValue()).format());
case "Interest" -> actualValues.add(repaymentPeriod.getInterestDue() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getInterestDue().doubleValue()).format());
case "Fees" -> actualValues.add(repaymentPeriod.getFeeChargesDue() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getFeeChargesDue()).format());
: new Utils.DoubleFormatter(repaymentPeriod.getFeeChargesDue().doubleValue()).format());
case "Penalties" -> actualValues.add(repaymentPeriod.getPenaltyChargesDue() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue()).format());
: new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue().doubleValue()).format());
case "Due" -> actualValues.add(repaymentPeriod.getTotalDueForPeriod() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getTotalDueForPeriod()).format());
case "Paid" -> actualValues.add(
repaymentPeriod.getTotalPaidForPeriod() == null ? null : String.valueOf(repaymentPeriod.getTotalPaidForPeriod()));
: new Utils.DoubleFormatter(repaymentPeriod.getTotalDueForPeriod().doubleValue()).format());
case "Paid" -> actualValues.add(repaymentPeriod.getTotalPaidForPeriod() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidForPeriod().doubleValue()).format());
case "In advance" -> actualValues.add(repaymentPeriod.getTotalPaidInAdvanceForPeriod() == null ? null
: String.valueOf(repaymentPeriod.getTotalPaidInAdvanceForPeriod()));
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidInAdvanceForPeriod().doubleValue()).format());
case "Late" -> actualValues.add(repaymentPeriod.getTotalPaidLateForPeriod() == null ? null
: String.valueOf(repaymentPeriod.getTotalPaidLateForPeriod()));
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidLateForPeriod().doubleValue()).format());
case "Waived" -> actualValues.add(repaymentPeriod.getTotalWaivedForPeriod() == null ? null
: String.valueOf(repaymentPeriod.getTotalWaivedForPeriod()));
: new Utils.DoubleFormatter(repaymentPeriod.getTotalWaivedForPeriod().doubleValue()).format());
case "Outstanding" -> actualValues.add(repaymentPeriod.getTotalOutstandingForPeriod() == null ? null
: new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod()).format());
: new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod().doubleValue()).format());
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
}
}
@@ -3784,7 +3789,7 @@ public class LoanStepDef extends AbstractStepDef {
List<GetLoansLoanIdRepaymentPeriod> periods = repaymentSchedule.getPeriods();
for (GetLoansLoanIdRepaymentPeriod period : periods) {
if (null != period.getTotalPaidForPeriod()) {
paidActual += period.getTotalPaidForPeriod();
paidActual += period.getTotalPaidForPeriod().doubleValue();
}
}
BigDecimal paidActualBd = new BigDecimal(paidActual).setScale(2, RoundingMode.HALF_DOWN);
@@ -3793,45 +3798,45 @@ public class LoanStepDef extends AbstractStepDef {
String headerName = header.get(i);
String expectedValue = expectedAmounts.get(i);
switch (headerName) {
case "Principal due" -> assertThat(repaymentSchedule.getTotalPrincipalExpected())//
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePrincipal(repaymentSchedule.getTotalPrincipalExpected(),
Double.valueOf(expectedValue)))//
case "Principal due" -> assertThat(repaymentSchedule.getTotalPrincipalExpected().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePrincipal(
repaymentSchedule.getTotalPrincipalExpected().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Interest" -> assertThat(repaymentSchedule.getTotalInterestCharged())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInterest(repaymentSchedule.getTotalInterestCharged(),
Double.valueOf(expectedValue)))//
case "Interest" -> assertThat(repaymentSchedule.getTotalInterestCharged().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInterest(
repaymentSchedule.getTotalInterestCharged().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Fees" -> assertThat(repaymentSchedule.getTotalFeeChargesCharged())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleFees(repaymentSchedule.getTotalFeeChargesCharged(),
Double.valueOf(expectedValue)))//
case "Fees" -> assertThat(repaymentSchedule.getTotalFeeChargesCharged().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleFees(
repaymentSchedule.getTotalFeeChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Penalties" -> assertThat(repaymentSchedule.getTotalPenaltyChargesCharged())//
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePenalties(repaymentSchedule.getTotalPenaltyChargesCharged(),
Double.valueOf(expectedValue)))//
case "Penalties" -> assertThat(repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePenalties(
repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Due" -> assertThat(repaymentSchedule.getTotalRepaymentExpected())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleDue(repaymentSchedule.getTotalRepaymentExpected(),
Double.valueOf(expectedValue)))//
case "Due" -> assertThat(repaymentSchedule.getTotalRepaymentExpected().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleDue(
repaymentSchedule.getTotalRepaymentExpected().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Paid" -> assertThat(paidActualBd.doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePaid(paidActualBd.doubleValue(),
Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "In advance" -> assertThat(repaymentSchedule.getTotalPaidInAdvance())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInAdvance(repaymentSchedule.getTotalPaidInAdvance(),
case "In advance" -> assertThat(repaymentSchedule.getTotalPaidInAdvance().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInAdvance(
repaymentSchedule.getTotalPaidInAdvance().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Late" -> assertThat(repaymentSchedule.getTotalPaidLate().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleLate(repaymentSchedule.getTotalPaidLate().doubleValue(),
Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Late" -> assertThat(repaymentSchedule.getTotalPaidLate())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleLate(repaymentSchedule.getTotalPaidLate(),
case "Waived" -> assertThat(repaymentSchedule.getTotalWaived().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleWaived(repaymentSchedule.getTotalWaived().doubleValue(),
Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Waived" -> assertThat(repaymentSchedule.getTotalWaived())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleWaived(repaymentSchedule.getTotalWaived(),
Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
case "Outstanding" -> assertThat(repaymentSchedule.getTotalOutstanding())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleOutstanding(repaymentSchedule.getTotalOutstanding(),
Double.valueOf(expectedValue)))//
case "Outstanding" -> assertThat(repaymentSchedule.getTotalOutstanding().doubleValue())//
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleOutstanding(
repaymentSchedule.getTotalOutstanding().doubleValue(), Double.valueOf(expectedValue)))//
.isEqualTo(Double.valueOf(expectedValue));//
}
}
@@ -3853,7 +3858,7 @@ public class LoanStepDef extends AbstractStepDef {
case "Applicable From" -> actualValues.add(emiVariation.getTermVariationApplicableFrom() == null ? null
: FORMATTER.format(emiVariation.getTermVariationApplicableFrom()));
case "Decimal Value" ->
actualValues.add(emiVariation.getDecimalValue() == null ? null : String.valueOf(emiVariation.getDecimalValue()));
actualValues.add(emiVariation.getDecimalValue() == null ? null : new Utils.DoubleFormatter(emiVariation.getDecimalValue().doubleValue()).format());
case "Date Value" ->
actualValues.add(emiVariation.getDateValue() == null ? null : FORMATTER.format(emiVariation.getDateValue()));
case "Is Specific To Installment" -> actualValues.add(String.valueOf(emiVariation.getIsSpecificToInstallment()));
@@ -188,12 +188,12 @@ Feature: Capitalized Income
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 100.0 | |
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 900.17 EUR transaction amount and system-generated Idempotency key
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000.17 EUR transaction amount and system-generated Idempotency key
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 02 January 2024 | Repayment | 900.17 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false |
| 02 January 2024 | Repayment | 1000.17| 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
@@ -222,12 +222,12 @@ Feature: Capitalized Income
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 100.0 | |
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000 EUR transaction amount and system-generated Idempotency key
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1100 EUR transaction amount and system-generated Idempotency key
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 02 January 2024 | Repayment | 1000.0 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false |
| 02 January 2024 | Repayment | 1100.0 | 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
@@ -262,7 +262,7 @@ Feature: Capitalized Income
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 02 January 2024 | Close (as written-off) | 905.25 | 900.0 | 5.25 | 0.0 | 0.0 | 100.0 | false |
| 02 January 2024 | Close (as written-off) | 1005.81| 1000.0 | 5.81 | 0.0 | 0.0 | 0.0 | false |
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
@@ -555,16 +555,16 @@ Feature: Capitalized Income
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 900.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
@@ -597,16 +597,16 @@ Feature: Capitalized Income
And Admin does charge-off the loan on "03 February 2024"
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 900.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 900.0 | |
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 1000.0| |
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
@@ -639,16 +639,16 @@ Feature: Capitalized Income
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 900.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
@@ -681,16 +681,16 @@ Feature: Capitalized Income
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 900.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
@@ -700,20 +700,20 @@ Feature: Capitalized Income
Then Admin does a charge-off undo the loan
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 900.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
| ASSET | 112601 | Loans Receivable | 900.0 | |
| ASSET | 112603 | Interest/Fee Receivable | 4.0 | |
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 900.0 |
| INCOME | 404001 | Interest Income Charge Off | | 4.0 |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
| ASSET | 112601 | Loans Receivable | 1000.0| |
| ASSET | 112603 | Interest/Fee Receivable | 4.58 | |
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 |
| INCOME | 404001 | Interest Income Charge Off | | 4.58 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | true |
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | true |
Then Reversed loan capitalized income amortization transaction has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
@@ -30,7 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
*/
@RequiredArgsConstructor
@Getter
public final class DisbursementData implements Comparable<DisbursementData> {
public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable<DisbursementData> {
private final Long id;
private final LocalDate expectedDisbursementDate;
@@ -102,8 +102,7 @@ public final class DisbursementData implements Comparable<DisbursementData> {
private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
final LocalDate target) {
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
&& DateUtils.isBefore(target, upToNotInclusive);
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromInclusive, upToNotInclusive, target);
}
public BigDecimal getWaivedChargeAmount() {
@@ -0,0 +1,21 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.data;
public interface LoanPrincipalRelatedDataHolder {}
@@ -34,6 +34,8 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
public class LoanSummaryData {
private final CurrencyData currency;
private final BigDecimal totalPrincipal;
private final BigDecimal totalPrincipalFromCapitalizedIncome;
private final BigDecimal principalDisbursed;
private final BigDecimal principalAdjustments;
private final BigDecimal principalPaid;
@@ -38,6 +38,12 @@ import org.apache.fineract.organisation.monetary.domain.Money;
public class LoanSummary {
// derived totals fields
@Column(name = "total_principal_derived", scale = 6, precision = 19)
private BigDecimal totalPrincipal;
@Column(name = "capitalized_income_derived", scale = 6, precision = 19)
private BigDecimal totalPrincipalFromCapitalizedIncome;
@Column(name = "principal_disbursed_derived", scale = 6, precision = 19)
private BigDecimal totalPrincipalDisbursed;
@@ -166,6 +172,8 @@ public class LoanSummary {
this.totalPenaltyChargesWaived = BigDecimal.ZERO;
this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO;
this.totalPrincipalAdjustments = BigDecimal.ZERO;
this.totalPrincipal = BigDecimal.ZERO;
this.totalPrincipalFromCapitalizedIncome = BigDecimal.ZERO;
this.totalPrincipalDisbursed = BigDecimal.ZERO;
this.totalPrincipalOutstanding = BigDecimal.ZERO;
this.totalPrincipalRepaid = BigDecimal.ZERO;
@@ -176,17 +184,22 @@ public class LoanSummary {
}
public void updateSummary(final MonetaryCurrency currency, final Money principal,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges) {
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges,
List<LoanTransaction> capitalizedIncomeTransactions) {
final BigDecimal principalFromCapitalizedIncome = capitalizedIncomeTransactions.stream().filter(LoanTransaction::isNotReversed)
.map(LoanTransaction::getPrincipalPortion).reduce(BigDecimal.ZERO, BigDecimal::add);
this.totalPrincipalDisbursed = principal.getAmount();
this.totalPrincipalFromCapitalizedIncome = principalFromCapitalizedIncome;
this.totalPrincipal = principal.plus(principalFromCapitalizedIncome).getAmount();
this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalOutstanding = principal.plus(this.totalPrincipalAdjustments).minus(this.totalPrincipalRepaid)
.minus(this.totalPrincipalWrittenOff).getAmount();
this.totalPrincipalOutstanding = principal.plus(principalFromCapitalizedIncome).plus(this.totalPrincipalAdjustments)
.minus(this.totalPrincipalRepaid).minus(this.totalPrincipalWrittenOff).getAmount();
final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency);
this.totalInterestCharged = totalInterestCharged.getAmount();
@@ -225,7 +238,7 @@ public class LoanSummary {
this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid)
.minus(this.totalPenaltyChargesWaived).minus(this.totalPenaltyChargesWrittenOff).getAmount();
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipalDisbursed).plus(this.totalInterestCharged)
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipal).plus(this.totalInterestCharged)
.plus(this.totalFeeChargesCharged).plus(this.totalPenaltyChargesCharged);
this.totalExpectedRepayment = totalExpectedRepayment.getAmount();
@@ -0,0 +1,53 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.apache.fineract.portfolio.loanaccount.data.LoanPrincipalRelatedDataHolder;
/**
* Immutable data object representing a subset of loan transaction data.
*/
@Getter
@EqualsAndHashCode
public class LoanTransactionRepaymentPeriodData implements LoanPrincipalRelatedDataHolder, Serializable {
private final Long transactionId;
private final Long loanId;
private final LocalDate date;
private final boolean reversed;
private final BigDecimal amount;
private final BigDecimal unrecognizedAmount;
private final BigDecimal feeChargesPortion;
public LoanTransactionRepaymentPeriodData(Long transactionId, Long loanId, LocalDate date, boolean reversed, BigDecimal amount,
BigDecimal unrecognizedAmount, BigDecimal feeChargesPortion) {
this.transactionId = transactionId;
this.loanId = loanId;
this.date = date;
this.reversed = reversed;
this.amount = amount;
this.unrecognizedAmount = unrecognizedAmount;
this.feeChargesPortion = feeChargesPortion;
}
}
@@ -104,7 +104,10 @@ public final class LoanRefundValidator {
if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) {
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
final BigDecimal totalPrincipalFromCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
.reduce(BigDecimal.ZERO, BigDecimal::add);
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalPrincipalFromCapitalizedIncome);
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction amount cannot exceed threshold.";
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
@@ -97,7 +97,8 @@ public class LoanBalanceService {
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges());
List<LoanTransaction> capitalizedIncomeTransactions = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome);
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(), capitalizedIncomeTransactions);
loan.updateLoanOutstandingBalances();
}
}
@@ -173,7 +173,10 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
if (loan.getLoanProduct().isMultiDisburseLoan()) {
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
final BigDecimal totalPrincipalFromCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
.reduce(BigDecimal.ZERO, BigDecimal::add);
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalPrincipalFromCapitalizedIncome);
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
final String errorMessage = "The transaction amount cannot exceed threshold.";
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
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.OverdueLoanScheduleData;
@@ -50,7 +51,8 @@ public interface LoanReadPlatformService {
LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData);
LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData,
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
Collection<LoanTransactionData> retrieveLoanTransactions(Long loanId);
@@ -344,6 +344,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
case REAGE -> handleReAge(loanTransaction, ctx);
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx);
// TODO: Cover rest of the transaction types
default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
}
@@ -1199,18 +1200,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
disbursementTransaction.resetDerivedComponents();
if (amortizableAmount.isGreaterThanZero()) {
model.repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(currency, transactionDate);
}
});
}
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
allocateOverpayment(disbursementTransaction, transactionCtx);
}
@@ -1222,7 +1212,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
.toList();
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
Money downPaymentAmount = Money.zero(currency);
@@ -1240,7 +1229,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
recalculateRepaymentInstallmentsWithoutEMICalculation(disbursementTransaction, amortizableAmount, candidateRepaymentInstallments,
currency, installmentAmountInMultiplesOf);
allocateOverpayment(disbursementTransaction, transactionCtx);
}
private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) {
handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx);
} else {
handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx);
}
}
private void handleCapitalizedIncomeWithEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
ProgressiveLoanInterestScheduleModel model;
if (!(transactionCtx instanceof ProgressiveTransactionCtx)
|| (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) {
throw new IllegalStateException("TransactionCtx has no model");
}
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
LocalDate transactionDate = capitalizedIncomeTransaction.getTransactionDate();
MonetaryCurrency currency = transactionCtx.getCurrency();
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
}
private void handleCapitalizedIncomeWithoutEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
MonetaryCurrency currency = transactionCtx.getCurrency();
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
i -> i.getDueDate().isAfter(capitalizedIncomeTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
.toList();
Integer installmentAmountInMultiplesOf = capitalizedIncomeTransaction.getLoan().getLoanProduct()
.getInstallmentAmountInMultiplesOf();
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
recalculateRepaymentInstallmentsWithoutEMICalculation(capitalizedIncomeTransaction, amortizableAmount,
candidateRepaymentInstallments, currency, installmentAmountInMultiplesOf);
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
}
private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model,
List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate, MonetaryCurrency currency) {
if (amortizableAmount.isGreaterThanZero()) {
model.repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(currency, transactionDate);
}
});
}
}
private void recalculateRepaymentInstallmentsWithoutEMICalculation(LoanTransaction loanTransaction, Money amortizableAmount,
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments, MonetaryCurrency currency,
Integer installmentAmountInMultiplesOf) {
if (amortizableAmount.isGreaterThanZero()) {
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
@@ -1252,14 +1307,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
i.updatePrincipal(newPrincipal.getAmount());
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate());
i.updateObligationsMet(currency, loanTransaction.getTransactionDate());
});
// Hence the rounding, we might need to amend the last installment amount
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
.addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject());
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1).addToPrincipal(loanTransaction.getTransactionDate(),
moneyHolder.getMoneyObject());
}
allocateOverpayment(disbursementTransaction, transactionCtx);
}
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
@@ -20,13 +20,20 @@ package org.apache.fineract.portfolio.loanaccount.repository;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
public interface LoanCapitalizedIncomeBalanceRepository
extends JpaRepository<LoanCapitalizedIncomeBalance, Long>, JpaSpecificationExecutor<LoanCapitalizedIncomeBalance> {
String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lcib.loanTransaction.id, lcib.loan.id, lcib.loanTransaction.dateOf, lcib.loanTransaction.reversed, lcib.amount, lcib.unrecognizedAmount, lcib.loanTransaction.feeChargesPortion) FROM LoanCapitalizedIncomeBalance lcib ";
List<LoanCapitalizedIncomeBalance> findAllByLoanId(Long loanId);
LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId);
@Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id = :loanId")
List<LoanTransactionRepaymentPeriodData> findRepaymentPeriodDataByLoanId(Long loanId);
}
@@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -54,6 +55,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
@Transactional
@Override
@@ -74,9 +76,10 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount);
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
transactionDate, txnExternalId);
// TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story)
// Update loan with capitalized income
loan.addLoanTransaction(capitalizedIncomeTransaction);
// Recalculate loan transactions
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
// Create capitalized income balances
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
// Save and flush (PK is set)
@@ -88,7 +91,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (noteText != null && !noteText.isEmpty()) {
noteWritePlatformService.createLoanNote(loanId, noteText);
noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText);
}
// Post journal entries
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
@@ -99,6 +102,14 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
.build();
}
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction capitalizedIncomeTransaction) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
} else {
reprocessLoanTransactionsService.processLatestTransaction(capitalizedIncomeTransaction, loan);
}
}
@Override
public void resetBalance(final Long loanId) {
capitalizedIncomeBalanceRepository
@@ -30,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -45,10 +46,11 @@ public class ProgressiveLoanAccountConfiguration {
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) {
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService) {
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
capitalizedIncomeBalanceRepository);
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService);
}
@Bean
@@ -65,6 +65,12 @@ public interface EMICalculator {
*/
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
/**
* Applies the capitalized income transaction on the interest model. This method recalculates the EMI amounts from
* the action date.
*/
void addCapitalizedIncome(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDueDate, Money transactionAmount);
/**
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
* date.
@@ -119,7 +119,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero())
scheduleModel.zero(), scheduleModel.zero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
operation));
@@ -140,6 +140,24 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return changedRepaymentPeriod.getDueDate();
}
/**
* Add capitalized income to Interest Period
*/
@Override
public void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate transactionDueDate,
final Money transactionAmount) {
addCapitalizedIncome(scheduleModel, EmiChangeOperation.capitalizedIncome(transactionDueDate, transactionAmount));
}
private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(),
scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
operation);
});
}
@Override
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
final BigDecimal newInterestRate) {
@@ -151,7 +169,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
scheduleModel.zero())
scheduleModel.zero(), scheduleModel.zero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
operation));
@@ -160,8 +178,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount)
.ifPresent(repaymentPeriod -> {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount,
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
@@ -237,7 +255,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
Money creditedInterestAmount) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount)
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
.ifPresent(repaymentPeriod -> {
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
@@ -442,8 +460,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
.plus(scheduleModel.getTotalCreditedPrincipal(), mc); //
Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream()
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal))
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(totalDueInterest, mc).minus(totalEMI, mc);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
@@ -809,7 +830,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return;
}
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount());
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
@@ -31,7 +31,8 @@ public class EmiChangeOperation {
public enum Action { //
DISBURSEMENT, //
INTEREST_RATE_CHANGE //
INTEREST_RATE_CHANGE, //
CAPITALIZED_INCOME //
}
private final Action action;
@@ -48,8 +49,12 @@ public class EmiChangeOperation {
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
}
public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) {
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null);
}
public EmiChangeOperation withZeroAmount() {
if (action == Action.DISBURSEMENT) {
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
}
return null;
@@ -62,6 +62,8 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;
private Money capitalizedIncomePrincipal;
@JsonExclude
private final MathContext mc;
private final boolean isPaused;
@@ -70,38 +72,38 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused());
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), mc, interestPeriod.isPaused());
}
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, mc, false);
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, null, mc, false);
}
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused());
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused());
}
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
LocalDate dueDate) {
final Money zero = repaymentPeriod.getEmi().zero();
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
zero.getMc(), false);
}
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate,
boolean isPaused) {
final Money zero = repaymentPeriod.getEmi().zero();
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
zero.getMc(), isPaused);
}
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
LocalDate dueDate) {
final Money zero = repaymentPeriod.getEmi().zero();
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
zero.getMc(), true);
}
@@ -126,6 +128,10 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc);
}
public void addCapitalizedIncomePrincipalAmount(final Money capitalizedIncomePrincipal) {
this.capitalizedIncomePrincipal = MathUtil.plus(this.capitalizedIncomePrincipal, capitalizedIncomePrincipal, mc);
}
public BigDecimal getCalculatedDueInterest() {
if (isPaused) {
return creditedInterest.getAmount();
@@ -156,6 +162,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()//
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)//
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);//
@@ -165,6 +172,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() //
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
.plus(previousInterestPeriod.getDisbursementAmount(), mc)); //
}
}
@@ -173,7 +181,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
* Include principal like amounts (all disbursement amount + credited principal)
*/
public Money getCreditedAmounts() {
return getDisbursementAmount().plus(getCreditedPrincipal(), mc);
return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc);
}
public boolean isFirstInterestPeriod() {
@@ -175,9 +175,10 @@ public class ProgressiveLoanInterestScheduleModel {
}
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
final Money disbursedAmount, final Money correctionAmount) {
final Money disbursedAmount, final Money correctionAmount, final Money capitalizedIncomePrincipal) {
return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()//
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount,
capitalizedIncomePrincipal))//
.findFirst();//
}
@@ -214,7 +215,7 @@ public class ProgressiveLoanInterestScheduleModel {
}
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
final Money correctionAmount, final Money capitalizedIncomePrincipal) {
return repaymentPeriod -> {
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
@@ -222,9 +223,10 @@ public class ProgressiveLoanInterestScheduleModel {
isChangeOnMaturityDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
interestPeriodOptional.get().addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
} else {
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount, capitalizedIncomePrincipal);
}
};
}
@@ -246,7 +248,7 @@ public class ProgressiveLoanInterestScheduleModel {
}
void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
final Money correctionAmount, Money capitalizedIncomePrincipal) {
final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate);
final LocalDate originalDueDate = previousInterestPeriod.getDueDate();
final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate);
@@ -254,6 +256,7 @@ public class ProgressiveLoanInterestScheduleModel {
previousInterestPeriod.setDueDate(newDueDate);
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
previousInterestPeriod.addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate, isPaused);
@@ -225,6 +225,17 @@ public final class RepaymentPeriod {
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
}
/**
* Sum of capitalized income principals
*
* @return
*/
public Money getCapitalizedIncomePrincipal() {
return MathUtil.negativeToZero(interestPeriods.stream() //
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
}
/**
* Gives back due principal + credited principal or paid principal
*
@@ -278,6 +289,7 @@ public final class RepaymentPeriod {
InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) //
.plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
.plus(lastInterestPeriod.getDisbursementAmount(), mc) //
.minus(getDuePrincipal(), mc)//
.plus(getPaidPrincipal(), mc);//
@@ -305,7 +317,10 @@ public final class RepaymentPeriod {
Money totalDisbursedAmount = getInterestPeriods().stream() //
.map(InterestPeriod::getDisbursementAmount) //
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
return initialBalance.add(totalDisbursedAmount, mc);
Money totalCapitalizedIncomeAmount = getInterestPeriods().stream() //
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
return initialBalance.add(totalDisbursedAmount, mc).add(totalCapitalizedIncomeAmount, mc);
}
private Money getZero(MathContext mc) {
@@ -464,6 +464,7 @@ public class LoanImportHandler implements ImportHandler {
JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject();
loanRepaymentJsonob.remove("manuallyReversed");
loanRepaymentJsonob.remove("numberOfRepayments");
loanRepaymentJsonob.remove("reversed");
String payload = loanRepaymentJsonob.toString();
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.loanRepaymentTransaction(result.getLoanId()) //
@@ -139,6 +139,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeTyp
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException;
import org.apache.fineract.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException;
@@ -150,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
@@ -297,6 +299,7 @@ public class LoansApiResource {
private final ClientReadPlatformService clientReadPlatformService;
private final LoanTermVariationsRepository loanTermVariationsRepository;
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
/*
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
@@ -498,11 +501,13 @@ public class LoansApiResource {
loanBasicDetails.getPageItems().forEach(i -> {
if (i.getSummary() != null) {
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
.findRepaymentPeriodDataByLoanId(i.getId());
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(),
repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
@@ -981,13 +986,15 @@ public class LoansApiResource {
if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
.findRepaymentPeriodDataByLoanId(resolvedLoanId);
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
loanBasicDetails.getFeeChargesAtDisbursementCharged());
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
disbursementData, loanBasicDetails.isInterestRecalculationEnabled(),
disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName)
@@ -42,9 +42,9 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2012, 4, 3]")
public LocalDate approvalDate;
@Schema(example = "200.000000")
public Double approvalAmount;
public BigDecimal approvalAmount;
@Schema(example = "200.000000")
public Double netDisbursalAmount;
public BigDecimal netDisbursalAmount;
public GetLoanCurrency currency;
}
@@ -289,29 +289,29 @@ final class LoansApiResourceSwagger {
@Schema(example = "30")
public Long loanTermInDays;
@Schema(example = "200.000000")
public Double totalPrincipalDisbursed;
public BigDecimal totalPrincipalDisbursed;
@Schema(example = "200.00")
public Double totalPrincipalExpected;
public BigDecimal totalPrincipalExpected;
@Schema(example = "200.00")
public Double totalPrincipalPaid;
public BigDecimal totalPrincipalPaid;
@Schema(example = "0.00")
public Double totalInterestCharged;
public BigDecimal totalInterestCharged;
@Schema(example = "0.00")
public Double totalFeeChargesCharged;
public BigDecimal totalFeeChargesCharged;
@Schema(example = "0.00")
public Double totalPenaltyChargesCharged;
public BigDecimal totalPenaltyChargesCharged;
@Schema(example = "0.00")
public Double totalWaived;
public BigDecimal totalWaived;
@Schema(example = "0.00")
public Double totalWrittenOff;
public BigDecimal totalWrittenOff;
@Schema(example = "200.00")
public Double totalRepaymentExpected;
public BigDecimal totalRepaymentExpected;
@Schema(example = "200.00")
public Double totalPaidInAdvance;
public BigDecimal totalPaidInAdvance;
@Schema(example = "0.00")
public Double totalPaidLate;
public BigDecimal totalPaidLate;
@Schema(example = "0.00")
public Double totalOutstanding;
public BigDecimal totalOutstanding;
public List<GetLoansLoanIdRepaymentPeriod> periods;
}
@@ -332,71 +332,71 @@ final class LoansApiResourceSwagger {
@Schema(example = "30")
public Long daysInPeriod;
@Schema(example = "200.000000")
public Double principalOriginalDue;
public BigDecimal principalOriginalDue;
@Schema(example = "200.000000")
public Double principalDue;
public BigDecimal principalDue;
@Schema(example = "200.000000")
public Double principalPaid;
public BigDecimal principalPaid;
@Schema(example = "0.000000")
public Double principalWrittenOff;
public BigDecimal principalWrittenOff;
@Schema(example = "20.000000")
public Double principalOutstanding;
public BigDecimal principalOutstanding;
@Schema(example = "20.000000")
public Double principalLoanBalanceOutstanding;
public BigDecimal principalLoanBalanceOutstanding;
@Schema(example = "0.000000")
public Double interestOriginalDue;
public BigDecimal interestOriginalDue;
@Schema(example = "0.000000")
public Double interestDue;
public BigDecimal interestDue;
@Schema(example = "0.000000")
public Double interestPaid;
public BigDecimal interestPaid;
@Schema(example = "0.000000")
public Double interestWaived;
public BigDecimal interestWaived;
@Schema(example = "0.000000")
public Double interestWrittenOff;
public BigDecimal interestWrittenOff;
@Schema(example = "0.000000")
public Double interestOutstanding;
public BigDecimal interestOutstanding;
@Schema(example = "0.000000")
public Double feeChargesDue;
public BigDecimal feeChargesDue;
@Schema(example = "20.000000")
public Double feeChargesPaid;
public BigDecimal feeChargesPaid;
@Schema(example = "20.000000")
public Double feeChargesWaived;
public BigDecimal feeChargesWaived;
@Schema(example = "20.000000")
public Double feeChargesWrittenOff;
public BigDecimal feeChargesWrittenOff;
@Schema(example = "20.000000")
public Double feeChargesOutstanding;
public BigDecimal feeChargesOutstanding;
@Schema(example = "20.000000")
public Double penaltyChargesDue;
public BigDecimal penaltyChargesDue;
@Schema(example = "20.000000")
public Double penaltyChargesPaid;
public BigDecimal penaltyChargesPaid;
@Schema(example = "20.000000")
public Double penaltyChargesWaived;
public BigDecimal penaltyChargesWaived;
@Schema(example = "20.000000")
public Double penaltyChargesWrittenOff;
public BigDecimal penaltyChargesWrittenOff;
@Schema(example = "20.000000")
public Double penaltyChargesOutstanding;
public BigDecimal penaltyChargesOutstanding;
@Schema(example = "20.000000")
public Double totalOriginalDueForPeriod;
public BigDecimal totalOriginalDueForPeriod;
@Schema(example = "20.000000")
public Double totalDueForPeriod;
public BigDecimal totalDueForPeriod;
@Schema(example = "20.000000")
public Double totalPaidForPeriod;
public BigDecimal totalPaidForPeriod;
@Schema(example = "20.000000")
public Double totalPaidInAdvanceForPeriod;
public BigDecimal totalPaidInAdvanceForPeriod;
@Schema(example = "20.000000")
public Double totalPaidLateForPeriod;
public BigDecimal totalPaidLateForPeriod;
@Schema(example = "20.000000")
public Double totalWaivedForPeriod;
public BigDecimal totalWaivedForPeriod;
@Schema(example = "20.000000")
public Double totalWrittenOffForPeriod;
public BigDecimal totalWrittenOffForPeriod;
@Schema(example = "200.000000")
public Double totalOutstandingForPeriod;
public BigDecimal totalOutstandingForPeriod;
@Schema(example = "20.000000")
public Double totalActualCostOfLoanForPeriod;
public BigDecimal totalActualCostOfLoanForPeriod;
@Schema(example = "200.000000")
public Double totalInstallmentAmountForPeriod;
public BigDecimal totalInstallmentAmountForPeriod;
@Schema(example = "2.000000")
public Double totalCredits;
public BigDecimal totalCredits;
@Schema(example = "true")
public Boolean downPaymentPeriod;
}
@@ -412,15 +412,15 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2022, 07, 01]")
public LocalDate actualDisbursementDate;
@Schema(example = "22000.000000")
public Double principal;
public BigDecimal principal;
@Schema(example = "22000.000000")
public Double netDisbursalAmount;
public BigDecimal netDisbursalAmount;
@Schema(example = "1")
public String loanChargeId;
@Schema(example = "22000.000000")
public Double chargeAmount;
public BigDecimal chargeAmount;
@Schema(example = "22000.000000")
public Double waivedChargeAmount;
public BigDecimal waivedChargeAmount;
@Schema(example = "dd MMMM yyyy")
public String dateFormat;
@Schema(example = "de_DE")
@@ -517,77 +517,81 @@ final class LoansApiResourceSwagger {
public GetLoansLoanIdCurrency currency;
@Schema(example = "1000000.000000")
public Double principalDisbursed;
@Schema(example = "0.000000")
public Double principalPaid;
@Schema(example = "0.00")
public Double principalAdjustments;
@Schema(example = "0.000000")
public Double principalWrittenOff;
public BigDecimal totalPrincipal;
@Schema(example = "1000000.000000")
public Double principalOutstanding;
public BigDecimal principalDisbursed;
@Schema(example = "1000000.000000")
public BigDecimal totalPrincipalFromCapitalizedIncome;
@Schema(example = "0.000000")
public BigDecimal principalPaid;
@Schema(example = "0.00")
public BigDecimal principalAdjustments;
@Schema(example = "0.000000")
public BigDecimal principalWrittenOff;
@Schema(example = "1000000.000000")
public BigDecimal principalOutstanding;
@Schema(example = "833333.300000")
public Double principalOverdue;
public BigDecimal principalOverdue;
@Schema(example = "240000.000000")
public Double interestCharged;
public BigDecimal interestCharged;
@Schema(example = "0.000000")
public Double interestPaid;
public BigDecimal interestPaid;
@Schema(example = "0.000000")
public Double interestWaived;
public BigDecimal interestWaived;
@Schema(example = "0.000000")
public Double interestWrittenOff;
public BigDecimal interestWrittenOff;
@Schema(example = "240000.000000")
public Double interestOutstanding;
public BigDecimal interestOutstanding;
@Schema(example = "200000.000000")
public Double interestOverdue;
public BigDecimal interestOverdue;
@Schema(example = "0.00")
public Double feeAdjustments;
public BigDecimal feeAdjustments;
@Schema(example = "18000.000000")
public Double feeChargesCharged;
public BigDecimal feeChargesCharged;
@Schema(example = "0.000000")
public Double feeChargesDueAtDisbursementCharged;
public BigDecimal feeChargesDueAtDisbursementCharged;
@Schema(example = "0.000000")
public Double feeChargesPaid;
public BigDecimal feeChargesPaid;
@Schema(example = "0.000000")
public Double feeChargesWaived;
public BigDecimal feeChargesWaived;
@Schema(example = "0.000000")
public Double feeChargesWrittenOff;
public BigDecimal feeChargesWrittenOff;
@Schema(example = "18000.000000")
public Double feeChargesOutstanding;
public BigDecimal feeChargesOutstanding;
@Schema(example = "15000.000000")
public Double feeChargesOverdue;
public BigDecimal feeChargesOverdue;
@Schema(example = "0.00")
public Double penaltyAdjustments;
public BigDecimal penaltyAdjustments;
@Schema(example = "0.000000")
public Double penaltyChargesCharged;
public BigDecimal penaltyChargesCharged;
@Schema(example = "0.000000")
public Double penaltyChargesPaid;
public BigDecimal penaltyChargesPaid;
@Schema(example = "0.000000")
public Double penaltyChargesWaived;
public BigDecimal penaltyChargesWaived;
@Schema(example = "0.000000")
public Double penaltyChargesWrittenOff;
public BigDecimal penaltyChargesWrittenOff;
@Schema(example = "0.000000")
public Double penaltyChargesOutstanding;
public BigDecimal penaltyChargesOutstanding;
@Schema(example = "0.000000")
public Double penaltyChargesOverdue;
public BigDecimal penaltyChargesOverdue;
@Schema(example = "1258000.000000")
public Double totalExpectedRepayment;
public BigDecimal totalExpectedRepayment;
@Schema(example = "0.000000")
public Double totalRepayment;
public BigDecimal totalRepayment;
@Schema(example = "258000.000000")
public Double totalExpectedCostOfLoan;
public BigDecimal totalExpectedCostOfLoan;
@Schema(example = "0.000000")
public Double totalCostOfLoan;
public BigDecimal totalCostOfLoan;
@Schema(example = "0.000000")
public Double totalWaived;
public BigDecimal totalWaived;
@Schema(example = "0.000000")
public Double totalWrittenOff;
public BigDecimal totalWrittenOff;
@Schema(example = "1258000.000000")
public Double totalOutstanding;
public BigDecimal totalOutstanding;
@Schema(example = "1048333.30000")
public Double totalOverdue;
public BigDecimal totalOverdue;
@Schema(example = "2456.30000")
public Double totalRecovered;
public BigDecimal totalRecovered;
@Schema(example = "[2012, 5, 10]")
public LocalDate overdueSinceDate;
@Schema(example = "1")
@@ -597,9 +601,9 @@ final class LoansApiResourceSwagger {
public GetLoansLoanIdLinkedAccount linkedAccount;
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@Schema(example = "1100.000000")
public Double fixedEmiAmount;
public BigDecimal fixedEmiAmount;
@Schema(example = "35000.000000")
public Double maxOutstandingLoanBalance;
public BigDecimal maxOutstandingLoanBalance;
@Schema(example = "false")
public Boolean canDisburse;
@Schema(example = "true")
@@ -607,33 +611,33 @@ final class LoansApiResourceSwagger {
@Schema(example = "false")
public Boolean isNPA;
@Schema(example = "0.000000")
public Double totalMerchantRefund;
public BigDecimal totalMerchantRefund;
@Schema(example = "0.000000")
public Double totalMerchantRefundReversed;
public BigDecimal totalMerchantRefundReversed;
@Schema(example = "0.000000")
public Double totalPayoutRefund;
public BigDecimal totalPayoutRefund;
@Schema(example = "0.000000")
public Double totalPayoutRefundReversed;
public BigDecimal totalPayoutRefundReversed;
@Schema(example = "0.000000")
public Double totalGoodwillCredit;
public BigDecimal totalGoodwillCredit;
@Schema(example = "0.000000")
public Double totalGoodwillCreditReversed;
public BigDecimal totalGoodwillCreditReversed;
@Schema(example = "0.000000")
public Double totalChargeAdjustment;
public BigDecimal totalChargeAdjustment;
@Schema(example = "0.000000")
public Double totalChargeAdjustmentReversed;
public BigDecimal totalChargeAdjustmentReversed;
@Schema(example = "0.000000")
public Double totalChargeback;
public BigDecimal totalChargeback;
@Schema(example = "0.000000")
public Double totalCreditBalanceRefund;
public BigDecimal totalCreditBalanceRefund;
@Schema(example = "0.000000")
public Double totalCreditBalanceRefundReversed;
public BigDecimal totalCreditBalanceRefundReversed;
@Schema(example = "0.000000")
public Double totalRepaymentTransaction;
public BigDecimal totalRepaymentTransaction;
@Schema(example = "0.000000")
public Double totalInterestPaymentWaiver;
public BigDecimal totalInterestPaymentWaiver;
@Schema(example = "0.000000")
public Double totalRepaymentTransactionReversed;
public BigDecimal totalRepaymentTransactionReversed;
@Schema(example = "0.000000")
public BigDecimal totalUnpaidPayableDueInterest;
@Schema(example = "0.000000")
@@ -754,7 +758,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "11")
public Long id;
@Schema(example = "100.000000")
public Double amount;
public BigDecimal amount;
@Schema(example = "9679")
public Integer installmentNumber;
@Schema(example = "1")
@@ -810,7 +814,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "CHARGEBACK")
public String relationType;
@Schema(example = "100.00")
public Double amount;
public BigDecimal amount;
@Schema(example = "Repayment Adjustment Chargeback")
public String paymentType;
@@ -831,27 +835,27 @@ final class LoansApiResourceSwagger {
@Schema(description = "Payment detail")
public GetLoansLoanIdPaymentDetailData paymentDetailData;
@Schema(example = "100.000000")
public Double amount;
public BigDecimal amount;
@Schema(example = "100.000000")
public Double netDisbursalAmount;
public BigDecimal netDisbursalAmount;
@Schema(example = "100.000000")
public Double principalPortion;
public BigDecimal principalPortion;
@Schema(example = "100.000000")
public Double interestPortion;
public BigDecimal interestPortion;
@Schema(example = "100.000000")
public Double feeChargesPortion;
public BigDecimal feeChargesPortion;
@Schema(example = "100.000000")
public Double penaltyChargesPortion;
public BigDecimal penaltyChargesPortion;
@Schema(example = "100.000000")
public Double overpaymentPortion;
public BigDecimal overpaymentPortion;
@Schema(example = "100.000000")
public Double unrecognizedIncomePortion;
public BigDecimal unrecognizedIncomePortion;
@Schema(example = "3")
public String externalId;
@Schema(example = "100.000000")
public Double fixedEmiAmount;
public BigDecimal fixedEmiAmount;
@Schema(example = "100.000000")
public Double outstandingLoanBalance;
public BigDecimal outstandingLoanBalance;
@Schema(example = "[2022, 07, 01]")
public LocalDate submittedOnDate;
public boolean manuallyReversed;
@@ -870,7 +874,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "de_DE")
public String locale;
@Schema(example = "100.000000")
public Double transactionAmount;
public BigDecimal transactionAmount;
@Schema(example = "[2022, 07, 01]")
public LocalDate transactionDate;
@Schema(example = "101")
@@ -924,19 +928,19 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2022, 07, 01]")
public LocalDate dueDate;
@Schema(example = "13.560000")
public Double amount;
public BigDecimal amount;
@Schema(example = "13.560000")
public Double amountOutstanding;
public BigDecimal amountOutstanding;
@Schema(example = "13.560000")
public Double amountWaived;
public BigDecimal amountWaived;
@Schema(example = "false")
public boolean paid;
@Schema(example = "false")
public boolean waived;
@Schema(example = "13.560000")
public Double amountAccrued;
public BigDecimal amountAccrued;
@Schema(example = "13.560000")
public Double amountUnrecognized;
public BigDecimal amountUnrecognized;
}
@Schema(example = "3")
@@ -952,23 +956,23 @@ final class LoansApiResourceSwagger {
@Schema(description = "Enum option data")
public GetLoansLoanIdEnumOptionData chargeCalculationType;
@Schema(example = "3.400000")
public Double percentage;
public BigDecimal percentage;
@Schema(example = "13.560000")
public Double amountPercentageAppliedTo;
public BigDecimal amountPercentageAppliedTo;
@Schema(description = "currency")
public GetLoansLoanIdCurrency currency;
@Schema(example = "102.000000")
public Double amount;
public BigDecimal amount;
@Schema(example = "12.000000")
public Double amountPaid;
public BigDecimal amountPaid;
@Schema(example = "14.000000")
public Double amountWaived;
public BigDecimal amountWaived;
@Schema(example = "102.000000")
public Double amountWrittenOff;
public BigDecimal amountWrittenOff;
@Schema(example = "102.000000")
public Double amountOutstanding;
public BigDecimal amountOutstanding;
@Schema(example = "102.000000")
public Double amountOrPercentage;
public BigDecimal amountOrPercentage;
@Schema(example = "false")
public boolean penalty;
@Schema(description = "Enum option data")
@@ -982,15 +986,15 @@ final class LoansApiResourceSwagger {
@Schema(example = "3")
public Long loanId;
@Schema(example = "30.000000")
public Double minCap;
public BigDecimal minCap;
@Schema(example = "30.000000")
public Double maxCap;
public BigDecimal maxCap;
@Schema(description = "List of GetLoansLoanIdLoanInstallmentChargeData")
public List<GetLoansLoanIdLoanInstallmentChargeData> installmentChargeData;
@Schema(example = "30.000000")
private Double amountAccrued;
private BigDecimal amountAccrued;
@Schema(example = "30.000000")
private Double amountUnrecognized;
private BigDecimal amountUnrecognized;
@Schema(example = "3ert3453")
private String externalId;
}
@@ -1000,7 +1004,7 @@ final class LoansApiResourceSwagger {
private GetLoansLoanIdDelinquencySummary() {}
@Schema(example = "100.000000")
public Double availableDisbursementAmount;
public BigDecimal availableDisbursementAmount;
@Schema(example = "12")
public Integer pastDueDays;
@Schema(example = "[2022, 07, 01]")
@@ -1010,7 +1014,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2022, 07, 01]")
public LocalDate delinquentDate;
@Schema(example = "100.000000")
public Double delinquentAmount;
public BigDecimal delinquentAmount;
@Schema(example = "80.000000")
public BigDecimal delinquentPrincipal;
@Schema(example = "10.000000")
@@ -1022,12 +1026,12 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2022, 07, 01]")
public LocalDate lastPaymentDate;
@Schema(example = "100.000000")
public Double lastPaymentAmount;
public BigDecimal lastPaymentAmount;
@Schema(example = "[2022, 07, 01]")
public LocalDate lastRepaymentDate;
@Schema(example = "100.000000")
public Double lastRepaymentAmount;
public BigDecimal lastRepaymentAmount;
@Schema(description = "List of GetLoansLoanIdDelinquencyPausePeriod")
public List<GetLoansLoanIdDelinquencyPausePeriod> delinquencyPausePeriods;
@@ -1094,7 +1098,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2024, 1, 1]")
public LocalDate termVariationApplicableFrom;
@Schema(example = "200.000000")
public Double decimalValue;
public BigDecimal decimalValue;
@Schema(example = "[2024, 1, 1]")
public LocalDate dateValue;
@Schema(example = "false")
@@ -1139,11 +1143,11 @@ final class LoansApiResourceSwagger {
@Schema(example = "1000000.00")
public BigDecimal principal;
@Schema(example = "1000.000000")
public Double approvedPrincipal;
public BigDecimal approvedPrincipal;
@Schema(example = "1001.000000")
public Double proposedPrincipal;
public BigDecimal proposedPrincipal;
@Schema(example = "200.000000")
public Double netDisbursalAmount;
public BigDecimal netDisbursalAmount;
@Schema(example = "12")
public Integer termFrequency;
public GetLoansLoanIdTermPeriodFrequencyType termPeriodFrequencyType;
@@ -1185,7 +1189,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "false")
public Boolean enableInstallmentLevelDelinquency;
@Schema(example = "250.000000")
public Double totalOverpaid;
public BigDecimal totalOverpaid;
public LocalDate lastClosedBusinessDate;
@Schema(example = "[2013, 11, 1]")
public LocalDate overpaidOnDate;
@@ -1240,7 +1244,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "1 November 2023")
public String expectedDisbursementDate;
@Schema(example = "1000.00")
public Double principal;
public BigDecimal principal;
}
private PostLoansRequest() {}
@@ -1387,7 +1391,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "0")
public Long totalPrincipalPaid;
@Schema(example = "13471.52")
public Double totalInterestCharged;
public BigDecimal totalInterestCharged;
@Schema(example = "0")
public Long totalFeeChargesCharged;
@Schema(example = "0")
@@ -1397,7 +1401,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "0")
public Long totalWrittenOff;
@Schema(example = "113471.52")
public Double totalRepaymentExpected;
public BigDecimal totalRepaymentExpected;
@Schema(example = "0")
public Long totalRepayment;
@Schema(example = "0")
@@ -1616,7 +1620,7 @@ final class LoansApiResourceSwagger {
@Schema(example = "[2012, 4, 3]")
public LocalDate expectedDisbursementDate;
@Schema(example = "22000")
public Double principal;
public BigDecimal principal;
}
@Schema(example = "2")
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.data;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class LoanSchedulePeriodDataWrapper {
private final LoanPrincipalRelatedDataHolder data;
private final LocalDate date;
private final boolean isDisbursement;
}
@@ -97,7 +97,8 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
}
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed())
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed()).totalPrincipal(defaultSummaryData.getTotalPrincipal())
.totalPrincipalFromCapitalizedIncome(defaultSummaryData.getTotalPrincipalFromCapitalizedIncome())
.principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid())
.principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff())
.principalOutstanding(defaultSummaryData.getPrincipalOutstanding())
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
@@ -51,9 +53,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
@Transactional
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
false);
amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction)));
}
@Override
@@ -67,13 +70,16 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
transactionDate = DateUtils.getBusinessLocalDate();
}
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
true);
if (amortizationTransaction.isPresent()) {
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
}
}
private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
private Optional<LoanTransaction> createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
final boolean isChargeOff) {
ExternalId externalId = ExternalId.empty();
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
@@ -92,13 +98,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
balance.setUnrecognizedAmount(BigDecimal.ZERO);
}
if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) {
return Optional.empty();
}
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
transactionDate, totalUnrecognizedAmount, externalId);
loan.addLoanTransaction(amortizationTransaction);
loanTransactionRepository.saveAndFlush(amortizationTransaction);
return amortizationTransaction;
return Optional.of(amortizationTransaction);
}
@Override
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanSchedulePeriodDataWrapper;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
@@ -118,6 +119,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -249,8 +251,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
accountData.getFeeChargesAtDisbursementCharged());
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
.findRepaymentPeriodDataByLoanId(accountData.getId());
final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData,
disbursementData, accountData.isInterestRecalculationEnabled(),
disbursementData, capitalizedIncomeData, accountData.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType()));
accountData.setRepaymentSchedule(repaymentSchedule);
return accountData;
@@ -259,13 +263,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
@Override
public LoanScheduleData retrieveRepaymentSchedule(final Long loanId,
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
LoanScheduleType loanScheduleType) {
try {
this.context.authenticatedUser();
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled, loanScheduleType);
repaymentScheduleRelatedLoanData, disbursementData, capitalizedIncomeData, isInterestRecalculationEnabled,
loanScheduleType);
final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment";
return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR
@@ -749,7 +755,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname,"
+ " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, "
+ " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate, "
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.total_principal_derived as totalPrincipal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
+ " l.grace_on_principal_periods as graceOnPrincipalPayment, l.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged,l.grace_on_arrears_ageing as graceOnArrearsAgeing,"
+ " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, "
+ " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, "
@@ -764,7 +770,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ sqlGenerator.escape("name")
+ " as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode, "
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, "
+ " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid,"
+ " l.principal_disbursed_derived as principalDisbursed, l.capitalized_income_derived as totalPrincipalFromCapitalizedIncome, l.principal_repaid_derived as principalPaid,"
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff,"
+ " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
@@ -1023,6 +1029,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
// loan summary
final BigDecimal principalDisbursed = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDisbursed");
final BigDecimal totalPrincipal = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPrincipal");
final BigDecimal totalPrincipalFromCapitalizedIncome = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
"totalPrincipalFromCapitalizedIncome");
final BigDecimal principalAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAdjustments");
final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
@@ -1066,6 +1075,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
inArrears = (overdueSinceDate != null);
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
.totalPrincipal(totalPrincipal).totalPrincipalFromCapitalizedIncome(totalPrincipalFromCapitalizedIncome)
.principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
.principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
.interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
@@ -1271,15 +1281,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final DisbursementData disbursement;
private final BigDecimal totalFeeChargesDueAtDisbursement;
private final Collection<DisbursementData> disbursementData;
private final Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData;
private final LoanScheduleType loanScheduleType;
private LocalDate lastDueDate;
private BigDecimal outstandingLoanPrincipalBalance;
private boolean excludePastUnDisbursed;
LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
this.currency = repaymentScheduleRelatedLoanData.getCurrency();
this.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
this.capitalizedIncomeData = capitalizedIncomeData;
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
this.lastDueDate = this.disbursement.disbursementDate();
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
@@ -1359,10 +1372,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
final boolean complete = rs.getBoolean("complete");
BigDecimal disbursedAmount = BigDecimal.ZERO;
disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds,
disbursementChargeAmount, waivedChargeAmount, periods);
List<LoanSchedulePeriodDataWrapper> combinedDataList = new ArrayList<>();
combinedDataList.addAll(
collectEligibleDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds));
combinedDataList.addAll(collectEligibleCapitalizedIncomeData(fromDate, dueDate, disbursementPeriodIds));
combinedDataList.sort(this::sortPeriodDataHolders);
fillLoanSchedulePeriodData(periods, combinedDataList, disbursementChargeAmount, waivedChargeAmount);
BigDecimal disbursedAmount = calculateDisbursedAmount(combinedDataList);
// Add the Charge back or Credits to the initial amount to avoid negative balance
final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits");
@@ -1473,26 +1491,82 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
}
private BigDecimal processDisbursementData(LoanScheduleType loanScheduleType, Collection<DisbursementData> disbursementData,
LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds, BigDecimal disbursementChargeAmount,
BigDecimal waivedChargeAmount, List<LoanSchedulePeriodData> periods) {
BigDecimal disbursedAmount = BigDecimal.ZERO;
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
// Collect eligible disbursement data
for (final DisbursementData data : disbursementData) {
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
if (((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
&& !disbursementPeriodIds.contains(data.getId())) {
disbursedAmount = disbursedAmount.add(data.getPrincipal());
LoanSchedulePeriodData periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
periods.add(periodData);
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
&& !disbursementPeriodIds.contains(data.getId());
if (isEligible) {
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
disbursementPeriodIds.add(data.getId());
}
}
return disbursementDataList;
}
private List<LoanSchedulePeriodDataWrapper> collectEligibleCapitalizedIncomeData(LocalDate fromDate, LocalDate dueDate,
Set<Long> disbursementPeriodIds) {
List<LoanSchedulePeriodDataWrapper> capitalizedIncomeDataList = new ArrayList<>();
// Collect eligible capitalized income data
for (LoanTransactionRepaymentPeriodData data : capitalizedIncomeData) {
boolean isEligible = canAddCapitalizedIncomeData(data, fromDate, dueDate)
&& !disbursementPeriodIds.contains(data.getTransactionId());
if (isEligible) {
capitalizedIncomeDataList.add(new LoanSchedulePeriodDataWrapper(data, data.getDate(), false));
disbursementPeriodIds.add(data.getTransactionId());
}
}
return capitalizedIncomeDataList;
}
private void fillLoanSchedulePeriodData(List<LoanSchedulePeriodData> periods, List<LoanSchedulePeriodDataWrapper> combinedDataList,
BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount) {
// Process all collected data in chronological order
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
LoanSchedulePeriodData periodData;
if (dataItem.isDisbursement()) {
// Process disbursement data
DisbursementData data = (DisbursementData) dataItem.getData();
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
} else {
// Process capitalized income data
LoanTransactionRepaymentPeriodData data = (LoanTransactionRepaymentPeriodData) dataItem.getData();
periodData = createLoanSchedulePeriodData(data);
}
// Common processing for both data types
periods.add(periodData);
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
}
}
private BigDecimal calculateDisbursedAmount(List<LoanSchedulePeriodDataWrapper> combinedDataList) {
BigDecimal disbursedAmount = BigDecimal.ZERO;
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
if (dataItem.isDisbursement()) {
DisbursementData data = (DisbursementData) dataItem.getData();
disbursedAmount = disbursedAmount.add(data.getPrincipal());
}
}
return disbursedAmount;
}
private int sortPeriodDataHolders(LoanSchedulePeriodDataWrapper item1, LoanSchedulePeriodDataWrapper item2) {
int dateComparison = item1.getDate().compareTo(item2.getDate());
if (dateComparison == 0 && item1.isDisbursement() != item2.isDisbursement()) {
// If dates are equal, prioritize disbursement data
return item1.isDisbursement() ? -1 : 1;
}
return dateComparison;
}
private LoanSchedulePeriodData createLoanSchedulePeriodData(final DisbursementData data, BigDecimal disbursementChargeAmount,
BigDecimal waivedChargeAmount) {
BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount
@@ -1501,11 +1575,20 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
data.isDisbursed());
}
private LoanSchedulePeriodData createLoanSchedulePeriodData(final LoanTransactionRepaymentPeriodData data) {
BigDecimal feeCharges = Objects.isNull(data.getFeeChargesPortion()) ? BigDecimal.ZERO : data.getFeeChargesPortion();
return LoanSchedulePeriodData.disbursementOnlyPeriod(data.getDate(), data.getAmount(), feeCharges, !data.isReversed());
}
private boolean canAddDisbursementData(DisbursementData data, boolean isDueForDisbursement, boolean excludePastUnDisbursed) {
return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate()))
&& isDueForDisbursement;
}
private boolean canAddCapitalizedIncomeData(LoanTransactionRepaymentPeriodData data, LocalDate fromDate, LocalDate dueDate) {
return !data.isReversed() && DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, dueDate, data.getDate());
}
}
private static final class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
@@ -2168,7 +2251,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
final Boolean isReversed = false;
final Boolean isManuallyReversed = false;
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
@@ -2178,7 +2261,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion,
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isReversed, loanId, loan.getExternalId());
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isManuallyReversed, loanId,
loan.getExternalId());
}
private static final class CurrencyMapper implements RowMapper<CurrencyData> {
@@ -64,6 +64,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@@ -93,6 +94,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
private final LoanChargeValidator loanChargeValidator;
private final LoanJournalEntryPoster journalEntryPoster;
private final LoanBalanceService loanBalanceService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
@Override
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
@@ -245,8 +247,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
transactionForAdjustment.getTransactionDate());
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) {
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted.";
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()
&& !transactionForAdjustment.isCapitalizedIncome()) {
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, capitalized income, accrual or credit balance refund can be adjusted.";
throw new InvalidLoanTransactionTypeException("transaction",
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
}
@@ -285,6 +288,10 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment,
scheduleGeneratorDTO);
}
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
}
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
@@ -204,9 +204,13 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
}
@Override
public void createLoanNote(final Long loanId, final String note) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final Note newNote = Note.loanNote(loan, note);
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
final Loan loan = loanTransaction.getLoan();
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
this.noteRepository.save(newNote);
}
@@ -196,4 +196,5 @@
<include file="parts/0175_add_fk_acc_product_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan">
<column defaultValueNumeric="0.000000" name="capitalized_income_derived" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column defaultValueNumeric="0.000000" name="total_principal_derived" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -5938,7 +5938,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
executeInlineCOB(createdLoanId.get());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
// totalUnpaidPayableDueInterest → Total outstanding interest amount on all the periods that are before
assertEquals(BigDecimal.valueOf(loanDetails.getSummary().getInterestCharged()).stripTrailingZeros(),
assertEquals(loanDetails.getSummary().getInterestCharged().stripTrailingZeros(),
loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros());
// Total outstanding interest amount on the current period, if the current period due date is after than the
// current date
@@ -6131,21 +6131,21 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().get(index);
assertEquals(dueDate, period.getDueDate());
assertEquals(paidDate, period.getObligationsMetOnDate());
assertEquals(balanceOfLoan, period.getPrincipalLoanBalanceOutstanding());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(feeDue, period.getFeeChargesDue());
assertEquals(feePaid, period.getFeeChargesPaid());
assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
assertEquals(penaltyDue, period.getPenaltyChargesDue());
assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
assertEquals(penaltyOutstanding, period.getPenaltyChargesOutstanding());
assertEquals(interestDue, period.getInterestDue());
assertEquals(interestPaid, period.getInterestPaid());
assertEquals(interestOutstanding, period.getInterestOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
assertEquals(balanceOfLoan, Utils.getDoubleValue(period.getPrincipalLoanBalanceOutstanding()));
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
assertEquals(feeDue, Utils.getDoubleValue(period.getFeeChargesDue()));
assertEquals(feePaid, Utils.getDoubleValue(period.getFeeChargesPaid()));
assertEquals(feeOutstanding, Utils.getDoubleValue(period.getFeeChargesOutstanding()));
assertEquals(penaltyDue, Utils.getDoubleValue(period.getPenaltyChargesDue()));
assertEquals(penaltyPaid, Utils.getDoubleValue(period.getPenaltyChargesPaid()));
assertEquals(penaltyOutstanding, Utils.getDoubleValue(period.getPenaltyChargesOutstanding()));
assertEquals(interestDue, Utils.getDoubleValue(period.getInterestDue()));
assertEquals(interestPaid, Utils.getDoubleValue(period.getInterestPaid()));
assertEquals(interestOutstanding, Utils.getDoubleValue(period.getInterestOutstanding()));
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
}
private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
@@ -6181,18 +6181,18 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount,
double principalPortion, double overPaidPortion, double loanBalance) {
assertEquals(transactionAmount, loanDetails.getTransactions().get(index).getAmount());
assertEquals(principalPortion, loanDetails.getTransactions().get(index).getPrincipalPortion());
assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion());
assertEquals(loanBalance, loanDetails.getTransactions().get(index).getOutstandingLoanBalance());
assertEquals(transactionAmount, Utils.getDoubleValue(loanDetails.getTransactions().get(index).getAmount()));
assertEquals(principalPortion, Utils.getDoubleValue(loanDetails.getTransactions().get(index).getPrincipalPortion()));
assertEquals(overPaidPortion, Utils.getDoubleValue(loanDetails.getTransactions().get(index).getOverpaymentPortion()));
assertEquals(loanBalance, Utils.getDoubleValue(loanDetails.getTransactions().get(index).getOutstandingLoanBalance()));
}
private void validateLoanCharge(GetLoansLoanIdResponse loanDetails, int index, LocalDate dueDate, double charged, double paid,
double outstanding) {
GetLoansLoanIdLoanChargeData chargeData = loanDetails.getCharges().get(index);
assertEquals(dueDate, chargeData.getDueDate());
assertEquals(charged, chargeData.getAmount());
assertEquals(paid, chargeData.getAmountPaid());
assertEquals(outstanding, chargeData.getAmountOutstanding());
assertEquals(charged, Utils.getDoubleValue(chargeData.getAmount()));
assertEquals(paid, Utils.getDoubleValue(chargeData.getAmountPaid()));
assertEquals(outstanding, Utils.getDoubleValue(chargeData.getAmountOutstanding()));
}
}
@@ -177,22 +177,22 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
}
protected static void validateFullyUnpaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
@@ -233,20 +233,20 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(feeDue, period.getFeeChargesDue());
assertEquals(feePaid, period.getFeeChargesPaid());
assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
assertEquals(penaltyDue, period.getPenaltyChargesDue());
assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
assertEquals(penaltyOutstanding, period.getPenaltyChargesOutstanding());
assertEquals(interestDue, period.getInterestDue());
assertEquals(interestPaid, period.getInterestPaid());
assertEquals(interestOutstanding, period.getInterestOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
assertEquals(feeDue, Utils.getDoubleValue(period.getFeeChargesDue()));
assertEquals(feePaid, Utils.getDoubleValue(period.getFeeChargesPaid()));
assertEquals(feeOutstanding, Utils.getDoubleValue(period.getFeeChargesOutstanding()));
assertEquals(penaltyDue, Utils.getDoubleValue(period.getPenaltyChargesDue()));
assertEquals(penaltyPaid, Utils.getDoubleValue(period.getPenaltyChargesPaid()));
assertEquals(penaltyOutstanding, Utils.getDoubleValue(period.getPenaltyChargesOutstanding()));
assertEquals(interestDue, Utils.getDoubleValue(period.getInterestDue()));
assertEquals(interestPaid, Utils.getDoubleValue(period.getInterestPaid()));
assertEquals(interestOutstanding, Utils.getDoubleValue(period.getInterestOutstanding()));
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
}
/**
@@ -410,6 +410,15 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
.loanScheduleProcessingType("HORIZONTAL");//
}
protected PostLoanProductsRequest create4IProgressiveWithCapitalizedIncome() {
return create4IProgressive().enableIncomeCapitalization(true)//
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())//
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())//
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE);
}
// Loan product with proper accounting setup
protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProduct(double interestRatePerPeriod) {
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
@@ -615,7 +624,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
Arrays.stream(transactions).forEach(tr -> {
Optional<GetLoansLoanIdTransactions> optTx = loanDetails.getTransactions().stream()
.filter(item -> Objects.equals(item.getAmount(), tr.amount) //
.filter(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)))
.findFirst();
@@ -638,16 +647,17 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
} else {
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size(), "Number of transactions on loan " + loanId);
Arrays.stream(transactions).forEach(tr -> {
boolean found = loanDetails.getTransactions().stream().anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) //
&& Objects.equals(item.getOutstandingLoanBalance(), tr.outstandingPrincipal) //
&& Objects.equals(item.getPrincipalPortion(), tr.principalPortion) //
&& Objects.equals(item.getInterestPortion(), tr.interestPortion) //
&& Objects.equals(item.getFeeChargesPortion(), tr.feePortion) //
&& Objects.equals(item.getPenaltyChargesPortion(), tr.penaltyPortion) //
&& Objects.equals(item.getOverpaymentPortion(), tr.overpaymentPortion) //
&& Objects.equals(item.getUnrecognizedIncomePortion(), tr.unrecognizedPortion) //
boolean found = loanDetails.getTransactions().stream()
.anyMatch(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) //
&& Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()), tr.outstandingPrincipal) //
&& Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()), tr.principalPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getInterestPortion()), tr.interestPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()), tr.feePortion) //
&& Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()), tr.penaltyPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()), tr.overpaymentPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()), tr.unrecognizedPortion) //
);
Assertions.assertTrue(found, "Required transaction not found: " + tr + " on loan " + loanId);
});
@@ -823,8 +833,8 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
private void verifyPeriodsEquality(List<GetLoansLoanIdRepaymentPeriod> savedPeriods, List<GetLoansLoanIdRepaymentPeriod> actualPeriods,
int startIndex, int endIndex, boolean shouldEqual) {
for (int i = startIndex; i < endIndex; i++) {
Double savedTotalDue = savedPeriods.get(i).getTotalDueForPeriod();
Double actualTotalDue = actualPeriods.get(i).getTotalDueForPeriod();
Double savedTotalDue = Utils.getDoubleValue(savedPeriods.get(i).getTotalDueForPeriod());
Double actualTotalDue = Utils.getDoubleValue(actualPeriods.get(i).getTotalDueForPeriod());
if (shouldEqual) {
assertEquals(savedTotalDue, actualTotalDue, String.format(
@@ -848,19 +858,19 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
int installmentNumber = 0;
for (int i = 0; i < installments.length; i++) {
GetLoansLoanIdRepaymentPeriod period = loanResponse.getRepaymentSchedule().getPeriods().get(i);
Double principalDue = period.getPrincipalDue();
Double principalDue = Utils.getDoubleValue(period.getPrincipalDue());
Double amount = installments[i].principalAmount;
if (installments[i].completed == null) { // this is for the disbursement
Assertions.assertEquals(amount, period.getPrincipalLoanBalanceOutstanding(),
Assertions.assertEquals(amount, Utils.getDoubleValue(period.getPrincipalLoanBalanceOutstanding()),
"%d. installment's principal due is different, expected: %.2f, actual: %.2f".formatted(i, amount,
period.getPrincipalLoanBalanceOutstanding()));
Utils.getDoubleValue(period.getPrincipalLoanBalanceOutstanding())));
} else {
Assertions.assertEquals(amount, principalDue,
"%d. installment's principal due is different, expected: %.2f, actual: %.2f".formatted(i, amount, principalDue));
Double interestAmount = installments[i].interestAmount;
Double interestDue = period.getInterestDue();
Double interestDue = Utils.getDoubleValue(period.getInterestDue());
if (interestAmount != null) {
Assertions.assertEquals(interestAmount, interestDue,
"%d. installment's interest due is different, expected: %.2f, actual: %.2f".formatted(i, interestAmount,
@@ -868,14 +878,14 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
}
Double feeAmount = installments[i].feeAmount;
Double feeDue = period.getFeeChargesDue();
Double feeDue = Utils.getDoubleValue(period.getFeeChargesDue());
if (feeAmount != null) {
Assertions.assertEquals(feeAmount, feeDue,
"%d. installment's fee charges due is different, expected: %.2f, actual: %.2f".formatted(i, feeAmount, feeDue));
}
Double penaltyAmount = installments[i].penaltyAmount;
Double penaltyDue = period.getPenaltyChargesDue();
Double penaltyDue = Utils.getDoubleValue(period.getPenaltyChargesDue());
if (penaltyAmount != null) {
Assertions.assertEquals(penaltyAmount, penaltyDue,
"%d. installment's penalty charges due is different, expected: %.2f, actual: %.2f".formatted(i, penaltyAmount,
@@ -883,7 +893,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
}
Double outstandingAmount = installments[i].totalOutstandingAmount;
Double totalOutstanding = period.getTotalOutstandingForPeriod();
Double totalOutstanding = Utils.getDoubleValue(period.getTotalOutstandingForPeriod());
if (outstandingAmount != null) {
Assertions.assertEquals(outstandingAmount, totalOutstanding,
"%d. installment's total outstanding is different, expected: %.2f, actual: %.2f".formatted(i, outstandingAmount,
@@ -893,7 +903,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Double outstandingPrincipalExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.principalOutstanding
: null;
Double outstandingPrincipal = period.getPrincipalOutstanding();
Double outstandingPrincipal = Utils.getDoubleValue(period.getPrincipalOutstanding());
if (outstandingPrincipalExpected != null) {
Assertions.assertEquals(outstandingPrincipalExpected, outstandingPrincipal,
"%d. installment's outstanding principal is different, expected: %.2f, actual: %.2f".formatted(i,
@@ -903,7 +913,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Double outstandingFeeExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.feeOutstanding
: null;
Double outstandingFee = period.getFeeChargesOutstanding();
Double outstandingFee = Utils.getDoubleValue(period.getFeeChargesOutstanding());
if (outstandingFeeExpected != null) {
Assertions.assertEquals(outstandingFeeExpected, outstandingFee,
"%d. installment's outstanding fee is different, expected: %.2f, actual: %.2f".formatted(i,
@@ -913,7 +923,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Double outstandingPenaltyExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.penaltyOutstanding
: null;
Double outstandingPenalty = period.getPenaltyChargesOutstanding();
Double outstandingPenalty = Utils.getDoubleValue(period.getPenaltyChargesOutstanding());
if (outstandingPenaltyExpected != null) {
Assertions.assertEquals(outstandingPenaltyExpected, outstandingPenalty,
"%d. installment's outstanding penalty is different, expected: %.2f, actual: %.2f".formatted(i,
@@ -923,7 +933,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Double outstandingTotalExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.totalOutstanding
: null;
Double outstandingTotal = period.getTotalOutstandingForPeriod();
Double outstandingTotal = Utils.getDoubleValue(period.getTotalOutstandingForPeriod());
if (outstandingTotalExpected != null) {
Assertions.assertEquals(outstandingTotalExpected, outstandingTotal,
"%d. installment's total outstanding is different, expected: %.2f, actual: %.2f".formatted(i,
@@ -931,7 +941,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
}
Double loanBalanceExpected = installments[i].loanBalance;
Double loanBalance = period.getPrincipalLoanBalanceOutstanding();
Double loanBalance = Utils.getDoubleValue(period.getPrincipalLoanBalanceOutstanding());
if (loanBalanceExpected != null) {
Assertions.assertEquals(loanBalanceExpected, loanBalance,
"%d. installment's loan balance is different, expected: %.2f, actual: %.2f".formatted(i, loanBalanceExpected,
@@ -1226,11 +1236,11 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
protected void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment,
Double principalOutstanding, Double principalPaid, Double totalOverpaid) {
assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());
assertEquals(totalRepayment, loanDetails.getSummary().getTotalRepayment());
assertEquals(principalOutstanding, loanDetails.getSummary().getPrincipalOutstanding());
assertEquals(principalPaid, loanDetails.getSummary().getPrincipalPaid());
assertEquals(totalOverpaid, loanDetails.getTotalOverpaid());
assertEquals(totalOutstanding, Utils.getDoubleValue(loanDetails.getSummary().getTotalOutstanding()));
assertEquals(totalRepayment, Utils.getDoubleValue(loanDetails.getSummary().getTotalRepayment()));
assertEquals(principalOutstanding, Utils.getDoubleValue(loanDetails.getSummary().getPrincipalOutstanding()));
assertEquals(principalPaid, Utils.getDoubleValue(loanDetails.getSummary().getPrincipalPaid()));
assertEquals(totalOverpaid, Utils.getDoubleValue(loanDetails.getTotalOverpaid()));
}
protected void checkMaturityDates(long loanId, LocalDate expectedMaturityDate, LocalDate actualMaturityDate) {
@@ -38,6 +38,7 @@ import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -619,7 +620,8 @@ public class InstallmentLevelDelinquencyAPIIntegrationTests extends BaseLoanInte
.getInstallmentLevelDelinquency();
assertThat(loan.getDelinquent().getDelinquentDays()).isEqualTo(loanLevelDelinquentDays);
assertThat(loan.getDelinquent().getDelinquentAmount()).isEqualByComparingTo(Double.valueOf(loanLevelDelinquentAmount));
assertThat(Utils.getDoubleValue(loan.getDelinquent().getDelinquentAmount()))
.isEqualByComparingTo(Double.valueOf(loanLevelDelinquentAmount));
if (expectedInstallmentLevelDelinquencyData != null && expectedInstallmentLevelDelinquencyData.length > 0) {
assertThat(installmentLevelDelinquency).isNotNull();
@@ -33,6 +33,7 @@ import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -468,8 +469,9 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
// No unexpected big accruals or any accrual adjustments
Assertions.assertTrue(loanDetails.getTransactions().stream().noneMatch(t -> (t.getType().getAccrual() && t.getAmount() > 0.31)
|| "loanTransactionType.accrualAdjustment".equals(t.getType().getCode())));
Assertions.assertTrue(
loanDetails.getTransactions().stream().noneMatch(t -> (t.getType().getAccrual() && t.getAmount().doubleValue() > 0.31)
|| "loanTransactionType.accrualAdjustment".equals(t.getType().getCode())));
// Accruals around installment due dates are as expected
validateTransactionsExist(loanDetails, //
@@ -691,16 +693,17 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest {
private void validateTransactionsExist(GetLoansLoanIdResponse loanDetails, TransactionExt... transactions) {
Arrays.stream(transactions).forEach(tr -> {
boolean found = loanDetails.getTransactions().stream().anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) //
&& Objects.equals(item.getOutstandingLoanBalance(), tr.outstandingPrincipal) //
&& Objects.equals(item.getPrincipalPortion(), tr.principalPortion) //
&& Objects.equals(item.getInterestPortion(), tr.interestPortion) //
&& Objects.equals(item.getFeeChargesPortion(), tr.feePortion) //
&& Objects.equals(item.getPenaltyChargesPortion(), tr.penaltyPortion) //
&& Objects.equals(item.getOverpaymentPortion(), tr.overpaymentPortion) //
&& Objects.equals(item.getUnrecognizedIncomePortion(), tr.unrecognizedPortion) //
boolean found = loanDetails.getTransactions().stream()
.anyMatch(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) //
&& Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()), tr.outstandingPrincipal) //
&& Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()), tr.principalPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getInterestPortion()), tr.interestPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()), tr.feePortion) //
&& Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()), tr.penaltyPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()), tr.overpaymentPortion) //
&& Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()), tr.unrecognizedPortion) //
);
Assertions.assertTrue(found, "Required transaction not found: " + tr + " on loan " + loanDetails.getId());
});
@@ -78,7 +78,7 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(50.0, "Capitalized Income", "01 January 2024"), //
transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), //
transaction(0.02, "Accrual", "02 January 2024"), //
transaction(0.03, "Accrual", "02 January 2024"), //
transaction(0.55, "Capitalized Income Amortization", "02 January 2024") //
);
@@ -89,8 +89,8 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
journalEntry(0.02, interestReceivableAccount, "DEBIT"), //
journalEntry(0.02, interestIncomeAccount, "CREDIT"), //
journalEntry(0.03, interestReceivableAccount, "DEBIT"), //
journalEntry(0.03, interestIncomeAccount, "CREDIT"), //
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(0.55, feeIncomeAccount, "CREDIT") //
);
@@ -27,6 +27,7 @@ import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -78,8 +79,8 @@ public class LoanChargeProgressiveTest extends BaseLoanIntegrationTest {
final PostChargesResponse chargeResponse = createCharge(20.0d, "EUR");
addLoanCharge(loanId, chargeResponse.getResourceId(), "03 October 2024", 20.0d);
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertTrue(
loanDetails.getTransactions().stream().anyMatch(t -> t.getType().getAccrual() && t.getAmount().equals(20.0d)));
Assertions.assertTrue(loanDetails.getTransactions().stream()
.anyMatch(t -> t.getType().getAccrual() && Utils.getDoubleValue(t.getAmount()).equals(20.0d)));
});
runAt("04 October 2024", () -> {
globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY,
@@ -87,7 +88,7 @@ public class LoanChargeProgressiveTest extends BaseLoanIntegrationTest {
executeInlineCOB(loanId);
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertTrue(loanDetails.getTransactions().stream()
.anyMatch(t -> t.getType().getAccrual() && t.getFeeChargesPortion().equals(20.0d)));
.anyMatch(t -> t.getType().getAccrual() && Utils.getDoubleValue(t.getFeeChargesPortion()).equals(20.0d)));
});
}
@@ -28,6 +28,7 @@ import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -231,12 +232,14 @@ public class LoanDelinquencyForNonActiveAccountsTest extends BaseLoanIntegration
.getInstallmentLevelDelinquency();
assertThat(loan.getDelinquent().getDelinquentDays()).isEqualTo(loanLevelDelinquentDays);
assertThat(loan.getDelinquent().getDelinquentAmount()).isEqualByComparingTo(Double.valueOf(loanLevelDelinquentAmount));
assertThat(Utils.getDoubleValue(loan.getDelinquent().getDelinquentAmount()))
.isEqualByComparingTo(Double.valueOf(loanLevelDelinquentAmount));
if (expectedLastRepaymentDate != null && expectedLastRepaymentAmount != null) {
assertThat(loan.getDelinquent().getLastRepaymentDate()).isNotNull();
Assertions.assertEquals(expectedLastRepaymentDate, loan.getDelinquent().getLastRepaymentDate().format(dateTimeFormatter));
assertThat(loan.getDelinquent().getLastRepaymentAmount()).isNotNull();
assertThat(loan.getDelinquent().getLastRepaymentAmount()).isEqualByComparingTo(Double.valueOf(expectedLastRepaymentAmount));
assertThat(Utils.getDoubleValue(loan.getDelinquent().getLastRepaymentAmount()))
.isEqualByComparingTo(Double.valueOf(expectedLastRepaymentAmount));
}
if (expectedInstallmentLevelDelinquencyData != null && expectedInstallmentLevelDelinquencyData.length > 0) {
@@ -314,7 +314,7 @@ public class LoanDisbursementDetailsIntegrationTest {
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
this.loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
final Double limit = 2.0;
final BigDecimal limit = BigDecimal.TWO;
evaluateEqualInstallmentsForRepaymentSchedule(getLoansLoanIdResponse.getRepaymentSchedule(), limit);
log.info("-----------MULTI DISBURSAL LOAN EQUAL INSTALLMENTS SUCCESSFULLY-------");
}
@@ -665,9 +665,9 @@ public class LoanDisbursementDetailsIntegrationTest {
return collateral;
}
public void evaluateEqualInstallmentsForRepaymentSchedule(GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule, Double limit) {
Double totalOutstandingForPeriod = 0.0;
Double totalInstallmentAmountForPeriod = 0.0;
public void evaluateEqualInstallmentsForRepaymentSchedule(GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule, BigDecimal limit) {
BigDecimal totalOutstandingForPeriod = BigDecimal.ZERO;
BigDecimal totalInstallmentAmountForPeriod = BigDecimal.ZERO;
if (getLoanRepaymentSchedule != null) {
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) {
@@ -678,8 +678,9 @@ public class LoanDisbursementDetailsIntegrationTest {
totalOutstandingForPeriod = period.getTotalOutstandingForPeriod();
totalInstallmentAmountForPeriod = period.getTotalInstallmentAmountForPeriod();
} else {
assertTrue(Math.abs(period.getTotalOutstandingForPeriod() - totalOutstandingForPeriod) <= limit);
assertTrue(Math.abs(period.getTotalInstallmentAmountForPeriod() - totalInstallmentAmountForPeriod) <= limit);
assertTrue(period.getTotalOutstandingForPeriod().subtract(totalOutstandingForPeriod).abs().compareTo(limit) <= 0);
assertTrue(period.getTotalInstallmentAmountForPeriod().subtract(totalInstallmentAmountForPeriod).abs()
.compareTo(limit) <= 0);
}
}
}
@@ -149,7 +149,7 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertTrue(loanDetails.getTransactions().stream()
.filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.interestRefund"))
.allMatch(transaction -> transaction.getAmount().compareTo(0.0) > 0));
.allMatch(transaction -> transaction.getAmount().doubleValue() > 0.0));
});
}
@@ -110,9 +110,9 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
}
@@ -157,11 +157,11 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalPaidForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalPaidForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate())));
assertEquals(expectedRepaymentAmount, summary.getTotalOutstanding());
assertEquals(expectedRepaymentAmount, Utils.getDoubleValue(summary.getTotalOutstanding()));
assertEquals(expectedDownPaymentAmount, summary.getTotalRepaymentTransaction());
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
}
@@ -205,9 +205,9 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
}
@@ -253,12 +253,12 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedFirstDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedFirstDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedFirstDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream() //
.anyMatch(period -> expectedSecondDownPaymentAmount.equals(period.getTotalDueForPeriod())
.anyMatch(period -> expectedSecondDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedSecondDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
}
@@ -306,15 +306,15 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
Double expectedTotalRepaymentAmount = expectedFirstDownPaymentAmount + expectedSecondDownPaymentAmount;
assertTrue(periods.stream() //
.anyMatch(period -> expectedFirstDownPaymentAmount.equals(period.getTotalPaidForPeriod()) //
.anyMatch(period -> expectedFirstDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalPaidForPeriod())) //
&& expectedFirstDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream() //
.anyMatch(period -> expectedSecondDownPaymentAmount.equals(period.getTotalPaidForPeriod())
.anyMatch(period -> expectedSecondDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalPaidForPeriod()))
&& expectedSecondDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
assertEquals(expectedRepaymentAmount, summary.getTotalOutstanding());
assertEquals(expectedTotalRepaymentAmount, summary.getTotalRepaymentTransaction());
assertEquals(expectedRepaymentAmount, Utils.getDoubleValue(summary.getTotalOutstanding()));
assertEquals(expectedTotalRepaymentAmount, Utils.getDoubleValue(summary.getTotalRepaymentTransaction()));
}
@Test
@@ -529,14 +529,14 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate()) //
&& Double.valueOf(0.00).equals(period.getFeeChargesDue())));
&& Double.valueOf(0.00).equals(Utils.getDoubleValue(period.getFeeChargesDue()))));
assertTrue(periods.stream() //
.anyMatch(period -> expectedTotalDueForRepaymentInstallment.equals(period.getTotalDueForPeriod()) //
&& expectedRepaymentAmount.equals(period.getPrincipalDue()) //
.anyMatch(period -> expectedTotalDueForRepaymentInstallment.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getPrincipalDue())) //
&& expectedRepaymentDueDate.equals(period.getDueDate()) //
&& feeAmount.equals(period.getFeeChargesDue())));
&& feeAmount.equals(Utils.getDoubleValue(period.getFeeChargesDue()))));
}
@Test
@@ -694,16 +694,16 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate()) //
&& Double.valueOf(0.00).equals(period.getFeeChargesDue()) //
&& Double.valueOf(0.00).equals(period.getInterestDue())));
&& Double.valueOf(0.00).equals(Utils.getDoubleValue(period.getFeeChargesDue())) //
&& Double.valueOf(0.00).equals(Utils.getDoubleValue(period.getInterestDue()))));
assertTrue(periods.stream() //
.anyMatch(period -> expectedTotalDueForRepaymentInstallment.equals(period.getTotalDueForPeriod()) //
&& expectedRepaymentAmount.equals(period.getPrincipalDue()) //
.anyMatch(period -> expectedTotalDueForRepaymentInstallment.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getPrincipalDue())) //
&& expectedRepaymentDueDate.equals(period.getDueDate()) //
&& feeAmount.equals(period.getFeeChargesDue()) //
&& Double.valueOf(10.0).equals(period.getInterestDue())));
&& feeAmount.equals(Utils.getDoubleValue(period.getFeeChargesDue())) //
&& Double.valueOf(10.0).equals(Utils.getDoubleValue(period.getInterestDue()))));
}
@Test
@@ -864,10 +864,11 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3);
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod())
&& expectedRepaymentDueDate.equals(period.getDueDate())));
assertTrue(
periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalDueForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
assertNotNull(loanDetails.getDelinquencyRange());
assertEquals(2, loanDetails.getDelinquent().getDelinquentDays());
} finally {
@@ -1662,12 +1663,13 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio
Double expectedTotalRepaymentTransactionAmount = 0.00;
assertTrue(periods.stream() //
.anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalOutstandingForPeriod()) //
.anyMatch(period -> expectedDownPaymentAmount.equals(Utils.getDoubleValue(period.getTotalOutstandingForPeriod())) //
&& expectedDownPaymentDueDate.equals(period.getDueDate())));
assertEquals(expectedTotalOutstandingAmount, summary.getTotalOutstanding());
assertEquals(expectedTotalOutstandingAmount, Utils.getDoubleValue(summary.getTotalOutstanding()));
assertEquals(expectedTotalRepaymentTransactionAmount, summary.getTotalRepaymentTransaction());
assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalOutstandingForPeriod())
&& expectedRepaymentDueDate.equals(period.getDueDate())));
assertTrue(periods.stream()
.anyMatch(period -> expectedRepaymentAmount.equals(Utils.getDoubleValue(period.getTotalOutstandingForPeriod()))
&& expectedRepaymentDueDate.equals(period.getDueDate())));
}
private void checkNoDownPaymentTransaction(final Integer loanID) {
@@ -19,7 +19,9 @@
package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansResponse;
@@ -248,4 +250,38 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest {
});
}
@Test
public void testCapitalizedIncomeExistsInRepaymentScheduleAndModifiesPrincipal() {
runAt("01 March 2023", () -> {
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressiveWithCapitalizedIncome());
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId,
loanProductsResponse.getResourceId(), "01 March 2023", 10000.00, 12.00, 4, null));
loanId = postLoansResponse.getLoanId();
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.00, "01 March 2023"));
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 March 2023");
});
runAt("02 March 2023", () -> {
loanTransactionHelper.addCapitalizedIncome(loanId, "02 March 2023", 100.00);
});
BigDecimal thousand = BigDecimal.valueOf(1000.0);
BigDecimal hundred = BigDecimal.valueOf(100.0);
BigDecimal thousandOneHundred = BigDecimal.valueOf(1100.0);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertEquals(thousand, loanDetails.getPrincipal().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(thousand, loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(hundred,
loanDetails.getSummary().getTotalPrincipalFromCapitalizedIncome().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getPrincipalOutstanding().setScale(1, RoundingMode.HALF_UP));
List<GetLoansLoanIdRepaymentPeriod> periods = loanDetails.getRepaymentSchedule().getPeriods();
Assertions.assertEquals(6, periods.size());
Assertions.assertEquals(thousand, periods.get(0).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(hundred, periods.get(1).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP));
}
}
@@ -46,8 +46,8 @@ public class ProgressiveLoanTrancheTest extends BaseLoanIntegrationTest {
runAt("20 December 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024",
500.0, 7.0, 6, (request) -> request.disbursementData(
List.of(new PostLoansDisbursementData().expectedDisbursementDate("20 December 2024").principal(100.0))));
500.0, 7.0, 6, (request) -> request.disbursementData(List.of(new PostLoansDisbursementData()
.expectedDisbursementDate("20 December 2024").principal(BigDecimal.valueOf(100.0)))));
loanIdRef.set(loanId);
@@ -39,6 +39,8 @@ import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.ParseException;
@@ -569,4 +571,8 @@ public final class Utils {
public static long getDifferenceInMonths(final LocalDate localDateBefore, final LocalDate localDateAfter) {
return MONTHS.between(localDateBefore, localDateAfter);
}
public static Double getDoubleValue(BigDecimal amount) {
return amount == null ? null : amount.setScale(2, RoundingMode.HALF_UP).doubleValue();
}
}
@@ -2688,7 +2688,7 @@ public class LoanTransactionHelper {
log.info(" Id {} code {} date {} amount {}", transaction.getId(), transaction.getType().getCode(), transaction.getDate(),
transaction.getAmount());
if (transactionType.equals(transaction.getType().getCode())) {
transactionsAmount += transaction.getAmount();
transactionsAmount += Utils.getDoubleValue(transaction.getAmount());
}
}
assertEquals(amountExpected, transactionsAmount);
@@ -2707,7 +2707,7 @@ public class LoanTransactionHelper {
}
}
assertEquals(transactionExpected, Utils.dateFormatter.format(lastTransaction.getDate()));
assertEquals(amountExpected, lastTransaction.getAmount());
assertEquals(amountExpected, Utils.getDoubleValue(lastTransaction.getAmount()));
return lastTransaction.getId();
}
@@ -2722,7 +2722,7 @@ public class LoanTransactionHelper {
if (getLoansLoanIdSummary != null) {
log.info("Loan with Principal Outstanding Balance {} expected {}", getLoansLoanIdSummary.getPrincipalOutstanding(),
amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getPrincipalOutstanding());
assertEquals(amountExpected, Utils.getDoubleValue(getLoansLoanIdSummary.getPrincipalOutstanding()));
}
}
@@ -2730,7 +2730,7 @@ public class LoanTransactionHelper {
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
if (getLoansLoanIdSummary != null) {
log.info("Loan with Fees Outstanding Balance {} expected {}", getLoansLoanIdSummary.getFeeChargesOutstanding(), amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getFeeChargesOutstanding());
assertEquals(amountExpected, Utils.getDoubleValue(getLoansLoanIdSummary.getFeeChargesOutstanding()));
}
}
@@ -2738,14 +2738,14 @@ public class LoanTransactionHelper {
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
assertNotNull(getLoansLoanIdSummary);
log.info("Loan with Fees Outstanding Balance {} expected {}", getLoansLoanIdSummary.getFeeChargesOutstanding(), amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getPenaltyChargesOutstanding());
assertEquals(amountExpected, Utils.getDoubleValue(getLoansLoanIdSummary.getPenaltyChargesOutstanding()));
}
public void validateLoanTotalOustandingBalance(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) {
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
if (getLoansLoanIdSummary != null) {
log.info("Loan with Total Outstanding Balance {} expected {}", getLoansLoanIdSummary.getTotalOutstanding(), amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getTotalOutstanding());
assertEquals(amountExpected, Utils.getDoubleValue(getLoansLoanIdSummary.getTotalOutstanding()));
}
}