Skip to content

Commit 61b0528

Browse files
FINERACT-2354: Introduce reaging preview API
1 parent 89e9cfc commit 61b0528

File tree

12 files changed

+1244
-418
lines changed

12 files changed

+1244
-418
lines changed

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

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,30 @@
1818
*/
1919
package org.apache.fineract.test.stepdef.loan;
2020

21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2124
import io.cucumber.datatable.DataTable;
2225
import io.cucumber.java.en.Then;
2326
import io.cucumber.java.en.When;
2427
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;
2532
import java.util.List;
33+
import java.util.stream.Collectors;
2634
import lombok.extern.slf4j.Slf4j;
35+
import org.apache.fineract.client.models.LoanScheduleData;
36+
import org.apache.fineract.client.models.LoanSchedulePeriodData;
2737
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
2838
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
2939
import org.apache.fineract.client.models.PostLoansResponse;
3040
import org.apache.fineract.client.services.LoanTransactionsApi;
3141
import org.apache.fineract.test.factory.LoanRequestFactory;
3242
import org.apache.fineract.test.helper.ErrorHelper;
43+
import org.apache.fineract.test.helper.ErrorMessageHelper;
44+
import org.apache.fineract.test.helper.Utils;
3345
import org.apache.fineract.test.messaging.EventAssertion;
3446
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
3547
import org.apache.fineract.test.stepdef.AbstractStepDef;
@@ -40,6 +52,9 @@
4052
@Slf4j
4153
public class LoanReAgingStepDef extends AbstractStepDef {
4254

55+
private static final String DATE_FORMAT = "dd MMMM yyyy";
56+
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
57+
4358
@Autowired
4459
private LoanTransactionsApi loanTransactionsApi;
4560

@@ -112,4 +127,198 @@ public void checkLoanReAmortizeBusinessEventCreated() {
112127

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

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)