FINERACT-2114: Reschedule validations enhancement

This commit is contained in:
Adam Saghy
2024-08-23 14:25:48 +02:00
parent 1286b1e72f
commit 3456e1eb81
13 changed files with 781 additions and 452 deletions
@@ -62,10 +62,10 @@ public class LoanRescheduleStepDef extends AbstractStepDef {
String rescheduleFromDate = rescheduleData.get(0);
String submittedOnDate = rescheduleData.get(1);
String adjustedDueDate = rescheduleData.get(2);
Integer graceOfPrincipal = Integer.parseInt(rescheduleData.get(3)) == 0 ? null : Integer.valueOf(rescheduleData.get(3));
Integer graceOnInterest = Integer.parseInt(rescheduleData.get(4)) == 0 ? null : Integer.valueOf(rescheduleData.get(4));
Integer extraTerms = Integer.parseInt(rescheduleData.get(5)) == 0 ? null : Integer.valueOf(rescheduleData.get(5));
BigDecimal newInterestRate = new BigDecimal(rescheduleData.get(6));
Integer graceOfPrincipal = rescheduleData.get(3) != null ? Integer.parseInt(rescheduleData.get(3)) : null;
Integer graceOnInterest = rescheduleData.get(4) != null ? Integer.parseInt(rescheduleData.get(4)) : null;
Integer extraTerms = rescheduleData.get(5) != null ? Integer.parseInt(rescheduleData.get(5)) : null;
BigDecimal newInterestRate = rescheduleData.get(6) != null ? new BigDecimal(rescheduleData.get(6)) : null;
PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()//
.loanId(loanId)//
@@ -105,10 +105,10 @@ public class LoanRescheduleStepDef extends AbstractStepDef {
String rescheduleFromDate = rescheduleData.get(0);
String submittedOnDate = rescheduleData.get(1);
String adjustedDueDate = rescheduleData.get(2);
Integer graceOfPrincipal = Integer.parseInt(rescheduleData.get(3)) == 0 ? null : Integer.valueOf(rescheduleData.get(3));
Integer graceOnInterest = Integer.parseInt(rescheduleData.get(4)) == 0 ? null : Integer.valueOf(rescheduleData.get(4));
Integer extraTerms = Integer.parseInt(rescheduleData.get(5)) == 0 ? null : Integer.valueOf(rescheduleData.get(5));
BigDecimal newInterestRate = new BigDecimal(rescheduleData.get(6));
Integer graceOfPrincipal = rescheduleData.get(3) != null ? Integer.parseInt(rescheduleData.get(3)) : null;
Integer graceOnInterest = rescheduleData.get(4) != null ? Integer.parseInt(rescheduleData.get(4)) : null;
Integer extraTerms = rescheduleData.get(5) != null ? Integer.parseInt(rescheduleData.get(5)) : null;
BigDecimal newInterestRate = rescheduleData.get(6) != null ? new BigDecimal(rescheduleData.get(6)) : null;
PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()//
.loanId(loanId)//
@@ -434,7 +434,7 @@ Feature: Loan DownPayment
When Admin sets the business date to "10 January 2022"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 February 2022 | 10 January 2022 | 01 April 2022 | 0 | 0 | 0 | 0 |
| 01 February 2022 | 10 January 2022 | 01 April 2022 | | | | |
When Admin sets the business date to "01 April 2022"
When Admin successfully disburse the loan on "01 April 2022" with "400" EUR transaction amount
Then Loan Repayment schedule has 5 periods, with the following data for periods:
@@ -490,7 +490,7 @@ Feature: Loan DownPayment
When Admin sets the business date to "10 January 2022"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 February 2022 | 10 January 2022 | 01 April 2022 | 0 | 0 | 0 | 0 |
| 01 February 2022 | 10 January 2022 | 01 April 2022 | | | | |
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 2022 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -544,7 +544,7 @@ Feature: Loan DownPayment
| 1000.0 | 0 | 0 | 50 | 1050.0 | 250.0 | 0 | 0 | 800 |
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 10 February 2022 | 10 January 2022 | 10 April 2022 | 0 | 0 | 0 | 0 |
| 10 February 2022 | 10 January 2022 | 10 April 2022 | | | | |
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 |
| | | 10 January 2022 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -610,7 +610,7 @@ Feature: Loan DownPayment
| 1000.0 | 0 | 0 | 50 | 1050.0 | 250.0 | 0 | 0 | 800 |
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 10 February 2022 | 10 January 2022 | 10 April 2022 | 0 | 0 | 0 | 0 |
| 10 February 2022 | 10 January 2022 | 10 April 2022 | | | | |
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 |
| | | 10 January 2022 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -114,7 +114,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 31 July 2023 | 05 July 2023 | 31 August 2023 | 0 | 0 | 0 | 0 |
| 31 July 2023 | 05 July 2023 | 31 August 2023 | | | | |
Then Loan Repayment schedule has 1 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 July 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -144,7 +144,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2023 | 05 July 2023 | 15 September 2023 | 0 | 0 | 0 | 0 |
| 01 September 2023 | 05 July 2023 | 15 September 2023 | | | | |
Then Loan Repayment schedule has 3 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 July 2023 | | 3000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -176,7 +176,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2023 | 05 July 2023 | | 0 | 0 | 2 | 0 |
| 01 September 2023 | 05 July 2023 | | | | 2 | |
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 July 2023 | | 3000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -210,7 +210,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2023 | 05 July 2023 | | 1 | 0 | 0 | 0 |
| 01 September 2023 | 05 July 2023 | | 1 | | | |
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 July 2023 | | 3000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -244,7 +244,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2023 | 05 July 2023 | | 0 | 2 | 0 | 0 |
| 01 September 2023 | 05 July 2023 | | | 2 | | |
# Then Loan Repayment schedule has 3 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 July 2023 | | 3000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -276,7 +276,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 July 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2023 | 05 July 2023 | | 0 | 0 | 0 | 6 |
| 01 September 2023 | 05 July 2023 | | | | | 6 |
Then Loan Repayment schedule has 3 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 July 2023 | | 3000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -298,7 +298,7 @@ Feature: LoanReschedule
When Admin successfully disburse the loan on "01 October 2023" with "1000" EUR transaction amount
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 01 October 2023 | | 0 | 0 | 2 | 0 |
| 16 October 2023 | 01 October 2023 | | | | 2 | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -326,7 +326,7 @@ Feature: LoanReschedule
When Admin sets the business date to "11 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 01 October 2023 | | 0 | 0 | 2 | 0 |
| 16 October 2023 | 01 October 2023 | | | | 2 | |
Then Loan Repayment schedule 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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -356,7 +356,7 @@ Feature: LoanReschedule
When Admin sets the business date to "11 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 01 October 2023 | | 0 | 0 | 2 | 0 |
| 16 October 2023 | 01 October 2023 | | | | 2 | |
When Admin sets the business date to "20 October 2023"
When Admin successfully disburse the loan on "20 October 2023" with "400" EUR transaction amount
Then Loan Repayment schedule has 7 periods, with the following data for periods:
@@ -386,7 +386,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -410,7 +410,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 15 November 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 15 November 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -434,7 +434,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 15 November 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 15 November 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -464,7 +464,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -492,7 +492,7 @@ Feature: LoanReschedule
When Admin successfully disburse the loan on "01 October 2023" with "1000" EUR transaction amount
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 01 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 01 October 2023 | 31 October 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -518,7 +518,7 @@ Feature: LoanReschedule
When Admin sets the business date to "11 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 01 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 01 October 2023 | 31 October 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -544,7 +544,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 0 | 0 | 2 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | | | 2 | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -570,7 +570,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 1 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 1 | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -597,7 +597,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
Then Loan reschedule with the following data results a 403 error and "LOAN_CHARGED_OFF" error message
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | | | | |
Scenario: Verify that reschedule keeps the N+1 installment
@@ -623,7 +623,7 @@ Feature: LoanReschedule
When Admin sets the business date to "05 October 2023"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | 0 | 0 | 0 | 0 |
| 16 October 2023 | 05 October 2023 | 31 October 2023 | | | | |
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 October 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -18,386 +18,15 @@
*/
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.data;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.springframework.stereotype.Component;
@Component
@AllArgsConstructor
public class LoanRescheduleRequestDataValidator {
public interface LoanRescheduleRequestDataValidator {
private final FromJsonHelper fromJsonHelper;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.graceOnPrincipalParamName,
RescheduleLoansApiConstants.recurringMoratoriumOnPrincipalPeriodsParamName,
RescheduleLoansApiConstants.graceOnInterestParamName, RescheduleLoansApiConstants.extraTermsParamName,
RescheduleLoansApiConstants.rescheduleFromDateParamName, RescheduleLoansApiConstants.newInterestRateParamName,
RescheduleLoansApiConstants.rescheduleReasonIdParamName, RescheduleLoansApiConstants.rescheduleReasonCommentParamName,
RescheduleLoansApiConstants.submittedOnDateParamName, RescheduleLoansApiConstants.loanIdParamName,
RescheduleLoansApiConstants.adjustedDueDateParamName, RescheduleLoansApiConstants.recalculateInterestParamName,
RescheduleLoansApiConstants.endDateParamName, RescheduleLoansApiConstants.emiParamName));
void validateForCreateAction(JsonCommand jsonCommand, Loan loan);
private static final Set<String> REJECT_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.rejectedOnDateParam));
void validateForApproveAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest);
private static final Set<String> APPROVE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.approvedOnDateParam));
/**
* Validates the request to create a new loan reschedule entry
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
*
**/
public void validateForCreateAction(final JsonCommand jsonCommand, final Loan loan) {
final String jsonString = jsonCommand.json();
if (StringUtils.isBlank(jsonString)) {
throw new InvalidJsonException();
}
final Type typeToken = new TypeToken<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
if (!loan.getStatus().isActive()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("loan.is.not.active", "Loan is not active");
}
final Long loanId = this.fromJsonHelper.extractLongNamed(RescheduleLoansApiConstants.loanIdParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.loanIdParamName).value(loanId).notNull()
.integerGreaterThanZero();
final LocalDate submittedOnDate = this.fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.submittedOnDateParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.submittedOnDateParamName).value(submittedOnDate).notNull();
if (submittedOnDate != null && DateUtils.isAfter(loan.getDisbursementDate(), submittedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.submittedOnDateParamName)
.failWithCode("before.loan.disbursement.date", "Submission date cannot be before the loan disbursement date");
}
final LocalDate rescheduleFromDate = this.fromJsonHelper
.extractLocalDateNamed(RescheduleLoansApiConstants.rescheduleFromDateParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).value(rescheduleFromDate).notNull();
final Integer graceOnPrincipal = this.fromJsonHelper
.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.graceOnPrincipalParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnPrincipalParamName).value(graceOnPrincipal).ignoreIfNull()
.integerGreaterThanZero();
final Integer graceOnInterest = this.fromJsonHelper
.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.graceOnInterestParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnInterestParamName).value(graceOnInterest).ignoreIfNull()
.integerGreaterThanZero();
final Integer extraTerms = this.fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.extraTermsParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.extraTermsParamName).value(extraTerms).ignoreIfNull()
.integerGreaterThanZero();
final Long rescheduleReasonId = this.fromJsonHelper.extractLongNamed(RescheduleLoansApiConstants.rescheduleReasonIdParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleReasonIdParamName).value(rescheduleReasonId).notNull()
.integerGreaterThanZero();
final String rescheduleReasonComment = this.fromJsonHelper
.extractStringNamed(RescheduleLoansApiConstants.rescheduleReasonCommentParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleReasonCommentParamName).value(rescheduleReasonComment)
.ignoreIfNull().notExceedingLengthOf(500);
final LocalDate adjustedDueDate = this.fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.adjustedDueDateParamName,
jsonElement);
if (adjustedDueDate != null && DateUtils.isBefore(adjustedDueDate, rescheduleFromDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).failWithCode(
"adjustedDueDate.before.rescheduleFromDate", "Adjusted due date cannot be before the reschedule from date");
}
if (loan.getLoanProduct().getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.CUMULATIVE) {
final LocalDate endDate = jsonCommand.localDateValueOfParameterNamed(RescheduleLoansApiConstants.endDateParamName);
final BigDecimal emi = jsonCommand.bigDecimalValueOfParameterNamed(RescheduleLoansApiConstants.emiParamName);
validateForCumulativeLoan(dataValidatorBuilder, loan, jsonElement, rescheduleFromDate, endDate, emi);
} else {
validateForProgressiveLoan(dataValidatorBuilder, loan, jsonElement, rescheduleFromDate, adjustedDueDate);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private void validateForProgressiveLoan(final DataValidatorBuilder dataValidatorBuilder, final Loan loan, final JsonElement jsonElement,
final LocalDate rescheduleFromDate, final LocalDate adjustedDueDate) {
final var unsupportedFields = List.of(RescheduleLoansApiConstants.graceOnPrincipalParamName, //
RescheduleLoansApiConstants.graceOnInterestParamName, //
RescheduleLoansApiConstants.extraTermsParamName, //
RescheduleLoansApiConstants.emiParamName//
);
for (var unsupportedField : unsupportedFields) {
if (this.fromJsonHelper.parameterHasValue(unsupportedField, jsonElement)) {
dataValidatorBuilder.reset().parameter(unsupportedField).failWithCode(
RescheduleLoansApiConstants.rescheduleSelectedOperationNotSupportedErrorCode,
"Selected operation is not supported by Progressive Loan at a time during Loan Rescheduling");
return;
}
}
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
LoanRepaymentScheduleInstallment installment = null;
if (rescheduleFromDate != null) {
boolean hasInterestRateChange = this.fromJsonHelper.parameterHasValue(RescheduleLoansApiConstants.newInterestRateParamName,
jsonElement);
if (hasInterestRateChange && adjustedDueDate != null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.adjustedDueDateParamName).failWithCode(
RescheduleLoansApiConstants.rescheduleMultipleOperationsNotSupportedErrorCode,
"Only one operation is supported at a time during Loan Rescheduling");
return;
}
if (hasInterestRateChange && !rescheduleFromDate.isAfter(businessDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).failWithCode(
"loan.reschedule.interestratechange.reschedulefrom.shouldbefuture",
"Loan Reschedule From date should be in the future.");
}
if (adjustedDueDate != null) {
installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
} else if (hasInterestRateChange) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
}
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private void validateForCumulativeLoan(final DataValidatorBuilder dataValidatorBuilder, final Loan loan, final JsonElement jsonElement,
final LocalDate rescheduleFromDate, final LocalDate endDate, final BigDecimal emi) {
if (emi != null || endDate != null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.endDateParamName).value(endDate).notNull();
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.emiParamName).value(emi).notNull().positiveAmount();
if (endDate != null) {
LoanRepaymentScheduleInstallment endInstallment = loan.getRepaymentScheduleInstallment(endDate);
if (endInstallment == null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.endDateParamName)
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
}
}
}
// at least one of the following must be provided => graceOnPrincipal,
// graceOnInterest, extraTerms, newInterestRate
if (!this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.graceOnPrincipalParamName, jsonElement)
&& !this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.graceOnInterestParamName, jsonElement)
&& !this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.extraTermsParamName, jsonElement)
&& !this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement)
&& !this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.adjustedDueDateParamName, jsonElement)
&& !this.fromJsonHelper.parameterExists(RescheduleLoansApiConstants.emiParamName, jsonElement)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnPrincipalParamName).notNull();
}
final LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
if (rescheduleFromDate != null) {
validateReschedulingInstallment(dataValidatorBuilder, installment);
}
if (loan.isMultiDisburmentLoan()) {
if (!loan.loanProduct().isDisallowExpectedDisbursements()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
RescheduleLoansApiConstants.rescheduleForMultiDisbursementNotSupportedErrorCode,
"Loan rescheduling is not supported for multidisbursement tranche loans");
}
}
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private static void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder,
LoanRepaymentScheduleInstallment installment) {
if (installment == null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
}
if (installment != null && installment.isObligationsMet()) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
.failWithCode("repayment.schedule.installment.obligation.met", "Repayment schedule installment obligation met");
}
}
private void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan,
final LoanRepaymentScheduleInstallment installment) {
if (installment != null) {
LocalDate rescheduleFromDate = installment.getFromDate();
Collection<LoanCharge> charges = loan.getLoanCharges();
for (LoanCharge loanCharge : charges) {
if (loanCharge.isOverdueInstallmentCharge() && DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate)) {
dataValidatorBuilder.failWithCodeNoParameterAddedToErrorCode("not.allowed.due.to.overdue.charges");
break;
}
}
}
}
/**
* Validates a user request to approve a loan reschedule request
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
*
**/
public void validateForApproveAction(final JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
final String jsonString = jsonCommand.json();
if (StringUtils.isBlank(jsonString)) {
throw new InvalidJsonException();
}
final Type typeToken = new TypeToken<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, APPROVE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
final LocalDate approvedOnDate = this.fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.approvedOnDateParam,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.approvedOnDateParam).value(approvedOnDate).notNull();
if (approvedOnDate != null && DateUtils.isAfter(loanRescheduleRequest.getSubmittedOnDate(), approvedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.approvedOnDateParam).failWithCode("before.submission.date",
"Approval date cannot be before the request submission date.");
}
LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatusEnumData = LoanRescheduleRequestEnumerations
.status(loanRescheduleRequest.getStatusEnum());
if (!loanRescheduleRequestStatusEnumData.isPendingApproval()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("request.is.not.in.submitted.and.pending.state",
"Loan reschedule request approval is not allowed. "
+ "Loan reschedule request is not in submitted and pending approval state.");
}
LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate();
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment = null;
if (loan != null) {
if (!loan.getStatus().isActive()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("loan.is.not.active", "Loan is not active");
}
if (rescheduleFromDate != null) {
final boolean isProgressiveLoanSchedule = loan.getLoanProductRelatedDetail()
.getLoanScheduleType() == LoanScheduleType.PROGRESSIVE;
installment = isProgressiveLoanSchedule ? loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate)
: loan.getRepaymentScheduleInstallment(rescheduleFromDate);
if (installment == null) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
"loan.repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
}
if (installment != null && installment.isObligationsMet()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
"loan.repayment.schedule.installment." + "obligation.met", "Repayment schedule installment obligation met");
}
}
}
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
/**
* Validates a user request to reject a loan reschedule request
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
*
**/
public void validateForRejectAction(final JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
final String jsonString = jsonCommand.json();
if (StringUtils.isBlank(jsonString)) {
throw new InvalidJsonException();
}
final Type typeToken = new TypeToken<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, REJECT_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
final LocalDate rejectedOnDate = this.fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.rejectedOnDateParam,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rejectedOnDateParam).value(rejectedOnDate).notNull();
if (rejectedOnDate != null && DateUtils.isAfter(loanRescheduleRequest.getSubmittedOnDate(), rejectedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rejectedOnDateParam).failWithCode("before.submission.date",
"Rejection date cannot be before the request submission date.");
}
LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatusEnumData = LoanRescheduleRequestEnumerations
.status(loanRescheduleRequest.getStatusEnum());
if (!loanRescheduleRequestStatusEnumData.isPendingApproval()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("request.is.not.in.submitted.and.pending.state",
"Loan reschedule request rejection is not allowed. "
+ "Loan reschedule request is not in submitted and pending approval state.");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
void validateForRejectAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest);
}
@@ -0,0 +1,382 @@
/**
* 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.rescheduleloan.data;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("loanRescheduleRequestDataValidator")
@AllArgsConstructor
public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleRequestDataValidator {
public static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.graceOnPrincipalParamName,
RescheduleLoansApiConstants.recurringMoratoriumOnPrincipalPeriodsParamName,
RescheduleLoansApiConstants.graceOnInterestParamName, RescheduleLoansApiConstants.extraTermsParamName,
RescheduleLoansApiConstants.rescheduleFromDateParamName, RescheduleLoansApiConstants.newInterestRateParamName,
RescheduleLoansApiConstants.rescheduleReasonIdParamName, RescheduleLoansApiConstants.rescheduleReasonCommentParamName,
RescheduleLoansApiConstants.submittedOnDateParamName, RescheduleLoansApiConstants.loanIdParamName,
RescheduleLoansApiConstants.adjustedDueDateParamName, RescheduleLoansApiConstants.recalculateInterestParamName,
RescheduleLoansApiConstants.endDateParamName, RescheduleLoansApiConstants.emiParamName));
private static final Set<String> REJECT_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.rejectedOnDateParam));
public static final Set<String> APPROVE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.approvedOnDateParam));
private final FromJsonHelper fromJsonHelper;
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;
public static BigDecimal validateInterestRate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final BigDecimal interestRate = fromJsonHelper
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
.positiveAmount();
return interestRate;
}
private static void validateMultiDisburseLoan(Loan loan, DataValidatorBuilder dataValidatorBuilder) {
if (loan.isMultiDisburmentLoan()) {
if (!loan.loanProduct().isDisallowExpectedDisbursements()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
RescheduleLoansApiConstants.rescheduleForMultiDisbursementNotSupportedErrorCode,
"Loan rescheduling is not supported for multidisbursement tranche loans");
}
}
}
public static void validateEMIAndEndDate(FromJsonHelper fromJsonHelper, Loan loan, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final LocalDate endDate = fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.endDateParamName, jsonElement);
final BigDecimal emi = fromJsonHelper.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.emiParamName, jsonElement);
if (emi != null || endDate != null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.endDateParamName).value(endDate).notNull();
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.emiParamName).value(emi).notNull().positiveAmount();
if (endDate != null) {
LoanRepaymentScheduleInstallment endInstallment = loan.getRepaymentScheduleInstallment(endDate);
if (endInstallment == null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.endDateParamName)
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
}
}
}
}
public static LocalDate validateAndRetrieveAdjustedDate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
LocalDate rescheduleFromDate, DataValidatorBuilder dataValidatorBuilder) {
final LocalDate adjustedDueDate = fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.adjustedDueDateParamName,
jsonElement);
if (adjustedDueDate != null && DateUtils.isBefore(adjustedDueDate, rescheduleFromDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).failWithCode(
"adjustedDueDate.before.rescheduleFromDate", "Adjusted due date cannot be before the reschedule from date");
}
return adjustedDueDate;
}
public static void validateRescheduleReasonComment(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final String rescheduleReasonComment = fromJsonHelper
.extractStringNamed(RescheduleLoansApiConstants.rescheduleReasonCommentParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleReasonCommentParamName).value(rescheduleReasonComment)
.ignoreIfNull().notExceedingLengthOf(500);
}
public static void validateRescheduleReasonId(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final Long rescheduleReasonId = fromJsonHelper.extractLongNamed(RescheduleLoansApiConstants.rescheduleReasonIdParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleReasonIdParamName).value(rescheduleReasonId).notNull()
.integerGreaterThanZero();
}
public static void validateExtraTerms(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final Integer extraTerms = fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.extraTermsParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.extraTermsParamName).value(extraTerms).ignoreIfNull()
.integerGreaterThanZero();
}
public static void validateGraceOnInterest(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final Integer graceOnInterest = fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.graceOnInterestParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnInterestParamName).value(graceOnInterest).ignoreIfNull()
.integerGreaterThanZero();
}
public static void validateGraceOnPrincipal(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final Integer graceOnPrincipal = fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.graceOnPrincipalParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnPrincipalParamName).value(graceOnPrincipal).ignoreIfNull()
.integerGreaterThanZero();
}
public static LocalDate validateAndRetrieveRescheduleFromDate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final LocalDate rescheduleFromDate = fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.rescheduleFromDateParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).value(rescheduleFromDate).notNull();
return rescheduleFromDate;
}
public static void validateSubmittedOnDate(FromJsonHelper fromJsonHelper, Loan loan, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final LocalDate submittedOnDate = fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.submittedOnDateParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.submittedOnDateParamName).value(submittedOnDate).notNull();
if (submittedOnDate != null && DateUtils.isAfter(loan.getDisbursementDate(), submittedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.submittedOnDateParamName)
.failWithCode("before.loan.disbursement.date", "Submission date cannot be before the loan disbursement date");
}
}
public static void validateLoanIsActive(Loan loan, DataValidatorBuilder dataValidatorBuilder) {
if (!loan.getStatus().isActive()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("loan.is.not.active", "Loan is not active");
}
}
public static void validateSupportedParameters(JsonCommand jsonCommand, Set<String> createRequestDataParameters) {
final String jsonString = jsonCommand.json();
if (StringUtils.isBlank(jsonString)) {
throw new InvalidJsonException();
}
final Type typeToken = new TypeToken<Map<String, Object>>() {}.getType();
jsonCommand.checkForUnsupportedParameters(typeToken, jsonString, createRequestDataParameters);
}
public static void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder,
LoanRepaymentScheduleInstallment installment) {
if (installment == null) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
}
if (installment != null && installment.isObligationsMet()) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
.failWithCode("repayment.schedule.installment.obligation.met", "Repayment schedule installment obligation met");
}
}
public static void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan,
final LoanRepaymentScheduleInstallment installment) {
if (installment != null) {
LocalDate rescheduleFromDate = installment.getFromDate();
Collection<LoanCharge> charges = loan.getLoanCharges();
for (LoanCharge loanCharge : charges) {
if (loanCharge.isOverdueInstallmentCharge() && DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate)) {
dataValidatorBuilder.failWithCodeNoParameterAddedToErrorCode("not.allowed.due.to.overdue.charges");
break;
}
}
}
}
/**
* Validates the request to create a new loan reschedule entry
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
**/
@Override
public void validateForCreateAction(final JsonCommand jsonCommand, final Loan loan) {
if (loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE) {
progressiveLoanRescheduleRequestDataValidatorDelegate.validateForCreateAction(jsonCommand, loan);
} else {
validateSupportedParameters(jsonCommand, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
validateLoanIsActive(loan, dataValidatorBuilder);
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
validateEMIAndEndDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
validateIsThereAnyIncomingChange(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateMultiDisburseLoan(loan, dataValidatorBuilder);
LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
private void validateIsThereAnyIncomingChange(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
// at least one of the following must be provided => graceOnPrincipal,
// graceOnInterest, extraTerms, newInterestRate
if (!fromJsonHelper.parameterExists(RescheduleLoansApiConstants.graceOnPrincipalParamName, jsonElement)
&& !fromJsonHelper.parameterExists(RescheduleLoansApiConstants.graceOnInterestParamName, jsonElement)
&& !fromJsonHelper.parameterExists(RescheduleLoansApiConstants.extraTermsParamName, jsonElement)
&& !fromJsonHelper.parameterExists(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement)
&& !fromJsonHelper.parameterExists(RescheduleLoansApiConstants.adjustedDueDateParamName, jsonElement)
&& !fromJsonHelper.parameterExists(RescheduleLoansApiConstants.emiParamName, jsonElement)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.graceOnPrincipalParamName).notNull();
}
}
/**
* Validates a user request to approve a loan reschedule request
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
**/
@Override
public void validateForApproveAction(final JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
if (loanRescheduleRequest.getLoan().getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) {
progressiveLoanRescheduleRequestDataValidatorDelegate.validateForApproveAction(jsonCommand, loanRescheduleRequest);
} else {
validateSupportedParameters(jsonCommand, APPROVE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
validateApprovalDate(fromJsonHelper, loanRescheduleRequest, jsonElement, dataValidatorBuilder);
validateRescheduleRequestStatus(loanRescheduleRequest, dataValidatorBuilder);
LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate();
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;
validateLoanIsActive(loan, dataValidatorBuilder);
installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
public static void validateRescheduleRequestStatus(LoanRescheduleRequest loanRescheduleRequest,
DataValidatorBuilder dataValidatorBuilder) {
LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatusEnumData = LoanRescheduleRequestEnumerations
.status(loanRescheduleRequest.getStatusEnum());
if (!loanRescheduleRequestStatusEnumData.isPendingApproval()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("request.is.not.in.submitted.and.pending.state",
"Loan reschedule request approval is not allowed. "
+ "Loan reschedule request is not in submitted and pending approval state.");
}
}
public static void validateApprovalDate(FromJsonHelper fromJsonHelper, LoanRescheduleRequest loanRescheduleRequest,
JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
final LocalDate approvedOnDate = fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.approvedOnDateParam, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.approvedOnDateParam).value(approvedOnDate).notNull();
if (approvedOnDate != null && DateUtils.isAfter(loanRescheduleRequest.getSubmittedOnDate(), approvedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.approvedOnDateParam).failWithCode("before.submission.date",
"Approval date cannot be before the request submission date.");
}
}
/**
* Validates a user request to reject a loan reschedule request
*
* @param jsonCommand
* the JSON command object (instance of the JsonCommand class)
**/
@Override
public void validateForRejectAction(final JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
validateSupportedParameters(jsonCommand, REJECT_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
final LocalDate rejectedOnDate = this.fromJsonHelper.extractLocalDateNamed(RescheduleLoansApiConstants.rejectedOnDateParam,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rejectedOnDateParam).value(rejectedOnDate).notNull();
if (rejectedOnDate != null && DateUtils.isAfter(loanRescheduleRequest.getSubmittedOnDate(), rejectedOnDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rejectedOnDateParam).failWithCode("before.submission.date",
"Rejection date cannot be before the request submission date.");
}
LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatusEnumData = LoanRescheduleRequestEnumerations
.status(loanRescheduleRequest.getStatusEnum());
if (!loanRescheduleRequestStatusEnumData.isPendingApproval()) {
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode("request.is.not.in.submitted.and.pending.state",
"Loan reschedule request rejection is not allowed. "
+ "Loan reschedule request is not in submitted and pending approval state.");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
@@ -272,14 +272,15 @@ public class LoanRescheduleRequest extends AbstractPersistableCustom<Long> {
return this.loanRescheduleRequestToTermVariationMappings;
}
public LoanTermVariations getInterestRateFromInstallmentTermVariationIfExists() {
return this.loanRescheduleRequestToTermVariationMappings.stream()
.map(LoanRescheduleRequestToTermVariationMapping::getLoanTermVariations)
.filter(loanTermVariations -> loanTermVariations.getTermType().isInterestRateFromInstallment()).findFirst().orElse(null);
}
public LoanTermVariations getDueDateTermVariationIfExists() {
if (this.loanRescheduleRequestToTermVariationMappings != null && this.loanRescheduleRequestToTermVariationMappings.size() > 0) {
for (LoanRescheduleRequestToTermVariationMapping mapping : this.loanRescheduleRequestToTermVariationMappings) {
if (mapping.getLoanTermVariations().getTermType().isDueDateVariation()) {
return mapping.getLoanTermVariations();
}
}
}
return null;
return this.loanRescheduleRequestToTermVariationMappings.stream()
.map(LoanRescheduleRequestToTermVariationMapping::getLoanTermVariations)
.filter(loanTermVariations -> loanTermVariations.getTermType().isDueDateVariation()).findFirst().orElse(null);
}
}
@@ -0,0 +1,188 @@
/**
* 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.rescheduleloan.data;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.APPROVE_REQUEST_DATA_PARAMETERS;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.CREATE_REQUEST_DATA_PARAMETERS;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveAdjustedDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveRescheduleFromDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateInterestRate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleRequestStatus;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateReschedulingInstallment;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSubmittedOnDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSupportedParameters;
import com.google.gson.JsonElement;
import jakarta.persistence.criteria.Predicate;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.stereotype.Component;
@Component("progressiveLoanRescheduleRequestDataValidatorImpl")
@AllArgsConstructor
public class ProgressiveLoanRescheduleRequestDataValidator implements LoanRescheduleRequestDataValidator {
private final FromJsonHelper fromJsonHelper;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
@Override
public void validateForCreateAction(JsonCommand jsonCommand, Loan loan) {
validateSupportedParameters(jsonCommand, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
validateLoanIsActive(loan, dataValidatorBuilder);
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
boolean hasInterestRateChange = interestRate != null;
boolean hasAdjustDueDateChange = adjustedDueDate != null;
if (hasInterestRateChange && hasAdjustDueDateChange) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.adjustedDueDateParamName).failWithCode(
RescheduleLoansApiConstants.rescheduleMultipleOperationsNotSupportedErrorCode,
"Only one operation is supported at a time during Loan Rescheduling");
}
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
if (rescheduleFromDate != null) {
if (hasInterestRateChange && !rescheduleFromDate.isAfter(businessDate)) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName).failWithCode(
"loan.reschedule.interest.rate.change.reschedule.from.date.should.be.in.future",
"Loan Reschedule From date should be in the future.");
}
if (hasInterestRateChange) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
}
}
LoanRepaymentScheduleInstallment installment;
if (hasInterestRateChange) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
} else {
installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private void validateUnsupportedParams(JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
final var unsupportedFields = List.of(RescheduleLoansApiConstants.graceOnPrincipalParamName, //
RescheduleLoansApiConstants.graceOnInterestParamName, //
RescheduleLoansApiConstants.extraTermsParamName, //
RescheduleLoansApiConstants.emiParamName//
);
for (var unsupportedField : unsupportedFields) {
if (this.fromJsonHelper.parameterHasValue(unsupportedField, jsonElement)) {
dataValidatorBuilder.reset().parameter(unsupportedField).failWithCode(
RescheduleLoansApiConstants.rescheduleSelectedOperationNotSupportedErrorCode,
"Selected operation is not supported by Progressive Loan at a time during Loan Rescheduling");
}
}
}
@Override
public void validateForApproveAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
validateSupportedParameters(jsonCommand, APPROVE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors)
.resource(StringUtils.lowerCase(RescheduleLoansApiConstants.ENTITY_NAME));
final JsonElement jsonElement = jsonCommand.parsedJson();
validateApprovalDate(fromJsonHelper, loanRescheduleRequest, jsonElement, dataValidatorBuilder);
validateRescheduleRequestStatus(loanRescheduleRequest, dataValidatorBuilder);
LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate();
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;
validateLoanIsActive(loan, dataValidatorBuilder);
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
if (!rescheduleFromDate.isAfter(DateUtils.getBusinessLocalDate())) {
throw new GeneralPlatformDomainRuleException(
"loan.reschedule.interest.rate.change.reschedule.from.date.should.be.in.future",
"Loan Reschedule From date should be in the future.", rescheduleFromDate);
}
} else {
installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
@Override
public void validateForRejectAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest) {
throw new UnsupportedOperationException("Nothing to override here");
}
private void validateInterestRateChangeRescheduleFromDate(Loan loan, LocalDate rescheduleFromDate) {
boolean alreadyExistInterestRateChange = loanRescheduleRequestRepository.exists((root, query, criteriaBuilder) -> {
Predicate loanPredicate = criteriaBuilder.equal(root.get("loan"), loan);
Predicate statusPredicate = root.get("statusEnum")
.in(List.of(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), LoanStatus.APPROVED.getValue()));
Predicate datePredicate = criteriaBuilder.equal(root.get("rescheduleFromDate"), rescheduleFromDate);
return criteriaBuilder.and(loanPredicate, statusPredicate, datePredicate);
});
if (alreadyExistInterestRateChange) {
throw new GeneralPlatformDomainRuleException("loan.reschedule.interest.rate.change.already.exists",
"Interest rate change for the provided date is already exists.", rescheduleFromDate);
}
}
}
@@ -64,7 +64,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
@@ -87,6 +86,7 @@ import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusi
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
@@ -101,11 +101,11 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
private final PlatformSecurityContext platformSecurityContext;
@Qualifier("loanRescheduleRequestDataValidator")
private final LoanRescheduleRequestDataValidator loanRescheduleRequestDataValidator;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private final LoanRepaymentScheduleHistoryRepository loanRepaymentScheduleHistoryRepository;
private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService;
private final LoanTransactionRepository loanTransactionRepository;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanAssembler loanAssembler;
@@ -333,7 +333,7 @@ public abstract class BaseLoanIntegrationTest {
}).toList();
}
private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) {
public AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
@@ -32,11 +32,9 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.AllowAttributeOverrides;
import org.apache.fineract.client.models.ChargeData;
@@ -48,7 +46,6 @@ import org.apache.fineract.client.models.GetLoanTransactionRelation;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.JournalEntryTransactionItem;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
@@ -73,7 +70,6 @@ import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHe
import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -751,29 +747,4 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest extends BaseL
return Long.valueOf(fundId);
}
private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder;
}).toList();
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder;
@@ -31,19 +32,31 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanRescheduleRequestTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,8 +64,7 @@ import org.slf4j.LoggerFactory;
* Test the creation, approval and rejection of a loan reschedule request
**/
@SuppressWarnings({ "rawtypes" })
@ExtendWith(LoanTestLifecycleExtension.class)
public class LoanRescheduleRequestTest {
public class LoanRescheduleRequestTest extends BaseLoanIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(LoanRescheduleRequestTest.class);
private ResponseSpecification responseSpec;
@@ -235,6 +247,99 @@ public class LoanRescheduleRequestTest {
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId);
}
@Test
public void testInterestRateChangeForProgressiveLoan() {
PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer commonLoanProductId = createLoanProduct("500", "15", "4", true, "25", true, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
AtomicReference<PostCreateRescheduleLoansResponse> rescheduleResponse = new AtomicReference<>();
AtomicReference<PostLoansResponse> loanResponse = new AtomicReference<>();
// Do not allow interest rate change on not active loan
// Do not allow interest rate change twice on the same day
runAt("15 February 2023", () -> {
loanResponse.set(applyForLoanApplication(client.getClientId(), commonLoanProductId, BigDecimal.valueOf(500.0), 45, 15, 3,
BigDecimal.ZERO, "01 January 2023", "01 January 2023"));
loanTransactionHelper.approveLoan(loanResponse.get().getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("01 January 2023").locale("en"));
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId())
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")));
assertEquals(400, exception.getResponse().code());
assertTrue(exception.getMessage().contains("loan.is.not.active"));
loanTransactionHelper.disburseLoan(loanResponse.get().getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("15 February 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
rescheduleResponse.set(loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
.loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")));
exception = assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId())
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.already.exists"));
});
// Do not allow approve an interest rate change if the reschedule from date is not in the future
// Do not allow create interest rate change if a previous interest rate change got already approved for that
// date
runAt("16 February 2023", () -> {
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleResponse.get().getResourceId(),
new PostUpdateRescheduleLoansRequest().approvedOnDate("16 February 2024").locale("en")
.dateFormat(DATETIME_PATTERN)));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.reschedule.from.date.should.be.in.future"));
PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId())
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023").newInterestRate(BigDecimal.ONE)
.rescheduleReasonId(1L).rescheduleFromDate("17 February 2023"));
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(),
new PostUpdateRescheduleLoansRequest().approvedOnDate("17 February 2024").locale("en").dateFormat(DATETIME_PATTERN));
exception = assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(rescheduleLoansResponse.getLoanId())
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("17 February 2023")));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.already.exists"));
});
// Allow new interest rate change if the previous got rejected
runAt("17 February 2023", () -> {
PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId())
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("18 February 2023").newInterestRate(BigDecimal.ONE)
.rescheduleReasonId(1L).rescheduleFromDate("18 February 2023"));
loanRescheduleRequestHelper.rejectLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(),
new PostUpdateRescheduleLoansRequest().rejectedOnDate("18 February 2024").locale("en").dateFormat(DATETIME_PATTERN));
loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
.loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("18 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("18 February 2023"));
});
}
/**
* create new loan reschedule request
**/
@@ -255,4 +360,53 @@ public class LoanRescheduleRequestTest {
public void testCreateLoanRescheduleChangeEMIRequest() {
this.createLoanRescheduleChangeEMIRequest();
}
private PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode,
String loanScheduleProcessingType) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
.transactionProcessingStrategyCode(transactionProcessorCode).locale("en").submittedOnDate(submittedOnDate)
.amortizationType(1).interestRatePerPeriod(interestRate).interestCalculationPeriodType(1).interestType(0)
.repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery).repaymentFrequencyType(0)
.numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency).loanTermFrequencyType(0).principal(principal)
.loanType("individual").loanScheduleProcessingType(loanScheduleProcessingType)
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000)));
}
private PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
final String expectedDisbursementDate, final String submittedOnDate) {
return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments,
interestRate, expectedDisbursementDate, submittedOnDate, LoanScheduleProcessingType.HORIZONTAL);
}
private PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
final String expectedDisbursementDate, final String submittedOnDate, LoanScheduleProcessingType loanScheduleProcessingType) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments,
interestRate, expectedDisbursementDate, submittedOnDate,
AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY, loanScheduleProcessingType.name());
}
private Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments,
boolean downPaymentEnabled, String downPaymentPercentage, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType,
LoanScheduleProcessingType loanScheduleProcessingType, final Account... accounts) {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
.withEnableDownPayment(downPaymentEnabled, downPaymentPercentage, autoPayForDownPayment).withinterestRatePerPeriod("0")
.withInterestRateFrequencyTypeAsMonths()
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.addAdvancedPaymentAllocation(defaultAllocation).withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withDisallowExpectedDisbursements(true)
.withLoanScheduleType(loanScheduleType).withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30")
.withDaysInYear("365").withMoratorium("0", "0").build(null);
return loanTransactionHelper.getLoanProductId(loanProductJSON);
}
}
@@ -939,7 +939,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
AtomicReference<Long> loanId = new AtomicReference<>();
runAt(creationBusinessDay, () -> {
Long localLoanProductId = loanTransactionHelper
.createLoanProduct(loanProductsRequestPin30InterestDecliningBalanceDailyRecalculationCompoundingNoneAccrualActivity())
.createLoanProduct(loanProductsRequestInterestDecliningBalanceDailyRecalculationCompoundingNoneAccrualActivity())
.getResourceId();
loanId.set(applyForLoanApplication(client.getClientId(), localLoanProductId, BigDecimal.valueOf(1000), disbursementDay));
loanTransactionHelper.approveLoan(loanId.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
@@ -964,7 +964,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
});
}
private PostLoanProductsRequest loanProductsRequestPin30InterestDecliningBalanceDailyRecalculationCompoundingNoneAccrualActivity() {
private PostLoanProductsRequest loanProductsRequestInterestDecliningBalanceDailyRecalculationCompoundingNoneAccrualActivity() {
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
String shortName = Utils.uniqueRandomStringGenerator("", 4);
@@ -83,4 +83,8 @@ public class LoanRescheduleRequestHelper extends IntegrationTest {
public PostUpdateRescheduleLoansResponse approveLoanRescheduleRequest(Long scheduleId, PostUpdateRescheduleLoansRequest request) {
return ok(fineract().rescheduleLoans.updateLoanRescheduleRequest(scheduleId, request, "approve"));
}
public PostUpdateRescheduleLoansResponse rejectLoanRescheduleRequest(Long scheduleId, PostUpdateRescheduleLoansRequest request) {
return ok(fineract().rescheduleLoans.updateLoanRescheduleRequest(scheduleId, request, "reject"));
}
}