Skip to content

Commit 1aaeab6

Browse files
FINERACT-2354: Introduce reaging preview API
1 parent b32837e commit 1aaeab6

File tree

12 files changed

+1240
-415
lines changed

12 files changed

+1240
-415
lines changed

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

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,29 @@
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;
28+
import java.math.BigDecimal;
29+
import java.math.RoundingMode;
30+
import java.time.format.DateTimeFormatter;
31+
import java.util.ArrayList;
2732
import java.util.List;
33+
import java.util.stream.Collectors;
2834
import lombok.extern.slf4j.Slf4j;
2935
import okhttp3.ResponseBody;
36+
import org.apache.fineract.client.models.LoanScheduleData;
37+
import org.apache.fineract.client.models.LoanSchedulePeriodData;
3038
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
3139
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
3240
import org.apache.fineract.client.models.PostLoansResponse;
3341
import org.apache.fineract.client.services.LoanTransactionsApi;
3442
import org.apache.fineract.test.factory.LoanRequestFactory;
3543
import org.apache.fineract.test.helper.ErrorHelper;
44+
import org.apache.fineract.test.helper.ErrorMessageHelper;
45+
import org.apache.fineract.test.helper.Utils;
3646
import org.apache.fineract.test.messaging.EventAssertion;
3747
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
3848
import org.apache.fineract.test.stepdef.AbstractStepDef;
@@ -44,6 +54,9 @@
4454
@Slf4j
4555
public class LoanReAgingStepDef extends AbstractStepDef {
4656

57+
private static final String DATE_FORMAT = "dd MMMM yyyy";
58+
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
59+
4760
@Autowired
4861
private LoanTransactionsApi loanTransactionsApi;
4962

@@ -146,4 +159,197 @@ public void adminFailsToCreateReAgingTransactionWithError(final String expectedE
146159
ErrorHelper.checkFailedApiCall(response, 403);
147160
}
148161

162+
@When("Admin creates a Loan re-aging preview with the following data:")
163+
public void createReAgingPreview(DataTable table) throws IOException {
164+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
165+
long loanId = loanResponse.body().getLoanId();
166+
167+
List<String> data = table.asLists().get(1);
168+
int frequencyNumber = Integer.parseInt(data.get(0));
169+
String frequencyType = data.get(1);
170+
String startDate = data.get(2);
171+
int numberOfInstallments = Integer.parseInt(data.get(3));
172+
173+
PostLoansLoanIdTransactionsRequest reAgingRequest = LoanRequestFactory//
174+
.defaultReAgingRequest()//
175+
.frequencyNumber(frequencyNumber)//
176+
.frequencyType(frequencyType)//
177+
.startDate(startDate)//
178+
.numberOfInstallments(numberOfInstallments);//
179+
180+
Response<LoanScheduleData> response = loanTransactionsApi.previewReAgeSchedule(loanId, reAgingRequest).execute();
181+
ErrorHelper.checkSuccessfulApiCall(response);
182+
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
183+
184+
log.info(
185+
"Re-aging preview created for loan ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
186+
loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
187+
}
188+
189+
@When("Admin creates a Loan re-aging preview by Loan external ID with the following data:")
190+
public void createReAgingPreviewByLoanExternalId(DataTable table) throws IOException {
191+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
192+
String loanExternalId = loanResponse.body().getResourceExternalId();
193+
194+
List<String> data = table.asLists().get(1);
195+
int frequencyNumber = Integer.parseInt(data.get(0));
196+
String frequencyType = data.get(1);
197+
String startDate = data.get(2);
198+
int numberOfInstallments = Integer.parseInt(data.get(3));
199+
200+
PostLoansLoanIdTransactionsRequest reAgingRequest = LoanRequestFactory//
201+
.defaultReAgingRequest()//
202+
.frequencyNumber(frequencyNumber)//
203+
.frequencyType(frequencyType)//
204+
.startDate(startDate)//
205+
.numberOfInstallments(numberOfInstallments);//
206+
207+
Response<LoanScheduleData> response = loanTransactionsApi.previewReAgeSchedule1(loanExternalId, reAgingRequest).execute();
208+
ErrorHelper.checkSuccessfulApiCall(response);
209+
testContext().set(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE, response);
210+
211+
log.info(
212+
"Re-aging preview created for loan external ID: {} with parameters: frequencyNumber={}, frequencyType={}, startDate={}, numberOfInstallments={}",
213+
loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments);
214+
}
215+
216+
@Then("Loan Repayment schedule preview has {int} periods, with the following data for periods:")
217+
public void loanRepaymentSchedulePreviewPeriodsCheck(int linesExpected, DataTable table) {
218+
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
219+
220+
List<LoanSchedulePeriodData> repaymentPeriods = scheduleResponse.body().getPeriods();
221+
222+
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
223+
String resourceId = String.valueOf(loanResponse.body().getLoanId());
224+
225+
List<List<String>> data = table.asLists();
226+
int nrLines = data.size();
227+
int linesActual = (int) repaymentPeriods.stream().filter(r -> r.getPeriod() != null).count();
228+
for (int i = 1; i < nrLines; i++) {
229+
List<String> expectedValues = data.get(i);
230+
String dueDateExpected = expectedValues.get(2);
231+
232+
List<List<String>> actualValuesList = repaymentPeriods.stream()
233+
.filter(r -> dueDateExpected.equals(FORMATTER.format(r.getDueDate())))
234+
.map(r -> fetchValuesOfRepaymentSchedule(data.get(0), r)).collect(Collectors.toList());
235+
236+
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
237+
assertThat(containsExpectedValues)
238+
.as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(resourceId, i, actualValuesList, expectedValues)).isTrue();
239+
240+
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(resourceId, linesActual, linesExpected))
241+
.isEqualTo(linesExpected);
242+
}
243+
}
244+
245+
@Then("Loan Repayment schedule preview has the following data in Total row:")
246+
public void loanRepaymentScheduleAmountCheck(DataTable table) {
247+
List<List<String>> data = table.asLists();
248+
List<String> header = data.get(0);
249+
List<String> expectedValues = data.get(1);
250+
Response<LoanScheduleData> scheduleResponse = testContext().get(TestContextKey.LOAN_REAGING_PREVIEW_RESPONSE);
251+
validateRepaymentScheduleTotal(header, scheduleResponse.body(), expectedValues);
252+
}
253+
254+
private List<String> fetchValuesOfRepaymentSchedule(List<String> header, LoanSchedulePeriodData repaymentPeriod) {
255+
List<String> actualValues = new ArrayList<>();
256+
for (String headerName : header) {
257+
switch (headerName) {
258+
case "Nr" -> actualValues.add(repaymentPeriod.getPeriod() == null ? null : String.valueOf(repaymentPeriod.getPeriod()));
259+
case "Days" ->
260+
actualValues.add(repaymentPeriod.getDaysInPeriod() == null ? null : String.valueOf(repaymentPeriod.getDaysInPeriod()));
261+
case "Date" ->
262+
actualValues.add(repaymentPeriod.getDueDate() == null ? null : FORMATTER.format(repaymentPeriod.getDueDate()));
263+
case "Paid date" -> actualValues.add(repaymentPeriod.getObligationsMetOnDate() == null ? null
264+
: FORMATTER.format(repaymentPeriod.getObligationsMetOnDate()));
265+
case "Balance of loan" -> actualValues.add(repaymentPeriod.getPrincipalLoanBalanceOutstanding() == null ? null
266+
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalLoanBalanceOutstanding().doubleValue()).format());
267+
case "Principal due" -> actualValues.add(repaymentPeriod.getPrincipalDue() == null ? null
268+
: new Utils.DoubleFormatter(repaymentPeriod.getPrincipalDue().doubleValue()).format());
269+
case "Interest" -> actualValues.add(repaymentPeriod.getInterestDue() == null ? null
270+
: new Utils.DoubleFormatter(repaymentPeriod.getInterestDue().doubleValue()).format());
271+
case "Fees" -> actualValues.add(repaymentPeriod.getFeeChargesDue() == null ? null
272+
: new Utils.DoubleFormatter(repaymentPeriod.getFeeChargesDue().doubleValue()).format());
273+
case "Penalties" -> actualValues.add(repaymentPeriod.getPenaltyChargesDue() == null ? null
274+
: new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue().doubleValue()).format());
275+
case "Due" -> actualValues.add(repaymentPeriod.getTotalDueForPeriod() == null ? null
276+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalDueForPeriod().doubleValue()).format());
277+
case "Paid" -> actualValues.add(repaymentPeriod.getTotalPaidForPeriod() == null ? null
278+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidForPeriod().doubleValue()).format());
279+
case "In advance" -> actualValues.add(repaymentPeriod.getTotalPaidInAdvanceForPeriod() == null ? null
280+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidInAdvanceForPeriod().doubleValue()).format());
281+
case "Late" -> actualValues.add(repaymentPeriod.getTotalPaidLateForPeriod() == null ? null
282+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalPaidLateForPeriod().doubleValue()).format());
283+
case "Waived" -> actualValues.add(repaymentPeriod.getTotalWaivedForPeriod() == null ? null
284+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalWaivedForPeriod().doubleValue()).format());
285+
case "Outstanding" -> actualValues.add(repaymentPeriod.getTotalOutstandingForPeriod() == null ? null
286+
: new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod().doubleValue()).format());
287+
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
288+
}
289+
}
290+
return actualValues;
291+
}
292+
293+
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
294+
private List<String> validateRepaymentScheduleTotal(List<String> header, LoanScheduleData repaymentSchedule,
295+
List<String> expectedAmounts) {
296+
List<String> actualValues = new ArrayList<>();
297+
Double paidActual = 0.0;
298+
List<LoanSchedulePeriodData> periods = repaymentSchedule.getPeriods();
299+
for (LoanSchedulePeriodData period : periods) {
300+
if (null != period.getTotalPaidForPeriod()) {
301+
paidActual += period.getTotalPaidForPeriod().doubleValue();
302+
}
303+
}
304+
BigDecimal paidActualBd = new BigDecimal(paidActual).setScale(2, RoundingMode.HALF_DOWN);
305+
306+
for (int i = 0; i < header.size(); i++) {
307+
String headerName = header.get(i);
308+
String expectedValue = expectedAmounts.get(i);
309+
switch (headerName) {
310+
case "Principal due" -> assertThat(repaymentSchedule.getTotalPrincipalExpected().doubleValue())//
311+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePrincipal(
312+
repaymentSchedule.getTotalPrincipalExpected().doubleValue(), Double.valueOf(expectedValue)))//
313+
.isEqualTo(Double.valueOf(expectedValue));//
314+
case "Interest" -> assertThat(repaymentSchedule.getTotalInterestCharged().doubleValue())//
315+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInterest(
316+
repaymentSchedule.getTotalInterestCharged().doubleValue(), Double.valueOf(expectedValue)))//
317+
.isEqualTo(Double.valueOf(expectedValue));//
318+
case "Fees" -> assertThat(repaymentSchedule.getTotalFeeChargesCharged().doubleValue())//
319+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleFees(
320+
repaymentSchedule.getTotalFeeChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
321+
.isEqualTo(Double.valueOf(expectedValue));//
322+
case "Penalties" -> assertThat(repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue())//
323+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePenalties(
324+
repaymentSchedule.getTotalPenaltyChargesCharged().doubleValue(), Double.valueOf(expectedValue)))//
325+
.isEqualTo(Double.valueOf(expectedValue));//
326+
case "Due" -> assertThat(repaymentSchedule.getTotalRepaymentExpected().doubleValue())//
327+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleDue(
328+
repaymentSchedule.getTotalRepaymentExpected().doubleValue(), Double.valueOf(expectedValue)))//
329+
.isEqualTo(Double.valueOf(expectedValue));//
330+
case "Paid" -> assertThat(paidActualBd.doubleValue())//
331+
.as(ErrorMessageHelper.wrongAmountInRepaymentSchedulePaid(paidActualBd.doubleValue(),
332+
Double.valueOf(expectedValue)))//
333+
.isEqualTo(Double.valueOf(expectedValue));//
334+
case "In advance" -> assertThat(repaymentSchedule.getTotalPaidInAdvance().doubleValue())//
335+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleInAdvance(
336+
repaymentSchedule.getTotalPaidInAdvance().doubleValue(), Double.valueOf(expectedValue)))//
337+
.isEqualTo(Double.valueOf(expectedValue));//
338+
case "Late" -> assertThat(repaymentSchedule.getTotalPaidLate().doubleValue())//
339+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleLate(repaymentSchedule.getTotalPaidLate().doubleValue(),
340+
Double.valueOf(expectedValue)))//
341+
.isEqualTo(Double.valueOf(expectedValue));//
342+
case "Waived" -> assertThat(repaymentSchedule.getTotalWaived().doubleValue())//
343+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleWaived(repaymentSchedule.getTotalWaived().doubleValue(),
344+
Double.valueOf(expectedValue)))//
345+
.isEqualTo(Double.valueOf(expectedValue));//
346+
case "Outstanding" -> assertThat(repaymentSchedule.getTotalOutstanding().doubleValue())//
347+
.as(ErrorMessageHelper.wrongAmountInRepaymentScheduleOutstanding(
348+
repaymentSchedule.getTotalOutstanding().doubleValue(), Double.valueOf(expectedValue)))//
349+
.isEqualTo(Double.valueOf(expectedValue));//
350+
}
351+
}
352+
return actualValues;
353+
}
354+
149355
}

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)