diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalDelinquencyRescheduleStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalDelinquencyRescheduleStepDef.java index f13f03ceafd..f0065fda2ae 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalDelinquencyRescheduleStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalDelinquencyRescheduleStepDef.java @@ -126,6 +126,24 @@ public void createWcDelinquencyBucket(final int frequency, final String frequenc frequencyType, minimumPayment, minimumPaymentType); } + @When("Admin creates WC delinquency reset action") + public void createResetAction() { + final PostWorkingCapitalLoansDelinquencyActionRequest request = new PostWorkingCapitalLoansDelinquencyActionRequest(); + request.setAction("reset"); + request.setLocale("en"); + + executeRescheduleAction(request); + } + + @When("Admin creates WC delinquency undo reset action") + public void createUndoResetAction() { + final PostWorkingCapitalLoansDelinquencyActionRequest request = new PostWorkingCapitalLoansDelinquencyActionRequest(); + request.setAction("undo_reset"); + request.setLocale("en"); + + executeRescheduleAction(request); + } + @When("Admin creates WC delinquency reschedule action with the following parameters:") public void createRescheduleAction(final DataTable table) { final Map params = table.asMaps().get(0); @@ -208,6 +226,8 @@ public void verifyPeriods(final DataTable table) { final List periods = ok( () -> fineractFeignClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId)); + log.info("Periods: {}", periods); + final List> expectedRows = table.asMaps(); assertThat(periods).as("Number of periods").hasSize(expectedRows.size()); @@ -318,9 +338,15 @@ private void verifyFullScheduleField(final WorkingCapitalLoanDelinquencyRangeSch case "periodNumber" -> assertThat(actual.getPeriodNumber()).as(label).isEqualTo(Integer.parseInt(expected)); case "fromDate" -> assertThat(actual.getFromDate()).as(label).isEqualTo(LocalDate.parse(expected, DATE_FORMAT)); case "toDate" -> assertThat(actual.getToDate()).as(label).isEqualTo(LocalDate.parse(expected, DATE_FORMAT)); - case "expectedAmount" -> assertThat(actual.getExpectedAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); - case "paidAmount" -> assertThat(actual.getPaidAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); - case "outstandingAmount" -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); + case "expectedAmount" -> verifyOptionalField(expected, + v -> assertThat(actual.getExpectedAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)), + () -> assertThat(actual.getExpectedAmount()).as(label).isNull()); + case "paidAmount" -> verifyOptionalField(expected, + v -> assertThat(actual.getPaidAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)), + () -> assertThat(actual.getPaidAmount()).as(label).isNull()); + case "outstandingAmount" -> verifyOptionalField(expected, + v -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)), + () -> assertThat(actual.getOutstandingAmount()).as(label).isNull()); case "minPaymentCriteriaMet" -> verifyOptionalField(expected, v -> assertThat(actual.getMinPaymentCriteriaMet()).as(label).isEqualTo(Boolean.parseBoolean(v)), () -> assertThat(actual.getMinPaymentCriteriaMet()).as(label).isNull()); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReset.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReset.feature new file mode 100644 index 00000000000..4976bbe73b9 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReset.feature @@ -0,0 +1,284 @@ +@WorkingCapital +@WorkingCapitalDelinquencyResetActionFeature @WCCOBFeature +Feature: Working Capital Delinquency Reset Action + + @TestRailId:TODO_ADD_1 + Scenario: Verify that Reset Delinquency Action has correct values on the delinquency range schedule and delinquency tags are lifted then verify undo + When Admin sets the business date to "01 January 2026" + When Admin creates a client with random data + When Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 10000 | 10000 | 1 | 0.0 | + When Admin successfully approves the working capital loan on "01 January 2026" with "10000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "10000" EUR transaction amount + When Admin sets the business date to "01 June 2026" + When Admin runs inline COB job for Working Capital Loan + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | 300 | 0 | 300 | false | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | false | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | false | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | false | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 0 | 300 | false | + | 6 | 31 May 2026 | 29 June 2026 | 300 | 0 | 300 | | + + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 5 | 2026-06-01 | | D00 | 1 | 30 | + | 4 | 2026-06-01 | | D30 | 31 | 60 | + | 3 | 2026-06-01 | | D60 | 61 | 90 | + | 2 | 2026-06-01 | | D90 | 91 | 120 | + | 1 | 2026-06-01 | | D120 | 121 | 150 | + When Admin creates WC delinquency reset action + Then WC loan last delinquency action has the following data: + | action | startDate | minimumPayment | minimumPaymentType | frequency | frequencyType | + | RESET | 01 June 2026 | | | | | + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | | | | | + | 5 | 01 May 2026 | 30 May 2026 | | | | | + | 6 | 31 May 2026 | 29 June 2026 | 300 | 0 | 300 | | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 5 | 2026-06-01 | 2026-06-01 | D00 | 1 | 30 | + | 4 | 2026-06-01 | 2026-06-01 | D30 | 31 | 60 | + | 3 | 2026-06-01 | 2026-06-01 | D60 | 61 | 90 | + | 2 | 2026-06-01 | 2026-06-01 | D90 | 91 | 120 | + | 1 | 2026-06-01 | 2026-06-01 | D120 | 121 | 150 | + + When Admin sets the business date to "01 July 2026" + When Admin runs inline COB job for Working Capital Loan + + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 6 | 2026-06-30 | | D00 | 1 | 30 | + | 5 | 2026-06-01 | 2026-06-01 | D00 | 1 | 30 | + | 4 | 2026-06-01 | 2026-06-01 | D30 | 31 | 60 | + | 3 | 2026-06-01 | 2026-06-01 | D60 | 61 | 90 | + | 2 | 2026-06-01 | 2026-06-01 | D90 | 91 | 120 | + | 1 | 2026-06-01 | 2026-06-01 | D120 | 121 | 150 | + + Then Admin creates WC delinquency undo reset action + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | 300 | 0 | 300 | false | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | false | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | false | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | false | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 0 | 300 | false | + | 6 | 31 May 2026 | 29 June 2026 | 300 | 0 | 300 | false | + | 7 | 30 June 2026 | 29 July 2026 | 300 | 0 | 300 | | + + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 5 | 2026-07-01 | | D30 | 31 | 60 | + | 4 | 2026-07-01 | | D60 | 61 | 90 | + | 3 | 2026-07-01 | | D90 | 91 | 120 | + | 2 | 2026-07-01 | | D120 | 121 | 150 | + | 1 | 2026-07-01 | | D150 | 151 | 180 | + | 6 | 2026-06-30 | | D00 | 1 | 30 | + | 5 | 2026-06-01 | 2026-06-01 | D00 | 1 | 30 | + | 4 | 2026-06-01 | 2026-06-01 | D30 | 31 | 60 | + | 3 | 2026-06-01 | 2026-06-01 | D60 | 61 | 90 | + | 2 | 2026-06-01 | 2026-06-01 | D90 | 91 | 120 | + | 1 | 2026-06-01 | 2026-06-01 | D120 | 121 | 150 | + Then Admin closes the Working Capital loan with a full repayment on "01 July 2026" + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | 300 | 300 | 0 | true | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 300 | 0 | true | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 300 | 0 | true | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 300 | 0 | true | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 300 | 0 | true | + | 6 | 31 May 2026 | 29 June 2026 | 300 | 300 | 0 | true | + | 7 | 30 June 2026 | 29 July 2026 | 300 | 8200 | 0 | true | + + @TestRailId:TODO_ADD_2 + Scenario: Verify multiple Reset Delinquency Action and undo reset + When Admin sets the business date to "01 January 2026" + When Admin creates a client with random data + When Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 10000 | 10000 | 1 | 0.0 | + When Admin successfully approves the working capital loan on "01 January 2026" with "10000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "10000" EUR transaction amount + + When Admin sets the business date to "31 January 2026" + When Admin runs inline COB job for Working Capital Loan + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | 300 | 0 | 300 | false | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + + When Admin creates WC delinquency reset action + + Then WC loan last delinquency action has the following data: + | action | startDate | minimumPayment | minimumPaymentType | frequency | frequencyType | + | RESET | 31 January 2026 | | | | | + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | 2026-01-31 | D00 | 1 | 30 | + + When Admin sets the business date to "2 March 2026" + When Admin runs inline COB job for Working Capital Loan + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | false | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 2 | 2026-03-02 | | D00 | 1 | 30 | + | 1 | 2026-01-31 | 2026-01-31 | D00 | 1 | 30 | + + When Admin creates WC delinquency reset action + + Then WC loan delinquency actions have the following data: + | action | startDate | endDate | minimumPayment | minimumPaymentType | frequency | frequencyType | + | RESET | 31 January 2026 | | | | | | + | RESET | 02 March 2026 | | | | | | + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 2 | 2026-03-02 | 2026-03-02 | D00 | 1 | 30 | + | 1 | 2026-01-31 | 2026-01-31 | D00 | 1 | 30 | + + When Admin sets the business date to "2 April 2026" + When Admin runs inline COB job for Working Capital Loan + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | false | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | | + + Then Admin creates WC delinquency undo reset action + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | false | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | false | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | | + + When Admin creates WC delinquency reset action + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | | + Then Admin closes the Working Capital loan with a full repayment on "02 April 2026" + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 10000 | 0 | true | + + @TestRailId:TODO_ADD_3 + Scenario: Verify multiple Reset Delinquency Action and undo reset for 2nd and 4th reset + When Admin sets the business date to "01 January 2026" + When Admin creates a client with random data + When Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 10000 | 10000 | 1 | 0.0 | + When Admin successfully approves the working capital loan on "01 January 2026" with "10000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "10000" EUR transaction amount + + When Admin sets the business date to "31 January 2026" + When Admin runs inline COB job for Working Capital Loan + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | 300 | 0 | 300 | false | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | | + When Admin creates WC delinquency reset action + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | | + + When Admin sets the business date to "02 March 2026" + When Admin runs inline COB job for Working Capital Loan + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | 300 | 0 | 300 | false | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | | + + When Admin creates WC delinquency reset action + + When Admin sets the business date to "02 April 2026" + When Admin runs inline COB job for Working Capital Loan + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | 300 | 0 | 300 | false | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | | + + Then Admin creates WC delinquency undo reset action + When Admin creates WC delinquency reset action + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | | + + + When Admin sets the business date to "02 May 2026" + When Admin runs inline COB job for Working Capital Loan + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | false | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 0 | 300 | | + + When Admin creates WC delinquency reset action + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | | | | | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 0 | 300 | | + + Then Admin creates WC delinquency undo reset action + + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 0 | 300 | false | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 0 | 300 | | + Then Admin closes the Working Capital loan with a full repayment on "02 May 2026" + Then WC loan delinquency range schedule has the following periods: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | + | 1 | 01 January 2026 | 30 January 2026 | | | | | + | 2 | 31 January 2026 | 01 March 2026 | | | | | + | 3 | 02 March 2026 | 31 March 2026 | | | | | + | 4 | 01 April 2026 | 30 April 2026 | 300 | 300 | 0 | true | + | 5 | 01 May 2026 | 30 May 2026 | 300 | 9700 | 0 | true | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyAction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyAction.java index b2192bf8410..05b273b4bcb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyAction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyAction.java @@ -21,5 +21,7 @@ public enum DelinquencyAction { PAUSE, // RESUME, // - RESCHEDULE // + RESCHEDULE, // + RESET, // + UNDO_RESET // } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeSchedule.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeSchedule.java index 72a6d70cd92..93b6c13bba0 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeSchedule.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeSchedule.java @@ -76,4 +76,20 @@ public class WorkingCapitalLoanDelinquencyRangeSchedule extends AbstractAuditabl @Column(name = "delinquent_amount", scale = 6, precision = 19) private BigDecimal delinquentAmount; + @Column(name = "reset") + private Boolean reset = false; + + public void reset() { + setReset(true); + setDelinquentDays(null); + setDelinquentAmount(null); + setMinPaymentCriteriaMet(null); + setPaidAmount(null); + setOutstandingAmount(null); + } + + public BigDecimal getExpectedAmount() { + return reset ? null : this.expectedAmount; + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java index 5f9eb42969b..7bf71bc92bd 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java @@ -45,7 +45,9 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper { @AfterMapping default void calculateTotal(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory source, @MappingTarget WorkingCapitalLoanDelinquencyTagHistoryData target) { - target.setDelinquentDays(source.getRangeSchedule().getDelinquentDays() - source.getDelinquencyRange().getMinimumAgeDays() + 1); + if (source.getRangeSchedule().getDelinquentDays() != null) { + target.setDelinquentDays(source.getRangeSchedule().getDelinquentDays() - source.getDelinquencyRange().getMinimumAgeDays() + 1); + } } @Mapping(target = "rangeId", source = "delinquencyRange.id") diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java index d6a83496129..44da6b8f0d8 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java @@ -42,6 +42,7 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleRepository WHERE s.loan.id = :loanId AND s.toDate < :transactionDate AND (s.minPaymentCriteriaMet IS NULL OR s.minPaymentCriteriaMet = FALSE) + AND s.reset = false ORDER BY s.periodNumber ASC""") List findPastOpenPeriodsForRepayment(@Param("loanId") Long loanId, @Param("transactionDate") LocalDate transactionDate); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyActionWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyActionWriteServiceImpl.java index 505d6b10c63..5f7873cc16c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyActionWriteServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyActionWriteServiceImpl.java @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction; @@ -40,11 +41,15 @@ @RequiredArgsConstructor public class WorkingCapitalLoanDelinquencyActionWriteServiceImpl implements WorkingCapitalLoanDelinquencyActionWriteService { + private static final String RESET_RESOURCE_NAME_FOR_PERMISSIONS = "WC_DELINQUENCY_RESET"; + private static final String UNDO_RESET_RESOURCE_NAME_FOR_PERMISSIONS = "WC_DELINQUENCY_UNDO_RESET"; + private final WorkingCapitalLoanRepository loanRepository; private final WorkingCapitalLoanDelinquencyActionRepository actionRepository; private final WorkingCapitalLoanDelinquencyActionParseAndValidator validator; private final WorkingCapitalLoanDelinquencyRangeScheduleService rangeScheduleService; private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; + private final PlatformSecurityContext context; @Transactional @Override @@ -69,6 +74,15 @@ public CommandProcessingResult createDelinquencyAction(final Long workingCapital final WorkingCapitalLoanDelinquencyAction activePause = validator.findActivePauseForResume(existing, DateUtils.getBusinessLocalDate()); rangeScheduleService.resumeActivePause(workingCapitalLoan, activePause, action); + } else if (DelinquencyAction.RESET.equals(action.getAction())) { + context.authenticatedUser().validateHasCreatePermission(RESET_RESOURCE_NAME_FOR_PERMISSIONS); + rangeScheduleService.resetPeriods(workingCapitalLoan, action); + } else if (DelinquencyAction.UNDO_RESET.equals(action.getAction())) { + context.authenticatedUser().validateHasCreatePermission(UNDO_RESET_RESOURCE_NAME_FOR_PERMISSIONS); + List byWorkingCapitalLoanIdOrderById = actionRepository + .findByWorkingCapitalLoanIdOrderById(workingCapitalLoanId); + rangeScheduleService.undoResetPeriods(workingCapitalLoan, action, byWorkingCapitalLoanIdOrderById); + rangeScheduleService.reprocessDelinquencySchedule(workingCapitalLoan); } return new CommandProcessingResultBuilder() // diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java index b5e4296c363..1ab6424f887 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java @@ -20,7 +20,9 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; import java.time.LocalDate; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; public interface WorkingCapitalLoanDelinquencyClassificationService { @@ -28,4 +30,6 @@ public interface WorkingCapitalLoanDelinquencyClassificationService { void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate); + void applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range, + DelinquencyRange currentRange, LocalDate businessDate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java index 120db0d0a79..9a088ac6cb4 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java @@ -85,7 +85,7 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate) .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); for (WorkingCapitalLoanDelinquencyRangeSchedule range : delinquencyRangeScheduleList) { - if (range.getToDate().isBefore(businessDate)) { + if (!Objects.equals(range.getReset(), true) && range.getToDate().isBefore(businessDate)) { long rangeDelinquentDays = range.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0 ? DateUtils.getDifferenceInDays(range.getToDate(), businessDate) : 0L; @@ -137,6 +137,7 @@ public Optional findDelinquencyRangeByDays(final DelinquencyBu * @param businessDate * the date on which the tagging operation is performed */ + @Override public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, final DelinquencyRange currentRange, final LocalDate businessDate) { List updatedList = new ArrayList<>(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java index f99d19e901e..8f2be03566c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java @@ -52,4 +52,8 @@ void resumeActivePause(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAct */ void reprocessDelinquencySchedule(WorkingCapitalLoan loan); + void resetPeriods(WorkingCapitalLoan workingCapitalLoan, WorkingCapitalLoanDelinquencyAction action); + + void undoResetPeriods(WorkingCapitalLoan workingCapitalLoan, WorkingCapitalLoanDelinquencyAction action, + List byWorkingCapitalLoanIdOrderById); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java index 0a27c97196f..0b87836977f 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java @@ -176,7 +176,7 @@ private List resetAllPeriodsForRepro final List periods = loanDelinquencyRangeScheduleRepository .findByLoanIdOrderByPeriodNumberAsc(loanId); for (final WorkingCapitalLoanDelinquencyRangeSchedule period : periods) { - period.setPaidAmount(BigDecimal.ZERO); + period.setPaidAmount(period.getReset() ? null : BigDecimal.ZERO); period.setOutstandingAmount(period.getExpectedAmount()); period.setMinPaymentCriteriaMet(null); period.setDelinquentAmount(null); @@ -195,8 +195,8 @@ private void allocateRepayment(final WorkingCapitalLoan loan, final LocalDate tr for (WorkingCapitalLoanDelinquencyRangeSchedule period : pastOpenPeriods) { BigDecimal payAmount = MathUtil.min(transactionAmount, period.getOutstandingAmount(), true); transactionAmount = transactionAmount.subtract(payAmount); - period.setPaidAmount(period.getPaidAmount().add(payAmount)); - period.setOutstandingAmount(period.getOutstandingAmount().subtract(payAmount)); + period.setPaidAmount(MathUtil.nullToZero(period.getPaidAmount()).add(payAmount)); + period.setOutstandingAmount(MathUtil.nullToZero(period.getOutstandingAmount()).subtract(payAmount)); if (period.getOutstandingAmount().compareTo(BigDecimal.ZERO) <= 0) { period.setMinPaymentCriteriaMet(true); period.setDelinquentAmount(BigDecimal.ZERO); @@ -213,8 +213,8 @@ private void allocateRepayment(final WorkingCapitalLoan loan, final LocalDate tr WorkingCapitalLoanDelinquencyRangeSchedule period = currentPeriod.get(); BigDecimal newPaidAmount = period.getPaidAmount().add(transactionAmount); period.setPaidAmount(newPaidAmount); - period.setOutstandingAmount(period.getExpectedAmount().subtract(newPaidAmount).max(BigDecimal.ZERO)); - if (newPaidAmount.compareTo(period.getExpectedAmount()) >= 0) { + period.setOutstandingAmount(MathUtil.nullToZero(period.getExpectedAmount()).subtract(newPaidAmount).max(BigDecimal.ZERO)); + if (newPaidAmount.compareTo(MathUtil.nullToZero(period.getExpectedAmount())) >= 0) { period.setMinPaymentCriteriaMet(true); period.setDelinquentAmount(BigDecimal.ZERO); period.setDelinquentDays(0L); @@ -230,6 +230,9 @@ public void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDa List unevaluatedPeriods = loanDelinquencyRangeScheduleRepository .findByLoanIdAndToDateLessThanEqualAndMinPaymentCriteriaMetIsNull(loan.getId(), businessDate); for (WorkingCapitalLoanDelinquencyRangeSchedule period : unevaluatedPeriods) { + if (period.getReset()) { + continue; + } boolean criteriaMet = period.getPaidAmount().compareTo(period.getExpectedAmount()) >= 0; period.setMinPaymentCriteriaMet(criteriaMet); loanDelinquencyRangeScheduleRepository.saveAndFlush(period); @@ -307,6 +310,39 @@ public void resumeActivePause(final WorkingCapitalLoan loan, final WorkingCapita recalculateDelinquencyAfterPauseResume(loan, businessDate); } + @Override + public void resetPeriods(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAction action) { + LocalDate resetDate = action.getStartDate(); + final List periods = loanDelinquencyRangeScheduleRepository + .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + periods.stream().filter(p -> !Objects.equals(p.getReset(), true) && p.getToDate().isBefore(action.getStartDate())) + .forEach(p1 -> resetPeriod(p1, resetDate)); + } + + private void resetPeriod(WorkingCapitalLoanDelinquencyRangeSchedule period, LocalDate resetDate) { + period.reset(); + delinquencyClassificationService.applyDelinquencyTagForRange(period.getLoan(), period, null, resetDate); + } + + @Override + public void undoResetPeriods(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAction action, + List byWorkingCapitalLoanIdOrderById) { + + List activeResets = byWorkingCapitalLoanIdOrderById.stream() + .filter(a -> DelinquencyAction.RESET.equals(a.getAction()) && a.getEndDate() == null).toList(); + if (!activeResets.isEmpty()) { + activeResets.getLast().setEndDate(action.getStartDate()); + } + LocalDate lastActiveResetStartDate = activeResets.size() >= 2 ? activeResets.get(activeResets.size() - 2).getStartDate() : null; + final List periods = loanDelinquencyRangeScheduleRepository + .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + periods.stream() + .filter(p -> Objects.equals(p.getReset(), true) && p.getToDate().isBefore(action.getStartDate()) + && (lastActiveResetStartDate == null || !p.getToDate().isBefore(lastActiveResetStartDate))) + .forEach(p1 -> p1.setReset(false)); + reprocessDelinquencySchedule(loan); + } + private void shrinkPeriodsForPause(final WorkingCapitalLoan loan, final LocalDate pauseStart, final LocalDate originalPauseEnd, final LocalDate newPauseEnd) { final long daysToRemove = WorkingCapitalLoanDelinquencyPauseUtils.calculateDaysRemovedOnResume(pauseStart, newPauseEnd, diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanDelinquencyActionParseAndValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanDelinquencyActionParseAndValidator.java index ac4b9375ba6..369f1d8afa6 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanDelinquencyActionParseAndValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanDelinquencyActionParseAndValidator.java @@ -76,6 +76,10 @@ public WorkingCapitalLoanDelinquencyAction validateAndParse(final JsonCommand co validateReschedule(parsedAction, workingCapitalLoan, dataValidator); } else if (DelinquencyAction.RESUME.equals(parsedAction.getAction())) { validateResume(parsedAction, existing, dataValidator); + } else if (DelinquencyAction.RESET.equals(parsedAction.getAction())) { + validateReset(parsedAction, dataValidator); + } else if (DelinquencyAction.UNDO_RESET.equals(parsedAction.getAction())) { + validateUndoReset(parsedAction, dataValidator); } throwExceptionIfValidationWarningsExist(dataValidator); @@ -120,6 +124,22 @@ private void validateResume(final WorkingCapitalLoanDelinquencyAction action, fi } } + private void validateReset(WorkingCapitalLoanDelinquencyAction action, DataValidatorBuilder dataValidator) { + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).ignoreIfNull().isOneOfTheseValues(businessDate); + if (action.getEndDate() == null) { + action.setStartDate(businessDate); + } + } + + private void validateUndoReset(WorkingCapitalLoanDelinquencyAction action, DataValidatorBuilder dataValidator) { + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).ignoreIfNull().isOneOfTheseValues(businessDate); + if (action.getEndDate() == null) { + action.setStartDate(businessDate); + } + } + private void validateResumeStartDate(final WorkingCapitalLoanDelinquencyAction action, final LocalDate businessDate, final DataValidatorBuilder dataValidator) { dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).notNull(); @@ -179,6 +199,10 @@ private WorkingCapitalLoanDelinquencyAction parseCommand(final JsonCommand comma action.setFrequencyType(extractFrequencyType(json, dataValidator)); } else if (DelinquencyAction.RESUME.equals(action.getAction())) { action.setStartDate(extractDate(json, START_DATE)); + } else if (DelinquencyAction.RESET.equals(action.getAction())) { + action.setStartDate(extractDate(json, START_DATE)); + } else if (DelinquencyAction.UNDO_RESET.equals(action.getAction())) { + action.setStartDate(extractDate(json, START_DATE)); } return action; @@ -196,6 +220,10 @@ private DelinquencyAction extractAction(final JsonElement json, final DataValida return DelinquencyAction.RESCHEDULE; } else if ("resume".equalsIgnoreCase(actionString)) { return DelinquencyAction.RESUME; + } else if ("reset".equalsIgnoreCase(actionString)) { + return DelinquencyAction.RESET; + } else if ("undo_reset".equalsIgnoreCase(actionString)) { + return DelinquencyAction.UNDO_RESET; } failParameterValidation(dataValidator, ACTION, "invalid.action", "Invalid Delinquency Action: " + actionString + ". Supported actions: pause, reschedule, resume"); diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 553e9d265a0..0c7fe7d2a23 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -72,4 +72,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0051_delinquency_reset.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0051_delinquency_reset.xml new file mode 100644 index 00000000000..005731fbb34 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0051_delinquency_reset.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + SELECT COUNT(*) + FROM m_permission + WHERE code = 'CREATE_WC_DELINQUENCY_RESET' + + + + + + + + + + + + + + + + +