Skip to content

Commit 3efafa7

Browse files
oleksii-novikov-onixadamsaghy
authored andcommitted
FINERACT-2354: Introduce reaging preview API
1 parent ed8e9d1 commit 3efafa7

File tree

19 files changed

+1772
-446
lines changed

19 files changed

+1772
-446
lines changed

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java

Lines changed: 197 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@
2020

2121
import static org.assertj.core.api.Assertions.assertThat;
2222

23+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2324
import io.cucumber.datatable.DataTable;
2425
import io.cucumber.java.en.Then;
2526
import io.cucumber.java.en.When;
2627
import java.io.IOException;
2728
import java.lang.reflect.Method;
2829
import java.math.BigDecimal;
30+
import java.math.RoundingMode;
31+
import java.time.format.DateTimeFormatter;
32+
import java.util.ArrayList;
2933
import java.util.Arrays;
3034
import java.util.LinkedHashMap;
3135
import java.util.List;
3236
import java.util.Map;
3337
import java.util.Set;
38+
import java.util.stream.Collectors;
39+
import lombok.RequiredArgsConstructor;
3440
import lombok.extern.slf4j.Slf4j;
3541
import okhttp3.ResponseBody;
42+
import org.apache.fineract.client.models.LoanScheduleData;
43+
import org.apache.fineract.client.models.LoanSchedulePeriodData;
3644
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
3745
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
3846
import org.apache.fineract.client.models.PostLoansResponse;
@@ -41,22 +49,23 @@
4149
import org.apache.fineract.test.helper.ErrorHelper;
4250
import org.apache.fineract.test.helper.ErrorMessageHelper;
4351
import org.apache.fineract.test.helper.ErrorResponse;
52+
import org.apache.fineract.test.helper.Utils;
4453
import org.apache.fineract.test.messaging.EventAssertion;
4554
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
4655
import org.apache.fineract.test.stepdef.AbstractStepDef;
4756
import org.apache.fineract.test.support.TestContextKey;
4857
import org.junit.jupiter.api.Assertions;
49-
import org.springframework.beans.factory.annotation.Autowired;
5058
import retrofit2.Response;
5159

5260
@Slf4j
61+
@RequiredArgsConstructor
5362
public class LoanReAgingStepDef extends AbstractStepDef {
5463

55-
@Autowired
56-
private LoanTransactionsApi loanTransactionsApi;
64+
private static final String DATE_FORMAT = "dd MMMM yyyy";
65+
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
5766

58-
@Autowired
59-
private EventAssertion eventAssertion;
67+
private final LoanTransactionsApi loanTransactionsApi;
68+
private final EventAssertion eventAssertion;
6069

6170
@When("Admin creates a Loan re-aging transaction with the following data:")
6271
public void createReAgingTransaction(DataTable table) throws IOException {
@@ -302,4 +311,187 @@ public void reAgeContractTerminatedLoanFailure(final DataTable table) throws IOE
302311
assertThat(errorDetails.getHttpStatusCode()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(403);
303312
assertThat(developerMessage).matches(ErrorMessageHelper.reAgeContractTerminatedLoanFailure());
304313
}
314+
315+
@When("Admin creates a Loan re-aging preview with the following data:")
316+
public void createReAgingPreview(DataTable table) throws IOException {
317+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
318+
long loanId = loanResponse.body().getLoanId();
319+
320+
List<String> data = table.asLists().get(1);
321+
int frequencyNumber = Integer.parseInt(data.get(0));
322+
String frequencyType = data.get(1);
323+
String startDate = data.get(2);
324+
int numberOfInstallments = Integer.parseInt(data.get(3));
325+
326+
Response<LoanScheduleData> response = loanTransactionsApi
327+
.previewReAgeSchedule(loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments, DATE_FORMAT, "en").execute();
328+
ErrorHelper.checkSuccessfulApiCall(response);
329+
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
330+
331+
log.info(
332+
"Re-aging preview created for loan ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
333+
loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
334+
}
335+
336+
@When("Admin creates a Loan re-aging preview by Loan external ID with the following data:")
337+
public void createReAgingPreviewByLoanExternalId(DataTable table) throws IOException {
338+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
339+
String loanExternalId = loanResponse.body().getResourceExternalId();
340+
341+
List<String> data = table.asLists().get(1);
342+
int frequencyNumber = Integer.parseInt(data.get(0));
343+
String frequencyType = data.get(1);
344+
String startDate = data.get(2);
345+
int numberOfInstallments = Integer.parseInt(data.get(3));
346+
347+
Response<LoanScheduleData> response = loanTransactionsApi
348+
.previewReAgeSchedule1(loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments, DATE_FORMAT, "en")
349+
.execute();
350+
ErrorHelper.checkSuccessfulApiCall(response);
351+
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
352+
353+
log.info(
354+
"Re-aging preview created for loan external ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
355+
loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
356+
}
357+
358+
@Then("Loan Repayment schedule preview has {int} periods, with the following data for periods:")
359+
public void loanRepaymentSchedulePreviewPeriodsCheck(int linesExpected, DataTable table) {
360+
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
361+
362+
List<LoanSchedulePeriodData> repaymentPeriods = scheduleResponse.body().getPeriods();
363+
364+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
365+
String resourceId = String.valueOf(loanResponse.body().getLoanId());
366+
367+
List<List<String>> data = table.asLists();
368+
int nrLines = data.size();
369+
int linesActual = (int) repaymentPeriods.stream().filter(r -> r.getPeriod() != null).count();
370+
for (int i = 1; i < nrLines; i++) {
371+
List<String> expectedValues = data.get(i);
372+
String dueDateExpected = expectedValues.get(2);
373+
374+
List<List<String>> actualValuesList = repaymentPeriods.stream()
375+
.filter(r -> dueDateExpected.equals(FORMATTER.format(r.getDueDate())))
376+
.map(r -> fetchValuesOfRepaymentSchedule(data.get(0), r)).collect(Collectors.toList());
377+
378+
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
379+
assertThat(containsExpectedValues)
380+
.as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(resourceId, i, actualValuesList, expectedValues)).isTrue();
381+
382+
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(resourceId, linesActual, linesExpected))
383+
.isEqualTo(linesExpected);
384+
}
385+
}
386+
387+
@Then("Loan Repayment schedule preview has the following data in Total row:")
388+
public void loanRepaymentScheduleAmountCheck(DataTable table) {
389+
List<List<String>> data = table.asLists();
390+
List<String> header = data.get(0);
391+
List<String> expectedValues = data.get(1);
392+
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
393+
validateRepaymentScheduleTotal(header, scheduleResponse.body(), expectedValues);
394+
}
395+
396+
private List<String> fetchValuesOfRepaymentSchedule(List<String> header, LoanSchedulePeriodData repaymentPeriod) {
397+
List<String> actualValues = new ArrayList<>();
398+
for (String headerName : header) {
399+
switch (headerName) {
400+
case "Nr" -> actualValues.add(repaymentPeriod.getPeriod() == null ? null : String.valueOf(repaymentPeriod.getPeriod()));
401+
case "Days" ->
402+
actualValues.add(repaymentPeriod.getDaysInPeriod() == null ? null : String.valueOf(repaymentPeriod.getDaysInPeriod()));
403+
case "Date" ->
404+
actualValues.add(repaymentPeriod.getDueDate() == null ? null : FORMATTER.format(repaymentPeriod.getDueDate()));
405+
case "Paid date" -> actualValues.add(repaymentPeriod.getObligationsMetOnDate() == null ? null
406+
: FORMATTER.format(repaymentPeriod.getObligationsMetOnDate()));
407+
case "Balance of loan" -> actualValues.add(repaymentPeriod.getPrincipalLoanBalanceOutstanding() == null ? null
408+
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalLoanBalanceOutstanding().doubleValue()).format());
409+
case "Principal due" -> actualValues.add(repaymentPeriod.getPrincipalDue() == null ? null
410+
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalDue().doubleValue()).format());
411+
case "Interest" -> actualValues.add(repaymentPeriod.getInterestDue() == null ? null
412+
: new Utils.DoubleFormatter(repaymentPeriod.getInterestDue().doubleValue()).format());
413+
case "Fees" -> actualValues.add(repaymentPeriod.getFeeChargesDue() == null ? null
414+
: new Utils.DoubleFormatter(repaymentPeriod.getFeeChargesDue().doubleValue()).format());
415+
case "Penalties" -> actualValues.add(repaymentPeriod.getPenaltyChargesDue() == null ? null
416+
: new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue().doubleValue()).format());
417+
case "Due" -> actualValues.add(repaymentPeriod.getTotalDueForPeriod() == null ? null
418+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalDueForPeriod().doubleValue()).format());
419+
case "Paid" -> actualValues.add(repaymentPeriod.getTotalPaidForPeriod() == null ? null
420+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidForPeriod().doubleValue()).format());
421+
case "In advance" -> actualValues.add(repaymentPeriod.getTotalPaidInAdvanceForPeriod() == null ? null
422+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidInAdvanceForPeriod().doubleValue()).format());
423+
case "Late" -> actualValues.add(repaymentPeriod.getTotalPaidLateForPeriod() == null ? null
424+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidLateForPeriod().doubleValue()).format());
425+
case "Waived" -> actualValues.add(repaymentPeriod.getTotalWaivedForPeriod() == null ? null
426+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalWaivedForPeriod().doubleValue()).format());
427+
case "Outstanding" -> actualValues.add(repaymentPeriod.getTotalOutstandingForPeriod() == null ? null
428+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod().doubleValue()).format());
429+
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
430+
}
431+
}
432+
return actualValues;
433+
}
434+
435+
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
436+
private List<String> validateRepaymentScheduleTotal(List<String> header, LoanScheduleData repaymentSchedule,
437+
List<String> expectedAmounts) {
438+
List<String> actualValues = new ArrayList<>();
439+
Double paidActual = 0.0;
440+
List<LoanSchedulePeriodData> periods = repaymentSchedule.getPeriods();
441+
for (LoanSchedulePeriodData period : periods) {
442+
if (null != period.getTotalPaidForPeriod()) {
443+
paidActual += period.getTotalPaidForPeriod().doubleValue();
444+
}
445+
}
446+
BigDecimal paidActualBd = new BigDecimal(paidActual).setScale(2, RoundingMode.HALF_DOWN);
447+
448+
for (int i = 0; i < header.size(); i++) {
449+
String headerName = header.get(i);
450+
String expectedValue = expectedAmounts.get(i);
451+
switch (headerName) {
452+
case "Principal due" -> assertThat(repaymentSchedule.getTotalPrincipalExpected().doubleValue())//
453+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePrincipal(
454+
repaymentSchedule.getTotalPrincipalExpected().doubleValue(), Double.valueOf(expectedValue)))//
455+
.isEqualTo(Double.valueOf(expectedValue));//
456+
case "Interest" -> assertThat(repaymentSchedule.getTotalInterestCharged().doubleValue())//
457+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInterest(
458+
repaymentSchedule.getTotalInterestCharged().doubleValue(), Double.valueOf(expectedValue)))//
459+
.isEqualTo(Double.valueOf(expectedValue));//
460+
case "Fees" -> assertThat(repaymentSchedule.getTotalFeeChargesCharged().doubleValue())//
461+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleFees(
462+
repaymentSchedule.getTotalFeeChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
463+
.isEqualTo(Double.valueOf(expectedValue));//
464+
case "Penalties" -> assertThat(repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue())//
465+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePenalties(
466+
repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
467+
.isEqualTo(Double.valueOf(expectedValue));//
468+
case "Due" -> assertThat(repaymentSchedule.getTotalRepaymentExpected().doubleValue())//
469+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleDue(
470+
repaymentSchedule.getTotalRepaymentExpected().doubleValue(), Double.valueOf(expectedValue)))//
471+
.isEqualTo(Double.valueOf(expectedValue));//
472+
case "Paid" -> assertThat(paidActualBd.doubleValue())//
473+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePaid(paidActualBd.doubleValue(),
474+
Double.valueOf(expectedValue)))//
475+
.isEqualTo(Double.valueOf(expectedValue));//
476+
case "In advance" -> assertThat(repaymentSchedule.getTotalPaidInAdvance().doubleValue())//
477+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInAdvance(
478+
repaymentSchedule.getTotalPaidInAdvance().doubleValue(), Double.valueOf(expectedValue)))//
479+
.isEqualTo(Double.valueOf(expectedValue));//
480+
case "Late" -> assertThat(repaymentSchedule.getTotalPaidLate().doubleValue())//
481+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleLate(repaymentSchedule.getTotalPaidLate().doubleValue(),
482+
Double.valueOf(expectedValue)))//
483+
.isEqualTo(Double.valueOf(expectedValue));//
484+
case "Waived" -> assertThat(repaymentSchedule.getTotalWaived().doubleValue())//
485+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleWaived(repaymentSchedule.getTotalWaived().doubleValue(),
486+
Double.valueOf(expectedValue)))//
487+
.isEqualTo(Double.valueOf(expectedValue));//
488+
case "Outstanding" -> assertThat(repaymentSchedule.getTotalOutstanding().doubleValue())//
489+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleOutstanding(
490+
repaymentSchedule.getTotalOutstanding().doubleValue(), Double.valueOf(expectedValue)))//
491+
.isEqualTo(Double.valueOf(expectedValue));//
492+
}
493+
}
494+
return actualValues;
495+
}
496+
305497
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public abstract class TestContextKey {
4848
public static final String LOAN_REFUND_RESPONSE = "loanRefundResponse";
4949
public static final String LOAN_REAGING_RESPONSE = "loanReAgingResponse";
5050
public static final String LOAN_REAGING_UNDO_RESPONSE = "loanReAgingUndoResponse";
51+
public static final String LOAN_REAGING_PREVIEW_RESPONSE = "loanReAgingPreviewResponse";
5152
public static final String LOAN_REAMORTIZATION_RESPONSE = "loanReAmortizationResponse";
5253
public static final String LOAN_REAMORTIZATION_UNDO_RESPONSE = "loanReAmortizationUndoResponse";
5354
public static final String BUSINESS_DATE_RESPONSE = "businessDateResponse";

0 commit comments

Comments
 (0)