FINERACT-2354: Introduce reaging preview API
This commit is contained in:
committed by
Adam Saghy
parent
ed8e9d1376
commit
3efafa778e
+197
-5
@@ -20,19 +20,27 @@ package org.apache.fineract.test.stepdef.loan;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import io.cucumber.datatable.DataTable;
|
import io.cucumber.datatable.DataTable;
|
||||||
import io.cucumber.java.en.Then;
|
import io.cucumber.java.en.Then;
|
||||||
import io.cucumber.java.en.When;
|
import io.cucumber.java.en.When;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
import org.apache.fineract.client.models.LoanScheduleData;
|
||||||
|
import org.apache.fineract.client.models.LoanSchedulePeriodData;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
@@ -41,22 +49,23 @@ import org.apache.fineract.test.factory.LoanRequestFactory;
|
|||||||
import org.apache.fineract.test.helper.ErrorHelper;
|
import org.apache.fineract.test.helper.ErrorHelper;
|
||||||
import org.apache.fineract.test.helper.ErrorMessageHelper;
|
import org.apache.fineract.test.helper.ErrorMessageHelper;
|
||||||
import org.apache.fineract.test.helper.ErrorResponse;
|
import org.apache.fineract.test.helper.ErrorResponse;
|
||||||
|
import org.apache.fineract.test.helper.Utils;
|
||||||
import org.apache.fineract.test.messaging.EventAssertion;
|
import org.apache.fineract.test.messaging.EventAssertion;
|
||||||
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
|
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
|
||||||
import org.apache.fineract.test.stepdef.AbstractStepDef;
|
import org.apache.fineract.test.stepdef.AbstractStepDef;
|
||||||
import org.apache.fineract.test.support.TestContextKey;
|
import org.apache.fineract.test.support.TestContextKey;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class LoanReAgingStepDef extends AbstractStepDef {
|
public class LoanReAgingStepDef extends AbstractStepDef {
|
||||||
|
|
||||||
@Autowired
|
private static final String DATE_FORMAT = "dd MMMM yyyy";
|
||||||
private LoanTransactionsApi loanTransactionsApi;
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
||||||
|
|
||||||
@Autowired
|
private final LoanTransactionsApi loanTransactionsApi;
|
||||||
private EventAssertion eventAssertion;
|
private final EventAssertion eventAssertion;
|
||||||
|
|
||||||
@When("Admin creates a Loan re-aging transaction with the following data:")
|
@When("Admin creates a Loan re-aging transaction with the following data:")
|
||||||
public void createReAgingTransaction(DataTable table) throws IOException {
|
public void createReAgingTransaction(DataTable table) throws IOException {
|
||||||
@@ -302,4 +311,187 @@ public class LoanReAgingStepDef extends AbstractStepDef {
|
|||||||
assertThat(errorDetails.getHttpStatusCode()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(403);
|
assertThat(errorDetails.getHttpStatusCode()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(403);
|
||||||
assertThat(developerMessage).matches(ErrorMessageHelper.reAgeContractTerminatedLoanFailure());
|
assertThat(developerMessage).matches(ErrorMessageHelper.reAgeContractTerminatedLoanFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@When("Admin creates a Loan re-aging preview with the following data:")
|
||||||
|
public void createReAgingPreview(DataTable table) throws IOException {
|
||||||
|
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||||
|
long loanId = loanResponse.body().getLoanId();
|
||||||
|
|
||||||
|
List<String> data = table.asLists().get(1);
|
||||||
|
int frequencyNumber = Integer.parseInt(data.get(0));
|
||||||
|
String frequencyType = data.get(1);
|
||||||
|
String startDate = data.get(2);
|
||||||
|
int numberOfInstallments = Integer.parseInt(data.get(3));
|
||||||
|
|
||||||
|
Response<LoanScheduleData> response = loanTransactionsApi
|
||||||
|
.previewReAgeSchedule(loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments, DATE_FORMAT, "en").execute();
|
||||||
|
ErrorHelper.checkSuccessfulApiCall(response);
|
||||||
|
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Re-aging preview created for loan ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
|
||||||
|
loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@When("Admin creates a Loan re-aging preview by Loan external ID with the following data:")
|
||||||
|
public void createReAgingPreviewByLoanExternalId(DataTable table) throws IOException {
|
||||||
|
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||||
|
String loanExternalId = loanResponse.body().getResourceExternalId();
|
||||||
|
|
||||||
|
List<String> data = table.asLists().get(1);
|
||||||
|
int frequencyNumber = Integer.parseInt(data.get(0));
|
||||||
|
String frequencyType = data.get(1);
|
||||||
|
String startDate = data.get(2);
|
||||||
|
int numberOfInstallments = Integer.parseInt(data.get(3));
|
||||||
|
|
||||||
|
Response<LoanScheduleData> response = loanTransactionsApi
|
||||||
|
.previewReAgeSchedule1(loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments, DATE_FORMAT, "en")
|
||||||
|
.execute();
|
||||||
|
ErrorHelper.checkSuccessfulApiCall(response);
|
||||||
|
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Re-aging preview created for loan external ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
|
||||||
|
loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Then("Loan Repayment schedule preview has {int} periods, with the following data for periods:")
|
||||||
|
public void loanRepaymentSchedulePreviewPeriodsCheck(int linesExpected, DataTable table) {
|
||||||
|
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
|
||||||
|
|
||||||
|
List<LoanSchedulePeriodData> repaymentPeriods = scheduleResponse.body().getPeriods();
|
||||||
|
|
||||||
|
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||||
|
String resourceId = String.valueOf(loanResponse.body().getLoanId());
|
||||||
|
|
||||||
|
List<List<String>> data = table.asLists();
|
||||||
|
int nrLines = data.size();
|
||||||
|
int linesActual = (int) repaymentPeriods.stream().filter(r -> r.getPeriod() != null).count();
|
||||||
|
for (int i = 1; i < nrLines; i++) {
|
||||||
|
List<String> expectedValues = data.get(i);
|
||||||
|
String dueDateExpected = expectedValues.get(2);
|
||||||
|
|
||||||
|
List<List<String>> actualValuesList = repaymentPeriods.stream()
|
||||||
|
.filter(r -> dueDateExpected.equals(FORMATTER.format(r.getDueDate())))
|
||||||
|
.map(r -> fetchValuesOfRepaymentSchedule(data.get(0), r)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
|
||||||
|
assertThat(containsExpectedValues)
|
||||||
|
.as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(resourceId, i, actualValuesList, expectedValues)).isTrue();
|
||||||
|
|
||||||
|
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(resourceId, linesActual, linesExpected))
|
||||||
|
.isEqualTo(linesExpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Then("Loan Repayment schedule preview has the following data in Total row:")
|
||||||
|
public void loanRepaymentScheduleAmountCheck(DataTable table) {
|
||||||
|
List<List<String>> data = table.asLists();
|
||||||
|
List<String> header = data.get(0);
|
||||||
|
List<String> expectedValues = data.get(1);
|
||||||
|
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
|
||||||
|
validateRepaymentScheduleTotal(header, scheduleResponse.body(), expectedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> fetchValuesOfRepaymentSchedule(List<String> header, LoanSchedulePeriodData repaymentPeriod) {
|
||||||
|
List<String> actualValues = new ArrayList<>();
|
||||||
|
for (String headerName : header) {
|
||||||
|
switch (headerName) {
|
||||||
|
case "Nr" -> actualValues.add(repaymentPeriod.getPeriod() == null ? null : String.valueOf(repaymentPeriod.getPeriod()));
|
||||||
|
case "Days" ->
|
||||||
|
actualValues.add(repaymentPeriod.getDaysInPeriod() == null ? null : String.valueOf(repaymentPeriod.getDaysInPeriod()));
|
||||||
|
case "Date" ->
|
||||||
|
actualValues.add(repaymentPeriod.getDueDate() == null ? null : FORMATTER.format(repaymentPeriod.getDueDate()));
|
||||||
|
case "Paid date" -> actualValues.add(repaymentPeriod.getObligationsMetOnDate() == null ? null
|
||||||
|
: FORMATTER.format(repaymentPeriod.getObligationsMetOnDate()));
|
||||||
|
case "Balance of loan" -> actualValues.add(repaymentPeriod.getPrincipalLoanBalanceOutstanding() == null ? null
|
||||||
|
: 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().doubleValue()).format());
|
||||||
|
case "Penalties" -> actualValues.add(repaymentPeriod.getPenaltyChargesDue() == null ? null
|
||||||
|
: new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue().doubleValue()).format());
|
||||||
|
case "Due" -> actualValues.add(repaymentPeriod.getTotalDueForPeriod() == null ? null
|
||||||
|
: 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
|
||||||
|
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidInAdvanceForPeriod().doubleValue()).format());
|
||||||
|
case "Late" -> actualValues.add(repaymentPeriod.getTotalPaidLateForPeriod() == null ? null
|
||||||
|
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidLateForPeriod().doubleValue()).format());
|
||||||
|
case "Waived" -> actualValues.add(repaymentPeriod.getTotalWaivedForPeriod() == null ? null
|
||||||
|
: new Utils.DoubleFormatter(repaymentPeriod.getTotalWaivedForPeriod().doubleValue()).format());
|
||||||
|
case "Outstanding" -> actualValues.add(repaymentPeriod.getTotalOutstandingForPeriod() == null ? null
|
||||||
|
: new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod().doubleValue()).format());
|
||||||
|
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actualValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
|
||||||
|
private List<String> validateRepaymentScheduleTotal(List<String> header, LoanScheduleData repaymentSchedule,
|
||||||
|
List<String> expectedAmounts) {
|
||||||
|
List<String> actualValues = new ArrayList<>();
|
||||||
|
Double paidActual = 0.0;
|
||||||
|
List<LoanSchedulePeriodData> periods = repaymentSchedule.getPeriods();
|
||||||
|
for (LoanSchedulePeriodData period : periods) {
|
||||||
|
if (null != period.getTotalPaidForPeriod()) {
|
||||||
|
paidActual += period.getTotalPaidForPeriod().doubleValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BigDecimal paidActualBd = new BigDecimal(paidActual).setScale(2, RoundingMode.HALF_DOWN);
|
||||||
|
|
||||||
|
for (int i = 0; i < header.size(); i++) {
|
||||||
|
String headerName = header.get(i);
|
||||||
|
String expectedValue = expectedAmounts.get(i);
|
||||||
|
switch (headerName) {
|
||||||
|
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().doubleValue())//
|
||||||
|
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInterest(
|
||||||
|
repaymentSchedule.getTotalInterestCharged().doubleValue(), Double.valueOf(expectedValue)))//
|
||||||
|
.isEqualTo(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().doubleValue())//
|
||||||
|
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePenalties(
|
||||||
|
repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
|
||||||
|
.isEqualTo(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().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 "Waived" -> assertThat(repaymentSchedule.getTotalWaived().doubleValue())//
|
||||||
|
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleWaived(repaymentSchedule.getTotalWaived().doubleValue(),
|
||||||
|
Double.valueOf(expectedValue)))//
|
||||||
|
.isEqualTo(Double.valueOf(expectedValue));//
|
||||||
|
case "Outstanding" -> assertThat(repaymentSchedule.getTotalOutstanding().doubleValue())//
|
||||||
|
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleOutstanding(
|
||||||
|
repaymentSchedule.getTotalOutstanding().doubleValue(), Double.valueOf(expectedValue)))//
|
||||||
|
.isEqualTo(Double.valueOf(expectedValue));//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actualValues;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -48,6 +48,7 @@ public abstract class TestContextKey {
|
|||||||
public static final String LOAN_REFUND_RESPONSE = "loanRefundResponse";
|
public static final String LOAN_REFUND_RESPONSE = "loanRefundResponse";
|
||||||
public static final String LOAN_REAGING_RESPONSE = "loanReAgingResponse";
|
public static final String LOAN_REAGING_RESPONSE = "loanReAgingResponse";
|
||||||
public static final String LOAN_REAGING_UNDO_RESPONSE = "loanReAgingUndoResponse";
|
public static final String LOAN_REAGING_UNDO_RESPONSE = "loanReAgingUndoResponse";
|
||||||
|
public static final String LOAN_REAGING_PREVIEW_RESPONSE = "loanReAgingPreviewResponse";
|
||||||
public static final String LOAN_REAMORTIZATION_RESPONSE = "loanReAmortizationResponse";
|
public static final String LOAN_REAMORTIZATION_RESPONSE = "loanReAmortizationResponse";
|
||||||
public static final String LOAN_REAMORTIZATION_UNDO_RESPONSE = "loanReAmortizationUndoResponse";
|
public static final String LOAN_REAMORTIZATION_UNDO_RESPONSE = "loanReAmortizationUndoResponse";
|
||||||
public static final String BUSINESS_DATE_RESPONSE = "businessDateResponse";
|
public static final String BUSINESS_DATE_RESPONSE = "businessDateResponse";
|
||||||
|
|||||||
@@ -0,0 +1,454 @@
|
|||||||
|
@LoanReAgingPreviewFeature
|
||||||
|
Feature: LoanReAgingPreview
|
||||||
|
|
||||||
|
Scenario: Basic verification of the loan re-aging preview schedule
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||||
|
When Admin sets the business date to "15 April 2025"
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | MONTHS | 15 April 2025 | 3 |
|
||||||
|
Then Loan Repayment schedule preview has 7 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 15 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 15 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 15 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | 15 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 5 | 14 | 15 April 2025 | | 666.67 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 |
|
||||||
|
| 6 | 30 | 15 May 2025 | | 333.34 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 |
|
||||||
|
| 7 | 31 | 15 June 2025 | | 0.0 | 333.34 | 0.0 | 0.0 | 0.0 | 333.34 | 0.0 | 0.0 | 0.0 | 333.34 |
|
||||||
|
Then Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||||
|
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||||
|
|
||||||
|
Scenario: Verify Loan re-aging preview with chargeback
|
||||||
|
When Admin sets the business date to "01 January 2024"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2024 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 15 | 15 February 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
When Admin sets the business date to "01 January 2024"
|
||||||
|
And Customer makes "AUTOPAY" repayment on "01 January 2024" with 250 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 15 | 15 February 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
When Admin sets the business date to "02 February 2024"
|
||||||
|
When Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 125 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 15 | 15 February 2024 | | 0.0 | 375.0 | 0.0 | 0.0 | 0.0 | 375.0 | 0.0 | 0.0 | 0.0 | 375.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1125.0 | 0.0 | 0.0 | 0.0 | 1125.0 | 250.0 | 0.0 | 0.0 | 875.0 |
|
||||||
|
When Admin sets the business date to "21 February 2024"
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 2 | MONTHS | 10 March 2024 | 3 |
|
||||||
|
Then Loan Repayment schedule preview has 7 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2024 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2024 | 21 February 2024 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 15 | 31 January 2024 | 21 February 2024 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 15 | 15 February 2024 | 21 February 2024 | 875.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 5 | 24 | 10 March 2024 | | 583.33 | 291.67 | 0.0 | 0.0 | 0.0 | 291.67 | 0.0 | 0.0 | 0.0 | 291.67 |
|
||||||
|
| 6 | 61 | 10 May 2024 | | 291.66 | 291.67 | 0.0 | 0.0 | 0.0 | 291.67 | 0.0 | 0.0 | 0.0 | 291.67 |
|
||||||
|
| 7 | 61 | 10 July 2024 | | 0.0 | 291.66 | 0.0 | 0.0 | 0.0 | 291.66 | 0.0 | 0.0 | 0.0 | 291.66 |
|
||||||
|
And Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 15 | 15 February 2024 | | 0.0 | 375.0 | 0.0 | 0.0 | 0.0 | 375.0 | 0.0 | 0.0 | 0.0 | 375.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1125.0 | 0.0 | 0.0 | 0.0 | 1125.0 | 250.0 | 0.0 | 0.0 | 875.0 |
|
||||||
|
|
||||||
|
Scenario: Verify Loan re-aging preview with charge N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
When Admin sets the business date to "3 May 2025"
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "3 May 2025" due date and 10 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 0.0 | 0.0 | 0.0 | 1010.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 01 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 01 April 2025 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 0.0 | 0.0 | 0.0 | 1010.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 0.0 | 0.0 | 0.0 | 1010.0 |
|
||||||
|
|
||||||
|
Scenario: Verify Loan re-aging preview with backdated repayment, charge and N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
When Admin sets the business date to "3 May 2025"
|
||||||
|
And Customer makes "AUTOPAY" repayment on "01 March 2025" with 250 EUR transaction amount
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "3 May 2025" due date and 10 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 250.0 | 0.0 | 250.0 | 760.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 250.0 | 0.0 | 250.0 | 760.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 250.0 | 0.0 | 250.0 | 760.0 |
|
||||||
|
|
||||||
|
Scenario: Verify Loan re-aging preview with downpayment, payoff and charge - N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
When Admin sets the business date to "20 March 2025"
|
||||||
|
When Loan Pay-off is made on "20 March 2025"
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "20 March 2025" due date and 10 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 January 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2025 | 20 March 2025 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 3 | 15 | 31 January 2025 | 20 March 2025 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 4 | 15 | 15 February 2025 | 20 March 2025 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 5 | 33 | 20 March 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 1000.0 | 0.0 | 750.0 | 10.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 15 February 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 January 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2025 | 15 February 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 15 | 31 January 2025 | 15 February 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 15 | 15 February 2025 | 20 March 2025 | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 | 750.0 | 0.0 | 750.0 | 0.0 |
|
||||||
|
| 5 | 33 | 20 March 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 1000.0 | 0.0 | 750.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 January 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 2 | 15 | 16 January 2025 | 20 March 2025 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 3 | 15 | 31 January 2025 | 20 March 2025 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 4 | 15 | 15 February 2025 | 20 March 2025 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 5 | 33 | 20 March 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 10.0 | 1010.0 | 1000.0 | 0.0 | 750.0 | 10.0 |
|
||||||
|
|
||||||
|
Scenario: Verify that Loan re-aging preview with repayment, chargeback and charge - N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
When Admin sets the business date to "3 May 2025"
|
||||||
|
And Customer makes "AUTOPAY" repayment on "01 March 2025" with 250 EUR transaction amount
|
||||||
|
When Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 125 EUR transaction amount
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "3 May 2025" due date and 10 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 125.0 | 0.0 | 0.0 | 10.0 | 135.0 | 0.0 | 0.0 | 0.0 | 135.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1125.0 | 0.0 | 0.0 | 10.0 | 1135.0 | 250.0 | 0.0 | 250.0 | 885.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 125.0 | 0.0 | 0.0 | 10.0 | 135.0 | 0.0 | 0.0 | 0.0 | 135.0 |
|
||||||
|
And Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1125.0 | 0.0 | 0.0 | 10.0 | 1135.0 | 250.0 | 0.0 | 250.0 | 885.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 125.0 | 0.0 | 0.0 | 10.0 | 135.0 | 0.0 | 0.0 | 0.0 | 135.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1125.0 | 0.0 | 0.0 | 10.0 | 1135.0 | 250.0 | 0.0 | 250.0 | 885.0 |
|
||||||
|
|
||||||
|
Scenario: Verify that Loan re-aging preview with repayment, charge and charge adjustment - N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
When Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
When Admin sets the business date to "3 May 2025"
|
||||||
|
And Customer makes "AUTOPAY" repayment on "01 March 2025" with 250 EUR transaction amount
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "03 May 2025" due date and 20 EUR transaction amount
|
||||||
|
When Admin sets the business date to "04 May 2025"
|
||||||
|
When Admin makes a charge adjustment for the last "LOAN_NSF_FEE" type charge which is due on "03 May 2025" with 20 EUR transaction amount and externalId ""
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 20.0 | 0.0 | 20.0 | 230.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 270.0 | 0.0 | 270.0 | 750.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 01 April 2025 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 750.0 | 0.0 | 0.0 | 0.0 | 750.0 | 20.0 | 0.0 | 20.0 | 730.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
Then Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 270.0 | 0.0 | 270.0 | 750.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 March 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 20.0 | 0.0 | 20.0 | 230.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 270.0 | 0.0 | 270.0 | 750.0 |
|
||||||
|
|
||||||
|
Scenario: Verify that Loan re-aging transaction with MIR and charge - N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
|
||||||
|
And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
|
||||||
|
And Admin sets the business date to "03 May 2025"
|
||||||
|
And Admin makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "01 April 2025" with 100 EUR transaction amount
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "03 May 2025" due date and 20 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 100.0 | 0.0 | 100.0 | 150.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 100.0 | 0.0 | 100.0 | 920.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 April 2025 | 900.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 100.0 | 0.0 | 100.0 | 0.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | 01 April 2025 | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | 01 April 2025 | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 900.0 | 0.0 | 0.0 | 0.0 | 900.0 | 0.0 | 0.0 | 0.0 | 900.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
And Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 100.0 | 0.0 | 100.0 | 920.0 |
|
||||||
|
And Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 100.0 | 0.0 | 100.0 | 150.0 |
|
||||||
|
| 2 | 31 | 01 February 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 3 | 28 | 01 March 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 4 | 31 | 01 April 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
|
||||||
|
| 5 | 32 | 03 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 100.0 | 0.0 | 100.0 | 920.0 |
|
||||||
|
|
||||||
|
Scenario: Verify that Loan re-aging preview with 2nd disbursement and charge - N+1 installment after maturity date
|
||||||
|
When Admin sets the business date to "01 January 2025"
|
||||||
|
When Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "01 January 2025" with "500" amount and expected disbursement date on "01 January 2025"
|
||||||
|
And Admin successfully disburse the loan on "01 January 2025" with "500" EUR transaction amount
|
||||||
|
And Admin sets the business date to "01 May 2025"
|
||||||
|
And Admin successfully disburse the loan on "16 January 2025" with "100" EUR transaction amount
|
||||||
|
And Admin adds "LOAN_NSF_FEE" due date charge with "01 May 2025" due date and 20 EUR transaction amount
|
||||||
|
Then Loan Repayment schedule has 6 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 375.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 |
|
||||||
|
| | | 16 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 2 | 0 | 16 January 2025 | | 450.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 |
|
||||||
|
| 3 | 31 | 01 February 2025 | | 300.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 4 | 28 | 01 March 2025 | | 150.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 5 | 31 | 01 April 2025 | | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 6 | 30 | 01 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 600.0 | 0.0 | 0.0 | 20.0 | 620.0 | 0.0 | 0.0 | 0.0 | 620.0 |
|
||||||
|
When Admin creates a Loan re-aging preview by Loan external ID with the following data:
|
||||||
|
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
|
||||||
|
| 1 | WEEKS | 01 April 2025 | 1 |
|
||||||
|
Then Loan Repayment schedule preview has 6 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | 01 April 2025 | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| | | 16 January 2025 | | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | |
|
||||||
|
| 2 | 0 | 16 January 2025 | 01 April 2025 | 600.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 3 | 31 | 01 February 2025 | 01 April 2025 | 600.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 4 | 28 | 01 March 2025 | 01 April 2025 | 600.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 5 | 31 | 01 April 2025 | | 0.0 | 600.0 | 0.0 | 0.0 | 0.0 | 600.0 | 0.0 | 0.0 | 0.0 | 600.0 |
|
||||||
|
| 6 | 30 | 01 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
Then Loan Repayment schedule preview has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 600.0 | 0.0 | 0.0 | 20.0 | 620.0 | 0.0 | 0.0 | 0.0 | 620.0 |
|
||||||
|
And Loan Repayment schedule has 6 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 01 January 2025 | | 500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 0 | 01 January 2025 | | 375.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 |
|
||||||
|
| | | 16 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 2 | 0 | 16 January 2025 | | 450.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 |
|
||||||
|
| 3 | 31 | 01 February 2025 | | 300.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 4 | 28 | 01 March 2025 | | 150.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 5 | 31 | 01 April 2025 | | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
|
||||||
|
| 6 | 30 | 01 May 2025 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
|
||||||
|
And Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 600.0 | 0.0 | 0.0 | 20.0 | 620.0 | 0.0 | 0.0 | 0.0 | 620.0 |
|
||||||
+8
@@ -64,4 +64,12 @@ public interface LoanRepaymentScheduleInstallmentRepository
|
|||||||
@Param("businessDate") LocalDate businessDate, @Param("obligationsMet") boolean obligationsMet,
|
@Param("businessDate") LocalDate businessDate, @Param("obligationsMet") boolean obligationsMet,
|
||||||
@Param("loanIds") List<Long> loanIds);
|
@Param("loanIds") List<Long> loanIds);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT i
|
||||||
|
FROM LoanRepaymentScheduleInstallment i
|
||||||
|
WHERE i.loan.id = :loanId
|
||||||
|
ORDER BY i.loan.id, i.installmentNumber
|
||||||
|
""")
|
||||||
|
List<LoanRepaymentScheduleInstallment> findByLoanId(@Param("loanId") Long loanId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -32,6 +32,8 @@ public interface ReprocessLoanTransactionsService {
|
|||||||
|
|
||||||
void reprocessTransactionsWithPostTransactionChecks(Loan loan, LocalDate transactionDate);
|
void reprocessTransactionsWithPostTransactionChecks(Loan loan, LocalDate transactionDate);
|
||||||
|
|
||||||
|
void reprocessTransactionsWithoutChecks(Loan loan, LocalDate transactionDate, List<LoanTransaction> newTransactions);
|
||||||
|
|
||||||
void processPostDisbursementTransactions(Loan loan);
|
void processPostDisbursementTransactions(Loan loan);
|
||||||
|
|
||||||
void removeLoanCharge(Loan loan, LoanCharge loanCharge);
|
void removeLoanCharge(Loan loan, LoanCharge loanCharge);
|
||||||
|
|||||||
+1
-1
@@ -2868,7 +2868,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
target.setReAged(true);
|
target.setReAged(true);
|
||||||
target.setFromDate(reAgedInstallment.getFromDate());
|
target.setFromDate(reAgedInstallment.getFromDate());
|
||||||
target.setDueDate(reAgedInstallment.getDueDate());
|
target.setDueDate(reAgedInstallment.getDueDate());
|
||||||
target.setPrincipal(reAgedInstallment.getPrincipal().add(MathUtil.nullToZero(target.getPrincipalCompleted())));
|
target.setPrincipal(MathUtil.add(reAgedInstallment.getPrincipal(), target.getPrincipalCompleted()));
|
||||||
target.setInterestCharged(MathUtil.add(reAgedInstallment.getInterestCharged(), target.getInterestPaid()));
|
target.setInterestCharged(MathUtil.add(reAgedInstallment.getInterestCharged(), target.getInterestPaid()));
|
||||||
target.updateObligationsMet(currency, transactionDate);
|
target.updateObligationsMet(currency, transactionDate);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -27,6 +27,8 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.BeanParam;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
@@ -63,14 +65,17 @@ import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
|
|||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.reaging.LoanReAgingService;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -104,6 +109,7 @@ public class LoanTransactionsApiResource {
|
|||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final PaymentTypeReadPlatformService paymentTypeReadPlatformService;
|
private final PaymentTypeReadPlatformService paymentTypeReadPlatformService;
|
||||||
private final LoanChargePaidByReadService loanChargePaidByReadService;
|
private final LoanChargePaidByReadService loanChargePaidByReadService;
|
||||||
|
private final LoanReAgingService loanReAgingService;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{loanId}/transactions/template")
|
@Path("{loanId}/transactions/template")
|
||||||
@@ -777,4 +783,26 @@ public class LoanTransactionsApiResource {
|
|||||||
throw new IllegalArgumentException("loanId and loanExternalId cannot be both null");
|
throw new IllegalArgumentException("loanId and loanExternalId cannot be both null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{loanId}/transactions/reage-preview")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Preview Re-Age Schedule", description = "Generates a preview of the re-aged loan schedule based on the provided parameters without creating any transactions or modifying the loan.")
|
||||||
|
public LoanScheduleData previewReAgeSchedule(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
|
||||||
|
@Valid @BeanParam final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
return loanReAgingService.previewReAge(loanId, null, reAgePreviewRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("external-id/{loanExternalId}/transactions/reage-preview")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Preview Re-Age Schedule", description = "Generates a preview of the re-aged loan schedule based on the provided parameters without creating any transactions or modifying the loan.")
|
||||||
|
public LoanScheduleData previewReAgeSchedule(
|
||||||
|
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
|
||||||
|
@Valid @BeanParam final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
return loanReAgingService.previewReAge(null, loanExternalId, reAgePreviewRequest);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 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.api.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
|
import org.apache.fineract.validation.constraints.EnumValue;
|
||||||
|
import org.apache.fineract.validation.constraints.LocalDate;
|
||||||
|
import org.apache.fineract.validation.constraints.Locale;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@LocalDate(dateField = "startDate", formatField = "dateFormat", localeField = "locale")
|
||||||
|
public class ReAgePreviewRequest implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@QueryParam("frequencyNumber")
|
||||||
|
@Parameter(description = "The frequency number for the re-aging schedule", required = true)
|
||||||
|
@NotNull(message = "{org.apache.fineract.reage.frequency-number.not-blank}")
|
||||||
|
@Min(value = 1, message = "{org.apache.fineract.reage.frequency-number.min}")
|
||||||
|
private Integer frequencyNumber;
|
||||||
|
|
||||||
|
@QueryParam("frequencyType")
|
||||||
|
@Parameter(description = "The frequency type (DAYS, WEEKS, MONTHS, YEARS)", required = true)
|
||||||
|
@NotBlank(message = "{org.apache.fineract.reage.frequency-type.not-blank}")
|
||||||
|
@EnumValue(enumClass = PeriodFrequencyType.class, message = "{org.apache.fineract.frequency-type.invalid}")
|
||||||
|
private String frequencyType;
|
||||||
|
|
||||||
|
@QueryParam("startDate")
|
||||||
|
@Parameter(description = "The start date for the re-aging schedule", required = true)
|
||||||
|
@NotBlank(message = "{org.apache.fineract.reage.start-date.not-blank}")
|
||||||
|
private String startDate;
|
||||||
|
|
||||||
|
@QueryParam("numberOfInstallments")
|
||||||
|
@Parameter(description = "The number of installments for the re-aged loan", required = true)
|
||||||
|
@NotNull(message = "{org.apache.fineract.reage.number-of-installments.not-blank}")
|
||||||
|
@Min(value = 1, message = "{org.apache.fineract.reage.number-of-installments.min}")
|
||||||
|
private Integer numberOfInstallments;
|
||||||
|
|
||||||
|
@QueryParam("dateFormat")
|
||||||
|
@Parameter(description = "The date format used for the startDate parameter", required = true)
|
||||||
|
@NotBlank(message = "{org.apache.fineract.businessdate.date-format.not-blank}")
|
||||||
|
private String dateFormat;
|
||||||
|
|
||||||
|
@QueryParam("locale")
|
||||||
|
@Parameter(description = "The locale to use for formatting", required = true)
|
||||||
|
@NotBlank(message = "{org.apache.fineract.businessdate.locale.not-blank}")
|
||||||
|
@Locale
|
||||||
|
private String locale;
|
||||||
|
|
||||||
|
}
|
||||||
+2
-2
@@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
|||||||
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.reaging.LoanReAgingServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.reaging.LoanReAgingService;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -34,7 +34,7 @@ import org.springframework.stereotype.Service;
|
|||||||
@CommandType(entity = "LOAN", action = "REAGE")
|
@CommandType(entity = "LOAN", action = "REAGE")
|
||||||
public class LoanReAgingCommandHandler implements NewCommandSourceHandler {
|
public class LoanReAgingCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
private final LoanReAgingServiceImpl loanReAgingService;
|
private final LoanReAgingService loanReAgingService;
|
||||||
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
|||||||
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.reaging.LoanReAgingServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.reaging.LoanReAgingService;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -34,7 +34,7 @@ import org.springframework.stereotype.Service;
|
|||||||
@CommandType(entity = "LOAN", action = "UNDO_REAGE")
|
@CommandType(entity = "LOAN", action = "UNDO_REAGE")
|
||||||
public class LoanUndoReAgingCommandHandler implements NewCommandSourceHandler {
|
public class LoanUndoReAgingCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
private final LoanReAgingServiceImpl loanReAgingService;
|
private final LoanReAgingService loanReAgingService;
|
||||||
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+5
-387
@@ -31,8 +31,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -103,7 +101,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
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.LoanStatusEnumData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||||
@@ -136,7 +133,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmo
|
|||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
@@ -158,9 +154,7 @@ import org.springframework.dao.DataAccessException;
|
|||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
|
||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
@@ -188,7 +182,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
private final ConfigurationDomainService configurationDomainService;
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
private final AccountDetailsReadPlatformService accountDetailsReadPlatformService;
|
private final AccountDetailsReadPlatformService accountDetailsReadPlatformService;
|
||||||
private final ColumnValidator columnValidator;
|
private final ColumnValidator columnValidator;
|
||||||
protected final DatabaseSpecificSQLGenerator sqlGenerator;
|
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||||
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
||||||
private final LoanTransactionRepository loanTransactionRepository;
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
private final LoanChargePaidByReadService loanChargePaidByReadService;
|
private final LoanChargePaidByReadService loanChargePaidByReadService;
|
||||||
@@ -201,6 +195,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
|
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
|
||||||
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
|
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
|
||||||
private final LoanMaximumAmountCalculator loanMaximumAmountCalculator;
|
private final LoanMaximumAmountCalculator loanMaximumAmountCalculator;
|
||||||
|
private final LoanRepaymentScheduleService loanRepaymentScheduleService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoanAccountData retrieveOne(final Long loanId) {
|
public LoanAccountData retrieveOne(final Long loanId) {
|
||||||
@@ -280,19 +275,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
||||||
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
||||||
LoanScheduleType loanScheduleType) {
|
LoanScheduleType loanScheduleType) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
try {
|
return loanRepaymentScheduleService.findLoanScheduleData(loanId, repaymentScheduleRelatedLoanData, disbursementData,
|
||||||
this.context.authenticatedUser();
|
capitalizedIncomeData, isInterestRecalculationEnabled, loanScheduleType);
|
||||||
|
|
||||||
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
|
|
||||||
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
|
|
||||||
} catch (final EmptyResultDataAccessException e) {
|
|
||||||
throw new LoanNotFoundException(loanId, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1339,373 +1324,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class LoanScheduleResultSetExtractor implements ResultSetExtractor<LoanScheduleData> {
|
|
||||||
|
|
||||||
private final CurrencyData currency;
|
|
||||||
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, 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();
|
|
||||||
this.disbursementData = disbursementData;
|
|
||||||
this.excludePastUnDisbursed = isInterestRecalculationEnabled;
|
|
||||||
this.loanScheduleType = loanScheduleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String schema() {
|
|
||||||
|
|
||||||
return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, ls.obligations_met_on_date as obligationsMetOnDate, ls.completed_derived as complete,"
|
|
||||||
+ " ls.principal_amount as principalDue, ls.principal_completed_derived as principalPaid, ls.principal_writtenoff_derived as principalWrittenOff, ls.is_additional as isAdditional, "
|
|
||||||
+ " ls.interest_amount as interestDue, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, ls.interest_writtenoff_derived as interestWrittenOff, "
|
|
||||||
+ " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, "
|
|
||||||
+ " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, "
|
|
||||||
+ " ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, "
|
|
||||||
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as principalCredits, ls.credited_fee as feeCredits, ls.credited_penalty as penaltyCredits, ls.is_down_payment isDownPayment, "
|
|
||||||
+ " ls.accrual_interest_derived as accrualInterest " + " from m_loan_repayment_schedule ls ";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LoanScheduleData extractData(@NonNull final ResultSet rs) throws SQLException, DataAccessException {
|
|
||||||
BigDecimal waivedChargeAmount = BigDecimal.ZERO;
|
|
||||||
for (DisbursementData disbursementDetail : disbursementData) {
|
|
||||||
waivedChargeAmount = waivedChargeAmount.add(disbursementDetail.getWaivedChargeAmount());
|
|
||||||
}
|
|
||||||
final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(
|
|
||||||
this.disbursement.disbursementDate(), this.disbursement.getPrincipal(), this.totalFeeChargesDueAtDisbursement,
|
|
||||||
this.disbursement.isDisbursed());
|
|
||||||
|
|
||||||
final List<LoanSchedulePeriodData> periods = new ArrayList<>();
|
|
||||||
final MonetaryCurrency monCurrency = new MonetaryCurrency(this.currency.getCode(), this.currency.getDecimalPlaces(),
|
|
||||||
this.currency.getInMultiplesOf());
|
|
||||||
BigDecimal totalPrincipalDisbursed = BigDecimal.ZERO;
|
|
||||||
BigDecimal disbursementChargeAmount = this.totalFeeChargesDueAtDisbursement;
|
|
||||||
if (disbursementData.isEmpty()) {
|
|
||||||
periods.add(disbursementPeriod);
|
|
||||||
totalPrincipalDisbursed = Money.of(monCurrency, this.disbursement.getPrincipal()).getAmount();
|
|
||||||
} else {
|
|
||||||
if (!this.disbursement.isDisbursed()) {
|
|
||||||
excludePastUnDisbursed = false;
|
|
||||||
}
|
|
||||||
for (DisbursementData data : disbursementData) {
|
|
||||||
if (data.getChargeAmount() != null) {
|
|
||||||
disbursementChargeAmount = disbursementChargeAmount.subtract(data.getChargeAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.outstandingLoanPrincipalBalance = BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
Money totalPrincipalExpected = Money.zero(monCurrency);
|
|
||||||
Money totalPrincipalPaid = Money.zero(monCurrency);
|
|
||||||
Money totalInterestCharged = Money.zero(monCurrency);
|
|
||||||
Money totalFeeChargesCharged = Money.zero(monCurrency);
|
|
||||||
Money totalPenaltyChargesCharged = Money.zero(monCurrency);
|
|
||||||
Money totalWaived = Money.zero(monCurrency);
|
|
||||||
Money totalWrittenOff = Money.zero(monCurrency);
|
|
||||||
Money totalRepaymentExpected = Money.zero(monCurrency);
|
|
||||||
Money totalRepayment = Money.zero(monCurrency);
|
|
||||||
Money totalPaidInAdvance = Money.zero(monCurrency);
|
|
||||||
Money totalPaidLate = Money.zero(monCurrency);
|
|
||||||
Money totalOutstanding = Money.zero(monCurrency);
|
|
||||||
Money totalCredits = Money.zero(monCurrency);
|
|
||||||
|
|
||||||
// update totals with details of fees charged during disbursement
|
|
||||||
totalFeeChargesCharged = totalFeeChargesCharged.plus(disbursementPeriod.getFeeChargesDue().subtract(waivedChargeAmount));
|
|
||||||
totalRepaymentExpected = totalRepaymentExpected.plus(disbursementPeriod.getFeeChargesDue()).minus(waivedChargeAmount);
|
|
||||||
totalRepayment = totalRepayment.plus(disbursementPeriod.getFeeChargesPaid()).minus(waivedChargeAmount);
|
|
||||||
totalOutstanding = totalOutstanding.plus(disbursementPeriod.getFeeChargesDue()).minus(disbursementPeriod.getFeeChargesPaid());
|
|
||||||
|
|
||||||
Integer loanTermInDays = 0;
|
|
||||||
Set<Long> disbursementPeriodIds = new HashSet<>();
|
|
||||||
while (rs.next()) {
|
|
||||||
final Integer period = JdbcSupport.getInteger(rs, "period");
|
|
||||||
LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromDate");
|
|
||||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
|
||||||
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
|
|
||||||
final boolean complete = rs.getBoolean("complete");
|
|
||||||
|
|
||||||
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");
|
|
||||||
final BigDecimal feeCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeCredits");
|
|
||||||
final BigDecimal penaltyCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyCredits");
|
|
||||||
final BigDecimal credits = principalCredits.add(feeCredits).add(penaltyCredits);
|
|
||||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalCredits);
|
|
||||||
|
|
||||||
totalPrincipalDisbursed = totalPrincipalDisbursed.add(disbursedAmount);
|
|
||||||
|
|
||||||
Integer daysInPeriod = 0;
|
|
||||||
if (fromDate != null) {
|
|
||||||
daysInPeriod = DateUtils.getExactDifferenceInDays(fromDate, dueDate);
|
|
||||||
loanTermInDays = loanTermInDays + daysInPeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BigDecimal principalDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDue");
|
|
||||||
totalPrincipalExpected = totalPrincipalExpected.plus(principalDue);
|
|
||||||
final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
|
|
||||||
totalPrincipalPaid = totalPrincipalPaid.plus(principalPaid);
|
|
||||||
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
|
|
||||||
|
|
||||||
final BigDecimal principalOutstanding = principalDue.subtract(principalPaid).subtract(principalWrittenOff);
|
|
||||||
|
|
||||||
final BigDecimal interestExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestDue");
|
|
||||||
totalInterestCharged = totalInterestCharged.plus(interestExpectedDue);
|
|
||||||
final BigDecimal interestPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestPaid");
|
|
||||||
final BigDecimal interestWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWaived");
|
|
||||||
final BigDecimal interestWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWrittenOff");
|
|
||||||
final BigDecimal accrualInterest = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accrualInterest");
|
|
||||||
|
|
||||||
final BigDecimal interestActualDue = interestExpectedDue.subtract(interestWaived).subtract(interestWrittenOff);
|
|
||||||
final BigDecimal interestOutstanding = interestActualDue.subtract(interestPaid);
|
|
||||||
|
|
||||||
final BigDecimal feeChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesDue");
|
|
||||||
totalFeeChargesCharged = totalFeeChargesCharged.plus(feeChargesExpectedDue);
|
|
||||||
final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesPaid");
|
|
||||||
final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWaived");
|
|
||||||
final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWrittenOff");
|
|
||||||
|
|
||||||
final BigDecimal feeChargesActualDue = feeChargesExpectedDue.subtract(feeChargesWaived).subtract(feeChargesWrittenOff);
|
|
||||||
final BigDecimal feeChargesOutstanding = feeChargesActualDue.subtract(feeChargesPaid);
|
|
||||||
|
|
||||||
final BigDecimal penaltyChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesDue");
|
|
||||||
totalPenaltyChargesCharged = totalPenaltyChargesCharged.plus(penaltyChargesExpectedDue);
|
|
||||||
final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid");
|
|
||||||
final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived");
|
|
||||||
final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff");
|
|
||||||
|
|
||||||
final BigDecimal totalPaidInAdvanceForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
|
|
||||||
"totalPaidInAdvanceForPeriod");
|
|
||||||
final BigDecimal totalPaidLateForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPaidLateForPeriod");
|
|
||||||
|
|
||||||
final BigDecimal penaltyChargesActualDue = penaltyChargesExpectedDue.subtract(penaltyChargesWaived)
|
|
||||||
.subtract(penaltyChargesWrittenOff);
|
|
||||||
final BigDecimal penaltyChargesOutstanding = penaltyChargesActualDue.subtract(penaltyChargesPaid);
|
|
||||||
|
|
||||||
final BigDecimal totalExpectedCostOfLoanForPeriod = interestExpectedDue.add(feeChargesExpectedDue)
|
|
||||||
.add(penaltyChargesExpectedDue);
|
|
||||||
|
|
||||||
final BigDecimal totalDueForPeriod = principalDue.add(totalExpectedCostOfLoanForPeriod);
|
|
||||||
final BigDecimal totalPaidForPeriod = principalPaid.add(interestPaid).add(feeChargesPaid).add(penaltyChargesPaid);
|
|
||||||
final BigDecimal totalWaivedForPeriod = interestWaived.add(feeChargesWaived).add(penaltyChargesWaived);
|
|
||||||
totalWaived = totalWaived.plus(totalWaivedForPeriod);
|
|
||||||
final BigDecimal totalWrittenOffForPeriod = principalWrittenOff.add(interestWrittenOff).add(feeChargesWrittenOff)
|
|
||||||
.add(penaltyChargesWrittenOff);
|
|
||||||
totalWrittenOff = totalWrittenOff.plus(totalWrittenOffForPeriod);
|
|
||||||
|
|
||||||
final BigDecimal totalOutstandingForPeriod = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding)
|
|
||||||
.add(penaltyChargesOutstanding);
|
|
||||||
|
|
||||||
totalRepaymentExpected = totalRepaymentExpected.plus(totalDueForPeriod);
|
|
||||||
totalRepayment = totalRepayment.plus(totalPaidForPeriod);
|
|
||||||
totalPaidInAdvance = totalPaidInAdvance.plus(totalPaidInAdvanceForPeriod);
|
|
||||||
totalPaidLate = totalPaidLate.plus(totalPaidLateForPeriod);
|
|
||||||
totalOutstanding = totalOutstanding.plus(totalOutstandingForPeriod);
|
|
||||||
totalCredits = totalCredits.add(credits);
|
|
||||||
|
|
||||||
if (fromDate == null) {
|
|
||||||
fromDate = this.lastDueDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.subtract(principalDue);
|
|
||||||
|
|
||||||
// update based on current period values
|
|
||||||
this.lastDueDate = dueDate;
|
|
||||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(principalDue);
|
|
||||||
|
|
||||||
final boolean isDownPayment = rs.getBoolean("isDownPayment");
|
|
||||||
|
|
||||||
LoanSchedulePeriodData periodData;
|
|
||||||
|
|
||||||
periodData = LoanSchedulePeriodData.periodWithPayments(period, fromDate, dueDate, obligationsMetOnDate, complete,
|
|
||||||
principalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
|
|
||||||
interestExpectedDue, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesExpectedDue,
|
|
||||||
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesExpectedDue,
|
|
||||||
penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalPaidForPeriod,
|
|
||||||
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod, credits,
|
|
||||||
isDownPayment, accrualInterest);
|
|
||||||
|
|
||||||
periods.add(periodData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LoanScheduleData(this.currency, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected.getAmount(),
|
|
||||||
totalPrincipalPaid.getAmount(), totalInterestCharged.getAmount(), totalFeeChargesCharged.getAmount(),
|
|
||||||
totalPenaltyChargesCharged.getAmount(), totalWaived.getAmount(), totalWrittenOff.getAmount(),
|
|
||||||
totalRepaymentExpected.getAmount(), totalRepayment.getAmount(), totalPaidInAdvance.getAmount(),
|
|
||||||
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
|
||||||
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
|
|
||||||
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
|
||||||
|
|
||||||
boolean hasMultipleTranchesOnSameDate = hasMultipleTranchesOnSameDate(disbursementData);
|
|
||||||
|
|
||||||
if (hasMultipleTranchesOnSameDate) {
|
|
||||||
Map<LocalDate, List<DisbursementData>> disbursementsByDate = new HashMap<>();
|
|
||||||
|
|
||||||
for (final DisbursementData data : disbursementData) {
|
|
||||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
|
||||||
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());
|
|
||||||
|
|
||||||
if (isEligible) {
|
|
||||||
disbursementsByDate.computeIfAbsent(data.disbursementDate(), k -> new ArrayList<>()).add(data);
|
|
||||||
disbursementPeriodIds.add(data.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Map.Entry<LocalDate, List<DisbursementData>> entry : disbursementsByDate.entrySet()) {
|
|
||||||
List<DisbursementData> sameDateDisbursements = entry.getValue();
|
|
||||||
|
|
||||||
if (sameDateDisbursements.size() > 1) {
|
|
||||||
List<DisbursementData> disbursedTranches = sameDateDisbursements.stream().filter(DisbursementData::isDisbursed)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (!disbursedTranches.isEmpty()) {
|
|
||||||
for (DisbursementData data : disbursedTranches) {
|
|
||||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (DisbursementData data : sameDateDisbursements) {
|
|
||||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DisbursementData data = sameDateDisbursements.get(0);
|
|
||||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (final DisbursementData data : disbursementData) {
|
|
||||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
|
||||||
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());
|
|
||||||
|
|
||||||
if (isEligible) {
|
|
||||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
|
||||||
disbursementPeriodIds.add(data.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return disbursementDataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasMultipleTranchesOnSameDate(Collection<DisbursementData> disbursementData) {
|
|
||||||
if (disbursementData == null || disbursementData.size() <= 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return disbursementData.stream().collect(Collectors.groupingBy(DisbursementData::disbursementDate, Collectors.counting()))
|
|
||||||
.values().stream().anyMatch(count -> count > 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
: disbursementChargeAmount.add(data.getChargeAmount()).subtract(waivedChargeAmount);
|
|
||||||
return LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(), chargeAmount,
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
|
protected static class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
|
||||||
|
|
||||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||||
|
|||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanSchedulePeriodDataWrapper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LoanRepaymentScheduleService {
|
||||||
|
|
||||||
|
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
||||||
|
|
||||||
|
public LoanScheduleData findLoanScheduleData(final Long loanId, final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
|
||||||
|
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
|
||||||
|
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments = this.loanRepaymentScheduleInstallmentRepository.findByLoanId(loanId);
|
||||||
|
|
||||||
|
return extractLoanScheduleData(installments, repaymentScheduleRelatedLoanData, disbursementData, capitalizedIncomeData,
|
||||||
|
isInterestRecalculationEnabled, loanScheduleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanScheduleData extractLoanScheduleData(final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
||||||
|
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
||||||
|
LoanScheduleType loanScheduleType) {
|
||||||
|
|
||||||
|
final CurrencyData currency = repaymentScheduleRelatedLoanData.getCurrency();
|
||||||
|
final DisbursementData disbursement = repaymentScheduleRelatedLoanData.disbursementData();
|
||||||
|
final BigDecimal totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
|
||||||
|
LocalDate lastDueDate = disbursement.disbursementDate();
|
||||||
|
BigDecimal outstandingLoanPrincipalBalance = disbursement.getPrincipal();
|
||||||
|
boolean excludePastUnDisbursed = isInterestRecalculationEnabled;
|
||||||
|
|
||||||
|
BigDecimal waivedChargeAmount = BigDecimal.ZERO;
|
||||||
|
for (DisbursementData disbursementDetail : disbursementData) {
|
||||||
|
waivedChargeAmount = waivedChargeAmount.add(disbursementDetail.getWaivedChargeAmount());
|
||||||
|
}
|
||||||
|
final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursement.disbursementDate(),
|
||||||
|
disbursement.getPrincipal(), totalFeeChargesDueAtDisbursement, disbursement.isDisbursed());
|
||||||
|
|
||||||
|
final List<LoanSchedulePeriodData> periods = new ArrayList<>();
|
||||||
|
final MonetaryCurrency monCurrency = new MonetaryCurrency(currency.getCode(), currency.getDecimalPlaces(),
|
||||||
|
currency.getInMultiplesOf());
|
||||||
|
BigDecimal totalPrincipalDisbursed = BigDecimal.ZERO;
|
||||||
|
BigDecimal disbursementChargeAmount = totalFeeChargesDueAtDisbursement;
|
||||||
|
if (disbursementData.isEmpty()) {
|
||||||
|
periods.add(disbursementPeriod);
|
||||||
|
totalPrincipalDisbursed = Money.of(monCurrency, disbursement.getPrincipal()).getAmount();
|
||||||
|
} else {
|
||||||
|
if (!disbursement.isDisbursed()) {
|
||||||
|
excludePastUnDisbursed = false;
|
||||||
|
}
|
||||||
|
for (DisbursementData data : disbursementData) {
|
||||||
|
if (data.getChargeAmount() != null) {
|
||||||
|
disbursementChargeAmount = disbursementChargeAmount.subtract(data.getChargeAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outstandingLoanPrincipalBalance = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
Money totalPrincipalExpected = Money.zero(monCurrency);
|
||||||
|
Money totalPrincipalPaid = Money.zero(monCurrency);
|
||||||
|
Money totalInterestCharged = Money.zero(monCurrency);
|
||||||
|
Money totalFeeChargesCharged = Money.zero(monCurrency);
|
||||||
|
Money totalPenaltyChargesCharged = Money.zero(monCurrency);
|
||||||
|
Money totalWaived = Money.zero(monCurrency);
|
||||||
|
Money totalWrittenOff = Money.zero(monCurrency);
|
||||||
|
Money totalRepaymentExpected = Money.zero(monCurrency);
|
||||||
|
Money totalRepayment = Money.zero(monCurrency);
|
||||||
|
Money totalPaidInAdvance = Money.zero(monCurrency);
|
||||||
|
Money totalPaidLate = Money.zero(monCurrency);
|
||||||
|
Money totalOutstanding = Money.zero(monCurrency);
|
||||||
|
Money totalCredits = Money.zero(monCurrency);
|
||||||
|
|
||||||
|
// update totals with details of fees charged during disbursement
|
||||||
|
totalFeeChargesCharged = totalFeeChargesCharged.plus(disbursementPeriod.getFeeChargesDue().subtract(waivedChargeAmount));
|
||||||
|
totalRepaymentExpected = totalRepaymentExpected.plus(disbursementPeriod.getFeeChargesDue()).minus(waivedChargeAmount);
|
||||||
|
totalRepayment = totalRepayment.plus(disbursementPeriod.getFeeChargesPaid()).minus(waivedChargeAmount);
|
||||||
|
totalOutstanding = totalOutstanding.plus(disbursementPeriod.getFeeChargesDue()).minus(disbursementPeriod.getFeeChargesPaid());
|
||||||
|
|
||||||
|
Integer loanTermInDays = 0;
|
||||||
|
Set<Long> disbursementPeriodIds = new HashSet<>();
|
||||||
|
|
||||||
|
for (LoanRepaymentScheduleInstallment installment : installments) {
|
||||||
|
final Integer period = installment.getInstallmentNumber();
|
||||||
|
LocalDate fromDate = installment.getFromDate();
|
||||||
|
final LocalDate dueDate = installment.getDueDate();
|
||||||
|
final LocalDate obligationsMetOnDate = installment.getObligationsMetOnDate();
|
||||||
|
final boolean complete = installment.isObligationsMet();
|
||||||
|
|
||||||
|
List<LoanSchedulePeriodDataWrapper> combinedDataList = new ArrayList<>();
|
||||||
|
combinedDataList.addAll(collectEligibleDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate,
|
||||||
|
disbursementPeriodIds, disbursement, excludePastUnDisbursed));
|
||||||
|
combinedDataList.addAll(collectEligibleCapitalizedIncomeData(capitalizedIncomeData, fromDate, dueDate, disbursementPeriodIds));
|
||||||
|
combinedDataList.sort(this::sortPeriodDataHolders);
|
||||||
|
outstandingLoanPrincipalBalance = fillLoanSchedulePeriodData(periods, combinedDataList, disbursementChargeAmount,
|
||||||
|
waivedChargeAmount, outstandingLoanPrincipalBalance);
|
||||||
|
|
||||||
|
BigDecimal disbursedAmount = calculateDisbursedAmount(combinedDataList);
|
||||||
|
|
||||||
|
// Add the Charge back or Credits to the initial amount to avoid negative balance
|
||||||
|
final BigDecimal principalCredits = installment.getCreditedPrincipal() != null ? installment.getCreditedPrincipal()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal feeCredits = installment.getCreditedFee() != null ? installment.getCreditedFee() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal penaltyCredits = installment.getCreditedPenalty() != null ? installment.getCreditedPenalty() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal credits = principalCredits.add(feeCredits).add(penaltyCredits);
|
||||||
|
outstandingLoanPrincipalBalance = outstandingLoanPrincipalBalance.add(principalCredits);
|
||||||
|
|
||||||
|
totalPrincipalDisbursed = totalPrincipalDisbursed.add(disbursedAmount);
|
||||||
|
|
||||||
|
Integer daysInPeriod = 0;
|
||||||
|
if (fromDate != null) {
|
||||||
|
daysInPeriod = DateUtils.getExactDifferenceInDays(fromDate, dueDate);
|
||||||
|
loanTermInDays = loanTermInDays + daysInPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal principalDue = installment.getPrincipal() != null ? installment.getPrincipal() : BigDecimal.ZERO;
|
||||||
|
totalPrincipalExpected = totalPrincipalExpected.plus(principalDue);
|
||||||
|
final BigDecimal principalPaid = installment.getPrincipalCompleted() != null ? installment.getPrincipalCompleted()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
totalPrincipalPaid = totalPrincipalPaid.plus(principalPaid);
|
||||||
|
final BigDecimal principalWrittenOff = installment.getPrincipalWrittenOff() != null ? installment.getPrincipalWrittenOff()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
final BigDecimal principalOutstanding = principalDue.subtract(principalPaid).subtract(principalWrittenOff);
|
||||||
|
|
||||||
|
final BigDecimal interestExpectedDue = installment.getInterestCharged() != null ? installment.getInterestCharged()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
totalInterestCharged = totalInterestCharged.plus(interestExpectedDue);
|
||||||
|
final BigDecimal interestPaid = installment.getInterestPaid() != null ? installment.getInterestPaid() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal interestWaived = installment.getInterestWaived() != null ? installment.getInterestWaived() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal interestWrittenOff = installment.getInterestWrittenOff() != null ? installment.getInterestWrittenOff()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal accrualInterest = installment.getInterestAccrued() != null ? installment.getInterestAccrued()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
final BigDecimal interestActualDue = interestExpectedDue.subtract(interestWaived).subtract(interestWrittenOff);
|
||||||
|
final BigDecimal interestOutstanding = interestActualDue.subtract(interestPaid);
|
||||||
|
|
||||||
|
final BigDecimal feeChargesExpectedDue = installment.getFeeChargesCharged() != null ? installment.getFeeChargesCharged()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
totalFeeChargesCharged = totalFeeChargesCharged.plus(feeChargesExpectedDue);
|
||||||
|
final BigDecimal feeChargesPaid = installment.getFeeChargesPaid() != null ? installment.getFeeChargesPaid() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal feeChargesWaived = installment.getFeeChargesWaived() != null ? installment.getFeeChargesWaived()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal feeChargesWrittenOff = installment.getFeeChargesWrittenOff() != null ? installment.getFeeChargesWrittenOff()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
final BigDecimal feeChargesActualDue = feeChargesExpectedDue.subtract(feeChargesWaived).subtract(feeChargesWrittenOff);
|
||||||
|
final BigDecimal feeChargesOutstanding = feeChargesActualDue.subtract(feeChargesPaid);
|
||||||
|
|
||||||
|
final BigDecimal penaltyChargesExpectedDue = installment.getPenaltyCharges() != null ? installment.getPenaltyCharges()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
totalPenaltyChargesCharged = totalPenaltyChargesCharged.plus(penaltyChargesExpectedDue);
|
||||||
|
final BigDecimal penaltyChargesPaid = installment.getPenaltyChargesPaid() != null ? installment.getPenaltyChargesPaid()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal penaltyChargesWaived = installment.getPenaltyChargesWaived() != null ? installment.getPenaltyChargesWaived()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal penaltyChargesWrittenOff = installment.getPenaltyChargesWrittenOff() != null
|
||||||
|
? installment.getPenaltyChargesWrittenOff()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
final BigDecimal totalPaidInAdvanceForPeriod = installment.getTotalPaidInAdvance() != null ? installment.getTotalPaidInAdvance()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
final BigDecimal totalPaidLateForPeriod = installment.getTotalPaidLate() != null ? installment.getTotalPaidLate()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
final BigDecimal penaltyChargesActualDue = penaltyChargesExpectedDue.subtract(penaltyChargesWaived)
|
||||||
|
.subtract(penaltyChargesWrittenOff);
|
||||||
|
final BigDecimal penaltyChargesOutstanding = penaltyChargesActualDue.subtract(penaltyChargesPaid);
|
||||||
|
|
||||||
|
final BigDecimal totalExpectedCostOfLoanForPeriod = interestExpectedDue.add(feeChargesExpectedDue)
|
||||||
|
.add(penaltyChargesExpectedDue);
|
||||||
|
|
||||||
|
final BigDecimal totalDueForPeriod = principalDue.add(totalExpectedCostOfLoanForPeriod);
|
||||||
|
final BigDecimal totalPaidForPeriod = principalPaid.add(interestPaid).add(feeChargesPaid).add(penaltyChargesPaid);
|
||||||
|
final BigDecimal totalWaivedForPeriod = interestWaived.add(feeChargesWaived).add(penaltyChargesWaived);
|
||||||
|
totalWaived = totalWaived.plus(totalWaivedForPeriod);
|
||||||
|
final BigDecimal totalWrittenOffForPeriod = principalWrittenOff.add(interestWrittenOff).add(feeChargesWrittenOff)
|
||||||
|
.add(penaltyChargesWrittenOff);
|
||||||
|
totalWrittenOff = totalWrittenOff.plus(totalWrittenOffForPeriod);
|
||||||
|
|
||||||
|
final BigDecimal totalOutstandingForPeriod = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding)
|
||||||
|
.add(penaltyChargesOutstanding);
|
||||||
|
|
||||||
|
totalRepaymentExpected = totalRepaymentExpected.plus(totalDueForPeriod);
|
||||||
|
totalRepayment = totalRepayment.plus(totalPaidForPeriod);
|
||||||
|
totalPaidInAdvance = totalPaidInAdvance.plus(totalPaidInAdvanceForPeriod);
|
||||||
|
totalPaidLate = totalPaidLate.plus(totalPaidLateForPeriod);
|
||||||
|
totalOutstanding = totalOutstanding.plus(totalOutstandingForPeriod);
|
||||||
|
totalCredits = totalCredits.add(credits);
|
||||||
|
|
||||||
|
if (fromDate == null) {
|
||||||
|
fromDate = lastDueDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal outstandingPrincipalBalanceOfLoan = outstandingLoanPrincipalBalance.subtract(principalDue);
|
||||||
|
|
||||||
|
// update based on current period values
|
||||||
|
lastDueDate = dueDate;
|
||||||
|
outstandingLoanPrincipalBalance = outstandingLoanPrincipalBalance.subtract(principalDue);
|
||||||
|
|
||||||
|
final boolean isDownPayment = installment.isDownPayment();
|
||||||
|
|
||||||
|
LoanSchedulePeriodData periodData;
|
||||||
|
|
||||||
|
periodData = LoanSchedulePeriodData.periodWithPayments(period, fromDate, dueDate, obligationsMetOnDate, complete, principalDue,
|
||||||
|
principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan, interestExpectedDue,
|
||||||
|
interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesExpectedDue, feeChargesPaid,
|
||||||
|
feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesExpectedDue, penaltyChargesPaid,
|
||||||
|
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalPaidForPeriod,
|
||||||
|
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod, credits,
|
||||||
|
isDownPayment, accrualInterest);
|
||||||
|
|
||||||
|
periods.add(periodData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LoanScheduleData(currency, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected.getAmount(),
|
||||||
|
totalPrincipalPaid.getAmount(), totalInterestCharged.getAmount(), totalFeeChargesCharged.getAmount(),
|
||||||
|
totalPenaltyChargesCharged.getAmount(), totalWaived.getAmount(), totalWrittenOff.getAmount(),
|
||||||
|
totalRepaymentExpected.getAmount(), totalRepayment.getAmount(), totalPaidInAdvance.getAmount(), totalPaidLate.getAmount(),
|
||||||
|
totalOutstanding.getAmount(), totalCredits.getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
||||||
|
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds,
|
||||||
|
DisbursementData mainDisbursement, boolean excludePastUnDisbursed) {
|
||||||
|
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean hasMultipleTranchesOnSameDate = hasMultipleTranchesOnSameDate(disbursementData);
|
||||||
|
|
||||||
|
if (hasMultipleTranchesOnSameDate) {
|
||||||
|
Map<LocalDate, List<DisbursementData>> disbursementsByDate = new HashMap<>();
|
||||||
|
|
||||||
|
for (final DisbursementData data : disbursementData) {
|
||||||
|
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||||
|
boolean isEligible = ((fromDate.equals(mainDisbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||||
|
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||||
|
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||||
|
&& !disbursementPeriodIds.contains(data.getId());
|
||||||
|
|
||||||
|
if (isEligible) {
|
||||||
|
disbursementsByDate.computeIfAbsent(data.disbursementDate(), k -> new ArrayList<>()).add(data);
|
||||||
|
disbursementPeriodIds.add(data.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<LocalDate, List<DisbursementData>> entry : disbursementsByDate.entrySet()) {
|
||||||
|
List<DisbursementData> sameDateDisbursements = entry.getValue();
|
||||||
|
|
||||||
|
if (sameDateDisbursements.size() > 1) {
|
||||||
|
List<DisbursementData> disbursedTranches = sameDateDisbursements.stream().filter(DisbursementData::isDisbursed)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!disbursedTranches.isEmpty()) {
|
||||||
|
for (DisbursementData data : disbursedTranches) {
|
||||||
|
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (DisbursementData data : sameDateDisbursements) {
|
||||||
|
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DisbursementData data = sameDateDisbursements.get(0);
|
||||||
|
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (final DisbursementData data : disbursementData) {
|
||||||
|
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||||
|
boolean isEligible = ((fromDate.equals(mainDisbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||||
|
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||||
|
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||||
|
&& !disbursementPeriodIds.contains(data.getId());
|
||||||
|
|
||||||
|
if (isEligible) {
|
||||||
|
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||||
|
disbursementPeriodIds.add(data.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return disbursementDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasMultipleTranchesOnSameDate(Collection<DisbursementData> disbursementData) {
|
||||||
|
if (disbursementData == null || disbursementData.size() <= 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return disbursementData.stream().collect(Collectors.groupingBy(DisbursementData::disbursementDate, Collectors.counting())).values()
|
||||||
|
.stream().anyMatch(count -> count > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanSchedulePeriodDataWrapper> collectEligibleCapitalizedIncomeData(
|
||||||
|
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, 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 BigDecimal fillLoanSchedulePeriodData(List<LoanSchedulePeriodData> periods,
|
||||||
|
List<LoanSchedulePeriodDataWrapper> combinedDataList, BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount,
|
||||||
|
BigDecimal outstandingLoanPrincipalBalance) {
|
||||||
|
// 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);
|
||||||
|
outstandingLoanPrincipalBalance = outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
||||||
|
}
|
||||||
|
return outstandingLoanPrincipalBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
: disbursementChargeAmount.add(data.getChargeAmount()).subtract(waivedChargeAmount);
|
||||||
|
return LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(), chargeAmount,
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+8
@@ -90,6 +90,14 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
|
|||||||
handleChangedDetail(changedTransactionDetail);
|
handleChangedDetail(changedTransactionDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reprocessTransactionsWithoutChecks(final Loan loan, final LocalDate transactionDate,
|
||||||
|
final List<LoanTransaction> newTransactions) {
|
||||||
|
final List<LoanTransaction> transactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
|
||||||
|
transactions.addAll(newTransactions);
|
||||||
|
reprocessTransactionsAndFetchChangedTransactions(loan, transactions);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processPostDisbursementTransactions(final Loan loan) {
|
public void processPostDisbursementTransactions(final Loan loan) {
|
||||||
loanTransactionProcessingService.processPostDisbursementTransactions(loan).ifPresent(this::handleChangedDetail);
|
loanTransactionProcessingService.processPostDisbursementTransactions(loan).ifPresent(this::handleChangedDetail);
|
||||||
|
|||||||
+131
-33
@@ -22,10 +22,13 @@ import static java.math.BigDecimal.ZERO;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
@@ -33,6 +36,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanReAgeBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanReAgeBusinessEvent;
|
||||||
@@ -40,14 +44,20 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.Loa
|
|||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
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.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
|
||||||
@@ -56,8 +66,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanRepaymentScheduleService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||||
@@ -69,7 +83,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional
|
@Transactional
|
||||||
public class LoanReAgingServiceImpl {
|
public class LoanReAgingService {
|
||||||
|
|
||||||
private final LoanAssembler loanAssembler;
|
private final LoanAssembler loanAssembler;
|
||||||
private final LoanReAgingValidator reAgingValidator;
|
private final LoanReAgingValidator reAgingValidator;
|
||||||
@@ -83,40 +97,22 @@ public class LoanReAgingServiceImpl {
|
|||||||
private final LoanScheduleService loanScheduleService;
|
private final LoanScheduleService loanScheduleService;
|
||||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||||
private final CodeValueRepository codeValueRepository;
|
private final CodeValueRepository codeValueRepository;
|
||||||
|
private final LoanRepaymentScheduleService loanRepaymentScheduleService;
|
||||||
|
private final LoanReadPlatformService loanReadPlatformService;
|
||||||
|
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||||
|
|
||||||
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
|
public CommandProcessingResult reAge(final Long loanId, final JsonCommand command) {
|
||||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
final Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
reAgingValidator.validateReAge(loan, command);
|
reAgingValidator.validateReAge(loan, command);
|
||||||
|
|
||||||
Map<String, Object> changes = new LinkedHashMap<>();
|
final LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command);
|
||||||
|
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
||||||
|
processReAgeTransaction(loan, reAgeTransaction, true);
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
|
||||||
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
changes.put(LoanReAgingApiConstants.localeParameterName, command.locale());
|
changes.put(LoanReAgingApiConstants.localeParameterName, command.locale());
|
||||||
changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat());
|
changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat());
|
||||||
|
|
||||||
LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command);
|
|
||||||
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
|
||||||
reAgeTransaction.setLoanReAgeParameter(reAgeParameter);
|
|
||||||
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
|
||||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
|
||||||
.determineProcessor(loan.transactionProcessingStrategy());
|
|
||||||
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())
|
|
||||||
&& !loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
|
||||||
reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan, reAgeTransaction.getTransactionDate());
|
|
||||||
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
|
||||||
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|
|
||||||
|| loan.hasContractTerminationTransaction()
|
|
||||||
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
|
|
||||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
|
|
||||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
|
||||||
}
|
|
||||||
final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
|
|
||||||
loanTransactions.add(reAgeTransaction);
|
|
||||||
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
|
|
||||||
} else {
|
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
|
|
||||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
|
||||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
|
||||||
}
|
|
||||||
loan.updateLoanScheduleDependentDerivedFields();
|
|
||||||
persistNote(loan, command, changes);
|
persistNote(loan, command, changes);
|
||||||
|
|
||||||
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
||||||
@@ -133,9 +129,38 @@ public class LoanReAgingServiceImpl {
|
|||||||
.with(changes).build();
|
.with(changes).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public LoanScheduleData previewReAge(final Long loanId, final String loanExternalId, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
final Loan loan = loanId != null ? loanAssembler.assembleFrom(loanId)
|
||||||
|
: loanAssembler.assembleFrom(ExternalIdFactory.produce(loanExternalId), false);
|
||||||
|
return previewReAge(loan, reAgePreviewRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanScheduleData previewReAge(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
reAgingValidator.validateReAge(loan, reAgePreviewRequest);
|
||||||
|
|
||||||
|
final LoanTransaction reAgeTransaction = createReAgeTransactionFromPreviewRequest(loan, reAgePreviewRequest);
|
||||||
|
processReAgeTransaction(loan, reAgeTransaction, false);
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
|
||||||
|
final CurrencyData currencyData = new CurrencyData(loan.getCurrencyCode(), null, loan.getCurrency().getDigitsAfterDecimal(),
|
||||||
|
loan.getCurrency().getInMultiplesOf(), null, null);
|
||||||
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData = new RepaymentScheduleRelatedLoanData(
|
||||||
|
loan.getDisbursementDate(), loan.getDisbursementDate(), currencyData, loan.getPrincipal().getAmount(),
|
||||||
|
loan.getInArrearsTolerance().getAmount(), ZERO);
|
||||||
|
final Collection<DisbursementData> disbursementData = loanReadPlatformService.retrieveLoanDisbursementDetails(loan.getId());
|
||||||
|
final Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
|
||||||
|
.findRepaymentPeriodDataByLoanId(loan.getId());
|
||||||
|
final List<LoanRepaymentScheduleInstallment> sortedInstallments = loan.getRepaymentScheduleInstallments().stream()
|
||||||
|
.sorted(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return loanRepaymentScheduleService.extractLoanScheduleData(sortedInstallments, repaymentScheduleRelatedLoanData, disbursementData,
|
||||||
|
capitalizedIncomeData, loan.isInterestRecalculationEnabled(), loan.getLoanProductRelatedDetail().getLoanScheduleType());
|
||||||
|
}
|
||||||
|
|
||||||
public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) {
|
public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) {
|
||||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
reAgingValidator.validateUndoReAge(loan, command);
|
reAgingValidator.validateUndoReAge(loan);
|
||||||
|
|
||||||
Map<String, Object> changes = new LinkedHashMap<>();
|
Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
changes.put(LoanReAgingApiConstants.localeParameterName, command.locale());
|
changes.put(LoanReAgingApiConstants.localeParameterName, command.locale());
|
||||||
@@ -170,6 +195,35 @@ public class LoanReAgingServiceImpl {
|
|||||||
.with(changes).build();
|
.with(changes).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processReAgeTransaction(final Loan loan, final LoanTransaction reAgeTransaction, final boolean withPostTransactionChecks) {
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
|
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())
|
||||||
|
&& !loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||||
|
if (withPostTransactionChecks) {
|
||||||
|
reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan,
|
||||||
|
reAgeTransaction.getTransactionDate());
|
||||||
|
} else {
|
||||||
|
reprocessLoanTransactionsService.reprocessTransactionsWithoutChecks(loan, reAgeTransaction.getTransactionDate(),
|
||||||
|
List.of(reAgeTransaction));
|
||||||
|
}
|
||||||
|
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||||
|
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|
||||||
|
|| loan.hasContractTerminationTransaction()
|
||||||
|
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
|
||||||
|
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
|
||||||
|
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||||
|
}
|
||||||
|
final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
|
||||||
|
loanTransactions.add(reAgeTransaction);
|
||||||
|
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
|
||||||
|
} else {
|
||||||
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
|
||||||
|
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||||
|
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void reverseReAgeTransaction(LoanTransaction reAgeTransaction, JsonCommand command) {
|
private void reverseReAgeTransaction(LoanTransaction reAgeTransaction, JsonCommand command) {
|
||||||
ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, LoanReAgingApiConstants.externalIdParameterName);
|
ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, LoanReAgingApiConstants.externalIdParameterName);
|
||||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAgeTransaction.getLoan(), reAgeTransaction,
|
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAgeTransaction.getLoan(), reAgeTransaction,
|
||||||
@@ -199,8 +253,13 @@ public class LoanReAgingServiceImpl {
|
|||||||
Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
|
Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
|
||||||
BigDecimal txPrincipalAmount = txPrincipal.getAmount();
|
BigDecimal txPrincipalAmount = txPrincipal.getAmount();
|
||||||
|
|
||||||
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE, transactionDate, txPrincipalAmount, txPrincipalAmount,
|
final LoanTransaction reAgeTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE, transactionDate,
|
||||||
ZERO, ZERO, ZERO, null, false, null, txExternalId);
|
txPrincipalAmount, txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
|
||||||
|
|
||||||
|
final LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
||||||
|
reAgeTransaction.setLoanReAgeParameter(reAgeParameter);
|
||||||
|
|
||||||
|
return reAgeTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
|
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
|
||||||
@@ -235,4 +294,43 @@ public class LoanReAgingServiceImpl {
|
|||||||
this.noteRepository.saveAndFlush(newNote);
|
this.noteRepository.saveAndFlush(newNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoanTransaction createReAgeTransactionFromPreviewRequest(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
||||||
|
final Locale locale = reAgePreviewRequest.getLocale() != null ? Locale.forLanguageTag(reAgePreviewRequest.getLocale())
|
||||||
|
: Locale.getDefault();
|
||||||
|
final LocalDate startDate = JsonParserHelper.convertFrom(reAgePreviewRequest.getStartDate(), LoanReAgingApiConstants.startDate,
|
||||||
|
reAgePreviewRequest.getDateFormat(), locale);
|
||||||
|
if (transactionDate.isAfter(startDate)) {
|
||||||
|
transactionDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
|
||||||
|
final BigDecimal txPrincipalAmount = txPrincipal.getAmount();
|
||||||
|
|
||||||
|
final LoanTransaction reAgeTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE, transactionDate,
|
||||||
|
txPrincipalAmount, txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, null);
|
||||||
|
|
||||||
|
final LoanReAgeParameter reAgeParameter = createReAgeParameterFromPreviewRequest(reAgeTransaction, reAgePreviewRequest);
|
||||||
|
reAgeTransaction.setLoanReAgeParameter(reAgeParameter);
|
||||||
|
|
||||||
|
return reAgeTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanReAgeParameter createReAgeParameterFromPreviewRequest(final LoanTransaction reAgeTransaction,
|
||||||
|
final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
final PeriodFrequencyType periodFrequencyType = PeriodFrequencyType.valueOf(reAgePreviewRequest.getFrequencyType());
|
||||||
|
final Locale locale = reAgePreviewRequest.getLocale() != null ? Locale.forLanguageTag(reAgePreviewRequest.getLocale())
|
||||||
|
: Locale.getDefault();
|
||||||
|
final LocalDate startDate = JsonParserHelper.convertFrom(reAgePreviewRequest.getStartDate(), LoanReAgingApiConstants.startDate,
|
||||||
|
reAgePreviewRequest.getDateFormat(), locale);
|
||||||
|
final Integer numberOfInstallments = reAgePreviewRequest.getNumberOfInstallments();
|
||||||
|
final Integer periodFrequencyNumber = reAgePreviewRequest.getFrequencyNumber();
|
||||||
|
|
||||||
|
final LoanReAgeInterestHandlingType reAgeInterestHandlingType = LoanReAgeInterestHandlingType.DEFAULT;
|
||||||
|
|
||||||
|
return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType, periodFrequencyNumber, startDate, numberOfInstallments,
|
||||||
|
reAgeInterestHandlingType, null);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+48
-1
@@ -24,6 +24,7 @@ import java.time.LocalDate;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
@@ -33,8 +34,10 @@ import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
|||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
@@ -57,6 +60,12 @@ public class LoanReAgingValidator {
|
|||||||
validateReAgeOutstandingBalance(loan, command);
|
validateReAgeOutstandingBalance(loan, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void validateReAge(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
validateReAgeRequest(loan, reAgePreviewRequest);
|
||||||
|
validateReAgeBusinessRules(loan);
|
||||||
|
validateReAgeOutstandingBalance(loan, reAgePreviewRequest);
|
||||||
|
}
|
||||||
|
|
||||||
private void validateReAgeRequest(Loan loan, JsonCommand command) {
|
private void validateReAgeRequest(Loan loan, JsonCommand command) {
|
||||||
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.reAge");
|
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.reAge");
|
||||||
@@ -145,7 +154,7 @@ public class LoanReAgingValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateUndoReAge(Loan loan, JsonCommand command) {
|
public void validateUndoReAge(Loan loan) {
|
||||||
validateUndoReAgeBusinessRules(loan);
|
validateUndoReAgeBusinessRules(loan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,4 +190,42 @@ public class LoanReAgingValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateReAgeRequest(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.reAge");
|
||||||
|
|
||||||
|
final Locale locale = reAgePreviewRequest.getLocale() != null ? Locale.forLanguageTag(reAgePreviewRequest.getLocale())
|
||||||
|
: Locale.getDefault();
|
||||||
|
final LocalDate startDate = JsonParserHelper.convertFrom(reAgePreviewRequest.getStartDate(), LoanReAgingApiConstants.startDate,
|
||||||
|
reAgePreviewRequest.getDateFormat(), locale);
|
||||||
|
|
||||||
|
if (loan.isProgressiveSchedule()) {
|
||||||
|
baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate)
|
||||||
|
.validateDateAfterOrEqual(loan.getDisbursementDate());
|
||||||
|
} else {
|
||||||
|
baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate)
|
||||||
|
.validateDateAfter(loan.getMaturityDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationErrorsExist(dataValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateReAgeOutstandingBalance(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||||
|
final LocalDate businessDate = getBusinessLocalDate();
|
||||||
|
Locale locale = reAgePreviewRequest.getLocale() != null ? Locale.forLanguageTag(reAgePreviewRequest.getLocale())
|
||||||
|
: Locale.getDefault();
|
||||||
|
final LocalDate startDate = JsonParserHelper.convertFrom(reAgePreviewRequest.getStartDate(), LoanReAgingApiConstants.startDate,
|
||||||
|
reAgePreviewRequest.getDateFormat(), locale);
|
||||||
|
|
||||||
|
final boolean isBackdated = businessDate.isAfter(startDate);
|
||||||
|
if (isBackdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loan.getSummary().getTotalPrincipalOutstanding().compareTo(java.math.BigDecimal.ZERO) == 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.no.outstanding.balance.to.reage",
|
||||||
|
"Loan cannot be re-aged as there are no outstanding balances to be re-aged", loan.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -152,6 +152,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanRepaymentScheduleService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
|
||||||
@@ -350,7 +351,8 @@ public class LoanAccountConfiguration {
|
|||||||
LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService,
|
LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService,
|
||||||
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
|
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
|
||||||
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
|
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
|
||||||
@Lazy InterestRefundServiceDelegate interestRefundServiceDelegate, LoanMaximumAmountCalculator loanMaximumAmountCalculator) {
|
@Lazy InterestRefundServiceDelegate interestRefundServiceDelegate, LoanMaximumAmountCalculator loanMaximumAmountCalculator,
|
||||||
|
LoanRepaymentScheduleService loanRepaymentScheduleService) {
|
||||||
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
|
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
|
||||||
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
|
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
|
||||||
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
|
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
|
||||||
@@ -359,7 +361,7 @@ public class LoanAccountConfiguration {
|
|||||||
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
|
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
|
||||||
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService,
|
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService,
|
||||||
loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository, interestRefundServiceDelegate,
|
loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository, interestRefundServiceDelegate,
|
||||||
loanMaximumAmountCalculator);
|
loanMaximumAmountCalculator, loanRepaymentScheduleService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* 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.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import jakarta.validation.Validation;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||||
|
import org.hibernate.validator.HibernateValidator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@SpringBootTest
|
||||||
|
@ContextConfiguration(classes = { ReAgePreviewRequestValidationTest.TestConfig.class })
|
||||||
|
class ReAgePreviewRequestValidationTest {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Import({ MessageSourceAutoConfiguration.class })
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Validator validator() {
|
||||||
|
return Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory().getValidator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidAllBlank() {
|
||||||
|
var params = ReAgePreviewRequest.builder().build();
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(6);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyNumber"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyType"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("startDate"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("numberOfInstallments"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("dateFormat"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("locale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyNumberNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyNumber(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyNumber"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'frequencyNumber' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyNumberZero() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyNumber(0);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyNumber"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'frequencyNumber' must be at least 1."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyNumberNegative() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyNumber(-1);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyNumber"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'frequencyNumber' must be at least 1."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyTypeNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyType"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'frequencyType' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyTypeEmpty() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType("");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyType"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'frequencyType' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyTypeBlank() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType(" ");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFrequencyTypeInvalidEnum() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType("NOT_A_VALID_ENUM");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("frequencyType"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage()
|
||||||
|
.equals("The parameter 'frequencyType' must be valid PeriodFrequencyType value. Provided value: 'NOT_A_VALID_ENUM'."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidStartDateNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setStartDate(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("startDate"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'startDate' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidStartDateEmpty() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setStartDate("");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("startDate"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'startDate' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidStartDateBlank() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setStartDate(" ");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("startDate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidStartDateFormat() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setStartDate("2025-05-12");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("Wrong local date fields."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidNumberOfInstallmentsNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setNumberOfInstallments(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("numberOfInstallments"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'numberOfInstallments' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidNumberOfInstallmentsZero() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setNumberOfInstallments(0);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("numberOfInstallments"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'numberOfInstallments' must be at least 1."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidNumberOfInstallmentsNegative() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setNumberOfInstallments(-1);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("numberOfInstallments"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'numberOfInstallments' must be at least 1."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidDateFormatNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setDateFormat(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("dateFormat"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'dateFormat' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidDateFormatEmpty() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setDateFormat("");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("dateFormat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidDateFormatMismatch() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setDateFormat("yyyy-MM-dd");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("Wrong local date fields."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidLocaleNull() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setLocale(null);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("locale"));
|
||||||
|
assertThat(errors).anyMatch(e -> e.getMessage().equals("The parameter 'locale' is mandatory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidLocaleEmpty() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setLocale("");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("locale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidLocaleInvalidFormat() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setLocale("invalid-locale");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).hasSize(1);
|
||||||
|
assertThat(errors).anyMatch(e -> e.getPropertyPath().toString().equals("locale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validAllParams() {
|
||||||
|
var params = validParams();
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validParamsWithDaysFrequency() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType("DAYS");
|
||||||
|
params.setFrequencyNumber(30);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validParamsWithWeeksFrequency() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType("WEEKS");
|
||||||
|
params.setFrequencyNumber(4);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validParamsWithYearsFrequency() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setFrequencyType("YEARS");
|
||||||
|
params.setFrequencyNumber(1);
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validParamsWithAlternativeLocale() {
|
||||||
|
var params = validParams();
|
||||||
|
params.setLocale("en_US");
|
||||||
|
|
||||||
|
var errors = validator.validate(params);
|
||||||
|
|
||||||
|
assertThat(errors).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReAgePreviewRequest validParams() {
|
||||||
|
return ReAgePreviewRequest.builder().frequencyNumber(1).frequencyType("MONTHS").startDate("12-05-2025").numberOfInstallments(6)
|
||||||
|
.dateFormat("dd-MM-yyyy").locale("en").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
-13
@@ -113,7 +113,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.externalId.exceeds.max.length");
|
.isEqualTo("validation.msg.loan.reAge.externalId.exceeds.max.length");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.startDate.cannot.be.blank");
|
.isEqualTo("validation.msg.loan.reAge.startDate.cannot.be.blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.frequencyType.cannot.be.blank");
|
.isEqualTo("validation.msg.loan.reAge.frequencyType.cannot.be.blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.frequencyNumber.cannot.be.blank");
|
.isEqualTo("validation.msg.loan.reAge.frequencyNumber.cannot.be.blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.frequencyNumber.not.greater.than.zero");
|
.isEqualTo("validation.msg.loan.reAge.frequencyNumber.not.greater.than.zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.cannot.be.blank");
|
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.cannot.be.blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
|
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
|
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ class LoanReAgingValidatorTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||||
assertThat(result.getErrors().get(0).getUserMessageGlobalisationCode())
|
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||||
.isEqualTo("validation.msg.loan.reAge.startDate.is.less.than.date");
|
.isEqualTo("validation.msg.loan.reAge.startDate.is.less.than.date");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,10 +371,9 @@ class LoanReAgingValidatorTest {
|
|||||||
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(3)));
|
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(3)));
|
||||||
Loan loan = loan();
|
Loan loan = loan();
|
||||||
given(loan.getLoanTransactions()).willReturn(transactions);
|
given(loan.getLoanTransactions()).willReturn(transactions);
|
||||||
JsonCommand command = jsonCommand();
|
|
||||||
// when
|
// when
|
||||||
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
|
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
|
||||||
() -> underTest.validateUndoReAge(loan, command));
|
() -> underTest.validateUndoReAge(loan));
|
||||||
// then
|
// then
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.reaging.transaction.missing");
|
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.reaging.transaction.missing");
|
||||||
@@ -390,9 +389,8 @@ class LoanReAgingValidatorTest {
|
|||||||
OffsetDateTime.of(actualDate, LocalTime.of(9, 0), ZoneOffset.UTC)));
|
OffsetDateTime.of(actualDate, LocalTime.of(9, 0), ZoneOffset.UTC)));
|
||||||
Loan loan = loan();
|
Loan loan = loan();
|
||||||
given(loan.getLoanTransactions()).willReturn(transactions);
|
given(loan.getLoanTransactions()).willReturn(transactions);
|
||||||
JsonCommand command = jsonCommand();
|
|
||||||
// when
|
// when
|
||||||
underTest.validateUndoReAge(loan, command);
|
underTest.validateUndoReAge(loan);
|
||||||
// then no exception thrown
|
// then no exception thrown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,3 +43,13 @@ org.apache.fineract.cache.cache-type.not-null=The parameter 'cacheType' is manda
|
|||||||
|
|
||||||
org.apache.fineract.organisation.monetary.currencies.not-null=The parameter 'currencies' is mandatory.
|
org.apache.fineract.organisation.monetary.currencies.not-null=The parameter 'currencies' is mandatory.
|
||||||
org.apache.fineract.organisation.monetary.currencies.not-empty=The parameter 'currencies' cannot be empty.
|
org.apache.fineract.organisation.monetary.currencies.not-empty=The parameter 'currencies' cannot be empty.
|
||||||
|
|
||||||
|
# Re-Age
|
||||||
|
|
||||||
|
org.apache.fineract.reage.frequency-number.not-blank=The parameter 'frequencyNumber' is mandatory.
|
||||||
|
org.apache.fineract.reage.frequency-number.min=The parameter 'frequencyNumber' must be at least 1.
|
||||||
|
org.apache.fineract.reage.frequency-type.not-blank=The parameter 'frequencyType' is mandatory.
|
||||||
|
org.apache.fineract.reage.start-date.not-blank=The parameter 'startDate' is mandatory.
|
||||||
|
org.apache.fineract.reage.number-of-installments.not-blank=The parameter 'numberOfInstallments' is mandatory.
|
||||||
|
org.apache.fineract.reage.number-of-installments.min=The parameter 'numberOfInstallments' must be at least 1.
|
||||||
|
org.apache.fineract.frequency-type.invalid=The parameter 'frequencyType' must be valid PeriodFrequencyType value. Provided value: '${validatedValue}'.
|
||||||
|
|||||||
Reference in New Issue
Block a user