FINERACT-2114: Reschedule validations enhancement
This commit is contained in:
+8
-8
@@ -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 | | | |
|
||||
|
||||
+4
-375
@@ -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);
|
||||
}
|
||||
|
||||
+382
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+188
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
-29
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+158
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
+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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user