From 3456e1eb81134f28f0649ab0e3b136422da08487 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Wed, 21 Aug 2024 19:57:14 +0200 Subject: [PATCH] FINERACT-2114: Reschedule validations enhancement --- .../stepdef/loan/LoanRescheduleStepDef.java | 16 +- .../features/LoanDownPayment.feature | 8 +- .../resources/features/LoanReschedule.feature | 38 +- .../LoanRescheduleRequestDataValidator.java | 379 +---------------- ...oanRescheduleRequestDataValidatorImpl.java | 382 ++++++++++++++++++ .../domain/LoanRescheduleRequest.java | 17 +- ...iveLoanRescheduleRequestDataValidator.java | 188 +++++++++ ...heduleRequestWritePlatformServiceImpl.java | 4 +- .../BaseLoanIntegrationTest.java | 2 +- ...eOffWithAdvancedPaymentAllocationTest.java | 29 -- .../LoanRescheduleRequestTest.java | 162 +++++++- ...TransactionAccrualActivityPostingTest.java | 4 +- .../common/LoanRescheduleRequestHelper.java | 4 + 13 files changed, 781 insertions(+), 452 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java index a71454903..8ae467cf9 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java @@ -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)// diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature index a2f316a2b..1c62109de 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature @@ -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 | | | | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature index 0dd01cd35..28d7e9e6a 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature @@ -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 | | | | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidator.java index be174c450..c41fb4da6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidator.java @@ -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 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 REJECT_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName, - RescheduleLoansApiConstants.rejectedOnDateParam)); + void validateForApproveAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest); - private static final Set 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>() {}.getType(); - this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, CREATE_REQUEST_DATA_PARAMETERS); - - final List 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 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>() {}.getType(); - this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, APPROVE_REQUEST_DATA_PARAMETERS); - - final List 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>() {}.getType(); - this.fromJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, REJECT_REQUEST_DATA_PARAMETERS); - - final List 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); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java new file mode 100644 index 000000000..4d22bde43 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java @@ -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 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 REJECT_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName, + RescheduleLoansApiConstants.rejectedOnDateParam)); + public static final Set 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 createRequestDataParameters) { + final String jsonString = jsonCommand.json(); + + if (StringUtils.isBlank(jsonString)) { + throw new InvalidJsonException(); + } + + final Type typeToken = new TypeToken>() {}.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 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 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 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 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); + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java index d7eb19a7c..b8211af7e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java @@ -272,14 +272,15 @@ public class LoanRescheduleRequest extends AbstractPersistableCustom { 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); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java new file mode 100644 index 000000000..8736ab5c5 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java @@ -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 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 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); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 751c3d41f..235155aaa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -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; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 15d557e88..0228e044a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java index 7f478bbcc..cf2877dca 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java @@ -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 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 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(); - } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java index 361b6b745..0f3b14c58 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java @@ -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 rescheduleResponse = new AtomicReference<>(); + AtomicReference 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); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index f9b15df13..2c593c0c5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -939,7 +939,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati AtomicReference 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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java index 901e52dfe..432a99301 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java @@ -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")); + } }